Skip to content

Commit

Permalink
custom editor: open a diff-uri in a side-by-side editor
Browse files Browse the repository at this point in the history
`CustomEditorOpener` is now able to open a diff-uri in a side-by-side editor,
which contains the corresponding `CustomEditor`s.

Fixes #9079
  • Loading branch information
pisv committed Aug 7, 2024
1 parent 5392e2d commit 90e6932
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 20 deletions.
66 changes: 65 additions & 1 deletion packages/core/src/browser/saveable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { MaybePromise } from '../common/types';
import { Key } from './keyboard/keys';
import { AbstractDialog } from './dialogs';
import { nls } from '../common/nls';
import { DisposableCollection, isObject } from '../common';
import { Disposable, DisposableCollection, isObject } from '../common';
import { BinaryBuffer } from '../common/buffer';

export type AutoSaveMode = 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange';
Expand Down Expand Up @@ -112,6 +112,70 @@ export class DelegatingSaveable implements Saveable {

}

export class CompositeSaveable implements Saveable {
protected isDirty = false;
protected readonly onDirtyChangedEmitter = new Emitter<void>();
protected readonly onContentChangedEmitter = new Emitter<void>();
protected readonly toDispose = new DisposableCollection(this.onDirtyChangedEmitter, this.onContentChangedEmitter);
protected readonly saveablesMap = new Map<Saveable, Disposable>();

get dirty(): boolean {
return this.isDirty;
}

get onDirtyChanged(): Event<void> {
return this.onDirtyChangedEmitter.event;
}

get onContentChanged(): Event<void> {
return this.onContentChangedEmitter.event;
}

async save(options?: SaveOptions): Promise<void> {
await Promise.all(this.saveables.map(saveable => saveable.save(options)));
}

get saveables(): readonly Saveable[] {
return Array.from(this.saveablesMap.keys());
}

add(saveable: Saveable): void {
if (this.saveablesMap.has(saveable)) {
return;
}
const toDispose = new DisposableCollection();
this.toDispose.push(toDispose);
this.saveablesMap.set(saveable, toDispose);
toDispose.push(Disposable.create(() => {
this.saveablesMap.delete(saveable);
}));
toDispose.push(saveable.onDirtyChanged(() => {
const wasDirty = this.isDirty;
this.isDirty = this.saveables.some(s => s.dirty);
if (this.isDirty !== wasDirty) {
this.onDirtyChangedEmitter.fire();
}
}));
toDispose.push(saveable.onContentChanged(() => {
this.onContentChangedEmitter.fire();
}));
if (saveable.dirty && !this.isDirty) {
this.isDirty = true;
this.onDirtyChangedEmitter.fire();
}
}

remove(saveable: Saveable): boolean {
const toDispose = this.saveablesMap.get(saveable);
toDispose?.dispose();
return !!toDispose;
}

dispose(): void {
this.toDispose.dispose();
}
}

