Skip to content

Commit 3004307

Browse files
authored
TextEditorDiffInformation API proposal (microsoft#233896)
* WIP - initial implementation * Introduce the diff model service * Remove code that is not needed * Handle DiffEditor * Performance optimization * Refactor code * More cleanup (V1) * More cleanup (V2) * More cleanup (V2.1) * Pull request feedback * Remove debugging statements * Update mock proxy to fix tests * Add proposed api check
1 parent 853676d commit 3004307

File tree

14 files changed

+338
-9
lines changed

14 files changed

+338
-9
lines changed

extensions/git/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"scmValidation",
3535
"tabInputMultiDiff",
3636
"tabInputTextMerge",
37+
"textEditorDiffInformation",
3738
"timeline"
3839
],
3940
"categories": [

extensions/git/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts",
2121
"../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts",
2222
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",
23+
"../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts",
2324
"../../src/vscode-dts/vscode.proposed.timeline.d.ts",
2425
"../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts",
2526
"../types/lib.textEncoder.d.ts"

src/vs/platform/editor/common/editor.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { equals } from '../../../base/common/arrays.js';
67
import { IDisposable } from '../../../base/common/lifecycle.js';
78
import { URI } from '../../../base/common/uri.js';
9+
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
810

911
export interface IResolvableEditorModel extends IDisposable {
1012

@@ -376,3 +378,29 @@ export interface ITextEditorOptions extends IEditorOptions {
376378
*/
377379
selectionSource?: TextEditorSelectionSource | string;
378380
}
381+
382+
export type ITextEditorDiff = [
383+
originalStartLineNumber: number,
384+
originalEndLineNumber: number,
385+
modifiedStartLineNumber: number,
386+
modifiedEndLineNumber: number
387+
];
388+
389+
export interface ITextEditorDiffInformation {
390+
readonly documentVersion: number;
391+
readonly original: URI | undefined;
392+
readonly modified: URI | undefined;
393+
readonly diff: readonly ITextEditorDiff[];
394+
}
395+
396+
export function isTextEditorDiffInformationEqual(
397+
uriIdentityService: IUriIdentityService,
398+
diff1: ITextEditorDiffInformation | undefined,
399+
diff2: ITextEditorDiffInformation | undefined): boolean {
400+
return diff1?.documentVersion === diff2?.documentVersion &&
401+
uriIdentityService.extUri.isEqual(diff1?.original, diff2?.original) &&
402+
uriIdentityService.extUri.isEqual(diff1?.modified, diff2?.modified) &&
403+
equals<ITextEditorDiff>(diff1?.diff, diff2?.diff, (a, b) => {
404+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
405+
});
406+
}

src/vs/platform/extensions/common/extensionsApiProposals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ const _allApiProposals = {
364364
testRelatedCode: {
365365
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts',
366366
},
367+
textEditorDiffInformation: {
368+
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts',
369+
},
367370
textSearchComplete2: {
368371
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchComplete2.d.ts',
369372
},

src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { diffSets, diffMaps } from '../../../base/common/collections.js';
3232
import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js';
3333
import { ViewContainerLocation } from '../../common/views.js';
3434
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
35+
import { IDirtyDiffModelService } from '../../contrib/scm/browser/diff.js';
3536

3637

3738
class TextEditorSnapshot {
@@ -296,14 +297,15 @@ export class MainThreadDocumentsAndEditors {
296297
@IUriIdentityService uriIdentityService: IUriIdentityService,
297298
@IClipboardService private readonly _clipboardService: IClipboardService,
298299
@IPathService pathService: IPathService,
299-
@IConfigurationService configurationService: IConfigurationService
300+
@IConfigurationService configurationService: IConfigurationService,
301+
@IDirtyDiffModelService dirtyDiffModelService: IDirtyDiffModelService
300302
) {
301303
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors);
302304

303305
this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService));
304306
extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments);
305307

306-
this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService));
308+
this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService, dirtyDiffModelService, uriIdentityService));
307309
extHostContext.set(MainContext.MainThreadTextEditors, this._mainThreadEditors);
308310

309311
// It is expected that the ctor of the state computer calls our `_onDelta`.

