Path: blob/main/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event } from '../../../base/common/event.js';6import { combinedDisposable, DisposableStore, DisposableMap } from '../../../base/common/lifecycle.js';7import { ICodeEditor, isCodeEditor, isDiffEditor, IActiveCodeEditor } from '../../../editor/browser/editorBrowser.js';8import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';9import { IEditor } from '../../../editor/common/editorCommon.js';10import { ITextModel, shouldSynchronizeModel } from '../../../editor/common/model.js';11import { IModelService } from '../../../editor/common/services/model.js';12import { ITextModelService } from '../../../editor/common/services/resolverService.js';13import { IFileService } from '../../../platform/files/common/files.js';14import { extHostCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';15import { MainThreadDocuments } from './mainThreadDocuments.js';16import { MainThreadTextEditor } from './mainThreadEditor.js';17import { MainThreadTextEditors } from './mainThreadEditors.js';18import { ExtHostContext, ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IModelAddedData, ITextEditorAddData, MainContext } from '../common/extHost.protocol.js';19import { AbstractTextEditor } from '../../browser/parts/editor/textEditor.js';20import { IEditorPane } from '../../common/editor.js';21import { EditorGroupColumn, editorGroupToColumn } from '../../services/editor/common/editorGroupColumn.js';22import { IEditorService } from '../../services/editor/common/editorService.js';23import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';24import { ITextFileService } from '../../services/textfile/common/textfiles.js';25import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';26import { IWorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js';27import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';28import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';29import { IPathService } from '../../services/path/common/pathService.js';30import { diffSets, diffMaps } from '../../../base/common/collections.js';31import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js';32import { ViewContainerLocation } from '../../common/views.js';33import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';34import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js';353637class TextEditorSnapshot {3839readonly id: string;4041constructor(42readonly editor: IActiveCodeEditor,43) {44this.id = `${editor.getId()},${editor.getModel().id}`;45}46}4748class DocumentAndEditorStateDelta {4950readonly isEmpty: boolean;5152constructor(53readonly removedDocuments: ITextModel[],54readonly addedDocuments: ITextModel[],55readonly removedEditors: TextEditorSnapshot[],56readonly addedEditors: TextEditorSnapshot[],57readonly oldActiveEditor: string | null | undefined,58readonly newActiveEditor: string | null | undefined,59) {60this.isEmpty = this.removedDocuments.length === 061&& this.addedDocuments.length === 062&& this.removedEditors.length === 063&& this.addedEditors.length === 064&& oldActiveEditor === newActiveEditor;65}6667toString(): string {68let ret = 'DocumentAndEditorStateDelta\n';69ret += `\tRemoved Documents: [${this.removedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`;70ret += `\tAdded Documents: [${this.addedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`;71ret += `\tRemoved Editors: [${this.removedEditors.map(e => e.id).join(', ')}]\n`;72ret += `\tAdded Editors: [${this.addedEditors.map(e => e.id).join(', ')}]\n`;73ret += `\tNew Active Editor: ${this.newActiveEditor}\n`;74return ret;75}76}7778class DocumentAndEditorState {7980static compute(before: DocumentAndEditorState | undefined, after: DocumentAndEditorState): DocumentAndEditorStateDelta {81if (!before) {82return new DocumentAndEditorStateDelta(83[], [...after.documents.values()],84[], [...after.textEditors.values()],85undefined, after.activeEditor86);87}88const documentDelta = diffSets(before.documents, after.documents);89const editorDelta = diffMaps(before.textEditors, after.textEditors);90const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;91const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;9293return new DocumentAndEditorStateDelta(94documentDelta.removed, documentDelta.added,95editorDelta.removed, editorDelta.added,96oldActiveEditor, newActiveEditor97);98}99100constructor(101readonly documents: Set<ITextModel>,102readonly textEditors: Map<string, TextEditorSnapshot>,103readonly activeEditor: string | null | undefined,104) {105//106}107}108109const enum ActiveEditorOrder {110Editor, Panel111}112113class MainThreadDocumentAndEditorStateComputer {114115private readonly _toDispose = new DisposableStore();116private readonly _toDisposeOnEditorRemove = new DisposableMap<string>();117private _currentState?: DocumentAndEditorState;118private _activeEditorOrder: ActiveEditorOrder = ActiveEditorOrder.Editor;119120constructor(121private readonly _onDidChangeState: (delta: DocumentAndEditorStateDelta) => void,122@IModelService private readonly _modelService: IModelService,123@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,124@IEditorService private readonly _editorService: IEditorService,125@IPaneCompositePartService private readonly _paneCompositeService: IPaneCompositePartService,126) {127this._modelService.onModelAdded(this._updateStateOnModelAdd, this, this._toDispose);128this._modelService.onModelRemoved(_ => this._updateState(), this, this._toDispose);129this._editorService.onDidActiveEditorChange(_ => this._updateState(), this, this._toDispose);130131this._codeEditorService.onCodeEditorAdd(this._onDidAddEditor, this, this._toDispose);132this._codeEditorService.onCodeEditorRemove(this._onDidRemoveEditor, this, this._toDispose);133this._codeEditorService.listCodeEditors().forEach(this._onDidAddEditor, this);134135Event.filter(this._paneCompositeService.onDidPaneCompositeOpen, event => event.viewContainerLocation === ViewContainerLocation.Panel)(_ => this._activeEditorOrder = ActiveEditorOrder.Panel, undefined, this._toDispose);136Event.filter(this._paneCompositeService.onDidPaneCompositeClose, event => event.viewContainerLocation === ViewContainerLocation.Panel)(_ => this._activeEditorOrder = ActiveEditorOrder.Editor, undefined, this._toDispose);137this._editorService.onDidVisibleEditorsChange(_ => this._activeEditorOrder = ActiveEditorOrder.Editor, undefined, this._toDispose);138139this._updateState();140}141142dispose(): void {143this._toDispose.dispose();144this._toDisposeOnEditorRemove.dispose();145}146147private _onDidAddEditor(e: ICodeEditor): void {148this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable(149e.onDidChangeModel(() => this._updateState()),150e.onDidFocusEditorText(() => this._updateState()),151e.onDidFocusEditorWidget(() => this._updateState(e))152));153this._updateState();154}155156private _onDidRemoveEditor(e: ICodeEditor): void {157const id = e.getId();158if (this._toDisposeOnEditorRemove.has(id)) {159this._toDisposeOnEditorRemove.deleteAndDispose(id);160this._updateState();161}162}163164private _updateStateOnModelAdd(model: ITextModel): void {165if (!shouldSynchronizeModel(model)) {166// ignore167return;168}169170if (!this._currentState) {171// too early172this._updateState();173return;174}175176// small (fast) delta177this._currentState = new DocumentAndEditorState(178this._currentState.documents.add(model),179this._currentState.textEditors,180this._currentState.activeEditor181);182183this._onDidChangeState(new DocumentAndEditorStateDelta(184[], [model],185[], [],186undefined, undefined187));188}189190private _updateState(widgetFocusCandidate?: ICodeEditor): void {191192// models: ignore too large models193const models = new Set<ITextModel>();194for (const model of this._modelService.getModels()) {195if (shouldSynchronizeModel(model)) {196models.add(model);197}198}199200// editor: only take those that have a not too large model201const editors = new Map<string, TextEditorSnapshot>();202let activeEditor: string | null = null; // Strict null work. This doesn't like being undefined!203204for (const editor of this._codeEditorService.listCodeEditors()) {205if (editor.isSimpleWidget) {206continue;207}208const model = editor.getModel();209if (editor.hasModel() && model && shouldSynchronizeModel(model)210&& !model.isDisposed() // model disposed211&& Boolean(this._modelService.getModel(model.uri)) // model disposing, the flag didn't flip yet but the model service already removed it212) {213const apiEditor = new TextEditorSnapshot(editor);214editors.set(apiEditor.id, apiEditor);215if (editor.hasTextFocus() || (widgetFocusCandidate === editor && editor.hasWidgetFocus())) {216// text focus has priority, widget focus is tricky because multiple217// editors might claim widget focus at the same time. therefore we use a218// candidate (which is the editor that has raised an widget focus event)219// in addition to the widget focus check220activeEditor = apiEditor.id;221}222}223}224225// active editor: if none of the previous editors had focus we try226// to match output panels or the active workbench editor with227// one of editor we have just computed228if (!activeEditor) {229let candidate: IEditor | undefined;230if (this._activeEditorOrder === ActiveEditorOrder.Editor) {231candidate = this._getActiveEditorFromEditorPart() || this._getActiveEditorFromPanel();232} else {233candidate = this._getActiveEditorFromPanel() || this._getActiveEditorFromEditorPart();234}235236if (candidate) {237for (const snapshot of editors.values()) {238if (candidate === snapshot.editor) {239activeEditor = snapshot.id;240}241}242}243}244245// compute new state and compare against old246const newState = new DocumentAndEditorState(models, editors, activeEditor);247const delta = DocumentAndEditorState.compute(this._currentState, newState);248if (!delta.isEmpty) {249this._currentState = newState;250this._onDidChangeState(delta);251}252}253254private _getActiveEditorFromPanel(): IEditor | undefined {255const panel = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel);256if (panel instanceof AbstractTextEditor) {257const control = panel.getControl();258if (isCodeEditor(control)) {259return control;260}261}262263return undefined;264}265266private _getActiveEditorFromEditorPart(): IEditor | undefined {267let activeTextEditorControl = this._editorService.activeTextEditorControl;268if (isDiffEditor(activeTextEditorControl)) {269activeTextEditorControl = activeTextEditorControl.getModifiedEditor();270}271return activeTextEditorControl;272}273}274275@extHostCustomer276export class MainThreadDocumentsAndEditors {277278private readonly _toDispose = new DisposableStore();279private readonly _proxy: ExtHostDocumentsAndEditorsShape;280private readonly _mainThreadDocuments: MainThreadDocuments;281private readonly _mainThreadEditors: MainThreadTextEditors;282private readonly _textEditors = new Map<string, MainThreadTextEditor>();283284constructor(285extHostContext: IExtHostContext,286@IModelService private readonly _modelService: IModelService,287@ITextFileService private readonly _textFileService: ITextFileService,288@IEditorService private readonly _editorService: IEditorService,289@ICodeEditorService codeEditorService: ICodeEditorService,290@IFileService fileService: IFileService,291@ITextModelService textModelResolverService: ITextModelService,292@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,293@IPaneCompositePartService paneCompositeService: IPaneCompositePartService,294@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,295@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,296@IUriIdentityService uriIdentityService: IUriIdentityService,297@IClipboardService private readonly _clipboardService: IClipboardService,298@IPathService pathService: IPathService,299@IConfigurationService configurationService: IConfigurationService,300@IQuickDiffModelService quickDiffModelService: IQuickDiffModelService301) {302this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors);303304this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService));305extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments);306307this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService, quickDiffModelService, uriIdentityService));308extHostContext.set(MainContext.MainThreadTextEditors, this._mainThreadEditors);309310// It is expected that the ctor of the state computer calls our `_onDelta`.311this._toDispose.add(new MainThreadDocumentAndEditorStateComputer(delta => this._onDelta(delta), _modelService, codeEditorService, this._editorService, paneCompositeService));312}313314dispose(): void {315this._toDispose.dispose();316}317318private _onDelta(delta: DocumentAndEditorStateDelta): void {319320const removedEditors: string[] = [];321const addedEditors: MainThreadTextEditor[] = [];322323// removed models324const removedDocuments = delta.removedDocuments.map(m => m.uri);325326// added editors327for (const apiEditor of delta.addedEditors) {328const mainThreadEditor = new MainThreadTextEditor(apiEditor.id, apiEditor.editor.getModel(),329apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._mainThreadDocuments, this._modelService, this._clipboardService);330331this._textEditors.set(apiEditor.id, mainThreadEditor);332addedEditors.push(mainThreadEditor);333}334335// removed editors336for (const { id } of delta.removedEditors) {337const mainThreadEditor = this._textEditors.get(id);338if (mainThreadEditor) {339mainThreadEditor.dispose();340this._textEditors.delete(id);341removedEditors.push(id);342}343}344345const extHostDelta: IDocumentsAndEditorsDelta = Object.create(null);346let empty = true;347if (delta.newActiveEditor !== undefined) {348empty = false;349extHostDelta.newActiveEditor = delta.newActiveEditor;350}351if (removedDocuments.length > 0) {352empty = false;353extHostDelta.removedDocuments = removedDocuments;354}355if (removedEditors.length > 0) {356empty = false;357extHostDelta.removedEditors = removedEditors;358}359if (delta.addedDocuments.length > 0) {360empty = false;361extHostDelta.addedDocuments = delta.addedDocuments.map(m => this._toModelAddData(m));362}363if (delta.addedEditors.length > 0) {364empty = false;365extHostDelta.addedEditors = addedEditors.map(e => this._toTextEditorAddData(e));366}367368if (!empty) {369// first update ext host370this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta);371372// second update dependent document/editor states373removedDocuments.forEach(this._mainThreadDocuments.handleModelRemoved, this._mainThreadDocuments);374delta.addedDocuments.forEach(this._mainThreadDocuments.handleModelAdded, this._mainThreadDocuments);375376removedEditors.forEach(this._mainThreadEditors.handleTextEditorRemoved, this._mainThreadEditors);377addedEditors.forEach(this._mainThreadEditors.handleTextEditorAdded, this._mainThreadEditors);378}379}380381private _toModelAddData(model: ITextModel): IModelAddedData {382return {383uri: model.uri,384versionId: model.getVersionId(),385lines: model.getLinesContent(),386EOL: model.getEOL(),387languageId: model.getLanguageId(),388isDirty: this._textFileService.isDirty(model.uri),389encoding: this._textFileService.getEncoding(model.uri)390};391}392393private _toTextEditorAddData(textEditor: MainThreadTextEditor): ITextEditorAddData {394const props = textEditor.getProperties();395return {396id: textEditor.getId(),397documentUri: textEditor.getModel().uri,398options: props.options,399selections: props.selections,400visibleRanges: props.visibleRanges,401editorPosition: this._findEditorPosition(textEditor)402};403}404405private _findEditorPosition(editor: MainThreadTextEditor): EditorGroupColumn | undefined {406for (const editorPane of this._editorService.visibleEditorPanes) {407if (editor.matches(editorPane)) {408return editorGroupToColumn(this._editorGroupService, editorPane.group);409}410}411return undefined;412}413414findTextEditorIdFor(editorPane: IEditorPane): string | undefined {415for (const [id, editor] of this._textEditors) {416if (editor.matches(editorPane)) {417return id;418}419}420return undefined;421}422423getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined {424for (const [id, editor] of this._textEditors) {425if (editor.getCodeEditor() === codeEditor) {426return id;427}428}429return undefined;430}431432getEditor(id: string): MainThreadTextEditor | undefined {433return this._textEditors.get(id);434}435}436437438