export namespace Saveable {
export interface RevertOptions {
/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,4 @@ button.secondary[disabled],
@import "./progress-bar.css";
@import "./breadcrumbs.css";
@import "./tooltip.css";
@import "./split-widget.css";
38 changes: 38 additions & 0 deletions packages/core/src/browser/style/split-widget.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/********************************************************************************
* Copyright (C) 2024 1C-Soft LLC and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/

.theia-split-widget > .p-SplitPanel {
height: 100%;
width: 100%;
outline: none;
}

.theia-split-widget > .p-SplitPanel > .p-SplitPanel-child {
min-width: 50px;
min-height: var(--theia-content-line-height);
}

.theia-split-widget > .p-SplitPanel > .p-SplitPanel-handle {
box-sizing: border-box;
}

.theia-split-widget > .p-SplitPanel[data-orientation="horizontal"] > .p-SplitPanel-handle {
border-left: var(--theia-border-width) solid var(--theia-sideBarSectionHeader-border);
}

.theia-split-widget > .p-SplitPanel[data-orientation="vertical"] > .p-SplitPanel-handle {
border-top: var(--theia-border-width) solid var(--theia-sideBarSectionHeader-border);
}
1 change: 1 addition & 0 deletions packages/core/src/browser/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './widget';
export * from './react-renderer';
export * from './react-widget';
export * from './extractable-widget';
export * from './split-widget';
163 changes: 163 additions & 0 deletions packages/core/src/browser/widgets/split-widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// *****************************************************************************
// Copyright (C) 2024 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { Emitter } from 'vscode-languageserver-protocol';
import { ApplicationShell, StatefulWidget } from '../shell';
import { BaseWidget, Message, PanelLayout, SplitPanel, Widget } from './widget';
import { CompositeSaveable, Saveable, SaveableSource } from '../saveable';
import { Navigatable } from '../navigatable-types';
import { URI } from '../../common';

/**
* A widget containing a number of panes in a split layout.
*/
export class SplitWidget extends BaseWidget implements ApplicationShell.TrackableWidgetProvider, SaveableSource, Navigatable, StatefulWidget {

protected readonly splitPanel: SplitPanel;

protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter<Widget[]>();
readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event;

protected readonly compositeSaveable = new CompositeSaveable();

protected navigatable?: Navigatable;

constructor(options?: SplitPanel.IOptions & { navigatable?: Navigatable }) {
super();

this.toDispose.pushAll([this.onDidChangeTrackableWidgetsEmitter]);

this.addClass('theia-split-widget');

const layout = new PanelLayout();
this.layout = layout;
const that = this;
this.splitPanel = new class extends SplitPanel {

protected override onChildAdded(msg: Widget.ChildMessage): void {
super.onChildAdded(msg);
that.onPaneAdded(msg.child);
}

protected override onChildRemoved(msg: Widget.ChildMessage): void {
super.onChildRemoved(msg);
that.onPaneRemoved(msg.child);
}
}({
spacing: 1, // --theia-border-width
...options
});
this.splitPanel.node.tabIndex = -1;
layout.addWidget(this.splitPanel);

this.navigatable = options?.navigatable;
}

get orientation(): SplitPanel.Orientation {
return this.splitPanel.orientation;
}

set orientation(value: SplitPanel.Orientation) {
this.splitPanel.orientation = value;
}

relativeSizes(): number[] {
return this.splitPanel.relativeSizes();
}

setRelativeSizes(sizes: number[]): void {
this.splitPanel.setRelativeSizes(sizes);
}

get handles(): readonly HTMLDivElement[] {
return this.splitPanel.handles;
}

get saveable(): Saveable {
return this.compositeSaveable;
}

getResourceUri(): URI | undefined {
return this.navigatable?.getResourceUri();
}

createMoveToUri(resourceUri: URI): URI | undefined {
return this.navigatable?.createMoveToUri(resourceUri);
}

storeState(): SplitWidget.State {
return { orientation: this.orientation, widgets: this.panes, relativeSizes: this.relativeSizes() };
}

restoreState(oldState: SplitWidget.State): void {
const { orientation, widgets, relativeSizes } = oldState;
if (orientation) {
this.orientation = orientation;
}
for (const widget of widgets) {
this.addPane(widget);
}
if (relativeSizes) {
this.setRelativeSizes(relativeSizes);
}
}

get panes(): readonly Widget[] {
return this.splitPanel.widgets;
}

getTrackableWidgets(): Widget[] {
return [...this.panes];
}

protected fireDidChangeTrackableWidgets(): void {
this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
}

addPane(pane: Widget): void {
this.splitPanel.addWidget(pane);
}

insertPane(index: number, pane: Widget): void {
this.splitPanel.insertWidget(index, pane);
}

protected onPaneAdded(pane: Widget): void {
if (Saveable.isSource(pane)) {
this.compositeSaveable.add(pane.saveable);
}
this.fireDidChangeTrackableWidgets();
}

protected onPaneRemoved(pane: Widget): void {
if (Saveable.isSource(pane)) {
this.compositeSaveable.remove(pane.saveable);
}
this.fireDidChangeTrackableWidgets();
}

protected override onActivateRequest(msg: Message): void {
this.splitPanel.node.focus();
}
}

export namespace SplitWidget {
export interface State {
orientation?: SplitPanel.Orientation;
widgets: readonly Widget[]; // note: don't rename this property; it has special meaning for `ShellLayoutRestorer`
relativeSizes?: number[];
}
}
Loading

0 comments on commit 90e6932

Please sign in to comment.