// ***************************************************************************** // Copyright (C) 2018 Red Hat, Inc. 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 { interfaces } from '@theia/core/shared/inversify'; import * as monaco from '@theia/monaco-editor-core'; import { type ILineChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer'; import { RPCProtocol } from '../../common/rpc-protocol'; import { MAIN_RPC_CONTEXT, EditorsAndDocumentsExt, EditorsAndDocumentsDelta, ModelAddedData, TextEditorAddData, EditorPosition } from '../../common/plugin-api-rpc'; import { Disposable } from '@theia/core/lib/common/disposable'; import { EditorModelService } from './text-editor-model-service'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { TextEditorMain } from './text-editor-main'; import { DisposableCollection, Emitter, URI } from '@theia/core'; import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; import { SaveableService } from '@theia/core/lib/browser/saveable-service'; import { TabsMainImpl } from './tabs/tabs-main'; import { NotebookCellEditorService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser'; import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; import { EncodingRegistry } from '@theia/core/lib/browser/encoding-registry'; export class EditorsAndDocumentsMain implements Disposable { private readonly proxy: EditorsAndDocumentsExt; private readonly stateComputer: EditorAndDocumentStateComputer; private readonly textEditors = new Map(); private readonly modelService: EditorModelService; private readonly editorManager: EditorManager; private readonly saveResourceService: SaveableService; private readonly encodingRegistry: EncodingRegistry; private readonly onTextEditorAddEmitter = new Emitter(); private readonly onTextEditorRemoveEmitter = new Emitter(); private readonly onDocumentAddEmitter = new Emitter(); private readonly onDocumentRemoveEmitter = new Emitter(); readonly onTextEditorAdd = this.onTextEditorAddEmitter.event; readonly onTextEditorRemove = this.onTextEditorRemoveEmitter.event; readonly onDocumentAdd = this.onDocumentAddEmitter.event; readonly onDocumentRemove = this.onDocumentRemoveEmitter.event; private readonly toDispose = new DisposableCollection( Disposable.create(() => this.textEditors.clear()) ); constructor(rpc: RPCProtocol, container: interfaces.Container, tabsMain: TabsMainImpl) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT); this.editorManager = container.get(EditorManager); this.modelService = container.get(EditorModelService); this.saveResourceService = container.get(SaveableService); this.encodingRegistry = container.get(EncodingRegistry); this.stateComputer = new EditorAndDocumentStateComputer(d => this.onDelta(d), this.editorManager, container.get(NotebookCellEditorService), container.get(NotebookEditorWidgetService), this.modelService, tabsMain); this.toDispose.push(this.stateComputer); this.toDispose.push(this.onTextEditorAddEmitter); this.toDispose.push(this.onTextEditorRemoveEmitter); this.toDispose.push(this.onDocumentAddEmitter); this.toDispose.push(this.onDocumentRemoveEmitter); } listen(): void { this.stateComputer.listen(); } dispose(): void { this.toDispose.dispose(); } private onDelta(delta: EditorAndDocumentStateDelta): void { const removedEditors = new Array(); const addedEditors = new Array(); const removedDocuments = delta.removedDocuments.map(d => d.textEditorModel.uri); for (const editor of delta.addedEditors) { const textEditorMain = new TextEditorMain(editor.id, editor.editor.getControl().getModel()!, editor.editor); this.textEditors.set(editor.id, textEditorMain); this.toDispose.push(textEditorMain); addedEditors.push(textEditorMain); } for (const { id } of delta.removedEditors) { const textEditorMain = this.textEditors.get(id); if (textEditorMain) { textEditorMain.dispose(); this.textEditors.delete(id); removedEditors.push(id); } } const deltaExt: EditorsAndDocumentsDelta = {}; let empty = true; if (delta.newActiveEditor !== undefined) { empty = false; deltaExt.newActiveEditor = delta.newActiveEditor; } if (removedDocuments.length > 0) { empty = false; deltaExt.removedDocuments = removedDocuments; } if (removedEditors.length > 0) { empty = false; deltaExt.removedEditors = removedEditors; } if (delta.addedDocuments.length > 0) { empty = false; deltaExt.addedDocuments = delta.addedDocuments.map(d => this.toModelAddData(d)); } if (delta.addedEditors.length > 0) { empty = false; deltaExt.addedEditors = addedEditors.map(e => this.toTextEditorAddData(e)); } if (!empty) { this.proxy.$acceptEditorsAndDocumentsDelta(deltaExt); this.onDocumentRemoveEmitter.fire(removedDocuments); this.onDocumentAddEmitter.fire(delta.addedDocuments); this.onTextEditorRemoveEmitter.fire(removedEditors); this.onTextEditorAddEmitter.fire(addedEditors); } } private toModelAddData(model: MonacoEditorModel): ModelAddedData { return { uri: model.textEditorModel.uri, versionId: model.textEditorModel.getVersionId(), lines: model.textEditorModel.getLinesContent(), languageId: model.getLanguageId(), EOL: model.textEditorModel.getEOL(), modeId: model.languageId, isDirty: model.dirty, encoding: this.encodingRegistry.getEncodingForResource(URI.fromComponents(model.textEditorModel.uri), model.getEncoding()) }; } private toTextEditorAddData(textEditor: TextEditorMain): TextEditorAddData { const properties = textEditor.getProperties(); return { id: textEditor.getId(), documentUri: textEditor.getModel().uri, options: properties!.options, selections: properties!.selections, visibleRanges: properties!.visibleRanges, editorPosition: this.findEditorPosition(textEditor) }; } private findEditorPosition(editor: TextEditorMain): EditorPosition | undefined { return EditorPosition.ONE; // TODO: fix this when Theia has support splitting editors } getEditor(id: string): TextEditorMain | undefined { return this.textEditors.get(id); } async save(uri: URI): Promise { const editor = await this.editorManager.getByUri(uri); if (!editor) { return undefined; } return this.saveResourceService.save(editor); } async saveAs(uri: URI): Promise { const editor = await this.editorManager.getByUri(uri); if (!editor) { return undefined; } if (!this.saveResourceService.canSaveAs(editor)) { return undefined; } return this.saveResourceService.saveAs(editor); } saveAll(includeUntitled?: boolean): Promise { return this.modelService.saveAll(includeUntitled); } hideEditor(id: string): Promise { for (const editorWidget of this.editorManager.all) { const monacoEditor = MonacoEditor.get(editorWidget); if (monacoEditor) { if (id === new EditorSnapshot(monacoEditor).id) { editorWidget.close(); break; } } } return Promise.resolve(); } getDiffInformation(id: string): ILineChange[] { const editor = this.getEditor(id); return editor?.diffInformation || []; } } class EditorAndDocumentStateComputer implements Disposable { private currentState: EditorAndDocumentState | undefined; private readonly editors = new Map(); private readonly toDispose = new DisposableCollection( Disposable.create(() => this.currentState = undefined) ); constructor( private callback: (delta: EditorAndDocumentStateDelta) => void, private readonly editorService: EditorManager, private readonly cellEditorService: NotebookCellEditorService, private readonly notebookWidgetService: NotebookEditorWidgetService, private readonly modelService: EditorModelService, private readonly tabsMain: TabsMainImpl ) { } listen(): void { if (this.toDispose.disposed) { return; } this.toDispose.push(this.editorService.onCreated(async widget => { await this.tabsMain.waitForWidget(widget); this.onTextEditorAdd(widget); this.update(); })); this.toDispose.push(this.editorService.onCurrentEditorChanged(async widget => { if (widget) { await this.tabsMain.waitForWidget(widget); } this.update(); })); this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this)); this.toDispose.push(this.modelService.onModelRemoved(() => this.update())); this.toDispose.push(this.cellEditorService.onDidChangeCellEditors(() => this.update())); this.toDispose.push(this.notebookWidgetService.onDidChangeCurrentEditor(() => { this.currentState = this.currentState && new EditorAndDocumentState( this.currentState.documents, this.currentState.editors, undefined ); })); for (const widget of this.editorService.all) { this.onTextEditorAdd(widget); } this.update(); } dispose(): void { this.toDispose.dispose(); } private onModelAdded(model: MonacoEditorModel): void { if (!this.currentState) { this.update(); return; } this.currentState = new EditorAndDocumentState( this.currentState.documents.add(model), this.currentState.editors, this.currentState.activeEditor); this.callback(new EditorAndDocumentStateDelta( [], [model], [], [], undefined, undefined )); } private onTextEditorAdd(widget: EditorWidget): void { if (widget.isDisposed) { return; } const editor = MonacoEditor.get(widget); if (!editor) { return; } const id = editor.getControl().getId(); const toDispose = new DisposableCollection( editor.onDispose(() => this.onTextEditorRemove(editor)), Disposable.create(() => this.editors.delete(id)) ); this.editors.set(id, toDispose); this.toDispose.push(toDispose); } private onTextEditorRemove(e: MonacoEditor): void { const toDispose = this.editors.get(e.getControl().getId()); if (toDispose) { toDispose.dispose(); this.update(); } } private update(): void { const models = new Set(); for (const model of this.modelService.getModels()) { models.add(model); } let activeId: string | null = null; const activeEditor = MonacoEditor.getCurrent(this.editorService) ?? this.cellEditorService.getActiveCell(); const editors = new Map(); for (const widget of this.editorService.all) { const editor = MonacoEditor.get(widget); // VS Code tracks only visible widgets if (!editor || !widget.isVisible) { continue; } const model = editor.getControl().getModel(); if (model && !model.isDisposed()) { const editorSnapshot = new EditorSnapshot(editor); editors.set(editorSnapshot.id, editorSnapshot); if (activeEditor === editor) { activeId = editorSnapshot.id; } } } for (const editor of this.cellEditorService.allCellEditors) { if (editor.getControl()?.getModel()) { const editorSnapshot = new EditorSnapshot(editor); editors.set(editorSnapshot.id, editorSnapshot); if (activeEditor === editor) { activeId = editorSnapshot.id; } } }; const newState = new EditorAndDocumentState(models, editors, activeId); const delta = EditorAndDocumentState.compute(this.currentState, newState); if (!delta.isEmpty) { this.currentState = newState; this.callback(delta); } } } class EditorAndDocumentStateDelta { readonly isEmpty: boolean; constructor( readonly removedDocuments: MonacoEditorModel[], readonly addedDocuments: MonacoEditorModel[], readonly removedEditors: EditorSnapshot[], readonly addedEditors: EditorSnapshot[], readonly oldActiveEditor: string | null | undefined, readonly newActiveEditor: string | null | undefined ) { this.isEmpty = this.removedDocuments.length === 0 && this.addedDocuments.length === 0 && this.addedEditors.length === 0 && this.removedEditors.length === 0 && this.newActiveEditor === this.oldActiveEditor; } } class EditorAndDocumentState { constructor( readonly documents: Set, readonly editors: Map, readonly activeEditor: string | null | undefined) { } static compute(before: EditorAndDocumentState | undefined, after: EditorAndDocumentState): EditorAndDocumentStateDelta { if (!before) { return new EditorAndDocumentStateDelta( [], Array.from(after.documents), [], Array.from(after.editors.values()), undefined, after.activeEditor ); } const documentDelta = Delta.ofSets(before.documents, after.documents); const editorDelta = Delta.ofMaps(before.editors, after.editors); const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined; const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined; return new EditorAndDocumentStateDelta( documentDelta.removed, documentDelta.added, editorDelta.removed, editorDelta.added, oldActiveEditor, newActiveEditor ); } } class EditorSnapshot { readonly id: string; constructor(readonly editor: MonacoEditor | SimpleMonacoEditor) { this.id = `${editor.getControl().getId()},${editor.getControl().getModel()!.id}`; } } namespace Delta { export function ofSets(before: Set, after: Set): { removed: T[], added: T[] } { const removed: T[] = []; const added: T[] = []; before.forEach(element => { if (!after.has(element)) { removed.push(element); } }); after.forEach(element => { if (!before.has(element)) { added.push(element); } }); return { removed, added }; } export function ofMaps(before: Map, after: Map): { removed: V[], added: V[] } { const removed: V[] = []; const added: V[] = []; before.forEach((value, index) => { if (!after.has(index)) { removed.push(value); } }); after.forEach((value, index) => { if (!before.has(index)) { added.push(value); } }); return { removed, added }; } }