Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple editors for same file #9369

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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