src/vs/workbench/api/browser/mainThreadEditors.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ISelection } from '../../../editor/common/core/selection.js';
1313
import { IDecorationOptions, IDecorationRenderOptions } from '../../../editor/common/editorCommon.js';
1414
import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js';
1515
import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
16-
import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution } from '../../../platform/editor/common/editor.js';
16+
import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution, ITextEditorDiffInformation, isTextEditorDiffInformationEqual, ITextEditorDiff } from '../../../platform/editor/common/editor.js';
1717
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
1818
import { MainThreadTextEditor } from './mainThreadEditor.js';
1919
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';
2929
import { getCodeEditor, ICodeEditor } from '../../../editor/browser/editorBrowser.js';
3030
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
3131
import { DirtyDiffContribution } from '../../contrib/scm/browser/dirtydiffDecorator.js';
32+
import { IDirtyDiffModelService } from '../../contrib/scm/browser/diff.js';
33+
import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js';
34+
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
3235

3336
export interface IMainThreadEditorLocator {
3437
getEditor(id: string): MainThreadTextEditor | undefined;
@@ -53,7 +56,9 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape {
5356
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
5457
@IEditorService private readonly _editorService: IEditorService,
5558
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
56-
@IConfigurationService private readonly _configurationService: IConfigurationService
59+
@IConfigurationService private readonly _configurationService: IConfigurationService,
60+
@IDirtyDiffModelService private readonly _dirtyDiffModelService: IDirtyDiffModelService,
61+
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService
5762
) {
5863
this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT);
5964
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors);
@@ -87,6 +92,12 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape {
8792
this._proxy.$acceptEditorPropertiesChanged(id, data);
8893
}));
8994

95+
const diffInformationObs = this._getTextEditorDiffInformation(textEditor);
96+
toDispose.push(autorun(reader => {
97+
const diffInformation = diffInformationObs.read(reader);
98+
this._proxy.$acceptEditorDiffInformation(id, diffInformation);
99+
}));
100+
90101
this._textEditorsListenersMap[id] = toDispose;
91102
}
92103

@@ -116,6 +127,75 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape {
116127
return result;
117128
}
118129

130+
private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor): IObservable<ITextEditorDiffInformation | undefined> {
131+
const codeEditor = textEditor.getCodeEditor();
132+
if (!codeEditor) {
133+
return constObservable(undefined);
134+
}
135+
136+
// Check if the TextModel belongs to a diff editor
137+
const diffEditors = this._codeEditorService.listDiffEditors();
138+
const [diffEditor] = diffEditors.filter(d =>
139+
d.getOriginalEditor().getId() === codeEditor.getId() ||
140+
d.getModifiedEditor().getId() === codeEditor.getId());
141+
142+
const codeEditorTextModelObs = !diffEditor ?
143+
observableFromEvent(this, codeEditor.onDidChangeModel, () => codeEditor.getModel()) :
144+
observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel()?.modified);
145+
146+
const dirtyDiffModelObs = derived(reader => {
147+
const codeEditorTextModel = codeEditorTextModelObs.read(reader);
148+
return codeEditorTextModel ? this._dirtyDiffModelService.getOrCreateModel(codeEditorTextModel.uri) : undefined;
149+
});
150+
151+
const scmQuickDiffChangesObs = derived(reader => {
152+
const dirtyDiffModel = dirtyDiffModelObs.read(reader);
153+
if (!dirtyDiffModel) {
154+
return constObservable(undefined);
155+
}
156+
157+
return observableFromEvent(this, dirtyDiffModel.onDidChange, e => {
158+
const scmQuickDiff = dirtyDiffModel.quickDiffs.find(diff => diff.isSCM === true);
159+
if (!e || !scmQuickDiff) {
160+
return undefined;
161+
}
162+
163+
return {
164+
originalResource: scmQuickDiff.originalResource,
165+
changes: e.changes
166+
.filter(change => change.label === scmQuickDiff.label)
167+
.map(change => change.change)
168+
};
169+
});
170+
});
171+
172+
return derivedOpts({
173+
owner: this,
174+
equalsFn: (diff1, diff2) => isTextEditorDiffInformationEqual(this._uriIdentityService, diff1, diff2)
175+
}, reader => {
176+
const codeEditorTextModel = codeEditorTextModelObs.read(reader);
177+
const scmQuickDiffChanges = scmQuickDiffChangesObs.read(reader).read(reader);
178+
if (!codeEditorTextModel || !scmQuickDiffChanges) {
179+
return undefined;
180+
}
181+
182+
const diff: ITextEditorDiff[] = scmQuickDiffChanges.changes
183+
.map(change => [
184+
change.originalStartLineNumber,
185+
change.originalEndLineNumber,
186+
change.modifiedStartLineNumber,
187+
change.modifiedEndLineNumber
188+
]);
189+
190+
return {
191+
documentVersion: codeEditorTextModel.getVersionId(),
192+
original: scmQuickDiffChanges.originalResource,
193+
modified: codeEditorTextModel.uri,
194+
diff
195+
};
196+
});
197+
}
198+
119199
// --- from extension host process
120200

