Skip to content

Commit

Permalink
Allow multiple editors for same file
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <[email protected]>
  • Loading branch information
colin-grant-work committed Apr 21, 2021
1 parent a44dfac commit 088533e
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/browser/navigatable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export namespace NavigatableWidget {

export interface NavigatableWidgetOptions {
kind: 'navigatable',
uri: string
uri: string,
counter?: number,
}
export namespace NavigatableWidgetOptions {
export function is(arg: Object | undefined): arg is NavigatableWidgetOptions {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/browser/widget-open-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ export abstract class WidgetOpenHandler<W extends BaseWidget> implements OpenHan

protected getWidget(uri: URI, options?: WidgetOpenerOptions): Promise<W | undefined> {
const widgetOptions = this.createWidgetOptions(uri, options);
return this.widgetManager.getWidget(this.id, widgetOptions) as Promise<W | undefined>;
return this.widgetManager.getWidget<W>(this.id, widgetOptions);
}

protected getOrCreateWidget(uri: URI, options?: WidgetOpenerOptions): Promise<W> {
const widgetOptions = this.createWidgetOptions(uri, options);
return this.widgetManager.getOrCreateWidget(this.id, widgetOptions) as Promise<W>;
return this.widgetManager.getOrCreateWidget<W>(this.id, widgetOptions);
}

protected abstract createWidgetOptions(uri: URI, options?: WidgetOpenerOptions): Object;
Expand Down
50 changes: 45 additions & 5 deletions packages/editor/src/browser/editor-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings
export namespace EditorCommands {

const EDITOR_CATEGORY = 'Editor';
const VIEW_CATEGORY = 'View';

/**
* Show editor references
Expand Down Expand Up @@ -106,41 +107,80 @@ export namespace EditorCommands {
*/
export const SHOW_ALL_OPENED_EDITORS: Command = {
id: 'workbench.action.showAllEditors',
category: 'View',
category: VIEW_CATEGORY,
label: 'Show All Opened Editors'
};
/**
* Command that toggles the minimap.
*/
export const TOGGLE_MINIMAP: Command = {
id: 'editor.action.toggleMinimap',
category: 'View',
category: VIEW_CATEGORY,
label: 'Toggle Minimap'
};
/**
* Command that toggles the rendering of whitespace characters in the editor.
*/
export const TOGGLE_RENDER_WHITESPACE: Command = {
id: 'editor.action.toggleRenderWhitespace',
category: 'View',
category: VIEW_CATEGORY,
label: 'Toggle Render Whitespace'
};
/**
* Command that toggles the word wrap.
*/
export const TOGGLE_WORD_WRAP: Command = {
id: 'editor.action.toggleWordWrap',
category: 'View',
category: VIEW_CATEGORY,
label: 'Toggle Word Wrap'
};
/**
* Command that re-opens the last closed editor.
*/
export const REOPEN_CLOSED_EDITOR: Command = {
id: 'workbench.action.reopenClosedEditor',
category: 'View',
category: VIEW_CATEGORY,
label: 'Reopen Closed Editor'
};
/**
* Opens a second instance of the current editor, splitting the view in the direction specified.
*/
export const SPLIT_EDITOR_RIGHT: Command = {
id: 'workbench.action.splitEditorRight',
category: VIEW_CATEGORY,
label: 'Split Editor Right'
};
export const SPLIT_EDITOR_DOWN: Command = {
id: 'workbench.action.splitEditorDown',
category: VIEW_CATEGORY,
label: 'Split Editor Down'
};
export const SPLIT_EDITOR_UP: Command = {
id: 'workbench.action.splitEditorUp',
category: VIEW_CATEGORY,
label: 'Split Editor Up'
};
export const SPLIT_EDITOR_LEFT: Command = {
id: 'workbench.action.splitEditorLeft',
category: VIEW_CATEGORY,
label: 'Split Editor Left'
};
/**
* Default horizontal split: right.
*/
export const SPLIT_EDITOR_HORIZONTAL: Command = {
id: 'workbench.action.splitEditor',
category: VIEW_CATEGORY,
label: 'Split Editor'
};
/**
* Default vertical split: down.
*/
export const SPLIT_EDITOR_VERTICAL: Command = {
id: 'workbench.action.splitEditorOrthogonal',
category: VIEW_CATEGORY,
label: 'Split Editor Orthogonal'
};
}

@injectable()
Expand Down
31 changes: 29 additions & 2 deletions packages/editor/src/browser/editor-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import { EditorManager } from './editor-manager';
import { TextEditor } from './editor';
import { injectable, inject } from '@theia/core/shared/inversify';
import { StatusBarAlignment, StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
import { FrontendApplicationContribution, DiffUris } from '@theia/core/lib/browser';
import { FrontendApplicationContribution, DiffUris, DockLayout } from '@theia/core/lib/browser';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { DisposableCollection } from '@theia/core';
import { CommandHandler, DisposableCollection } from '@theia/core';
import { EditorCommands } from './editor-command';
import { EditorQuickOpenService } from './editor-quick-open-service';
import { CommandRegistry, CommandContribution } from '@theia/core/lib/common';
Expand Down Expand Up @@ -133,13 +133,40 @@ export class EditorContribution implements FrontendApplicationContribution, Comm
commands.registerCommand(EditorCommands.SHOW_ALL_OPENED_EDITORS, {
execute: () => this.editorQuickOpenService.open()
});
const splitHandlerFactory = (splitMode: DockLayout.InsertMode): CommandHandler => ({
isEnabled: () => !!this.editorManager.currentEditor,
isVisible: () => !!this.editorManager.currentEditor,
execute: async () => {
const { currentEditor } = this.editorManager;
if (currentEditor) {
const selection = currentEditor.editor.selection;
const newEditor = await this.editorManager.openToSide(currentEditor.editor.uri, { selection, widgetOptions: { mode: splitMode } });
const oldEditorState = currentEditor.editor.storeViewState();
newEditor.editor.restoreViewState(oldEditorState);
}
}
});
commands.registerCommand(EditorCommands.SPLIT_EDITOR_HORIZONTAL, splitHandlerFactory('split-right'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_VERTICAL, splitHandlerFactory('split-bottom'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_RIGHT, splitHandlerFactory('split-right'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_DOWN, splitHandlerFactory('split-bottom'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_UP, splitHandlerFactory('split-top'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_LEFT, splitHandlerFactory('split-left'));
}

registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: EditorCommands.SHOW_ALL_OPENED_EDITORS.id,
keybinding: 'ctrlcmd+k ctrlcmd+p'
});
keybindings.registerKeybinding({
command: EditorCommands.SPLIT_EDITOR_HORIZONTAL.id,
keybinding: 'ctrlcmd+\\',
});
keybindings.registerKeybinding({
command: EditorCommands.SPLIT_EDITOR_VERTICAL.id,
keybinding: 'ctrlcmd+k ctrlcmd+\\',
});
}

registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void {
Expand Down
123 changes: 104 additions & 19 deletions packages/editor/src/browser/editor-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@
import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { RecursivePartial, Emitter, Event } from '@theia/core/lib/common';
import { WidgetOpenerOptions, NavigatableWidgetOpenHandler } from '@theia/core/lib/browser';
import { WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions } from '@theia/core/lib/browser';
import { EditorWidget } from './editor-widget';
import { Range, Position, Location } from './editor';
import { EditorWidgetFactory } from './editor-widget-factory';
import { TextEditor } from './editor';

export interface WidgetId {
id: number;
uri: string;
}

export interface EditorOpenerOptions extends WidgetOpenerOptions {
selection?: RecursivePartial<Range>;
preview?: boolean;
counter?: number
}

@injectable()
Expand All @@ -35,6 +41,8 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {

readonly label = 'Code Editor';

protected readonly editorCounters = new Map<string, number>();

protected readonly onActiveEditorChangedEmitter = new Emitter<EditorWidget | undefined>();
/**
* Emit when the active editor is changed.
Expand All @@ -50,8 +58,8 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
@postConstruct()
protected init(): void {
super.init();
this.shell.activeChanged.connect(() => this.updateActiveEditor());
this.shell.currentChanged.connect(() => this.updateCurrentEditor());
this.shell.onDidChangeActiveWidget(() => this.updateActiveEditor());
this.shell.onDidChangeCurrentWidget(() => this.updateCurrentEditor());
this.onCreated(widget => {
widget.onDidChangeVisibility(() => {
if (widget.isVisible) {
Expand All @@ -61,7 +69,9 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
}
this.updateCurrentEditor();
});
this.checkCounterForWidget(widget);
widget.disposed.connect(() => {
this.removeFromCounter(widget);
this.removeRecentlyVisible(widget);
this.updateCurrentEditor();
});
Expand All @@ -75,21 +85,30 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
}

async getByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget | undefined> {
const widget = await super.getByUri(uri);
if (widget) {
// Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955)
this.revealSelection(widget, options, uri);
}
return widget;
return this.getWidget(uri, options);
}

async getOrCreateByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const widget = await super.getOrCreateByUri(uri);
if (widget) {
getOrCreateByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
return this.getOrCreateWidget(uri, options);
}

protected async getWidget(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget | undefined> {
const optionsWithCounter: EditorOpenerOptions = { counter: this.getCounterForUri(uri), ...options };
const editor = await super.getWidget(uri, optionsWithCounter);
if (editor) {
// Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955)
this.revealSelection(widget, options, uri);
this.revealSelection(editor, optionsWithCounter, uri);
}
return widget;
return editor;
}

protected async getOrCreateWidget(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const counter = options?.counter === undefined ? this.getOrCreateCounterForUri(uri) : options.counter;
const optionsWithCounter: EditorOpenerOptions = { ...options, counter };
const editor = await super.getOrCreateWidget(uri, optionsWithCounter);
// Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955)
this.revealSelection(editor, options, uri);
return editor;
}

protected readonly recentlyVisibleIds: string[] = [];
Expand Down Expand Up @@ -154,14 +173,23 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
return 100;
}

async open(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const editor = await this.getOrCreateByUri(uri, options);
await super.open(uri, options);
return editor;
// This override only serves to inform external callers that they can use EditorOpenerOptions.
open(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
return super.open(uri, options);
}

/**
* Opens an editor to the side of the current editor. Defaults to opening to the right.
* To modify direction, pass options with `{widgetOptions: {mode: ...}}`
*/
openToSide(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const counter = this.createCounterForUri(uri);
const splitOptions: EditorOpenerOptions = { widgetOptions: { mode: 'split-right' }, ...options, counter };
return this.open(uri, splitOptions);
}

protected revealSelection(widget: EditorWidget, input?: EditorOpenerOptions, uri?: URI): void {
let inputSelection = input && input.selection;
let inputSelection = input?.selection;
if (!inputSelection && uri) {
const match = /^L?(\d+)(?:,(\d+))?/.exec(uri.fragment);
if (match) {
Expand Down Expand Up @@ -207,6 +235,63 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
};
}

protected removeFromCounter(widget: EditorWidget): void {
const { id, uri } = this.extractIdFromWidget(widget);
if (uri && !Number.isNaN(id)) {
let max = -Infinity;
this.all.forEach(editor => {
const candidateID = this.extractIdFromWidget(editor);
if ((candidateID.uri === uri) && (candidateID.id > max)) {
max = candidateID.id!;
}
});

if (max > -Infinity) {
this.editorCounters.set(uri, max);
} else {
this.editorCounters.delete(uri);
}
}
}

protected extractIdFromWidget(widget: EditorWidget): WidgetId {
const uri = widget.editor.uri.toString();
const id = Number(widget.id.slice(widget.id.lastIndexOf(':') + 1));
return { id, uri };
}

protected checkCounterForWidget(widget: EditorWidget): void {
const { id, uri } = this.extractIdFromWidget(widget);
const numericalId = Number(id);
if (uri && !Number.isNaN(numericalId)) {
const highestKnownId = this.editorCounters.get(uri) ?? -Infinity;
if (numericalId > highestKnownId) {
this.editorCounters.set(uri, numericalId);
}
}
}

protected createCounterForUri(uri: URI): number {
const identifier = uri.toString();
const next = (this.editorCounters.get(identifier) ?? 0) + 1;
return next;
}

protected getCounterForUri(uri: URI): number | undefined {
return this.editorCounters.get(uri.toString());
}

protected getOrCreateCounterForUri(uri: URI): number {
return this.getCounterForUri(uri) ?? this.createCounterForUri(uri);
}

protected createWidgetOptions(uri: URI, options?: EditorOpenerOptions): NavigatableWidgetOptions {
const navigatableOptions = super.createWidgetOptions(uri, options);
if (options?.counter !== undefined) {
navigatableOptions.counter = options.counter;
}
return navigatableOptions;
}
}

/**
Expand Down
7 changes: 5 additions & 2 deletions packages/editor/src/browser/editor-widget-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export class EditorWidgetFactory implements WidgetFactory {

createWidget(options: NavigatableWidgetOptions): Promise<EditorWidget> {
const uri = new URI(options.uri);
return this.createEditor(uri);
return this.createEditor(uri, options);
}

protected async createEditor(uri: URI): Promise<EditorWidget> {
protected async createEditor(uri: URI, options?: NavigatableWidgetOptions): Promise<EditorWidget> {
const textEditor = await this.editorProvider(uri);
const newEditor = new EditorWidget(textEditor, this.selectionService);

Expand All @@ -55,6 +55,9 @@ export class EditorWidgetFactory implements WidgetFactory {
newEditor.onDispose(() => labelListener.dispose());

newEditor.id = this.id + ':' + uri.toString();
if (options?.counter !== undefined) {
newEditor.id += `:${options.counter}`;
}
newEditor.title.closable = true;
return newEditor;
}
Expand Down

0 comments on commit 088533e

Please sign in to comment.