diff --git a/extensions/git/package.json b/extensions/git/package.json index 937f9591b05..5e65475fe73 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -34,6 +34,7 @@ "scmValidation", "tabInputMultiDiff", "tabInputTextMerge", + "textEditorDiffInformation", "timeline" ], "categories": [ diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 75fb8217df7..82ba0abe546 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -20,6 +20,7 @@ "../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", + "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../types/lib.textEncoder.d.ts" diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index ae3d50b2570..eaa80d0ef7b 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equals } from '../../../base/common/arrays.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; +import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; export interface IResolvableEditorModel extends IDisposable { @@ -376,3 +378,29 @@ export interface ITextEditorOptions extends IEditorOptions { */ selectionSource?: TextEditorSelectionSource | string; } + +export type ITextEditorDiff = [ + originalStartLineNumber: number, + originalEndLineNumber: number, + modifiedStartLineNumber: number, + modifiedEndLineNumber: number +]; + +export interface ITextEditorDiffInformation { + readonly documentVersion: number; + readonly original: URI | undefined; + readonly modified: URI | undefined; + readonly diff: readonly ITextEditorDiff[]; +} + +export function isTextEditorDiffInformationEqual( + uriIdentityService: IUriIdentityService, + diff1: ITextEditorDiffInformation | undefined, + diff2: ITextEditorDiffInformation | undefined): boolean { + return diff1?.documentVersion === diff2?.documentVersion && + uriIdentityService.extUri.isEqual(diff1?.original, diff2?.original) && + uriIdentityService.extUri.isEqual(diff1?.modified, diff2?.modified) && + equals(diff1?.diff, diff2?.diff, (a, b) => { + return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; + }); +} diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index b8d13ce3f00..2f9d66464b3 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -364,6 +364,9 @@ const _allApiProposals = { testRelatedCode: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts', }, + textEditorDiffInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts', + }, textSearchComplete2: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchComplete2.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index c7a3dcea721..4f5e72a58f7 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -32,6 +32,7 @@ import { diffSets, diffMaps } from '../../../base/common/collections.js'; import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js'; import { ViewContainerLocation } from '../../common/views.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { IDirtyDiffModelService } from '../../contrib/scm/browser/diff.js'; class TextEditorSnapshot { @@ -296,14 +297,15 @@ export class MainThreadDocumentsAndEditors { @IUriIdentityService uriIdentityService: IUriIdentityService, @IClipboardService private readonly _clipboardService: IClipboardService, @IPathService pathService: IPathService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IDirtyDiffModelService dirtyDiffModelService: IDirtyDiffModelService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService)); extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments); - this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService)); + this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService, dirtyDiffModelService, uriIdentityService)); extHostContext.set(MainContext.MainThreadTextEditors, this._mainThreadEditors); // It is expected that the ctor of the state computer calls our `_onDelta`. diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index a51ada95deb..51816c7afdf 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -13,7 +13,7 @@ import { ISelection } from '../../../editor/common/core/selection.js'; import { IDecorationOptions, IDecorationRenderOptions } from '../../../editor/common/editorCommon.js'; import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js'; import { CommandsRegistry } from '../../../platform/commands/common/commands.js'; -import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution } from '../../../platform/editor/common/editor.js'; +import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution, ITextEditorDiffInformation, isTextEditorDiffInformationEqual, ITextEditorDiff } from '../../../platform/editor/common/editor.js'; import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { MainThreadTextEditor } from './mainThreadEditor.js'; import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js'; @@ -29,6 +29,9 @@ import { IEditorControl } from '../../common/editor.js'; import { getCodeEditor, ICodeEditor } from '../../../editor/browser/editorBrowser.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; import { DirtyDiffContribution } from '../../contrib/scm/browser/dirtydiffDecorator.js'; +import { IDirtyDiffModelService } from '../../contrib/scm/browser/diff.js'; +import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js'; +import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; export interface IMainThreadEditorLocator { getEditor(id: string): MainThreadTextEditor | undefined; @@ -53,7 +56,9 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IDirtyDiffModelService private readonly _dirtyDiffModelService: IDirtyDiffModelService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService ) { this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors); @@ -87,6 +92,12 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._proxy.$acceptEditorPropertiesChanged(id, data); })); + const diffInformationObs = this._getTextEditorDiffInformation(textEditor); + toDispose.push(autorun(reader => { + const diffInformation = diffInformationObs.read(reader); + this._proxy.$acceptEditorDiffInformation(id, diffInformation); + })); + this._textEditorsListenersMap[id] = toDispose; } @@ -116,6 +127,75 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return result; } + private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor): IObservable { + const codeEditor = textEditor.getCodeEditor(); + if (!codeEditor) { + return constObservable(undefined); + } + + // Check if the TextModel belongs to a diff editor + const diffEditors = this._codeEditorService.listDiffEditors(); + const [diffEditor] = diffEditors.filter(d => + d.getOriginalEditor().getId() === codeEditor.getId() || + d.getModifiedEditor().getId() === codeEditor.getId()); + + const codeEditorTextModelObs = !diffEditor ? + observableFromEvent(this, codeEditor.onDidChangeModel, () => codeEditor.getModel()) : + observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel()?.modified); + + const dirtyDiffModelObs = derived(reader => { + const codeEditorTextModel = codeEditorTextModelObs.read(reader); + return codeEditorTextModel ? this._dirtyDiffModelService.getOrCreateModel(codeEditorTextModel.uri) : undefined; + }); + + const scmQuickDiffChangesObs = derived(reader => { + const dirtyDiffModel = dirtyDiffModelObs.read(reader); + if (!dirtyDiffModel) { + return constObservable(undefined); + } + + return observableFromEvent(this, dirtyDiffModel.onDidChange, e => { + const scmQuickDiff = dirtyDiffModel.quickDiffs.find(diff => diff.isSCM === true); + if (!e || !scmQuickDiff) { + return undefined; + } + + return { + originalResource: scmQuickDiff.originalResource, + changes: e.changes + .filter(change => change.label === scmQuickDiff.label) + .map(change => change.change) + }; + }); + }); + + return derivedOpts({ + owner: this, + equalsFn: (diff1, diff2) => isTextEditorDiffInformationEqual(this._uriIdentityService, diff1, diff2) + }, reader => { + const codeEditorTextModel = codeEditorTextModelObs.read(reader); + const scmQuickDiffChanges = scmQuickDiffChangesObs.read(reader).read(reader); + if (!codeEditorTextModel || !scmQuickDiffChanges) { + return undefined; + } + + const diff: ITextEditorDiff[] = scmQuickDiffChanges.changes + .map(change => [ + change.originalStartLineNumber, + change.originalEndLineNumber, + change.modifiedStartLineNumber, + change.modifiedEndLineNumber + ]); + + return { + documentVersion: codeEditorTextModel.getVersionId(), + original: scmQuickDiffChanges.originalResource, + modified: codeEditorTextModel.uri, + diff + }; + }); + } + // --- from extension host process async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index cca8cfe756a..3eb6932cb73 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -723,6 +723,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidChangeTextEditorViewColumn(listener, thisArg?, disposables?) { return _asExtensionEvent(extHostEditors.onDidChangeTextEditorViewColumn)(listener, thisArg, disposables); }, + onDidChangeTextEditorDiffInformation(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'textEditorDiffInformation'); + return _asExtensionEvent(extHostEditors.onDidChangeTextEditorDiffInformation)(listener, thisArg, disposables); + }, onDidCloseTerminal(listener, thisArg?, disposables?) { return _asExtensionEvent(extHostTerminalService.onDidCloseTerminal)(listener, thisArg, disposables); }, @@ -1661,6 +1665,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, TextEditorCursorStyle: TextEditorCursorStyle, + TextEditorDiffKind: extHostTypes.TextEditorDiffKind, TextEditorLineNumbersStyle: extHostTypes.TextEditorLineNumbersStyle, TextEditorRevealType: extHostTypes.TextEditorRevealType, TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a4532ed13ec..69ba29ff992 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1817,6 +1817,21 @@ export interface ITextEditorAddData { export interface ITextEditorPositionData { [id: string]: EditorGroupColumn; } + +export type ITextEditorDiff = [ + originalStartLineNumber: number, + originalEndLineNumber: number, + modifiedStartLineNumber: number, + modifiedEndLineNumber: number +]; + +export interface ITextEditorDiffInformation { + readonly documentVersion: number; + readonly original: UriComponents | undefined; + readonly modified: UriComponents | undefined; + readonly diff: readonly ITextEditorDiff[]; +} + export interface IEditorPropertiesChangeData { options: IResolvedTextEditorConfiguration | null; selections: ISelectionChangeEvent | null; @@ -1830,6 +1845,7 @@ export interface ISelectionChangeEvent { export interface ExtHostEditorsShape { $acceptEditorPropertiesChanged(id: string, props: IEditorPropertiesChangeData): void; $acceptEditorPositionData(data: ITextEditorPositionData): void; + $acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation | undefined): void; } export interface IDocumentsAndEditorsDelta { diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index cba39eb86b5..df874f990a3 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -412,6 +412,7 @@ export class ExtHostTextEditor { private _viewColumn: vscode.ViewColumn | undefined; private _disposed: boolean = false; private _hasDecorationsForKey = new Set(); + private _diffInformation: vscode.TextEditorDiffInformation | undefined; readonly value: vscode.TextEditor; @@ -465,6 +466,9 @@ export class ExtHostTextEditor { set visibleRanges(_value: Range[]) { throw new ReadonlyError('visibleRanges'); }, + get diffInformation() { + return that._diffInformation; + }, // --- options get options(): vscode.TextEditorOptions { return that._options.value; @@ -600,6 +604,11 @@ export class ExtHostTextEditor { this._selections = selections; } + _acceptDiffInformation(diffInformation: vscode.TextEditorDiffInformation | undefined): void { + ok(!this._disposed); + this._diffInformation = diffInformation; + } + private async _trySetSelection(): Promise { const selection = this._selections.map(TypeConverters.Selection.from); await this._runOnProxy(() => this._proxy.$trySetSelections(this.id, selection)); diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 178c6c5bea9..a519961cbe2 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -6,12 +6,13 @@ import * as arrays from '../../../base/common/arrays.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; +import { URI } from '../../../base/common/uri.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from './extHost.protocol.js'; +import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorDiffInformation, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from './extHost.protocol.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; import { ExtHostTextEditor, TextEditorDecorationType } from './extHostTextEditor.js'; import * as TypeConverters from './extHostTypeConverters.js'; -import { TextEditorSelectionChangeKind } from './extHostTypes.js'; +import { TextEditorSelectionChangeKind, TextEditorDiffKind } from './extHostTypes.js'; import * as vscode from 'vscode'; export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { @@ -20,6 +21,7 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorOptions = new Emitter(); private readonly _onDidChangeTextEditorVisibleRanges = new Emitter(); private readonly _onDidChangeTextEditorViewColumn = new Emitter(); + private readonly _onDidChangeTextEditorDiffInformation = new Emitter(); private readonly _onDidChangeActiveTextEditor = new Emitter(); private readonly _onDidChangeVisibleTextEditors = new Emitter(); @@ -27,6 +29,7 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { readonly onDidChangeTextEditorOptions: Event = this._onDidChangeTextEditorOptions.event; readonly onDidChangeTextEditorVisibleRanges: Event = this._onDidChangeTextEditorVisibleRanges.event; readonly onDidChangeTextEditorViewColumn: Event = this._onDidChangeTextEditorViewColumn.event; + readonly onDidChangeTextEditorDiffInformation: Event = this._onDidChangeTextEditorDiffInformation.event; readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; @@ -157,6 +160,53 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { } } + $acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation | undefined): void { + const textEditor = this._extHostDocumentsAndEditors.getEditor(id); + if (!textEditor) { + throw new Error('unknown text editor'); + } + + if (!diffInformation) { + textEditor._acceptDiffInformation(undefined); + this._onDidChangeTextEditorDiffInformation.fire({ + textEditor: textEditor.value, + diffInformation: undefined + }); + return; + } + + const original = URI.revive(diffInformation.original); + const modified = URI.revive(diffInformation.modified); + + const diff = diffInformation.diff.map(diff => { + const [originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber] = diff; + + const kind = originalEndLineNumber === 0 ? TextEditorDiffKind.Addition : + modifiedEndLineNumber === 0 ? TextEditorDiffKind.Deletion : TextEditorDiffKind.Modification; + + return { + originalStartLineNumber, + originalEndLineNumber, + modifiedStartLineNumber, + modifiedEndLineNumber, + kind + } satisfies vscode.TextEditorDiff; + }); + + const result = Object.freeze({ + documentVersion: diffInformation.documentVersion, + original, + modified, + diff + }); + + textEditor._acceptDiffInformation(result); + this._onDidChangeTextEditorDiffInformation.fire({ + textEditor: textEditor.value, + diffInformation: result + }); + } + getDiffInformation(id: string): Promise { return Promise.resolve(this._proxy.$getDiffInformation(id)); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c1ed849707b..98a80defddf 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1909,6 +1909,12 @@ export enum TextEditorSelectionChangeKind { Command = 3 } +export enum TextEditorDiffKind { + Addition = 1, + Deletion = 2, + Modification = 3 +} + export enum TextDocumentChangeReason { Undo = 1, Redo = 2, diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index 08a5f0682c6..cdebb67e526 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -10,7 +10,7 @@ import { TestConfigurationService } from '../../../../platform/configuration/tes import { ModelService } from '../../../../editor/common/services/modelService.js'; import { TestCodeEditorService } from '../../../../editor/test/browser/editorTestServices.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta } from '../../common/extHost.protocol.js'; +import { IDocumentsAndEditorsDelta } from '../../common/extHost.protocol.js'; import { createTestCodeEditor, ITestCodeEditor } from '../../../../editor/test/browser/testCodeEditor.js'; import { mock } from '../../../../base/test/common/mock.js'; import { TestEditorService, TestEditorGroupsService, TestEnvironmentService, TestPathService } from '../../../test/browser/workbenchTestServices.js'; @@ -36,6 +36,8 @@ import { LanguageService } from '../../../../editor/common/services/languageServ import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js'; import { TestLanguageConfigurationService } from '../../../../editor/test/common/modes/testLanguageConfigurationService.js'; import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; +import { IDirtyDiffModelService } from '../../../contrib/scm/browser/diff.js'; +import { ITextEditorDiffInformation } from '../../../../platform/editor/common/editor.js'; suite('MainThreadDocumentsAndEditors', () => { @@ -94,8 +96,9 @@ suite('MainThreadDocumentsAndEditors', () => { }; new MainThreadDocumentsAndEditors( - SingleProxyRPCProtocol(new class extends mock() { - override $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta) { deltas.push(delta); } + SingleProxyRPCProtocol({ + $acceptDocumentsAndEditorsDelta: (delta: IDocumentsAndEditorsDelta) => { deltas.push(delta); }, + $acceptEditorDiffInformation: (id: string, diffInformation: ITextEditorDiffInformation | undefined) => { } }), modelService, textFileService, @@ -121,6 +124,11 @@ suite('MainThreadDocumentsAndEditors', () => { }, new TestPathService(), new TestConfigurationService(), + new class extends mock() { + override getOrCreateModel() { + return undefined; + } + } ); }); diff --git a/src/vs/workbench/contrib/scm/browser/diff.ts b/src/vs/workbench/contrib/scm/browser/diff.ts new file mode 100644 index 00000000000..9f8a925bcc7 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/diff.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceMap } from '../../../../base/common/map.js'; +import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { DirtyDiffModel } from './dirtydiffDecorator.js'; +import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; +import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { URI } from '../../../../base/common/uri.js'; + +export const IDirtyDiffModelService = createDecorator('IDirtyDiffModelService'); + +export interface IDirtyDiffModelService { + _serviceBrand: undefined; + getOrCreateModel(uri: URI): DirtyDiffModel | undefined; +} + +export class DirtyDiffModelService extends Disposable implements IDirtyDiffModelService { + _serviceBrand: undefined; + + private _models = new ResourceMap(); + + private _visibleTextEditorControls = observableFromEvent( + this.editorService.onDidVisibleEditorsChange, + () => this.editorService.visibleTextEditorControls); + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextFileService private readonly textFileService: ITextFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + ) { + super(); + + this._register(autorun(reader => { + const visibleTextEditorControls = this._visibleTextEditorControls.read(reader); + + // Dispose models for editors that are not visible + for (const [uri, dirtyDiffModel] of this._models) { + const textEditorControl = visibleTextEditorControls + .find(editor => isCodeEditor(editor) && + this.uriIdentityService.extUri.isEqual(editor.getModel()?.uri, uri)); + + if (textEditorControl) { + continue; + } + + dirtyDiffModel.dispose(); + this._models.delete(uri); + } + })); + } + + getOrCreateModel(uri: URI): DirtyDiffModel | undefined { + let model = this._models.get(uri); + if (!model) { + const textFileModel = this.textFileService.files.get(uri); + if (!textFileModel?.isResolved()) { + return undefined; + } + + model = this.instantiationService.createInstance(DirtyDiffModel, textFileModel); + this._models.set(uri, model); + } + + return model; + } +} + +registerSingleton(IDirtyDiffModelService, DirtyDiffModelService, InstantiationType.Delayed); diff --git a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts new file mode 100644 index 00000000000..02dc22483d4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/84899 + + export enum TextEditorDiffKind { + Addition = 1, + Deletion = 2, + Modification = 3 + } + + export interface TextEditorDiff { + readonly originalStartLineNumber: number; + readonly originalEndLineNumber: number; + readonly modifiedStartLineNumber: number; + readonly modifiedEndLineNumber: number; + readonly kind: TextEditorDiffKind; + } + + export interface TextEditorDiffInformation { + readonly documentVersion: number; + readonly original: Uri | undefined; + readonly modified: Uri | undefined; + readonly diff: readonly TextEditorDiff[]; + } + + export interface TextEditorDiffInformationChangeEvent { + readonly textEditor: TextEditor; + readonly diffInformation: TextEditorDiffInformation | undefined; + } + + export interface TextEditor { + readonly diffInformation: TextEditorDiffInformation | undefined; + } + + export namespace window { + export const onDidChangeTextEditorDiffInformation: Event; + } + +}