121201
async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string | undefined> {

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
723723
onDidChangeTextEditorViewColumn(listener, thisArg?, disposables?) {
724724
return _asExtensionEvent(extHostEditors.onDidChangeTextEditorViewColumn)(listener, thisArg, disposables);
725725
},
726+
onDidChangeTextEditorDiffInformation(listener, thisArg?, disposables?) {
727+
checkProposedApiEnabled(extension, 'textEditorDiffInformation');
728+
return _asExtensionEvent(extHostEditors.onDidChangeTextEditorDiffInformation)(listener, thisArg, disposables);
729+
},
726730
onDidCloseTerminal(listener, thisArg?, disposables?) {
727731
return _asExtensionEvent(extHostTerminalService.onDidCloseTerminal)(listener, thisArg, disposables);
728732
},
@@ -1661,6 +1665,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
16611665
TextEdit: extHostTypes.TextEdit,
16621666
SnippetTextEdit: extHostTypes.SnippetTextEdit,
16631667
TextEditorCursorStyle: TextEditorCursorStyle,
1668+
TextEditorDiffKind: extHostTypes.TextEditorDiffKind,
16641669
TextEditorLineNumbersStyle: extHostTypes.TextEditorLineNumbersStyle,
16651670
TextEditorRevealType: extHostTypes.TextEditorRevealType,
16661671
TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind,

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,6 +1817,21 @@ export interface ITextEditorAddData {
18171817
export interface ITextEditorPositionData {
18181818
[id: string]: EditorGroupColumn;
18191819
}
1820+
1821+
export type ITextEditorDiff = [
1822+
originalStartLineNumber: number,
1823+
originalEndLineNumber: number,
1824+
modifiedStartLineNumber: number,
1825+
modifiedEndLineNumber: number
1826+
];
1827+
1828+
export interface ITextEditorDiffInformation {
1829+
readonly documentVersion: number;
1830+
readonly original: UriComponents | undefined;
1831+
readonly modified: UriComponents | undefined;
1832+
readonly diff: readonly ITextEditorDiff[];
1833+
}
1834+
18201835
export interface IEditorPropertiesChangeData {
18211836
options: IResolvedTextEditorConfiguration | null;
18221837
selections: ISelectionChangeEvent | null;
@@ -1830,6 +1845,7 @@ export interface ISelectionChangeEvent {
18301845
export interface ExtHostEditorsShape {
18311846
$acceptEditorPropertiesChanged(id: string, props: IEditorPropertiesChangeData): void;
18321847
$acceptEditorPositionData(data: ITextEditorPositionData): void;
1848+
$acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation | undefined): void;
18331849
}
18341850

18351851
export interface IDocumentsAndEditorsDelta {

src/vs/workbench/api/common/extHostTextEditor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export class ExtHostTextEditor {
412412
private _viewColumn: vscode.ViewColumn | undefined;
413413
private _disposed: boolean = false;
414414
private _hasDecorationsForKey = new Set<string>();
415+
private _diffInformation: vscode.TextEditorDiffInformation | undefined;
415416

416417
readonly value: vscode.TextEditor;
417418

@@ -465,6 +466,9 @@ export class ExtHostTextEditor {
465466
set visibleRanges(_value: Range[]) {
466467
throw new ReadonlyError('visibleRanges');
467468
},
469+
get diffInformation() {
470+
return that._diffInformation;
471+
},
468472
// --- options
469473
get options(): vscode.TextEditorOptions {
470474
return that._options.value;
@@ -600,6 +604,11 @@ export class ExtHostTextEditor {
600604
this._selections = selections;
601605
}
602606

607+
_acceptDiffInformation(diffInformation: vscode.TextEditorDiffInformation | undefined): void {
608+
ok(!this._disposed);
609+
this._diffInformation = diffInformation;
610+
}
611+
603612
private async _trySetSelection(): Promise<vscode.TextEditor | null | undefined> {
604613
const selection = this._selections.map(TypeConverters.Selection.from);
605614
await this._runOnProxy(() => this._proxy.$trySetSelections(this.id, selection));

src/vs/workbench/api/common/extHostTextEditors.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
import * as arrays from '../../../base/common/arrays.js';
77
import { Emitter, Event } from '../../../base/common/event.js';
88
import { Disposable } from '../../../base/common/lifecycle.js';
9+
import { URI } from '../../../base/common/uri.js';
910
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
10-
import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from './extHost.protocol.js';
11+
import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorDiffInformation, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from './extHost.protocol.js';
1112
import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';
1213
import { ExtHostTextEditor, TextEditorDecorationType } from './extHostTextEditor.js';
1314
import * as TypeConverters from './extHostTypeConverters.js';
14-
import { TextEditorSelectionChangeKind } from './extHostTypes.js';
15+
import { TextEditorSelectionChangeKind, TextEditorDiffKind } from './extHostTypes.js';
1516
import * as vscode from 'vscode';
1617

1718
export class ExtHostEditors extends Disposable implements ExtHostEditorsShape {
@@ -20,13 +21,15 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape {
2021
private readonly _onDidChangeTextEditorOptions = new Emitter<vscode.TextEditorOptionsChangeEvent>();
2122
private readonly _onDidChangeTextEditorVisibleRanges = new Emitter<vscode.TextEditorVisibleRangesChangeEvent>();
2223
private readonly _onDidChangeTextEditorViewColumn = new Emitter<vscode.TextEditorViewColumnChangeEvent>();
24+
private readonly _onDidChangeTextEditorDiffInformation = new Emitter<vscode.TextEditorDiffInformationChangeEvent>();
2325
private readonly _onDidChangeActiveTextEditor = new Emitter<vscode.TextEditor | undefined>();
2426
private readonly _onDidChangeVisibleTextEditors = new Emitter<readonly vscode.TextEditor[]>();
2527

2628
readonly onDidChangeTextEditorSelection: Event<vscode.TextEditorSelectionChangeEvent> = this._onDidChangeTextEditorSelection.event;
2729
readonly onDidChangeTextEditorOptions: Event<vscode.TextEditorOptionsChangeEvent> = this._onDidChangeTextEditorOptions.event;
2830
readonly onDidChangeTextEditorVisibleRanges: Event<vscode.TextEditorVisibleRangesChangeEvent> = this._onDidChangeTextEditorVisibleRanges.event;
2931
readonly onDidChangeTextEditorViewColumn: Event<vscode.TextEditorViewColumnChangeEvent> = this._onDidChangeTextEditorViewColumn.event;
32+
readonly onDidChangeTextEditorDiffInformation: Event<vscode.TextEditorDiffInformationChangeEvent> = this._onDidChangeTextEditorDiffInformation.event;
3033
readonly onDidChangeActiveTextEditor: Event<vscode.TextEditor | undefined> = this._onDidChangeActiveTextEditor.event;
3134
readonly onDidChangeVisibleTextEditors: Event<readonly vscode.TextEditor[]> = this._onDidChangeVisibleTextEditors.event;
3235

@@ -157,6 +160,53 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape {
157160
}
158161
}
159162

163+
$acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation | undefined): void {
164+
const textEditor = this._extHostDocumentsAndEditors.getEditor(id);
165+
if (!textEditor) {
166+
throw new Error('unknown text editor');
167+
}
168+
169+
if (!diffInformation) {
170+
textEditor._acceptDiffInformation(undefined);
171+
this._onDidChangeTextEditorDiffInformation.fire({
172+
textEditor: textEditor.value,
173+
diffInformation: undefined
174+
});
175+
return;
176+
}
177+
178+
const original = URI.revive(diffInformation.original);
179+
const modified = URI.revive(diffInformation.modified);
180+
181+
const diff = diffInformation.diff.map(diff => {
182+
const [originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber] = diff;
183+
184+
const kind = originalEndLineNumber === 0 ? TextEditorDiffKind.Addition :
185+
modifiedEndLineNumber === 0 ? TextEditorDiffKind.Deletion : TextEditorDiffKind.Modification;
186+
187+
return {
188+
originalStartLineNumber,
189+
originalEndLineNumber,
190+
modifiedStartLineNumber,
191+
modifiedEndLineNumber,
192+
kind
193+
} satisfies vscode.TextEditorDiff;
194+
});
195+
196+
const result = Object.freeze({
197+
documentVersion: diffInformation.documentVersion,
198+
original,
199+
modified,
200+
diff
201+
});
202+
203+
textEditor._acceptDiffInformation(result);
204+
this._onDidChangeTextEditorDiffInformation.fire({
205+
textEditor: textEditor.value,
206+
diffInformation: result
207+
});
208+
}
209+
160210
getDiffInformation(id: string): Promise<vscode.LineChange[]> {
161211
return Promise.resolve(this._proxy.$getDiffInformation(id));
162212
}

0 commit comments

Comments
 (0)