Path: blob/main/src/vs/workbench/api/browser/mainThreadEditors.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 { illegalArgument } from '../../../base/common/errors.js';6import { IDisposable, dispose, DisposableStore } from '../../../base/common/lifecycle.js';7import { equals as objectEquals } from '../../../base/common/objects.js';8import { URI, UriComponents } from '../../../base/common/uri.js';9import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';10import { IRange } from '../../../editor/common/core/range.js';11import { ISelection } from '../../../editor/common/core/selection.js';12import { IDecorationOptions, IDecorationRenderOptions } from '../../../editor/common/editorCommon.js';13import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js';14import { CommandsRegistry } from '../../../platform/commands/common/commands.js';15import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution, ITextEditorDiffInformation, isTextEditorDiffInformationEqual, ITextEditorChange } from '../../../platform/editor/common/editor.js';16import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';17import { MainThreadTextEditor } from './mainThreadEditor.js';18import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js';19import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';20import { IEditorService } from '../../services/editor/common/editorService.js';21import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';22import { IEnvironmentService } from '../../../platform/environment/common/environment.js';23import { IWorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js';24import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';25import { IChange } from '../../../editor/common/diff/legacyLinesDiffComputer.js';26import { IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';27import { IEditorControl } from '../../common/editor.js';28import { getCodeEditor, ICodeEditor } from '../../../editor/browser/editorBrowser.js';29import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';30import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js';31import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js';32import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';33import { isITextModel } from '../../../editor/common/model.js';34import { LineRangeMapping } from '../../../editor/common/diff/rangeMapping.js';35import { equals } from '../../../base/common/arrays.js';36import { Event } from '../../../base/common/event.js';37import { DiffAlgorithmName } from '../../../editor/common/services/editorWorker.js';3839export interface IMainThreadEditorLocator {40getEditor(id: string): MainThreadTextEditor | undefined;41findTextEditorIdFor(editorControl: IEditorControl): string | undefined;42getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined;43}4445export class MainThreadTextEditors implements MainThreadTextEditorsShape {4647private static INSTANCE_COUNT: number = 0;4849private readonly _instanceId: string;50private readonly _proxy: ExtHostEditorsShape;51private readonly _toDispose = new DisposableStore();52private _textEditorsListenersMap: { [editorId: string]: IDisposable[] };53private _editorPositionData: ITextEditorPositionData | null;54private _registeredDecorationTypes: { [decorationType: string]: boolean };5556constructor(57private readonly _editorLocator: IMainThreadEditorLocator,58extHostContext: IExtHostContext,59@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,60@IEditorService private readonly _editorService: IEditorService,61@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,62@IConfigurationService private readonly _configurationService: IConfigurationService,63@IQuickDiffModelService private readonly _quickDiffModelService: IQuickDiffModelService,64@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService65) {66this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT);67this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors);6869this._textEditorsListenersMap = Object.create(null);70this._editorPositionData = null;7172this._toDispose.add(this._editorService.onDidVisibleEditorsChange(() => this._updateActiveAndVisibleTextEditors()));73this._toDispose.add(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors()));74this._toDispose.add(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors()));7576this._registeredDecorationTypes = Object.create(null);77}7879dispose(): void {80Object.keys(this._textEditorsListenersMap).forEach((editorId) => {81dispose(this._textEditorsListenersMap[editorId]);82});83this._textEditorsListenersMap = Object.create(null);84this._toDispose.dispose();85for (const decorationType in this._registeredDecorationTypes) {86this._codeEditorService.removeDecorationType(decorationType);87}88this._registeredDecorationTypes = Object.create(null);89}9091handleTextEditorAdded(textEditor: MainThreadTextEditor): void {92const id = textEditor.getId();93const toDispose: IDisposable[] = [];94toDispose.push(textEditor.onPropertiesChanged((data) => {95this._proxy.$acceptEditorPropertiesChanged(id, data);96}));9798const diffInformationObs = this._getTextEditorDiffInformation(textEditor, toDispose);99toDispose.push(autorun(reader => {100const diffInformation = diffInformationObs.read(reader);101this._proxy.$acceptEditorDiffInformation(id, diffInformation);102}));103104this._textEditorsListenersMap[id] = toDispose;105}106107handleTextEditorRemoved(id: string): void {108dispose(this._textEditorsListenersMap[id]);109delete this._textEditorsListenersMap[id];110}111112private _updateActiveAndVisibleTextEditors(): void {113114// editor columns115const editorPositionData = this._getTextEditorPositionData();116if (!objectEquals(this._editorPositionData, editorPositionData)) {117this._editorPositionData = editorPositionData;118this._proxy.$acceptEditorPositionData(this._editorPositionData);119}120}121122private _getTextEditorPositionData(): ITextEditorPositionData {123const result: ITextEditorPositionData = Object.create(null);124for (const editorPane of this._editorService.visibleEditorPanes) {125const id = this._editorLocator.findTextEditorIdFor(editorPane);126if (id) {127result[id] = editorGroupToColumn(this._editorGroupService, editorPane.group);128}129}130return result;131}132133private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor, toDispose: IDisposable[]): IObservable<ITextEditorDiffInformation[] | undefined> {134const codeEditor = textEditor.getCodeEditor();135if (!codeEditor) {136return constObservable(undefined);137}138139// Check if the TextModel belongs to a DiffEditor140const [diffEditor] = this._codeEditorService.listDiffEditors()141.filter(d =>142d.getOriginalEditor().getId() === codeEditor.getId() ||143d.getModifiedEditor().getId() === codeEditor.getId());144145const editorModelObs = diffEditor146? observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel())147: observableFromEvent(this, codeEditor.onDidChangeModel, () => codeEditor.getModel());148149const editorChangesObs = derived<IObservable<{ original: URI; modified: URI; changes: readonly LineRangeMapping[] }[] | undefined>>(reader => {150const editorModel = editorModelObs.read(reader);151if (!editorModel) {152return constObservable(undefined);153}154155const editorModelUri = isITextModel(editorModel)156? editorModel.uri157: editorModel.modified.uri;158159// TextEditor160if (isITextModel(editorModel)) {161const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri);162if (!quickDiffModelRef) {163return constObservable(undefined);164}165166toDispose.push(quickDiffModelRef);167return observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => {168return quickDiffModelRef.object.getQuickDiffResults()169.map(result => ({170original: result.original,171modified: result.modified,172changes: result.changes2173}));174});175}176177// DirtyDiffModel - we create a dirty diff model for diff editor so that178// we can provide multiple "original resources" to diff with the modified179// resource.180const diffAlgorithm = this._configurationService.getValue<DiffAlgorithmName>('diffEditor.diffAlgorithm');181const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri, { algorithm: diffAlgorithm });182if (!quickDiffModelRef) {183return constObservable(undefined);184}185186toDispose.push(quickDiffModelRef);187return observableFromEvent(Event.any(quickDiffModelRef.object.onDidChange, diffEditor.onDidUpdateDiff), () => {188const quickDiffInformation = quickDiffModelRef.object.getQuickDiffResults()189.map(result => ({190original: result.original,191modified: result.modified,192changes: result.changes2193}));194195const diffChanges = diffEditor.getDiffComputationResult()?.changes2 ?? [];196const diffInformation = [{197original: editorModel.original.uri,198modified: editorModel.modified.uri,199changes: diffChanges.map(change => change as LineRangeMapping)200}];201202return [...quickDiffInformation, ...diffInformation];203});204});205206return derivedOpts({207owner: this,208equalsFn: (diff1, diff2) => equals(diff1, diff2, (a, b) => isTextEditorDiffInformationEqual(this._uriIdentityService, a, b))209}, reader => {210const editorModel = editorModelObs.read(reader);211const editorChanges = editorChangesObs.read(reader).read(reader);212if (!editorModel || !editorChanges) {213return undefined;214}215216const documentVersion = isITextModel(editorModel)217? editorModel.getVersionId()218: editorModel.modified.getVersionId();219220return editorChanges.map(change => {221const changes: ITextEditorChange[] = change.changes222.map(change => [223change.original.startLineNumber,224change.original.endLineNumberExclusive,225change.modified.startLineNumber,226change.modified.endLineNumberExclusive227]);228229return {230documentVersion,231original: change.original,232modified: change.modified,233changes234};235});236});237}238239// --- from extension host process240241async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string | undefined> {242const uri = URI.revive(resource);243244const editorOptions: ITextEditorOptions = {245preserveFocus: options.preserveFocus,246pinned: options.pinned,247selection: options.selection,248// preserve pre 1.38 behaviour to not make group active when preserveFocus: true249// but make sure to restore the editor to fix https://github.com/microsoft/vscode/issues/79633250activation: options.preserveFocus ? EditorActivation.RESTORE : undefined,251override: EditorResolution.EXCLUSIVE_ONLY252};253254const input: IResourceEditorInput = {255resource: uri,256options: editorOptions257};258259const editor = await this._editorService.openEditor(input, columnToEditorGroup(this._editorGroupService, this._configurationService, options.position));260if (!editor) {261return undefined;262}263// Composite editors are made up of many editors so we return the active one at the time of opening264const editorControl = editor.getControl();265const codeEditor = getCodeEditor(editorControl);266return codeEditor ? this._editorLocator.getIdOfCodeEditor(codeEditor) : undefined;267}268269async $tryShowEditor(id: string, position?: EditorGroupColumn): Promise<void> {270const mainThreadEditor = this._editorLocator.getEditor(id);271if (mainThreadEditor) {272const model = mainThreadEditor.getModel();273await this._editorService.openEditor({274resource: model.uri,275options: { preserveFocus: false }276}, columnToEditorGroup(this._editorGroupService, this._configurationService, position));277return;278}279}280281async $tryHideEditor(id: string): Promise<void> {282const mainThreadEditor = this._editorLocator.getEditor(id);283if (mainThreadEditor) {284const editorPanes = this._editorService.visibleEditorPanes;285for (const editorPane of editorPanes) {286if (mainThreadEditor.matches(editorPane)) {287await editorPane.group.closeEditor(editorPane.input);288return;289}290}291}292}293294$trySetSelections(id: string, selections: ISelection[]): Promise<void> {295const editor = this._editorLocator.getEditor(id);296if (!editor) {297return Promise.reject(illegalArgument(`TextEditor(${id})`));298}299editor.setSelections(selections);300return Promise.resolve(undefined);301}302303$trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): Promise<void> {304key = `${this._instanceId}-${key}`;305const editor = this._editorLocator.getEditor(id);306if (!editor) {307return Promise.reject(illegalArgument(`TextEditor(${id})`));308}309editor.setDecorations(key, ranges);310return Promise.resolve(undefined);311}312313$trySetDecorationsFast(id: string, key: string, ranges: number[]): Promise<void> {314key = `${this._instanceId}-${key}`;315const editor = this._editorLocator.getEditor(id);316if (!editor) {317return Promise.reject(illegalArgument(`TextEditor(${id})`));318}319editor.setDecorationsFast(key, ranges);320return Promise.resolve(undefined);321}322323$tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Promise<void> {324const editor = this._editorLocator.getEditor(id);325if (!editor) {326return Promise.reject(illegalArgument(`TextEditor(${id})`));327}328editor.revealRange(range, revealType);329return Promise.resolve();330}331332$trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Promise<void> {333const editor = this._editorLocator.getEditor(id);334if (!editor) {335return Promise.reject(illegalArgument(`TextEditor(${id})`));336}337editor.setConfiguration(options);338return Promise.resolve(undefined);339}340341$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Promise<boolean> {342const editor = this._editorLocator.getEditor(id);343if (!editor) {344return Promise.reject(illegalArgument(`TextEditor(${id})`));345}346return Promise.resolve(editor.applyEdits(modelVersionId, edits, opts));347}348349$tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise<boolean> {350const editor = this._editorLocator.getEditor(id);351if (!editor) {352return Promise.reject(illegalArgument(`TextEditor(${id})`));353}354return Promise.resolve(editor.insertSnippet(modelVersionId, template, ranges, opts));355}356357$registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: IDecorationRenderOptions): void {358key = `${this._instanceId}-${key}`;359this._registeredDecorationTypes[key] = true;360this._codeEditorService.registerDecorationType(`exthost-api-${extensionId}`, key, options);361}362363$removeTextEditorDecorationType(key: string): void {364key = `${this._instanceId}-${key}`;365delete this._registeredDecorationTypes[key];366this._codeEditorService.removeDecorationType(key);367}368369$getDiffInformation(id: string): Promise<IChange[]> {370const editor = this._editorLocator.getEditor(id);371372if (!editor) {373return Promise.reject(new Error('No such TextEditor'));374}375376const codeEditor = editor.getCodeEditor();377if (!codeEditor) {378return Promise.reject(new Error('No such CodeEditor'));379}380381const codeEditorId = codeEditor.getId();382const diffEditors = this._codeEditorService.listDiffEditors();383const [diffEditor] = diffEditors.filter(d => d.getOriginalEditor().getId() === codeEditorId || d.getModifiedEditor().getId() === codeEditorId);384385if (diffEditor) {386return Promise.resolve(diffEditor.getLineChanges() || []);387}388389if (!codeEditor.hasModel()) {390return Promise.resolve([]);391}392393const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(codeEditor.getModel().uri);394if (!quickDiffModelRef) {395return Promise.resolve([]);396}397398try {399const primaryQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary');400const primaryQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.providerId === primaryQuickDiff?.id);401402return Promise.resolve(primaryQuickDiffChanges.map(change => change.change) ?? []);403} finally {404quickDiffModelRef.dispose();405}406}407}408409// --- commands410411CommandsRegistry.registerCommand('_workbench.revertAllDirty', async function (accessor: ServicesAccessor) {412const environmentService = accessor.get(IEnvironmentService);413if (!environmentService.extensionTestsLocationURI) {414throw new Error('Command is only available when running extension tests.');415}416417const workingCopyService = accessor.get(IWorkingCopyService);418for (const workingCopy of workingCopyService.dirtyWorkingCopies) {419await workingCopy.revert({ soft: true });420}421});422423424