From 34d31f4b9e7be13cddbe9a3c085b81595d91fcb9 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 9 May 2022 14:09:00 +0200 Subject: [PATCH 001/270] merge editor kick off --- build/lib/i18n.resources.json | 4 + .../mergeEditor/browser/media/mergeEditor.css | 4 + .../browser/mergeEditor.contribution.ts | 61 ++++++++++ .../mergeEditor/browser/mergeEditor.ts | 110 ++++++++++++++++++ .../mergeEditor/browser/mergeEditorInput.ts | 93 +++++++++++++++ .../mergeEditor/browser/mergeEditorModel.ts | 20 ++++ .../browser/mergeEditorSerializer.ts | 31 +++++ src/vs/workbench/workbench.common.main.ts | 3 + 8 files changed, 326 insertions(+) create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 06bfbb2b506..246384ae15a 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -118,6 +118,10 @@ "name": "vs/workbench/contrib/markers", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/mergeEditor", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/localizations", "project": "vscode-workbench" diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css new file mode 100644 index 00000000000..a4a092d8349 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -0,0 +1,4 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts new file mode 100644 index 00000000000..27d3d7c8c9b --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { localize } from 'vs/nls'; +import { URI } from 'vs/base/common/uri'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditor'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { MergeEditorSerializer } from './mergeEditorSerializer'; + + +//#region Editor Descriptior + +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + MergeEditor, + MergeEditor.ID, + localize('name', "Merge Editor") + ), + [ + new SyncDescriptor(MergeEditorInput) + ] +); + +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( + MergeEditorInput.ID, + MergeEditorSerializer +); + +registerAction2(class Foo extends Action2 { + + constructor() { + super({ + id: 'testMergeEditor', + title: '3wm', + f1: true + }); + } + + run(accessor: ServicesAccessor, ...args: any[]): void { + const instaService = accessor.get(IInstantiationService); + const input = instaService.createInstance( + MergeEditorInput, + URI.file('/Users/jrieken/Code/_samples/abc/test.md'), + URI.file('/Users/jrieken/Code/_samples/abc/test.md'), + URI.file('/Users/jrieken/Code/_samples/abc/test.md'), + URI.file('/Users/jrieken/Code/_samples/abc/test.md'), + ); + accessor.get(IEditorService).openEditor(input); + } + +}); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts new file mode 100644 index 00000000000..54f3b6dadb0 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/mergeEditor'; +import { Dimension, reset } from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Direction, Grid, IView, IViewSize, LayoutPriority } from 'vs/base/browser/ui/grid/grid'; +import { Sizing } from 'vs/base/browser/ui/splitview/splitview'; + + +class CodeEditorView implements IView { + + preferredWidth?: number | undefined; + preferredHeight?: number | undefined; + + element: HTMLElement = document.createElement('div'); + + minimumWidth: number = 10; + maximumWidth: number = Number.MAX_SAFE_INTEGER; + minimumHeight: number = 10; + maximumHeight: number = Number.MAX_SAFE_INTEGER; + priority?: LayoutPriority | undefined; + snap?: boolean | undefined; + + private readonly _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + constructor(text: string) { + this.element.innerText = text; + } + + layout(width: number, height: number, top: number, left: number): void { + this.element.style.width = `${width}px`; + this.element.style.height = `${height}px`; + this.element.style.top = `${top}px`; + this.element.style.left = `${left}px`; + } + +} + +export class MergeEditor extends EditorPane { + + static readonly ID = 'mergeEditor'; + + private readonly _sessionDisposables = new DisposableStore(); + + private _grid!: Grid; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IThemeService themeService: IThemeService, + ) { + super(MergeEditor.ID, telemetryService, themeService, storageService); + } + + override dispose(): void { + this._sessionDisposables.dispose(); + super.dispose(); + } + + protected createEditor(parent: HTMLElement): void { + + + const inputOneView = new CodeEditorView('one'); + const inputTwoView = new CodeEditorView('two'); + const inputResultView = new CodeEditorView('result'); + + this._grid = new Grid(inputResultView); + + this._grid.addView(inputOneView, Sizing.Distribute, inputResultView, Direction.Up); + this._grid.addView(inputTwoView, Sizing.Distribute, inputOneView, Direction.Right); + reset(parent, this._grid.element); + } + + layout(dimension: Dimension): void { + this._grid.layout(dimension.width, dimension.height); + } + + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + if (!(input instanceof MergeEditorInput)) { + throw new BugIndicatingError('ONLY MergeEditorInput is supported'); + } + await super.setInput(input, options, context, token); + console.trace('mergeEditor@53'); + this._sessionDisposables.clear(); + // const model = await input.resolve(); + // if (token.isCancellationRequested) { + // return; + // } + } + + override clearInput(): void { + console.trace('mergeEditor@66'); + } + +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts new file mode 100644 index 00000000000..9a3c69d7cc2 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel'; + +export interface MergeEditorInputJSON { + anchestor: URI; + inputOne: URI; + inputTwo: URI; + result: URI; +} + +export class MergeEditorInput extends EditorInput { + + static readonly ID = 'mergeEditor.Input'; + + private _model?: MergeEditorModel; + + constructor( + private readonly _anchestor: URI, + private readonly _inputOne: URI, + private readonly _inputTwo: URI, + private readonly _result: URI, + @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextModelService private readonly _textModelService: ITextModelService, + ) { + super(); + } + + override dispose(): void { + super.dispose(); + } + + get typeId(): string { + return MergeEditorInput.ID; + } + + get resource(): URI | undefined { + return this._result; + } + + override async resolve(): Promise { + if (!this._model) { + + const anchestor = await this._textModelService.createModelReference(this._anchestor); + const inputOne = await this._textModelService.createModelReference(this._inputOne); + const inputTwo = await this._textModelService.createModelReference(this._inputTwo); + const result = await this._textModelService.createModelReference(this._result); + + this._model = this._instaService.createInstance( + MergeEditorModel, + anchestor.object.textEditorModel, + inputOne.object.textEditorModel, + inputTwo.object.textEditorModel, + result.object.textEditorModel + ); + + this._store.add(this._model); + this._store.add(anchestor); + this._store.add(inputOne); + this._store.add(inputTwo); + this._store.add(result); + } + return this._model; + } + + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { + if (!(otherInput instanceof MergeEditorInput)) { + return false; + } + return isEqual(this._anchestor, otherInput._anchestor) + && isEqual(this._inputOne, otherInput._inputOne) + && isEqual(this._inputTwo, otherInput._inputTwo) + && isEqual(this._result, otherInput._result); + } + + toJSON(): MergeEditorInputJSON { + return { + anchestor: this._anchestor, + inputOne: this._inputOne, + inputTwo: this._inputTwo, + result: this._result, + }; + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts new file mode 100644 index 00000000000..172a5c8e1a0 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextModel } from 'vs/editor/common/model'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; + +export class MergeEditorModel extends EditorModel { + + constructor( + readonly anchestor: ITextModel, + readonly inputOne: ITextModel, + readonly inputTwo: ITextModel, + readonly result: ITextModel, + ) { + super(); + } + +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts new file mode 100644 index 00000000000..2480f0950ba --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { parse } from 'vs/base/common/marshalling'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorSerializer } from 'vs/workbench/common/editor'; +import { MergeEditorInput, MergeEditorInputJSON } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; + +export class MergeEditorSerializer implements IEditorSerializer { + + canSerialize(): boolean { + return true; + } + + serialize(editor: MergeEditorInput): string { + return JSON.stringify(editor.toJSON()); + } + + deserialize(instantiationService: IInstantiationService, raw: string): MergeEditorInput | undefined { + try { + const data = parse(raw); + return instantiationService.createInstance(MergeEditorInput, data.anchestor, data.inputOne, data.inputTwo, data.result); + } catch (err) { + onUnexpectedError(err); + return undefined; + } + } +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index bf87db19d53..74ebaf956ec 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -210,6 +210,9 @@ import 'vs/workbench/contrib/debug/browser/debugViewlet'; // Markers import 'vs/workbench/contrib/markers/browser/markers.contribution'; +// Merge Editor +import 'vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution'; + // Comments import 'vs/workbench/contrib/comments/browser/comments.contribution'; From 6435afb931f5f1ac496e115c91fb0a1ed6ead82a Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 9 May 2022 15:26:23 +0200 Subject: [PATCH 002/270] remove trace, resolve input to model, log model --- .../workbench/contrib/mergeEditor/browser/mergeEditor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 54f3b6dadb0..4ce0cc0eb12 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -74,7 +74,6 @@ export class MergeEditor extends EditorPane { protected createEditor(parent: HTMLElement): void { - const inputOneView = new CodeEditorView('one'); const inputTwoView = new CodeEditorView('two'); const inputResultView = new CodeEditorView('result'); @@ -95,16 +94,17 @@ export class MergeEditor extends EditorPane { throw new BugIndicatingError('ONLY MergeEditorInput is supported'); } await super.setInput(input, options, context, token); - console.trace('mergeEditor@53'); + this._sessionDisposables.clear(); - // const model = await input.resolve(); + const model = await input.resolve(); + console.log(model); // if (token.isCancellationRequested) { // return; // } } override clearInput(): void { - console.trace('mergeEditor@66'); + } } From 26a86d4a42da140bb268ee16f1ae1f04b1827b4b Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 9 May 2022 15:53:22 +0200 Subject: [PATCH 003/270] Loads paths from command argument. --- .../browser/mergeEditor.contribution.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index 27d3d7c8c9b..c1fc3b0fbf2 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -45,17 +45,39 @@ registerAction2(class Foo extends Action2 { f1: true }); } - run(accessor: ServicesAccessor, ...args: any[]): void { + const validatedArgs = ITestMergeEditorArgs.validate(args[0]); + + function normalize(uri: URI | string): URI { + if (typeof uri === 'string') { + return URI.parse(uri); + } else { + return uri; + } + } + const instaService = accessor.get(IInstantiationService); const input = instaService.createInstance( MergeEditorInput, - URI.file('/Users/jrieken/Code/_samples/abc/test.md'), - URI.file('/Users/jrieken/Code/_samples/abc/test.md'), - URI.file('/Users/jrieken/Code/_samples/abc/test.md'), - URI.file('/Users/jrieken/Code/_samples/abc/test.md'), + normalize(validatedArgs.ancestor), + normalize(validatedArgs.input1), + normalize(validatedArgs.input2), + normalize(validatedArgs.output), ); accessor.get(IEditorService).openEditor(input); } }); + +namespace ITestMergeEditorArgs { + export function validate(args: any): ITestMergeEditorArgs { + return args as ITestMergeEditorArgs; + } +} + +interface ITestMergeEditorArgs { + ancestor: URI | string; + input1: URI | string; + input2: URI | string; + output: URI | string; +} From 6a051dc03a7763ed75fbc746b7bb16ed84aef9eb Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 9 May 2022 16:17:29 +0200 Subject: [PATCH 004/270] Creates editors & MVP of scroll synchronization. --- .../mergeEditor/browser/mergeEditor.ts | 109 +++++++++++------- 1 file changed, 70 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 4ce0cc0eb12..14950aef60d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -19,37 +19,10 @@ import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/merge import { DisposableStore } from 'vs/base/common/lifecycle'; import { Direction, Grid, IView, IViewSize, LayoutPriority } from 'vs/base/browser/ui/grid/grid'; import { Sizing } from 'vs/base/browser/ui/splitview/splitview'; - - -class CodeEditorView implements IView { - - preferredWidth?: number | undefined; - preferredHeight?: number | undefined; - - element: HTMLElement = document.createElement('div'); - - minimumWidth: number = 10; - maximumWidth: number = Number.MAX_SAFE_INTEGER; - minimumHeight: number = 10; - maximumHeight: number = Number.MAX_SAFE_INTEGER; - priority?: LayoutPriority | undefined; - snap?: boolean | undefined; - - private readonly _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - constructor(text: string) { - this.element.innerText = text; - } - - layout(width: number, height: number, top: number, left: number): void { - this.element.style.width = `${width}px`; - this.element.style.height = `${height}px`; - this.element.style.top = `${top}px`; - this.element.style.left = `${left}px`; - } - -} +import { ITextModel } from 'vs/editor/common/model'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ScrollType } from 'vs/editor/common/editorCommon'; export class MergeEditor extends EditorPane { @@ -59,12 +32,28 @@ export class MergeEditor extends EditorPane { private _grid!: Grid; + private readonly inputOneView = this.instantiation.createInstance(CodeEditorView); + private readonly inputTwoView = this.instantiation.createInstance(CodeEditorView); + private readonly inputResultView = this.instantiation.createInstance(CodeEditorView); + constructor( + @IInstantiationService private readonly instantiation: IInstantiationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService, ) { super(MergeEditor.ID, telemetryService, themeService, storageService); + + this._store.add(this.inputOneView.editor.onDidScrollChange(c => { + if (c.scrollTopChanged) { + this.inputTwoView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + } + })); + this._store.add(this.inputTwoView.editor.onDidScrollChange(c => { + if (c.scrollTopChanged) { + this.inputOneView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + } + })); } override dispose(): void { @@ -73,15 +62,10 @@ export class MergeEditor extends EditorPane { } protected createEditor(parent: HTMLElement): void { + this._grid = new Grid(this.inputResultView); - const inputOneView = new CodeEditorView('one'); - const inputTwoView = new CodeEditorView('two'); - const inputResultView = new CodeEditorView('result'); - - this._grid = new Grid(inputResultView); - - this._grid.addView(inputOneView, Sizing.Distribute, inputResultView, Direction.Up); - this._grid.addView(inputTwoView, Sizing.Distribute, inputOneView, Direction.Right); + this._grid.addView(this.inputOneView, Sizing.Distribute, this.inputResultView, Direction.Up); + this._grid.addView(this.inputTwoView, Sizing.Distribute, this.inputOneView, Direction.Right); reset(parent, this._grid.element); } @@ -97,6 +81,11 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); + + this.inputOneView.setModel(model.inputOne); + this.inputTwoView.setModel(model.inputTwo); + this.inputResultView.setModel(model.result); + console.log(model); // if (token.isCancellationRequested) { // return; @@ -108,3 +97,45 @@ export class MergeEditor extends EditorPane { } } + +class CodeEditorView implements IView { + preferredWidth?: number | undefined; + preferredHeight?: number | undefined; + + element: HTMLElement = document.createElement('div'); + + minimumWidth: number = 10; + maximumWidth: number = Number.MAX_SAFE_INTEGER; + minimumHeight: number = 10; + maximumHeight: number = Number.MAX_SAFE_INTEGER; + priority?: LayoutPriority | undefined; + snap?: boolean | undefined; + + private readonly _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + public readonly editor = this.instantiationService.createInstance( + CodeEditorWidget, + this.element, + { minimap: { enabled: false } }, + {} + ); + + constructor( + @IInstantiationService + private readonly instantiationService: IInstantiationService + ) { + } + + public setModel(model: ITextModel | undefined): void { + this.editor.setModel(model); + } + + layout(width: number, height: number, top: number, left: number): void { + this.element.style.width = `${width}px`; + this.element.style.height = `${height}px`; + this.element.style.top = `${top}px`; + this.element.style.left = `${left}px`; + this.editor.layout({ width, height }); + } +} From 52a634dd87122c021f2ae6645d27fc043463442e Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 9 May 2022 16:25:11 +0200 Subject: [PATCH 005/270] Synchronize scrolling of all three editors. --- .../mergeEditor/browser/mergeEditor.ts | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 14950aef60d..c7c4d8e9358 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -44,14 +44,29 @@ export class MergeEditor extends EditorPane { ) { super(MergeEditor.ID, telemetryService, themeService, storageService); + const reentrancyBarrier = new ReentrancyBarrier(); this._store.add(this.inputOneView.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { - this.inputTwoView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + reentrancyBarrier.runExclusively(() => { + this.inputTwoView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.inputResultView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + }); } })); this._store.add(this.inputTwoView.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { - this.inputOneView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + reentrancyBarrier.runExclusively(() => { + this.inputOneView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.inputResultView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + }); + } + })); + this._store.add(this.inputResultView.editor.onDidScrollChange(c => { + if (c.scrollTopChanged) { + reentrancyBarrier.runExclusively(() => { + this.inputOneView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.inputTwoView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + }); } })); } @@ -139,3 +154,19 @@ class CodeEditorView implements IView { this.editor.layout({ width, height }); } } + +class ReentrancyBarrier { + private isActive = false; + + public runExclusively(fn: () => void): void { + if (this.isActive) { + return; + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } +} From beeb2e420210bb4673d239b85c7a59fc7b692dba Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 09:37:43 +0200 Subject: [PATCH 006/270] save and dirty state for 3wm --- .../mergeEditor/browser/mergeEditorInput.ts | 63 +++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 9a3c69d7cc2..b50f8f92516 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -3,13 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IUntypedEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export interface MergeEditorInputJSON { anchestor: URI; @@ -18,11 +25,12 @@ export interface MergeEditorInputJSON { result: URI; } -export class MergeEditorInput extends EditorInput { +export class MergeEditorInput extends AbstractTextResourceEditorInput { static readonly ID = 'mergeEditor.Input'; private _model?: MergeEditorModel; + private _outTextModel?: ITextFileEditorModel; constructor( private readonly _anchestor: URI, @@ -31,8 +39,32 @@ export class MergeEditorInput extends EditorInput { private readonly _result: URI, @IInstantiationService private readonly _instaService: IInstantiationService, @ITextModelService private readonly _textModelService: ITextModelService, + @IEditorService editorService: IEditorService, + @ITextFileService textFileService: ITextFileService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService ) { - super(); + super(_result, undefined, editorService, textFileService, labelService, fileService); + + const modelListener = new DisposableStore(); + const handleDidCreate = (model: ITextFileEditorModel) => { + // TODO@jrieken copied from fileEditorInput.ts + if (isEqual(_result, model.resource)) { + modelListener.clear(); + this._outTextModel = model; + modelListener.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + modelListener.add(model.onDidSaveError(() => this._onDidChangeDirty.fire())); + + modelListener.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + + modelListener.add(model.onWillDispose(() => { + this._outTextModel = undefined; + modelListener.clear(); + })); + } + }; + textFileService.files.onDidCreate(handleDidCreate, this, modelListener); + textFileService.files.models.forEach(handleDidCreate); } override dispose(): void { @@ -43,11 +75,20 @@ export class MergeEditorInput extends EditorInput { return MergeEditorInput.ID; } - get resource(): URI | undefined { - return this._result; + override getName(): string { + return localize('name', "Merging: {0}", super.getName()); + } + + override get capabilities(): EditorInputCapabilities { + let result = EditorInputCapabilities.Singleton; + if (!this.fileService.hasProvider(this._result) || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + result |= EditorInputCapabilities.Readonly; + } + return result; } override async resolve(): Promise { + if (!this._model) { const anchestor = await this._textModelService.createModelReference(this._anchestor); @@ -68,6 +109,8 @@ export class MergeEditorInput extends EditorInput { this._store.add(inputOne); this._store.add(inputTwo); this._store.add(result); + + // result.object. } return this._model; } @@ -90,4 +133,14 @@ export class MergeEditorInput extends EditorInput { result: this._result, }; } + + // ---- FileEditorInput + + override isDirty(): boolean { + return Boolean(this._outTextModel?.isDirty()); + } + + + // implement get/set languageId + // implement get/set encoding } From 8614c14318abbc33bd759bf979d1f0df954b2c58 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 11:52:32 +0200 Subject: [PATCH 007/270] dispose model listener --- src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index b50f8f92516..cb5773e9f86 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -65,6 +65,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { }; textFileService.files.onDidCreate(handleDidCreate, this, modelListener); textFileService.files.models.forEach(handleDidCreate); + this._store.add(modelListener); } override dispose(): void { From 1cd79ce2d97edbf56c464040f707dcd8b53a25bb Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 11:55:51 +0200 Subject: [PATCH 008/270] add grid border color --- src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index c7c4d8e9358..e8f02b90ea2 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -23,6 +23,8 @@ import { ITextModel } from 'vs/editor/common/model'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ScrollType } from 'vs/editor/common/editorCommon'; +import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; +import { Color } from 'vs/base/common/color'; export class MergeEditor extends EditorPane { @@ -77,7 +79,7 @@ export class MergeEditor extends EditorPane { } protected createEditor(parent: HTMLElement): void { - this._grid = new Grid(this.inputResultView); + this._grid = new Grid(this.inputResultView, { styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent } }); this._grid.addView(this.inputOneView, Sizing.Distribute, this.inputResultView, Direction.Up); this._grid.addView(this.inputTwoView, Sizing.Distribute, this.inputOneView, Direction.Right); From 9bcc9a1d3e5798f4b494ce066cbe3a2e2a90f3a3 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 12:17:54 +0200 Subject: [PATCH 009/270] add title area above code editor, add css styles, little :lipstick: --- .../mergeEditor/browser/media/mergeEditor.css | 11 +++++ .../mergeEditor/browser/mergeEditor.ts | 42 ++++++++++++------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index a4a092d8349..2a1dd749cbb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -2,3 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .merge-editor .code-view>.title { + padding: 0 0 0 10px; + height: 30px; + display: flex; + align-content: center; +} + +.monaco-workbench .merge-editor .code-view>.title .monaco-icon-label { + margin: auto 0; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index e8f02b90ea2..7029d266c30 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -17,7 +17,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { BugIndicatingError } from 'vs/base/common/errors'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Direction, Grid, IView, IViewSize, LayoutPriority } from 'vs/base/browser/ui/grid/grid'; +import { Direction, Grid, IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { ITextModel } from 'vs/editor/common/model'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -25,6 +25,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ScrollType } from 'vs/editor/common/editorCommon'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { Color } from 'vs/base/common/color'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { localize } from 'vs/nls'; +import { ILabelService } from 'vs/platform/label/common/label'; export class MergeEditor extends EditorPane { @@ -40,6 +43,7 @@ export class MergeEditor extends EditorPane { constructor( @IInstantiationService private readonly instantiation: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService, @@ -79,6 +83,7 @@ export class MergeEditor extends EditorPane { } protected createEditor(parent: HTMLElement): void { + parent.classList.add('merge-editor'); this._grid = new Grid(this.inputResultView, { styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent } }); this._grid.addView(this.inputOneView, Sizing.Distribute, this.inputResultView, Direction.Up); @@ -99,14 +104,10 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); - this.inputOneView.setModel(model.inputOne); - this.inputTwoView.setModel(model.inputTwo); - this.inputResultView.setModel(model.result); + this.inputOneView.setModel(model.inputOne, localize('yours', 'Yours'), undefined); + this.inputTwoView.setModel(model.inputTwo, localize('theirs', 'Theirs',), undefined); + this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); - console.log(model); - // if (token.isCancellationRequested) { - // return; - // } } override clearInput(): void { @@ -116,36 +117,47 @@ export class MergeEditor extends EditorPane { } class CodeEditorView implements IView { - preferredWidth?: number | undefined; - preferredHeight?: number | undefined; + + // preferredWidth?: number | undefined; + // preferredHeight?: number | undefined; element: HTMLElement = document.createElement('div'); + private _titleElement = document.createElement('div'); + private _editorElement = document.createElement('div'); minimumWidth: number = 10; maximumWidth: number = Number.MAX_SAFE_INTEGER; minimumHeight: number = 10; maximumHeight: number = Number.MAX_SAFE_INTEGER; - priority?: LayoutPriority | undefined; - snap?: boolean | undefined; + // priority?: LayoutPriority | undefined; + // snap?: boolean | undefined; private readonly _onDidChange = new Emitter(); readonly onDidChange = this._onDidChange.event; + private _title = new IconLabel(this._titleElement, { supportIcons: true }); + public readonly editor = this.instantiationService.createInstance( CodeEditorWidget, - this.element, + this._editorElement, { minimap: { enabled: false } }, {} ); + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService ) { + this.element.classList.add('code-view'); + this._titleElement.classList.add('title'); + this.element.appendChild(this._titleElement); + this.element.appendChild(this._editorElement); } - public setModel(model: ITextModel | undefined): void { + public setModel(model: ITextModel, title: string, description: string | undefined): void { this.editor.setModel(model); + this._title.setLabel(title, description); } layout(width: number, height: number, top: number, left: number): void { @@ -153,7 +165,7 @@ class CodeEditorView implements IView { this.element.style.height = `${height}px`; this.element.style.top = `${top}px`; this.element.style.left = `${left}px`; - this.editor.layout({ width, height }); + this.editor.layout({ width, height: height - this._titleElement.clientHeight }); } } From 45abbdf08690a5011dae1eb87980eea005080595 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 12:32:54 +0200 Subject: [PATCH 010/270] ensure default implementation of `clearInput` runs, make sure editor can be re-opened... --- .../workbench/contrib/mergeEditor/browser/mergeEditor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 7029d266c30..8afe3dbd737 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -110,9 +110,13 @@ export class MergeEditor extends EditorPane { } - override clearInput(): void { + // override clearInput(): void { + // super.clearInput(); + // } - } + // protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + // console.log('VISISBLE', visible); + // } } From 44acc7887bc9869ffacf49d62a797ce3916bb86b Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 12:37:52 +0200 Subject: [PATCH 011/270] add floating btn for merge (all fake tho...) --- .../workbench/contrib/mergeEditor/browser/mergeEditor.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 8afe3dbd737..ac0265355b6 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -28,6 +28,7 @@ import { Color } from 'vs/base/common/color'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; export class MergeEditor extends EditorPane { @@ -75,6 +76,12 @@ export class MergeEditor extends EditorPane { }); } })); + + // TODO@jrieken make this proper: add menu id and allow extensions to contribute + const acceptBtn = this.instantiation.createInstance(FloatingClickWidget, this.inputResultView.editor, 'Accept Merge', ''); + acceptBtn.render(); + this._store.add(acceptBtn.onClick(() => { console.log('DO IT'); })); + this._store.add(acceptBtn); } override dispose(): void { From c7fa36480e452bc30b11061e56330d0b68e22f1a Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 12:53:19 +0200 Subject: [PATCH 012/270] implement `getControl` and `scopedContextKeyService` for better interaction with "outside", add `ICodeEditorViewOptions` and make yours/theirs readonly --- .../mergeEditor/browser/mergeEditor.ts | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index ac0265355b6..3a1e6b68079 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -12,7 +12,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { BugIndicatingError } from 'vs/base/common/errors'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; @@ -29,6 +29,8 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; export class MergeEditor extends EditorPane { @@ -36,11 +38,11 @@ export class MergeEditor extends EditorPane { private readonly _sessionDisposables = new DisposableStore(); - private _grid!: Grid; + private _grid!: Grid; - private readonly inputOneView = this.instantiation.createInstance(CodeEditorView); - private readonly inputTwoView = this.instantiation.createInstance(CodeEditorView); - private readonly inputResultView = this.instantiation.createInstance(CodeEditorView); + private readonly inputOneView = this.instantiation.createInstance(CodeEditorView, { readonly: true }); + private readonly inputTwoView = this.instantiation.createInstance(CodeEditorView, { readonly: true }); + private readonly inputResultView = this.instantiation.createInstance(CodeEditorView, { readonly: false }); constructor( @IInstantiationService private readonly instantiation: IInstantiationService, @@ -125,6 +127,27 @@ export class MergeEditor extends EditorPane { // console.log('VISISBLE', visible); // } + // ---- interact with "outside world" via `getControl`, `scopedContextKeyService` + + override getControl(): IEditorControl | undefined { + for (const view of [this.inputOneView, this.inputTwoView, this.inputResultView]) { + if (view.editor.hasWidgetFocus()) { + return view.editor; + } + } + return undefined; + } + + override get scopedContextKeyService(): IContextKeyService | undefined { + const control = this.getControl(); + return isCodeEditor(control) + ? control.invokeWithinContext(accessor => accessor.get(IContextKeyService)) + : undefined; + } +} + +interface ICodeEditorViewOptions { + readonly: boolean; } class CodeEditorView implements IView { @@ -151,14 +174,14 @@ class CodeEditorView implements IView { public readonly editor = this.instantiationService.createInstance( CodeEditorWidget, this._editorElement, - { minimap: { enabled: false } }, + { minimap: { enabled: false }, readOnly: this._options.readonly }, {} ); constructor( - @IInstantiationService - private readonly instantiationService: IInstantiationService + private readonly _options: ICodeEditorViewOptions, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { this.element.classList.add('code-view'); this._titleElement.classList.add('title'); From 638ba65eccc030511fab770d731a348cc77360a4 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 15:04:22 +0200 Subject: [PATCH 013/270] rename `testMergeEditor` to `_open.mergeEditor` --- .../mergeEditor/browser/mergeEditor.contribution.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index c1fc3b0fbf2..a9446f8a183 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -40,19 +40,18 @@ registerAction2(class Foo extends Action2 { constructor() { super({ - id: 'testMergeEditor', - title: '3wm', - f1: true + id: '_open.mergeEditor', + title: localize('title', "Open Merge Editor"), }); } run(accessor: ServicesAccessor, ...args: any[]): void { const validatedArgs = ITestMergeEditorArgs.validate(args[0]); - function normalize(uri: URI | string): URI { + function normalize(uri: URI | UriComponents | string): URI { if (typeof uri === 'string') { return URI.parse(uri); } else { - return uri; + return URI.revive(uri); } } From 5971d3188148969dfd101dfccad134b313919368 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 15:09:35 +0200 Subject: [PATCH 014/270] (prototype) use `_open.mergeEditor` from git extension for files under conflict --- extensions/git/package.json | 3 +- extensions/git/src/main.ts | 4 ++- extensions/git/src/mergeInfoProvider.ts | 47 +++++++++++++++++++++++++ extensions/git/src/repository.ts | 15 +++++++- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 extensions/git/src/mergeInfoProvider.ts diff --git a/extensions/git/package.json b/extensions/git/package.json index 3517a2803d9..2d3da8735b3 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -22,7 +22,8 @@ ], "activationEvents": [ "*", - "onFileSystem:git" + "onFileSystem:git", + "onFileSystem:git-show" ], "extensionDependencies": [ "vscode.git-base" diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 24768c1dba4..4977cd4058c 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -25,6 +25,7 @@ import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; import { TerminalEnvironmentManager } from './terminal'; import { OutputChannelLogger } from './log'; +import { GitMergeShowContentProvider } from './mergeInfoProvider'; const deactivateTasks: { (): Promise }[] = []; @@ -102,7 +103,8 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu new GitFileSystemProvider(model), new GitDecorations(model), new GitProtocolHandler(), - new GitTimelineProvider(model, cc) + new GitTimelineProvider(model, cc), + new GitMergeShowContentProvider(model) ); checkGitVersion(info); diff --git a/extensions/git/src/mergeInfoProvider.ts b/extensions/git/src/mergeInfoProvider.ts new file mode 100644 index 00000000000..83853a18f47 --- /dev/null +++ b/extensions/git/src/mergeInfoProvider.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextDocumentContentProvider, Uri, workspace } from 'vscode'; +import { Model } from './model'; + +export class GitMergeShowContentProvider implements TextDocumentContentProvider { + + static readonly Scheme = 'git-show'; + + readonly dispose: () => void; + + constructor(private model: Model) { + const reg = workspace.registerTextDocumentContentProvider(GitMergeShowContentProvider.Scheme, this); + this.dispose = reg.dispose.bind(reg); + } + + async provideTextDocumentContent(uri: Uri): Promise { + await this.model.isInitialized; + + const repository = this.model.getRepository(uri); + if (!repository) { + return undefined; + } + + if (!/^:[123]$/.test(uri.query)) { + return undefined; + } + + try { + return await repository.show(uri.query, uri.fsPath); + } catch (error) { + console.error(error); + return undefined; + } + } +} + +export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } { + return { + base: uri.with({ scheme: GitMergeShowContentProvider.Scheme, query: ':1' }), + ours: uri.with({ scheme: GitMergeShowContentProvider.Scheme, query: ':2' }), + theirs: uri.with({ scheme: GitMergeShowContentProvider.Scheme, query: ':3' }), + }; +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index c7962fb1a62..6d603936b80 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -21,6 +21,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { ActionButtonCommand } from './actionButton'; +import { toMergeUris } from './mergeInfoProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -602,7 +603,19 @@ class ResourceCommandResolver { resolveChangeCommand(resource: Resource): Command { const title = this.getTitle(resource); - if (!resource.leftUri) { + if (!resource.leftUri && resource.rightUri && resource.type === Status.BOTH_MODIFIED) { + const mergeUris = toMergeUris(resource.rightUri); + return { + command: '_open.mergeEditor', + title: localize('open.merge', "Open Merge"), + arguments: [{ + ancestor: mergeUris.base, + input1: mergeUris.ours, + input2: mergeUris.theirs, + output: resource.rightUri + }] + }; + } else if (!resource.leftUri) { return { command: 'vscode.open', title: localize('open', "Open"), From 983fc7a6b6c9da3272219ab00a59ce5146a8afbf Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 May 2022 16:14:28 +0200 Subject: [PATCH 015/270] connect floating button to a menu and contribute to that from git --- extensions/git/package.json | 12 +++++++ extensions/git/package.nls.json | 1 + extensions/git/src/commands.ts | 13 +++++++ src/vs/platform/actions/common/actions.ts | 1 + .../browser/mergeEditor.contribution.ts | 3 -- .../mergeEditor/browser/mergeEditor.ts | 35 +++++++++++++++---- .../actions/common/menusExtensionPoint.ts | 6 ++++ .../common/extensionsApiProposals.ts | 1 + ...de.proposed.contribMergeEditorToolbar.d.ts | 6 ++++ 9 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts diff --git a/extensions/git/package.json b/extensions/git/package.json index 2d3da8735b3..ced39d466d1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -11,6 +11,7 @@ "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "enabledApiProposals": [ "diffCommand", + "contribMergeEditorToolbar", "contribViewsWelcome", "scmActionButton", "scmSelectedProvider", @@ -550,6 +551,11 @@ "command": "git.api.getRemoteSources", "title": "%command.api.getRemoteSources%", "category": "Git API" + }, + { + "command": "git.acceptMerge", + "title": "%command.git.acceptMerge%", + "category": "Git" } ], "keybindings": [ @@ -1419,6 +1425,12 @@ "when": "isInDiffRightEditor && !isInEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" } ], + "merge/toolbar": [ + { + "command": "git.acceptMerge", + "when": "isMergeEditor" + } + ], "scm/change/title": [ { "command": "git.stageChange", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 523b9945928..2a1ec856252 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -97,6 +97,7 @@ "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", + "command.git.acceptMerge": "Accept Merge", "config.enabled": "Whether git is enabled.", "config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows). This can also be an array of string values containing multiple paths to look up.", "config.autoRepositoryDetection": "Configures when repositories should be automatically detected.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a3479a0b3b5..93cf4df895a 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1033,6 +1033,19 @@ export class CommandCenter { await this._stageChanges(textEditor, selectedChanges); } + @command('git.acceptMerge') + async acceptMerge(uri: Uri | unknown): Promise { + // TODO@jrieken make this proper, needs help from SCM folks + if (!(uri instanceof Uri)) { + return; + } + const repository = this.model.getRepository(uri); + if (!repository) { + return; + } + await repository.add([uri]); + } + private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise { const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index e97c241b95c..aef37651a7d 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -158,6 +158,7 @@ export class MenuId { static readonly WebviewContext = new MenuId('WebviewContext'); static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); static readonly NewFile = new MenuId('NewFile'); + static readonly MergeToolbar = new MenuId('MergeToolbar'); readonly id: number; readonly _debugName: string; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index a9446f8a183..88fc90c9d2a 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -17,9 +17,6 @@ import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/merge import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { MergeEditorSerializer } from './mergeEditorSerializer'; - -//#region Editor Descriptior - Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( MergeEditor, diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 3a1e6b68079..d3a8f704b36 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -29,8 +29,13 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IAction } from 'vs/base/common/actions'; + +export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false); export class MergeEditor extends EditorPane { @@ -47,12 +52,16 @@ export class MergeEditor extends EditorPane { constructor( @IInstantiationService private readonly instantiation: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService, ) { super(MergeEditor.ID, telemetryService, themeService, storageService); + ctxIsMergeEditor.bindTo(_contextKeyService).set(true); + const reentrancyBarrier = new ReentrancyBarrier(); this._store.add(this.inputOneView.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { @@ -80,10 +89,25 @@ export class MergeEditor extends EditorPane { })); // TODO@jrieken make this proper: add menu id and allow extensions to contribute - const acceptBtn = this.instantiation.createInstance(FloatingClickWidget, this.inputResultView.editor, 'Accept Merge', ''); - acceptBtn.render(); - this._store.add(acceptBtn.onClick(() => { console.log('DO IT'); })); - this._store.add(acceptBtn); + const toolbarMenu = this._menuService.createMenu(MenuId.MergeToolbar, this._contextKeyService); + const toolbarMenuDisposables = new DisposableStore(); + const toolbarMenuRender = () => { + toolbarMenuDisposables.clear(); + + const actions: IAction[] = []; + createAndFillInActionBarActions(toolbarMenu, { renderShortTitle: true, shouldForwardArgs: true }, actions); + if (actions.length > 0) { + const [first] = actions; + const acceptBtn = this.instantiation.createInstance(FloatingClickWidget, this.inputResultView.editor, first.label, first.id); + toolbarMenuDisposables.add(acceptBtn.onClick(() => first.run(this.inputResultView.editor.getModel()?.uri))); + toolbarMenuDisposables.add(acceptBtn); + acceptBtn.render(); + } + }; + this._store.add(toolbarMenu); + this._store.add(toolbarMenuDisposables); + this._store.add(toolbarMenu.onDidChange(toolbarMenuRender)); + toolbarMenuRender(); } override dispose(): void { @@ -116,7 +140,6 @@ export class MergeEditor extends EditorPane { this.inputOneView.setModel(model.inputOne, localize('yours', 'Yours'), undefined); this.inputTwoView.setModel(model.inputTwo, localize('theirs', 'Theirs',), undefined); this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); - } // override clearInput(): void { diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 7e06c120f95..b43e964676d 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -252,6 +252,12 @@ const apiMenus: IAPIMenu[] = [ supportsSubmenus: false, proposed: 'inlineCompletions' }, + { + key: 'merge/toolbar', + id: MenuId.MergeToolbar, + description: localize('merge.toolbar', "The prominent botton in the merge editor"), + proposed: 'contribMergeEditorToolbar' + } ]; namespace schema { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 4b51252484a..bf9cf9d7024 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -11,6 +11,7 @@ export const allApiProposals = Object.freeze({ commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', + contribMergeEditorToolbar: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts', contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', diff --git a/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts b/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts new file mode 100644 index 00000000000..323ff90cecb --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for the `mergeEditor/toolbar` menu From 02f42326ab9c83b822e1860e528457c08fa4f40a Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 11 May 2022 12:03:41 +0200 Subject: [PATCH 016/270] use descriptive view sizing for grid --- .../mergeEditor/browser/mergeEditor.ts | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index d3a8f704b36..c300d43dd54 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -17,8 +17,8 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { BugIndicatingError } from 'vs/base/common/errors'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Direction, Grid, IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; -import { Sizing } from 'vs/base/browser/ui/splitview/splitview'; +import { Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { ITextModel } from 'vs/editor/common/model'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -117,10 +117,29 @@ export class MergeEditor extends EditorPane { protected createEditor(parent: HTMLElement): void { parent.classList.add('merge-editor'); - this._grid = new Grid(this.inputResultView, { styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent } }); - this._grid.addView(this.inputOneView, Sizing.Distribute, this.inputResultView, Direction.Up); - this._grid.addView(this.inputTwoView, Sizing.Distribute, this.inputOneView, Direction.Right); + this._grid = SerializableGrid.from({ + orientation: Orientation.VERTICAL, + size: 100, + groups: [ + { + size: 38, + groups: [{ + data: this.inputOneView + }, { + data: this.inputTwoView + }] + }, + { + size: 62, + data: this.inputResultView + }, + ] + }, { + styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent }, + proportionalLayout: true + }); + reset(parent, this._grid.element); } From abd3e66bac0970c2a87c627903053f2f4f28da8d Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 11 May 2022 12:04:04 +0200 Subject: [PATCH 017/270] use git-fs to read base, ours, and theirs --- extensions/git/src/main.ts | 4 +-- extensions/git/src/mergeInfoProvider.ts | 47 ------------------------- extensions/git/src/repository.ts | 3 +- extensions/git/src/uri.ts | 11 ++++++ 4 files changed, 13 insertions(+), 52 deletions(-) delete mode 100644 extensions/git/src/mergeInfoProvider.ts diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 4977cd4058c..24768c1dba4 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -25,7 +25,6 @@ import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; import { TerminalEnvironmentManager } from './terminal'; import { OutputChannelLogger } from './log'; -import { GitMergeShowContentProvider } from './mergeInfoProvider'; const deactivateTasks: { (): Promise }[] = []; @@ -103,8 +102,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu new GitFileSystemProvider(model), new GitDecorations(model), new GitProtocolHandler(), - new GitTimelineProvider(model, cc), - new GitMergeShowContentProvider(model) + new GitTimelineProvider(model, cc) ); checkGitVersion(info); diff --git a/extensions/git/src/mergeInfoProvider.ts b/extensions/git/src/mergeInfoProvider.ts deleted file mode 100644 index 83853a18f47..00000000000 --- a/extensions/git/src/mergeInfoProvider.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { TextDocumentContentProvider, Uri, workspace } from 'vscode'; -import { Model } from './model'; - -export class GitMergeShowContentProvider implements TextDocumentContentProvider { - - static readonly Scheme = 'git-show'; - - readonly dispose: () => void; - - constructor(private model: Model) { - const reg = workspace.registerTextDocumentContentProvider(GitMergeShowContentProvider.Scheme, this); - this.dispose = reg.dispose.bind(reg); - } - - async provideTextDocumentContent(uri: Uri): Promise { - await this.model.isInitialized; - - const repository = this.model.getRepository(uri); - if (!repository) { - return undefined; - } - - if (!/^:[123]$/.test(uri.query)) { - return undefined; - } - - try { - return await repository.show(uri.query, uri.fsPath); - } catch (error) { - console.error(error); - return undefined; - } - } -} - -export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } { - return { - base: uri.with({ scheme: GitMergeShowContentProvider.Scheme, query: ':1' }), - ours: uri.with({ scheme: GitMergeShowContentProvider.Scheme, query: ':2' }), - theirs: uri.with({ scheme: GitMergeShowContentProvider.Scheme, query: ':3' }), - }; -} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 6d603936b80..4a81c8fac64 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -13,7 +13,7 @@ import { AutoFetcher } from './autofetch'; import { debounce, memoize, throttle } from './decorators'; import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; import { StatusBarCommands } from './statusbar'; -import { toGitUri } from './uri'; +import { toGitUri, toMergeUris } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { LogLevel, OutputChannelLogger } from './log'; @@ -21,7 +21,6 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { ActionButtonCommand } from './actionButton'; -import { toMergeUris } from './mergeInfoProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); diff --git a/extensions/git/src/uri.ts b/extensions/git/src/uri.ts index 94e6b5e38ae..5694c920d6b 100644 --- a/extensions/git/src/uri.ts +++ b/extensions/git/src/uri.ts @@ -51,3 +51,14 @@ export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Ur query: JSON.stringify(params) }); } + +/** + * Assuming `uri` is being merged it creates uris for `base`, `ours`, and `theirs` + */ +export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } { + return { + base: toGitUri(uri, ':1'), + ours: toGitUri(uri, ':2'), + theirs: toGitUri(uri, ':3'), + }; +} From 27fc8f70758943221c8a9c0a248b4a4f7b18b198 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 11 May 2022 14:13:30 +0200 Subject: [PATCH 018/270] First draft of the model implementation. --- src/vs/base/common/arrays.ts | 16 + src/vs/base/common/errors.ts | 4 +- .../mergeEditor/browser/mergeEditorInput.ts | 6 +- .../mergeEditor/browser/mergeEditorModel.ts | 207 ++++++++++- .../contrib/mergeEditor/browser/model.ts | 341 ++++++++++++++++++ 5 files changed, 568 insertions(+), 6 deletions(-) create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/model.ts diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 8f6ab7661ae..e876d9d4304 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -748,15 +748,31 @@ export class ArrayQueue { } peek(): T | undefined { + if (this.length === 0) { + return undefined; + } return this.items[this.firstIdx]; } + peekLast(): T | undefined { + if (this.length === 0) { + return undefined; + } + return this.items[this.lastIdx]; + } + dequeue(): T | undefined { const result = this.items[this.firstIdx]; this.firstIdx++; return result; } + removeLast(): T | undefined { + const result = this.items[this.lastIdx]; + this.lastIdx--; + return result; + } + takeCount(count: number): T[] { const result = this.items.slice(this.firstIdx, this.firstIdx + count); this.firstIdx += count; diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index 4f397866ae4..38ca3e4e102 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -259,8 +259,8 @@ export class ErrorNoTelemetry extends Error { * Only catch this error to recover gracefully from bugs. */ export class BugIndicatingError extends Error { - constructor(message: string) { - super(message); + constructor(message?: string) { + super(message || 'An unexpected bug occurred.'); Object.setPrototypeOf(this, BugIndicatingError.prototype); // Because we know for sure only buggy code throws this, diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index cb5773e9f86..8943def8741 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -14,7 +14,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IUntypedEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; -import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel'; +import { MergeEditorModel, MergeEditorModelFactory } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -31,6 +31,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { private _model?: MergeEditorModel; private _outTextModel?: ITextFileEditorModel; + private readonly mergeEditorModelFactory = this._instaService.createInstance(MergeEditorModelFactory); constructor( private readonly _anchestor: URI, @@ -97,8 +98,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { const inputTwo = await this._textModelService.createModelReference(this._inputTwo); const result = await this._textModelService.createModelReference(this._result); - this._model = this._instaService.createInstance( - MergeEditorModel, + this._model = await this.mergeEditorModelFactory.create( anchestor.object.textEditorModel, inputOne.object.textEditorModel, inputTwo.object.textEditorModel, diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 172a5c8e1a0..ce2eb6b3afb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -3,18 +3,223 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { MergeableDiff, LineEdit, LineEdits, LineDiff, MergeState, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; + +export class MergeEditorModelFactory { + constructor( + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + //@IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + } + + public async create( + ancestor: ITextModel, + inputOne: ITextModel, + inputTwo: ITextModel, + result: ITextModel, + ): Promise { + + const ancestorToInputOneDiffPromise = this._editorWorkerService.computeDiff( + ancestor.uri, + inputOne.uri, + false, + 1000 + ); + const ancestorToInputTwoDiffPromise = this._editorWorkerService.computeDiff( + ancestor.uri, + inputTwo.uri, + false, + 1000 + ); + + const [ancestorToInputOneDiff, ancestorToInputTwoDiff] = await Promise.all([ + ancestorToInputOneDiffPromise, + ancestorToInputTwoDiffPromise, + ]); + + const changesInput1 = + ancestorToInputOneDiff?.changes.map((c) => + LineDiff.fromLineChange(c, ancestor, inputOne) + ) || []; + const changesInput2 = + ancestorToInputTwoDiff?.changes.map((c) => + LineDiff.fromLineChange(c, ancestor, inputTwo) + ) || []; + + return new MergeEditorModel( + InternalSymbol, + ancestor, + inputOne, + inputTwo, + result, + changesInput1, + changesInput2, + ); + } +} + +const InternalSymbol = Symbol(); export class MergeEditorModel extends EditorModel { + private resultEdits = new ResultEdits([], this.ancestor, this.result); constructor( - readonly anchestor: ITextModel, + symbol: typeof InternalSymbol, + readonly ancestor: ITextModel, readonly inputOne: ITextModel, readonly inputTwo: ITextModel, readonly result: ITextModel, + private readonly inputOneLinesDiffs: readonly LineDiff[], + private readonly inputTwoLinesDiffs: readonly LineDiff[], ) { super(); + + result.setValue(ancestor.getValue()); + + /* + // Apply all non-conflicts diffs + const lineEditsArr: LineEdit[] = []; + for (const diff of this.mergeableDiffs) { + if (!diff.isConflicting) { + for (const d of diff.inputOneDiffs) { + lineEditsArr.push(d.getLineEdit()); + } + for (const d of diff.inputTwoDiffs) { + lineEditsArr.push(d.getLineEdit()); + } + } + } + new LineEdits(lineEditsArr).apply(result); + */ + + console.log(this, 'hello'); + (globalThis as any)['mergeEditorModel'] = this; } + public get resultDiffs(): readonly LineDiff[] { + return this.resultEdits.diffs; + } + + public readonly mergeableDiffs = MergeableDiff.fromDiffs(this.inputOneLinesDiffs, this.inputTwoLinesDiffs); + + public getState(conflict: MergeableDiff): MergeState | undefined { + return undefined; + } + + // Undo all edits of result that conflict with the conflict!! + public setConflictResolutionStatus(conflict: MergeableDiff, status: MergeState): void { + const existingDiff = this.resultEdits.findConflictingDiffs(conflict.originalRange); + if (existingDiff) { + this.resultEdits.removeDiff(existingDiff); + } + + const edit = status.input1 ? conflict.getInput1LineEdit() : conflict.getInput2LineEdit(); + if (edit) { + this.resultEdits.applyEditRelativeToOriginal(edit); + } + } +} + +class ResultEdits { + constructor( + private _diffs: LineDiff[], + private readonly originalTextModel: ITextModel, + private readonly textModel: ITextModel, + ) { + this._diffs.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator)); + } + + public get diffs(): readonly LineDiff[] { + return this._diffs; + } + + public removeDiff(diffToRemove: LineDiff): void { + const len = this._diffs.length; + this._diffs = this._diffs.filter((d) => d !== diffToRemove); + if (len === this._diffs.length) { + throw new BugIndicatingError(); + } + + const edits = new LineEdits([diffToRemove.getReverseLineEdit()]); + edits.apply(this.textModel); + + this._diffs = this._diffs.map((d) => + d.modifiedRange.isAfter(diffToRemove.modifiedRange) + ? new LineDiff( + d.originalTextModel, + d.originalRange, + d.modifiedTextModel, + d.modifiedRange.delta( + diffToRemove.originalRange.lineCount - diffToRemove.modifiedRange.lineCount + ) + ) + : d + ); + } + + /** + * Edit must be conflict free. + */ + public applyEditRelativeToOriginal(edit: LineEdit): void { + let firstAfter = false; + let delta = 0; + const newDiffs = new Array(); + for (let i = 0; i < this._diffs.length; i++) { + const diff = this._diffs[i]; + + if (diff.originalRange.intersects(edit.range)) { + throw new BugIndicatingError(); + } else if (diff.originalRange.isAfter(edit.range)) { + if (!firstAfter) { + firstAfter = true; + + newDiffs.push(new LineDiff( + this.originalTextModel, + edit.range, + this.textModel, + new LineRange(edit.range.startLineNumber + delta, edit.newLines.length) + )); + } + + newDiffs.push(new LineDiff( + diff.originalTextModel, + diff.originalRange, + diff.modifiedTextModel, + diff.modifiedRange.delta(edit.newLines.length - edit.range.lineCount) + )); + } else { + newDiffs.push(diff); + } + + if (!firstAfter) { + delta += diff.modifiedRange.lineCount - diff.originalRange.lineCount; + } + } + + if (!firstAfter) { + firstAfter = true; + + newDiffs.push(new LineDiff( + this.originalTextModel, + edit.range, + this.textModel, + new LineRange(edit.range.startLineNumber + delta, edit.newLines.length) + )); + } + this._diffs = newDiffs; + + const edits = new LineEdits([new LineEdit(edit.range.delta(delta), edit.newLines, edit.data)]); + edits.apply(this.textModel); + } + + // TODO return many! + public findConflictingDiffs(rangeInOriginalTextModel: LineRange): LineDiff | undefined { + // TODO binary search + return this.diffs.find(d => d.originalRange.intersects(rangeInOriginalTextModel)); + } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model.ts b/src/vs/workbench/contrib/mergeEditor/browser/model.ts new file mode 100644 index 00000000000..c26686a0466 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model.ts @@ -0,0 +1,341 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ArrayQueue, compareBy, numberComparator } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Range } from 'vs/editor/common/core/range'; +import { ILineChange } from 'vs/editor/common/diff/diffComputer'; +import { ITextModel } from 'vs/editor/common/model'; + +export class LineEdits { + constructor(public readonly edits: readonly LineEdit[]) { + + } + + public apply(model: ITextModel): void { + model.pushEditOperations( + null, + this.edits.map((e) => ({ + range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), + text: e.newLines.map(l => l + '\n').join(), + })), + () => null + ); + } +} + +export class LineEdit { + constructor( + public readonly range: LineRange, + public readonly newLines: string[], + public readonly data: T + ) { } +} + +export class MergeableDiff { + public static fromDiffs( + diffs1: readonly LineDiff[], + diffs2: readonly LineDiff[] + ): MergeableDiff[] { + const compareByStartLineNumber = compareBy( + (d) => d.originalRange.startLineNumber, + numberComparator + ); + + const queueDiffs1 = new ArrayQueue( + diffs1.slice().sort(compareByStartLineNumber) + ); + const queueDiffs2 = new ArrayQueue( + diffs2.slice().sort(compareByStartLineNumber) + ); + + const result = new Array(); + + while (true) { + const lastDiff1 = queueDiffs1.peekLast(); + const lastDiff2 = queueDiffs2.peekLast(); + + if ( + lastDiff1 && + (!lastDiff2 || + lastDiff1.originalRange.startLineNumber >= + lastDiff2.originalRange.startLineNumber) + ) { + queueDiffs1.removeLast(); + + const otherConflictingWith = + queueDiffs2.takeFromEndWhile((d) => d.conflicts(lastDiff1)) || []; + + const singleLinesDiff = LineDiff.hull(otherConflictingWith); + + const moreConflictingWith = + (singleLinesDiff && + queueDiffs1.takeFromEndWhile((d) => + d.conflicts(singleLinesDiff) + )) || + []; + moreConflictingWith.push(lastDiff1); + + result.push( + new MergeableDiff(moreConflictingWith, otherConflictingWith) + ); + } else if (lastDiff2) { + queueDiffs2.removeLast(); + + const otherConflictingWith = + queueDiffs1.takeFromEndWhile((d) => d.conflicts(lastDiff2)) || []; + + const singleLinesDiff = LineDiff.hull(otherConflictingWith); + + const moreConflictingWith = + (singleLinesDiff && + queueDiffs2.takeFromEndWhile((d) => + d.conflicts(singleLinesDiff) + )) || + []; + moreConflictingWith.push(lastDiff2); + + result.push( + new MergeableDiff(otherConflictingWith, moreConflictingWith) + ); + } else { + break; + } + } + + result.reverse(); + + return result; + } + + public readonly originalRange = LineRange.hull( + this.input1Diffs + .map((d) => d.originalRange) + .concat(this.input2Diffs.map((d) => d.originalRange)) + )!; + + constructor( + public readonly input1Diffs: readonly LineDiff[], + public readonly input2Diffs: readonly LineDiff[] + ) { } + + public get isConflicting(): boolean { + return this.input1Diffs.length > 0 && this.input2Diffs.length > 0; + } + + public getInput1LineEdit(): LineEdit | undefined { + if (this.input1Diffs.length === 0) { + return undefined; + } + if (this.input1Diffs.length === 1) { + return this.input1Diffs[0].getLineEdit(); + } else { + throw new Error('Method not implemented.'); + } + } + + public getInput2LineEdit(): LineEdit | undefined { + if (this.input2Diffs.length === 0) { + return undefined; + } + if (this.input2Diffs.length === 1) { + return this.input2Diffs[0].getLineEdit(); + } else { + throw new Error('Method not implemented.'); + } + } +} + +export class LineRange { + public static hull(ranges: LineRange[]): LineRange | undefined { + if (ranges.length === 0) { + return undefined; + } + + let startLineNumber = Number.MAX_SAFE_INTEGER; + let endLineNumber = 0; + for (const range of ranges) { + startLineNumber = Math.min(startLineNumber, range.startLineNumber); + endLineNumber = Math.max(endLineNumber, range.startLineNumber + range.lineCount); + } + return new LineRange(startLineNumber, endLineNumber - startLineNumber); + } + + constructor( + public readonly startLineNumber: number, + public readonly lineCount: number + ) { + if (lineCount < 0) { + throw new BugIndicatingError(); + } + } + + public get endLineNumberExclusive(): number { + return this.startLineNumber + this.lineCount; + } + + public get isEmpty(): boolean { + return this.lineCount === 0; + } + + public intersects(other: LineRange): boolean { + return ( + this.endLineNumberExclusive >= other.startLineNumber && + other.endLineNumberExclusive >= this.startLineNumber + ); + } + + public isAfter(modifiedRange: LineRange): boolean { + return this.startLineNumber >= modifiedRange.endLineNumberExclusive; + } + + public delta(lineDelta: number): LineRange { + return new LineRange(this.startLineNumber + lineDelta, this.lineCount); + } + + public toString() { + return `[${this.startLineNumber},${this.endLineNumberExclusive})`; + } +} + +export class LineDiff { + public static fromLineChange(lineChange: ILineChange, originalTextModel: ITextModel, modifiedTextModel: ITextModel): LineDiff { + let originalRange: LineRange; + if (lineChange.originalEndLineNumber === 0) { + // Insertion + originalRange = new LineRange(lineChange.originalStartLineNumber + 1, 0); + } else { + originalRange = new LineRange(lineChange.originalStartLineNumber, lineChange.originalEndLineNumber - lineChange.originalStartLineNumber + 1); + } + + let modifiedRange: LineRange; + if (lineChange.modifiedEndLineNumber === 0) { + // Insertion + modifiedRange = new LineRange(lineChange.modifiedStartLineNumber + 1, 0); + } else { + modifiedRange = new LineRange(lineChange.modifiedStartLineNumber, lineChange.modifiedEndLineNumber - lineChange.modifiedStartLineNumber + 1); + } + + return new LineDiff( + originalTextModel, + originalRange, + modifiedTextModel, + modifiedRange, + ); + } + + public static hull(lineDiffs: LineDiff[]): LineDiff | undefined { + if (lineDiffs.length === 0) { + return undefined; + } + + return new LineDiff( + lineDiffs[0].originalTextModel, + LineRange.hull(lineDiffs.map((d) => d.originalRange))!, + lineDiffs[0].modifiedTextModel, + LineRange.hull(lineDiffs.map((d) => d.modifiedRange))!, + ); + } + + constructor( + public readonly originalTextModel: ITextModel, + public readonly originalRange: LineRange, + public readonly modifiedTextModel: ITextModel, + public readonly modifiedRange: LineRange, + ) { + } + + private ensureSameOriginalModel(other: LineDiff): void { + if (this.originalTextModel !== other.originalTextModel) { + // Both changes must refer to the same original model + throw new BugIndicatingError(); + } + } + + public conflicts(other: LineDiff): boolean { + this.ensureSameOriginalModel(other); + return this.originalRange.intersects(other.originalRange); + } + + public isStrictBefore(other: LineDiff): boolean { + this.ensureSameOriginalModel(other); + return this.originalRange.endLineNumberExclusive <= other.originalRange.startLineNumber; + } + + public getLineEdit(): LineEdit { + return new LineEdit( + this.originalRange, + this.getModifiedLines(), + undefined + ); + } + + public getReverseLineEdit(): LineEdit { + return new LineEdit( + this.modifiedRange, + this.getOriginalLines(), + undefined + ); + } + + private getModifiedLines(): string[] { + const result = new Array(this.modifiedRange.lineCount); + for (let i = 0; i < this.modifiedRange.lineCount; i++) { + result[i] = this.modifiedTextModel.getLineContent(this.modifiedRange.startLineNumber + i); + } + return result; + } + + private getOriginalLines(): string[] { + const result = new Array(this.originalRange.lineCount); + for (let i = 0; i < this.originalRange.lineCount; i++) { + result[i] = this.originalTextModel.getLineContent(this.originalRange.startLineNumber + i); + } + return result; + } +} + + +export class MergeState { + constructor( + public readonly input1: boolean, + public readonly input2: boolean, + public readonly input2First: boolean + ) { } + + public withInput1(value: boolean): MergeState { + return new MergeState( + value, + this.input2, + value && this.isEmpty ? false : this.input2First + ); + } + + public withInput2(value: boolean): MergeState { + return new MergeState( + this.input1, + value, + value && this.isEmpty ? true : this.input2First + ); + } + + public get isEmpty(): boolean { + return !this.input1 && !this.input2; + } + + public toString(): string { + const arr: ('1' | '2')[] = []; + if (this.input1) { + arr.push('1'); + } + if (this.input2) { + arr.push('2'); + } + if (this.input2First) { + arr.reverse(); + } + return arr.join(','); + } +} From a38e09d1380f81c84ca19abfdd2e8d04e107452c Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 11 May 2022 15:37:12 +0200 Subject: [PATCH 019/270] add toggle layout action - column vs 2 by 1 --- .../browser/mergeEditor.contribution.ts | 35 ++++++++++++++-- .../mergeEditor/browser/mergeEditor.ts | 40 ++++++++++++++----- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index 88fc90c9d2a..3f72bfca524 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -6,16 +6,17 @@ import { localize } from 'vs/nls'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditor'; +import { ctxIsMergeEditor, ctxUsesColumnLayout, MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditor'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { MergeEditorSerializer } from './mergeEditorSerializer'; +import { Codicon } from 'vs/base/common/codicons'; Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -33,7 +34,35 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit MergeEditorSerializer ); -registerAction2(class Foo extends Action2 { +registerAction2(class ToggleLayout extends Action2 { + + constructor() { + super({ + id: 'merge.toggleLayout', + title: localize('toggle.title', "Switch to column view"), + icon: Codicon.layoutCentered, + toggled: { + condition: ctxUsesColumnLayout, + icon: Codicon.layoutPanel, + title: localize('toggle.title2', "Switch to 2 by 1 view"), + }, + menu: [{ + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: 'navigation' + }] + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.toggleLayout(); + } + } +}); + +registerAction2(class Open extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index c300d43dd54..93918c9ae43 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -17,8 +17,8 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { BugIndicatingError } from 'vs/base/common/errors'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; -import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; +import { Direction, Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; +import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { ITextModel } from 'vs/editor/common/model'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -29,13 +29,14 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IAction } from 'vs/base/common/actions'; export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false); +export const ctxUsesColumnLayout = new RawContextKey('mergeEditorUsesColumnLayout', false); export class MergeEditor extends EditorPane { @@ -49,6 +50,9 @@ export class MergeEditor extends EditorPane { private readonly inputTwoView = this.instantiation.createInstance(CodeEditorView, { readonly: true }); private readonly inputResultView = this.instantiation.createInstance(CodeEditorView, { readonly: false }); + private readonly _ctxIsMergeEditor: IContextKey; + private readonly _ctxUsesColumnLayout: IContextKey; + constructor( @IInstantiationService private readonly instantiation: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, @@ -60,7 +64,8 @@ export class MergeEditor extends EditorPane { ) { super(MergeEditor.ID, telemetryService, themeService, storageService); - ctxIsMergeEditor.bindTo(_contextKeyService).set(true); + this._ctxIsMergeEditor = ctxIsMergeEditor.bindTo(_contextKeyService); + this._ctxUsesColumnLayout = ctxUsesColumnLayout.bindTo(_contextKeyService); const reentrancyBarrier = new ReentrancyBarrier(); this._store.add(this.inputOneView.editor.onDidScrollChange(c => { @@ -112,6 +117,7 @@ export class MergeEditor extends EditorPane { override dispose(): void { this._sessionDisposables.dispose(); + this._ctxIsMergeEditor.reset(); super.dispose(); } @@ -141,6 +147,7 @@ export class MergeEditor extends EditorPane { }); reset(parent, this._grid.element); + this._ctxUsesColumnLayout.set(false); } layout(dimension: Dimension): void { @@ -161,13 +168,9 @@ export class MergeEditor extends EditorPane { this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); } - // override clearInput(): void { - // super.clearInput(); - // } - - // protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - // console.log('VISISBLE', visible); - // } + protected override setEditorVisible(visible: boolean): void { + this._ctxIsMergeEditor.set(visible); + } // ---- interact with "outside world" via `getControl`, `scopedContextKeyService` @@ -186,6 +189,21 @@ export class MergeEditor extends EditorPane { ? control.invokeWithinContext(accessor => accessor.get(IContextKeyService)) : undefined; } + + // --- layout + + private _usesColumnLayout = false; + + toggleLayout(): void { + if (!this._usesColumnLayout) { + this._grid.moveView(this.inputResultView, Sizing.Distribute, this.inputOneView, Direction.Right); + } else { + this._grid.moveView(this.inputResultView, this._grid.height * .62, this.inputOneView, Direction.Down); + this._grid.moveView(this.inputTwoView, Sizing.Distribute, this.inputOneView, Direction.Right); + } + this._usesColumnLayout = !this._usesColumnLayout; + this._ctxUsesColumnLayout.set(this._usesColumnLayout); + } } interface ICodeEditorViewOptions { From 1c559569eabfe84fa303f795066027bd5dd845f6 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 11 May 2022 17:13:47 +0200 Subject: [PATCH 020/270] one -> 1, two -> 2 --- .../mergeEditor/browser/mergeEditor.ts | 4 +- .../mergeEditor/browser/mergeEditorInput.ts | 24 ++++++------ .../mergeEditor/browser/mergeEditorModel.ts | 39 +++++++++++-------- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 93918c9ae43..81b82849c05 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -163,8 +163,8 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); - this.inputOneView.setModel(model.inputOne, localize('yours', 'Yours'), undefined); - this.inputTwoView.setModel(model.inputTwo, localize('theirs', 'Theirs',), undefined); + this.inputOneView.setModel(model.input1, localize('yours', 'Yours'), undefined); + this.inputTwoView.setModel(model.input2, localize('theirs', 'Theirs',), undefined); this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 8943def8741..da2ada49ee0 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -35,8 +35,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { constructor( private readonly _anchestor: URI, - private readonly _inputOne: URI, - private readonly _inputTwo: URI, + private readonly _input1: URI, + private readonly _input2: URI, private readonly _result: URI, @IInstantiationService private readonly _instaService: IInstantiationService, @ITextModelService private readonly _textModelService: ITextModelService, @@ -94,21 +94,21 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { if (!this._model) { const anchestor = await this._textModelService.createModelReference(this._anchestor); - const inputOne = await this._textModelService.createModelReference(this._inputOne); - const inputTwo = await this._textModelService.createModelReference(this._inputTwo); + const input1 = await this._textModelService.createModelReference(this._input1); + const input2 = await this._textModelService.createModelReference(this._input2); const result = await this._textModelService.createModelReference(this._result); this._model = await this.mergeEditorModelFactory.create( anchestor.object.textEditorModel, - inputOne.object.textEditorModel, - inputTwo.object.textEditorModel, + input1.object.textEditorModel, + input2.object.textEditorModel, result.object.textEditorModel ); this._store.add(this._model); this._store.add(anchestor); - this._store.add(inputOne); - this._store.add(inputTwo); + this._store.add(input1); + this._store.add(input2); this._store.add(result); // result.object. @@ -121,16 +121,16 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { return false; } return isEqual(this._anchestor, otherInput._anchestor) - && isEqual(this._inputOne, otherInput._inputOne) - && isEqual(this._inputTwo, otherInput._inputTwo) + && isEqual(this._input1, otherInput._input1) + && isEqual(this._input2, otherInput._input2) && isEqual(this._result, otherInput._result); } toJSON(): MergeEditorInputJSON { return { anchestor: this._anchestor, - inputOne: this._inputOne, - inputTwo: this._inputTwo, + inputOne: this._input1, + inputTwo: this._input2, result: this._result, }; } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index ce2eb6b3afb..13fd032a5ea 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -19,43 +19,43 @@ export class MergeEditorModelFactory { public async create( ancestor: ITextModel, - inputOne: ITextModel, - inputTwo: ITextModel, + input1: ITextModel, + input2: ITextModel, result: ITextModel, ): Promise { - const ancestorToInputOneDiffPromise = this._editorWorkerService.computeDiff( + const ancestorToInput1DiffPromise = this._editorWorkerService.computeDiff( ancestor.uri, - inputOne.uri, + input1.uri, false, 1000 ); - const ancestorToInputTwoDiffPromise = this._editorWorkerService.computeDiff( + const ancestorToInput2DiffPromise = this._editorWorkerService.computeDiff( ancestor.uri, - inputTwo.uri, + input2.uri, false, 1000 ); - const [ancestorToInputOneDiff, ancestorToInputTwoDiff] = await Promise.all([ - ancestorToInputOneDiffPromise, - ancestorToInputTwoDiffPromise, + const [ancestorToInput1Diff, ancestorToInput2Diff] = await Promise.all([ + ancestorToInput1DiffPromise, + ancestorToInput2DiffPromise, ]); const changesInput1 = - ancestorToInputOneDiff?.changes.map((c) => - LineDiff.fromLineChange(c, ancestor, inputOne) + ancestorToInput1Diff?.changes.map((c) => + LineDiff.fromLineChange(c, ancestor, input1) ) || []; const changesInput2 = - ancestorToInputTwoDiff?.changes.map((c) => - LineDiff.fromLineChange(c, ancestor, inputTwo) + ancestorToInput2Diff?.changes.map((c) => + LineDiff.fromLineChange(c, ancestor, input2) ) || []; return new MergeEditorModel( InternalSymbol, ancestor, - inputOne, - inputTwo, + input1, + input2, result, changesInput1, changesInput2, @@ -71,8 +71,8 @@ export class MergeEditorModel extends EditorModel { constructor( symbol: typeof InternalSymbol, readonly ancestor: ITextModel, - readonly inputOne: ITextModel, - readonly inputTwo: ITextModel, + readonly input1: ITextModel, + readonly input2: ITextModel, readonly result: ITextModel, private readonly inputOneLinesDiffs: readonly LineDiff[], private readonly inputTwoLinesDiffs: readonly LineDiff[], @@ -108,6 +108,11 @@ export class MergeEditorModel extends EditorModel { public readonly mergeableDiffs = MergeableDiff.fromDiffs(this.inputOneLinesDiffs, this.inputTwoLinesDiffs); public getState(conflict: MergeableDiff): MergeState | undefined { + const existingDiff = this.resultEdits.findConflictingDiffs(conflict.originalRange); + if (existingDiff) { + //existingDiff. + } + return undefined; } From e00030977e6eb14422594c0fa475eb133d4cbc39 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 13 May 2022 10:19:59 +0200 Subject: [PATCH 021/270] Implements glyph buttons, alignment and decorations. --- .../browser/breakpointEditorContribution.ts | 2 +- .../contrib/mergeEditor/browser/icons.ts | 10 + .../mergeEditor/browser/media/mergeEditor.css | 31 ++- .../mergeEditor/browser/mergeEditor.ts | 221 +++++++++++++++--- .../mergeEditor/browser/mergeEditorModel.ts | 47 +++- .../contrib/mergeEditor/browser/model.ts | 128 ++++++++-- 6 files changed, 375 insertions(+), 64 deletions(-) create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/icons.ts diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 4eab6791a7f..a5bc41d7cb0 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -433,7 +433,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi if (decorations) { for (const { options } of decorations) { const clz = options.glyphMarginClassName; - if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) { + if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-') || clz.includes('codicon-merge-'))) { return false; } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/icons.ts b/src/vs/workbench/contrib/mergeEditor/browser/icons.ts new file mode 100644 index 00000000000..a37f0e66faf --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/icons.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { localize } from 'vs/nls'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; + +export const acceptConflictIcon = registerIcon('merge-accept-conflict-icon', Codicon.check, localize('acceptConflictIcon', 'TODO.')); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index 2a1dd749cbb..749b947af55 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -3,13 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .merge-editor .code-view>.title { +.monaco-workbench .merge-editor .code-view > .title { padding: 0 0 0 10px; height: 30px; display: flex; align-content: center; } -.monaco-workbench .merge-editor .code-view>.title .monaco-icon-label { +.monaco-workbench .merge-editor .code-view > .title .monaco-icon-label { margin: auto 0; } + +.monaco-workbench .merge-editor .code-view > .container { + display: flex; + flex-direction: row; +} + +.monaco-workbench .merge-editor .code-view > .container > .gutter { + min-width: 20px; + /* background-color: yellow; */ + margin-right: 3px; + position: relative; +} + +.monaco-editor .merge-accept-conflict-glyph { + cursor: pointer; +} + +.merge-accept-foo { + background-color: rgb(170 175 170 / 15%); +} + +.merge-accept-gutter-marker { + min-height: 30px; + min-width: 20px; + background-color: yellow; + position: absolute; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 81b82849c05..c846f45c87f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/mergeEditor'; -import { Dimension, reset } from 'vs/base/browser/dom'; +import { $, Dimension, reset } from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; @@ -19,7 +19,7 @@ import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/merge import { DisposableStore } from 'vs/base/common/lifecycle'; import { Direction, Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; -import { ITextModel } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ScrollType } from 'vs/editor/common/editorCommon'; @@ -30,10 +30,15 @@ import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IAction } from 'vs/base/common/actions'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import { acceptConflictIcon } from 'vs/workbench/contrib/mergeEditor/browser/icons'; +import { ConflictGroup, MergeState } from 'vs/workbench/contrib/mergeEditor/browser/model'; + export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false); export const ctxUsesColumnLayout = new RawContextKey('mergeEditorUsesColumnLayout', false); @@ -46,8 +51,8 @@ export class MergeEditor extends EditorPane { private _grid!: Grid; - private readonly inputOneView = this.instantiation.createInstance(CodeEditorView, { readonly: true }); - private readonly inputTwoView = this.instantiation.createInstance(CodeEditorView, { readonly: true }); + private readonly input1View = this.instantiation.createInstance(CodeEditorView, { readonly: true }); + private readonly input2View = this.instantiation.createInstance(CodeEditorView, { readonly: true }); private readonly inputResultView = this.instantiation.createInstance(CodeEditorView, { readonly: false }); private readonly _ctxIsMergeEditor: IContextKey; @@ -68,18 +73,18 @@ export class MergeEditor extends EditorPane { this._ctxUsesColumnLayout = ctxUsesColumnLayout.bindTo(_contextKeyService); const reentrancyBarrier = new ReentrancyBarrier(); - this._store.add(this.inputOneView.editor.onDidScrollChange(c => { + this._store.add(this.input1View.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { - this.inputTwoView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); this.inputResultView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); }); } })); - this._store.add(this.inputTwoView.editor.onDidScrollChange(c => { + this._store.add(this.input2View.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { - this.inputOneView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); this.inputResultView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); }); } @@ -87,8 +92,8 @@ export class MergeEditor extends EditorPane { this._store.add(this.inputResultView.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { - this.inputOneView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - this.inputTwoView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); }); } })); @@ -131,9 +136,9 @@ export class MergeEditor extends EditorPane { { size: 38, groups: [{ - data: this.inputOneView + data: this.input1View }, { - data: this.inputTwoView + data: this.input2View }] }, { @@ -163,9 +168,122 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); - this.inputOneView.setModel(model.input1, localize('yours', 'Yours'), undefined); - this.inputTwoView.setModel(model.input2, localize('theirs', 'Theirs',), undefined); + this.input1View.setModel(model.input1, localize('yours', 'Yours'), undefined); + this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), undefined); this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); + + + const input1LineNumberToMergeableDiffMap = new Map(); + + this.input1View.editor.onMouseDown(e => { + if (e.target.element && e.target.element.className.includes('merge-')) { + const lineNumber = e.target.position?.lineNumber; + if (lineNumber) { + const diff = input1LineNumberToMergeableDiffMap.get(lineNumber); + if (diff) { + const state = (model.getState(diff) || new MergeState(false, false, false)).toggleInput1(); + model.setConflictResolutionStatus(diff, state); + } + } + } + }); + + this.input1View.editor.changeDecorations(c => { + for (const mergeableDiff of model.mergeableDiffs) { + if (mergeableDiff.input1FullDiff) { + const lineNumber = mergeableDiff.input1FullDiff.modifiedRange.startLineNumber; + input1LineNumberToMergeableDiffMap.set(lineNumber, mergeableDiff); + c.addDecoration(Range.fromPositions(new Position(lineNumber, 0)), { + description: 'foo', + glyphMarginClassName: ThemeIcon.asClassName(acceptConflictIcon) + ' merge-accept-conflict-glyph', + }); + } + } + }); + + + + const input2LineNumberToMergeableDiffMap = new Map(); + + this.input2View.editor.onMouseDown(e => { + if (e.target.element && e.target.element.className.includes('merge-')) { + const lineNumber = e.target.position?.lineNumber; + if (lineNumber) { + const diff = input2LineNumberToMergeableDiffMap.get(lineNumber); + if (diff) { + const state = (model.getState(diff) || new MergeState(false, false, false)).toggleInput2(); + model.setConflictResolutionStatus(diff, state); + } + } + } + }); + + + + this.input2View.editor.changeDecorations(c => { + for (const mergeableDiff of model.mergeableDiffs) { + if (mergeableDiff.input2FullDiff) { + const lineNumber = mergeableDiff.input2FullDiff.modifiedRange.startLineNumber; + input2LineNumberToMergeableDiffMap.set(lineNumber, mergeableDiff); + c.addDecoration(Range.fromPositions(new Position(lineNumber, 1)), { + description: 'foo', + glyphMarginClassName: ThemeIcon.asClassName(acceptConflictIcon) + ' merge-accept-conflict-glyph', + }); + } + } + }); + + + let input1Decorations = new Array(); + let input2Decorations = new Array(); + + + for (const m of model.mergeableDiffs) { + + if (!m.totalInput1Range.isEmpty) { + input1Decorations.push({ + range: new Range(m.totalInput1Range.startLineNumber, 1, m.totalInput1Range.endLineNumberExclusive - 1, 1), + options: { + isWholeLine: true, + className: 'merge-accept-foo', + description: 'foo2' + } + }); + } + + if (!m.totalInput2Range.isEmpty) { + input2Decorations.push({ + range: new Range(m.totalInput2Range.startLineNumber, 1, m.totalInput2Range.endLineNumberExclusive - 1, 1), + options: { + isWholeLine: true, + className: 'merge-accept-foo', + description: 'foo2' + } + }); + } + + const max = Math.max(m.totalInput1Range.lineCount, m.totalInput2Range.lineCount, 1); + + this.input1View.editor.changeViewZones(a => { + a.addZone({ + afterLineNumber: m.totalInput1Range.endLineNumberExclusive - 1, + heightInLines: max - m.totalInput1Range.lineCount, + domNode: $('div.diagonal-fill'), + }); + }); + + this.input2View.editor.changeViewZones(a => { + a.addZone({ + afterLineNumber: m.totalInput2Range.endLineNumberExclusive - 1, + heightInLines: max - m.totalInput2Range.lineCount, + domNode: $('div.diagonal-fill'), + }); + }); + } + + this.input1View.editor.deltaDecorations([], input1Decorations); + this.input2View.editor.deltaDecorations([], input2Decorations); + } protected override setEditorVisible(visible: boolean): void { @@ -175,7 +293,7 @@ export class MergeEditor extends EditorPane { // ---- interact with "outside world" via `getControl`, `scopedContextKeyService` override getControl(): IEditorControl | undefined { - for (const view of [this.inputOneView, this.inputTwoView, this.inputResultView]) { + for (const view of [this.input1View, this.input2View, this.inputResultView]) { if (view.editor.hasWidgetFocus()) { return view.editor; } @@ -196,10 +314,10 @@ export class MergeEditor extends EditorPane { toggleLayout(): void { if (!this._usesColumnLayout) { - this._grid.moveView(this.inputResultView, Sizing.Distribute, this.inputOneView, Direction.Right); + this._grid.moveView(this.inputResultView, Sizing.Distribute, this.input1View, Direction.Right); } else { - this._grid.moveView(this.inputResultView, this._grid.height * .62, this.inputOneView, Direction.Down); - this._grid.moveView(this.inputTwoView, Sizing.Distribute, this.inputOneView, Direction.Right); + this._grid.moveView(this.inputResultView, this._grid.height * .62, this.input1View, Direction.Down); + this._grid.moveView(this.input2View, Sizing.Distribute, this.input1View, Direction.Right); } this._usesColumnLayout = !this._usesColumnLayout; this._ctxUsesColumnLayout.set(this._usesColumnLayout); @@ -215,9 +333,10 @@ class CodeEditorView implements IView { // preferredWidth?: number | undefined; // preferredHeight?: number | undefined; - element: HTMLElement = document.createElement('div'); - private _titleElement = document.createElement('div'); - private _editorElement = document.createElement('div'); + element: HTMLElement; + private _titleElement: HTMLElement; + private _editorElement: HTMLElement; + private _gutterDiv: HTMLElement; minimumWidth: number = 10; maximumWidth: number = Number.MAX_SAFE_INTEGER; @@ -229,24 +348,36 @@ class CodeEditorView implements IView { private readonly _onDidChange = new Emitter(); readonly onDidChange = this._onDidChange.event; - private _title = new IconLabel(this._titleElement, { supportIcons: true }); + private _title: IconLabel; - public readonly editor = this.instantiationService.createInstance( - CodeEditorWidget, - this._editorElement, - { minimap: { enabled: false }, readOnly: this._options.readonly }, - {} - ); + public readonly editor: CodeEditorWidget; + private readonly gutter: EditorGutter; constructor( private readonly _options: ICodeEditorViewOptions, @IInstantiationService private readonly instantiationService: IInstantiationService ) { - this.element.classList.add('code-view'); - this._titleElement.classList.add('title'); - this.element.appendChild(this._titleElement); - this.element.appendChild(this._editorElement); + this.element = $( + 'div.code-view', + {}, + this._titleElement = $('div.title'), + $('div.container', {}, + this._gutterDiv = $('div.gutter'), + this._editorElement = $('div'), + ), + ); + + this.editor = this.instantiationService.createInstance( + CodeEditorWidget, + this._editorElement, + { minimap: { enabled: false }, readOnly: this._options.readonly, /*glyphMargin: false,*/ lineNumbersMinChars: 2 }, + { contributions: [] } + ); + + this._title = new IconLabel(this._titleElement, { supportIcons: true }); + this.gutter = new EditorGutter(this.editor, this._gutterDiv); + console.log(this.gutter); // keep alive } public setModel(model: ITextModel, title: string, description: string | undefined): void { @@ -259,7 +390,7 @@ class CodeEditorView implements IView { this.element.style.height = `${height}px`; this.element.style.top = `${top}px`; this.element.style.left = `${left}px`; - this.editor.layout({ width, height: height - this._titleElement.clientHeight }); + this.editor.layout({ width: width - this._gutterDiv.clientWidth, height: height - this._titleElement.clientHeight }); } } @@ -278,3 +409,23 @@ class ReentrancyBarrier { } } } + +class EditorGutter { + constructor( + private readonly _editor: ICodeEditor, + private readonly _domNode: HTMLElement, + ) { + this._editor.onDidScrollChange(() => { + this.render(); + }); + } + + private node = $('div.merge-accept-gutter-marker'); + + private render(): void { + this._domNode.append(this.node); + + const top = this._editor.getTopForLineNumber(10) - this._editor.getScrollTop(); + this.node.style.top = `${top}px`; + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 13fd032a5ea..081869931c3 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -8,7 +8,7 @@ import { BugIndicatingError } from 'vs/base/common/errors'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; -import { MergeableDiff, LineEdit, LineEdits, LineDiff, MergeState, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { ConflictGroup, LineEdit, LineEdits, LineDiff, MergeState, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; export class MergeEditorModelFactory { constructor( @@ -75,7 +75,7 @@ export class MergeEditorModel extends EditorModel { readonly input2: ITextModel, readonly result: ITextModel, private readonly inputOneLinesDiffs: readonly LineDiff[], - private readonly inputTwoLinesDiffs: readonly LineDiff[], + private readonly inputTwoLinesDiffs: readonly LineDiff[] ) { super(); @@ -105,25 +105,52 @@ export class MergeEditorModel extends EditorModel { return this.resultEdits.diffs; } - public readonly mergeableDiffs = MergeableDiff.fromDiffs(this.inputOneLinesDiffs, this.inputTwoLinesDiffs); + public readonly mergeableDiffs = ConflictGroup.partitionDiffs( + this.ancestor, + this.input1, + this.inputOneLinesDiffs, + this.input2, + this.inputTwoLinesDiffs + ); - public getState(conflict: MergeableDiff): MergeState | undefined { - const existingDiff = this.resultEdits.findConflictingDiffs(conflict.originalRange); - if (existingDiff) { - //existingDiff. + public getState(conflict: ConflictGroup): MergeState | undefined { + const existingDiff = this.resultEdits.findConflictingDiffs( + conflict.totalOriginalRange + ); + if (!existingDiff) { + return new MergeState(false, false, false); + } + + const input1Edit = conflict.getInput1LineEdit(); + if (input1Edit && existingDiff.getLineEdit().equals(input1Edit)) { + return new MergeState(true, false, false); + } + + const input2Edit = conflict.getInput2LineEdit(); + if (input2Edit && existingDiff.getLineEdit().equals(input2Edit)) { + return new MergeState(false, true, false); } return undefined; } // Undo all edits of result that conflict with the conflict!! - public setConflictResolutionStatus(conflict: MergeableDiff, status: MergeState): void { - const existingDiff = this.resultEdits.findConflictingDiffs(conflict.originalRange); + public setConflictResolutionStatus( + conflict: ConflictGroup, + status: MergeState + ): void { + const existingDiff = this.resultEdits.findConflictingDiffs( + conflict.totalOriginalRange + ); if (existingDiff) { this.resultEdits.removeDiff(existingDiff); } - const edit = status.input1 ? conflict.getInput1LineEdit() : conflict.getInput2LineEdit(); + const edit = status.input1 + ? conflict.getInput1LineEdit() + : status.input2 + ? conflict.getInput2LineEdit() + : undefined; if (edit) { this.resultEdits.applyEditRelativeToOriginal(edit); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model.ts b/src/vs/workbench/contrib/mergeEditor/browser/model.ts index c26686a0466..deae7ace505 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ArrayQueue, compareBy, numberComparator } from 'vs/base/common/arrays'; +import { ArrayQueue, compareBy, equals, numberComparator } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Range } from 'vs/editor/common/core/range'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; @@ -19,7 +19,7 @@ export class LineEdits { null, this.edits.map((e) => ({ range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), - text: e.newLines.map(l => l + '\n').join(), + text: e.newLines.map(l => l + '\n').join(''), })), () => null ); @@ -27,6 +27,10 @@ export class LineEdits { } export class LineEdit { + equals(other: LineEdit) { + return this.range.equals(other.range) && equals(this.newLines, other.newLines); + } + constructor( public readonly range: LineRange, public readonly newLines: string[], @@ -34,11 +38,18 @@ export class LineEdit { ) { } } -export class MergeableDiff { - public static fromDiffs( +export class ConflictGroup { + /** + * diffs1 and diffs2 together with the conflict relation form a bipartite graph. + * This method computes strongly connected components of that graph while maintaining the side of each diff. + */ + public static partitionDiffs( + originalTextModel: ITextModel, + input1TextModel: ITextModel, diffs1: readonly LineDiff[], + input2TextModel: ITextModel, diffs2: readonly LineDiff[] - ): MergeableDiff[] { + ): ConflictGroup[] { const compareByStartLineNumber = compareBy( (d) => d.originalRange.startLineNumber, numberComparator @@ -51,7 +62,7 @@ export class MergeableDiff { diffs2.slice().sort(compareByStartLineNumber) ); - const result = new Array(); + const result = new Array(); while (true) { const lastDiff1 = queueDiffs1.peekLast(); @@ -79,7 +90,15 @@ export class MergeableDiff { moreConflictingWith.push(lastDiff1); result.push( - new MergeableDiff(moreConflictingWith, otherConflictingWith) + new ConflictGroup( + originalTextModel, + input1TextModel, + moreConflictingWith, + queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + input2TextModel, + otherConflictingWith, + queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + ) ); } else if (lastDiff2) { queueDiffs2.removeLast(); @@ -98,7 +117,15 @@ export class MergeableDiff { moreConflictingWith.push(lastDiff2); result.push( - new MergeableDiff(otherConflictingWith, moreConflictingWith) + new ConflictGroup( + originalTextModel, + input1TextModel, + otherConflictingWith, + queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + input2TextModel, + moreConflictingWith, + queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + ) ); } else { break; @@ -110,16 +137,49 @@ export class MergeableDiff { return result; } - public readonly originalRange = LineRange.hull( - this.input1Diffs - .map((d) => d.originalRange) - .concat(this.input2Diffs.map((d) => d.originalRange)) - )!; + public readonly input1FullDiff = LineDiff.hull(this.input1Diffs); + public readonly input2FullDiff = LineDiff.hull(this.input2Diffs); + + public readonly totalOriginalRange: LineRange; + public readonly totalInput1Range: LineRange; + public readonly totalInput2Range: LineRange; constructor( + public readonly originalTextModel: ITextModel, + public readonly input1TextModel: ITextModel, public readonly input1Diffs: readonly LineDiff[], - public readonly input2Diffs: readonly LineDiff[] - ) { } + public readonly input1DeltaLineCount: number, + public readonly input2TextModel: ITextModel, + public readonly input2Diffs: readonly LineDiff[], + public readonly input2DeltaLineCount: number, + ) { + if (this.input1Diffs.length === 0 && this.input2Diffs.length === 0) { + throw new BugIndicatingError('must have at least one diff'); + } + + const input1Diff = + this.input1FullDiff || + new LineDiff( + originalTextModel, + this.input2FullDiff!.originalRange, + input1TextModel, + this.input2FullDiff!.originalRange.delta(input1DeltaLineCount) + ); + + const input2Diff = + this.input2FullDiff || + new LineDiff( + originalTextModel, + this.input1FullDiff!.originalRange, + input1TextModel, + this.input1FullDiff!.originalRange.delta(input2DeltaLineCount) + ); + + const results = LineDiff.alignOriginalRegion([input1Diff, input2Diff]); + this.totalOriginalRange = results[0].originalRange; + this.totalInput1Range = results[0].modifiedRange; + this.totalInput2Range = results[1].modifiedRange; + } public get isConflicting(): boolean { return this.input1Diffs.length > 0 && this.input2Diffs.length > 0; @@ -198,6 +258,10 @@ export class LineRange { public toString() { return `[${this.startLineNumber},${this.endLineNumberExclusive})`; } + + public equals(originalRange: LineRange) { + return this.startLineNumber === originalRange.startLineNumber && this.lineCount === originalRange.lineCount; + } } export class LineDiff { @@ -226,7 +290,7 @@ export class LineDiff { ); } - public static hull(lineDiffs: LineDiff[]): LineDiff | undefined { + public static hull(lineDiffs: readonly LineDiff[]): LineDiff | undefined { if (lineDiffs.length === 0) { return undefined; } @@ -239,6 +303,26 @@ export class LineDiff { ); } + public static alignOriginalRegion(lineDiffs: readonly LineDiff[]): LineDiff[] { + if (lineDiffs.length === 0) { + return []; + } + const originalRange = LineRange.hull(lineDiffs.map((d) => d.originalRange))!; + return lineDiffs.map(l => { + const startDelta = originalRange.startLineNumber - l.originalRange.startLineNumber; + const endDelta = originalRange.endLineNumberExclusive - l.originalRange.endLineNumberExclusive; + return new LineDiff( + l.originalTextModel, + originalRange, + l.modifiedTextModel, + new LineRange( + l.modifiedRange.startLineNumber + startDelta, + l.modifiedRange.lineCount - startDelta + endDelta + ) + ); + }); + } + constructor( public readonly originalTextModel: ITextModel, public readonly originalRange: LineRange, @@ -247,6 +331,10 @@ export class LineDiff { ) { } + public get resultingDeltaFromOriginalToModified(): number { + return this.modifiedRange.endLineNumberExclusive - this.originalRange.endLineNumberExclusive; + } + private ensureSameOriginalModel(other: LineDiff): void { if (this.originalTextModel !== other.originalTextModel) { // Both changes must refer to the same original model @@ -321,6 +409,14 @@ export class MergeState { ); } + public toggleInput1(): MergeState { + return this.withInput1(!this.input1); + } + + public toggleInput2(): MergeState { + return this.withInput2(!this.input2); + } + public get isEmpty(): boolean { return !this.input1 && !this.input2; } From f790b3ba1f03b7071b77000213f386c5e92205b4 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 16 May 2022 14:15:23 +0200 Subject: [PATCH 022/270] Implements gutter buttons. --- .../mergeEditor/browser/editorGutterWidget.ts | 100 ++++ .../mergeEditor/browser/media/mergeEditor.css | 49 +- .../mergeEditor/browser/mergeEditor.ts | 234 +++++----- .../mergeEditor/browser/mergeEditorModel.ts | 158 ++++--- .../contrib/mergeEditor/browser/model.ts | 434 ++++++++++-------- 5 files changed, 596 insertions(+), 379 deletions(-) create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts diff --git a/src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts b/src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts new file mode 100644 index 00000000000..9b3954eed48 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; + +export class EditorGutterWidget { + constructor( + private readonly _editor: ICodeEditor, + private readonly _domNode: HTMLElement, + private readonly itemProvider: IGutterItemProvider, + ) { + this._domNode.className = 'gutter'; + + this._editor.onDidScrollChange(() => { + this.render(); + }); + } + + private readonly views = new Map(); + + private render(): void { + const visibleRange = this._editor.getVisibleRanges()[0]; + const visibleRange2 = new LineRange( + visibleRange.startLineNumber, + visibleRange.endLineNumber - visibleRange.startLineNumber + ); + + const gutterItems = this.itemProvider.getIntersectingGutterItems(visibleRange2); + + const lineHeight = this._editor.getOptions().get(EditorOption.lineHeight); + + const unusedIds = new Set(this.views.keys()); + for (const gutterItem of gutterItems) { + if (!gutterItem.range.touches(visibleRange2)) { + continue; + } + + unusedIds.delete(gutterItem.id); + let view = this.views.get(gutterItem.id); + if (!view) { + const viewDomNode = document.createElement('div'); + viewDomNode.className = 'gutter-item'; + this._domNode.appendChild(viewDomNode); + const itemView = this.itemProvider.createView(gutterItem, viewDomNode); + view = new ManagedGutterItemView(itemView, viewDomNode); + this.views.set(gutterItem.id, view); + } + + const scrollTop = this._editor.getScrollTop(); + const top = this._editor.getTopForLineNumber(gutterItem.range.startLineNumber) - scrollTop; + const height = lineHeight * gutterItem.range.lineCount; + + view.domNode.style.top = `${top}px`; + view.domNode.style.height = `${height}px`; + + view.gutterItemView.layout(top, height, 0, -1); + } + + for (const id of unusedIds) { + const view = this.views.get(id)!; + view.gutterItemView.dispose(); + this._domNode.removeChild(view.domNode); + this.views.delete(id); + } + } +} + +class ManagedGutterItemView { + constructor( + public readonly gutterItemView: IGutterItemView, + public readonly domNode: HTMLDivElement + ) { } +} + +export interface IGutterItemProvider { + // onDidChange + getIntersectingGutterItems(range: LineRange): TItem[]; + + createView(item: TItem, target: HTMLElement): IGutterItemView; +} + +export interface IGutterItemInfo { + id: string; + range: LineRange; + + // To accommodate view zones: + offsetInPx: number; + additionalHeightInPx: number; +} + +export interface IGutterItemView extends IDisposable { + update(item: T): void; + layout(top: number, height: number, viewTop: number, viewHeight: number): void; +} + diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index 749b947af55..2e3929e236c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -20,10 +20,12 @@ } .monaco-workbench .merge-editor .code-view > .container > .gutter { - min-width: 20px; - /* background-color: yellow; */ - margin-right: 3px; + width: 24px; position: relative; + overflow: hidden; + + flex-shrink: 0; + flex-grow: 0; } .monaco-editor .merge-accept-conflict-glyph { @@ -34,9 +36,42 @@ background-color: rgb(170 175 170 / 15%); } -.merge-accept-gutter-marker { - min-height: 30px; - min-width: 20px; - background-color: yellow; +.gutter-item { position: absolute; } + +.merge-accept-gutter-marker { + width: 28px; + /* background-color: yellow; */ + + + margin-left: 4px; +} + +.merge-accept-gutter-marker .background { + height: 100%; + width: 50%; + position: absolute; +} + +.merge-accept-gutter-marker.multi-line .background { + border: 2px solid #323232FF; + border-right: 0; + left: 8px; + width: 10px; +} + +.merge-accept-gutter-marker .checkbox { + height: 100%; + width: 100%; + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; +} + +.merge-accept-gutter-marker .checkbox .accept-conflict-group.monaco-custom-toggle.monaco-checkbox { + margin: 0; + padding: 0; + background-color: #3C3C3CFF; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index c846f45c87f..b3f0ffcbbab 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -16,7 +16,7 @@ import { IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { BugIndicatingError } from 'vs/base/common/errors'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Direction, Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; @@ -30,15 +30,17 @@ import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IAction } from 'vs/base/common/actions'; import { Range } from 'vs/editor/common/core/range'; -import { Position } from 'vs/editor/common/core/position'; -import { acceptConflictIcon } from 'vs/workbench/contrib/mergeEditor/browser/icons'; -import { ConflictGroup, MergeState } from 'vs/workbench/contrib/mergeEditor/browser/model'; - +import { Checkbox, Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { EditorGutterWidget, IGutterItemInfo, IGutterItemView } from './editorGutterWidget'; +import { noBreakWhitespace } from 'vs/base/common/strings'; +import { ModifiedBaseRange, ModifiedBaseRangeState, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { autorun, derivedObservable, derivedWritebaleObservable, IObservable, ITransaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { Codicon } from 'vs/base/common/codicons'; export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false); export const ctxUsesColumnLayout = new RawContextKey('mergeEditorUsesColumnLayout', false); @@ -172,77 +174,13 @@ export class MergeEditor extends EditorPane { this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), undefined); this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); - - const input1LineNumberToMergeableDiffMap = new Map(); - - this.input1View.editor.onMouseDown(e => { - if (e.target.element && e.target.element.className.includes('merge-')) { - const lineNumber = e.target.position?.lineNumber; - if (lineNumber) { - const diff = input1LineNumberToMergeableDiffMap.get(lineNumber); - if (diff) { - const state = (model.getState(diff) || new MergeState(false, false, false)).toggleInput1(); - model.setConflictResolutionStatus(diff, state); - } - } - } - }); - - this.input1View.editor.changeDecorations(c => { - for (const mergeableDiff of model.mergeableDiffs) { - if (mergeableDiff.input1FullDiff) { - const lineNumber = mergeableDiff.input1FullDiff.modifiedRange.startLineNumber; - input1LineNumberToMergeableDiffMap.set(lineNumber, mergeableDiff); - c.addDecoration(Range.fromPositions(new Position(lineNumber, 0)), { - description: 'foo', - glyphMarginClassName: ThemeIcon.asClassName(acceptConflictIcon) + ' merge-accept-conflict-glyph', - }); - } - } - }); - - - - const input2LineNumberToMergeableDiffMap = new Map(); - - this.input2View.editor.onMouseDown(e => { - if (e.target.element && e.target.element.className.includes('merge-')) { - const lineNumber = e.target.position?.lineNumber; - if (lineNumber) { - const diff = input2LineNumberToMergeableDiffMap.get(lineNumber); - if (diff) { - const state = (model.getState(diff) || new MergeState(false, false, false)).toggleInput2(); - model.setConflictResolutionStatus(diff, state); - } - } - } - }); - - - - this.input2View.editor.changeDecorations(c => { - for (const mergeableDiff of model.mergeableDiffs) { - if (mergeableDiff.input2FullDiff) { - const lineNumber = mergeableDiff.input2FullDiff.modifiedRange.startLineNumber; - input2LineNumberToMergeableDiffMap.set(lineNumber, mergeableDiff); - c.addDecoration(Range.fromPositions(new Position(lineNumber, 1)), { - description: 'foo', - glyphMarginClassName: ThemeIcon.asClassName(acceptConflictIcon) + ' merge-accept-conflict-glyph', - }); - } - } - }); - - let input1Decorations = new Array(); let input2Decorations = new Array(); - - for (const m of model.mergeableDiffs) { - - if (!m.totalInput1Range.isEmpty) { + for (const m of model.modifiedBaseRanges) { + if (!m.input1Range.isEmpty) { input1Decorations.push({ - range: new Range(m.totalInput1Range.startLineNumber, 1, m.totalInput1Range.endLineNumberExclusive - 1, 1), + range: new Range(m.input1Range.startLineNumber, 1, m.input1Range.endLineNumberExclusive - 1, 1), options: { isWholeLine: true, className: 'merge-accept-foo', @@ -251,9 +189,9 @@ export class MergeEditor extends EditorPane { }); } - if (!m.totalInput2Range.isEmpty) { + if (!m.input2Range.isEmpty) { input2Decorations.push({ - range: new Range(m.totalInput2Range.startLineNumber, 1, m.totalInput2Range.endLineNumberExclusive - 1, 1), + range: new Range(m.input2Range.startLineNumber, 1, m.input2Range.endLineNumberExclusive - 1, 1), options: { isWholeLine: true, className: 'merge-accept-foo', @@ -262,20 +200,20 @@ export class MergeEditor extends EditorPane { }); } - const max = Math.max(m.totalInput1Range.lineCount, m.totalInput2Range.lineCount, 1); + const max = Math.max(m.input1Range.lineCount, m.input2Range.lineCount, 1); this.input1View.editor.changeViewZones(a => { a.addZone({ - afterLineNumber: m.totalInput1Range.endLineNumberExclusive - 1, - heightInLines: max - m.totalInput1Range.lineCount, + afterLineNumber: m.input1Range.endLineNumberExclusive - 1, + heightInLines: max - m.input1Range.lineCount, domNode: $('div.diagonal-fill'), }); }); this.input2View.editor.changeViewZones(a => { a.addZone({ - afterLineNumber: m.totalInput2Range.endLineNumberExclusive - 1, - heightInLines: max - m.totalInput2Range.lineCount, + afterLineNumber: m.input2Range.endLineNumberExclusive - 1, + heightInLines: max - m.input2Range.lineCount, domNode: $('div.diagonal-fill'), }); }); @@ -284,6 +222,61 @@ export class MergeEditor extends EditorPane { this.input1View.editor.deltaDecorations([], input1Decorations); this.input2View.editor.deltaDecorations([], input2Decorations); + new EditorGutterWidget(this.input1View.editor, this.input1View._gutterDiv, { + getIntersectingGutterItems: (range) => + model.modifiedBaseRanges + .filter((r) => r.input1Diffs.length > 0) + .map((baseRange, idx) => ({ + id: idx.toString(), + additionalHeightInPx: 0, + offsetInPx: 0, + range: baseRange.input1Range, + toggleState: derivedObservable( + 'toggle', + (reader) => model.getState(baseRange).read(reader)?.input1 + ), + setState(value, tx) { + model.setState( + baseRange, + ( + model.getState(baseRange).get() || + new ModifiedBaseRangeState(false, false, false) + ).withInput1(value), + tx + ); + }, + })), + createView: (item, target) => new ButtonView(item, target), + }); + + new EditorGutterWidget(this.input2View.editor, this.input2View._gutterDiv, { + getIntersectingGutterItems: (range) => + model.modifiedBaseRanges + .filter((r) => r.input2Diffs.length > 0) + .map((baseRange, idx) => ({ + id: idx.toString(), + additionalHeightInPx: 0, + offsetInPx: 0, + range: baseRange.input2Range, + baseRange, + toggleState: derivedObservable( + 'toggle', + (reader) => model.getState(baseRange).read(reader)?.input2 + ), + setState(value, tx) { + model.setState( + baseRange, + ( + model.getState(baseRange).get() || + new ModifiedBaseRangeState(false, false, false) + ).withInput2(value), + tx + ); + }, + })), + createView: (item, target) => new ButtonView(item, target), + }); + } protected override setEditorVisible(visible: boolean): void { @@ -324,6 +317,43 @@ export class MergeEditor extends EditorPane { } } +interface ButtonViewData extends IGutterItemInfo { + toggleState: IObservable; + setState(value: boolean, tx: ITransaction | undefined): void; +} + +class ButtonView extends Disposable implements IGutterItemView { + constructor(item: ButtonViewData, target: HTMLElement) { + super(); + + target.classList.add('merge-accept-gutter-marker'); + target.classList.add(item.range.lineCount > 1 ? 'multi-line' : 'single-line'); + + const checkBox = new Toggle({ isChecked: false, title: 'TODO', icon: Codicon.check, actionClassName: 'monaco-checkbox' }); + checkBox.domNode.classList.add('accept-conflict-group'); + + this._register( + autorun((reader) => { + const value = item.toggleState.read(reader); + checkBox.checked = value === true; + }, 'Update Toggle State') + ); + + this._register(checkBox.onChange(() => { + item.setState(checkBox.checked, undefined); + })); + + target.appendChild($('div.background', {}, noBreakWhitespace)); + target.appendChild($('div.checkbox', {}, checkBox.domNode)); + } + layout(top: number, height: number, viewTop: number, viewHeight: number): void { + + } + + update(baseRange: ButtonViewData): void { + } +} + interface ICodeEditorViewOptions { readonly: boolean; } @@ -336,7 +366,7 @@ class CodeEditorView implements IView { element: HTMLElement; private _titleElement: HTMLElement; private _editorElement: HTMLElement; - private _gutterDiv: HTMLElement; + public _gutterDiv: HTMLElement; minimumWidth: number = 10; maximumWidth: number = Number.MAX_SAFE_INTEGER; @@ -351,7 +381,7 @@ class CodeEditorView implements IView { private _title: IconLabel; public readonly editor: CodeEditorWidget; - private readonly gutter: EditorGutter; + // private readonly gutter: EditorGutterWidget; constructor( @@ -371,13 +401,11 @@ class CodeEditorView implements IView { this.editor = this.instantiationService.createInstance( CodeEditorWidget, this._editorElement, - { minimap: { enabled: false }, readOnly: this._options.readonly, /*glyphMargin: false,*/ lineNumbersMinChars: 2 }, + { minimap: { enabled: false }, readOnly: this._options.readonly, glyphMargin: false, lineNumbersMinChars: 2 }, { contributions: [] } ); this._title = new IconLabel(this._titleElement, { supportIcons: true }); - this.gutter = new EditorGutter(this.editor, this._gutterDiv); - console.log(this.gutter); // keep alive } public setModel(model: ITextModel, title: string, description: string | undefined): void { @@ -393,39 +421,3 @@ class CodeEditorView implements IView { this.editor.layout({ width: width - this._gutterDiv.clientWidth, height: height - this._titleElement.clientHeight }); } } - -class ReentrancyBarrier { - private isActive = false; - - public runExclusively(fn: () => void): void { - if (this.isActive) { - return; - } - this.isActive = true; - try { - fn(); - } finally { - this.isActive = false; - } - } -} - -class EditorGutter { - constructor( - private readonly _editor: ICodeEditor, - private readonly _domNode: HTMLElement, - ) { - this._editor.onDidScrollChange(() => { - this.render(); - }); - } - - private node = $('div.merge-accept-gutter-marker'); - - private render(): void { - this._domNode.append(this.node); - - const top = this._editor.getTopForLineNumber(10) - this._editor.getScrollTop(); - this.node.style.top = `${top}px`; - } -} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 081869931c3..989febccdb7 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -3,62 +3,64 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter } from 'vs/base/common/event'; import { compareBy, numberComparator } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; -import { ConflictGroup, LineEdit, LineEdits, LineDiff, MergeState, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { IObservable, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { ModifiedBaseRange, LineEdit, LineEdits, LineDiff, ModifiedBaseRangeState, LineRange, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; export class MergeEditorModelFactory { constructor( - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - //@IInstantiationService private readonly instantiationService: IInstantiationService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService ) { } public async create( - ancestor: ITextModel, + base: ITextModel, input1: ITextModel, input2: ITextModel, result: ITextModel, ): Promise { - const ancestorToInput1DiffPromise = this._editorWorkerService.computeDiff( - ancestor.uri, + const baseToInput1DiffPromise = this._editorWorkerService.computeDiff( + base.uri, input1.uri, false, 1000 ); - const ancestorToInput2DiffPromise = this._editorWorkerService.computeDiff( - ancestor.uri, + const baseToInput2DiffPromise = this._editorWorkerService.computeDiff( + base.uri, input2.uri, false, 1000 ); - const [ancestorToInput1Diff, ancestorToInput2Diff] = await Promise.all([ - ancestorToInput1DiffPromise, - ancestorToInput2DiffPromise, + const [baseToInput1Diff, baseToInput2Diff] = await Promise.all([ + baseToInput1DiffPromise, + baseToInput2DiffPromise, ]); const changesInput1 = - ancestorToInput1Diff?.changes.map((c) => - LineDiff.fromLineChange(c, ancestor, input1) + baseToInput1Diff?.changes.map((c) => + LineDiff.fromLineChange(c, base, input1) ) || []; const changesInput2 = - ancestorToInput2Diff?.changes.map((c) => - LineDiff.fromLineChange(c, ancestor, input2) + baseToInput2Diff?.changes.map((c) => + LineDiff.fromLineChange(c, base, input2) ) || []; return new MergeEditorModel( InternalSymbol, - ancestor, + base, input1, input2, result, changesInput1, changesInput2, + this._editorWorkerService ); } } @@ -66,20 +68,29 @@ export class MergeEditorModelFactory { const InternalSymbol = Symbol(); export class MergeEditorModel extends EditorModel { - private resultEdits = new ResultEdits([], this.ancestor, this.result); + private resultEdits = new ResultEdits([], this.base, this.result, this.editorWorkerService); constructor( symbol: typeof InternalSymbol, - readonly ancestor: ITextModel, + readonly base: ITextModel, readonly input1: ITextModel, readonly input2: ITextModel, readonly result: ITextModel, private readonly inputOneLinesDiffs: readonly LineDiff[], - private readonly inputTwoLinesDiffs: readonly LineDiff[] + private readonly inputTwoLinesDiffs: readonly LineDiff[], + private readonly editorWorkerService: IEditorWorkerService ) { super(); - result.setValue(ancestor.getValue()); + result.setValue(base.getValue()); + + this.resultEdits.onDidChange(() => { + transaction(tx => { + for (const [key, value] of this.modifiedBaseRangeStateStores) { + value.set(this.computeState(key), tx); + } + }); + }); /* // Apply all non-conflicts diffs @@ -96,60 +107,75 @@ export class MergeEditorModel extends EditorModel { } new LineEdits(lineEditsArr).apply(result); */ - - console.log(this, 'hello'); - (globalThis as any)['mergeEditorModel'] = this; } public get resultDiffs(): readonly LineDiff[] { return this.resultEdits.diffs; } - public readonly mergeableDiffs = ConflictGroup.partitionDiffs( - this.ancestor, + public readonly modifiedBaseRanges = ModifiedBaseRange.fromDiffs( + this.base, this.input1, this.inputOneLinesDiffs, this.input2, this.inputTwoLinesDiffs ); - public getState(conflict: ConflictGroup): MergeState | undefined { + private readonly modifiedBaseRangeStateStores = new Map>( + this.modifiedBaseRanges.map(s => ([s, new ObservableValue(new ModifiedBaseRangeState(false, false, false), 'State')])) + ); + + private computeState(baseRange: ModifiedBaseRange): ModifiedBaseRangeState | undefined { const existingDiff = this.resultEdits.findConflictingDiffs( - conflict.totalOriginalRange + baseRange.baseRange ); if (!existingDiff) { - return new MergeState(false, false, false); + return new ModifiedBaseRangeState(false, false, false); } - const input1Edit = conflict.getInput1LineEdit(); + const input1Edit = baseRange.getInput1LineEdit(); if (input1Edit && existingDiff.getLineEdit().equals(input1Edit)) { - return new MergeState(true, false, false); + return new ModifiedBaseRangeState(true, false, false); } - const input2Edit = conflict.getInput2LineEdit(); + const input2Edit = baseRange.getInput2LineEdit(); if (input2Edit && existingDiff.getLineEdit().equals(input2Edit)) { - return new MergeState(false, true, false); + return new ModifiedBaseRangeState(false, true, false); } return undefined; } - // Undo all edits of result that conflict with the conflict!! - public setConflictResolutionStatus( - conflict: ConflictGroup, - status: MergeState + public getState(baseRange: ModifiedBaseRange): IObservable { + const existingState = this.modifiedBaseRangeStateStores.get(baseRange); + if (!existingState) { + throw new BugIndicatingError('object must be from this instance'); + } + return existingState; + } + + public setState( + baseRange: ModifiedBaseRange, + state: ModifiedBaseRangeState, + transaction: ITransaction | undefined ): void { + const existingState = this.modifiedBaseRangeStateStores.get(baseRange); + if (!existingState) { + throw new BugIndicatingError('object must be from this instance'); + } + existingState.set(state, transaction); + const existingDiff = this.resultEdits.findConflictingDiffs( - conflict.totalOriginalRange + baseRange.baseRange ); if (existingDiff) { this.resultEdits.removeDiff(existingDiff); } - const edit = status.input1 - ? conflict.getInput1LineEdit() - : status.input2 - ? conflict.getInput2LineEdit() + const edit = state.input1 + ? baseRange.getInput1LineEdit() + : state.input2 + ? baseRange.getInput2LineEdit() : undefined; if (edit) { this.resultEdits.applyEditRelativeToOriginal(edit); @@ -158,12 +184,36 @@ export class MergeEditorModel extends EditorModel { } class ResultEdits { + private readonly barrier = new ReentrancyBarrier(); + private readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + constructor( private _diffs: LineDiff[], - private readonly originalTextModel: ITextModel, - private readonly textModel: ITextModel, + private readonly baseTextModel: ITextModel, + private readonly resultTextModel: ITextModel, + private readonly _editorWorkerService: IEditorWorkerService ) { this._diffs.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator)); + + resultTextModel.onDidChangeContent(e => { + this.barrier.runExclusively(() => { + this._editorWorkerService.computeDiff( + baseTextModel.uri, + resultTextModel.uri, + false, + 1000 + ).then(e => { + const diffs = + e?.changes.map((c) => + LineDiff.fromLineChange(c, baseTextModel, resultTextModel) + ) || []; + this._diffs = diffs; + + this.onDidChangeEmitter.fire(undefined); + }); + }); + }); } public get diffs(): readonly LineDiff[] { @@ -177,8 +227,9 @@ class ResultEdits { throw new BugIndicatingError(); } - const edits = new LineEdits([diffToRemove.getReverseLineEdit()]); - edits.apply(this.textModel); + this.barrier.runExclusivelyOrThrow(() => { + diffToRemove.getReverseLineEdit().apply(this.resultTextModel); + }); this._diffs = this._diffs.map((d) => d.modifiedRange.isAfter(diffToRemove.modifiedRange) @@ -204,16 +255,16 @@ class ResultEdits { for (let i = 0; i < this._diffs.length; i++) { const diff = this._diffs[i]; - if (diff.originalRange.intersects(edit.range)) { + if (diff.originalRange.touches(edit.range)) { throw new BugIndicatingError(); } else if (diff.originalRange.isAfter(edit.range)) { if (!firstAfter) { firstAfter = true; newDiffs.push(new LineDiff( - this.originalTextModel, + this.baseTextModel, edit.range, - this.textModel, + this.resultTextModel, new LineRange(edit.range.startLineNumber + delta, edit.newLines.length) )); } @@ -237,21 +288,22 @@ class ResultEdits { firstAfter = true; newDiffs.push(new LineDiff( - this.originalTextModel, + this.baseTextModel, edit.range, - this.textModel, + this.resultTextModel, new LineRange(edit.range.startLineNumber + delta, edit.newLines.length) )); } this._diffs = newDiffs; - const edits = new LineEdits([new LineEdit(edit.range.delta(delta), edit.newLines, edit.data)]); - edits.apply(this.textModel); + this.barrier.runExclusivelyOrThrow(() => { + new LineEdit(edit.range.delta(delta), edit.newLines).apply(this.resultTextModel); + }); } // TODO return many! public findConflictingDiffs(rangeInOriginalTextModel: LineRange): LineDiff | undefined { // TODO binary search - return this.diffs.find(d => d.originalRange.intersects(rangeInOriginalTextModel)); + return this.diffs.find(d => d.originalRange.touches(rangeInOriginalTextModel)); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model.ts b/src/vs/workbench/contrib/mergeEditor/browser/model.ts index deae7ace505..5d21dc17180 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model.ts @@ -9,11 +9,24 @@ import { Range } from 'vs/editor/common/core/range'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import { ITextModel } from 'vs/editor/common/model'; -export class LineEdits { - constructor(public readonly edits: readonly LineEdit[]) { +export class LineEdit { + constructor( + public readonly range: LineRange, + public readonly newLines: string[] + ) { } + public equals(other: LineEdit): boolean { + return this.range.equals(other.range) && equals(this.newLines, other.newLines); } + public apply(model: ITextModel): void { + new LineEdits([this]).apply(model); + } +} + +export class LineEdits { + constructor(public readonly edits: readonly LineEdit[]) { } + public apply(model: ITextModel): void { model.pushEditOperations( null, @@ -26,188 +39,6 @@ export class LineEdits { } } -export class LineEdit { - equals(other: LineEdit) { - return this.range.equals(other.range) && equals(this.newLines, other.newLines); - } - - constructor( - public readonly range: LineRange, - public readonly newLines: string[], - public readonly data: T - ) { } -} - -export class ConflictGroup { - /** - * diffs1 and diffs2 together with the conflict relation form a bipartite graph. - * This method computes strongly connected components of that graph while maintaining the side of each diff. - */ - public static partitionDiffs( - originalTextModel: ITextModel, - input1TextModel: ITextModel, - diffs1: readonly LineDiff[], - input2TextModel: ITextModel, - diffs2: readonly LineDiff[] - ): ConflictGroup[] { - const compareByStartLineNumber = compareBy( - (d) => d.originalRange.startLineNumber, - numberComparator - ); - - const queueDiffs1 = new ArrayQueue( - diffs1.slice().sort(compareByStartLineNumber) - ); - const queueDiffs2 = new ArrayQueue( - diffs2.slice().sort(compareByStartLineNumber) - ); - - const result = new Array(); - - while (true) { - const lastDiff1 = queueDiffs1.peekLast(); - const lastDiff2 = queueDiffs2.peekLast(); - - if ( - lastDiff1 && - (!lastDiff2 || - lastDiff1.originalRange.startLineNumber >= - lastDiff2.originalRange.startLineNumber) - ) { - queueDiffs1.removeLast(); - - const otherConflictingWith = - queueDiffs2.takeFromEndWhile((d) => d.conflicts(lastDiff1)) || []; - - const singleLinesDiff = LineDiff.hull(otherConflictingWith); - - const moreConflictingWith = - (singleLinesDiff && - queueDiffs1.takeFromEndWhile((d) => - d.conflicts(singleLinesDiff) - )) || - []; - moreConflictingWith.push(lastDiff1); - - result.push( - new ConflictGroup( - originalTextModel, - input1TextModel, - moreConflictingWith, - queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - input2TextModel, - otherConflictingWith, - queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - ) - ); - } else if (lastDiff2) { - queueDiffs2.removeLast(); - - const otherConflictingWith = - queueDiffs1.takeFromEndWhile((d) => d.conflicts(lastDiff2)) || []; - - const singleLinesDiff = LineDiff.hull(otherConflictingWith); - - const moreConflictingWith = - (singleLinesDiff && - queueDiffs2.takeFromEndWhile((d) => - d.conflicts(singleLinesDiff) - )) || - []; - moreConflictingWith.push(lastDiff2); - - result.push( - new ConflictGroup( - originalTextModel, - input1TextModel, - otherConflictingWith, - queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - input2TextModel, - moreConflictingWith, - queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - ) - ); - } else { - break; - } - } - - result.reverse(); - - return result; - } - - public readonly input1FullDiff = LineDiff.hull(this.input1Diffs); - public readonly input2FullDiff = LineDiff.hull(this.input2Diffs); - - public readonly totalOriginalRange: LineRange; - public readonly totalInput1Range: LineRange; - public readonly totalInput2Range: LineRange; - - constructor( - public readonly originalTextModel: ITextModel, - public readonly input1TextModel: ITextModel, - public readonly input1Diffs: readonly LineDiff[], - public readonly input1DeltaLineCount: number, - public readonly input2TextModel: ITextModel, - public readonly input2Diffs: readonly LineDiff[], - public readonly input2DeltaLineCount: number, - ) { - if (this.input1Diffs.length === 0 && this.input2Diffs.length === 0) { - throw new BugIndicatingError('must have at least one diff'); - } - - const input1Diff = - this.input1FullDiff || - new LineDiff( - originalTextModel, - this.input2FullDiff!.originalRange, - input1TextModel, - this.input2FullDiff!.originalRange.delta(input1DeltaLineCount) - ); - - const input2Diff = - this.input2FullDiff || - new LineDiff( - originalTextModel, - this.input1FullDiff!.originalRange, - input1TextModel, - this.input1FullDiff!.originalRange.delta(input2DeltaLineCount) - ); - - const results = LineDiff.alignOriginalRegion([input1Diff, input2Diff]); - this.totalOriginalRange = results[0].originalRange; - this.totalInput1Range = results[0].modifiedRange; - this.totalInput2Range = results[1].modifiedRange; - } - - public get isConflicting(): boolean { - return this.input1Diffs.length > 0 && this.input2Diffs.length > 0; - } - - public getInput1LineEdit(): LineEdit | undefined { - if (this.input1Diffs.length === 0) { - return undefined; - } - if (this.input1Diffs.length === 1) { - return this.input1Diffs[0].getLineEdit(); - } else { - throw new Error('Method not implemented.'); - } - } - - public getInput2LineEdit(): LineEdit | undefined { - if (this.input2Diffs.length === 0) { - return undefined; - } - if (this.input2Diffs.length === 1) { - return this.input2Diffs[0].getLineEdit(); - } else { - throw new Error('Method not implemented.'); - } - } -} - export class LineRange { public static hull(ranges: LineRange[]): LineRange | undefined { if (ranges.length === 0) { @@ -240,7 +71,10 @@ export class LineRange { return this.lineCount === 0; } - public intersects(other: LineRange): boolean { + /** + * Returns false if there is at least one line between `this` and `other`. + */ + public touches(other: LineRange): boolean { return ( this.endLineNumberExclusive >= other.startLineNumber && other.endLineNumberExclusive >= this.startLineNumber @@ -303,7 +137,7 @@ export class LineDiff { ); } - public static alignOriginalRegion(lineDiffs: readonly LineDiff[]): LineDiff[] { + public static alignOriginalRange(lineDiffs: readonly LineDiff[]): LineDiff[] { if (lineDiffs.length === 0) { return []; } @@ -344,7 +178,7 @@ export class LineDiff { public conflicts(other: LineDiff): boolean { this.ensureSameOriginalModel(other); - return this.originalRange.intersects(other.originalRange); + return this.originalRange.touches(other.originalRange); } public isStrictBefore(other: LineDiff): boolean { @@ -355,16 +189,14 @@ export class LineDiff { public getLineEdit(): LineEdit { return new LineEdit( this.originalRange, - this.getModifiedLines(), - undefined + this.getModifiedLines() ); } public getReverseLineEdit(): LineEdit { return new LineEdit( this.modifiedRange, - this.getOriginalLines(), - undefined + this.getOriginalLines() ); } @@ -386,34 +218,212 @@ export class LineDiff { } -export class MergeState { +/** + * Describes modifications in input 1 and input 2 for a specific range in base. + * + * The UI offers a mechanism to either apply all changes from input 1 or input 2 or both. + * + * Immutable. +*/ +export class ModifiedBaseRange { + /** + * diffs1 and diffs2 together with the conflict relation form a bipartite graph. + * This method computes strongly connected components of that graph while maintaining the side of each diff. + */ + public static fromDiffs( + originalTextModel: ITextModel, + input1TextModel: ITextModel, + diffs1: readonly LineDiff[], + input2TextModel: ITextModel, + diffs2: readonly LineDiff[] + ): ModifiedBaseRange[] { + const compareByStartLineNumber = compareBy( + (d) => d.originalRange.startLineNumber, + numberComparator + ); + + const queueDiffs1 = new ArrayQueue( + diffs1.slice().sort(compareByStartLineNumber) + ); + const queueDiffs2 = new ArrayQueue( + diffs2.slice().sort(compareByStartLineNumber) + ); + + const result = new Array(); + + while (true) { + const lastDiff1 = queueDiffs1.peekLast(); + const lastDiff2 = queueDiffs2.peekLast(); + + if ( + lastDiff1 && + (!lastDiff2 || + lastDiff1.originalRange.startLineNumber >= + lastDiff2.originalRange.startLineNumber) + ) { + queueDiffs1.removeLast(); + + const otherConflictingWith = + queueDiffs2.takeFromEndWhile((d) => d.conflicts(lastDiff1)) || []; + + const singleLinesDiff = LineDiff.hull(otherConflictingWith); + + const moreConflictingWith = + (singleLinesDiff && + queueDiffs1.takeFromEndWhile((d) => + d.conflicts(singleLinesDiff) + )) || + []; + moreConflictingWith.push(lastDiff1); + + result.push( + new ModifiedBaseRange( + originalTextModel, + input1TextModel, + moreConflictingWith, + queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + input2TextModel, + otherConflictingWith, + queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + ) + ); + } else if (lastDiff2) { + queueDiffs2.removeLast(); + + const otherConflictingWith = + queueDiffs1.takeFromEndWhile((d) => d.conflicts(lastDiff2)) || []; + + const singleLinesDiff = LineDiff.hull(otherConflictingWith); + + const moreConflictingWith = + (singleLinesDiff && + queueDiffs2.takeFromEndWhile((d) => + d.conflicts(singleLinesDiff) + )) || + []; + moreConflictingWith.push(lastDiff2); + + result.push( + new ModifiedBaseRange( + originalTextModel, + input1TextModel, + otherConflictingWith, + queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + input2TextModel, + moreConflictingWith, + queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, + ) + ); + } else { + break; + } + } + + result.reverse(); + + return result; + } + + private readonly input1FullDiff = LineDiff.hull(this.input1Diffs); + private readonly input2FullDiff = LineDiff.hull(this.input2Diffs); + + public readonly baseRange: LineRange; + public readonly input1Range: LineRange; + public readonly input2Range: LineRange; + + constructor( + public readonly baseTextModel: ITextModel, + public readonly input1TextModel: ITextModel, + public readonly input1Diffs: readonly LineDiff[], + public readonly input1DeltaLineCount: number, + public readonly input2TextModel: ITextModel, + public readonly input2Diffs: readonly LineDiff[], + public readonly input2DeltaLineCount: number, + ) { + if (this.input1Diffs.length === 0 && this.input2Diffs.length === 0) { + throw new BugIndicatingError('must have at least one diff'); + } + + const input1Diff = + this.input1FullDiff || + new LineDiff( + baseTextModel, + this.input2FullDiff!.originalRange, + input1TextModel, + this.input2FullDiff!.originalRange.delta(input1DeltaLineCount) + ); + + const input2Diff = + this.input2FullDiff || + new LineDiff( + baseTextModel, + this.input1FullDiff!.originalRange, + input1TextModel, + this.input1FullDiff!.originalRange.delta(input2DeltaLineCount) + ); + + const results = LineDiff.alignOriginalRange([input1Diff, input2Diff]); + this.baseRange = results[0].originalRange; + this.input1Range = results[0].modifiedRange; + this.input2Range = results[1].modifiedRange; + } + + public get isConflicting(): boolean { + return this.input1Diffs.length > 0 && this.input2Diffs.length > 0; + } + + public getInput1LineEdit(): LineEdit | undefined { + if (this.input1Diffs.length === 0) { + return undefined; + } + //new LineDiff(this.baseTextModel, this.tota) + if (this.input1Diffs.length === 1) { + return this.input1Diffs[0].getLineEdit(); + } else { + throw new Error('Method not implemented.'); + } + } + + public getInput2LineEdit(): LineEdit | undefined { + if (this.input2Diffs.length === 0) { + return undefined; + } + if (this.input2Diffs.length === 1) { + return this.input2Diffs[0].getLineEdit(); + } else { + throw new Error('Method not implemented.'); + } + } +} + +export class ModifiedBaseRangeState { constructor( public readonly input1: boolean, public readonly input2: boolean, public readonly input2First: boolean ) { } - public withInput1(value: boolean): MergeState { - return new MergeState( + public withInput1(value: boolean): ModifiedBaseRangeState { + return new ModifiedBaseRangeState( value, this.input2, value && this.isEmpty ? false : this.input2First ); } - public withInput2(value: boolean): MergeState { - return new MergeState( + public withInput2(value: boolean): ModifiedBaseRangeState { + return new ModifiedBaseRangeState( this.input1, value, value && this.isEmpty ? true : this.input2First ); } - public toggleInput1(): MergeState { + public toggleInput1(): ModifiedBaseRangeState { return this.withInput1(!this.input1); } - public toggleInput2(): MergeState { + public toggleInput2(): ModifiedBaseRangeState { return this.withInput2(!this.input2); } @@ -435,3 +445,31 @@ export class MergeState { return arr.join(','); } } + +export class ReentrancyBarrier { + private isActive = false; + + public runExclusively(fn: () => void): void { + if (this.isActive) { + return; + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } + + public runExclusivelyOrThrow(fn: () => void): void { + if (this.isActive) { + throw new BugIndicatingError(); + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } +} From 3601028fd6582dc3483c4d9d3a345879f5d0acc3 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 16 May 2022 16:06:32 +0200 Subject: [PATCH 023/270] Fixes unused imports. --- .../mergeEditor/browser/mergeEditor.ts | 56 +++++++++---------- .../mergeEditor/browser/mergeEditorModel.ts | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index b3f0ffcbbab..541b8995161 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -3,44 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/mergeEditor'; import { $, Dimension, reset } from 'vs/base/browser/dom'; +import { Direction, Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { IAction } from 'vs/base/common/actions'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { Color } from 'vs/base/common/color'; +import { BugIndicatingError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { noBreakWhitespace } from 'vs/base/common/strings'; +import 'vs/css!./media/mergeEditor'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { Range } from 'vs/editor/common/core/range'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { localize } from 'vs/nls'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { BugIndicatingError } from 'vs/base/common/errors'; +import { autorun, derivedObservable, IObservable, ITransaction } from 'vs/workbench/contrib/audioCues/browser/observable'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { Direction, Grid, IView, IViewSize, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; -import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; -import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { ModifiedBaseRangeState, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; -import { Color } from 'vs/base/common/color'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { localize } from 'vs/nls'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IAction } from 'vs/base/common/actions'; -import { Range } from 'vs/editor/common/core/range'; -import { Checkbox, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { EditorGutterWidget, IGutterItemInfo, IGutterItemView } from './editorGutterWidget'; -import { noBreakWhitespace } from 'vs/base/common/strings'; -import { ModifiedBaseRange, ModifiedBaseRangeState, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; -import { autorun, derivedObservable, derivedWritebaleObservable, IObservable, ITransaction } from 'vs/workbench/contrib/audioCues/browser/observable'; -import { Codicon } from 'vs/base/common/codicons'; export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false); export const ctxUsesColumnLayout = new RawContextKey('mergeEditorUsesColumnLayout', false); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 989febccdb7..2305e1b80d5 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -10,7 +10,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IObservable, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; -import { ModifiedBaseRange, LineEdit, LineEdits, LineDiff, ModifiedBaseRangeState, LineRange, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { ModifiedBaseRange, LineEdit, LineDiff, ModifiedBaseRangeState, LineRange, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; export class MergeEditorModelFactory { constructor( From d8e76554d1d4b36ebc216ce9689cba609660a397 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 16 May 2022 17:08:31 +0200 Subject: [PATCH 024/270] Avoid symbol instantiation. --- .../workbench/contrib/mergeEditor/browser/mergeEditorModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 2305e1b80d5..eb8a9f23856 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -65,7 +65,7 @@ export class MergeEditorModelFactory { } } -const InternalSymbol = Symbol(); +const InternalSymbol: unique symbol = null!; export class MergeEditorModel extends EditorModel { private resultEdits = new ResultEdits([], this.base, this.result, this.editorWorkerService); From 4f741d6370be488aa3d68d7eda76376a962ce885 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 16 May 2022 21:44:12 +0200 Subject: [PATCH 025/270] add ability to provide detail and description for `input1` and `input2`. This isn't wire up yet because it requires bigger changes in git-land --- .../browser/mergeEditor.contribution.ts | 90 ++++++++++++++----- .../mergeEditor/browser/mergeEditor.ts | 4 +- .../mergeEditor/browser/mergeEditorInput.ts | 28 ++++-- .../mergeEditor/browser/mergeEditorModel.ts | 14 ++- .../browser/mergeEditorSerializer.ts | 10 ++- 5 files changed, 111 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index 3f72bfca524..b4495ccc657 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -13,7 +13,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; import { ctxIsMergeEditor, ctxUsesColumnLayout, MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditor'; -import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { MergeEditorSerializer } from './mergeEditorSerializer'; import { Codicon } from 'vs/base/common/codicons'; @@ -71,38 +71,84 @@ registerAction2(class Open extends Action2 { }); } run(accessor: ServicesAccessor, ...args: any[]): void { - const validatedArgs = ITestMergeEditorArgs.validate(args[0]); - - function normalize(uri: URI | UriComponents | string): URI { - if (typeof uri === 'string') { - return URI.parse(uri); - } else { - return URI.revive(uri); - } - } + const validatedArgs = IRelaxedOpenArgs.validate(args[0]); const instaService = accessor.get(IInstantiationService); const input = instaService.createInstance( MergeEditorInput, - normalize(validatedArgs.ancestor), - normalize(validatedArgs.input1), - normalize(validatedArgs.input2), - normalize(validatedArgs.output), + validatedArgs.ancestor, + validatedArgs.input1, + validatedArgs.input2, + validatedArgs.output, ); accessor.get(IEditorService).openEditor(input); } }); -namespace ITestMergeEditorArgs { - export function validate(args: any): ITestMergeEditorArgs { - return args as ITestMergeEditorArgs; +namespace IRelaxedOpenArgs { + function toUri(obj: unknown): URI { + if (typeof obj === 'string') { + return URI.parse(obj, true); + } else if (obj && typeof obj === 'object') { + return URI.revive(obj); + } + throw new TypeError('invalid argument'); + } + + function isUriComponents(obj: unknown): obj is UriComponents { + if (!obj || typeof obj !== 'object') { + return false; + } + return typeof (obj).scheme === 'string' + && typeof (obj).authority === 'string' + && typeof (obj).path === 'string' + && typeof (obj).query === 'string' + && typeof (obj).fragment === 'string'; + } + + function toInputResource(obj: unknown): MergeEditorInputData { + if (typeof obj === 'string') { + return new MergeEditorInputData(URI.parse(obj, true), undefined, undefined); + } + if (!obj || typeof obj !== 'object') { + throw new TypeError('invalid argument'); + } + + if (isUriComponents(obj)) { + return new MergeEditorInputData(URI.revive(obj), undefined, undefined); + } + + let uri = toUri((obj).uri); + let detail = (obj).detail; + let description = (obj).description; + return new MergeEditorInputData(uri, detail, description); + } + + export function validate(obj: unknown): IOpenEditorArgs { + if (!obj || typeof obj !== 'object') { + throw new TypeError('invalid argument'); + } + const ancestor = toUri((obj).ancestor); + const output = toUri((obj).output); + const input1 = toInputResource((obj).input1); + const input2 = toInputResource((obj).input2); + return { ancestor, input1, input2, output }; } } -interface ITestMergeEditorArgs { - ancestor: URI | string; - input1: URI | string; - input2: URI | string; - output: URI | string; +type IRelaxedInputData = { uri: UriComponents; detail?: string; description?: string }; + +type IRelaxedOpenArgs = { + ancestor: UriComponents | string; + input1: IRelaxedInputData | string; + input2: IRelaxedInputData | string; + output: UriComponents | string; +}; + +interface IOpenEditorArgs { + ancestor: URI; + input1: MergeEditorInputData; + input2: MergeEditorInputData; + output: URI; } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 541b8995161..bb5cb6334e6 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -170,8 +170,8 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); - this.input1View.setModel(model.input1, localize('yours', 'Yours'), undefined); - this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), undefined); + this.input1View.setModel(model.input1, localize('yours', 'Yours'), model.input1Detail); + this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), model.input2Detail); this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); let input1Decorations = new Array(); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index da2ada49ee0..f6b69488351 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -20,11 +20,19 @@ import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/te export interface MergeEditorInputJSON { anchestor: URI; - inputOne: URI; - inputTwo: URI; + inputOne: { uri: URI; detail?: string; description?: string }; + inputTwo: { uri: URI; detail?: string; description?: string }; result: URI; } +export class MergeEditorInputData { + constructor( + readonly uri: URI, + readonly detail: string | undefined, + readonly description: string | undefined, + ) { } +} + export class MergeEditorInput extends AbstractTextResourceEditorInput { static readonly ID = 'mergeEditor.Input'; @@ -35,8 +43,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { constructor( private readonly _anchestor: URI, - private readonly _input1: URI, - private readonly _input2: URI, + private readonly _input1: MergeEditorInputData, + private readonly _input2: MergeEditorInputData, private readonly _result: URI, @IInstantiationService private readonly _instaService: IInstantiationService, @ITextModelService private readonly _textModelService: ITextModelService, @@ -94,14 +102,18 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { if (!this._model) { const anchestor = await this._textModelService.createModelReference(this._anchestor); - const input1 = await this._textModelService.createModelReference(this._input1); - const input2 = await this._textModelService.createModelReference(this._input2); + const input1 = await this._textModelService.createModelReference(this._input1.uri); + const input2 = await this._textModelService.createModelReference(this._input2.uri); const result = await this._textModelService.createModelReference(this._result); this._model = await this.mergeEditorModelFactory.create( anchestor.object.textEditorModel, input1.object.textEditorModel, + this._input1.detail, + this._input1.description, input2.object.textEditorModel, + this._input2.detail, + this._input2.description, result.object.textEditorModel ); @@ -121,8 +133,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput { return false; } return isEqual(this._anchestor, otherInput._anchestor) - && isEqual(this._input1, otherInput._input1) - && isEqual(this._input2, otherInput._input2) + && isEqual(this._input1.uri, otherInput._input1.uri) + && isEqual(this._input2.uri, otherInput._input2.uri) && isEqual(this._result, otherInput._result); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index eb8a9f23856..ef5003afc4c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -21,7 +21,11 @@ export class MergeEditorModelFactory { public async create( base: ITextModel, input1: ITextModel, + input1Detail: string | undefined, + input1Description: string | undefined, input2: ITextModel, + input2Detail: string | undefined, + input2Description: string | undefined, result: ITextModel, ): Promise { @@ -56,7 +60,11 @@ export class MergeEditorModelFactory { InternalSymbol, base, input1, + input1Detail, + input1Description, input2, + input2Detail, + input2Description, result, changesInput1, changesInput2, @@ -71,10 +79,14 @@ export class MergeEditorModel extends EditorModel { private resultEdits = new ResultEdits([], this.base, this.result, this.editorWorkerService); constructor( - symbol: typeof InternalSymbol, + _symbol: typeof InternalSymbol, readonly base: ITextModel, readonly input1: ITextModel, + readonly input1Detail: string | undefined, + readonly input1Description: string | undefined, readonly input2: ITextModel, + readonly input2Detail: string | undefined, + readonly input2Description: string | undefined, readonly result: ITextModel, private readonly inputOneLinesDiffs: readonly LineDiff[], private readonly inputTwoLinesDiffs: readonly LineDiff[], diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts index 2480f0950ba..d4e52947bb8 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts @@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { parse } from 'vs/base/common/marshalling'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorSerializer } from 'vs/workbench/common/editor'; -import { MergeEditorInput, MergeEditorInputJSON } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { MergeEditorInput, MergeEditorInputJSON, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; export class MergeEditorSerializer implements IEditorSerializer { @@ -22,7 +22,13 @@ export class MergeEditorSerializer implements IEditorSerializer { deserialize(instantiationService: IInstantiationService, raw: string): MergeEditorInput | undefined { try { const data = parse(raw); - return instantiationService.createInstance(MergeEditorInput, data.anchestor, data.inputOne, data.inputTwo, data.result); + return instantiationService.createInstance( + MergeEditorInput, + data.anchestor, + new MergeEditorInputData(data.inputOne.uri, data.inputOne.detail, data.inputOne.description), + new MergeEditorInputData(data.inputTwo.uri, data.inputTwo.detail, data.inputTwo.description), + data.result + ); } catch (err) { onUnexpectedError(err); return undefined; From bf7dc6646f063898682b30ab48c42d1c2961f697 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Thu, 24 Mar 2022 13:45:23 +0000 Subject: [PATCH 026/270] Use maxTokenizationLineLength in monarch --- .../standalone/browser/standaloneLanguages.ts | 5 +- .../standalone/common/monarch/monarchLexer.ts | 19 ++++++- .../standalone/test/browser/monarch.test.ts | 57 ++++++++++++++++--- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index f738b97ed24..decc60790f5 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -23,6 +23,7 @@ import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneT import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** * Register information about a new language. @@ -374,7 +375,7 @@ export function registerTokensProviderFactory(languageId: string, factory: Token if (isATokensProvider(result)) { return createTokenizationSupportAdapter(languageId, result); } - return new MonarchTokenizer(StandaloneServices.get(ILanguageService), StandaloneServices.get(IStandaloneThemeService), languageId, compile(languageId, result)); + return new MonarchTokenizer(StandaloneServices.get(ILanguageService), StandaloneServices.get(IStandaloneThemeService), languageId, compile(languageId, result), StandaloneServices.get(IConfigurationService)); } }; return languages.TokenizationRegistry.registerFactory(languageId, adaptedFactory); @@ -405,7 +406,7 @@ export function setTokensProvider(languageId: string, provider: TokensProvider | */ export function setMonarchTokensProvider(languageId: string, languageDef: IMonarchLanguage | Thenable): IDisposable { const create = (languageDef: IMonarchLanguage) => { - return new MonarchTokenizer(StandaloneServices.get(ILanguageService), StandaloneServices.get(IStandaloneThemeService), languageId, compile(languageId, languageDef)); + return new MonarchTokenizer(StandaloneServices.get(ILanguageService), StandaloneServices.get(IStandaloneThemeService), languageId, compile(languageId, languageDef), StandaloneServices.get(IConfigurationService)); }; if (isThenable(languageDef)) { return registerTokensProviderFactory(languageId, { create: () => languageDef }); diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index bdc490fdb41..dde538afac9 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -10,11 +10,12 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as languages from 'vs/editor/common/languages'; -import { NullState } from 'vs/editor/common/languages/nullTokenize'; +import { NullState, nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize'; import { TokenTheme } from 'vs/editor/common/languages/supports/tokenization'; import { ILanguageService } from 'vs/editor/common/languages/language'; import * as monarchCommon from 'vs/editor/standalone/common/monarch/monarchCommon'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneTheme'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const CACHE_STACK_DEPTH = 5; @@ -394,8 +395,9 @@ export class MonarchTokenizer implements languages.ITokenizationSupport { private readonly _embeddedLanguages: { [languageId: string]: boolean }; public embeddedLoaded: Promise; private readonly _tokenizationRegistryListener: IDisposable; + private _maxTokenizationLineLength: number; - constructor(languageService: ILanguageService, standaloneThemeService: IStandaloneThemeService, languageId: string, lexer: monarchCommon.ILexer) { + constructor(languageService: ILanguageService, standaloneThemeService: IStandaloneThemeService, languageId: string, lexer: monarchCommon.ILexer, @IConfigurationService private readonly _configurationService: IConfigurationService) { this._languageService = languageService; this._standaloneThemeService = standaloneThemeService; this._languageId = languageId; @@ -423,6 +425,16 @@ export class MonarchTokenizer implements languages.ITokenizationSupport { emitting = false; } }); + this._maxTokenizationLineLength = this._configurationService.getValue('editor.maxTokenizationLineLength', { + overrideIdentifier: this._languageId + }); + this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.maxTokenizationLineLength')) { + this._maxTokenizationLineLength = this._configurationService.getValue('editor.maxTokenizationLineLength', { + overrideIdentifier: this._languageId + }); + } + }); } public dispose(): void { @@ -473,6 +485,9 @@ export class MonarchTokenizer implements languages.ITokenizationSupport { } public tokenizeEncoded(line: string, hasEOL: boolean, lineState: languages.IState): languages.EncodedTokenizationResult { + if (line.length >= this._maxTokenizationLineLength) { + return nullTokenizeEncoded(this._languageService.languageIdCodec.encodeLanguageId(this._languageId), lineState); + } const tokensCollector = new MonarchModernTokensCollector(this._languageService, this._standaloneThemeService.getColorTheme().tokenTheme); const endLineState = this._tokenize(line, hasEOL, lineState, tokensCollector); return tokensCollector.finalize(endLineState); diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index 5e67bcb4cba..22487d563aa 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -11,11 +11,13 @@ import { compile } from 'vs/editor/standalone/common/monarch/monarchCompile'; import { Token, TokenizationRegistry } from 'vs/editor/common/languages'; import { IMonarchLanguage } from 'vs/editor/standalone/common/monarch/monarchTypes'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { StandaloneConfigurationService } from 'vs/editor/standalone/browser/standaloneServices'; suite('Monarch', () => { - function createMonarchTokenizer(languageService: ILanguageService, languageId: string, language: IMonarchLanguage): MonarchTokenizer { - return new MonarchTokenizer(languageService, null!, languageId, compile(languageId, language)); + function createMonarchTokenizer(languageService: ILanguageService, languageId: string, language: IMonarchLanguage, configurationService: IConfigurationService): MonarchTokenizer { + return new MonarchTokenizer(languageService, null!, languageId, compile(languageId, language), configurationService); } function getTokens(tokenizer: MonarchTokenizer, lines: string[]): Token[][] { @@ -32,6 +34,7 @@ suite('Monarch', () => { test('Ensure @rematch and nextEmbedded can be used together in Monarch grammar', () => { const disposables = new DisposableStore(); const languageService = disposables.add(new LanguageService()); + const configurationService = new StandaloneConfigurationService(); disposables.add(languageService.registerLanguage({ id: 'sql' })); disposables.add(TokenizationRegistry.register('sql', createMonarchTokenizer(languageService, 'sql', { tokenizer: { @@ -39,7 +42,7 @@ suite('Monarch', () => { [/./, 'token'] ] } - }))); + }, configurationService))); const SQL_QUERY_START = '(SELECT|INSERT|UPDATE|DELETE|CREATE|REPLACE|ALTER|WITH)'; const tokenizer = createMonarchTokenizer(languageService, 'test1', { tokenizer: { @@ -63,7 +66,7 @@ suite('Monarch', () => { ], endStringWithSQL: [[/"""/, { token: 'string.quote', next: '@popall', nextEmbedded: '@pop', },]], } - }); + }, configurationService); const lines = [ `mysql_query("""SELECT * FROM table_name WHERE ds = ''""")`, @@ -106,6 +109,7 @@ suite('Monarch', () => { }); test('microsoft/monaco-editor#1235: Empty Line Handling', () => { + const configurationService = new StandaloneConfigurationService(); const languageService = new LanguageService(); const tokenizer = createMonarchTokenizer(languageService, 'test', { tokenizer: { @@ -125,7 +129,7 @@ suite('Monarch', () => { // No possible rule to detect an empty line and @pop? ], }, - }); + }, configurationService); const lines = [ `// This comment \\`, @@ -163,6 +167,7 @@ suite('Monarch', () => { }); test('microsoft/monaco-editor#2265: Exit a state at end of line', () => { + const configurationService = new StandaloneConfigurationService(); const languageService = new LanguageService(); const tokenizer = createMonarchTokenizer(languageService, 'test', { includeLF: true, @@ -179,7 +184,7 @@ suite('Monarch', () => { [/[^\d]+/, ''] ] } - }); + }, configurationService); const lines = [ `PRINT 10 * 20`, @@ -211,6 +216,7 @@ suite('Monarch', () => { }); test('issue #115662: monarchCompile function need an extra option which can control replacement', () => { + const configurationService = new StandaloneConfigurationService(); const languageService = new LanguageService(); const tokenizer1 = createMonarchTokenizer(languageService, 'test', { @@ -230,7 +236,7 @@ suite('Monarch', () => { }, ], }, - }); + }, configurationService); const tokenizer2 = createMonarchTokenizer(languageService, 'test', { ignoreCase: false, @@ -242,7 +248,7 @@ suite('Monarch', () => { }, ], }, - }); + }, configurationService); const lines = [ `@ham` @@ -265,6 +271,7 @@ suite('Monarch', () => { }); test('microsoft/monaco-editor#2424: Allow to target @@', () => { + const configurationService = new StandaloneConfigurationService(); const languageService = new LanguageService(); const tokenizer = createMonarchTokenizer(languageService, 'test', { @@ -277,7 +284,7 @@ suite('Monarch', () => { }, ], }, - }); + }, configurationService); const lines = [ `@@` @@ -292,4 +299,36 @@ suite('Monarch', () => { languageService.dispose(); }); + test('microsoft/monaco-editor#3025: Check maxTokenizationLineLength before tokenizing', () => { + const configurationService = new StandaloneConfigurationService(); + const languageService = new LanguageService(); + + const tokenizer = createMonarchTokenizer(languageService, 'test', { + maxTokenizationLineLength: 4, + tokenizer: { + root: [ + { + regex: /ham/, + action: { token: 'ham' } + }, + ], + }, + }, configurationService); + + const lines = [ + 'ham', // length 3, should be tokenized + 'hamham' // length 6, should NOT be tokenized + ]; + + const actualTokens = getTokens(tokenizer, lines); + assert.deepStrictEqual(actualTokens, [ + [ + new Token(0, 'ham.test', 'test'), + ], [ + new Token(0, '', 'test') + ] + ]); + languageService.dispose(); + }); + }); From d1fcde7fdd1d80195fae5d7a162c42e3c1d233f3 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Thu, 14 Apr 2022 17:40:41 +0100 Subject: [PATCH 027/270] fix bad rebase --- src/vs/editor/standalone/common/monarch/monarchLexer.ts | 5 ++++- src/vs/editor/standalone/test/browser/monarch.test.ts | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index dde538afac9..72988e4212f 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -10,7 +10,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as languages from 'vs/editor/common/languages'; -import { NullState, nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize'; +import { NullState, nullTokenizeEncoded, nullTokenize } from 'vs/editor/common/languages/nullTokenize'; import { TokenTheme } from 'vs/editor/common/languages/supports/tokenization'; import { ILanguageService } from 'vs/editor/common/languages/language'; import * as monarchCommon from 'vs/editor/standalone/common/monarch/monarchCommon'; @@ -479,6 +479,9 @@ export class MonarchTokenizer implements languages.ITokenizationSupport { } public tokenize(line: string, hasEOL: boolean, lineState: languages.IState): languages.TokenizationResult { + if (line.length >= this._maxTokenizationLineLength) { + return nullTokenize(this._languageId, lineState); + } const tokensCollector = new MonarchClassicTokensCollector(); const endLineState = this._tokenize(line, hasEOL, lineState, tokensCollector); return tokensCollector.finalize(endLineState); diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index 22487d563aa..da034e57b4e 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -299,12 +299,14 @@ suite('Monarch', () => { languageService.dispose(); }); - test('microsoft/monaco-editor#3025: Check maxTokenizationLineLength before tokenizing', () => { + test('microsoft/monaco-editor#3025: Check maxTokenizationLineLength before tokenizing', async () => { const configurationService = new StandaloneConfigurationService(); const languageService = new LanguageService(); + // Set maxTokenizationLineLength to 4 so that "ham" works but "hamham" would fail + await configurationService.updateValue('editor.maxTokenizationLineLength', 4); + const tokenizer = createMonarchTokenizer(languageService, 'test', { - maxTokenizationLineLength: 4, tokenizer: { root: [ { From 4182f3b2ad6f762f24df8da490bce0e403a0fdce Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 17 May 2022 13:46:32 +0200 Subject: [PATCH 028/270] show `detail` atop of code view --- .../mergeEditor/browser/media/mergeEditor.css | 3 ++- .../mergeEditor/browser/mergeEditor.ts | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index 2e3929e236c..0b9caf7faf8 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ .monaco-workbench .merge-editor .code-view > .title { - padding: 0 0 0 10px; + padding: 0 10px; height: 30px; display: flex; align-content: center; + justify-content: space-between; } .monaco-workbench .merge-editor .code-view > .title .monaco-icon-label { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index bb5cb6334e6..3cb4562da9e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -170,9 +170,9 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); - this.input1View.setModel(model.input1, localize('yours', 'Yours'), model.input1Detail); - this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), model.input2Detail); - this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true })); + this.input1View.setModel(model.input1, localize('yours', 'Yours'), model.input1Detail, model.input1Description); + this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), model.input2Detail, model.input1Description); + this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true }), undefined); let input1Decorations = new Array(); let input2Decorations = new Array(); @@ -364,9 +364,9 @@ class CodeEditorView implements IView { // preferredHeight?: number | undefined; element: HTMLElement; - private _titleElement: HTMLElement; - private _editorElement: HTMLElement; - public _gutterDiv: HTMLElement; + private readonly _titleElement: HTMLElement; + private readonly _editorElement: HTMLElement; + public readonly _gutterDiv: HTMLElement; minimumWidth: number = 10; maximumWidth: number = Number.MAX_SAFE_INTEGER; @@ -378,7 +378,8 @@ class CodeEditorView implements IView { private readonly _onDidChange = new Emitter(); readonly onDidChange = this._onDidChange.event; - private _title: IconLabel; + private readonly _title: IconLabel; + private readonly _detail: IconLabel; public readonly editor: CodeEditorWidget; // private readonly gutter: EditorGutterWidget; @@ -406,11 +407,13 @@ class CodeEditorView implements IView { ); this._title = new IconLabel(this._titleElement, { supportIcons: true }); + this._detail = new IconLabel(this._titleElement, { supportIcons: true }); } - public setModel(model: ITextModel, title: string, description: string | undefined): void { + public setModel(model: ITextModel, title: string, description: string | undefined, detail: string | undefined): void { this.editor.setModel(model); this._title.setLabel(title, description); + this._detail.setLabel('', detail); } layout(width: number, height: number, top: number, left: number): void { From 7a511786185e49b39b85e71cf8a01006b3e1e4bb Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 17 May 2022 13:46:42 +0200 Subject: [PATCH 029/270] git should open merge editor with detail and description (tag and commit-hash) --- extensions/git/src/commands.ts | 47 ++++++++++++++++++++++++++++- extensions/git/src/git.ts | 9 ++++-- extensions/git/src/repository.ts | 12 ++------ extensions/git/src/test/git.test.ts | 6 ++++ 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 14138c13bc5..da619d4feb5 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -15,7 +15,7 @@ import { Git, Stash } from './git'; import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; -import { fromGitUri, toGitUri, isGitUri } from './uri'; +import { fromGitUri, toGitUri, isGitUri, toMergeUris } from './uri'; import { grep, isDescendant, pathEquals, relativePath } from './util'; import { LogLevel, OutputChannelLogger } from './log'; import { GitTimelineItem } from './timelineProvider'; @@ -405,6 +405,51 @@ export class CommandCenter { } } + @command('_git.openMergeEditor') + async openMergeEditor(uri: unknown) { + if (!(uri instanceof Uri)) { + return; + } + const repo = this.model.getRepository(uri); + if (!repo) { + return; + } + + + type InputData = { uri: Uri; detail?: string; description?: string }; + const mergeUris = toMergeUris(uri); + let input1: InputData = { uri: mergeUris.ours }; + let input2: InputData = { uri: mergeUris.theirs }; + + try { + const [head, mergeHead] = await Promise.all([repo.getCommit('HEAD'), repo.getCommit('MERGE_HEAD')]); + // ours (current branch and commit) + input1.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', '); + input1.description = head.hash.substring(0, 7); + + // theirs + input2.detail = mergeHead.refNames.join(', '); + input2.description = mergeHead.hash.substring(0, 7); + + } catch (error) { + // not so bad, can continue with just uris + console.error('FAILED to read HEAD, MERGE_HEAD commits'); + console.error(error); + } + + const options = { + ancestor: mergeUris.base, + input1, + input2, + output: uri + }; + + await commands.executeCommand( + '_open.mergeEditor', + options + ); + } + async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise { if (!url || typeof url !== 'string') { url = await pickRemoteSource({ diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 8fdab7f659b..a511db761a6 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -354,7 +354,7 @@ function sanitizePath(path: string): string { return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } -const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B'; +const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; export interface ICloneOptions { readonly parentPath: string; @@ -660,6 +660,7 @@ export interface Commit { authorName?: string; authorEmail?: string; commitDate?: Date; + refNames: string[]; } export class GitStatusParser { @@ -790,7 +791,7 @@ export function parseGitmodules(raw: string): Submodule[] { return result; } -const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; +const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; export function parseGitCommits(data: string): Commit[] { let commits: Commit[] = []; @@ -801,6 +802,7 @@ export function parseGitCommits(data: string): Commit[] { let authorDate; let commitDate; let parents; + let refNames; let message; let match; @@ -810,7 +812,7 @@ export function parseGitCommits(data: string): Commit[] { break; } - [, ref, authorName, authorEmail, authorDate, commitDate, parents, message] = match; + [, ref, authorName, authorEmail, authorDate, commitDate, parents, refNames, message] = match; if (message[message.length - 1] === '\n') { message = message.substr(0, message.length - 1); @@ -825,6 +827,7 @@ export function parseGitCommits(data: string): Commit[] { authorName: ` ${authorName}`.substr(1), authorEmail: ` ${authorEmail}`.substr(1), commitDate: new Date(Number(commitDate) * 1000), + refNames: refNames.split(',').map(s => s.trim()) }); } while (true); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4a81c8fac64..ebe1105ab94 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -13,7 +13,7 @@ import { AutoFetcher } from './autofetch'; import { debounce, memoize, throttle } from './decorators'; import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; import { StatusBarCommands } from './statusbar'; -import { toGitUri, toMergeUris } from './uri'; +import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { LogLevel, OutputChannelLogger } from './log'; @@ -603,16 +603,10 @@ class ResourceCommandResolver { const title = this.getTitle(resource); if (!resource.leftUri && resource.rightUri && resource.type === Status.BOTH_MODIFIED) { - const mergeUris = toMergeUris(resource.rightUri); return { - command: '_open.mergeEditor', + command: '_git.openMergeEditor', title: localize('open.merge', "Open Merge"), - arguments: [{ - ancestor: mergeUris.base, - input1: mergeUris.ours, - input2: mergeUris.theirs, - output: resource.rightUri - }] + arguments: [resource.rightUri] }; } else if (!resource.leftUri) { return { diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index c0d0d2003c9..3b225157c3b 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -205,6 +205,7 @@ john.doe@mail.com 1580811030 1580811031 8e5a374372b8393906c7e380dbb09349c5385554 +main,branch This is a commit message.\x00`; assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ @@ -215,6 +216,7 @@ This is a commit message.\x00`; authorName: 'John Doe', authorEmail: 'john.doe@mail.com', commitDate: new Date(1580811031000), + refNames: ['main', 'branch'], }]); }); @@ -225,6 +227,7 @@ john.doe@mail.com 1580811030 1580811031 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 +main This is a commit message.\x00`; assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ @@ -235,6 +238,7 @@ This is a commit message.\x00`; authorName: 'John Doe', authorEmail: 'john.doe@mail.com', commitDate: new Date(1580811031000), + refNames: ['main'], }]); }); @@ -245,6 +249,7 @@ john.doe@mail.com 1580811030 1580811031 +main This is a commit message.\x00`; assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ @@ -255,6 +260,7 @@ This is a commit message.\x00`; authorName: 'John Doe', authorEmail: 'john.doe@mail.com', commitDate: new Date(1580811031000), + refNames: ['main'], }]); }); }); From 73b2655e9b2d21388ecbd8bb22ed599bd1c0e5a3 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 May 2022 01:30:32 +0200 Subject: [PATCH 030/270] Lots of bug fixing & minor improvements. --- src/vs/base/common/arrays.ts | 30 +- .../mergeEditor/browser/editorGutter.ts | 141 ++++++ .../mergeEditor/browser/editorGutterWidget.ts | 100 ----- .../mergeEditor/browser/mergeEditor.ts | 401 ++++++++++-------- .../mergeEditor/browser/mergeEditorModel.ts | 255 +++++++---- .../contrib/mergeEditor/browser/model.ts | 235 ++++------ .../contrib/mergeEditor/browser/utils.ts | 169 ++++++++ 7 files changed, 818 insertions(+), 513 deletions(-) create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts delete mode 100644 src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/utils.ts diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index e876d9d4304..826d3558c35 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -640,12 +640,38 @@ function getActualStartIndex(array: T[], start: number): number { return start < 0 ? Math.max(start + array.length, 0) : Math.min(start, array.length); } +/** + * When comparing two values, + * a negative number indicates that the first value is less than the second, + * a positive number indicates that the first value is greater than the second, + * and zero indicates that neither is the case. +*/ +export type CompareResult = number; + +export namespace CompareResult { + export function isLessThan(result: CompareResult): boolean { + return result < 0; + } + + export function isGreaterThan(result: CompareResult): boolean { + return result > 0; + } + + export function isNeitherLessOrGreaterThan(result: CompareResult): boolean { + return result === 0; + } + + export const greaterThan = 1; + export const lessThan = -1; + export const neitherLessOrGreaterThan = 0; +} + /** * A comparator `c` defines a total order `<=` on `T` as following: * `c(a, b) <= 0` iff `a` <= `b`. * We also have `c(a, b) == 0` iff `c(b, a) == 0`. */ -export type Comparator = (a: T, b: T) => number; +export type Comparator = (a: T, b: T) => CompareResult; export function compareBy(selector: (item: TItem) => TCompareBy, comparator: Comparator): Comparator { return (a, b) => comparator(selector(a), selector(b)); @@ -706,7 +732,7 @@ export class ArrayQueue { /** * Constructs a queue that is backed by the given array. Runtime is O(1). */ - constructor(private readonly items: T[]) { } + constructor(private readonly items: readonly T[]) { } get length(): number { return this.lastIdx - this.firstIdx + 1; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts new file mode 100644 index 00000000000..c4390489209 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { autorun, IReader, observableFromEvent, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; + +export class EditorGutter< + T extends IGutterItemInfo = IGutterItemInfo + > extends Disposable { + private readonly scrollTop = observableFromEvent( + this._editor.onDidScrollChange, + (e) => this._editor.getScrollTop() + ); + private readonly modelAttached = observableFromEvent( + this._editor.onDidChangeModel, + (e) => this._editor.hasModel() + ); + + private readonly viewZoneChanges = new ObservableValue(0, 'counter'); + + constructor( + private readonly _editor: CodeEditorWidget, + private readonly _domNode: HTMLElement, + private readonly itemProvider: IGutterItemProvider + ) { + super(); + this._domNode.className = 'gutter'; + this._register(autorun((reader) => this.render(reader), 'Render')); + + this._editor.onDidChangeViewZones(e => { + this.viewZoneChanges.set(this.viewZoneChanges.get() + 1, undefined); + }); + } + + private readonly views = new Map(); + + private render(reader: IReader): void { + if (!this.modelAttached.read(reader)) { + return; + } + this.viewZoneChanges.read(reader); + const scrollTop = this.scrollTop.read(reader); + + const visibleRanges = this._editor.getVisibleRanges(); + const unusedIds = new Set(this.views.keys()); + + if (visibleRanges.length > 0) { + const visibleRange = visibleRanges[0]; + + const visibleRange2 = new LineRange( + visibleRange.startLineNumber, + visibleRange.endLineNumber - visibleRange.startLineNumber + ); + + const gutterItems = this.itemProvider.getIntersectingGutterItems( + visibleRange2, + reader + ); + + const lineHeight = this._editor.getOptions().get(EditorOption.lineHeight); + + for (const gutterItem of gutterItems) { + if (!gutterItem.range.touches(visibleRange2)) { + continue; + } + + unusedIds.delete(gutterItem.id); + let view = this.views.get(gutterItem.id); + if (!view) { + const viewDomNode = document.createElement('div'); + viewDomNode.className = 'gutter-item'; + this._domNode.appendChild(viewDomNode); + const itemView = this.itemProvider.createView( + gutterItem, + viewDomNode + ); + view = new ManagedGutterItemView(itemView, viewDomNode); + this.views.set(gutterItem.id, view); + } else { + view.gutterItemView.update(gutterItem); + } + + const top = + this._editor.getTopForLineNumber(gutterItem.range.startLineNumber - 1) - + scrollTop + lineHeight; + + // +1 line and -lineHeight makes the height to cover view zones at the end of the range. + const bottom = + this._editor.getTopForLineNumber(gutterItem.range.endLineNumberExclusive + 1) - + scrollTop - lineHeight; + + const height = bottom - top; + + view.domNode.style.top = `${top}px`; + view.domNode.style.height = `${height}px`; + + view.gutterItemView.layout(top, height, 0, -1); + } + } + + for (const id of unusedIds) { + const view = this.views.get(id)!; + view.gutterItemView.dispose(); + this._domNode.removeChild(view.domNode); + this.views.delete(id); + } + } +} + +class ManagedGutterItemView { + constructor( + public readonly gutterItemView: IGutterItemView, + public readonly domNode: HTMLDivElement + ) { } +} + +export interface IGutterItemProvider { + getIntersectingGutterItems(range: LineRange, reader: IReader): TItem[]; + + createView(item: TItem, target: HTMLElement): IGutterItemView; +} + +export interface IGutterItemInfo { + id: string; + range: LineRange; + + // To accommodate view zones: + offsetInPx: number; + additionalHeightInPx: number; +} + +export interface IGutterItemView extends IDisposable { + update(item: T): void; + layout(top: number, height: number, viewTop: number, viewHeight: number): void; +} + diff --git a/src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts b/src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts deleted file mode 100644 index 9b3954eed48..00000000000 --- a/src/vs/workbench/contrib/mergeEditor/browser/editorGutterWidget.ts +++ /dev/null @@ -1,100 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; - -export class EditorGutterWidget { - constructor( - private readonly _editor: ICodeEditor, - private readonly _domNode: HTMLElement, - private readonly itemProvider: IGutterItemProvider, - ) { - this._domNode.className = 'gutter'; - - this._editor.onDidScrollChange(() => { - this.render(); - }); - } - - private readonly views = new Map(); - - private render(): void { - const visibleRange = this._editor.getVisibleRanges()[0]; - const visibleRange2 = new LineRange( - visibleRange.startLineNumber, - visibleRange.endLineNumber - visibleRange.startLineNumber - ); - - const gutterItems = this.itemProvider.getIntersectingGutterItems(visibleRange2); - - const lineHeight = this._editor.getOptions().get(EditorOption.lineHeight); - - const unusedIds = new Set(this.views.keys()); - for (const gutterItem of gutterItems) { - if (!gutterItem.range.touches(visibleRange2)) { - continue; - } - - unusedIds.delete(gutterItem.id); - let view = this.views.get(gutterItem.id); - if (!view) { - const viewDomNode = document.createElement('div'); - viewDomNode.className = 'gutter-item'; - this._domNode.appendChild(viewDomNode); - const itemView = this.itemProvider.createView(gutterItem, viewDomNode); - view = new ManagedGutterItemView(itemView, viewDomNode); - this.views.set(gutterItem.id, view); - } - - const scrollTop = this._editor.getScrollTop(); - const top = this._editor.getTopForLineNumber(gutterItem.range.startLineNumber) - scrollTop; - const height = lineHeight * gutterItem.range.lineCount; - - view.domNode.style.top = `${top}px`; - view.domNode.style.height = `${height}px`; - - view.gutterItemView.layout(top, height, 0, -1); - } - - for (const id of unusedIds) { - const view = this.views.get(id)!; - view.gutterItemView.dispose(); - this._domNode.removeChild(view.domNode); - this.views.delete(id); - } - } -} - -class ManagedGutterItemView { - constructor( - public readonly gutterItemView: IGutterItemView, - public readonly domNode: HTMLDivElement - ) { } -} - -export interface IGutterItemProvider { - // onDidChange - getIntersectingGutterItems(range: LineRange): TItem[]; - - createView(item: TItem, target: HTMLElement): IGutterItemView; -} - -export interface IGutterItemInfo { - id: string; - range: LineRange; - - // To accommodate view zones: - offsetInPx: number; - additionalHeightInPx: number; -} - -export interface IGutterItemView extends IDisposable { - update(item: T): void; - layout(top: number, height: number, viewTop: number, viewHeight: number): void; -} - diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 3cb4562da9e..f46dce24ff1 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -15,13 +15,17 @@ import { Color } from 'vs/base/common/color'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { deepClone } from 'vs/base/common/objects'; import { noBreakWhitespace } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/mergeEditor'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; @@ -36,11 +40,12 @@ import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { autorun, derivedObservable, IObservable, ITransaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { autorun, derivedObservable, IObservable, ITransaction, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import { ModifiedBaseRangeState, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel'; +import { applyObservableDecorations, n, ReentrancyBarrier, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; -import { EditorGutterWidget, IGutterItemInfo, IGutterItemView } from './editorGutterWidget'; +import { EditorGutter, IGutterItemInfo, IGutterItemView } from './editorGutter'; export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false); export const ctxUsesColumnLayout = new RawContextKey('mergeEditorUsesColumnLayout', false); @@ -51,11 +56,11 @@ export class MergeEditor extends EditorPane { private readonly _sessionDisposables = new DisposableStore(); - private _grid!: Grid; + private _grid!: Grid; - private readonly input1View = this.instantiation.createInstance(CodeEditorView, { readonly: true }); - private readonly input2View = this.instantiation.createInstance(CodeEditorView, { readonly: true }); - private readonly inputResultView = this.instantiation.createInstance(CodeEditorView, { readonly: false }); + private readonly input1View = this.instantiation.createInstance(InputCodeEditorView, 1, { readonly: true }); + private readonly input2View = this.instantiation.createInstance(InputCodeEditorView, 2, { readonly: true }); + private readonly inputResultView = this.instantiation.createInstance(ResultCodeEditorView, { readonly: false }); private readonly _ctxIsMergeEditor: IContextKey; private readonly _ctxUsesColumnLayout: IContextKey; @@ -68,6 +73,7 @@ export class MergeEditor extends EditorPane { @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService, + @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, ) { super(MergeEditor.ID, telemetryService, themeService, storageService); @@ -75,6 +81,7 @@ export class MergeEditor extends EditorPane { this._ctxUsesColumnLayout = ctxUsesColumnLayout.bindTo(_contextKeyService); const reentrancyBarrier = new ReentrancyBarrier(); + this._store.add(this.input1View.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { @@ -100,6 +107,7 @@ export class MergeEditor extends EditorPane { } })); + // TODO@jrieken make this proper: add menu id and allow extensions to contribute const toolbarMenu = this._menuService.createMenu(MenuId.MergeToolbar, this._contextKeyService); const toolbarMenuDisposables = new DisposableStore(); @@ -128,6 +136,11 @@ export class MergeEditor extends EditorPane { super.dispose(); } + // TODO use this method & make it private + getEditorOptions(resource: URI): IEditorConfiguration { + return deepClone(this.textResourceConfigurationService.getValue(resource)); + } + protected createEditor(parent: HTMLElement): void { parent.classList.add('merge-editor'); @@ -138,14 +151,14 @@ export class MergeEditor extends EditorPane { { size: 38, groups: [{ - data: this.input1View + data: this.input1View.view }, { - data: this.input2View + data: this.input2View.view }] }, { size: 62, - data: this.inputResultView + data: this.inputResultView.view }, ] }, { @@ -170,113 +183,48 @@ export class MergeEditor extends EditorPane { this._sessionDisposables.clear(); const model = await input.resolve(); - this.input1View.setModel(model.input1, localize('yours', 'Yours'), model.input1Detail, model.input1Description); - this.input2View.setModel(model.input2, localize('theirs', 'Theirs',), model.input2Detail, model.input1Description); - this.inputResultView.setModel(model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true }), undefined); + this.input1View.setModel(model, model.input1, localize('yours', 'Yours'), model.input1Detail, model.input1Description); + this.input2View.setModel(model, model.input2, localize('theirs', 'Theirs',), model.input2Detail, model.input1Description); + this.inputResultView.setModel(model, model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true }), undefined); - let input1Decorations = new Array(); - let input2Decorations = new Array(); + // TODO: Update editor options! + const input1ViewZoneIds: string[] = []; + const input2ViewZoneIds: string[] = []; for (const m of model.modifiedBaseRanges) { - if (!m.input1Range.isEmpty) { - input1Decorations.push({ - range: new Range(m.input1Range.startLineNumber, 1, m.input1Range.endLineNumberExclusive - 1, 1), - options: { - isWholeLine: true, - className: 'merge-accept-foo', - description: 'foo2' - } - }); - } - - if (!m.input2Range.isEmpty) { - input2Decorations.push({ - range: new Range(m.input2Range.startLineNumber, 1, m.input2Range.endLineNumberExclusive - 1, 1), - options: { - isWholeLine: true, - className: 'merge-accept-foo', - description: 'foo2' - } - }); - } - const max = Math.max(m.input1Range.lineCount, m.input2Range.lineCount, 1); this.input1View.editor.changeViewZones(a => { - a.addZone({ + input1ViewZoneIds.push(a.addZone({ afterLineNumber: m.input1Range.endLineNumberExclusive - 1, heightInLines: max - m.input1Range.lineCount, domNode: $('div.diagonal-fill'), - }); + })); }); this.input2View.editor.changeViewZones(a => { - a.addZone({ + input2ViewZoneIds.push(a.addZone({ afterLineNumber: m.input2Range.endLineNumberExclusive - 1, heightInLines: max - m.input2Range.lineCount, domNode: $('div.diagonal-fill'), - }); + })); }); } - this.input1View.editor.deltaDecorations([], input1Decorations); - this.input2View.editor.deltaDecorations([], input2Decorations); - - new EditorGutterWidget(this.input1View.editor, this.input1View._gutterDiv, { - getIntersectingGutterItems: (range) => - model.modifiedBaseRanges - .filter((r) => r.input1Diffs.length > 0) - .map((baseRange, idx) => ({ - id: idx.toString(), - additionalHeightInPx: 0, - offsetInPx: 0, - range: baseRange.input1Range, - toggleState: derivedObservable( - 'toggle', - (reader) => model.getState(baseRange).read(reader)?.input1 - ), - setState(value, tx) { - model.setState( - baseRange, - ( - model.getState(baseRange).get() || - new ModifiedBaseRangeState(false, false, false) - ).withInput1(value), - tx - ); - }, - })), - createView: (item, target) => new ButtonView(item, target), + this._sessionDisposables.add({ + dispose: () => { + this.input1View.editor.changeViewZones(a => { + for (const zone of input1ViewZoneIds) { + a.removeZone(zone); + } + }); + this.input2View.editor.changeViewZones(a => { + for (const zone of input2ViewZoneIds) { + a.removeZone(zone); + } + }); + } }); - - new EditorGutterWidget(this.input2View.editor, this.input2View._gutterDiv, { - getIntersectingGutterItems: (range) => - model.modifiedBaseRanges - .filter((r) => r.input2Diffs.length > 0) - .map((baseRange, idx) => ({ - id: idx.toString(), - additionalHeightInPx: 0, - offsetInPx: 0, - range: baseRange.input2Range, - baseRange, - toggleState: derivedObservable( - 'toggle', - (reader) => model.getState(baseRange).read(reader)?.input2 - ), - setState(value, tx) { - model.setState( - baseRange, - ( - model.getState(baseRange).get() || - new ModifiedBaseRangeState(false, false, false) - ).withInput2(value), - tx - ); - }, - })), - createView: (item, target) => new ButtonView(item, target), - }); - } protected override setEditorVisible(visible: boolean): void { @@ -307,23 +255,170 @@ export class MergeEditor extends EditorPane { toggleLayout(): void { if (!this._usesColumnLayout) { - this._grid.moveView(this.inputResultView, Sizing.Distribute, this.input1View, Direction.Right); + this._grid.moveView(this.inputResultView.view, Sizing.Distribute, this.input1View.view, Direction.Right); } else { - this._grid.moveView(this.inputResultView, this._grid.height * .62, this.input1View, Direction.Down); - this._grid.moveView(this.input2View, Sizing.Distribute, this.input1View, Direction.Right); + this._grid.moveView(this.inputResultView.view, this._grid.height * .62, this.input1View.view, Direction.Down); + this._grid.moveView(this.input2View.view, Sizing.Distribute, this.input1View.view, Direction.Right); } this._usesColumnLayout = !this._usesColumnLayout; this._ctxUsesColumnLayout.set(this._usesColumnLayout); } } -interface ButtonViewData extends IGutterItemInfo { +interface ICodeEditorViewOptions { + readonly: boolean; +} + + +abstract class CodeEditorView extends Disposable { + private readonly _model = new ObservableValue(undefined, 'model'); + protected readonly model: IObservable = this._model; + + protected readonly htmlElements = n('div.code-view', [ + n('div.title', { $: 'title' }), + n('div.container', [ + n('div.gutter', { $: 'gutterDiv' }), + n('div', { $: 'editor' }), + ]), + ]); + + private readonly _onDidViewChange = new Emitter(); + + public readonly view: IView = { + element: this.htmlElements.root, + minimumWidth: 10, + maximumWidth: Number.MAX_SAFE_INTEGER, + minimumHeight: 10, + maximumHeight: Number.MAX_SAFE_INTEGER, + onDidChange: this._onDidViewChange.event, + + layout: (width: number, height: number, top: number, left: number) => { + setStyle(this.htmlElements.root, { width, height, top, left }); + this.editor.layout({ + width: width - this.htmlElements.gutterDiv.clientWidth, + height: height - this.htmlElements.title.clientHeight, + }); + } + + // preferredWidth?: number | undefined; + // preferredHeight?: number | undefined; + // priority?: LayoutPriority | undefined; + // snap?: boolean | undefined; + }; + + private readonly _title = new IconLabel(this.htmlElements.title, { supportIcons: true }); + private readonly _detail = new IconLabel(this.htmlElements.title, { supportIcons: true }); + + public readonly editor = this.instantiationService.createInstance( + CodeEditorWidget, + this.htmlElements.editor, + { + minimap: { enabled: false }, + readOnly: this._options.readonly, + glyphMargin: false, + lineNumbersMinChars: 2, + }, + { contributions: [] } + ); + + constructor( + private readonly _options: ICodeEditorViewOptions, + @IInstantiationService + private readonly instantiationService: IInstantiationService + ) { + super(); + } + + public setModel( + model: MergeEditorModel, + textModel: ITextModel, + title: string, + description: string | undefined, + detail: string | undefined + ): void { + this.editor.setModel(textModel); + this._title.setLabel(title, description); + this._detail.setLabel('', detail); + + this._model.set(model, undefined); + } +} + +class InputCodeEditorView extends CodeEditorView { + private readonly decorations = derivedObservable('decorations', reader => { + const model = this.model.read(reader); + if (!model) { + return []; + } + const result = new Array(); + for (const m of model.modifiedBaseRanges) { + const range = m.getInputRange(this.inputNumber); + if (!range.isEmpty) { + result.push({ + range: new Range(range.startLineNumber, 1, range.endLineNumberExclusive - 1, 1), + options: { + isWholeLine: true, + className: 'merge-accept-foo', + description: 'foo2' + } + }); + } + } + return result; + }); + + constructor( + public readonly inputNumber: 1 | 2, + options: ICodeEditorViewOptions, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(options, instantiationService); + + this._register(applyObservableDecorations(this.editor, this.decorations)); + + this._register( + new EditorGutter(this.editor, this.htmlElements.gutterDiv, { + getIntersectingGutterItems: (range, reader) => { + const model = this.model.read(reader); + if (!model) { return []; } + return model.modifiedBaseRanges + .filter((r) => r.getInputDiffs(this.inputNumber).length > 0) + .map((baseRange, idx) => ({ + id: idx.toString(), + additionalHeightInPx: 0, + offsetInPx: 0, + range: baseRange.getInputRange(this.inputNumber), + toggleState: derivedObservable('toggle', (reader) => + model + .getState(baseRange) + .read(reader) + .getInput(this.inputNumber) + ), + setState: (value, tx) => + model.setState( + baseRange, + model + .getState(baseRange) + .get() + .withInputValue(this.inputNumber, value), + tx + ), + })); + }, + createView: (item, target) => + new MergeConflictGutterItemView(item, target), + }) + ); + } +} + +interface MergeConflictData extends IGutterItemInfo { toggleState: IObservable; setState(value: boolean, tx: ITransaction | undefined): void; } -class ButtonView extends Disposable implements IGutterItemView { - constructor(item: ButtonViewData, target: HTMLElement) { +class MergeConflictGutterItemView extends Disposable implements IGutterItemView { + constructor(private item: MergeConflictData, target: HTMLElement) { super(); target.classList.add('merge-accept-gutter-marker'); @@ -334,93 +429,57 @@ class ButtonView extends Disposable implements IGutterItemView { this._register( autorun((reader) => { - const value = item.toggleState.read(reader); + const value = this.item.toggleState.read(reader); checkBox.checked = value === true; }, 'Update Toggle State') ); this._register(checkBox.onChange(() => { - item.setState(checkBox.checked, undefined); + this.item.setState(checkBox.checked, undefined); })); target.appendChild($('div.background', {}, noBreakWhitespace)); target.appendChild($('div.checkbox', {}, checkBox.domNode)); } + layout(top: number, height: number, viewTop: number, viewHeight: number): void { } - update(baseRange: ButtonViewData): void { + update(baseRange: MergeConflictData): void { + this.item = baseRange; } } -interface ICodeEditorViewOptions { - readonly: boolean; -} - -class CodeEditorView implements IView { - - // preferredWidth?: number | undefined; - // preferredHeight?: number | undefined; - - element: HTMLElement; - private readonly _titleElement: HTMLElement; - private readonly _editorElement: HTMLElement; - public readonly _gutterDiv: HTMLElement; - - minimumWidth: number = 10; - maximumWidth: number = Number.MAX_SAFE_INTEGER; - minimumHeight: number = 10; - maximumHeight: number = Number.MAX_SAFE_INTEGER; - // priority?: LayoutPriority | undefined; - // snap?: boolean | undefined; - - private readonly _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - private readonly _title: IconLabel; - private readonly _detail: IconLabel; - - public readonly editor: CodeEditorWidget; - // private readonly gutter: EditorGutterWidget; - +class ResultCodeEditorView extends CodeEditorView { + private readonly decorations = derivedObservable('decorations', reader => { + const model = this.model.read(reader); + if (!model) { + return []; + } + const result = new Array(); + for (const m of model.resultDiffs.read(reader)) { + const range = m.modifiedRange; + if (!range.isEmpty) { + result.push({ + range: new Range(range.startLineNumber, 1, range.endLineNumberExclusive - 1, 1), + options: { + isWholeLine: true, + className: 'merge-accept-foo', + description: 'foo2' + } + }); + } + } + return result; + }); constructor( - private readonly _options: ICodeEditorViewOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService + options: ICodeEditorViewOptions, + @IInstantiationService instantiationService: IInstantiationService ) { - this.element = $( - 'div.code-view', - {}, - this._titleElement = $('div.title'), - $('div.container', {}, - this._gutterDiv = $('div.gutter'), - this._editorElement = $('div'), - ), - ); + super(options, instantiationService); - this.editor = this.instantiationService.createInstance( - CodeEditorWidget, - this._editorElement, - { minimap: { enabled: false }, readOnly: this._options.readonly, glyphMargin: false, lineNumbersMinChars: 2 }, - { contributions: [] } - ); - - this._title = new IconLabel(this._titleElement, { supportIcons: true }); - this._detail = new IconLabel(this._titleElement, { supportIcons: true }); - } - - public setModel(model: ITextModel, title: string, description: string | undefined, detail: string | undefined): void { - this.editor.setModel(model); - this._title.setLabel(title, description); - this._detail.setLabel('', detail); - } - - layout(width: number, height: number, top: number, left: number): void { - this.element.style.width = `${width}px`; - this.element.style.height = `${height}px`; - this.element.style.top = `${top}px`; - this.element.style.left = `${left}px`; - this.editor.layout({ width: width - this._gutterDiv.clientWidth, height: height - this._titleElement.clientHeight }); + this._register(applyObservableDecorations(this.editor, this.decorations)); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index ef5003afc4c..024aa737d42 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { compareBy, CompareResult, equals, numberComparator } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IObservable, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; -import { ModifiedBaseRange, LineEdit, LineDiff, ModifiedBaseRangeState, LineRange, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { ModifiedBaseRange, LineEdit, LineDiff, ModifiedBaseRangeState, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; +import { leftJoin, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils'; export class MergeEditorModelFactory { constructor( @@ -41,10 +42,17 @@ export class MergeEditorModelFactory { false, 1000 ); + const baseToResultDiffPromise = this._editorWorkerService.computeDiff( + base.uri, + result.uri, + false, + 1000 + ); - const [baseToInput1Diff, baseToInput2Diff] = await Promise.all([ + const [baseToInput1Diff, baseToInput2Diff, baseToResultDiff] = await Promise.all([ baseToInput1DiffPromise, baseToInput2DiffPromise, + baseToResultDiffPromise ]); const changesInput1 = @@ -55,6 +63,10 @@ export class MergeEditorModelFactory { baseToInput2Diff?.changes.map((c) => LineDiff.fromLineChange(c, base, input2) ) || []; + const changesResult = + baseToResultDiff?.changes.map((c) => + LineDiff.fromLineChange(c, base, result) + ) || []; return new MergeEditorModel( InternalSymbol, @@ -68,7 +80,8 @@ export class MergeEditorModelFactory { result, changesInput1, changesInput2, - this._editorWorkerService + changesResult, + this._editorWorkerService, ); } } @@ -76,7 +89,7 @@ export class MergeEditorModelFactory { const InternalSymbol: unique symbol = null!; export class MergeEditorModel extends EditorModel { - private resultEdits = new ResultEdits([], this.base, this.result, this.editorWorkerService); + private resultEdits: ResultEdits; constructor( _symbol: typeof InternalSymbol, @@ -88,77 +101,104 @@ export class MergeEditorModel extends EditorModel { readonly input2Detail: string | undefined, readonly input2Description: string | undefined, readonly result: ITextModel, - private readonly inputOneLinesDiffs: readonly LineDiff[], - private readonly inputTwoLinesDiffs: readonly LineDiff[], + private readonly input1LinesDiffs: readonly LineDiff[], + private readonly input2LinesDiffs: readonly LineDiff[], + resultDiffs: LineDiff[], private readonly editorWorkerService: IEditorWorkerService ) { super(); - result.setValue(base.getValue()); - + this.resultEdits = new ResultEdits(resultDiffs, this.base, this.result, this.editorWorkerService); this.resultEdits.onDidChange(() => { - transaction(tx => { - for (const [key, value] of this.modifiedBaseRangeStateStores) { - value.set(this.computeState(key), tx); - } - }); + this.recomputeState(); }); - - /* - // Apply all non-conflicts diffs - const lineEditsArr: LineEdit[] = []; - for (const diff of this.mergeableDiffs) { - if (!diff.isConflicting) { - for (const d of diff.inputOneDiffs) { - lineEditsArr.push(d.getLineEdit()); - } - for (const d of diff.inputTwoDiffs) { - lineEditsArr.push(d.getLineEdit()); - } - } - } - new LineEdits(lineEditsArr).apply(result); - */ + this.recomputeState(); } - public get resultDiffs(): readonly LineDiff[] { + private recomputeState(): void { + transaction(tx => { + const baseRangeWithStoreAndTouchingDiffs = leftJoin( + this.modifiedBaseRangeStateStores, + this.resultEdits.diffs.get(), + (baseRange, diff) => + baseRange[0].baseRange.touches(diff.originalRange) + ? CompareResult.neitherLessOrGreaterThan + : LineRange.compareByStart( + baseRange[0].baseRange, + diff.originalRange + ) + ); + + for (const row of baseRangeWithStoreAndTouchingDiffs) { + row.left[1].set(this.computeState(row.left[0], row.rights), tx); + } + }); + } + + public mergeNonConflictingDiffs(): void { + transaction((tx) => { + for (const m of this.modifiedBaseRanges) { + if (m.isConflicting) { + continue; + } + this.setState( + m, + m.input1Diffs.length > 0 + ? ModifiedBaseRangeState.default.withInput1(true) + : ModifiedBaseRangeState.default.withInput2(true), + tx + ); + } + }); + } + + public get resultDiffs(): IObservable { return this.resultEdits.diffs; } public readonly modifiedBaseRanges = ModifiedBaseRange.fromDiffs( this.base, this.input1, - this.inputOneLinesDiffs, + this.input1LinesDiffs, this.input2, - this.inputTwoLinesDiffs + this.input2LinesDiffs ); - private readonly modifiedBaseRangeStateStores = new Map>( - this.modifiedBaseRanges.map(s => ([s, new ObservableValue(new ModifiedBaseRangeState(false, false, false), 'State')])) + private readonly modifiedBaseRangeStateStores = new Map>( + this.modifiedBaseRanges.map(s => ([s, new ObservableValue(ModifiedBaseRangeState.default, 'State')])) ); - private computeState(baseRange: ModifiedBaseRange): ModifiedBaseRangeState | undefined { - const existingDiff = this.resultEdits.findConflictingDiffs( - baseRange.baseRange - ); - if (!existingDiff) { - return new ModifiedBaseRangeState(false, false, false); + private computeState(baseRange: ModifiedBaseRange, conflictingDiffs?: LineDiff[]): ModifiedBaseRangeState { + if (!conflictingDiffs) { + conflictingDiffs = this.resultEdits.findTouchingDiffs( + baseRange.baseRange + ); } - const input1Edit = baseRange.getInput1LineEdit(); - if (input1Edit && existingDiff.getLineEdit().equals(input1Edit)) { - return new ModifiedBaseRangeState(true, false, false); + if (conflictingDiffs.length === 0) { + return ModifiedBaseRangeState.default; + } + const conflictingEdits = conflictingDiffs.map((d) => d.getLineEdit()); + + function editsAgreeWithDiffs(diffs: readonly LineDiff[]): boolean { + return equals( + conflictingEdits, + diffs.map((d) => d.getLineEdit()), + (a, b) => a.equals(b) + ); } - const input2Edit = baseRange.getInput2LineEdit(); - if (input2Edit && existingDiff.getLineEdit().equals(input2Edit)) { - return new ModifiedBaseRangeState(false, true, false); + if (editsAgreeWithDiffs(baseRange.input1Diffs)) { + return ModifiedBaseRangeState.default.withInput1(true); + } + if (editsAgreeWithDiffs(baseRange.input2Diffs)) { + return ModifiedBaseRangeState.default.withInput2(true); } - return undefined; + return ModifiedBaseRangeState.conflicting; } - public getState(baseRange: ModifiedBaseRange): IObservable { + public getState(baseRange: ModifiedBaseRange): IObservable { const existingState = this.modifiedBaseRangeStateStores.get(baseRange); if (!existingState) { throw new BugIndicatingError('object must be from this instance'); @@ -177,22 +217,26 @@ export class MergeEditorModel extends EditorModel { } existingState.set(state, transaction); - const existingDiff = this.resultEdits.findConflictingDiffs( + const conflictingDiffs = this.resultEdits.findTouchingDiffs( baseRange.baseRange ); - if (existingDiff) { - this.resultEdits.removeDiff(existingDiff); + if (conflictingDiffs) { + this.resultEdits.removeDiffs(conflictingDiffs, transaction); } - const edit = state.input1 - ? baseRange.getInput1LineEdit() + const diff = state.input1 + ? baseRange.input1CombinedDiff : state.input2 - ? baseRange.getInput2LineEdit() + ? baseRange.input2CombinedDiff : undefined; - if (edit) { - this.resultEdits.applyEditRelativeToOriginal(edit); + if (diff) { + this.resultEdits.applyEditRelativeToOriginal(diff.getLineEdit(), transaction); } } + + public getResultRange(baseRange: LineRange): LineRange { + return this.resultEdits.getResultRange(baseRange); + } } class ResultEdits { @@ -201,12 +245,13 @@ class ResultEdits { public readonly onDidChange = this.onDidChangeEmitter.event; constructor( - private _diffs: LineDiff[], + diffs: LineDiff[], private readonly baseTextModel: ITextModel, private readonly resultTextModel: ITextModel, private readonly _editorWorkerService: IEditorWorkerService ) { - this._diffs.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator)); + diffs.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator)); + this._diffs.set(diffs, undefined); resultTextModel.onDidChangeContent(e => { this.barrier.runExclusively(() => { @@ -220,7 +265,7 @@ class ResultEdits { e?.changes.map((c) => LineDiff.fromLineChange(c, baseTextModel, resultTextModel) ) || []; - this._diffs = diffs; + this._diffs.set(diffs, undefined); this.onDidChangeEmitter.fire(undefined); }); @@ -228,47 +273,55 @@ class ResultEdits { }); } - public get diffs(): readonly LineDiff[] { - return this._diffs; - } + private readonly _diffs = new ObservableValue([], 'diffs'); - public removeDiff(diffToRemove: LineDiff): void { - const len = this._diffs.length; - this._diffs = this._diffs.filter((d) => d !== diffToRemove); - if (len === this._diffs.length) { - throw new BugIndicatingError(); + public readonly diffs: IObservable = this._diffs; + + public removeDiffs(diffToRemoves: LineDiff[], transaction: ITransaction | undefined): void { + diffToRemoves.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator)); + diffToRemoves.reverse(); + + let diffs = this._diffs.get(); + + for (const diffToRemove of diffToRemoves) { + // TODO improve performance + const len = diffs.length; + diffs = diffs.filter((d) => d !== diffToRemove); + if (len === diffs.length) { + throw new BugIndicatingError(); + } + + this.barrier.runExclusivelyOrThrow(() => { + diffToRemove.getReverseLineEdit().apply(this.resultTextModel); + }); + + diffs = diffs.map((d) => + d.modifiedRange.isAfter(diffToRemove.modifiedRange) + ? new LineDiff( + d.originalTextModel, + d.originalRange, + d.modifiedTextModel, + d.modifiedRange.delta( + diffToRemove.originalRange.lineCount - diffToRemove.modifiedRange.lineCount + ) + ) + : d + ); } - this.barrier.runExclusivelyOrThrow(() => { - diffToRemove.getReverseLineEdit().apply(this.resultTextModel); - }); - - this._diffs = this._diffs.map((d) => - d.modifiedRange.isAfter(diffToRemove.modifiedRange) - ? new LineDiff( - d.originalTextModel, - d.originalRange, - d.modifiedTextModel, - d.modifiedRange.delta( - diffToRemove.originalRange.lineCount - diffToRemove.modifiedRange.lineCount - ) - ) - : d - ); + this._diffs.set(diffs, transaction); } /** * Edit must be conflict free. */ - public applyEditRelativeToOriginal(edit: LineEdit): void { + public applyEditRelativeToOriginal(edit: LineEdit, transaction: ITransaction | undefined): void { let firstAfter = false; let delta = 0; const newDiffs = new Array(); - for (let i = 0; i < this._diffs.length; i++) { - const diff = this._diffs[i]; - + for (const diff of this._diffs.get()) { if (diff.originalRange.touches(edit.range)) { - throw new BugIndicatingError(); + throw new BugIndicatingError('Edit must be conflict free.'); } else if (diff.originalRange.isAfter(edit.range)) { if (!firstAfter) { firstAfter = true; @@ -306,16 +359,30 @@ class ResultEdits { new LineRange(edit.range.startLineNumber + delta, edit.newLines.length) )); } - this._diffs = newDiffs; + this._diffs.set(newDiffs, transaction); this.barrier.runExclusivelyOrThrow(() => { new LineEdit(edit.range.delta(delta), edit.newLines).apply(this.resultTextModel); }); } - // TODO return many! - public findConflictingDiffs(rangeInOriginalTextModel: LineRange): LineDiff | undefined { - // TODO binary search - return this.diffs.find(d => d.originalRange.touches(rangeInOriginalTextModel)); + public findTouchingDiffs(baseRange: LineRange): LineDiff[] { + return this.diffs.get().filter(d => d.originalRange.touches(baseRange)); + } + + public getResultRange(baseRange: LineRange): LineRange { + let startOffset = 0; + let lengthOffset = 0; + for (const diff of this.diffs.get()) { + if (diff.originalRange.endLineNumberExclusive <= baseRange.startLineNumber) { + startOffset += diff.resultingDeltaFromOriginalToModified; + } else if (diff.originalRange.startLineNumber <= baseRange.endLineNumberExclusive) { + lengthOffset += diff.resultingDeltaFromOriginalToModified; + } else { + break; + } + } + + return new LineRange(baseRange.startLineNumber + startOffset, baseRange.lineCount + lengthOffset); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model.ts b/src/vs/workbench/contrib/mergeEditor/browser/model.ts index 5d21dc17180..9a045854182 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ArrayQueue, compareBy, equals, numberComparator } from 'vs/base/common/arrays'; +import { Comparator, compareBy, equals, numberComparator } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Range } from 'vs/editor/common/core/range'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; @@ -40,7 +40,9 @@ export class LineEdits { } export class LineRange { - public static hull(ranges: LineRange[]): LineRange | undefined { + public static readonly compareByStart: Comparator = compareBy(l => l.startLineNumber, numberComparator); + + public static join(ranges: LineRange[]): LineRange | undefined { if (ranges.length === 0) { return undefined; } @@ -63,6 +65,10 @@ export class LineRange { } } + public join(other: LineRange): LineRange { + return new LineRange(Math.min(this.startLineNumber, other.startLineNumber), Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive) - this.startLineNumber); + } + public get endLineNumberExclusive(): number { return this.startLineNumber + this.lineCount; } @@ -131,9 +137,9 @@ export class LineDiff { return new LineDiff( lineDiffs[0].originalTextModel, - LineRange.hull(lineDiffs.map((d) => d.originalRange))!, + LineRange.join(lineDiffs.map((d) => d.originalRange))!, lineDiffs[0].modifiedTextModel, - LineRange.hull(lineDiffs.map((d) => d.modifiedRange))!, + LineRange.join(lineDiffs.map((d) => d.modifiedRange))!, ); } @@ -141,7 +147,7 @@ export class LineDiff { if (lineDiffs.length === 0) { return []; } - const originalRange = LineRange.hull(lineDiffs.map((d) => d.originalRange))!; + const originalRange = LineRange.join(lineDiffs.map((d) => d.originalRange))!; return lineDiffs.map(l => { const startDelta = originalRange.startLineNumber - l.originalRange.startLineNumber; const endDelta = originalRange.endLineNumberExclusive - l.originalRange.endLineNumberExclusive; @@ -231,7 +237,7 @@ export class ModifiedBaseRange { * This method computes strongly connected components of that graph while maintaining the side of each diff. */ public static fromDiffs( - originalTextModel: ITextModel, + baseTextModel: ITextModel, input1TextModel: ITextModel, diffs1: readonly LineDiff[], input2TextModel: ITextModel, @@ -242,90 +248,52 @@ export class ModifiedBaseRange { numberComparator ); - const queueDiffs1 = new ArrayQueue( - diffs1.slice().sort(compareByStartLineNumber) - ); - const queueDiffs2 = new ArrayQueue( - diffs2.slice().sort(compareByStartLineNumber) - ); + const diffs = diffs1 + .map((diff) => ({ source: 0 as 0 | 1, diff })) + .concat(diffs2.map((diff) => ({ source: 1 as const, diff }))); + + diffs.sort(compareBy(d => d.diff, compareByStartLineNumber)); + + const currentDiffs = [ + new Array(), + new Array(), + ]; + let deltaFromBaseToInput = [0, 0]; const result = new Array(); - while (true) { - const lastDiff1 = queueDiffs1.peekLast(); - const lastDiff2 = queueDiffs2.peekLast(); - - if ( - lastDiff1 && - (!lastDiff2 || - lastDiff1.originalRange.startLineNumber >= - lastDiff2.originalRange.startLineNumber) - ) { - queueDiffs1.removeLast(); - - const otherConflictingWith = - queueDiffs2.takeFromEndWhile((d) => d.conflicts(lastDiff1)) || []; - - const singleLinesDiff = LineDiff.hull(otherConflictingWith); - - const moreConflictingWith = - (singleLinesDiff && - queueDiffs1.takeFromEndWhile((d) => - d.conflicts(singleLinesDiff) - )) || - []; - moreConflictingWith.push(lastDiff1); - - result.push( - new ModifiedBaseRange( - originalTextModel, - input1TextModel, - moreConflictingWith, - queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - input2TextModel, - otherConflictingWith, - queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - ) - ); - } else if (lastDiff2) { - queueDiffs2.removeLast(); - - const otherConflictingWith = - queueDiffs1.takeFromEndWhile((d) => d.conflicts(lastDiff2)) || []; - - const singleLinesDiff = LineDiff.hull(otherConflictingWith); - - const moreConflictingWith = - (singleLinesDiff && - queueDiffs2.takeFromEndWhile((d) => - d.conflicts(singleLinesDiff) - )) || - []; - moreConflictingWith.push(lastDiff2); - - result.push( - new ModifiedBaseRange( - originalTextModel, - input1TextModel, - otherConflictingWith, - queueDiffs1.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - input2TextModel, - moreConflictingWith, - queueDiffs2.peekLast()?.resultingDeltaFromOriginalToModified ?? 0, - ) - ); - } else { - break; - } + function pushAndReset() { + result.push(new ModifiedBaseRange( + baseTextModel, + input1TextModel, + currentDiffs[0], + deltaFromBaseToInput[0], + input2TextModel, + currentDiffs[1], + deltaFromBaseToInput[1], + )); + currentDiffs[0] = []; + currentDiffs[1] = []; } - result.reverse(); + let currentRange: LineRange | undefined; + + for (const diff of diffs) { + const range = diff.diff.originalRange; + if (currentRange && !currentRange.touches(range)) { + pushAndReset(); + } + deltaFromBaseToInput[diff.source] = diff.diff.resultingDeltaFromOriginalToModified; + currentRange = currentRange ? currentRange.join(range) : range; + currentDiffs[diff.source].push(diff.diff); + } + pushAndReset(); return result; } - private readonly input1FullDiff = LineDiff.hull(this.input1Diffs); - private readonly input2FullDiff = LineDiff.hull(this.input2Diffs); + public readonly input1CombinedDiff = LineDiff.hull(this.input1Diffs); + public readonly input2CombinedDiff = LineDiff.hull(this.input2Diffs); public readonly baseRange: LineRange; public readonly input1Range: LineRange; @@ -335,31 +303,31 @@ export class ModifiedBaseRange { public readonly baseTextModel: ITextModel, public readonly input1TextModel: ITextModel, public readonly input1Diffs: readonly LineDiff[], - public readonly input1DeltaLineCount: number, + input1DeltaLineCount: number, public readonly input2TextModel: ITextModel, public readonly input2Diffs: readonly LineDiff[], - public readonly input2DeltaLineCount: number, + input2DeltaLineCount: number, ) { if (this.input1Diffs.length === 0 && this.input2Diffs.length === 0) { throw new BugIndicatingError('must have at least one diff'); } const input1Diff = - this.input1FullDiff || + this.input1CombinedDiff || new LineDiff( baseTextModel, - this.input2FullDiff!.originalRange, + this.input2CombinedDiff!.originalRange, input1TextModel, - this.input2FullDiff!.originalRange.delta(input1DeltaLineCount) + this.input2CombinedDiff!.originalRange.delta(input1DeltaLineCount) ); const input2Diff = - this.input2FullDiff || + this.input2CombinedDiff || new LineDiff( baseTextModel, - this.input1FullDiff!.originalRange, + this.input1CombinedDiff!.originalRange, input1TextModel, - this.input1FullDiff!.originalRange.delta(input2DeltaLineCount) + this.input1CombinedDiff!.originalRange.delta(input2DeltaLineCount) ); const results = LineDiff.alignOriginalRange([input1Diff, input2Diff]); @@ -368,54 +336,57 @@ export class ModifiedBaseRange { this.input2Range = results[1].modifiedRange; } + public getInputRange(inputNumber: 1 | 2): LineRange { + return inputNumber === 1 ? this.input1Range : this.input2Range; + } + + public getInputDiffs(inputNumber: 1 | 2): readonly LineDiff[] { + return inputNumber === 1 ? this.input1Diffs : this.input2Diffs; + } + public get isConflicting(): boolean { return this.input1Diffs.length > 0 && this.input2Diffs.length > 0; } - - public getInput1LineEdit(): LineEdit | undefined { - if (this.input1Diffs.length === 0) { - return undefined; - } - //new LineDiff(this.baseTextModel, this.tota) - if (this.input1Diffs.length === 1) { - return this.input1Diffs[0].getLineEdit(); - } else { - throw new Error('Method not implemented.'); - } - } - - public getInput2LineEdit(): LineEdit | undefined { - if (this.input2Diffs.length === 0) { - return undefined; - } - if (this.input2Diffs.length === 1) { - return this.input2Diffs[0].getLineEdit(); - } else { - throw new Error('Method not implemented.'); - } - } } export class ModifiedBaseRangeState { - constructor( + public static readonly default = new ModifiedBaseRangeState(false, false, false, false); + public static readonly conflicting = new ModifiedBaseRangeState(false, false, false, true); + + private constructor( public readonly input1: boolean, public readonly input2: boolean, - public readonly input2First: boolean + public readonly input2First: boolean, + public readonly conflicting: boolean, ) { } + public getInput(inputNumber: 1 | 2): boolean { + if (inputNumber === 1) { + return this.input1; + } else { + return this.input2; + } + } + + public withInputValue(inputNumber: 1 | 2, value: boolean): ModifiedBaseRangeState { + return inputNumber === 1 ? this.withInput1(value) : this.withInput2(value); + } + public withInput1(value: boolean): ModifiedBaseRangeState { return new ModifiedBaseRangeState( value, - this.input2, - value && this.isEmpty ? false : this.input2First + false, + value && this.isEmpty ? false : this.input2First, + false, ); } public withInput2(value: boolean): ModifiedBaseRangeState { return new ModifiedBaseRangeState( - this.input1, + false, value, - value && this.isEmpty ? true : this.input2First + value && this.isEmpty ? true : this.input2First, + false ); } @@ -445,31 +416,3 @@ export class ModifiedBaseRangeState { return arr.join(','); } } - -export class ReentrancyBarrier { - private isActive = false; - - public runExclusively(fn: () => void): void { - if (this.isActive) { - return; - } - this.isActive = true; - try { - fn(); - } finally { - this.isActive = false; - } - } - - public runExclusivelyOrThrow(fn: () => void): void { - if (this.isActive) { - throw new BugIndicatingError(); - } - this.isActive = true; - try { - fn(); - } finally { - this.isActive = false; - } - } -} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts new file mode 100644 index 00000000000..cf5aaa569d7 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CompareResult, ArrayQueue } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IObservable, autorun } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { IDisposable } from 'xterm'; + +export class ReentrancyBarrier { + private isActive = false; + + public runExclusively(fn: () => void): void { + if (this.isActive) { + return; + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } + + public runExclusivelyOrThrow(fn: () => void): void { + if (this.isActive) { + throw new BugIndicatingError(); + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } +} + +export function n(tag: TTag): never; +export function n( + tag: TTag, + children: T +): (ArrayToObj & Record<'root', TagToElement>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; +export function n( + tag: TTag, + attributes: { $: TId } +): Record>; +export function n( + tag: TTag, + attributes: { $: TId }, + children: T +): (ArrayToObj & Record>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; +export function n(tag: string, ...args: [] | [attributes: { $: string } | Record, children?: any[]] | [children: any[]]): Record { + let attributes: Record; + let children: (Record | HTMLElement)[] | undefined; + + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } else { + attributes = args[0] as any || {}; + children = args[1]; + } + + const [tagName, className] = tag.split('.'); + const el = document.createElement(tagName); + if (className) { + el.className = className; + } + + const result: Record = {}; + + if (children) { + for (const c of children) { + if (c instanceof HTMLElement) { + el.appendChild(c); + } else { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + + result['root'] = el; + + for (const [key, value] of Object.entries(attributes)) { + if (key === '$') { + result[value] = el; + continue; + } + el.setAttribute(key, value); + } + + return result; +} + +type RemoveHTMLElement = T extends HTMLElement ? never : T; + +type ArrayToObj = UnionToIntersection>; + + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; + +type HTMLElementsByTagName = { + div: HTMLDivElement; + span: HTMLSpanElement; + a: HTMLAnchorElement; +}; + +type TagToElement = T extends `${infer TStart}.${string}` + ? TStart extends keyof HTMLElementsByTagName + ? HTMLElementsByTagName[TStart] + : HTMLElement + : T extends keyof HTMLElementsByTagName + ? HTMLElementsByTagName[T] + : HTMLElement; + +export function setStyle( + element: HTMLElement, + style: { + width?: number | string; + height?: number | string; + left?: number | string; + top?: number | string; + } +): void { + Object.entries(style).forEach(([key, value]) => { + element.style.setProperty(key, toSize(value)); + }); +} + +function toSize(value: number | string): string { + return typeof value === 'number' ? `${value}px` : value; +} + +export function applyObservableDecorations(editor: CodeEditorWidget, decorations: IObservable): IDisposable { + const d = new DisposableStore(); + let decorationIds: string[] = []; + d.add(autorun(reader => { + const d = decorations.read(reader); + editor.changeDecorations(a => { + decorationIds = a.deltaDecorations(decorationIds, d); + }); + }, 'Update Decorations')); + d.add({ + dispose: () => { + editor.changeDecorations(a => { + decorationIds = a.deltaDecorations(decorationIds, []); + }); + } + }); + return d; +} + +export function* leftJoin( + left: Iterable, + right: readonly TRight[], + compare: (left: TLeft, right: TRight) => CompareResult, +): IterableIterator<{ left: TLeft; rights: TRight[] }> { + const rightQueue = new ArrayQueue(right); + for (const leftElement of left) { + rightQueue.takeWhile(rightElement => CompareResult.isGreaterThan(compare(leftElement, rightElement))); + const equals = rightQueue.takeWhile(rightElement => CompareResult.isNeitherLessOrGreaterThan(compare(leftElement, rightElement))); + yield { left: leftElement, rights: equals || [] }; + } +} From 03694c8c5ca6fde4e9624a7caca989e39c673564 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Fri, 20 May 2022 17:17:03 -0700 Subject: [PATCH 031/270] Ideas for tracking last output --- .../browser/diff/notebookTextDiffEditor.ts | 4 ++ .../notebook/browser/notebookEditorWidget.ts | 1 + .../view/renderers/backLayerWebView.ts | 21 +++++++++ .../browser/view/renderers/webviewMessages.ts | 17 ++++++- .../browser/view/renderers/webviewPreloads.ts | 45 +++++++++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 1369de699b9..a791887d639 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -789,6 +789,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD throw new Error('Not implemented'); } + getCellByHandle(cellHandle: number): IGenericCellViewModel | undefined { + throw new Error('Not implemented'); + } + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide) { this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 8567bdded40..7197952d03c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1385,6 +1385,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD triggerScroll(event: IMouseWheelEvent) { that._listViewInfoAccessor.triggerScroll(event); }, getCellByInfo: that.getCellByInfo.bind(that), getCellById: that._getCellById.bind(that), + getCellByHandle: that.getCellByHandle.bind(that), toggleNotebookCellSelection: that._toggleNotebookCellSelection.bind(that), focusNotebookCell: that.focusNotebookCell.bind(that), focusNextNotebookCell: that.focusNextNotebookCell.bind(that), diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index af4606ffee6..59046e6159e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -37,6 +37,7 @@ import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebo import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -66,6 +67,7 @@ export interface INotebookDelegateForWebview { focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): Promise; toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + getCellByHandle(cellHandle: number): IGenericCellViewModel | undefined; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; @@ -134,6 +136,7 @@ export class BackLayerWebView extends Disposable { @ILanguageService private readonly languageService: ILanguageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService ) { super(); @@ -173,6 +176,19 @@ export class BackLayerWebView extends Disposable { css: getTokenizationCss(), }); })); + + this._register(notebookExecutionStateService.onDidChangeCellExecution((e) => { + // If e.changed is undefined, it means notebook execution just finished. + if (e.changed === undefined && e.affectsNotebook(this.documentUri)) { + const cell = this.notebookEditor.getCellByHandle(e.cellHandle); + if (cell) { + this._sendMessageToWebview({ + type: 'trackFinalOutputRender', + cellId: cell.id + }); + } + } + })); } updateOptions(options: BacklayerWebviewOptions) { @@ -818,6 +834,11 @@ var requirejs = (function() { this._handleHighlightCodeBlock(data.codeBlocks); break; } + + case 'didRenderFinalOutput': { + console.log(`Rendered final output for ${data.cellId}`); + break; + } } })); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 8b98bba83f8..e37283b1809 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -401,6 +401,16 @@ export interface IDidFindHighlightMessage extends BaseToWebviewMessage { readonly offset: number; } +export interface ITrackFinalOutputRenderMessage { + readonly type: 'trackFinalOutputRender'; + readonly cellId: string; +} + +export interface IDidRenderFinalOutputMessage extends BaseToWebviewMessage { + readonly type: 'didRenderFinalOutput'; + readonly cellId: string; +} + export type FromWebviewMessage = WebviewInitialized | IDimensionMessage | IMouseEnterMessage | @@ -428,7 +438,8 @@ export type FromWebviewMessage = WebviewInitialized | IRenderedMarkupMessage | IRenderedCellOutputMessage | IDidFindMessage | - IDidFindHighlightMessage; + IDidFindHighlightMessage | + IDidRenderFinalOutputMessage; export type ToWebviewMessage = IClearMessage | IFocusOutputMessage | @@ -458,6 +469,8 @@ export type ToWebviewMessage = IClearMessage | IFindMessage | IFindHighlightMessage | IFindUnHighlightMessage | - IFindStopMessage; + IFindStopMessage | + ITrackFinalOutputRenderMessage; + export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 33155f8dc15..786cc9377ce 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1172,6 +1172,10 @@ async function webviewPreloads(ctx: PreloadContext) { _highlighter?.dispose(); break; } + case 'trackFinalOutputRender': { + viewModel.trackFinalOutput(event.data.cellId); + break; + } } }); @@ -1394,6 +1398,7 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _markupCells = new Map(); private readonly _outputCells = new Map(); + private _pendingFinalOutput: string | undefined; public clearAll() { this._markupCells.clear(); @@ -1405,6 +1410,15 @@ async function webviewPreloads(ctx: PreloadContext) { this.renderOutputCells(); } + public trackFinalOutput(cellId: string) { + const cell = this._outputCells.get(cellId); + if (cell) { + cell.addFinalOutputTracker(); + } else { + this._pendingFinalOutput = cellId; + } + } + private async createMarkupCell(init: webviewMessages.IMarkupCellInitialization, top: number, visible: boolean): Promise { const existing = this._markupCells.get(init.cellId); if (existing) { @@ -1517,6 +1531,12 @@ async function webviewPreloads(ctx: PreloadContext) { // don't hide until after this step so that the height is right cellOutput.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; + + // If notebook side has indicated we are done (and we haven't added it yet), stick in the final render + if (data.cellId === this._pendingFinalOutput) { + this._pendingFinalOutput = undefined; + cellOutput.addFinalOutputTracker(); + } } public ensureOutputCell(cellId: string, cellTop: number, skipCellTopUpdateIfExist: boolean): OutputCell { @@ -1817,8 +1837,15 @@ async function webviewPreloads(ctx: PreloadContext) { container.appendChild(this.element); this.element = this.element; + const lowerWrapperElement = createFocusSink(cellId, true); container.appendChild(lowerWrapperElement); + + // New thoughts. + // Send message to indicate force scroll. + // Add resizeObserver on the element + // When resize occurs, scroll lowerWrapperElement into view + // Send message to indicate stop scrolling } public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { @@ -1878,6 +1905,24 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.visibility = 'visible'; } } + + public addFinalOutputTracker() { + const id = `${this.element.id}-final-output-tracker`; + let tracker: HTMLImageElement | null = document.getElementById(id) as HTMLImageElement; + if (!tracker) { + tracker = document.createElement('img') as HTMLImageElement; + tracker.id = id; + tracker.style.height = '0px'; + tracker.src = ''; + tracker.addEventListener('error', (ev) => { + console.log(`Final cell output has rendered`); + + // Src is empty so this should fire as soon as it renders + postNotebookMessage('didRenderFinalOutput', { cellId: this.element.id }); + }); + this.element.appendChild(tracker); + } + } } class OutputContainer { From d3af646dd02d1c73ecf15b4193781b771cce1e6a Mon Sep 17 00:00:00 2001 From: Andre Weinand Date: Mon, 23 May 2022 09:21:10 +0200 Subject: [PATCH 032/270] switch bot assignment fro debug back to weinand --- .github/classifier.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/classifier.json b/.github/classifier.json index 3b23ece3ce8..5b5625f6e93 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -23,7 +23,7 @@ "context-keys": {"assign": []}, "css-less-scss": {"assign": ["aeschli"]}, "custom-editors": {"assign": ["mjbvz"]}, - "debug": {"assign": ["roblourens"]}, + "debug": {"assign": ["weinand"]}, "dialogs": {"assign": ["sbatten"]}, "diff-editor": {"assign": ["alexdima"]}, "dropdown": {"assign": []}, From 96479074a4c233d229567c8494bb78d32a9c80d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 23 May 2022 07:22:15 -0700 Subject: [PATCH 033/270] Create bash explicitly on linux/mac in shell integration smoke tests Part of #149324 --- .../terminal/terminal-shellIntegration.test.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts index b94b40045f7..fb3c40d1547 100644 --- a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts +++ b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Application, Terminal, SettingsEditor } from '../../../../automation'; +import { Application, Terminal, SettingsEditor, TerminalCommandIdWithValue } from '../../../../automation'; import { setTerminalTestSettings } from './terminal-helpers'; export function setup() { @@ -24,28 +24,31 @@ export function setup() { await settingsEditor.clearUserSettings(); }); + async function createShellIntegrationProfile() { + await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile, process.platform === 'win32' ? 'PowerShell' : 'bash'); + } + describe('Shell integration', function () { - // TODO: Fix on Linux, some distros use sh as the default shell in which case shell integration will fail - (process.platform === 'win32' || process.platform === 'linux' ? describe.skip : describe)('Decorations', function () { + (process.platform === 'win32' ? describe.skip : describe)('Decorations', function () { describe('Should show default icons', function () { it('Placeholder', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 }); }); it('Success', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.runCommandInTerminal(`ls`); await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 0 }); }); it('Error', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.runCommandInTerminal(`fsdkfsjdlfksjdkf`); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 1 }); }); }); describe('Custom configuration', function () { it('Should update and show custom icons', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 }); await terminal.runCommandInTerminal(`ls`); await terminal.runCommandInTerminal(`fsdkfsjdlfksjdkf`); From 99065b484f7fadddec2ad7c89028d92787e10224 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 23 May 2022 12:05:21 -0700 Subject: [PATCH 034/270] add tests --- .../contrib/tasks/common/taskConfiguration.ts | 26 ++-- .../tasks/test/common/configuration.test.ts | 126 +++++++++++++++++- 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 9c6f0ba9e34..1fd6ca72d5b 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -14,8 +14,8 @@ import * as UUID from 'vs/base/common/uuid'; import { ValidationStatus, IProblemReporter as IProblemReporterBase } from 'vs/base/common/parsers'; import { - NamedProblemMatcher, ProblemMatcher, ProblemMatcherParser, Config as ProblemMatcherConfig, - isNamedProblemMatcher, ProblemMatcherRegistry + NamedProblemMatcher, ProblemMatcherParser, Config as ProblemMatcherConfig, + isNamedProblemMatcher, ProblemMatcherRegistry, ProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; @@ -710,7 +710,7 @@ export namespace RunOptions { } } -interface ParseContext { +export interface ParseContext { workspaceFolder: IWorkspaceFolder; workspace: IWorkspace | undefined; problemReporter: IProblemReporter; @@ -1130,7 +1130,7 @@ namespace CommandConfiguration { } } -namespace ProblemMatcherConverter { +export namespace ProblemMatcherConverter { export function namedFrom(this: void, declares: ProblemMatcherConfig.NamedProblemMatcher[] | undefined, context: ParseContext): IStringDictionary { let result: IStringDictionary = Object.create(null); @@ -1389,7 +1389,7 @@ namespace ConfiguringTask { customize: string; } - export function from(this: void, external: ConfiguringTask, context: ParseContext, index: number, source: TaskConfigSource): Tasks.ConfiguringTask | undefined { + export function from(this: void, external: ConfiguringTask, context: ParseContext, index: number, source: TaskConfigSource, testTaskDefinition?: Tasks.TaskDefinition): Tasks.ConfiguringTask | undefined { if (!external) { return undefined; } @@ -1399,7 +1399,7 @@ namespace ConfiguringTask { context.problemReporter.error(nls.localize('ConfigurationParser.noTaskType', 'Error: tasks configuration must have a type property. The configuration will be ignored.\n{0}\n', JSON.stringify(external, null, 4))); return undefined; } - let typeDeclaration = type ? TaskDefinitionRegistry.get(type) : undefined; + let typeDeclaration = type ? testTaskDefinition || TaskDefinitionRegistry.get(type) : undefined; if (!typeDeclaration) { let message = nls.localize('ConfigurationParser.noTypeDefinition', 'Error: there is no registered task type \'{0}\'. Did you miss installing an extension that provides a corresponding task provider?', type); context.problemReporter.error(message); @@ -1654,12 +1654,12 @@ namespace CustomTask { } } -interface TaskParseResult { +export interface TaskParseResult { custom: Tasks.CustomTask[]; configured: Tasks.ConfiguringTask[]; } -namespace TaskParser { +export namespace TaskParser { function isCustomTask(value: CustomTask | ConfiguringTask): value is CustomTask { let type = value.type; @@ -1672,7 +1672,7 @@ namespace TaskParser { process: ProcessExecutionSupportedContext }; - export function from(this: void, externals: Array | undefined, globals: Globals, context: ParseContext, source: TaskConfigSource): TaskParseResult { + export function from(this: void, externals: Array | undefined, globals: Globals, context: ParseContext, source: TaskConfigSource, testTaskDefinition?: Tasks.TaskDefinition): TaskParseResult { let result: TaskParseResult = { custom: [], configured: [] }; if (!externals) { return result; @@ -1683,7 +1683,7 @@ namespace TaskParser { const baseLoadIssues = Objects.deepClone(context.taskLoadIssues); for (let index = 0; index < externals.length; index++) { let external = externals[index]; - const definition = external.type ? TaskDefinitionRegistry.get(external.type) : undefined; + const definition = external.type ? testTaskDefinition || TaskDefinitionRegistry.get(external.type) : undefined; let typeNotSupported: boolean = false; if (definition && definition.when && !context.contextKeyService.contextMatchesRules(definition.when)) { typeNotSupported = true; @@ -1743,7 +1743,7 @@ namespace TaskParser { result.custom.push(customTask); } } else { - let configuredTask = ConfiguringTask.from(external, context, index, source); + let configuredTask = ConfiguringTask.from(external, context, index, source, testTaskDefinition); if (configuredTask) { configuredTask.addTaskLoadMessages(context.taskLoadIssues); result.configured.push(configuredTask); @@ -1795,7 +1795,7 @@ namespace TaskParser { } } -interface Globals { +export interface Globals { command?: Tasks.CommandConfiguration; problemMatcher?: ProblemMatcher[]; promptOnClose?: boolean; @@ -1933,7 +1933,7 @@ export interface ParseResult { export interface IProblemReporter extends IProblemReporterBase { } -class UUIDMap { +export class UUIDMap { private last: IStringDictionary | undefined; private current: IStringDictionary; diff --git a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts b/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts index c1ef36f8b17..577893066c8 100644 --- a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts @@ -10,14 +10,15 @@ import * as UUID from 'vs/base/common/uuid'; import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; import { ValidationStatus } from 'vs/base/common/parsers'; -import { ProblemMatcher, FileLocationKind, ProblemPattern, ApplyToKind } from 'vs/workbench/contrib/tasks/common/problemMatcher'; +import { ProblemMatcher, FileLocationKind, ProblemPattern, ApplyToKind, NamedProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { WorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import * as Tasks from 'vs/workbench/contrib/tasks/common/tasks'; -import { parse, ParseResult, IProblemReporter, ExternalTaskRunnerConfiguration, CustomTask, TaskConfigSource } from 'vs/workbench/contrib/tasks/common/taskConfiguration'; +import { parse, ParseResult, IProblemReporter, ExternalTaskRunnerConfiguration, CustomTask, TaskConfigSource, ParseContext, ProblemMatcherConverter, Globals, TaskParseResult, UUIDMap, TaskParser } from 'vs/workbench/contrib/tasks/common/taskConfiguration'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContext } from 'vs/platform/contextkey/common/contextkey'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; const workspaceFolder: WorkspaceFolder = new WorkspaceFolder({ uri: URI.file('/workspace/folderOne'), @@ -1760,3 +1761,124 @@ suite('Bugs / regression tests', () => { testConfiguration(external, builder); }); }); + +class TestNamedProblemMatcher implements Partial { + +} + +class TestParseContext implements Partial { + +} + +suite('Task Configuration Test', () => { + let instantiationService: TestInstantiationService; + let parseContext: ParseContext; + let namedProblemMatcher: NamedProblemMatcher; + let problemReporter: ProblemReporter; + setup(() => { + instantiationService = new TestInstantiationService(); + namedProblemMatcher = instantiationService.createInstance(TestNamedProblemMatcher); + namedProblemMatcher.name = 'real'; + namedProblemMatcher.label = 'real label'; + problemReporter = new ProblemReporter(); + parseContext = instantiationService.createInstance(TestParseContext); + parseContext.problemReporter = problemReporter; + parseContext.namedProblemMatchers = { + 'real': namedProblemMatcher + }; + parseContext.uuidMap = new UUIDMap(); + }); + suite('ProblemMatcherConverter', () => { + test('returns [] and an error for an unknown problem matcher', () => { + const result = (ProblemMatcherConverter.from('$fake', parseContext)); + assert.deepEqual(result.value, []); + assert.strictEqual(result.errors?.length, 1); + }); + test('returns config for a known problem matcher', () => { + const result = (ProblemMatcherConverter.from('$real', parseContext)); + assert.strictEqual(result.errors?.length, 0); + assert.deepEqual(result.value, [{ "label": "real label" }]); + }); + test('returns config for a known problem matcher including applyTo', () => { + namedProblemMatcher.applyTo = ApplyToKind.closedDocuments; + const result = (ProblemMatcherConverter.from('$real', parseContext)); + assert.strictEqual(result.errors?.length, 0); + assert.deepEqual(result.value, [{ "label": "real label", "applyTo": ApplyToKind.closedDocuments }]); + }); + }); + suite('TaskParser from', () => { + suite('CustomTask', () => { + suite('incomplete config reports an appropriate error for missing', () => { + test('name', () => { + const result = TaskParser.from([{} as CustomTask], {} as Globals, parseContext, {} as TaskConfigSource); + assertTaskParseResult(result, undefined, problemReporter, 'Error: a task must provide a label property'); + }); + test('command', () => { + const result = TaskParser.from([{ taskName: 'task' } as CustomTask], {} as Globals, parseContext, {} as TaskConfigSource); + assertTaskParseResult(result, undefined, problemReporter, "Error: the task 'task' doesn't define a command"); + }); + }); + suite('returns expected result', () => { + test('single', () => { + const expected = [{ taskName: 'task', command: 'echo test' } as CustomTask]; + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource); + assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); + }); + test('multiple', () => { + const expected = [{ taskName: 'task', command: 'echo test' } as CustomTask, { taskName: 'task 2', command: 'echo test' } as CustomTask]; + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource); + assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); + }); + }); + }); + suite('ConfiguredTask', () => { + suite('returns expected result', () => { + test('single', () => { + const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }]; + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, { extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); + assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); + }); + test('multiple', () => { + const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }, { taskName: 'task 2', command: 'echo test', type: 'any', label: 'task 2' }]; + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, { extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); + assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); + }); + }); + }); + }); +}); + +function assertTaskParseResult(actual: TaskParseResult, expected: ITestTaskParseResult | undefined, problemReporter: ProblemReporter, expectedMessage?: string): void { + if (expectedMessage === undefined) { + assert.strictEqual(problemReporter.lastMessage, undefined); + } else { + assert.ok(problemReporter.lastMessage?.includes(expectedMessage)); + } + + assert.deepEqual(actual.custom.length, expected?.custom?.length || 0); + assert.deepEqual(actual.configured.length, expected?.configured?.length || 0); + + let index = 0; + if (expected?.configured) { + for (const taskParseResult of expected?.configured) { + assert.strictEqual(actual.configured[index]._label, taskParseResult.label); + index++; + } + } + index = 0; + if (expected?.custom) { + for (const taskParseResult of expected?.custom) { + assert.strictEqual(actual.custom[index]._label, taskParseResult.taskName); + index++; + } + } +} + +interface ITestTaskParseResult { + custom?: Partial[]; + configured?: Partial[]; +} + +interface TestConfiguringTask extends Partial { + label: string; +} From 977123e6d840c9a5df5611b4504bf46cf3606068 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 23 May 2022 12:38:51 -0700 Subject: [PATCH 035/270] Update src/vs/workbench/contrib/tasks/test/common/configuration.test.ts --- src/vs/workbench/contrib/tasks/test/common/configuration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts b/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts index 577893066c8..56ffdbced3b 100644 --- a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts @@ -1763,7 +1763,6 @@ suite('Bugs / regression tests', () => { }); class TestNamedProblemMatcher implements Partial { - } class TestParseContext implements Partial { From 2f0de4444998f992763eb58b5ced1a6f5186594a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 23 May 2022 12:39:16 -0700 Subject: [PATCH 036/270] Update src/vs/workbench/contrib/tasks/test/common/configuration.test.ts --- src/vs/workbench/contrib/tasks/test/common/configuration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts b/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts index 56ffdbced3b..083d47dcf28 100644 --- a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts @@ -1766,7 +1766,6 @@ class TestNamedProblemMatcher implements Partial { } class TestParseContext implements Partial { - } suite('Task Configuration Test', () => { From a48a9f8fc94c94a1c95104d2200852758db480cd Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 23 May 2022 12:39:44 -0700 Subject: [PATCH 037/270] rename --- .../common/{configuration.test.ts => taskConfiguration.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/vs/workbench/contrib/tasks/test/common/{configuration.test.ts => taskConfiguration.test.ts} (99%) diff --git a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts similarity index 99% rename from src/vs/workbench/contrib/tasks/test/common/configuration.test.ts rename to src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 083d47dcf28..1353ab13768 100644 --- a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -1768,7 +1768,7 @@ class TestNamedProblemMatcher implements Partial { class TestParseContext implements Partial { } -suite('Task Configuration Test', () => { +suite('Problem matcher and task parser', () => { let instantiationService: TestInstantiationService; let parseContext: ParseContext; let namedProblemMatcher: NamedProblemMatcher; From 45aefc8f376223ce0eabd0d5641eca855973b226 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 23 May 2022 12:52:56 -0700 Subject: [PATCH 038/270] cleanup --- .../test/common/taskConfiguration.test.ts | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 1353ab13768..105d5d3dce6 100644 --- a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -59,6 +59,10 @@ class ProblemReporter implements IProblemReporter { this.receivedMessage = true; this.lastMessage = message; } + + public clearMessage(): void { + this.lastMessage = undefined; + } } class ConfiguationBuilder { @@ -1768,7 +1772,7 @@ class TestNamedProblemMatcher implements Partial { class TestParseContext implements Partial { } -suite('Problem matcher and task parser', () => { +suite.only('To task configuration from', () => { let instantiationService: TestInstantiationService; let parseContext: ParseContext; let namedProblemMatcher: NamedProblemMatcher; @@ -1781,12 +1785,10 @@ suite('Problem matcher and task parser', () => { problemReporter = new ProblemReporter(); parseContext = instantiationService.createInstance(TestParseContext); parseContext.problemReporter = problemReporter; - parseContext.namedProblemMatchers = { - 'real': namedProblemMatcher - }; + parseContext.namedProblemMatchers = { 'real': namedProblemMatcher }; parseContext.uuidMap = new UUIDMap(); }); - suite('ProblemMatcherConverter', () => { + suite('ProblemMatcher config', () => { test('returns [] and an error for an unknown problem matcher', () => { const result = (ProblemMatcherConverter.from('$fake', parseContext)); assert.deepEqual(result.value, []); @@ -1804,7 +1806,7 @@ suite('Problem matcher and task parser', () => { assert.deepEqual(result.value, [{ "label": "real label", "applyTo": ApplyToKind.closedDocuments }]); }); }); - suite('TaskParser from', () => { + suite('TaskParser external config', () => { suite('CustomTask', () => { suite('incomplete config reports an appropriate error for missing', () => { test('name', () => { @@ -1816,31 +1818,17 @@ suite('Problem matcher and task parser', () => { assertTaskParseResult(result, undefined, problemReporter, "Error: the task 'task' doesn't define a command"); }); }); - suite('returns expected result', () => { - test('single', () => { - const expected = [{ taskName: 'task', command: 'echo test' } as CustomTask]; - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource); - assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); - }); - test('multiple', () => { - const expected = [{ taskName: 'task', command: 'echo test' } as CustomTask, { taskName: 'task 2', command: 'echo test' } as CustomTask]; - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource); - assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); - }); + test('returns expected result', () => { + const expected = [{ taskName: 'task', command: 'echo test' } as CustomTask, { taskName: 'task 2', command: 'echo test' } as CustomTask]; + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource); + assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); }); }); suite('ConfiguredTask', () => { - suite('returns expected result', () => { - test('single', () => { - const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }]; - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, { extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); - assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); - }); - test('multiple', () => { - const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }, { taskName: 'task 2', command: 'echo test', type: 'any', label: 'task 2' }]; - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, { extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); - assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); - }); + test('returns expected result', () => { + const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }, { taskName: 'task 2', command: 'echo test', type: 'any', label: 'task 2' }]; + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, { extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); + assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); }); }); }); @@ -1870,6 +1858,7 @@ function assertTaskParseResult(actual: TaskParseResult, expected: ITestTaskParse index++; } } + problemReporter.clearMessage(); } interface ITestTaskParseResult { From 276e982310b5944b5bb4ba9bcc1340abf20695ed Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 23 May 2022 12:53:43 -0700 Subject: [PATCH 039/270] Update src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts --- .../contrib/tasks/test/common/taskConfiguration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 105d5d3dce6..95fb3eb7b55 100644 --- a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -1772,7 +1772,7 @@ class TestNamedProblemMatcher implements Partial { class TestParseContext implements Partial { } -suite.only('To task configuration from', () => { +suite('To task configuration from', () => { let instantiationService: TestInstantiationService; let parseContext: ParseContext; let namedProblemMatcher: NamedProblemMatcher; From f5f375d295f4ef7be4d8fdb398d22fccc08142ea Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 23 May 2022 14:39:33 -0700 Subject: [PATCH 040/270] Scrolling working --- .../browser/interactive.contribution.ts | 16 ++++ .../interactive/browser/interactiveEditor.ts | 8 +- .../browser/diff/notebookTextDiffEditor.ts | 3 + .../notebook/browser/notebookEditorWidget.ts | 12 ++- .../view/renderers/backLayerWebView.ts | 23 +++-- .../browser/view/renderers/webviewMessages.ts | 19 ++-- .../browser/view/renderers/webviewPreloads.ts | 87 +++++++++++-------- 7 files changed, 117 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 5559096223e..ddd9a857eaa 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -23,6 +23,7 @@ import { peekViewBorder /*, peekViewEditorBackground, peekViewResultsBackground import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/suggest'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -58,6 +59,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( InteractiveEditor, @@ -711,3 +713,17 @@ registerThemingParticipant((theme) => { // hc: Color.black // }, localize('interactive.inactiveCodeBackground', 'The backgorund color for the current interactive code cell when the editor does not have focus.')); }); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'interactive', + order: 100, + type: 'object', + 'properties': { + 'interactive.alwaysScrollOnNewCell': { + type: 'boolean', + default: true, + markdownDescription: localize('interactive.alwaysScrollOnNewCell', "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.") + }, + } +}); + diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 52fe78d35bb..9265c2fca79 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -125,7 +125,7 @@ export class InteractiveEditor extends EditorPane { @INotebookKernelService notebookKernelService: INotebookKernelService, @ILanguageService languageService: ILanguageService, @IKeybindingService keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private configurationService: IConfigurationService, @IMenuService menuService: IMenuService, @IContextMenuService contextMenuService: IContextMenuService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @@ -402,6 +402,12 @@ export class InteractiveEditor extends EditorPane { this.#notebookWidget.value!.setOptions({ isReadOnly: true }); + this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidResizeOutput((cvm) => { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this.configurationService.getValue('interactive.alwaysScrollOnNewCell') || this.#state === ScrollingState.StickyToBottom) { + this.#notebookWidget.value!.scrollToBottom(); + } + })); this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidFocusWidget(() => this.#onDidFocusWidget.fire())); this.#widgetDisposableStore.add(model.notebook.onDidChangeContent(() => { (model as ComplexNotebookEditorModel).setDirty(false); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index a791887d639..61c5daf9949 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -202,6 +202,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD didDropMarkupCell(cellId: string) { // throw new Error('Method not implemented.'); } + didResizeOutput(cellId: string): void { + // throw new Error('Method not implemented.'); + } protected createEditor(parent: HTMLElement): void { this._rootElement = DOM.append(parent, DOM.$('.notebook-text-diff-editor')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 7197952d03c..4bd1f431a7f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -239,6 +239,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; private readonly _onDidRenderOutput = this._register(new Emitter()); private readonly onDidRenderOutput = this._onDidRenderOutput.event; + private readonly _onDidResizeOutputEmitter = this._register(new Emitter()); + readonly onDidResizeOutput = this._onDidResizeOutputEmitter.event; //#endregion @@ -1396,7 +1398,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD didStartDragMarkupCell: that._didStartDragMarkupCell.bind(that), didDragMarkupCell: that._didDragMarkupCell.bind(that), didDropMarkupCell: that._didDropMarkupCell.bind(that), - didEndDragMarkupCell: that._didEndDragMarkupCell.bind(that) + didEndDragMarkupCell: that._didEndDragMarkupCell.bind(that), + didResizeOutput: that._didResizeOutput.bind(that) }, id, resource, { ...this._notebookOptions.computeWebviewOptions(), fontFamily: this._generateFontFamily() @@ -2980,6 +2983,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } + private _didResizeOutput(cellId: string): void { + const cell = this._getCellById(cellId); + if (cell) { + this._onDidResizeOutputEmitter.fire(cell); + } + } + //#endregion //#region Editor Contributions diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 59046e6159e..513f8b4856e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -36,7 +36,7 @@ import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; -import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookRendererInfo, NotebookCellExecutionState, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; @@ -77,6 +77,7 @@ export interface INotebookDelegateForWebview { didDragMarkupCell(cellId: string, event: { dragOffsetY: number }): void; didDropMarkupCell(cellId: string, event: { dragOffsetY: number; ctrlKey: boolean; altKey: boolean }): void; didEndDragMarkupCell(cellId: string): void; + didResizeOutput(cellId: string): void; setScrollTop(scrollTop: number): void; triggerScroll(event: IMouseWheelEvent): void; } @@ -178,12 +179,21 @@ export class BackLayerWebView extends Disposable { })); this._register(notebookExecutionStateService.onDidChangeCellExecution((e) => { - // If e.changed is undefined, it means notebook execution just finished. - if (e.changed === undefined && e.affectsNotebook(this.documentUri)) { + // If e.changed state is pending, it means notebook execution just started + if (e.changed && e.changed.state === NotebookCellExecutionState.Pending && e.affectsNotebook(this.documentUri)) { const cell = this.notebookEditor.getCellByHandle(e.cellHandle); if (cell) { this._sendMessageToWebview({ - type: 'trackFinalOutputRender', + type: 'startWatchingOutputResize', + cellId: cell.id + }); + } + // If e.changed is undefined, it means notebook execution just finished + } else if (e.changed === undefined && e.affectsNotebook(this.documentUri)) { + const cell = this.notebookEditor.getCellByHandle(e.cellHandle); + if (cell) { + this._sendMessageToWebview({ + type: 'stopWatchingOutputResize', cellId: cell.id }); } @@ -835,10 +845,9 @@ var requirejs = (function() { break; } - case 'didRenderFinalOutput': { - console.log(`Rendered final output for ${data.cellId}`); + case 'outputResized': + this.notebookEditor.didResizeOutput(data.cellId); break; - } } })); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index e37283b1809..9c0941b1ffc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -401,16 +401,22 @@ export interface IDidFindHighlightMessage extends BaseToWebviewMessage { readonly offset: number; } -export interface ITrackFinalOutputRenderMessage { - readonly type: 'trackFinalOutputRender'; +export interface IStartWatchingOutputMessage { + readonly type: 'startWatchingOutputResize'; readonly cellId: string; } -export interface IDidRenderFinalOutputMessage extends BaseToWebviewMessage { - readonly type: 'didRenderFinalOutput'; +export interface IStopWatchingOutputMessage { + readonly type: 'stopWatchingOutputResize'; readonly cellId: string; } +export interface IOutputResizedMessage extends BaseToWebviewMessage { + readonly type: 'outputResized'; + readonly cellId: string; +} + + export type FromWebviewMessage = WebviewInitialized | IDimensionMessage | IMouseEnterMessage | @@ -439,7 +445,7 @@ export type FromWebviewMessage = WebviewInitialized | IRenderedCellOutputMessage | IDidFindMessage | IDidFindHighlightMessage | - IDidRenderFinalOutputMessage; + IOutputResizedMessage; export type ToWebviewMessage = IClearMessage | IFocusOutputMessage | @@ -470,7 +476,8 @@ export type ToWebviewMessage = IClearMessage | IFindHighlightMessage | IFindUnHighlightMessage | IFindStopMessage | - ITrackFinalOutputRenderMessage; + IStartWatchingOutputMessage | + IStopWatchingOutputMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 786cc9377ce..41efb7a678f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -387,6 +387,7 @@ async function webviewPreloads(ctx: PreloadContext) { function createFocusSink(cellId: string, focusNext?: boolean) { const element = document.createElement('div'); + element.id = `focus-sink-${cellId}`; element.tabIndex = 0; element.addEventListener('focus', () => { postNotebookMessage('focus-editor', { @@ -1172,8 +1173,12 @@ async function webviewPreloads(ctx: PreloadContext) { _highlighter?.dispose(); break; } - case 'trackFinalOutputRender': { - viewModel.trackFinalOutput(event.data.cellId); + case 'startWatchingOutputResize': { + viewModel.startWatchingOutput(event.data.cellId); + break; + } + case 'stopWatchingOutputResize': { + viewModel.stopWatchingOutput(event.data.cellId); break; } } @@ -1398,7 +1403,9 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _markupCells = new Map(); private readonly _outputCells = new Map(); - private _pendingFinalOutput: string | undefined; + private _trackingResize: string | undefined; + private _outputResizeObservers: ResizeObserver[] = []; + private _outputResizeTimer: any; public clearAll() { this._markupCells.clear(); @@ -1410,13 +1417,14 @@ async function webviewPreloads(ctx: PreloadContext) { this.renderOutputCells(); } - public trackFinalOutput(cellId: string) { - const cell = this._outputCells.get(cellId); - if (cell) { - cell.addFinalOutputTracker(); - } else { - this._pendingFinalOutput = cellId; - } + public startWatchingOutput(cellId: string) { + this._trackingResize = cellId; + this._outputResizeObservers.forEach(o => o.disconnect()); + this._outputResizeObservers = []; + } + + public stopWatchingOutput(cellId: string) { + this._trackingResize = undefined; } private async createMarkupCell(init: webviewMessages.IMarkupCellInitialization, top: number, visible: boolean): Promise { @@ -1529,14 +1537,11 @@ async function webviewPreloads(ctx: PreloadContext) { const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left); outputNode.render(data.content, preloadsAndErrors); + // Track resizes on this output + this.trackOutputResize(data.cellId, outputNode.element); + // don't hide until after this step so that the height is right cellOutput.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; - - // If notebook side has indicated we are done (and we haven't added it yet), stick in the final render - if (data.cellId === this._pendingFinalOutput) { - this._pendingFinalOutput = undefined; - cellOutput.addFinalOutputTracker(); - } } public ensureOutputCell(cellId: string, cellTop: number, skipCellTopUpdateIfExist: boolean): OutputCell { @@ -1586,6 +1591,33 @@ async function webviewPreloads(ctx: PreloadContext) { cell?.updateScroll(request); } } + + private outputResizeHandler(cellId: string) { + // If no longer tracking, disconnect. However + // send one more resize event. + if (this._trackingResize !== cellId) { + this._outputResizeObservers.forEach(o => o.disconnect()); + this._outputResizeObservers = []; + } + + // Debounce this callback to only happen after + // 250 ms. Don't need resize events that often. + clearTimeout(this._outputResizeTimer); + this._outputResizeTimer = setTimeout(() => { + postNotebookMessage('outputResized', { + cellId + }); + }, 250); + } + + private trackOutputResize(cellId: string, outputContainer: HTMLElement) { + if (this._trackingResize === cellId) { + const handler = this.outputResizeHandler.bind(this, cellId); + const observer = new ResizeObserver(handler); + this._outputResizeObservers.push(observer); + observer.observe(outputContainer); + } + } }(); class MarkdownCodeBlock { @@ -1819,6 +1851,7 @@ async function webviewPreloads(ctx: PreloadContext) { class OutputCell { public readonly element: HTMLElement; + public readonly bottomElement: HTMLElement; private readonly outputElements = new Map(); @@ -1838,8 +1871,8 @@ async function webviewPreloads(ctx: PreloadContext) { this.element = this.element; - const lowerWrapperElement = createFocusSink(cellId, true); - container.appendChild(lowerWrapperElement); + this.bottomElement = createFocusSink(cellId, true); + container.appendChild(this.bottomElement); // New thoughts. // Send message to indicate force scroll. @@ -1905,24 +1938,6 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.visibility = 'visible'; } } - - public addFinalOutputTracker() { - const id = `${this.element.id}-final-output-tracker`; - let tracker: HTMLImageElement | null = document.getElementById(id) as HTMLImageElement; - if (!tracker) { - tracker = document.createElement('img') as HTMLImageElement; - tracker.id = id; - tracker.style.height = '0px'; - tracker.src = ''; - tracker.addEventListener('error', (ev) => { - console.log(`Final cell output has rendered`); - - // Src is empty so this should fire as soon as it renders - postNotebookMessage('didRenderFinalOutput', { cellId: this.element.id }); - }); - this.element.appendChild(tracker); - } - } } class OutputContainer { From 033e4eadda87433c107b2ff106039101fc82dacd Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 23 May 2022 15:42:51 -0700 Subject: [PATCH 041/270] Always watch resize so can handle post execute resizes --- .../interactive/browser/interactiveEditor.ts | 10 +++-- .../view/renderers/backLayerWebView.ts | 24 +---------- .../browser/view/renderers/webviewMessages.ts | 14 +------ .../browser/view/renderers/webviewPreloads.ts | 42 +++++-------------- 4 files changed, 19 insertions(+), 71 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 9265c2fca79..5093e65b993 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -403,9 +403,13 @@ export class InteractiveEditor extends EditorPane { isReadOnly: true }); this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidResizeOutput((cvm) => { - // If we're already at the bottom or auto scroll is enabled, scroll to the bottom - if (this.configurationService.getValue('interactive.alwaysScrollOnNewCell') || this.#state === ScrollingState.StickyToBottom) { - this.#notebookWidget.value!.scrollToBottom(); + // Ignore resizes on anything but the last cell. + const index = this.#notebookWidget.value!.getCellIndex(cvm); + if (index === this.#notebookWidget.value!.getLength() - 1) { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this.configurationService.getValue('interactive.alwaysScrollOnNewCell') || this.#state === ScrollingState.StickyToBottom) { + this.#notebookWidget.value!.scrollToBottom(); + } } })); this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidFocusWidget(() => this.#onDidFocusWidget.fire())); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 513f8b4856e..af57430b06e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -36,7 +36,7 @@ import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; -import { CellUri, INotebookRendererInfo, NotebookCellExecutionState, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; @@ -177,28 +177,6 @@ export class BackLayerWebView extends Disposable { css: getTokenizationCss(), }); })); - - this._register(notebookExecutionStateService.onDidChangeCellExecution((e) => { - // If e.changed state is pending, it means notebook execution just started - if (e.changed && e.changed.state === NotebookCellExecutionState.Pending && e.affectsNotebook(this.documentUri)) { - const cell = this.notebookEditor.getCellByHandle(e.cellHandle); - if (cell) { - this._sendMessageToWebview({ - type: 'startWatchingOutputResize', - cellId: cell.id - }); - } - // If e.changed is undefined, it means notebook execution just finished - } else if (e.changed === undefined && e.affectsNotebook(this.documentUri)) { - const cell = this.notebookEditor.getCellByHandle(e.cellHandle); - if (cell) { - this._sendMessageToWebview({ - type: 'stopWatchingOutputResize', - cellId: cell.id - }); - } - } - })); } updateOptions(options: BacklayerWebviewOptions) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 9c0941b1ffc..4888d56bd8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -401,16 +401,6 @@ export interface IDidFindHighlightMessage extends BaseToWebviewMessage { readonly offset: number; } -export interface IStartWatchingOutputMessage { - readonly type: 'startWatchingOutputResize'; - readonly cellId: string; -} - -export interface IStopWatchingOutputMessage { - readonly type: 'stopWatchingOutputResize'; - readonly cellId: string; -} - export interface IOutputResizedMessage extends BaseToWebviewMessage { readonly type: 'outputResized'; readonly cellId: string; @@ -475,9 +465,7 @@ export type ToWebviewMessage = IClearMessage | IFindMessage | IFindHighlightMessage | IFindUnHighlightMessage | - IFindStopMessage | - IStartWatchingOutputMessage | - IStopWatchingOutputMessage; + IFindStopMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 41efb7a678f..5aff1145fd8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1173,14 +1173,6 @@ async function webviewPreloads(ctx: PreloadContext) { _highlighter?.dispose(); break; } - case 'startWatchingOutputResize': { - viewModel.startWatchingOutput(event.data.cellId); - break; - } - case 'stopWatchingOutputResize': { - viewModel.stopWatchingOutput(event.data.cellId); - break; - } } }); @@ -1403,13 +1395,14 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _markupCells = new Map(); private readonly _outputCells = new Map(); - private _trackingResize: string | undefined; private _outputResizeObservers: ResizeObserver[] = []; private _outputResizeTimer: any; public clearAll() { this._markupCells.clear(); this._outputCells.clear(); + this._outputResizeObservers.forEach(o => o.disconnect()); + this._outputResizeObservers = []; } public rerender() { @@ -1417,16 +1410,6 @@ async function webviewPreloads(ctx: PreloadContext) { this.renderOutputCells(); } - public startWatchingOutput(cellId: string) { - this._trackingResize = cellId; - this._outputResizeObservers.forEach(o => o.disconnect()); - this._outputResizeObservers = []; - } - - public stopWatchingOutput(cellId: string) { - this._trackingResize = undefined; - } - private async createMarkupCell(init: webviewMessages.IMarkupCellInitialization, top: number, visible: boolean): Promise { const existing = this._markupCells.get(init.cellId); if (existing) { @@ -1550,6 +1533,10 @@ async function webviewPreloads(ctx: PreloadContext) { if (!cell) { cell = new OutputCell(cellId); this._outputCells.set(cellId, cell); + + // New output cell, clear resize handlers + this._outputResizeObservers.forEach(o => o.disconnect); + this._outputResizeObservers = []; } if (existed && skipCellTopUpdateIfExist) { @@ -1593,13 +1580,6 @@ async function webviewPreloads(ctx: PreloadContext) { } private outputResizeHandler(cellId: string) { - // If no longer tracking, disconnect. However - // send one more resize event. - if (this._trackingResize !== cellId) { - this._outputResizeObservers.forEach(o => o.disconnect()); - this._outputResizeObservers = []; - } - // Debounce this callback to only happen after // 250 ms. Don't need resize events that often. clearTimeout(this._outputResizeTimer); @@ -1611,12 +1591,10 @@ async function webviewPreloads(ctx: PreloadContext) { } private trackOutputResize(cellId: string, outputContainer: HTMLElement) { - if (this._trackingResize === cellId) { - const handler = this.outputResizeHandler.bind(this, cellId); - const observer = new ResizeObserver(handler); - this._outputResizeObservers.push(observer); - observer.observe(outputContainer); - } + const handler = this.outputResizeHandler.bind(this, cellId); + const observer = new ResizeObserver(handler); + this._outputResizeObservers.push(observer); + observer.observe(outputContainer); } }(); From e2816721e14e8e69affdd0334690e66bd7266e64 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 23 May 2022 16:50:27 -0700 Subject: [PATCH 042/270] Fix scrolling to happen on execution too --- .../browser/interactive.contribution.ts | 8 +- .../interactive/browser/interactiveEditor.ts | 132 +++--------------- .../contrib/notebook/common/notebookCommon.ts | 3 +- 3 files changed, 26 insertions(+), 117 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index ddd9a857eaa..b276b2a4771 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -49,7 +49,7 @@ import { IInteractiveHistoryService, InteractiveHistoryService } from 'vs/workbe import { NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { CellEditType, CellKind, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellOutput, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookContentProvider, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; @@ -715,14 +715,14 @@ registerThemingParticipant((theme) => { }); Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - id: 'interactive', + id: 'notebook', order: 100, type: 'object', 'properties': { - 'interactive.alwaysScrollOnNewCell': { + [NotebookSetting.interactiveWindowAlwaysScrollOnNewCell]: { type: 'boolean', default: true, - markdownDescription: localize('interactive.alwaysScrollOnNewCell', "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.") + markdownDescription: localize('interactiveWindow.alwaysScrollOnNewCell', "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.") }, } }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 5093e65b993..c6e561c2c4d 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -22,7 +22,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; -import { CodeCellLayoutChangeEvent, IActiveNotebookEditorDelegate, ICellViewModel, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { cellEditorBackground, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; @@ -35,14 +35,13 @@ import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; import { ComplexNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { NotebookCellExecutionState, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IAction } from 'vs/base/common/actions'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; @@ -62,11 +61,6 @@ import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; -const enum ScrollingState { - Initial = 0, - StickyToBottom = 1 -} - const INPUT_CELL_VERTICAL_PADDING = 8; const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; const INPUT_EDITOR_PADDING = 8; @@ -154,6 +148,12 @@ export class InteractiveEditor extends EditorPane { codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); this._register(this.#keybindingService.onDidUpdateKeybindings(this.#updateInputDecoration, this)); + this._register(this.#notebookExecutionStateService.onDidChangeCellExecution((e) => { + const cell = this.#notebookWidget.value?.getCellByHandle(e.cellHandle); + if (cell && e.changed?.state) { + this.#scrollIfNecessary(cell); + } + })); } get #inputCellContainerHeight() { @@ -403,14 +403,7 @@ export class InteractiveEditor extends EditorPane { isReadOnly: true }); this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidResizeOutput((cvm) => { - // Ignore resizes on anything but the last cell. - const index = this.#notebookWidget.value!.getCellIndex(cvm); - if (index === this.#notebookWidget.value!.getLength() - 1) { - // If we're already at the bottom or auto scroll is enabled, scroll to the bottom - if (this.configurationService.getValue('interactive.alwaysScrollOnNewCell') || this.#state === ScrollingState.StickyToBottom) { - this.#notebookWidget.value!.scrollToBottom(); - } - } + this.#scrollIfNecessary(cvm); })); this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidFocusWidget(() => this.#onDidFocusWidget.fire())); this.#widgetDisposableStore.add(model.notebook.onDidChangeContent(() => { @@ -468,10 +461,6 @@ export class InteractiveEditor extends EditorPane { } })); - if (this.#notebookWidget.value?.hasModel()) { - this.#registerExecutionScrollListener(this.#notebookWidget.value); - } - const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this.#contextKeyService); if (input.resource && input.historyService.has(input.resource)) { cursorAtBoundaryContext.set('top'); @@ -521,107 +510,26 @@ export class InteractiveEditor extends EditorPane { } } - #lastCell: ICellViewModel | undefined = undefined; - #lastCellDisposable = new DisposableStore(); - #state: ScrollingState = ScrollingState.Initial; - - #cellAtBottom(widget: IActiveNotebookEditorDelegate, cell: ICellViewModel): boolean { - const visibleRanges = widget.visibleRanges; - const cellIndex = widget.getCellIndex(cell); - if (cellIndex === Math.max(...visibleRanges.map(range => range.end))) { + #cellAtBottom(cell: ICellViewModel): boolean { + const visibleRanges = this.#notebookWidget.value?.visibleRanges || []; + const cellIndex = this.#notebookWidget.value?.getCellIndex(cell); + if (cellIndex === Math.max(...visibleRanges.map(range => range.end - 1))) { return true; } return false; } - /** - * - Init state: 0 - * - Will cell insertion: check if the last cell is at the bottom, false, stay 0 - * if true, state 1 (ready for auto reveal) - * - receive a scroll event (scroll even already happened). If the last cell is at bottom, false, 0, true, state 1 - * - height change of the last cell, if state 0, do nothing, if state 1, scroll the last cell fully into view - */ - #registerExecutionScrollListener(widget: NotebookEditorWidget & IActiveNotebookEditorDelegate) { - this.#widgetDisposableStore.add(widget.textModel.onWillAddRemoveCells(e => { - const lastViewCell = widget.cellAt(widget.getLength() - 1); - - // check if the last cell is at the bottom - if (lastViewCell && this.#cellAtBottom(widget, lastViewCell)) { - this.#state = ScrollingState.StickyToBottom; - } else { - this.#state = ScrollingState.Initial; + #scrollIfNecessary(cvm: ICellViewModel) { + const index = this.#notebookWidget.value!.getCellIndex(cvm); + if (index === this.#notebookWidget.value!.getLength() - 1) { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this.configurationService.getValue(NotebookSetting.interactiveWindowAlwaysScrollOnNewCell) || this.#cellAtBottom(cvm)) { + this.#notebookWidget.value!.scrollToBottom(); } - })); - - this.#widgetDisposableStore.add(widget.onDidScroll(() => { - const lastViewCell = widget.cellAt(widget.getLength() - 1); - - // check if the last cell is at the bottom - if (lastViewCell && this.#cellAtBottom(widget, lastViewCell)) { - this.#state = ScrollingState.StickyToBottom; - } else { - this.#state = ScrollingState.Initial; - } - })); - - this.#widgetDisposableStore.add(widget.textModel.onDidChangeContent(e => { - for (let i = 0; i < e.rawEvents.length; i++) { - const event = e.rawEvents[i]; - - if (event.kind === NotebookCellsChangeType.ModelChange && this.#notebookWidget.value?.hasModel()) { - const lastViewCell = this.#notebookWidget.value.cellAt(this.#notebookWidget.value.getLength() - 1); - if (lastViewCell !== this.#lastCell) { - this.#lastCellDisposable.clear(); - this.#lastCell = lastViewCell; - this.#registerListenerForCell(); - } - } - } - })); - } - - #registerListenerForCell() { - if (!this.#lastCell) { - return; } - - this.#lastCellDisposable.add(this.#lastCell.onDidChangeLayout((e) => { - if (e.totalHeight === undefined) { - // not cell height change - return; - } - - if (!this.#notebookWidget.value) { - return; - } - - if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value.isScrolledToBottom()) { - return; - } - - if (this.#state !== ScrollingState.StickyToBottom) { - return; - } - - if (this.#lastCell) { - const runState = this.#notebookExecutionStateService.getCellExecution(this.#lastCell.uri)?.state; - if (runState === NotebookCellExecutionState.Executing) { - return; - } - - } - - // scroll to bottom - // postpone to next tick as the list view might not process the output height change yet - // e.g., when we register this listener later than the list view - this.#lastCellDisposable.add(DOM.scheduleAtNextAnimationFrame(() => { - if (this.#state === ScrollingState.StickyToBottom) { - this.#notebookWidget.value!.scrollToBottom(); - } - })); - })); } + #syncWithKernel() { const notebook = this.#notebookWidget.value?.textModel; const textModel = this.#codeEditorWidget.getModel(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 40b6b6a2074..e678891f59a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -915,7 +915,8 @@ export const NotebookSetting = { interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', outputLineHeight: 'notebook.outputLineHeight', outputFontSize: 'notebook.outputFontSize', - outputFontFamily: 'notebook.outputFontFamily' + outputFontFamily: 'notebook.outputFontFamily', + interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell' } as const; export const enum CellStatusbarAlignment { From 5d8bd237564650c7257b65b6839597b7150ffe1a Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 23 May 2022 17:25:00 -0700 Subject: [PATCH 043/270] Add a test --- .../interactiveWindow.test.ts | 65 +++++++++++++++++++ .../src/singlefolder-tests/notebook.test.ts | 4 +- 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts new file mode 100644 index 00000000000..eafc18804fd --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; +import { disposeAll } from '../utils'; +import { Kernel, saveAllFilesAndCloseAll } from './notebook.test'; + +export type INativeInteractiveWindow = { notebookUri: vscode.Uri; inputUri: vscode.Uri; notebookEditor: vscode.NotebookEditor }; + +async function createInteractiveWindow(kernel: Kernel) { + const { notebookEditor } = (await vscode.commands.executeCommand( + 'interactive.open', + // Keep focus on the owning file if there is one + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }, + undefined, + kernel.controller.id, + undefined + )) as unknown as INativeInteractiveWindow; + + return notebookEditor; +} + + +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Interactive Window', function () { + + const testDisposables: vscode.Disposable[] = []; + let defaultKernel: Kernel; + + setup(async function () { + // there should be ONE default kernel in this suite + defaultKernel = new Kernel('mainKernel', 'Notebook Default Kernel'); + testDisposables.push(defaultKernel.controller); + await saveAllFilesAndCloseAll(); + }); + + teardown(async function () { + disposeAll(testDisposables); + testDisposables.length = 0; + await saveAllFilesAndCloseAll(); + }); + + test('Can open an interactive window', async () => { + assert.ok(vscode.workspace.workspaceFolders); + const notebookEditor = await createInteractiveWindow(defaultKernel); + assert.ok(notebookEditor); + + // Try adding a cell and running it. + const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print foo', 'typescript'); + const edit = vscode.NotebookEdit.insertCells(0, [cell]); + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(notebookEditor.notebook.uri, [edit]); + await vscode.workspace.applyEdit(workspaceEdit); + + assert.strictEqual(notebookEditor.notebook.cellCount, 1); + assert.strictEqual(notebookEditor.notebook.cellAt(0).kind, vscode.NotebookCellKind.Code); + + await vscode.commands.executeCommand('notebook.execute'); + assert.strictEqual(notebookEditor.notebook.cellAt(0).outputs.length, 1, 'should execute'); + + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 462e3113698..b68eb587922 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -18,7 +18,7 @@ async function openRandomNotebookDocument() { return vscode.workspace.openNotebookDocument(uri); } -async function saveAllFilesAndCloseAll() { +export async function saveAllFilesAndCloseAll() { await saveAllEditors(); await closeAllEditors(); } @@ -29,7 +29,7 @@ async function withEvent(event: vscode.Event, callback: (e: Promise) => } -class Kernel { +export class Kernel { readonly controller: vscode.NotebookController; From 34a75f45d688ec88f1635670b783e805a1778db4 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 24 May 2022 14:45:09 +0200 Subject: [PATCH 044/270] use CSS variables for colors --- .../contrib/mergeEditor/browser/media/mergeEditor.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index 0b9caf7faf8..abae99ac16c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -56,7 +56,7 @@ } .merge-accept-gutter-marker.multi-line .background { - border: 2px solid #323232FF; + border: 2px solid var(--vscode-checkbox-border); border-right: 0; left: 8px; width: 10px; @@ -74,5 +74,5 @@ .merge-accept-gutter-marker .checkbox .accept-conflict-group.monaco-custom-toggle.monaco-checkbox { margin: 0; padding: 0; - background-color: #3C3C3CFF; + background-color: var(--vscode-checkbox-border); } From 0e8b6a43b0deb31e8c23d0c3b1c23f218f2c593f Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 24 May 2022 15:00:51 +0200 Subject: [PATCH 045/270] show correct description for input2 --- src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index f46dce24ff1..0f0ef7c4cba 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -184,7 +184,7 @@ export class MergeEditor extends EditorPane { const model = await input.resolve(); this.input1View.setModel(model, model.input1, localize('yours', 'Yours'), model.input1Detail, model.input1Description); - this.input2View.setModel(model, model.input2, localize('theirs', 'Theirs',), model.input2Detail, model.input1Description); + this.input2View.setModel(model, model.input2, localize('theirs', 'Theirs',), model.input2Detail, model.input2Description); this.inputResultView.setModel(model, model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true }), undefined); // TODO: Update editor options! From 9322fd543d401efa136d24656c8be455edd7c010 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 24 May 2022 10:13:04 -0700 Subject: [PATCH 046/270] Fix test to pass --- .../src/singlefolder-tests/interactiveWindow.test.ts | 2 +- .../vscode-api-tests/src/singlefolder-tests/notebook.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts index eafc18804fd..0890418484c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -32,7 +32,7 @@ async function createInteractiveWindow(kernel: Kernel) { setup(async function () { // there should be ONE default kernel in this suite - defaultKernel = new Kernel('mainKernel', 'Notebook Default Kernel'); + defaultKernel = new Kernel('mainKernel', 'Notebook Default Kernel', 'interactive'); testDisposables.push(defaultKernel.controller); await saveAllFilesAndCloseAll(); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index b68eb587922..8c413aaf9fb 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -35,8 +35,8 @@ export class Kernel { readonly associatedNotebooks = new Set(); - constructor(id: string, label: string) { - this.controller = vscode.notebooks.createNotebookController(id, 'notebookCoreTest', label); + constructor(id: string, label: string, viewType: string = 'notebookCoreTest') { + this.controller = vscode.notebooks.createNotebookController(id, viewType, label); this.controller.executeHandler = this._execute.bind(this); this.controller.supportsExecutionOrder = true; this.controller.supportedLanguages = ['typescript', 'javascript']; From 1b88f28b495c90bb656bc3fb710a49d662074a3e Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 24 May 2022 10:32:27 -0700 Subject: [PATCH 047/270] pass in registry --- .../contrib/tasks/common/taskConfiguration.ts | 12 ++++++------ .../tasks/common/taskDefinitionRegistry.ts | 2 +- .../tasks/test/common/taskConfiguration.test.ts | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 1fd6ca72d5b..7bf202d5c8e 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -20,7 +20,7 @@ import { import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import * as Tasks from './tasks'; -import { TaskDefinitionRegistry } from './taskDefinitionRegistry'; +import { ITaskDefinitionRegistry, TaskDefinitionRegistry } from './taskDefinitionRegistry'; import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { URI } from 'vs/base/common/uri'; import { USER_TASKS_GROUP_KEY, ShellExecutionSupportedContext, ProcessExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; @@ -1389,7 +1389,7 @@ namespace ConfiguringTask { customize: string; } - export function from(this: void, external: ConfiguringTask, context: ParseContext, index: number, source: TaskConfigSource, testTaskDefinition?: Tasks.TaskDefinition): Tasks.ConfiguringTask | undefined { + export function from(this: void, external: ConfiguringTask, context: ParseContext, index: number, source: TaskConfigSource, registry?: Partial): Tasks.ConfiguringTask | undefined { if (!external) { return undefined; } @@ -1399,7 +1399,7 @@ namespace ConfiguringTask { context.problemReporter.error(nls.localize('ConfigurationParser.noTaskType', 'Error: tasks configuration must have a type property. The configuration will be ignored.\n{0}\n', JSON.stringify(external, null, 4))); return undefined; } - let typeDeclaration = type ? testTaskDefinition || TaskDefinitionRegistry.get(type) : undefined; + let typeDeclaration = type ? registry?.get?.(type) || TaskDefinitionRegistry.get(type) : undefined; if (!typeDeclaration) { let message = nls.localize('ConfigurationParser.noTypeDefinition', 'Error: there is no registered task type \'{0}\'. Did you miss installing an extension that provides a corresponding task provider?', type); context.problemReporter.error(message); @@ -1672,7 +1672,7 @@ export namespace TaskParser { process: ProcessExecutionSupportedContext }; - export function from(this: void, externals: Array | undefined, globals: Globals, context: ParseContext, source: TaskConfigSource, testTaskDefinition?: Tasks.TaskDefinition): TaskParseResult { + export function from(this: void, externals: Array | undefined, globals: Globals, context: ParseContext, source: TaskConfigSource, registry?: Partial): TaskParseResult { let result: TaskParseResult = { custom: [], configured: [] }; if (!externals) { return result; @@ -1683,7 +1683,7 @@ export namespace TaskParser { const baseLoadIssues = Objects.deepClone(context.taskLoadIssues); for (let index = 0; index < externals.length; index++) { let external = externals[index]; - const definition = external.type ? testTaskDefinition || TaskDefinitionRegistry.get(external.type) : undefined; + const definition = external.type ? registry?.get?.(external.type) || TaskDefinitionRegistry.get(external.type) : undefined; let typeNotSupported: boolean = false; if (definition && definition.when && !context.contextKeyService.contextMatchesRules(definition.when)) { typeNotSupported = true; @@ -1743,7 +1743,7 @@ export namespace TaskParser { result.custom.push(customTask); } } else { - let configuredTask = ConfiguringTask.from(external, context, index, source, testTaskDefinition); + let configuredTask = ConfiguringTask.from(external, context, index, source, registry); if (configuredTask) { configuredTask.addTaskLoadMessages(context.taskLoadIssues); result.configured.push(configuredTask); diff --git a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts index 8d612f6d9a6..340990cb62c 100644 --- a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts @@ -99,7 +99,7 @@ export interface ITaskDefinitionRegistry { onDefinitionsChanged: Event; } -class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { +export class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { private taskTypes: IStringDictionary; private readyPromise: Promise; diff --git a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 95fb3eb7b55..4db99d3955c 100644 --- a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -19,6 +19,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { IContext } from 'vs/platform/contextkey/common/contextkey'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ITaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; const workspaceFolder: WorkspaceFolder = new WorkspaceFolder({ uri: URI.file('/workspace/folderOne'), @@ -1772,7 +1773,19 @@ class TestNamedProblemMatcher implements Partial { class TestParseContext implements Partial { } +class TestTaskDefinitionRegistry implements Partial { + private _task: Tasks.TaskDefinition | undefined; + public get(key: string): Tasks.TaskDefinition { + return this._task!; + } + public set(task: Tasks.TaskDefinition) { + this._task = task; + } +} + suite('To task configuration from', () => { + + const TaskDefinitionRegistry = new TestTaskDefinitionRegistry(); let instantiationService: TestInstantiationService; let parseContext: ParseContext; let namedProblemMatcher: NamedProblemMatcher; @@ -1827,7 +1840,8 @@ suite('To task configuration from', () => { suite('ConfiguredTask', () => { test('returns expected result', () => { const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }, { taskName: 'task 2', command: 'echo test', type: 'any', label: 'task 2' }]; - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, { extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); + TaskDefinitionRegistry.set({ extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); + const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, TaskDefinitionRegistry); assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); }); }); From b6aaee8c617f6861ddd355bab7d702264be9dc73 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 24 May 2022 10:39:21 -0700 Subject: [PATCH 048/270] remove a bunch of casts --- .../test/common/taskConfiguration.test.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 4db99d3955c..bf496555a15 100644 --- a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -1783,8 +1783,9 @@ class TestTaskDefinitionRegistry implements Partial { } } -suite('To task configuration from', () => { - +suite('Task configuration conversions', () => { + const globals = {} as Globals; + const taskConfigSource = {} as TaskConfigSource; const TaskDefinitionRegistry = new TestTaskDefinitionRegistry(); let instantiationService: TestInstantiationService; let parseContext: ParseContext; @@ -1801,7 +1802,7 @@ suite('To task configuration from', () => { parseContext.namedProblemMatchers = { 'real': namedProblemMatcher }; parseContext.uuidMap = new UUIDMap(); }); - suite('ProblemMatcher config', () => { + suite('ProblemMatcherConverter.from', () => { test('returns [] and an error for an unknown problem matcher', () => { const result = (ProblemMatcherConverter.from('$fake', parseContext)); assert.deepEqual(result.value, []); @@ -1819,21 +1820,24 @@ suite('To task configuration from', () => { assert.deepEqual(result.value, [{ "label": "real label", "applyTo": ApplyToKind.closedDocuments }]); }); }); - suite('TaskParser external config', () => { + suite('TaskParser.from', () => { suite('CustomTask', () => { suite('incomplete config reports an appropriate error for missing', () => { test('name', () => { - const result = TaskParser.from([{} as CustomTask], {} as Globals, parseContext, {} as TaskConfigSource); + const result = TaskParser.from([{} as CustomTask], globals, parseContext, taskConfigSource); assertTaskParseResult(result, undefined, problemReporter, 'Error: a task must provide a label property'); }); test('command', () => { - const result = TaskParser.from([{ taskName: 'task' } as CustomTask], {} as Globals, parseContext, {} as TaskConfigSource); + const result = TaskParser.from([{ taskName: 'task' } as CustomTask], globals, parseContext, taskConfigSource); assertTaskParseResult(result, undefined, problemReporter, "Error: the task 'task' doesn't define a command"); }); }); test('returns expected result', () => { - const expected = [{ taskName: 'task', command: 'echo test' } as CustomTask, { taskName: 'task 2', command: 'echo test' } as CustomTask]; - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource); + const expected = [ + { taskName: 'task', command: 'echo test' } as CustomTask, + { taskName: 'task 2', command: 'echo test' } as CustomTask + ]; + const result = TaskParser.from(expected, globals, parseContext, taskConfigSource); assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); }); }); @@ -1841,7 +1845,7 @@ suite('To task configuration from', () => { test('returns expected result', () => { const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }, { taskName: 'task 2', command: 'echo test', type: 'any', label: 'task 2' }]; TaskDefinitionRegistry.set({ extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.TaskDefinition); - const result = TaskParser.from(expected, {} as Globals, parseContext, {} as TaskConfigSource, TaskDefinitionRegistry); + const result = TaskParser.from(expected, globals, parseContext, taskConfigSource, TaskDefinitionRegistry); assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); }); }); From b79d02db5c11960188a43d8bfa12001ab84bb3b8 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 24 May 2022 11:01:51 -0700 Subject: [PATCH 049/270] Add test for scrolling --- .../interactiveWindow.test.ts | 37 +++++++++++++++---- .../src/singlefolder-tests/notebook.test.ts | 9 ++++- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts index 0890418484c..6880607ca7d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -24,6 +24,22 @@ async function createInteractiveWindow(kernel: Kernel) { return notebookEditor; } +async function addCell(code: string, notebook: vscode.NotebookDocument) { + const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, code, 'typescript'); + const edit = vscode.NotebookEdit.insertCells(notebook.cellCount, [cell]); + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(notebook.uri, [edit]); + await vscode.workspace.applyEdit(workspaceEdit); + return notebook.cellAt(notebook.cellCount - 1); +} + +async function addCellAndRun(code: string, notebook: vscode.NotebookDocument) { + const cell = await addCell(code, notebook); + await vscode.commands.executeCommand('notebook.execute'); + assert.strictEqual(cell.outputs.length, 1, 'execute failed'); + return cell; +} + (vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Interactive Window', function () { @@ -49,17 +65,24 @@ async function createInteractiveWindow(kernel: Kernel) { assert.ok(notebookEditor); // Try adding a cell and running it. - const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print foo', 'typescript'); - const edit = vscode.NotebookEdit.insertCells(0, [cell]); - const workspaceEdit = new vscode.WorkspaceEdit(); - workspaceEdit.set(notebookEditor.notebook.uri, [edit]); - await vscode.workspace.applyEdit(workspaceEdit); + await addCell('print foo', notebookEditor.notebook); assert.strictEqual(notebookEditor.notebook.cellCount, 1); assert.strictEqual(notebookEditor.notebook.cellAt(0).kind, vscode.NotebookCellKind.Code); + }); - await vscode.commands.executeCommand('notebook.execute'); - assert.strictEqual(notebookEditor.notebook.cellAt(0).outputs.length, 1, 'should execute'); + test('Interactive window scrolls after execute', async () => { + assert.ok(vscode.workspace.workspaceFolders); + const notebookEditor = await createInteractiveWindow(defaultKernel); + assert.ok(notebookEditor); + + // Run and add a bunch of cells + for (let i = 0; i < 20; i++) { + await addCellAndRun(`print ${i}`, notebookEditor.notebook); + } + + // Verify visible range has the last cell + assert.strictEqual(notebookEditor.visibleRanges[notebookEditor.visibleRanges.length - 1].end, notebookEditor.notebook.cellCount, `Last cell is not visible`); }); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 8c413aaf9fb..33cad6edb97 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -29,6 +29,12 @@ async function withEvent(event: vscode.Event, callback: (e: Promise) => } +function sleep(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + export class Kernel { readonly controller: vscode.NotebookController; @@ -59,8 +65,9 @@ export class Kernel { // create a single output with exec order 1 and output is plain/text // of either the cell itself or (iff empty) the cell's document's uri const task = this.controller.createNotebookCellExecution(cell); - task.start(); + task.start(Date.now()); task.executionOrder = 1; + await sleep(10); // Force to be take some time await task.replaceOutput([new vscode.NotebookCellOutput([ vscode.NotebookCellOutputItem.text(cell.document.getText() || cell.document.uri.toString(), 'text/plain') ])]); From f9b9f42034dd39fc473ea27003afdb57e5588c03 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 24 May 2022 11:14:12 -0700 Subject: [PATCH 050/270] update layout troubleshooting --- .../browser/contrib/troubleshoot/layout.ts | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index 29d4e867f65..b3af4143741 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -7,9 +7,10 @@ import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/commo import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { getNotebookEditorFromEditorPane, ICellViewModel, ICommonCellViewModelLayoutChangeInfo, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getNotebookEditorFromEditorPane, ICellViewModel, ICommonCellViewModelLayoutChangeInfo, INotebookDeltaCellStatusBarItems, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -18,31 +19,37 @@ export class TroubleshootController extends Disposable implements INotebookEdito private readonly _localStore = this._register(new DisposableStore()); private _cellStateListeners: IDisposable[] = []; - private _logging: boolean = false; + private _enabled: boolean = false; + private _cellStatusItems: string[] = []; constructor(private readonly _notebookEditor: INotebookEditor) { super(); this._register(this._notebookEditor.onDidChangeModel(() => { - this._localStore.clear(); - this._cellStateListeners.forEach(listener => listener.dispose()); - - if (!this._notebookEditor.hasModel()) { - return; - } - - this._updateListener(); + this._update(); })); + this._update(); + } + + toggle(): void { + this._enabled = !this._enabled; + this._update(); + } + + private _update() { + this._localStore.clear(); + this._cellStateListeners.forEach(listener => listener.dispose()); + + if (!this._notebookEditor.hasModel()) { + return; + } + this._updateListener(); } - toggleLogging(): void { - this._logging = !this._logging; - } - private _log(cell: ICellViewModel, e: any) { - if (this._logging) { + if (this._enabled) { const oldHeight = (this._notebookEditor as NotebookEditorWidget).getViewHeight(cell); console.log(`cell#${cell.handle}`, e, `${oldHeight} -> ${cell.layoutInfo.totalHeight}`); } @@ -73,6 +80,33 @@ export class TroubleshootController extends Disposable implements INotebookEdito dispose(deletedCells); }); })); + + const vm = this._notebookEditor._getViewModel(); + let items: INotebookDeltaCellStatusBarItems[] = []; + + if (this._enabled) { + items = this._getItemsForCells(); + } + + this._cellStatusItems = vm.deltaCellStatusBarItems(this._cellStatusItems, items); + } + + private _getItemsForCells(): INotebookDeltaCellStatusBarItems[] { + const items: INotebookDeltaCellStatusBarItems[] = []; + for (let i = 0; i < this._notebookEditor.getLength(); i++) { + items.push({ + handle: i, + items: [ + { + text: `index: ${i}`, + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER + } + ] + }); + } + + return items; } override dispose() { @@ -102,7 +136,7 @@ registerAction2(class extends Action2 { } const controller = editor.getContribution(TroubleshootController.id); - controller?.toggleLogging(); + controller?.toggle(); } }); From 98fdef2b5e18193dccab264da977f6d2988c4804 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 24 May 2022 11:15:46 -0700 Subject: [PATCH 051/270] make sure page up and down goes to next page. --- src/vs/base/browser/ui/list/listWidget.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 55082221467..87a271fc02f 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1598,9 +1598,10 @@ export class List implements ISpliceable, IThemable, IDisposable { let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight); lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1; const lastPageElement = this.view.element(lastPageIndex); - const currentlyFocusedElement = this.getFocusedElements()[0]; + const currentlyFocusedElementIndex = this.getFocus()[0]; + const currentlyFocusedElement = this.view.element(currentlyFocusedElementIndex); - if (currentlyFocusedElement !== lastPageElement) { + if (currentlyFocusedElement !== lastPageElement && lastPageIndex > currentlyFocusedElementIndex) { const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter); if (lastGoodPageIndex > -1 && currentlyFocusedElement !== this.view.element(lastGoodPageIndex)) { @@ -1610,7 +1611,13 @@ export class List implements ISpliceable, IThemable, IDisposable { } } else { const previousScrollTop = this.view.getScrollTop(); - this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex)); + let nextpageScrollTop = previousScrollTop + this.view.renderHeight; + if (lastPageIndex > currentlyFocusedElementIndex) { + // scroll last page element to the top only if the last page element is below the focused element + nextpageScrollTop -= this.view.elementHeight(lastPageIndex); + } + + this.view.setScrollTop(nextpageScrollTop); if (this.view.getScrollTop() !== previousScrollTop) { this.setFocus([]); @@ -1633,9 +1640,10 @@ export class List implements ISpliceable, IThemable, IDisposable { } const firstPageElement = this.view.element(firstPageIndex); - const currentlyFocusedElement = this.getFocusedElements()[0]; + const currentlyFocusedElementIndex = this.getFocus()[0]; + const currentlyFocusedElement = this.view.element(currentlyFocusedElementIndex); - if (currentlyFocusedElement !== firstPageElement) { + if (currentlyFocusedElement !== firstPageElement && currentlyFocusedElementIndex >= firstPageIndex) { const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter); if (firstGoodPageIndex > -1 && currentlyFocusedElement !== this.view.element(firstGoodPageIndex)) { From 21e64f84458090e52aede145202c9940ae21172c Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 24 May 2022 11:24:44 -0700 Subject: [PATCH 052/270] Remove unnecessary changes --- .../browser/view/renderers/webviewPreloads.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 5aff1145fd8..bdaa36e08b3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1829,8 +1829,6 @@ async function webviewPreloads(ctx: PreloadContext) { class OutputCell { public readonly element: HTMLElement; - public readonly bottomElement: HTMLElement; - private readonly outputElements = new Map(); constructor(cellId: string) { @@ -1849,14 +1847,8 @@ async function webviewPreloads(ctx: PreloadContext) { this.element = this.element; - this.bottomElement = createFocusSink(cellId, true); - container.appendChild(this.bottomElement); - - // New thoughts. - // Send message to indicate force scroll. - // Add resizeObserver on the element - // When resize occurs, scroll lowerWrapperElement into view - // Send message to indicate stop scrolling + const lowerWrapperElement = createFocusSink(cellId, true); + container.appendChild(lowerWrapperElement); } public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { From acc1f9c29ba3c16214af1e77832454e25d66afcd Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 24 May 2022 11:48:40 -0700 Subject: [PATCH 053/270] Remove empty line --- .../contrib/notebook/browser/view/renderers/webviewPreloads.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index bdaa36e08b3..441f75067b9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1846,7 +1846,6 @@ async function webviewPreloads(ctx: PreloadContext) { container.appendChild(this.element); this.element = this.element; - const lowerWrapperElement = createFocusSink(cellId, true); container.appendChild(lowerWrapperElement); } From b1a0aa5cfdacecf5e4f7b1991353243a07bd3f89 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 24 May 2022 13:10:40 -0700 Subject: [PATCH 054/270] Use existing resize observer instead --- .../browser/view/renderers/webviewPreloads.ts | 66 ++++++++----------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 441f75067b9..471fdba68bc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -294,7 +294,8 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _observer: ResizeObserver; - private readonly _observedElements = new WeakMap(); + private readonly _observedElements = new WeakMap(); + private _outputResizeTimer: any; constructor() { this._observer = new ResizeObserver(entries => { @@ -308,6 +309,8 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + this.postResizeMessage(observedElementInfo.cellId); + if (entry.target.id === observedElementInfo.id && entry.contentRect) { if (observedElementInfo.output) { if (entry.contentRect.height !== 0) { @@ -329,14 +332,26 @@ async function webviewPreloads(ctx: PreloadContext) { }); } - public observe(container: Element, id: string, output: boolean) { + public observe(container: Element, id: string, output: boolean, cellId: string) { if (this._observedElements.has(container)) { return; } - this._observedElements.set(container, { id, output, lastKnownHeight: -1 }); + this._observedElements.set(container, { id, output, lastKnownHeight: -1, cellId }); this._observer.observe(container); } + + private postResizeMessage(cellId: string) { + // Debounce this callback to only happen after + // 250 ms. Don't need resize events that often. + clearTimeout(this._outputResizeTimer); + this._outputResizeTimer = setTimeout(() => { + postNotebookMessage('outputResized', { + cellId + }); + }, 250); + + } }; function scrollWillGoToParent(event: WheelEvent) { @@ -1395,14 +1410,10 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _markupCells = new Map(); private readonly _outputCells = new Map(); - private _outputResizeObservers: ResizeObserver[] = []; - private _outputResizeTimer: any; public clearAll() { this._markupCells.clear(); this._outputCells.clear(); - this._outputResizeObservers.forEach(o => o.disconnect()); - this._outputResizeObservers = []; } public rerender() { @@ -1517,12 +1528,9 @@ async function webviewPreloads(ctx: PreloadContext) { } const cellOutput = this.ensureOutputCell(data.cellId, data.cellTop, false); - const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left); + const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left, data.cellId); outputNode.render(data.content, preloadsAndErrors); - // Track resizes on this output - this.trackOutputResize(data.cellId, outputNode.element); - // don't hide until after this step so that the height is right cellOutput.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; } @@ -1533,10 +1541,6 @@ async function webviewPreloads(ctx: PreloadContext) { if (!cell) { cell = new OutputCell(cellId); this._outputCells.set(cellId, cell); - - // New output cell, clear resize handlers - this._outputResizeObservers.forEach(o => o.disconnect); - this._outputResizeObservers = []; } if (existed && skipCellTopUpdateIfExist) { @@ -1578,24 +1582,6 @@ async function webviewPreloads(ctx: PreloadContext) { cell?.updateScroll(request); } } - - private outputResizeHandler(cellId: string) { - // Debounce this callback to only happen after - // 250 ms. Don't need resize events that often. - clearTimeout(this._outputResizeTimer); - this._outputResizeTimer = setTimeout(() => { - postNotebookMessage('outputResized', { - cellId - }); - }, 250); - } - - private trackOutputResize(cellId: string, outputContainer: HTMLElement) { - const handler = this.outputResizeHandler.bind(this, cellId); - const observer = new ResizeObserver(handler); - this._outputResizeObservers.push(observer); - observer.observe(outputContainer); - } }(); class MarkdownCodeBlock { @@ -1695,7 +1681,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.addEventListeners(); this.updateContentAndRender(this._content.value).then(() => { - resizeObserver.observe(this.element, this.id, false); + resizeObserver.observe(this.element, this.id, false, this.id); resolveReady(); }); } @@ -1850,7 +1836,7 @@ async function webviewPreloads(ctx: PreloadContext) { container.appendChild(lowerWrapperElement); } - public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { + public createOutputElement(outputId: string, outputOffset: number, left: number, cellId: string): OutputElement { let outputContainer = this.outputElements.get(outputId); if (!outputContainer) { outputContainer = new OutputContainer(outputId); @@ -1858,7 +1844,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.outputElements.set(outputId, outputContainer); } - return outputContainer.createOutputElement(outputId, outputOffset, left); + return outputContainer.createOutputElement(outputId, outputOffset, left, cellId); } public clearOutput(outputId: string, rendererId: string | undefined) { @@ -1940,12 +1926,12 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.top = `${outputOffset}px`; } - public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { + public createOutputElement(outputId: string, outputOffset: number, left: number, cellId: string): OutputElement { this.element.innerText = ''; this.element.style.maxHeight = '0px'; this.element.style.top = `${outputOffset}px`; - this._outputNode = new OutputElement(outputId, left); + this._outputNode = new OutputElement(outputId, left, cellId); this.element.appendChild(this._outputNode.element); return this._outputNode; } @@ -1977,13 +1963,13 @@ async function webviewPreloads(ctx: PreloadContext) { class OutputElement { public readonly element: HTMLElement; - private _content?: { content: webviewMessages.ICreationContent; preloadsAndErrors: unknown[] }; private hasResizeObserver = false; constructor( private readonly outputId: string, left: number, + public readonly cellId: string ) { this.element = document.createElement('div'); this.element.id = outputId; @@ -2017,7 +2003,7 @@ async function webviewPreloads(ctx: PreloadContext) { if (!this.hasResizeObserver) { this.hasResizeObserver = true; - resizeObserver.observe(this.element, this.outputId, true); + resizeObserver.observe(this.element, this.outputId, true, this.cellId); } const offsetHeight = this.element.offsetHeight; From 60c8307e9642d2d950584450efdd8bd240b056c9 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 24 May 2022 13:18:29 -0700 Subject: [PATCH 055/270] Remove unnecessary interface addition --- .../contrib/notebook/browser/diff/notebookTextDiffEditor.ts | 4 ---- .../contrib/notebook/browser/notebookEditorWidget.ts | 1 - .../notebook/browser/view/renderers/backLayerWebView.ts | 1 - 3 files changed, 6 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 61c5daf9949..c0951594b1c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -792,10 +792,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD throw new Error('Not implemented'); } - getCellByHandle(cellHandle: number): IGenericCellViewModel | undefined { - throw new Error('Not implemented'); - } - removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide) { this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 4bd1f431a7f..f5ce4eaa696 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1387,7 +1387,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD triggerScroll(event: IMouseWheelEvent) { that._listViewInfoAccessor.triggerScroll(event); }, getCellByInfo: that.getCellByInfo.bind(that), getCellById: that._getCellById.bind(that), - getCellByHandle: that.getCellByHandle.bind(that), toggleNotebookCellSelection: that._toggleNotebookCellSelection.bind(that), focusNotebookCell: that.focusNotebookCell.bind(that), focusNextNotebookCell: that.focusNextNotebookCell.bind(that), diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 316ac6490d7..dedbe999773 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -67,7 +67,6 @@ export interface INotebookDelegateForWebview { focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): Promise; toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; - getCellByHandle(cellHandle: number): IGenericCellViewModel | undefined; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; From 1ed660b17e3c01e57937bbfb53aadf21407ceb11 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 25 May 2022 11:59:53 +0200 Subject: [PATCH 056/270] editor gutter accept buttons bug-fixing. --- .../contrib/mergeEditor/browser/editorGutter.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts index c4390489209..429457d5a50 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/editorGutter.ts @@ -86,13 +86,17 @@ export class EditorGutter< } const top = - this._editor.getTopForLineNumber(gutterItem.range.startLineNumber - 1) - - scrollTop + lineHeight; + (gutterItem.range.startLineNumber === 1 + ? -lineHeight + : this._editor.getTopForLineNumber( + gutterItem.range.startLineNumber - 1 + )) - + scrollTop + + lineHeight; - // +1 line and -lineHeight makes the height to cover view zones at the end of the range. const bottom = - this._editor.getTopForLineNumber(gutterItem.range.endLineNumberExclusive + 1) - - scrollTop - lineHeight; + this._editor.getTopForLineNumber(gutterItem.range.endLineNumberExclusive) - + scrollTop; const height = bottom - top; From 52c5c31139826f8c3448c22e6c9472376ea40ba8 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 25 May 2022 12:31:56 +0200 Subject: [PATCH 057/270] use fake timers (#150356) --- .../test/common/userDataSyncStoreService.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 4790464ce4a..96031d04737 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -484,14 +484,14 @@ suite('UserDataSyncRequestsSession', () => { assert.fail('Should fail with limit exceeded'); }); - test('requests are handled after session is expired', async () => { + test('requests are handled after session is expired', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const testObject = new RequestsSession(1, 100, requestService, new NullLogService()); await testObject.request('url', {}, CancellationToken.None); await timeout(125); await testObject.request('url', {}, CancellationToken.None); - }); + })); - test('too many requests are thrown after session is expired', async () => { + test('too many requests are thrown after session is expired', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const testObject = new RequestsSession(1, 100, requestService, new NullLogService()); await testObject.request('url', {}, CancellationToken.None); await timeout(125); @@ -505,6 +505,6 @@ suite('UserDataSyncRequestsSession', () => { return; } assert.fail('Should fail with limit exceeded'); - }); + })); }); From 73dda0c06add139307f6be753cea50028bcde05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 25 May 2022 14:42:17 +0200 Subject: [PATCH 058/270] remove UpdateMode policy (#150357) --- build/azure-pipelines/win32/product-build-win32.yml | 7 ------- build/gulpfile.vscode.js | 3 --- .../platform/update/common/update.config.contribution.ts | 6 +----- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 4b746afc2a1..7501869b57d 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -136,13 +136,6 @@ steps: displayName: Download Electron condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build\lib\policies } - displayName: Generate Group Policy definitions - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 94b22026a07..7dff1645210 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -330,9 +330,6 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); - result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) - .pipe(rename(f => f.dirname = `policies/${f.dirname}`))); - } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) .pipe(replace('@@PRODNAME@@', product.nameLong)) diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index 4134233def6..5ceb95727b5 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -27,11 +27,7 @@ configurationRegistry.registerConfiguration({ localize('manual', "Disable automatic background update checks. Updates will be available if you manually check for updates."), localize('start', "Check for updates only on startup. Disable automatic background update checks."), localize('default', "Enable automatic update checks. Code will check for updates automatically and periodically.") - ], - policy: { - name: 'UpdateMode', - minimumVersion: '1.67', - } + ] }, 'update.channel': { type: 'string', From 986ef1c76d5c6a031c272c4172d87e6220011834 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 25 May 2022 15:43:11 +0200 Subject: [PATCH 059/270] Disable Terrapin for OSS builds (#150374) --- build/azure-pipelines/product-build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/product-build-pr.yml b/build/azure-pipelines/product-build-pr.yml index 5b09f8ee77e..3c45858413e 100644 --- a/build/azure-pipelines/product-build-pr.yml +++ b/build/azure-pipelines/product-build-pr.yml @@ -21,7 +21,7 @@ variables: - name: skipComponentGovernanceDetection value: true - name: ENABLE_TERRAPIN - value: true + value: false - name: VSCODE_PUBLISH value: false - name: VSCODE_QUALITY From 07655f3a23b9b168e7ed351a754fc77bd7faf3ff Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 25 May 2022 16:21:16 +0200 Subject: [PATCH 060/270] use remote cli when in remote terminal (#150372) --- build/gulpfile.vscode.js | 3 ++- resources/darwin/bin/code.sh | 9 +++++++++ resources/linux/bin/code.sh | 15 ++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 7dff1645210..268650959cd 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -288,6 +288,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); } else if (platform === 'darwin') { const shortcut = gulp.src('resources/darwin/bin/code.sh') + .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(rename('bin/code')); all = es.merge(all, shortcut); @@ -333,7 +334,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) .pipe(replace('@@PRODNAME@@', product.nameLong)) - .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(rename('bin/' + product.applicationName))); } diff --git a/resources/darwin/bin/code.sh b/resources/darwin/bin/code.sh index eecdf9c68b5..8c058727071 100755 --- a/resources/darwin/bin/code.sh +++ b/resources/darwin/bin/code.sh @@ -3,6 +3,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. +# when run in remote terminal, use the remote cli +if [ -n "$VSCODE_IPC_HOOK_CLI" ]; then + REMOTE_CLI="$(which -a '@@APPNAME@@' | grep /remote-cli/)" + if [ -n "$REMOTE_CLI" ]; then + "$REMOTE_CLI" "$@" + exit $? + fi +fi + function app_realpath() { SOURCE=$1 while [ -h "$SOURCE" ]; do diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index bfebec1aa8e..5fe68cb4f3e 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -3,9 +3,18 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. +# when run in remote terminal, use the remote cli +if [ -n "$VSCODE_IPC_HOOK_CLI" ]; then + REMOTE_CLI="$(which -a '@@APPNAME@@' | grep /remote-cli/)" + if [ -n "$REMOTE_CLI" ]; then + "$REMOTE_CLI" "$@" + exit $? + fi +fi + # test that VSCode wasn't installed inside WSL if grep -qi Microsoft /proc/version && [ -z "$DONT_PROMPT_WSL_INSTALL" ]; then - echo "To use @@PRODNAME@@ with the Windows Subsystem for Linux, please install @@PRODNAME@@ in Windows and uninstall the Linux version in WSL. You can then use the \`@@NAME@@\` command in a WSL terminal just as you would in a normal command prompt." 1>&2 + echo "To use @@PRODNAME@@ with the Windows Subsystem for Linux, please install @@PRODNAME@@ in Windows and uninstall the Linux version in WSL. You can then use the \`@@APPNAME@@\` command in a WSL terminal just as you would in a normal command prompt." 1>&2 printf "Do you want to continue anyway? [y/N] " 1>&2 read -r YN YN=$(printf '%s' "$YN" | tr '[:upper:]' '[:lower:]') @@ -44,11 +53,11 @@ else VSCODE_PATH="$(dirname "$(readlink -f "$0")")/.." else # else use the standard install location - VSCODE_PATH="/usr/share/@@NAME@@" + VSCODE_PATH="/usr/share/@@APPNAME@@" fi fi -ELECTRON="$VSCODE_PATH/@@NAME@@" +ELECTRON="$VSCODE_PATH/@@APPNAME@@" CLI="$VSCODE_PATH/resources/app/out/cli.js" ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --ms-enable-electron-run-as-node "$@" exit $? From 38931b6a3db467bf1c5ba51f053ddedbeeed6acd Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 25 May 2022 17:08:15 +0200 Subject: [PATCH 061/270] add `git.experimental.mergeEditor` setting to enable/disable merge editor for conflicting files --- extensions/git/package.json | 6 ++++++ extensions/git/package.nls.json | 1 + extensions/git/src/repository.ts | 37 +++++++++++++++++++++----------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index bb077b6f23d..b5ad659a010 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2445,6 +2445,12 @@ ], "markdownDescription": "%config.logLevel%", "scope": "window" + }, + "git.experimental.mergeEditor": { + "type": "boolean", + "default": false, + "markdownDescription": "%config.experimental.mergeEditor%", + "scope": "window" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index e43df92114f..e1a8c6a9faf 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -236,6 +236,7 @@ "config.logLevel.error": "Log only error, and critical information", "config.logLevel.critical": "Log only critical information", "config.logLevel.off": "Log nothing", + "config.experimental.mergeEditor": "Open the _experimental_ merge editor for files that are currently under conflict.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ebe1105ab94..61548892e7c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -296,6 +296,10 @@ export class Resource implements SourceControlResourceState { const command = this._commandResolver.resolveChangeCommand(this); await commands.executeCommand(command.command, ...(command.arguments || [])); } + + clone() { + return new Resource(this._commandResolver, this._resourceGroupType, this._resourceUri, this._type, this._useIcons, this._renameResourceUri); + } } export const enum Operation { @@ -602,18 +606,21 @@ class ResourceCommandResolver { resolveChangeCommand(resource: Resource): Command { const title = this.getTitle(resource); - if (!resource.leftUri && resource.rightUri && resource.type === Status.BOTH_MODIFIED) { - return { - command: '_git.openMergeEditor', - title: localize('open.merge', "Open Merge"), - arguments: [resource.rightUri] - }; - } else if (!resource.leftUri) { - return { - command: 'vscode.open', - title: localize('open', "Open"), - arguments: [resource.rightUri, { override: resource.type === Status.BOTH_MODIFIED ? false : undefined }, title] - }; + if (!resource.leftUri) { + const bothModified = resource.type === Status.BOTH_MODIFIED; + if (resource.rightUri && bothModified && workspace.getConfiguration('git').get('experimental.mergeEditor', false)) { + return { + command: '_git.openMergeEditor', + title: localize('open.merge', "Open Merge"), + arguments: [resource.rightUri] + }; + } else { + return { + command: 'vscode.open', + title: localize('open', "Open"), + arguments: [resource.rightUri, { override: bothModified ? false : undefined }, title] + }; + } } else { return { command: 'vscode.diff', @@ -918,6 +925,12 @@ export class Repository implements Disposable { onConfigListener(updateIndexGroupVisibility, this, this.disposables); updateIndexGroupVisibility(); + workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('git.experimental.mergeEditor')) { + this.mergeGroup.resourceStates = this.mergeGroup.resourceStates.map(r => r.clone()); + } + }, undefined, this.disposables); + filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchSortOrder', root) || e.affectsConfiguration('git.untrackedChanges', root) From 97f8e66d74b7b2d9cefbcb8db8589aa55c0e9cb9 Mon Sep 17 00:00:00 2001 From: Jonas Dellinger Date: Wed, 25 May 2022 17:16:10 +0200 Subject: [PATCH 062/270] A full editor can be used as git commit message editor (#95266) Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- extensions/git/extension.webpack.config.js | 3 +- extensions/git/package.json | 62 +++++++++++++----- extensions/git/package.nls.json | 2 + extensions/git/src/api/git.d.ts | 3 + extensions/git/src/askpass.ts | 14 +--- extensions/git/src/commands.ts | 39 +++++++++-- extensions/git/src/git.ts | 34 ++++++++-- extensions/git/src/gitEditor/gitEditor.ts | 64 +++++++++++++++++++ extensions/git/src/gitEditor/main.ts | 21 ++++++ .../gitEditor/scripts/git-editor-empty.bat | 1 + .../src/gitEditor/scripts/git-editor-empty.sh | 1 + .../git/src/gitEditor/scripts/git-editor.bat | 4 ++ .../git/src/gitEditor/scripts/git-editor.sh | 4 ++ extensions/git/src/main.ts | 17 ++++- extensions/git/src/repository.ts | 7 ++ extensions/git/tsconfig.json | 2 + src/vs/workbench/contrib/scm/browser/util.ts | 2 +- 17 files changed, 235 insertions(+), 45 deletions(-) create mode 100644 extensions/git/src/gitEditor/gitEditor.ts create mode 100644 extensions/git/src/gitEditor/main.ts create mode 100644 extensions/git/src/gitEditor/scripts/git-editor-empty.bat create mode 100755 extensions/git/src/gitEditor/scripts/git-editor-empty.sh create mode 100644 extensions/git/src/gitEditor/scripts/git-editor.bat create mode 100755 extensions/git/src/gitEditor/scripts/git-editor.sh diff --git a/extensions/git/extension.webpack.config.js b/extensions/git/extension.webpack.config.js index 5efa2052e88..ee6203af47e 100644 --- a/extensions/git/extension.webpack.config.js +++ b/extensions/git/extension.webpack.config.js @@ -13,6 +13,7 @@ module.exports = withDefaults({ context: __dirname, entry: { main: './src/main.ts', - ['askpass-main']: './src/askpass-main.ts' + ['askpass-main']: './src/askpass-main.ts', + ['git-editor-main']: './src/gitEditor/main.ts' } }); diff --git a/extensions/git/package.json b/extensions/git/package.json index de341a095ee..1c6ca2f4aff 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -12,7 +12,9 @@ "enabledApiProposals": [ "diffCommand", "contribViewsWelcome", + "resolvers", "scmActionButton", + "scmInput", "scmSelectedProvider", "scmValidation", "timeline" @@ -211,83 +213,99 @@ "command": "git.commit", "title": "%command.commit%", "category": "Git", - "icon": "$(check)" + "icon": "$(check)", + "enablement": "!commitInProgress" }, { "command": "git.commitStaged", "title": "%command.commitStaged%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitEmpty", "title": "%command.commitEmpty%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitStagedSigned", "title": "%command.commitStagedSigned%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitStagedAmend", "title": "%command.commitStagedAmend%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitAll", "title": "%command.commitAll%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitAllSigned", "title": "%command.commitAllSigned%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitAllAmend", "title": "%command.commitAllAmend%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitNoVerify", "title": "%command.commitNoVerify%", "category": "Git", - "icon": "$(check)" + "icon": "$(check)", + "enablement": "!commitInProgress" }, { "command": "git.commitStagedNoVerify", "title": "%command.commitStagedNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitEmptyNoVerify", "title": "%command.commitEmptyNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitStagedSignedNoVerify", "title": "%command.commitStagedSignedNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitStagedAmendNoVerify", "title": "%command.commitStagedAmendNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitAllNoVerify", "title": "%command.commitAllNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitAllSignedNoVerify", "title": "%command.commitAllSignedNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.commitAllAmendNoVerify", "title": "%command.commitAllAmendNoVerify%", - "category": "Git" + "category": "Git", + "enablement": "!commitInProgress" }, { "command": "git.restoreCommitTemplate", @@ -1986,6 +2004,18 @@ "scope": "machine", "description": "%config.defaultCloneDirectory%" }, + "git.useEditorAsCommitInput": { + "type": "boolean", + "scope": "resource", + "description": "%config.useEditorAsCommitInput%", + "default": false + }, + "git.verboseCommit": { + "type": "boolean", + "scope": "resource", + "markdownDescription": "%config.verboseCommit%", + "default": false + }, "git.enableSmartCommit": { "type": "boolean", "scope": "resource", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index ec691130508..d70493951b6 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -135,6 +135,8 @@ "config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.", "config.ignoreRebaseWarning": "Ignores the warning when it looks like the branch might have been rebased when pulling.", "config.defaultCloneDirectory": "The default location to clone a git repository.", + "config.useEditorAsCommitInput": "Use an editor to author the commit message.", + "config.verboseCommit": "Enable verbose output when `#git.useEditorAsCommitInput#` is enabled.", "config.enableSmartCommit": "Commit all changes when there are no staged changes.", "config.smartCommitChanges": "Control which changes are automatically staged by Smart Commit.", "config.smartCommitChanges.all": "Automatically stage all changes.", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 7ba58a0e607..e552f03f25d 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -137,6 +137,8 @@ export interface CommitOptions { empty?: boolean; noVerify?: boolean; requireUserConfig?: boolean; + useEditor?: boolean; + verbose?: boolean; } export interface FetchOptions { @@ -336,4 +338,5 @@ export const enum GitErrorCodes { PatchDoesNotApply = 'PatchDoesNotApply', NoPathFound = 'NoPathFound', UnknownPath = 'UnknownPath', + EmptyCommitMessage = 'EmptyCommitMessage' } diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index 81895a0e0d6..ffbd7e48a0e 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -6,9 +6,8 @@ import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode'; import { IDisposable, EmptyDisposable, toDisposable } from './util'; import * as path from 'path'; -import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer'; +import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; import { CredentialsProvider, Credentials } from './api/git'; -import { OutputChannelLogger } from './log'; export class Askpass implements IIPCHandler { @@ -16,16 +15,7 @@ export class Askpass implements IIPCHandler { private cache = new Map(); private credentialsProviders = new Set(); - static async create(outputChannelLogger: OutputChannelLogger, context?: string): Promise { - try { - return new Askpass(await createIPCServer(context)); - } catch (err) { - outputChannelLogger.logError(`Failed to create git askpass IPC: ${err}`); - return new Askpass(); - } - } - - private constructor(private ipc?: IIPCServer) { + constructor(private ipc?: IIPCServer) { if (ipc) { this.disposable = ipc.registerHandler('askpass', this); } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 6852639fcda..3597d62a52b 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1443,6 +1443,14 @@ export class CommandCenter { opts.signoff = true; } + if (config.get('useEditorAsCommitInput')) { + opts.useEditor = true; + + if (config.get('verboseCommit')) { + opts.verbose = true; + } + } + const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges'); if ( @@ -1490,7 +1498,7 @@ export class CommandCenter { let message = await getCommitMessage(); - if (!message && !opts.amend) { + if (!message && !opts.amend && !opts.useEditor) { return false; } @@ -1550,10 +1558,13 @@ export class CommandCenter { private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise { const message = repository.inputBox.value; + const root = Uri.file(repository.root); + const config = workspace.getConfiguration('git', root); + const getCommitMessage = async () => { let _message: string | undefined = message; - if (!_message) { + if (!_message && !config.get('useEditorAsCommitInput')) { let value: string | undefined = undefined; if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) { @@ -2937,7 +2948,7 @@ export class CommandCenter { }; let message: string; - let type: 'error' | 'warning' = 'error'; + let type: 'error' | 'warning' | 'information' = 'error'; const choices = new Map void>(); const openOutputChannelChoice = localize('open git log', "Open Git Log"); @@ -3000,6 +3011,12 @@ export class CommandCenter { message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git."); choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git'))); break; + case GitErrorCodes.EmptyCommitMessage: + message = localize('empty commit', "Commit operation was cancelled due to empty commit message."); + choices.clear(); + type = 'information'; + options.modal = false; + break; default: { const hint = (err.stderr || err.message || String(err)) .replace(/^error: /mi, '') @@ -3021,10 +3038,20 @@ export class CommandCenter { return; } + let result: string | undefined; const allChoices = Array.from(choices.keys()); - const result = type === 'error' - ? await window.showErrorMessage(message, options, ...allChoices) - : await window.showWarningMessage(message, options, ...allChoices); + + switch (type) { + case 'error': + result = await window.showErrorMessage(message, options, ...allChoices); + break; + case 'warning': + result = await window.showWarningMessage(message, options, ...allChoices); + break; + case 'information': + result = await window.showInformationMessage(message, options, ...allChoices); + break; + } if (result) { const resultFn = choices.get(result); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 8fdab7f659b..21fa0f0bf8e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1397,20 +1397,37 @@ export class Repository { } async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise { - const args = ['commit', '--quiet', '--allow-empty-message']; + const args = ['commit', '--quiet']; + const options: SpawnOptions = {}; + + if (message) { + options.input = message; + args.push('--file', '-'); + } + + if (opts.verbose) { + args.push('--verbose'); + } if (opts.all) { args.push('--all'); } - if (opts.amend && message) { + if (opts.amend) { args.push('--amend'); } - if (opts.amend && !message) { - args.push('--amend', '--no-edit'); - } else { - args.push('--file', '-'); + if (!opts.useEditor) { + if (!message) { + if (opts.amend) { + args.push('--no-edit'); + } else { + options.input = ''; + args.push('--file', '-'); + } + } + + args.push('--allow-empty-message'); } if (opts.signoff) { @@ -1435,7 +1452,7 @@ export class Repository { } try { - await this.exec(args, !opts.amend || message ? { input: message || '' } : {}); + await this.exec(args, options); } catch (commitErr) { await this.handleCommitError(commitErr); } @@ -1459,6 +1476,9 @@ export class Repository { if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) { commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges; throw commitErr; + } else if (/Aborting commit due to empty commit message/.test(commitErr.stderr || '')) { + commitErr.gitErrorCode = GitErrorCodes.EmptyCommitMessage; + throw commitErr; } try { diff --git a/extensions/git/src/gitEditor/gitEditor.ts b/extensions/git/src/gitEditor/gitEditor.ts new file mode 100644 index 00000000000..34f8ee9f20f --- /dev/null +++ b/extensions/git/src/gitEditor/gitEditor.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import { TabInputText, Uri, window, workspace } from 'vscode'; +import { IIPCHandler, IIPCServer } from '../ipc/ipcServer'; +import { EmptyDisposable, IDisposable } from '../util'; + +interface GitEditorRequest { + commitMessagePath?: string; +} + +export class GitEditor implements IIPCHandler { + + private disposable: IDisposable = EmptyDisposable; + + constructor(private ipc?: IIPCServer) { + if (ipc) { + this.disposable = ipc.registerHandler('git-editor', this); + } + } + + async handle({ commitMessagePath }: GitEditorRequest): Promise { + if (commitMessagePath) { + const uri = Uri.file(commitMessagePath); + const doc = await workspace.openTextDocument(uri); + await window.showTextDocument(doc, { preview: false }); + + return new Promise((c) => { + const onDidClose = window.tabGroups.onDidChangeTabs(async (tabs) => { + if (tabs.closed.some(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString())) { + onDidClose.dispose(); + return c(true); + } + }); + }); + } + } + + getEnv(): { [key: string]: string } { + if (!this.ipc) { + const fileType = process.platform === 'win32' ? 'bat' : 'sh'; + const gitEditor = path.join(__dirname, `scripts/git-editor-empty.${fileType}`); + + return { + GIT_EDITOR: `'${gitEditor}'` + }; + } + + const fileType = process.platform === 'win32' ? 'bat' : 'sh'; + const gitEditor = path.join(__dirname, `scripts/git-editor.${fileType}`); + + return { + GIT_EDITOR: `'${gitEditor}'`, + VSCODE_GIT_EDITOR_NODE: process.execPath, + VSCODE_GIT_EDITOR_MAIN: path.join(__dirname, 'main.js') + }; + } + + dispose(): void { + this.disposable.dispose(); + } +} diff --git a/extensions/git/src/gitEditor/main.ts b/extensions/git/src/gitEditor/main.ts new file mode 100644 index 00000000000..661540e0030 --- /dev/null +++ b/extensions/git/src/gitEditor/main.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IPCClient } from '../ipc/ipcClient'; + +function fatal(err: any): void { + console.error(err); + process.exit(1); +} + +function main(argv: string[]): void { + const ipcClient = new IPCClient('git-editor'); + const commitMessagePath = argv[2]; + + ipcClient.call({ commitMessagePath }).then(() => { + setTimeout(() => process.exit(0), 0); + }).catch(err => fatal(err)); +} + +main(process.argv); diff --git a/extensions/git/src/gitEditor/scripts/git-editor-empty.bat b/extensions/git/src/gitEditor/scripts/git-editor-empty.bat new file mode 100644 index 00000000000..56eeb8a5801 --- /dev/null +++ b/extensions/git/src/gitEditor/scripts/git-editor-empty.bat @@ -0,0 +1 @@ +@ECHO off diff --git a/extensions/git/src/gitEditor/scripts/git-editor-empty.sh b/extensions/git/src/gitEditor/scripts/git-editor-empty.sh new file mode 100755 index 00000000000..1a2485251c3 --- /dev/null +++ b/extensions/git/src/gitEditor/scripts/git-editor-empty.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/extensions/git/src/gitEditor/scripts/git-editor.bat b/extensions/git/src/gitEditor/scripts/git-editor.bat new file mode 100644 index 00000000000..ce28807cae1 --- /dev/null +++ b/extensions/git/src/gitEditor/scripts/git-editor.bat @@ -0,0 +1,4 @@ +@ECHO off + +set ELECTRON_RUN_AS_NODE=1 +"%VSCODE_GIT_EDITOR_NODE%" "%VSCODE_GIT_EDITOR_MAIN%" %* diff --git a/extensions/git/src/gitEditor/scripts/git-editor.sh b/extensions/git/src/gitEditor/scripts/git-editor.sh new file mode 100755 index 00000000000..f4ba2e4fd73 --- /dev/null +++ b/extensions/git/src/gitEditor/scripts/git-editor.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +ELECTRON_RUN_AS_NODE="1" \ +"$VSCODE_GIT_EDITOR_NODE" "$VSCODE_GIT_EDITOR_MAIN" $@ diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index ee60d2cb1b1..e5d99158a4e 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -25,6 +25,8 @@ import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; import { TerminalEnvironmentManager } from './terminal'; import { OutputChannelLogger } from './log'; +import { createIPCServer, IIPCServer } from './ipc/ipcServer'; +import { GitEditor } from './gitEditor/gitEditor'; const deactivateTasks: { (): Promise }[] = []; @@ -60,10 +62,21 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu return !skip; }); - const askpass = await Askpass.create(outputChannelLogger, context.storagePath); + let ipc: IIPCServer | undefined = undefined; + + try { + ipc = await createIPCServer(context.storagePath); + } catch (err) { + outputChannelLogger.logError(`Failed to create git IPC: ${err}`); + } + + const askpass = new Askpass(ipc); disposables.push(askpass); - const environment = askpass.getEnv(); + const gitEditor = new GitEditor(ipc); + disposables.push(gitEditor); + + const environment = { ...askpass.getEnv(), ...gitEditor.getEnv() }; const terminalEnvironmentManager = new TerminalEnvironmentManager(context, environment); disposables.push(terminalEnvironmentManager); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index c337f91a7f5..b5ed89ac94b 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -450,6 +450,13 @@ class ProgressManager { const onDidChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git', Uri.file(this.repository.root))); onDidChange(_ => this.updateEnablement()); this.updateEnablement(); + + this.repository.onDidChangeOperations(() => { + const commitInProgress = this.repository.operations.isRunning(Operation.Commit); + + this.repository.sourceControl.inputBox.enabled = !commitInProgress; + commands.executeCommand('setContext', 'commitInProgress', commitInProgress); + }); } private updateEnablement(): void { diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 13997275056..899f8c9c33d 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -11,7 +11,9 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", + "../../src/vscode-dts/vscode.proposed.resolvers.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", + "../../src/vscode-dts/vscode.proposed.scmInput.d.ts", "../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts", "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", "../../src/vscode-dts/vscode.proposed.tabs.d.ts", diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 80d5d19302d..c8f4524d017 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -37,7 +37,7 @@ export function isSCMResource(element: any): element is ISCMResource { return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup); } -const compareActions = (a: IAction, b: IAction) => a.id === b.id; +const compareActions = (a: IAction, b: IAction) => a.id === b.id && a.enabled === b.enabled; export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], secondary: IAction[]) => void, primaryGroup?: string): IDisposable { let cachedDisposable: IDisposable = Disposable.None; From ce01702bbec4419cfb76ab10d2b04ce09d3f6600 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 25 May 2022 17:43:02 +0200 Subject: [PATCH 063/270] make git "accept from merge editor" command save the document and close the merge editor (uses heuristic to identify merge editor tab) --- extensions/git/src/commands.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index da619d4feb5..2805dcab768 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as path from 'path'; import * as picomatch from 'picomatch'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; @@ -1082,15 +1082,30 @@ export class CommandCenter { @command('git.acceptMerge') async acceptMerge(uri: Uri | unknown): Promise { - // TODO@jrieken make this proper, needs help from SCM folks if (!(uri instanceof Uri)) { return; } const repository = this.model.getRepository(uri); if (!repository) { + console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't belong to any repository`); return; } + + const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); + if (!doc) { + console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't match a document`); + return; + } + + await doc.save(); await repository.add([uri]); + + // TODO@jrieken there isn't a `TabInputTextMerge` instance yet, till now the merge editor + // uses the `TabInputText` for the out-resource and we use that to identify and CLOSE the tab + const { activeTab } = window.tabGroups.activeTabGroup; + if (activeTab && activeTab?.input instanceof TabInputText && activeTab.input.uri.toString() === uri.toString()) { + await window.tabGroups.close(activeTab, true); + } } private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise { From cdeaf55221397893f30ea52473d6f057476f9586 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 25 May 2022 17:55:57 +0200 Subject: [PATCH 064/270] Implements synchronized scrolling. --- .../contrib/audioCues/browser/observable.ts | 10 ++ .../mergeEditor/browser/mergeEditor.ts | 97 +++++++++++++++++-- .../mergeEditor/browser/mergeEditorModel.ts | 6 +- .../contrib/mergeEditor/browser/model.ts | 83 ++++++++++++++++ 4 files changed, 187 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/audioCues/browser/observable.ts b/src/vs/workbench/contrib/audioCues/browser/observable.ts index 937f4b2f9cf..c3974606057 100644 --- a/src/vs/workbench/contrib/audioCues/browser/observable.ts +++ b/src/vs/workbench/contrib/audioCues/browser/observable.ts @@ -612,3 +612,13 @@ export function wasEventTriggeredRecently(event: Event, timeoutMs: number, return observable; } + +/** + * This ensures the observable is kept up-to-date. + * This is useful when the observables `get` method is used. +*/ +export function keepAlive(observable: IObservable): IDisposable { + return autorun(reader => { + observable.read(reader); + }, 'keep-alive'); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 0f0ef7c4cba..184d0812770 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -9,6 +9,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IAction } from 'vs/base/common/actions'; +import { findLast } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; @@ -40,9 +41,10 @@ import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { autorun, derivedObservable, IObservable, ITransaction, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { autorun, derivedObservable, IObservable, ITransaction, keepAlive, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel'; +import { LineRange, ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model'; import { applyObservableDecorations, n, ReentrancyBarrier, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { EditorGutter, IGutterItemInfo, IGutterItemView } from './editorGutter'; @@ -82,27 +84,67 @@ export class MergeEditor extends EditorPane { const reentrancyBarrier = new ReentrancyBarrier(); + const input1ResultMapping = derivedObservable('input1ResultMapping', reader => { + const model = this.input1View.model.read(reader); + if (!model) { + return undefined; + } + const resultDiffs = model.resultDiffs.read(reader); + const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input1, model.input1LinesDiffs, model.result, resultDiffs); + return modifiedBaseRanges; + }); + const input2ResultMapping = derivedObservable('input2ResultMapping', reader => { + const model = this.input2View.model.read(reader); + if (!model) { + return undefined; + } + const resultDiffs = model.resultDiffs.read(reader); + const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input2, model.input2LinesDiffs, model.result, resultDiffs); + return modifiedBaseRanges; + }); + + this._register(keepAlive(input1ResultMapping)); + this._register(keepAlive(input2ResultMapping)); + this._store.add(this.input1View.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { + const mapping = input1ResultMapping.get(); + if (!mapping) { + return; + } + synchronizeScrolling(this.input1View.editor, this.inputResultView.editor, mapping, 1); this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - this.inputResultView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); }); } })); + this._store.add(this.input2View.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { + const mapping = input2ResultMapping.get(); + if (!mapping) { + return; + } + synchronizeScrolling(this.input2View.editor, this.inputResultView.editor, mapping, 1); this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - this.inputResultView.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); }); } })); this._store.add(this.inputResultView.editor.onDidScrollChange(c => { if (c.scrollTopChanged) { reentrancyBarrier.runExclusively(() => { - this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + const mapping = input1ResultMapping.get(); + if (!mapping) { + return; + } + synchronizeScrolling(this.inputResultView.editor, this.input1View.editor, mapping, 2); + + const mapping2 = input2ResultMapping.get(); + if (!mapping2) { + return; + } + synchronizeScrolling(this.inputResultView.editor, this.input2View.editor, mapping2, 2); }); } })); @@ -265,6 +307,49 @@ export class MergeEditor extends EditorPane { } } +function flip(value: 1 | 2): 1 | 2 { + return value === 1 ? 2 : 1; +} + +function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: ModifiedBaseRange[], sourceNumber: 1 | 2) { + const visibleRanges = scrollingEditor.getVisibleRanges(); + if (visibleRanges.length === 0) { + return; + } + const topLineNumber = visibleRanges[0].startLineNumber - 1; + + const firstBefore = findLast(mapping, r => r.getInputRange(sourceNumber).startLineNumber <= topLineNumber); + let sourceRange: LineRange; + let targetRange: LineRange; + + const targetNumber = flip(sourceNumber); + + if (firstBefore && firstBefore.getInputRange(sourceNumber).contains(topLineNumber)) { + sourceRange = firstBefore.getInputRange(sourceNumber); + targetRange = firstBefore.getInputRange(targetNumber); + } else if (firstBefore && firstBefore.getInputRange(sourceNumber).isEmpty && firstBefore.getInputRange(sourceNumber).startLineNumber === topLineNumber) { + sourceRange = firstBefore.getInputRange(sourceNumber).deltaEnd(1); + targetRange = firstBefore.getInputRange(targetNumber).deltaEnd(1); + } else { + const delta = firstBefore ? firstBefore.getInputRange(targetNumber).endLineNumberExclusive - firstBefore.getInputRange(sourceNumber).endLineNumberExclusive : 0; + sourceRange = new LineRange(topLineNumber, 1); + targetRange = new LineRange(topLineNumber + delta, 1); + } + + // sourceRange is not empty! + + const resultStartTopPx = targetEditor.getTopForLineNumber(targetRange.startLineNumber); + const resultEndPx = targetEditor.getTopForLineNumber(targetRange.endLineNumberExclusive); + + const sourceStartTopPx = scrollingEditor.getTopForLineNumber(sourceRange.startLineNumber); + const sourceEndPx = scrollingEditor.getTopForLineNumber(sourceRange.endLineNumberExclusive); + + const factor = (scrollingEditor.getScrollTop() - sourceStartTopPx) / (sourceEndPx - sourceStartTopPx); + const resultScrollPosition = resultStartTopPx + (resultEndPx - resultStartTopPx) * factor; + + targetEditor.setScrollTop(resultScrollPosition, ScrollType.Immediate); +} + interface ICodeEditorViewOptions { readonly: boolean; } @@ -272,7 +357,7 @@ interface ICodeEditorViewOptions { abstract class CodeEditorView extends Disposable { private readonly _model = new ObservableValue(undefined, 'model'); - protected readonly model: IObservable = this._model; + readonly model: IObservable = this._model; protected readonly htmlElements = n('div.code-view', [ n('div.title', { $: 'title' }), diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 024aa737d42..2c23f66b502 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -101,8 +101,8 @@ export class MergeEditorModel extends EditorModel { readonly input2Detail: string | undefined, readonly input2Description: string | undefined, readonly result: ITextModel, - private readonly input1LinesDiffs: readonly LineDiff[], - private readonly input2LinesDiffs: readonly LineDiff[], + public readonly input1LinesDiffs: readonly LineDiff[], + public readonly input2LinesDiffs: readonly LineDiff[], resultDiffs: LineDiff[], private readonly editorWorkerService: IEditorWorkerService ) { @@ -164,7 +164,7 @@ export class MergeEditorModel extends EditorModel { this.input2LinesDiffs ); - private readonly modifiedBaseRangeStateStores = new Map>( + private readonly modifiedBaseRangeStateStores: ReadonlyMap> = new Map( this.modifiedBaseRanges.map(s => ([s, new ObservableValue(ModifiedBaseRangeState.default, 'State')])) ); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model.ts b/src/vs/workbench/contrib/mergeEditor/browser/model.ts index 9a045854182..9ee3176fc92 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model.ts @@ -102,6 +102,14 @@ export class LineRange { public equals(originalRange: LineRange) { return this.startLineNumber === originalRange.startLineNumber && this.lineCount === originalRange.lineCount; } + + public contains(lineNumber: number): boolean { + return this.startLineNumber <= lineNumber && lineNumber < this.endLineNumberExclusive; + } + + public deltaEnd(delta: number): LineRange { + return new LineRange(this.startLineNumber, this.lineCount + delta); + } } export class LineDiff { @@ -263,6 +271,9 @@ export class ModifiedBaseRange { const result = new Array(); function pushAndReset() { + if (currentDiffs[0].length === 0 && currentDiffs[1].length === 0) { + return; + } result.push(new ModifiedBaseRange( baseTextModel, input1TextModel, @@ -416,3 +427,75 @@ export class ModifiedBaseRangeState { return arr.join(','); } } + +/* +export class LineMappings { + public static fromDiffs( + diffs1: readonly LineDiff[], + diffs2: readonly LineDiff[], + inputLineCount: number, + ): LineMappings { + const compareByStartLineNumber = compareBy( + (d) => d.originalRange.startLineNumber, + numberComparator + ); + + const diffs = diffs1 + .map((diff) => ({ source: 0 as 0 | 1, diff })) + .concat(diffs2.map((diff) => ({ source: 1 as const, diff }))); + + diffs.sort(compareBy(d => d.diff, compareByStartLineNumber)); + + const currentDiffs = [ + new Array(), + new Array(), + ]; + let deltaFromBaseToInput = [0, 0]; + + const result = new Array(); + + function pushAndReset() { + result.push(LineMapping.create( + baseTextModel, + input1TextModel, + currentDiffs[0], + deltaFromBaseToInput[0], + input2TextModel, + currentDiffs[1], + deltaFromBaseToInput[1], + )); + currentDiffs[0] = []; + currentDiffs[1] = []; + } + + let currentRange: LineRange | undefined; + + for (const diff of diffs) { + const range = diff.diff.originalRange; + if (currentRange && !currentRange.touches(range)) { + pushAndReset(); + } + deltaFromBaseToInput[diff.source] = diff.diff.resultingDeltaFromOriginalToModified; + currentRange = currentRange ? currentRange.join(range) : range; + currentDiffs[diff.source].push(diff.diff); + } + pushAndReset(); + + return result; + } + + constructor(private readonly lineMappings: LineMapping[]) {} +} + +// A lightweight ModifiedBaseRange. Maybe they can be united? +export class LineMapping { + public static create(input: LineDiff, ): LineMapping { + + } + + constructor( + public readonly inputRange: LineRange, + public readonly resultRange: LineRange + ) { } +} +*/ From 63121f8039cb8dd6485655488a10252cdcaa64e5 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 25 May 2022 18:02:31 +0200 Subject: [PATCH 065/270] Code polishing --- .../workbench/contrib/mergeEditor/browser/icons.ts | 10 ---------- .../mergeEditor/browser/media/mergeEditor.css | 2 +- .../contrib/mergeEditor/browser/mergeEditor.ts | 12 +++++++----- 3 files changed, 8 insertions(+), 16 deletions(-) delete mode 100644 src/vs/workbench/contrib/mergeEditor/browser/icons.ts diff --git a/src/vs/workbench/contrib/mergeEditor/browser/icons.ts b/src/vs/workbench/contrib/mergeEditor/browser/icons.ts deleted file mode 100644 index a37f0e66faf..00000000000 --- a/src/vs/workbench/contrib/mergeEditor/browser/icons.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from 'vs/base/common/codicons'; -import { localize } from 'vs/nls'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export const acceptConflictIcon = registerIcon('merge-accept-conflict-icon', Codicon.check, localize('acceptConflictIcon', 'TODO.')); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index abae99ac16c..363dffaaf6a 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -33,7 +33,7 @@ cursor: pointer; } -.merge-accept-foo { +.merge-base-range-projection { background-color: rgb(170 175 170 / 15%); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 184d0812770..8c95edcc1fa 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -443,8 +443,8 @@ class InputCodeEditorView extends CodeEditorView { range: new Range(range.startLineNumber, 1, range.endLineNumberExclusive - 1, 1), options: { isWholeLine: true, - className: 'merge-accept-foo', - description: 'foo2' + className: 'merge-base-range-projection', + description: 'Base Range Projection' } }); } @@ -509,7 +509,8 @@ class MergeConflictGutterItemView extends Disposable implements IGutterItemView< target.classList.add('merge-accept-gutter-marker'); target.classList.add(item.range.lineCount > 1 ? 'multi-line' : 'single-line'); - const checkBox = new Toggle({ isChecked: false, title: 'TODO', icon: Codicon.check, actionClassName: 'monaco-checkbox' }); + // TODO: Tri-State-Toggle, localized title + const checkBox = new Toggle({ isChecked: false, title: 'Accept Merge', icon: Codicon.check, actionClassName: 'monaco-checkbox' }); checkBox.domNode.classList.add('accept-conflict-group'); this._register( @@ -550,8 +551,9 @@ class ResultCodeEditorView extends CodeEditorView { range: new Range(range.startLineNumber, 1, range.endLineNumberExclusive - 1, 1), options: { isWholeLine: true, - className: 'merge-accept-foo', - description: 'foo2' + // TODO + className: 'merge-base-range-projection', + description: 'Result Diff' } }); } From 4cf4438fc001e0c925905af51577cfc1db72f42c Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 25 May 2022 09:33:13 -0700 Subject: [PATCH 066/270] Ensure execution and kernel status visible when there are source commands contributed. --- .../browser/contrib/editorStatusBar/editorStatusBar.ts | 6 +++--- .../contrib/notebook/browser/controller/coreActions.ts | 4 ++-- .../notebook/browser/controller/executeActions.ts | 3 ++- .../browser/viewParts/notebookEditorWidgetContextKeys.ts | 9 ++++++++- .../contrib/notebook/common/notebookContextKeys.ts | 1 + 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 20c8c594f12..4de014d82ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -22,7 +22,7 @@ import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { CENTER_ACTIVE_CELL } from 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow'; import { NOTEBOOK_ACTIONS_CATEGORY, SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { getNotebookEditorFromEditorPane, INotebookEditor, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -48,7 +48,7 @@ registerAction2(class extends Action2 { id: MenuId.EditorTitle, when: ContextKeyExpr.and( NOTEBOOK_IS_ACTIVE_EDITOR, - ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), + ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_KERNEL_SOURCE_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) ), group: 'navigation', @@ -56,7 +56,7 @@ registerAction2(class extends Action2 { }, { id: MenuId.NotebookToolbar, when: ContextKeyExpr.and( - ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), + ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_KERNEL_SOURCE_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), ContextKeyExpr.equals('config.notebook.globalToolbar', true) ), group: 'status', diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 1c601fbf36f..f7b01782aa0 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, cellRangeToViewCells } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorCommandsContext } from 'vs/workbench/common/editor'; @@ -276,7 +276,7 @@ export abstract class NotebookCellAction extends abstract override runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } -export const executeNotebookCondition = ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0); +export const executeNotebookCondition = ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), ContextKeyExpr.greater(NOTEBOOK_KERNEL_SOURCE_COUNT.key, 0)); interface IMultiCellArgs { ranges: ICellRange[]; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 351b57c8bef..58227467a66 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -15,7 +15,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { EditorsOrder } from 'vs/workbench/common/editor'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { cellExecutionArgs, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, executeNotebookCondition, getContextFromActiveEditor, getContextFromUri, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookAction, NotebookCellAction, NotebookMultiCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -41,6 +41,7 @@ export const executeCondition = ContextKeyExpr.and( NOTEBOOK_CELL_TYPE.isEqualTo('code'), ContextKeyExpr.or( ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), + ContextKeyExpr.greater(NOTEBOOK_KERNEL_SOURCE_COUNT.key, 0), NOTEBOOK_MISSING_KERNEL_EXTENSION )); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index a90477db542..3702ac26261 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -6,7 +6,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICellViewModel, INotebookEditorDelegate, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NOTEBOOK_CELL_TOOLBAR_LOCATION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_TOOLBAR_LOCATION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL_SOURCE_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -15,6 +15,7 @@ export class NotebookEditorContextKeys { private readonly _notebookKernel: IContextKey; private readonly _notebookKernelCount: IContextKey; + private readonly _notebookKernelSourceCount: IContextKey; private readonly _notebookKernelSelected: IContextKey; private readonly _interruptibleKernel: IContextKey; private readonly _someCellRunning: IContextKey; @@ -44,6 +45,7 @@ export class NotebookEditorContextKeys { this._hasOutputs = NOTEBOOK_HAS_OUTPUTS.bindTo(contextKeyService); this._viewType = NOTEBOOK_VIEW_TYPE.bindTo(contextKeyService); this._missingKernelExtension = NOTEBOOK_MISSING_KERNEL_EXTENSION.bindTo(contextKeyService); + this._notebookKernelSourceCount = NOTEBOOK_KERNEL_SOURCE_COUNT.bindTo(contextKeyService); this._cellToolbarLocation = NOTEBOOK_CELL_TOOLBAR_LOCATION.bindTo(contextKeyService); this._handleDidChangeModel(); @@ -52,6 +54,7 @@ export class NotebookEditorContextKeys { this._disposables.add(_editor.onDidChangeModel(this._handleDidChangeModel, this)); this._disposables.add(_notebookKernelService.onDidAddKernel(this._updateKernelContext, this)); this._disposables.add(_notebookKernelService.onDidChangeSelectedNotebooks(this._updateKernelContext, this)); + this._disposables.add(_notebookKernelService.onDidChangeSourceActions(this._updateKernelContext, this)); this._disposables.add(_editor.notebookOptions.onDidChangeOptions(this._updateForNotebookOptions, this)); this._disposables.add(_extensionService.onDidChangeExtensions(this._updateForInstalledExtension, this)); this._disposables.add(_notebookExecutionStateService.onDidChangeCellExecution(this._updateForCellExecution, this)); @@ -61,6 +64,7 @@ export class NotebookEditorContextKeys { this._disposables.dispose(); this._viewModelDisposables.dispose(); this._notebookKernelCount.reset(); + this._notebookKernelSourceCount.reset(); this._interruptibleKernel.reset(); this._someCellRunning.reset(); this._viewType.reset(); @@ -142,12 +146,15 @@ export class NotebookEditorContextKeys { private _updateKernelContext(): void { if (!this._editor.hasModel()) { this._notebookKernelCount.reset(); + this._notebookKernelSourceCount.reset(); this._interruptibleKernel.reset(); return; } const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.textModel); + const sourceActions = this._notebookKernelService.getSourceActions(); this._notebookKernelCount.set(all.length); + this._notebookKernelSourceCount.set(sourceActions.length); this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); this._notebookKernelSelected.set(Boolean(selected)); this._notebookKernel.set(selected?.id ?? ''); diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 689f9ca995e..882a76d6092 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -43,6 +43,7 @@ export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellRes // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); export const NOTEBOOK_KERNEL_COUNT = new RawContextKey('notebookKernelCount', 0); +export const NOTEBOOK_KERNEL_SOURCE_COUNT = new RawContextKey('notebookKernelSourceCount', 0); export const NOTEBOOK_KERNEL_SELECTED = new RawContextKey('notebookKernelSelected', false); export const NOTEBOOK_INTERRUPTIBLE_KERNEL = new RawContextKey('notebookInterruptibleKernel', false); export const NOTEBOOK_MISSING_KERNEL_EXTENSION = new RawContextKey('notebookMissingKernelExtension', false); From 3837e55e31f1e44c514516873a4457c292607d58 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 25 May 2022 09:38:54 -0700 Subject: [PATCH 067/270] update bash status correctly (#150322) --- .../contrib/terminal/browser/media/shellIntegration-bash.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index ec96e7d3fd3..3c0cb9cfb87 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -96,6 +96,7 @@ if [[ -n "$__vsc_original_trap" ]]; then fi __vsc_preexec() { + __vsc_status="$?" eval ${__vsc_original_trap} PS1="$__vsc_prior_prompt" if [ -z "${__vsc_in_command_execution-}" ]; then @@ -110,7 +111,6 @@ __vsc_prompt_cmd_original() { if [[ ${IFS+set} ]]; then __vsc_original_ifs="$IFS" fi - __vsc_status="$?" if [[ "$__vsc_original_prompt_command" =~ .+\;.+ ]]; then IFS=';' else @@ -124,7 +124,7 @@ __vsc_prompt_cmd_original() { unset IFS fi for ((i = 0; i < ${#ADDR[@]}; i++)); do - # unset IFS + (exit ${__vsc_status}) builtin eval ${ADDR[i]} done __vsc_precmd From 3e8d674698b814e2d65bc206b9c19926655e4519 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 25 May 2022 11:09:59 -0700 Subject: [PATCH 068/270] Clicking outline in notebook should reveal cell near the top of the editor (#150336) Fixes #123828 --- .../contrib/outline/notebookOutline.ts | 8 ++++-- .../notebook/browser/notebookBrowser.ts | 7 +++++ .../notebook/browser/notebookEditorWidget.ts | 8 +++++- .../notebook/browser/view/notebookCellList.ts | 26 +++++++++++++++---- .../browser/view/notebookRenderingCommon.ts | 1 + 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 473ff42494f..da58d351ae4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -8,7 +8,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, IDisposable, Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IActiveNotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealType, IActiveNotebookEditor, ICellViewModel, INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; @@ -583,7 +583,11 @@ export class NotebookCellOutline extends Disposable implements IOutline { await this._editorService.openEditor({ resource: entry.cell.uri, - options: { ...options, override: this._editor.input?.editorId }, + options: { + ...options, + override: this._editor.input?.editorId, + cellRevealType: CellRevealType.NearTopIfOutsideViewport + } as INotebookEditorOptions, }, sideBySide ? SIDE_GROUP : undefined); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 75452af0bde..780ff8fc21a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -277,8 +277,15 @@ export interface INotebookDeltaCellStatusBarItems { items: INotebookCellStatusBarItem[]; } + +export enum CellRevealType { + NearTopIfOutsideViewport, + CenterIfOutsideViewport +} + export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellOptions?: ITextResourceEditorInput; + readonly cellRevealType?: CellRevealType; readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; readonly viewState?: INotebookEditorViewState; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 2f1bf44324c..51c50c5d6a3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -49,7 +49,7 @@ import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeg import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger'; @@ -1244,6 +1244,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const selection = cellOptions.options?.selection; if (selection) { await this.revealLineInCenterIfOutsideViewportAsync(cell, selection.startLineNumber); + } else if (options?.cellRevealType === CellRevealType.NearTopIfOutsideViewport) { + await this.revealNearTopIfOutsideViewportAync(cell); } else { await this.revealInCenterIfOutsideViewportAsync(cell); } @@ -2028,6 +2030,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._listViewInfoAccessor.revealInCenter(cell); } + revealNearTopIfOutsideViewportAync(cell: ICellViewModel) { + return this._listViewInfoAccessor.revealNearTopIfOutsideViewportAync(cell); + } + async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { return this._listViewInfoAccessor.revealLineInViewAsync(cell, line); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 0318aa6a288..c68b97e7df5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -39,7 +39,8 @@ const enum CellRevealType { const enum CellRevealPosition { Top, Center, - Bottom + Bottom, + NearTop } function getVisibleCells(cells: CellViewModel[], hiddenRanges: ICellRange[]) { @@ -861,7 +862,15 @@ export class NotebookCellList extends WorkbenchList implements ID const index = this._getViewIndexUpperBound(cell); if (index >= 0) { - return this._revealInCenterIfOutsideViewportAsync(index); + return this._revealIfOutsideViewportAsync(index, CellRevealPosition.Center); + } + } + + async revealNearTopIfOutsideViewportAync(cell: ICellViewModel): Promise { + const index = this._getViewIndexUpperBound(cell); + + if (index >= 0) { + return this._revealIfOutsideViewportAsync(index, CellRevealPosition.NearTop); } } @@ -1200,8 +1209,8 @@ export class NotebookCellList extends WorkbenchList implements ID } } - private async _revealInCenterIfOutsideViewportAsync(viewIndex: number): Promise { - this._revealInternal(viewIndex, true, CellRevealPosition.Center); + private async _revealIfOutsideViewportAsync(viewIndex: number, revealPosition: CellRevealPosition): Promise { + this._revealInternal(viewIndex, true, revealPosition); const element = this.view.element(viewIndex); // wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor) @@ -1249,6 +1258,7 @@ export class NotebookCellList extends WorkbenchList implements ID this.view.setScrollTop(this.view.elementTop(viewIndex)); break; case CellRevealPosition.Center: + case CellRevealPosition.NearTop: { // reveal the cell top in the viewport center initially this.view.setScrollTop(elementTop - this.view.renderHeight / 2); @@ -1259,8 +1269,10 @@ export class NotebookCellList extends WorkbenchList implements ID if (newElementHeight >= renderHeight) { // cell is larger than viewport, reveal top this.view.setScrollTop(newElementTop); - } else { + } else if (revealPosition === CellRevealPosition.Center) { this.view.setScrollTop(newElementTop + (newElementHeight / 2) - (renderHeight / 2)); + } else if (revealPosition === CellRevealPosition.NearTop) { + this.view.setScrollTop(newElementTop - (renderHeight / 5)); } } break; @@ -1497,6 +1509,10 @@ export class ListViewInfoAccessor extends Disposable { this.list.revealElementInCenter(cell); } + async revealNearTopIfOutsideViewportAync(cell: ICellViewModel) { + return this.list.revealNearTopIfOutsideViewportAync(cell); + } + async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { return this.list.revealElementLineInViewAsync(cell, line); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 0bdb4ff330f..ceabb8d6de9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -66,6 +66,7 @@ export interface INotebookCellList { revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; revealElementInCenter(element: ICellViewModel): void; revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise; + revealNearTopIfOutsideViewportAync(element: ICellViewModel): Promise; revealElementLineInViewAsync(element: ICellViewModel, line: number): Promise; revealElementLineInCenterAsync(element: ICellViewModel, line: number): Promise; revealElementLineInCenterIfOutsideViewportAsync(element: ICellViewModel, line: number): Promise; From 8eeed3dd6c140da47b326d99e5a0579465c4f62e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 25 May 2022 11:27:58 -0700 Subject: [PATCH 069/270] Fix bad layout of search input (#150405) Fix #150381 --- src/vs/workbench/contrib/search/browser/media/searchview.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index f9e5c40fa0d..f59811de9a2 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -47,8 +47,7 @@ .search-view .search-widget .monaco-inputbox > .ibwrapper > .mirror, .search-view .search-widget .monaco-inputbox > .ibwrapper > textarea.input { - padding: 3px; - padding-left: 4px; + padding: 3px 0px 3px 4px; } /* NOTE: height is also used in searchWidget.ts as a constant*/ From 149e2b7674ba0888cbb7cf8bbd70f45db7393fa5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 25 May 2022 20:51:08 +0200 Subject: [PATCH 070/270] Fix #149831 (#150276) --- extensions/git/src/repository.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e6bd193aa0c..c6fa51b5497 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1408,15 +1408,15 @@ export class Repository implements Disposable { } @throttle - async fetchAll(): Promise { - await this._fetch({ all: true }); + async fetchAll(cancellationToken?: CancellationToken): Promise { + await this._fetch({ all: true, cancellationToken }); } async fetch(options: FetchOptions): Promise { await this._fetch(options); } - private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean } = {}): Promise { + private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean; cancellationToken?: CancellationToken } = {}): Promise { if (!options.prune) { const config = workspace.getConfiguration('git', Uri.file(this.root)); const prune = config.get('pruneOnFetch'); @@ -1461,7 +1461,7 @@ export class Repository implements Disposable { // When fetchOnPull is enabled, fetch all branches when pulling if (fetchOnPull) { - await this.repository.fetch({ all: true }); + await this.fetchAll(); } if (await this.checkIfMaybeRebased(this.HEAD?.name)) { @@ -1532,7 +1532,7 @@ export class Repository implements Disposable { const fn = async (cancellationToken?: CancellationToken) => { // When fetchOnPull is enabled, fetch all branches when pulling if (fetchOnPull) { - await this.repository.fetch({ all: true, cancellationToken }); + await this.fetchAll(cancellationToken); } if (await this.checkIfMaybeRebased(this.HEAD?.name)) { From b118105bf28d773fbbce683f7230d058be2f89a7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 25 May 2022 12:05:19 -0700 Subject: [PATCH 071/270] Prevent work for really long links - We no longer get all xterm.js content for a full wrapped line but limit it to a certain context, this context depends on the type of link detector. - There is also a sanity check when calling the low level helper function just in case. Fixes #150401 --- .../contrib/terminal/browser/links/links.ts | 6 ++++++ .../browser/links/terminalExternalLinkDetector.ts | 11 +++-------- .../browser/links/terminalLinkDetectorAdapter.ts | 11 +++++++++-- .../terminal/browser/links/terminalLinkHelpers.ts | 4 ++++ .../browser/links/terminalLocalLinkDetector.ts | 6 ++++++ .../browser/links/terminalUriLinkDetector.ts | 13 +++++-------- .../browser/links/terminalWordLinkDetector.ts | 4 ++++ .../browser/links/terminalUriLinkDetector.test.ts | 14 +++++++------- 8 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/links/links.ts b/src/vs/workbench/contrib/terminal/browser/links/links.ts index 012b95f3214..a93a42c4b49 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/links.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/links.ts @@ -17,6 +17,12 @@ export interface ITerminalLinkDetector { */ readonly xterm: Terminal; + /** + * The maximum link length possible for this detector, this puts a cap on how much of a wrapped + * line to consider to prevent performance problems. + */ + readonly maxLinkLength: number; + /** * Detects links within the _wrapped_ line range provided and returns them as an array. * diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts index 9a633e8956a..521a7b2ee44 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts @@ -8,14 +8,9 @@ import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/cont import { ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IBufferLine, Terminal } from 'xterm'; -const enum Constants { - /** - * The max line length to try extract word links from. - */ - MaxLineLength = 2000 -} - export class TerminalExternalLinkDetector implements ITerminalLinkDetector { + readonly maxLinkLength = 2000; + constructor( readonly id: string, readonly xterm: Terminal, @@ -26,7 +21,7 @@ export class TerminalExternalLinkDetector implements ITerminalLinkDetector { async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise { // Get the text representation of the wrapped line const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols); - if (text === '' || text.length > Constants.MaxLineLength) { + if (text === '' || text.length > this.maxLinkLength) { return []; } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts index 868bf9600e8..43d8923d291 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts @@ -59,12 +59,19 @@ export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProv this._detector.xterm.buffer.active.getLine(startLine)! ]; - while (startLine >= 0 && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) { + // Cap the maximum context on either side of the line being provided, by taking the context + // around the line being provided for this ensures the line the pointer is on will have + // links provided. + const maxLineContext = Math.max(this._detector.maxLinkLength / this._detector.xterm.cols); + const minStartLine = Math.max(startLine - maxLineContext, 0); + const maxEndLine = Math.min(endLine + maxLineContext, this._detector.xterm.buffer.active.length); + + while (startLine >= minStartLine && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) { lines.unshift(this._detector.xterm.buffer.active.getLine(startLine - 1)!); startLine--; } - while (endLine < this._detector.xterm.buffer.active.length && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) { + while (endLine < maxEndLine && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) { lines.push(this._detector.xterm.buffer.active.getLine(endLine + 1)!); endLine++; } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index 202ba173cc0..bb6042ce80a 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -122,6 +122,10 @@ export function convertBufferRangeToViewport(bufferRange: IBufferRange, viewport } export function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string { + // Cap the maximum number of lines generated to prevent potential performance problems. This is + // more of a sanity check as the wrapped line should already be trimmed down at this point. + const maxLineLength = Math.max(2048 / cols * 2); + lineEnd = Math.min(lineEnd, lineStart + maxLineLength); let content = ''; for (let i = lineStart; i <= lineEnd; i++) { // Make sure only 0 to cols are considered as resizing when windows mode is enabled will diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts index 936af397072..48e5c0659bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts @@ -67,6 +67,12 @@ export const lineAndColumnClauseGroupCount = 6; export class TerminalLocalLinkDetector implements ITerminalLinkDetector { static id = 'local'; + // This was chosen as a reasonable maximum line length given the tradeoff between performance + // and how likely it is to encounter such a large line length. Some useful reference points: + // - Window old max length: 260 ($MAX_PATH) + // - Linux max length: 4096 ($PATH_MAX) + readonly maxLinkLength = 500; + constructor( readonly xterm: Terminal, private readonly _capabilities: ITerminalCapabilityStore, diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts index 948b257d8ab..446a7305405 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts @@ -17,18 +17,15 @@ const enum Constants { * The maximum number of links in a line to resolve against the file system. This limit is put * in place to avoid sending excessive data when remote connections are in place. */ - MaxResolvedLinksInLine = 10, - - /** - * The maximum length of a link to resolve against the file system. This limit is put in place - * to avoid sending excessive data when remote connections are in place. - */ - MaxResolvedLinkLength = 1024, + MaxResolvedLinksInLine = 10 } export class TerminalUriLinkDetector implements ITerminalLinkDetector { static id = 'uri'; + // 2048 is the maximum URL length + readonly maxLinkLength = 2048; + constructor( readonly xterm: Terminal, private readonly _resolvePath: (link: string, uri?: URI) => Promise, @@ -70,7 +67,7 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { } // Don't try resolve any links of excessive length - if (text.length > Constants.MaxResolvedLinkLength) { + if (text.length > this.maxLinkLength) { continue; } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts index 6bfa63c5038..637ff8dad9c 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts @@ -25,6 +25,10 @@ interface Word { export class TerminalWordLinkDetector implements ITerminalLinkDetector { static id = 'word'; + // Word links typically search the workspace so it makes sense that their maximum link length is + // quite small. + readonly maxLinkLength = 100; + constructor( readonly xterm: Terminal, @IConfigurationService private readonly _configurationService: IConfigurationService, diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts index 448f145fda2..bc867788d4b 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts @@ -91,13 +91,13 @@ suite('Workbench - TerminalUriLinkDetector', () => { text: `https://${'foobarbaz/'.repeat(102)}` }]); }); - test('should filter out file:// links that exceed 1024 characters', async () => { - // 8 + 101 * 10 = 1018 characters - await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(101)}`, [{ - text: `file:///${'foobarbaz/'.repeat(101)}`, - range: [[1, 1], [58, 13]] + test('should filter out file:// links that exceed 4096 characters', async () => { + // 8 + 200 * 10 = 2008 characters + await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(200)}`, [{ + text: `file:///${'foobarbaz/'.repeat(200)}`, + range: [[1, 1], [8, 26]] }]); - // 8 + 102 * 10 = 1028 characters - await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(102)}`, []); + // 8 + 450 * 10 = 4508 characters + await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(450)}`, []); }); }); From 9d18042706a81afd0a92e86a8cbbdd0abb74c922 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 25 May 2022 12:06:51 -0700 Subject: [PATCH 072/270] fix #150390 (#150395) fix #150392 --- src/vs/workbench/contrib/terminal/browser/terminalInstance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 5c841da1021..b7edef134d5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1369,6 +1369,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Send it to the process await this._processManager.write(text); this._onDidInputData.fire(this); + this.xterm?.scrollToBottom(); } async sendPath(originalPath: string, addNewLine: boolean): Promise { From 4d166fc71a5c8f5f1c33aa2a10609f5ec87754d6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 25 May 2022 12:07:16 -0700 Subject: [PATCH 073/270] don't add telemetry for feature terminals etc (#150224) --- .../common/xterm/shellIntegrationAddon.ts | 24 +++++++++++-------- src/vs/platform/terminal/node/ptyService.ts | 2 +- .../terminal/browser/terminalInstance.ts | 3 ++- .../terminal/browser/xterm/xtermTerminal.ts | 3 ++- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 2e456109e9e..8119de5aa52 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -128,6 +128,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati private _activationTimeout: any; constructor( + private readonly _disableTelemetry: boolean | undefined, private readonly _telemetryService: ITelemetryService | undefined, @ILogService private readonly _logService: ILogService ) { @@ -154,6 +155,19 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return didHandle; } + private async _ensureCapabilitiesOrAddFailureTelemetry(): Promise { + if (!this._telemetryService || this._disableTelemetry) { + return; + } + this._activationTimeout = setTimeout(() => { + if (!this.capabilities.get(TerminalCapability.CommandDetection) && !this.capabilities.get(TerminalCapability.CwdDetection)) { + this._telemetryService?.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationActivationTimeout'); + this._logService.warn('Shell integration failed to add capabilities within 10 seconds'); + } + this._hasUpdatedTelemetry = true; + }, 10000); + } + private _doHandleVSCodeSequence(data: string): boolean { if (!this._terminal) { return false; @@ -229,16 +243,6 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return false; } - private async _ensureCapabilitiesOrAddFailureTelemetry(): Promise { - this._activationTimeout = setTimeout(() => { - if (!this.capabilities.get(TerminalCapability.CommandDetection) && !this.capabilities.get(TerminalCapability.CwdDetection)) { - this._telemetryService?.publicLog2<{}, { owner: 'meganrogge'; comment: 'Indicates shell integration activation did not occur within 10 seconds' }>('terminal/shellIntegrationActivationTimeout'); - this._logService.warn('Shell integration failed to add capabilities within 10 seconds'); - } - this._hasUpdatedTelemetry = true; - }, 10000); - } - serialize(): ISerializedCommandDetectionCapability { if (!this._terminal || !this.capabilities.has(TerminalCapability.CommandDetection)) { return { diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index d9f8ae2b092..397fee9a493 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -742,7 +742,7 @@ class XtermSerializer implements ITerminalSerializer { this._xterm.writeln(reviveBuffer); } this.setUnicodeVersion(unicodeVersion); - this._shellIntegrationAddon = new ShellIntegrationAddon(undefined, logService); + this._shellIntegrationAddon = new ShellIntegrationAddon(true, undefined, logService); this._xterm.loadAddon(this._shellIntegrationAddon); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index b7edef134d5..82088ea5347 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -657,7 +657,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { throw new ErrorNoTelemetry('Terminal disposed of during xterm.js creation'); } - const xterm = this._instantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows, this.target || TerminalLocation.Panel, this.capabilities); + const disableShellIntegrationTelemetry = this.shellLaunchConfig.isFeatureTerminal || this.shellLaunchConfig.hideFromUser || this.shellLaunchConfig.executable === undefined; + const xterm = this._instantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows, this.target || TerminalLocation.Panel, this.capabilities, disableShellIntegrationTelemetry); this.xterm = xterm; const lineDataEventAddon = new LineDataEventAddon(); this.xterm.raw.loadAddon(lineDataEventAddon); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 3285ae2c363..56fc30dfdaa 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -99,6 +99,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { rows: number, location: TerminalLocation, private readonly _capabilities: ITerminalCapabilityStore, + disableShellIntegrationTelemetry: boolean, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @@ -175,7 +176,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { this._updateUnicodeVersion(); this._commandNavigationAddon = this._instantiationService.createInstance(CommandNavigationAddon, _capabilities); this.raw.loadAddon(this._commandNavigationAddon); - this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon, this._telemetryService); + this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon, disableShellIntegrationTelemetry, this._telemetryService); this.raw.loadAddon(this._shellIntegrationAddon); this._updateShellIntegrationAddons(); } From 280f1be7ba9a60d32d0a1949b38d212b99870906 Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Wed, 25 May 2022 12:19:28 -0700 Subject: [PATCH 074/270] quick draft PR for not throwing when looking for a chunked password (#150402) * quick draft PR for not throwing when looking for a chunked password * update comment with more info Co-authored-by: Ian Huff --- .../common/credentialsMainService.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/credentials/common/credentialsMainService.ts b/src/vs/platform/credentials/common/credentialsMainService.ts index 12a591de763..c9615fe27d8 100644 --- a/src/vs/platform/credentials/common/credentialsMainService.ts +++ b/src/vs/platform/credentials/common/credentialsMainService.ts @@ -150,19 +150,28 @@ export abstract class BaseCredentialsMainService extends Disposable implements I return false; } const didDelete = await keytar.deletePassword(service, account); - let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); - if (content && hasNextChunk) { - // need to delete additional chunks - let index = 1; - while (hasNextChunk) { - const accountWithIndex = `${account}-${index}`; - const nextChunk = await keytar.getPassword(service, accountWithIndex); - await keytar.deletePassword(service, accountWithIndex); + try { + let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); + if (content && hasNextChunk) { + // need to delete additional chunks + let index = 1; + while (hasNextChunk) { + const accountWithIndex = `${account}-${index}`; + const nextChunk = await keytar.getPassword(service, accountWithIndex); + await keytar.deletePassword(service, accountWithIndex); - const result: ChunkedPassword = JSON.parse(nextChunk!); - hasNextChunk = result.hasNextChunk; - index++; + const result: ChunkedPassword = JSON.parse(nextChunk!); + hasNextChunk = result.hasNextChunk; + index++; + } } + } catch { + // When the password is saved the entire JSON payload is encrypted then stored, thus the result from getPassword might not be valid JSON + // https://github.com/microsoft/vscode/blob/c22cb87311b5eb1a3bf5600d18733f7485355dc0/src/vs/workbench/api/browser/mainThreadSecretState.ts#L83 + // However in the chunked case we JSONify each chunk after encryption so for the chunked case we do expect valid JSON here + // https://github.com/microsoft/vscode/blob/708cb0c507d656b760f9d08115b8ebaf8964fd73/src/vs/platform/credentials/common/credentialsMainService.ts#L128 + // Empty catch here just as in getPassword because we expect to handle both JSON cases and non JSON cases here it's not an error case to fail to parse + // https://github.com/microsoft/vscode/blob/708cb0c507d656b760f9d08115b8ebaf8964fd73/src/vs/platform/credentials/common/credentialsMainService.ts#L76 } if (didDelete) { From 5f7983c8a56149771132cb1a6c60ba5e374d93af Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 25 May 2022 12:30:19 -0700 Subject: [PATCH 075/270] Fix tests to work with new link filtering --- .../browser/links/terminalUriLinkDetector.ts | 10 +++++----- .../links/terminalUriLinkDetector.test.ts | 17 +++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts index 446a7305405..921164f5de3 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts @@ -55,6 +55,11 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { const text = computedLink.url?.toString() || ''; + // Don't try resolve any links of excessive length + if (text.length > this.maxLinkLength) { + continue; + } + // Handle non-file scheme links if (uri.scheme !== Schemas.file) { links.push({ @@ -66,11 +71,6 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { continue; } - // Don't try resolve any links of excessive length - if (text.length > this.maxLinkLength) { - continue; - } - // Filter out URI with unrecognized authorities if (uri.authority.length !== 2 && uri.authority.endsWith(':')) { continue; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts index bc867788d4b..2b6becbb4c5 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts @@ -79,17 +79,14 @@ suite('Workbench - TerminalUriLinkDetector', () => { { range: [[16, 1], [29, 1]], text: 'http://bar.foo' } ]); }); - test('should not filtrer out https:// link that exceed 1024 characters', async () => { - // 8 + 101 * 10 = 1018 characters - await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(101)}`, [{ - range: [[1, 1], [58, 13]], - text: `https://${'foobarbaz/'.repeat(101)}` - }]); - // 8 + 102 * 10 = 1028 characters - await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(102)}`, [{ - range: [[1, 1], [68, 13]], - text: `https://${'foobarbaz/'.repeat(102)}` + test('should filter out https:// link that exceed 4096 characters', async () => { + // 8 + 200 * 10 = 2008 characters + await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(200)}`, [{ + range: [[1, 1], [8, 26]], + text: `https://${'foobarbaz/'.repeat(200)}` }]); + // 8 + 450 * 10 = 4508 characters + await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(450)}`, []); }); test('should filter out file:// links that exceed 4096 characters', async () => { // 8 + 200 * 10 = 2008 characters From 9c1926d72676a90733232fee94f949acb466aa33 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 25 May 2022 12:51:23 -0700 Subject: [PATCH 076/270] xterm@4.19.0-beta.52 Fixes #148061 --- package.json | 4 ++-- remote/package.json | 4 ++-- remote/web/package.json | 2 +- remote/web/yarn.lock | 8 ++++---- remote/yarn.lock | 16 ++++++++-------- yarn.lock | 16 ++++++++-------- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 3768321d2bb..2728502f1fb 100644 --- a/package.json +++ b/package.json @@ -85,12 +85,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.49", + "xterm": "4.19.0-beta.52", "xterm-addon-search": "0.9.0-beta.37", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", "xterm-addon-webgl": "0.12.0-beta.36", - "xterm-headless": "4.19.0-beta.49", + "xterm-headless": "4.19.0-beta.52", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 3e833d3c5f6..fe99f034f83 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,12 +24,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.49", + "xterm": "4.19.0-beta.52", "xterm-addon-search": "0.9.0-beta.37", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", "xterm-addon-webgl": "0.12.0-beta.36", - "xterm-headless": "4.19.0-beta.49", + "xterm-headless": "4.19.0-beta.52", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 4ec42b01f12..470499091e3 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -10,7 +10,7 @@ "tas-client-umd": "0.1.5", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.49", + "xterm": "4.19.0-beta.52", "xterm-addon-search": "0.9.0-beta.37", "xterm-addon-unicode11": "0.4.0-beta.3", "xterm-addon-webgl": "0.12.0-beta.36" diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index d8eac016d98..0b6e2d9535a 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -128,7 +128,7 @@ xterm-addon-webgl@0.12.0-beta.36: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.36.tgz#460f80829a78c979a448d5b764699af3f0366ff1" integrity sha512-sgX7OHSGZQZE5b4xtPqd/5NEcll0Z+00tnTVxKZlXf5XEENcG0tnBF4I4f+k9K3cmjE1UIUVG2yYPrqWlYCdpA== -xterm@4.19.0-beta.49: - version "4.19.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.49.tgz#96d0d748c97a2f0cfa4c6112bc4de8b0d5d559fe" - integrity sha512-qPhOeGtnB357mZOvfDckVlPDR7EDkSASQrB3aujhjgJlOiBBqcNRJcuSvF7v1DOho7xdEI9UQxiSiT5diHhMlg== +xterm@4.19.0-beta.52: + version "4.19.0-beta.52" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.52.tgz#21c7ad0bd905f3fddaa0583951514c8271dcd822" + integrity sha512-2itY6dyiimOEkgVwc31TngYQ6npHt7or1wxU0Flbv4PjH4ke8K26NDB7KAi0YYkn2g0/qzIjQc7ykdrrTaEgMg== diff --git a/remote/yarn.lock b/remote/yarn.lock index 52a1cc813ed..8f228e501bf 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -934,15 +934,15 @@ xterm-addon-webgl@0.12.0-beta.36: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.36.tgz#460f80829a78c979a448d5b764699af3f0366ff1" integrity sha512-sgX7OHSGZQZE5b4xtPqd/5NEcll0Z+00tnTVxKZlXf5XEENcG0tnBF4I4f+k9K3cmjE1UIUVG2yYPrqWlYCdpA== -xterm-headless@4.19.0-beta.49: - version "4.19.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.49.tgz#8e4178620be9a9dee25a586ef992a6fc621a829a" - integrity sha512-uRnHpR2pv1MJPbE3sa7XbP6x0uHubAWVMbiD26e3a5ICtWJQVVpLojkA0t4qU7CoMX85pRL1rIclg9CKY0B0xA== +xterm-headless@4.19.0-beta.52: + version "4.19.0-beta.52" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.52.tgz#87298cb205694cfdd701a9a96dcd06434a9773fe" + integrity sha512-9s8VtGa9JUdEWxrNDJpEng2mEFn0KjWM3R9nxezA8mXHsJ/0cMqoK00s/fzhAEXZqHycWASSECMWgFOaT0MrnQ== -xterm@4.19.0-beta.49: - version "4.19.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.49.tgz#96d0d748c97a2f0cfa4c6112bc4de8b0d5d559fe" - integrity sha512-qPhOeGtnB357mZOvfDckVlPDR7EDkSASQrB3aujhjgJlOiBBqcNRJcuSvF7v1DOho7xdEI9UQxiSiT5diHhMlg== +xterm@4.19.0-beta.52: + version "4.19.0-beta.52" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.52.tgz#21c7ad0bd905f3fddaa0583951514c8271dcd822" + integrity sha512-2itY6dyiimOEkgVwc31TngYQ6npHt7or1wxU0Flbv4PjH4ke8K26NDB7KAi0YYkn2g0/qzIjQc7ykdrrTaEgMg== yallist@^4.0.0: version "4.0.0" diff --git a/yarn.lock b/yarn.lock index e483a32e500..5f155fdde19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12218,15 +12218,15 @@ xterm-addon-webgl@0.12.0-beta.36: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.36.tgz#460f80829a78c979a448d5b764699af3f0366ff1" integrity sha512-sgX7OHSGZQZE5b4xtPqd/5NEcll0Z+00tnTVxKZlXf5XEENcG0tnBF4I4f+k9K3cmjE1UIUVG2yYPrqWlYCdpA== -xterm-headless@4.19.0-beta.49: - version "4.19.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.49.tgz#8e4178620be9a9dee25a586ef992a6fc621a829a" - integrity sha512-uRnHpR2pv1MJPbE3sa7XbP6x0uHubAWVMbiD26e3a5ICtWJQVVpLojkA0t4qU7CoMX85pRL1rIclg9CKY0B0xA== +xterm-headless@4.19.0-beta.52: + version "4.19.0-beta.52" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.52.tgz#87298cb205694cfdd701a9a96dcd06434a9773fe" + integrity sha512-9s8VtGa9JUdEWxrNDJpEng2mEFn0KjWM3R9nxezA8mXHsJ/0cMqoK00s/fzhAEXZqHycWASSECMWgFOaT0MrnQ== -xterm@4.19.0-beta.49: - version "4.19.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.49.tgz#96d0d748c97a2f0cfa4c6112bc4de8b0d5d559fe" - integrity sha512-qPhOeGtnB357mZOvfDckVlPDR7EDkSASQrB3aujhjgJlOiBBqcNRJcuSvF7v1DOho7xdEI9UQxiSiT5diHhMlg== +xterm@4.19.0-beta.52: + version "4.19.0-beta.52" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.52.tgz#21c7ad0bd905f3fddaa0583951514c8271dcd822" + integrity sha512-2itY6dyiimOEkgVwc31TngYQ6npHt7or1wxU0Flbv4PjH4ke8K26NDB7KAi0YYkn2g0/qzIjQc7ykdrrTaEgMg== y18n@^3.2.1: version "3.2.2" From 22806eaa8e961ca5dff67410e8bbe7ff7cc5e77b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 25 May 2022 12:54:31 -0700 Subject: [PATCH 077/270] re-enable terminal tabs test (#148966) --- test/smoke/src/areas/terminal/terminal-tabs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/smoke/src/areas/terminal/terminal-tabs.test.ts b/test/smoke/src/areas/terminal/terminal-tabs.test.ts index 1f962540d75..cc4573978e8 100644 --- a/test/smoke/src/areas/terminal/terminal-tabs.test.ts +++ b/test/smoke/src/areas/terminal/terminal-tabs.test.ts @@ -67,7 +67,7 @@ export function setup() { await terminal.assertSingleTab({ name }); }); - it.skip('should reset the tab name to the default value when no name is provided', async () => { // https://github.com/microsoft/vscode/issues/146796 + it('should reset the tab name to the default value when no name is provided', async () => { await terminal.createTerminal(); const defaultName = await terminal.getSingleTabName(); const name = 'my terminal name'; From 3ed15398de076e1ef26cce6cc3532a1c21183078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 25 May 2022 22:22:30 +0200 Subject: [PATCH 078/270] update endgame notebook (#150407) --- .vscode/notebooks/endgame.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 90cc4971f43..48195c79000 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-remotehub repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-unpkg\n\n$MILESTONE=milestone:\"April 2022\"" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-remotehub repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-unpkg\r\n\r\n$MILESTONE=milestone:\"May 2022\"" }, { "kind": 1, From e7833fe75c5ca9e10e89c67dc201362da623cda0 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 25 May 2022 13:38:11 -0700 Subject: [PATCH 079/270] show terminal find history placeholder on focus (#150409) --- .../contrib/codeEditor/browser/find/simpleFindWidget.css | 4 +++- .../contrib/codeEditor/browser/find/simpleFindWidget.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index 46b11d7eedf..b280e584779 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -9,7 +9,7 @@ position: absolute; top: 0; right: 18px; - width: 220px; + width: 360px; max-width: calc(100% - 28px - 28px - 8px); pointer-events: none; padding: 0 10px 10px; @@ -29,6 +29,7 @@ align-items: center; pointer-events: all; transition: top 200ms linear; + min-width: 360px; } .monaco-workbench.reduce-motion .monaco-editor .find-widget { @@ -45,6 +46,7 @@ .monaco-workbench .simple-find-part .monaco-findInput { flex: 1; + min-width: 220px; } .monaco-workbench .simple-find-part .button { diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 5fa3bb659ab..d31078de067 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -22,6 +22,7 @@ import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; import * as strings from 'vs/base/common/strings'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find (\u21C5 for history)"); @@ -80,9 +81,9 @@ export abstract class SimpleFindWidget extends Widget { }, appendCaseSensitiveLabel: options.appendCaseSensitiveLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindCaseSensitive) : undefined, appendRegexLabel: options.appendRegexLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindRegex) : undefined, - appendWholeWordsLabel: options.appendWholeWordsLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindWholeWord) : undefined + appendWholeWordsLabel: options.appendWholeWordsLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindWholeWord) : undefined, + showHistoryHint: () => showHistoryKeybindingHint(_keybindingService) }, contextKeyService, options.showOptionButtons)); - // Find History with update delayer this._updateHistoryDelayer = new Delayer(500); From eb3dcea9528c19bddd3f94213e0a90819cec5f54 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 25 May 2022 22:56:37 +0200 Subject: [PATCH 080/270] Adopt `setTimeout0` which doesn't suffer from the 4ms artificial delay that browsers introduce when the nesting level is > 5 --- src/vs/base/test/common/timeTravelScheduler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/base/test/common/timeTravelScheduler.ts b/src/vs/base/test/common/timeTravelScheduler.ts index 9081b7c5632..470d93ac864 100644 --- a/src/vs/base/test/common/timeTravelScheduler.ts +++ b/src/vs/base/test/common/timeTravelScheduler.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { setTimeout0 } from 'vs/base/common/platform'; interface PriorityQueue { length: number; @@ -178,7 +179,7 @@ export class AsyncSchedulerProcessor extends Disposable { if (this.useSetImmediate) { originalGlobalValues.setImmediate(() => this.process()); } else { - originalGlobalValues.setTimeout(() => this.process()); + setTimeout0(() => this.process()); } }); } From b180649a62f01a5ccb8c271dc8490c05aba29de3 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 25 May 2022 23:23:43 +0200 Subject: [PATCH 081/270] Fix unit tests on node --- src/vs/base/common/platform.ts | 4 +++- src/vs/base/test/common/timeTravelScheduler.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index e0ea1d62c64..948b62e0a93 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -195,6 +195,8 @@ export const locale = _locale; */ export const translationsConfigFile = _translationsConfigFile; +export const setTimeout0IsFaster = (typeof globals.postMessage === 'function' && !globals.importScripts); + /** * See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-. * @@ -202,7 +204,7 @@ export const translationsConfigFile = _translationsConfigFile; * that browsers set when the nesting level is > 5. */ export const setTimeout0 = (() => { - if (typeof globals.postMessage === 'function' && !globals.importScripts) { + if (setTimeout0IsFaster) { interface IQueueElement { id: number; callback: () => void; diff --git a/src/vs/base/test/common/timeTravelScheduler.ts b/src/vs/base/test/common/timeTravelScheduler.ts index 470d93ac864..797f308dc06 100644 --- a/src/vs/base/test/common/timeTravelScheduler.ts +++ b/src/vs/base/test/common/timeTravelScheduler.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { setTimeout0 } from 'vs/base/common/platform'; +import { setTimeout0, setTimeout0IsFaster } from 'vs/base/common/platform'; interface PriorityQueue { length: number; @@ -178,8 +178,10 @@ export class AsyncSchedulerProcessor extends Disposable { Promise.resolve().then(() => { if (this.useSetImmediate) { originalGlobalValues.setImmediate(() => this.process()); - } else { + } else if (setTimeout0IsFaster) { setTimeout0(() => this.process()); + } else { + originalGlobalValues.setTimeout(() => this.process()); } }); } From 6844f0b7d15e881ded03e93b828d85a4aa7417e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 25 May 2022 16:01:25 -0700 Subject: [PATCH 082/270] Clear activation timeout if terminal is disposed Fixes #150419 --- .../common/xterm/shellIntegrationAddon.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 8119de5aa52..3e48db34a63 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IShellIntegration } from 'vs/platform/terminal/common/terminal'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; import { CwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/cwdDetectionCapability'; @@ -133,6 +133,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati @ILogService private readonly _logService: ILogService ) { super(); + this._register(toDisposable(() => this._clearActivationTimeout())); } activate(xterm: Terminal) { @@ -147,10 +148,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati if (!this._hasUpdatedTelemetry && didHandle) { this._telemetryService?.publicLog2<{}, { owner: 'meganrogge'; comment: 'Indicates shell integration was activated' }>('terminal/shellIntegrationActivationSucceeded'); this._hasUpdatedTelemetry = true; - if (this._activationTimeout !== undefined) { - clearTimeout(this._activationTimeout); - this._activationTimeout = undefined; - } + this._clearActivationTimeout(); } return didHandle; } @@ -168,6 +166,13 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati }, 10000); } + private _clearActivationTimeout(): void { + if (this._activationTimeout !== undefined) { + clearTimeout(this._activationTimeout); + this._activationTimeout = undefined; + } + } + private _doHandleVSCodeSequence(data: string): boolean { if (!this._terminal) { return false; From 12cb9932382bcde504d05477c5049f22e5f07d26 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 25 May 2022 17:30:39 -0700 Subject: [PATCH 083/270] Create cell execution beforehand (#150410) --- .../api/browser/mainThreadNotebookKernels.ts | 13 ++++++- .../browser/notebookExecutionServiceImpl.ts | 37 ++++++++++++------- .../notebookExecutionStateServiceImpl.ts | 9 +---- .../common/notebookExecutionStateService.ts | 2 +- .../notebookExecutionStateService.test.ts | 8 ++-- .../test/browser/testNotebookEditor.ts | 2 +- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 6b734982787..c02518f694c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -18,6 +18,7 @@ import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workb import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; abstract class MainThreadKernel implements INotebookKernel { private readonly _onDidChange = new Emitter(); @@ -112,6 +113,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape @ILanguageService private readonly _languageService: ILanguageService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, + @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorService notebookEditorService: INotebookEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookKernels); @@ -246,7 +248,16 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape $createExecution(handle: number, controllerId: string, rawUri: UriComponents, cellHandle: number): void { const uri = URI.revive(rawUri); - const execution = this._notebookExecutionStateService.createCellExecution(controllerId, uri, cellHandle); + const notebook = this._notebookService.getNotebookTextModel(uri); + if (!notebook) { + throw new Error(`Notebook not found: ${uri.toString()}`); + } + + const kernel = this._notebookKernelService.getMatchingKernel(notebook); + if (!kernel.selected || kernel.selected.id !== controllerId) { + throw new Error(`Kernel is not selected: ${kernel.selected?.id} !== ${controllerId}`); + } + const execution = this._notebookExecutionStateService.createCellExecution(uri, cellHandle); execution.confirm(); this._executions.set(handle, execution); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 0be1459d36a..de0c18f7638 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -13,7 +13,7 @@ import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controll import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; export class NotebookExecutionService implements INotebookExecutionService, IDisposable { @@ -38,6 +38,16 @@ export class NotebookExecutionService implements INotebookExecutionService, IDis return; } + // create cell executions + const cellExecutions: [NotebookCellTextModel, INotebookCellExecution][] = []; + for (const cell of cellsArr) { + const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); + if (cell.cellKind !== CellKind.Code || !!cellExe) { + continue; + } + cellExecutions.push([cell, this._notebookExecutionStateService.createCellExecution(notebook.uri, cell.handle)]); + } + let kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); if (!kernel) { kernel = await this.resolveSourceActions(notebook); @@ -49,28 +59,27 @@ export class NotebookExecutionService implements INotebookExecutionService, IDis } if (!kernel) { + // clear all pending cell executions + cellExecutions.forEach(cellExe => cellExe[1].complete({})); return; } - const executeCells: NotebookCellTextModel[] = []; - for (const cell of cellsArr) { - const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); - if (cell.cellKind !== CellKind.Code || !!cellExe) { - continue; - } + // filter cell executions based on selected kernel + const validCellExecutions: INotebookCellExecution[] = []; + for (const [cell, cellExecution] of cellExecutions) { if (!kernel.supportedLanguages.includes(cell.language)) { - continue; + cellExecution.complete({}); + } else { + validCellExecutions.push(cellExecution); } - executeCells.push(cell); } - if (executeCells.length > 0) { + // request execution + if (validCellExecutions.length > 0) { this._notebookKernelService.selectKernelForNotebook(kernel, notebook); - - const exes = executeCells.map(c => this._notebookExecutionStateService.createCellExecution(kernel!.id, notebook.uri, c.handle)); - await kernel.executeNotebookCellsRequest(notebook.uri, executeCells.map(c => c.handle)); + await kernel.executeNotebookCellsRequest(notebook.uri, validCellExecutions.map(c => c.cellHandle)); // the connecting state can change before the kernel resolves executeNotebookCellsRequest - const unconfirmed = exes.filter(exe => exe.state === NotebookCellExecutionState.Unconfirmed); + const unconfirmed = validCellExecutions.filter(exe => exe.state === NotebookCellExecutionState.Unconfirmed); if (unconfirmed.length) { this._logService.debug(`NotebookExecutionService#executeNotebookCells completing unconfirmed executions ${JSON.stringify(unconfirmed.map(exe => exe.cellHandle))}`); unconfirmed.forEach(exe => exe.complete({})); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts index be0522ab323..3b487ec7684 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts @@ -14,7 +14,6 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CellEditType, CellUri, ICellEditOperation, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookTextModelWillAddRemoveEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType, INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecuteUpdate, ICellExecutionComplete, ICellExecutionStateChangedEvent, ICellExecutionStateUpdate, INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; export class NotebookExecutionStateService extends Disposable implements INotebookExecutionStateService { @@ -28,7 +27,6 @@ export class NotebookExecutionStateService extends Disposable implements INotebo onDidChangeCellExecution = this._onDidChangeCellExecution.event; constructor( - @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @INotebookService private readonly _notebookService: INotebookService, @@ -91,17 +89,12 @@ export class NotebookExecutionStateService extends Disposable implements INotebo this._onDidChangeCellExecution.fire(new NotebookExecutionEvent(notebookUri, cellHandle)); } - createCellExecution(controllerId: string, notebookUri: URI, cellHandle: number): INotebookCellExecution { + createCellExecution(notebookUri: URI, cellHandle: number): INotebookCellExecution { const notebook = this._notebookService.getNotebookTextModel(notebookUri); if (!notebook) { throw new Error(`Notebook not found: ${notebookUri.toString()}`); } - const kernel = this._notebookKernelService.getMatchingKernel(notebook); - if (!kernel.selected || kernel.selected.id !== controllerId) { - throw new Error(`Kernel is not selected: ${kernel.selected?.id} !== ${controllerId}`); - } - let notebookExecutionMap = this._executions.get(notebookUri); if (!notebookExecutionMap) { const listeners = this._instantiationService.createInstance(NotebookExecutionListeners, notebookUri); diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts index 0a9902a5ff4..f6317245bb1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts @@ -50,7 +50,7 @@ export interface INotebookExecutionStateService { forceCancelNotebookExecutions(notebookUri: URI): void; getCellExecutionStatesForNotebook(notebook: URI): INotebookCellExecution[]; getCellExecution(cellUri: URI): INotebookCellExecution | undefined; - createCellExecution(controllerId: string, notebook: URI, cellHandle: number): INotebookCellExecution; + createCellExecution(notebook: URI, cellHandle: number): INotebookCellExecution; } export interface INotebookCellExecution { diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 0b05037e51e..e925ef9e011 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -94,7 +94,7 @@ suite('NotebookExecutionStateService', () => { const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + executionStateService.createCellExecution(viewModel.uri, cell.handle); assert.strictEqual(didCancel, false); viewModel.notebookDocument.applyEdits([{ editType: CellEditType.Replace, index: 0, count: 1, cells: [] @@ -113,7 +113,7 @@ suite('NotebookExecutionStateService', () => { const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - const exe = executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); let didFire = false; disposables.add(executionStateService.onDidChangeCellExecution(e => { @@ -154,7 +154,7 @@ suite('NotebookExecutionStateService', () => { deferred.complete(); })); - executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + executionStateService.createCellExecution(viewModel.uri, cell.handle); return deferred.p; }); @@ -170,7 +170,7 @@ suite('NotebookExecutionStateService', () => { const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + executionStateService.createCellExecution(viewModel.uri, cell.handle); const exe = executionStateService.getCellExecution(cell.uri); assert.ok(exe); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 514c5231443..8570bf9146a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -422,7 +422,7 @@ class TestNotebookExecutionStateService implements INotebookExecutionStateServic return this._executions.get(cellUri); } - createCellExecution(controllerId: string, notebook: URI, cellHandle: number): INotebookCellExecution { + createCellExecution(notebook: URI, cellHandle: number): INotebookCellExecution { const onComplete = () => this._executions.delete(CellUri.generate(notebook, cellHandle)); const exe = new TestCellExecution(notebook, cellHandle, onComplete); this._executions.set(CellUri.generate(notebook, cellHandle), exe); From e262c88fb16e1d4ec5c7d5cf82ac78dad7961091 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 25 May 2022 18:28:46 -0700 Subject: [PATCH 084/270] Fix process.platform for picomatch (#150430) The `picomatch` library currently checks `process.platform`. This check fails on web, which causes the markdown extension to not load To fix this, I'm replacing `process.platform` with the string`'web'` --- extensions/shared.webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 84aa4259a3b..3ebb9c62b52 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -151,6 +151,7 @@ const browserPlugins = [ ] }), new DefinePlugin({ + 'process.platform': JSON.stringify('web'), 'process.env': JSON.stringify({}), 'process.env.BROWSER_ENV': JSON.stringify('true') }) From 528ee1ae3daabe30c1307cf9dcd6e77eb96094bc Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 25 May 2022 18:29:28 -0700 Subject: [PATCH 085/270] Allow multiple entries with the same mimetype in dataTransfer (#150425) Currently our data transfer implementation only allows a single entry of each mimeType. There can only be a single `image/gif` file for example. However this doesn't match how the DOM apis work. If you drop multiple gifs into VS Code for example, the DataTransfer you get contains entries for each of the gifs. This change allows us to also support DataTransfers that have multiple entries with the same mime type. Just like with the DOM, we support constructing these duplicate mime data transfers internally, but do not allow extensions to create them As part of this change, I've also made a few clean ups: - Add helpers for creating dataTransfer items - Clarify when adding a data transfer item should `append` or `replace` - Adopt some helper functions in a few more places --- src/vs/base/common/dataTransfer.ts | 75 ++++++++++++------- src/vs/editor/browser/dnd.ts | 19 +++-- .../copyPaste/browser/copyPasteController.ts | 12 +-- .../browser/dropIntoEditorContribution.ts | 27 ++----- .../api/browser/mainThreadLanguageFeatures.ts | 4 +- .../api/browser/mainThreadTreeViews.ts | 4 +- .../api/common/extHostTypeConverters.ts | 10 +-- src/vs/workbench/api/common/extHostTypes.ts | 23 +++++- .../workbench/browser/parts/views/treeView.ts | 16 ++-- 9 files changed, 104 insertions(+), 86 deletions(-) diff --git a/src/vs/base/common/dataTransfer.ts b/src/vs/base/common/dataTransfer.ts index d5d99bd283b..5074f30c75e 100644 --- a/src/vs/base/common/dataTransfer.ts +++ b/src/vs/base/common/dataTransfer.ts @@ -17,55 +17,74 @@ export interface IDataTransferItem { value: any; } +export function createStringDataTransferItem(stringOrPromise: string | Promise): IDataTransferItem { + return { + asString: async () => stringOrPromise, + asFile: () => undefined, + value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined, + }; +} + +export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise): IDataTransferItem { + return { + asString: async () => '', + asFile: () => ({ name: fileName, uri, data }), + value: undefined, + }; +} + export class VSDataTransfer { - private readonly _data = new Map(); + private readonly _entries = new Map(); public get size(): number { - return this._data.size; + return this._entries.size; } public has(mimeType: string): boolean { - return this._data.has(mimeType); + return this._entries.has(this.toKey(mimeType)); } public get(mimeType: string): IDataTransferItem | undefined { - return this._data.get(mimeType); + return this._entries.get(this.toKey(mimeType))?.[0]; } - public set(mimeType: string, value: IDataTransferItem): void { - this._data.set(mimeType, value); + public append(mimeType: string, value: IDataTransferItem): void { + const existing = this._entries.get(mimeType); + if (existing) { + existing.push(value); + } else { + this._entries.set(this.toKey(mimeType), [value]); + } + } + + public replace(mimeType: string, value: IDataTransferItem): void { + this._entries.set(this.toKey(mimeType), [value]); } public delete(mimeType: string) { - this._data.delete(mimeType); + this._entries.delete(this.toKey(mimeType)); } - public setString(mimeType: string, stringOrPromise: string | Promise) { - this.set(mimeType, { - asString: async () => stringOrPromise, - asFile: () => undefined, - value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined, - }); + public *entries(): Iterable<[string, IDataTransferItem]> { + for (const [mine, items] of this._entries.entries()) { + for (const item of items) { + yield [mine, item]; + } + } } - public setFile(mimeType: string, fileName: string, uri: URI | undefined, data: () => Promise) { - this.set(mimeType, { - asString: async () => '', - asFile: () => ({ name: fileName, uri, data }), - value: undefined, - }); - } - - public entries(): IterableIterator<[string, IDataTransferItem]> { - return this._data.entries(); - } - - public values(): IterableIterator { - return this._data.values(); + public values(): Iterable { + return Array.from(this._entries.values()).flat(); } public forEach(f: (value: IDataTransferItem, key: string) => void) { - this._data.forEach(f); + for (const [mime, item] of this.entries()) { + f(item, mime); + } + } + + private toKey(mimeType: string): string { + return mimeType.toLowerCase(); } } diff --git a/src/vs/editor/browser/dnd.ts b/src/vs/editor/browser/dnd.ts index 08a3f558786..80a5958504d 100644 --- a/src/vs/editor/browser/dnd.ts +++ b/src/vs/editor/browser/dnd.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { createFileDataTransferItem, createStringDataTransferItem, IDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { URI } from 'vs/base/common/uri'; +import { FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd'; export function toVSDataTransfer(dataTransfer: DataTransfer) { @@ -13,16 +14,20 @@ export function toVSDataTransfer(dataTransfer: DataTransfer) { const type = item.type; if (item.kind === 'string') { const asStringValue = new Promise(resolve => item.getAsString(resolve)); - vsDataTransfer.setString(type, asStringValue); + vsDataTransfer.append(type, createStringDataTransferItem(asStringValue)); } else if (item.kind === 'file') { - const file = item.getAsFile() as null | (File & { path?: string }); + const file = item.getAsFile(); if (file) { - const uri = file.path ? URI.parse(file.path) : undefined; - vsDataTransfer.setFile(type, file.name, uri, async () => { - return new Uint8Array(await file.arrayBuffer()); - }); + vsDataTransfer.append(type, createFileDataTransferItemFromFile(file)); } } } return vsDataTransfer; } + +export function createFileDataTransferItemFromFile(file: File): IDataTransferItem { + const uri = (file as FileAdditionalNativeProperties).path ? URI.parse((file as FileAdditionalNativeProperties).path!) : undefined; + return createFileDataTransferItem(file.name, uri, async () => { + return new Uint8Array(await file.arrayBuffer()); + }); +} diff --git a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts index 6c203af56cd..74ddf65d392 100644 --- a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts @@ -6,7 +6,7 @@ import { addDisposableListener } from 'vs/base/browser/dom'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Disposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { generateUuid } from 'vs/base/common/uuid'; @@ -103,7 +103,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi for (const result of results) { result?.forEach((value, key) => { - dataTransfer.set(key, value); + dataTransfer.replace(key, value); }); } @@ -148,7 +148,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi if (handle && this._currentClipboardItem?.handle === handle) { const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise; toMergeDataTransfer.forEach((value, key) => { - dataTransfer.set(key, value); + dataTransfer.append(key, value); }); } @@ -156,11 +156,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const resources = await this._clipboardService.readResources(); if (resources.length) { const value = resources.join('\n'); - dataTransfer.set(Mimes.uriList, { - value, - asString: async () => value, - asFile: () => undefined, - }); + dataTransfer.append(Mimes.uriList, createStringDataTransferItem(value)); } } diff --git a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index b713b71a7dd..4abda27de6c 100644 --- a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -5,11 +5,12 @@ import { distinct } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Disposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { toVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IPosition } from 'vs/editor/common/core/position'; @@ -20,7 +21,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { extractEditorsDropData, FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd'; +import { extractEditorsDropData } from 'vs/platform/dnd/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -94,27 +95,11 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } public async extractDataTransferData(dragEvent: DragEvent): Promise { - const textEditorDataTransfer = new VSDataTransfer(); if (!dragEvent.dataTransfer) { - return textEditorDataTransfer; - } - - for (const item of dragEvent.dataTransfer.items) { - const type = item.type; - if (item.kind === 'string') { - const asStringValue = new Promise(resolve => item.getAsString(resolve)); - textEditorDataTransfer.setString(type, asStringValue); - } else if (item.kind === 'file') { - const file = item.getAsFile(); - if (file) { - const uri = (file as FileAdditionalNativeProperties).path ? URI.parse((file as FileAdditionalNativeProperties).path!) : undefined; - textEditorDataTransfer.setFile(type, file.name, uri, async () => { - return new Uint8Array(await file.arrayBuffer()); - }); - } - } + return new VSDataTransfer(); } + const textEditorDataTransfer = toVSDataTransfer(dragEvent.dataTransfer); if (!textEditorDataTransfer.has(Mimes.uriList)) { const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) .filter(input => input.resource) @@ -122,7 +107,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr if (editorData.length) { const str = distinct(editorData).join('\n'); - textEditorDataTransfer.setString(Mimes.uriList.toLowerCase(), str); + textEditorDataTransfer.replace(Mimes.uriList, createStringDataTransferItem(str)); } } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index f89e0698b9d..ff55fc5844f 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -5,7 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -385,7 +385,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread const dataTransferOut = new VSDataTransfer(); result.items.forEach(([type, item]) => { - dataTransferOut.setString(type, item.asString); + dataTransferOut.replace(type, createStringDataTransferItem(item.asString)); }); return dataTransferOut; } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 56133dc4b20..7c8aa9d2323 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -14,7 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { VSBuffer } from 'vs/base/common/buffer'; import { DataTransferCache } from 'vs/workbench/api/common/shared/dataTransferCache'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; @@ -225,7 +225,7 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { const additionalDataTransfer = new VSDataTransfer(); additionalDataTransferDTO.items.forEach(([type, item]) => { - additionalDataTransfer.setString(type, item.asString); + additionalDataTransfer.replace(type, createStringDataTransferItem(item.asString)); }); return additionalDataTransfer; } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 95e24e8692a..18d71be8a26 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,6 +40,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed import type * as vscode from 'vscode'; import * as types from './extHostTypes'; import { once } from 'vs/base/common/functional'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; export namespace Command { @@ -1980,14 +1981,13 @@ export namespace DataTransferItem { export namespace DataTransfer { export function toDataTransfer(value: extHostProtocol.DataTransferDTO, resolveFileData: (dataItemIndex: number) => Promise): types.DataTransfer { - const newDataTransfer = new types.DataTransfer(); - value.items.forEach(([type, item], index) => { - newDataTransfer.set(type, DataTransferItem.toDataTransferItem(item, () => resolveFileData(index))); + const init = value.items.map(([type, item], index) => { + return [type, DataTransferItem.toDataTransferItem(item, () => resolveFileData(index))] as const; }); - return newDataTransfer; + return new types.DataTransfer(init); } - export async function toDataTransferDTO(value: vscode.DataTransfer): Promise { + export async function toDataTransferDTO(value: vscode.DataTransfer | VSDataTransfer): Promise { const newDTO: extHostProtocol.DataTransferDTO = { items: [] }; const promises: Promise[] = []; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 76ae9232db9..a1c2a3ea107 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2451,18 +2451,33 @@ export class DataTransferItem { @es5ClassCompat export class DataTransfer { - #items = new Map(); + #items = new Map(); + + constructor(init?: Iterable) { + for (const [mime, item] of init ?? []) { + const existing = this.#items.get(mime); + if (existing) { + existing.push(item); + } else { + this.#items.set(mime, [item]); + } + } + } get(mimeType: string): DataTransferItem | undefined { - return this.#items.get(mimeType); + return this.#items.get(mimeType)?.[0]; } set(mimeType: string, value: DataTransferItem): void { - this.#items.set(mimeType, value); + // This intentionally overwrites all entries for a given mimetype. + // This is similar to how the DOM DataTransfer type works + this.#items.set(mimeType, [value]); } forEach(callbackfn: (value: DataTransferItem, key: string) => void): void { - this.#items.forEach(callbackfn); + for (const [mime, items] of this.#items) { + items.forEach(item => callbackfn(item, mime)); + } } } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 051926ebcb4..524979149fe 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -31,7 +31,7 @@ import { isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import 'vs/css!./media/views'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Command } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -66,7 +66,8 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { CodeDataTransfers, FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd'; +import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; +import { createFileDataTransferItemFromFile } from 'vs/editor/browser/dnd'; export class TreeViewPane extends ViewPane { @@ -1524,7 +1525,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { } if (dataValue) { const converted = this.convertKnownMimes(type, kind, dataValue); - treeDataTransfer.setString(converted.type, converted.value + ''); + treeDataTransfer.append(converted.type, createStringDataTransferItem(converted.value + '')); } resolve(); })); @@ -1532,11 +1533,8 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { const file = dataItem.getAsFile(); if (file) { uris.push(URI.file(file.path)); - const uri = (file as FileAdditionalNativeProperties).path ? URI.parse((file as FileAdditionalNativeProperties).path!) : undefined; if (dndController.supportsFileDataTransfers) { - treeDataTransfer.setFile(type, file.name, uri, async () => { - return new Uint8Array(await file.arrayBuffer()); - }); + treeDataTransfer.append(type, createFileDataTransferItemFromFile(file)); } } } @@ -1545,7 +1543,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { // Check if there are uris to add and add them if (uris.length) { - treeDataTransfer.setString(Mimes.uriList, uris.map(uri => uri.toString()).join('\n')); + treeDataTransfer.replace(Mimes.uriList, createStringDataTransferItem(uris.map(uri => uri.toString()).join('\n'))); } const additionalWillDropPromise = this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid); @@ -1555,7 +1553,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { return additionalWillDropPromise.then(additionalDataTransfer => { if (additionalDataTransfer) { for (const item of additionalDataTransfer.entries()) { - treeDataTransfer.set(item[0], item[1]); + treeDataTransfer.append(item[0], item[1]); } } return dndController.handleDrop(treeDataTransfer, targetNode, new CancellationTokenSource().token, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles); From f86bbaa2a510b411ce4fb29da5766d8870880924 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 26 May 2022 07:36:10 -0700 Subject: [PATCH 086/270] Fix terminal find styles Fixes #150453 --- .../contrib/codeEditor/browser/find/simpleFindWidget.css | 3 --- .../workbench/contrib/terminal/browser/media/terminal.css | 7 +++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index b280e584779..cc120227164 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -9,7 +9,6 @@ position: absolute; top: 0; right: 18px; - width: 360px; max-width: calc(100% - 28px - 28px - 8px); pointer-events: none; padding: 0 10px 10px; @@ -29,7 +28,6 @@ align-items: center; pointer-events: all; transition: top 200ms linear; - min-width: 360px; } .monaco-workbench.reduce-motion .monaco-editor .find-widget { @@ -46,7 +44,6 @@ .monaco-workbench .simple-find-part .monaco-findInput { flex: 1; - min-width: 220px; } .monaco-workbench .simple-find-part .button { diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 945161cad7f..e3ec3613456 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -87,13 +87,12 @@ .monaco-workbench .simple-find-part-wrapper.result-count { z-index: 33 !important; - padding-right: 80px; } .result-count .simple-find-part { - width: 280px; - max-width: 280px; - min-width: 280px; + width: 330px; + max-width: 330px; + min-width: 330px; } .result-count .matchesCount { From 27e10113dc53dace848912a4975f15087e0c584e Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 26 May 2022 07:42:03 -0700 Subject: [PATCH 087/270] improve configure display language & add clear display language preference (#150433) --- .../contrib/localizationsUpdater.ts | 4 +- .../sharedProcess/sharedProcessMain.ts | 4 +- src/vs/code/node/cliProcessMain.ts | 4 +- .../languagePacks/common/languagePacks.ts | 76 +++++++- .../languagePacks/node/languagePacks.ts | 39 ++-- .../node/remoteExtensionHostAgentCli.ts | 4 +- src/vs/server/node/serverServices.ts | 4 +- .../browser/localizationsActions.ts | 166 ++++++++++++------ .../localization.contribution.ts | 20 +-- .../services/localization/common/locale.ts | 13 ++ .../electron-sandbox/localeService.ts | 36 ++++ src/vs/workbench/workbench.sandbox.main.ts | 1 + 12 files changed, 289 insertions(+), 82 deletions(-) create mode 100644 src/vs/workbench/services/localization/common/locale.ts create mode 100644 src/vs/workbench/services/localization/electron-sandbox/localeService.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts index 68085dcefcc..f035c7d8e4f 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts @@ -5,12 +5,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; -import { LanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; +import { NativeLanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; export class LocalizationsUpdater extends Disposable { constructor( - @ILanguagePackService private readonly localizationsService: LanguagePackService + @ILanguagePackService private readonly localizationsService: NativeLanguagePackService ) { super(); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index fb6cac109a9..3d5d20b8c71 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -47,7 +47,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { MessagePortMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; -import { LanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; +import { NativeLanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; import { ConsoleLogger, ILoggerService, ILogService, MultiplexLogService } from 'vs/platform/log/common/log'; import { FollowerLogService, LoggerChannelClient, LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; @@ -311,7 +311,7 @@ class SharedProcessMain extends Disposable { services.set(IExtensionTipsService, new SyncDescriptor(ExtensionTipsService)); // Localizations - services.set(ILanguagePackService, new SyncDescriptor(LanguagePackService)); + services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); // Diagnostics services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 88f9dc88a7a..32d5fb9c6a3 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -37,7 +37,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; -import { LanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; +import { NativeLanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; import { ConsoleLogger, getLogLevel, ILogger, ILogService, LogLevel, MultiplexLogService } from 'vs/platform/log/common/log'; import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; @@ -161,7 +161,7 @@ class CliMain extends Disposable { services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); // Localizations - services.set(ILanguagePackService, new SyncDescriptor(LanguagePackService)); + services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); // Telemetry const appenders: AppInsightsAppender[] = []; diff --git a/src/vs/platform/languagePacks/common/languagePacks.ts b/src/vs/platform/languagePacks/common/languagePacks.ts index 473e358148a..61c6c04553a 100644 --- a/src/vs/platform/languagePacks/common/languagePacks.ts +++ b/src/vs/platform/languagePacks/common/languagePacks.ts @@ -3,10 +3,84 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { language } from 'vs/base/common/platform'; +import { IQuickPickItem } from 'vs/base/parts/quickinput/common/quickInput'; +import { localize } from 'vs/nls'; +import { IExtensionGalleryService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const ILanguagePackService = createDecorator('languagePackService'); + +export interface ILanguagePackItem extends IQuickPickItem { + readonly extensionId: string; + readonly galleryExtension?: IGalleryExtension; +} + export interface ILanguagePackService { readonly _serviceBrand: undefined; - getInstalledLanguages(): Promise; + getAvailableLanguages(): Promise>; + getInstalledLanguages(): Promise>; +} + +export abstract class LanguagePackBaseService extends Disposable implements ILanguagePackService { + _serviceBrand: undefined; + + constructor(@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService) { + super(); + } + + abstract getInstalledLanguages(): Promise>; + + async getAvailableLanguages(): Promise { + const timeout = new CancellationTokenSource(); + setTimeout(() => timeout.cancel(), 1000); + + let result; + try { + result = await this.extensionGalleryService.query({ + text: 'category:"language packs"', + pageSize: 20 + }, timeout.token); + } catch (_) { + // This method is best effort. So, we ignore any errors. + return []; + } + + const languagePackExtensions = result.firstPage.filter(e => e.properties.localizedLanguages?.length && e.tags.some(t => t.startsWith('lp-'))); + const allFromMarketplace: ILanguagePackItem[] = languagePackExtensions.map(lp => { + const languageName = lp.properties.localizedLanguages?.[0]; + const locale = lp.tags.find(t => t.startsWith('lp-'))!.split('lp-')[1]; + const baseQuickPick = this.createQuickPickItem({ locale, label: languageName }); + return { + ...baseQuickPick, + extensionId: lp.identifier.id, + galleryExtension: lp + }; + }); + + allFromMarketplace.push({ + ...this.createQuickPickItem({ locale: 'en', label: 'English' }), + extensionId: 'default', + }); + + return allFromMarketplace; + } + + protected createQuickPickItem(languageItem: { locale: string; label?: string | undefined }): IQuickPickItem { + const label = languageItem.label ?? languageItem.locale; + let description: string | undefined = languageItem.locale !== languageItem.label ? languageItem.locale : undefined; + if (languageItem.locale.toLowerCase() === language.toLowerCase()) { + if (!description) { + description = ''; + } + description += localize('currentDisplayLanguage', " (Current)"); + } + return { + id: languageItem.locale, + label, + description + }; + } } diff --git a/src/vs/platform/languagePacks/node/languagePacks.ts b/src/vs/platform/languagePacks/node/languagePacks.ts index 009c936bdb0..369f4bb32f6 100644 --- a/src/vs/platform/languagePacks/node/languagePacks.ts +++ b/src/vs/platform/languagePacks/node/languagePacks.ts @@ -4,21 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { createHash } from 'crypto'; -import { distinct, equals } from 'vs/base/common/arrays'; +import { equals } from 'vs/base/common/arrays'; import { Queue } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { join } from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ILocalizationContribution } from 'vs/platform/extensions/common/extensions'; -import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; import { ILogService } from 'vs/platform/log/common/log'; +import { ILocalizationContribution } from 'vs/platform/extensions/common/extensions'; +import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'; interface ILanguagePack { hash: string; + label: string | undefined; extensions: { extensionIdentifier: IExtensionIdentifier; version: string; @@ -26,7 +27,7 @@ interface ILanguagePack { translations: { [id: string]: string }; } -export class LanguagePackService extends Disposable implements ILanguagePackService { +export class NativeLanguagePackService extends LanguagePackBaseService { declare readonly _serviceBrand: undefined; @@ -35,9 +36,10 @@ export class LanguagePackService extends Disposable implements ILanguagePackServ constructor( @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @INativeEnvironmentService environmentService: INativeEnvironmentService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService ) { - super(); + super(extensionGalleryService); this.cache = this._register(new LanguagePacksCache(environmentService, logService)); this.extensionManagementService.registerParticipant({ postInstall: async (extension: ILocalExtension): Promise => { @@ -49,11 +51,21 @@ export class LanguagePackService extends Disposable implements ILanguagePackServ }); } - async getInstalledLanguages(): Promise { + async getInstalledLanguages(): Promise> { const languagePacks = await this.cache.getLanguagePacks(); - // Contributed languages are those installed via extension packs, so does not include English - const languages = ['en', ...Object.keys(languagePacks)]; - return distinct(languages); + const languages = Object.keys(languagePacks).map(locale => { + const languagePack = languagePacks[locale]; + const baseQuickPick = this.createQuickPickItem({ locale, label: languagePack.label }); + return { + ...baseQuickPick, + extensionId: languagePack.extensions[0].extensionIdentifier.id, + }; + }); + languages.push({ + ...this.createQuickPickItem({ locale: 'en', label: 'English' }), + extensionId: 'default', + }); + return languages; } private async postInstallExtension(extension: ILocalExtension): Promise { @@ -126,7 +138,12 @@ class LanguagePacksCache extends Disposable { if (extension.location.scheme === Schemas.file && isValidLocalization(localizationContribution)) { let languagePack = languagePacks[localizationContribution.languageId]; if (!languagePack) { - languagePack = { hash: '', extensions: [], translations: {} }; + languagePack = { + hash: '', + extensions: [], + translations: {}, + label: localizationContribution.localizedLanguageName ?? localizationContribution.languageName + }; languagePacks[localizationContribution.languageId] = languagePack; } let extensionInLanguagePack = languagePack.extensions.filter(e => areSameExtensions(e.extensionIdentifier, extensionIdentifier))[0]; diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 2d8bde94e63..898ff7d56b9 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -29,7 +29,7 @@ import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/ import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/serverEnvironmentService'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; -import { LanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; +import { NativeLanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; import { getErrorMessage } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { isAbsolute, join } from 'vs/base/common/path'; @@ -104,7 +104,7 @@ class CliMain extends Disposable { services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); - services.set(ILanguagePackService, new SyncDescriptor(LanguagePackService)); + services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); return new InstantiationService(services); } diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 9a4bdee0c1a..a1f9cd11405 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -36,7 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; -import { LanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; +import { NativeLanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; import { AbstractLogger, DEFAULT_LOG_LEVEL, getLogLevel, ILogService, LogLevel, LogService, MultiplexLogService } from 'vs/platform/log/common/log'; import { LogLevelChannel } from 'vs/platform/log/common/logIpc'; import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; @@ -159,7 +159,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); - services.set(ILanguagePackService, instantiationService.createInstance(LanguagePackService)); + services.set(ILanguagePackService, instantiationService.createInstance(NativeLanguagePackService)); const extensionManagementCLIService = instantiationService.createInstance(ExtensionManagementCLIService); services.set(IExtensionManagementCLIService, extensionManagementCLIService); diff --git a/src/vs/workbench/contrib/localization/browser/localizationsActions.ts b/src/vs/workbench/contrib/localization/browser/localizationsActions.ts index afebaf8f1e1..45df8ab135e 100644 --- a/src/vs/workbench/contrib/localization/browser/localizationsActions.ts +++ b/src/vs/workbench/contrib/localization/browser/localizationsActions.ts @@ -4,88 +4,154 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { language } from 'vs/base/common/platform'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILanguagePackItem, ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; +import { ILocaleService } from 'vs/workbench/services/localization/common/locale'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { isWeb } from 'vs/base/common/platform'; + +const restart = localize('restart', "&&Restart"); + +export class ConfigureDisplayLanguageAction extends Action2 { + public static readonly ID = 'workbench.action.configureLocale'; + public static readonly LABEL = localize('configureLocale', "Configure Display Language"); -export class ConfigureLocaleAction extends Action2 { constructor() { super({ - id: 'workbench.action.configureLocale', - title: { original: 'Configure Display Language', value: localize('configureLocale', "Configure Display Language") }, + id: ConfigureDisplayLanguageAction.ID, + title: { original: 'Configure Display Language', value: ConfigureDisplayLanguageAction.LABEL }, menu: { id: MenuId.CommandPalette } }); } - private async getLanguageOptions(localizationService: ILanguagePackService): Promise { - const availableLanguages = await localizationService.getInstalledLanguages(); - availableLanguages.sort(); - - return availableLanguages - .map(language => { return { label: language }; }) - .concat({ label: localize('installAdditionalLanguages', "Install Additional Languages...") }); - } - - public override async run(accessor: ServicesAccessor): Promise { - const environmentService: IEnvironmentService = accessor.get(IEnvironmentService); + public async run(accessor: ServicesAccessor): Promise { const languagePackService: ILanguagePackService = accessor.get(ILanguagePackService); const quickInputService: IQuickInputService = accessor.get(IQuickInputService); - const jsonEditingService: IJSONEditingService = accessor.get(IJSONEditingService); const hostService: IHostService = accessor.get(IHostService); - const notificationService: INotificationService = accessor.get(INotificationService); - const paneCompositeService: IPaneCompositePartService = accessor.get(IPaneCompositePartService); const dialogService: IDialogService = accessor.get(IDialogService); const productService: IProductService = accessor.get(IProductService); + const localeService: ILocaleService = accessor.get(ILocaleService); + const extensionManagementService: IExtensionManagementService = accessor.get(IExtensionManagementService); + const paneCompositePartService: IPaneCompositePartService = accessor.get(IPaneCompositePartService); + const notificationService: INotificationService = accessor.get(INotificationService); - const languageOptions = await this.getLanguageOptions(languagePackService); - const currentLanguageIndex = languageOptions.findIndex(l => l.label === language); + const installedLanguages = await languagePackService.getInstalledLanguages(); - try { - const selectedLanguage = await quickInputService.pick(languageOptions, - { - canPickMany: false, - placeHolder: localize('chooseDisplayLanguage', "Select Display Language"), - activeItem: languageOptions[currentLanguageIndex] - }); + const qp = quickInputService.createQuickPick(); + qp.placeholder = localize('chooseLocale', "Select Display Language"); - if (selectedLanguage === languageOptions[languageOptions.length - 1]) { - return paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer()) - .then(viewlet => { - const extensionsViewlet = viewlet as IExtensionsViewPaneContainer; - extensionsViewlet.search('@category:"language packs"'); - extensionsViewlet.focus(); - }); + if (installedLanguages?.length) { + const items: Array = [{ type: 'separator', label: localize('installed', "Installed languages") }]; + qp.items = items.concat(installedLanguages); + } + + const disposables = new DisposableStore(); + const source = new CancellationTokenSource(); + disposables.add(qp.onDispose(() => { + source.cancel(); + disposables.dispose(); + })); + + const installedSet = new Set(installedLanguages?.map(language => language.id!) ?? []); + languagePackService.getAvailableLanguages().then(availableLanguages => { + const newLanguages = availableLanguages.filter(l => l.id && !installedSet.has(l.id)); + if (newLanguages.length) { + qp.items = [ + ...qp.items, + { type: 'separator', label: localize('available', "Available languages") }, + ...newLanguages + ]; + } + qp.busy = false; + }); + + disposables.add(qp.onDidAccept(async () => { + const selectedLanguage = qp.activeItems[0]; + qp.hide(); + + // Only Desktop has the concept of installing language packs so we only do this for Desktop + // and only if the language pack is not installed + if (!isWeb && !installedSet.has(selectedLanguage.id!)) { + try { + // Show the view so the user can see the language pack to be installed + let viewlet = await paneCompositePartService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar); + (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(`@id:${selectedLanguage.extensionId}`); + + // Only actually install a language pack from Microsoft + if (selectedLanguage.galleryExtension?.publisher.toLowerCase() !== 'ms-ceintl') { + return; + } + + await extensionManagementService.installFromGallery(selectedLanguage.galleryExtension); + } catch (err) { + notificationService.error(err); + return; + } } - if (selectedLanguage) { - await jsonEditingService.write(environmentService.argvResource, [{ path: ['locale'], value: selectedLanguage.label }], true); - const restart = await dialogService.confirm({ + if (await localeService.setLocale(selectedLanguage.id!)) { + const restartDialog = await dialogService.confirm({ type: 'info', message: localize('relaunchDisplayLanguageMessage', "A restart is required for the change in display language to take effect."), detail: localize('relaunchDisplayLanguageDetail', "Press the restart button to restart {0} and change the display language.", productService.nameLong), - primaryButton: localize('restart', "&&Restart") + primaryButton: restart }); - if (restart.confirmed) { + if (restartDialog.confirmed) { hostService.restart(); } } - } catch (e) { - notificationService.error(e); + })); + + qp.show(); + qp.busy = true; + } +} + +export class ClearDisplayLanguageAction extends Action2 { + public static readonly ID = 'workbench.action.clearLocalePreference'; + public static readonly LABEL = localize('clearDisplayLanguage', "Clear Display Language Preference"); + + constructor() { + super({ + id: ClearDisplayLanguageAction.ID, + title: { original: 'Clear Display Language Preference', value: ClearDisplayLanguageAction.LABEL }, + menu: { + id: MenuId.CommandPalette + } + }); + } + + public async run(accessor: ServicesAccessor): Promise { + const localeService: ILocaleService = accessor.get(ILocaleService); + const dialogService: IDialogService = accessor.get(IDialogService); + const productService: IProductService = accessor.get(IProductService); + const hostService: IHostService = accessor.get(IHostService); + + if (await localeService.setLocale(undefined)) { + const restartDialog = await dialogService.confirm({ + type: 'info', + message: localize('relaunchAfterClearDisplayLanguageMessage', "A restart is required for the change in display language to take effect."), + detail: localize('relaunchAfterClearDisplayLanguageDetail', "Press the restart button to restart {0} and change the display language.", productService.nameLong), + primaryButton: restart + }); + + if (restartDialog.confirmed) { + hostService.restart(); + } } } } diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts index c725565a55d..b04105ab3c9 100644 --- a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts @@ -24,10 +24,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { ConfigureLocaleAction } from 'vs/workbench/contrib/localization/browser/localizationsActions'; +import { ClearDisplayLanguageAction, ConfigureDisplayLanguageAction } from 'vs/workbench/contrib/localization/browser/localizationsActions'; // Register action to configure locale and related settings -registerAction2(ConfigureLocaleAction); +registerAction2(ConfigureDisplayLanguageAction); +registerAction2(ClearDisplayLanguageAction); const LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY = 'extensionsAssistant/languagePackSuggestionIgnore'; @@ -195,14 +196,13 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo } - private isLanguageInstalled(language: string | undefined): Promise { - return this.extensionManagementService.getInstalled() - .then(installed => installed.some(i => - !!(i.manifest - && i.manifest.contributes - && i.manifest.contributes.localizations - && i.manifest.contributes.localizations.length - && i.manifest.contributes.localizations.some(l => l.languageId.toLowerCase() === language)))); + private async isLanguageInstalled(language: string | undefined): Promise { + const installed = await this.extensionManagementService.getInstalled(); + return installed.some(i => !!(i.manifest + && i.manifest.contributes + && i.manifest.contributes.localizations + && i.manifest.contributes.localizations.length + && i.manifest.contributes.localizations.some(l => l.languageId.toLowerCase() === language))); } private installExtension(extension: IGalleryExtension): Promise { diff --git a/src/vs/workbench/services/localization/common/locale.ts b/src/vs/workbench/services/localization/common/locale.ts new file mode 100644 index 00000000000..1b67af98c04 --- /dev/null +++ b/src/vs/workbench/services/localization/common/locale.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ILocaleService = createDecorator('localizationService'); + +export interface ILocaleService { + readonly _serviceBrand: undefined; + setLocale(languagePackItem: string | undefined): Promise; +} diff --git a/src/vs/workbench/services/localization/electron-sandbox/localeService.ts b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts new file mode 100644 index 00000000000..d435b3725d5 --- /dev/null +++ b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { language } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { ILocaleService } from 'vs/workbench/services/localization/common/locale'; + +export class NativeLocaleService implements ILocaleService { + _serviceBrand: undefined; + + constructor( + @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @INotificationService private readonly notificationService: INotificationService, + ) { } + + async setLocale(locale: string | undefined): Promise { + try { + if (locale === language || (!locale && language === 'en')) { + return false; + } + await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true); + return true; + } catch (err) { + this.notificationService.error(err); + return false; + } + } +} + +registerSingleton(ILocaleService, NativeLocaleService, true); diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index dd9ddd715b1..41435143c48 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -83,6 +83,7 @@ import 'vs/workbench/services/files/electron-sandbox/elevatedFileService'; import 'vs/workbench/services/search/electron-sandbox/searchService'; import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; +import 'vs/workbench/services/localization/electron-sandbox/localeService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; From cc86b15a44fe28300180108f4d95cacee98717dc Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 26 May 2022 08:19:15 -0700 Subject: [PATCH 088/270] Enable grabbing translations from an alternate location for server distro/serverful scenarios (#150436) * nls web story * better handling of urls * clean up code and don't do nls in dev * use version instead of quality * revert changes in workbench-dev.html * update nls from changes in vscode-loader * sanitize url a bit * revert loader change --- src/vs/base/common/product.ts | 1 + src/vs/code/browser/workbench/workbench.html | 16 +++++++++++++++- src/vs/server/node/webClientServer.ts | 7 ++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index c66cf41aa6c..731c77b8893 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -74,6 +74,7 @@ export interface IProductConfiguration { readonly resourceUrlTemplate: string; readonly controlUrl: string; readonly recommendationsUrl: string; + readonly nlsBaseUrl: string; }; readonly extensionTips?: { [id: string]: string }; diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 2e4eddb19b4..d8914408e4a 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -40,6 +40,19 @@ Object.keys(self.webPackagePaths).map(function (key, index) { self.webPackagePaths[key] = `${baseUrl}/node_modules/${key}/${self.webPackagePaths[key]}`; }); + + // Set up nls if the user is not using the default language (English) + const nlsConfig = {}; + const locale = navigator.language; + if (!locale.startsWith('en')) { + nlsConfig['vs/nls'] = { + availableLanguages: { + '*': locale + }, + baseUrl: '{{WORKBENCH_NLS_BASE_URL}}' + }; + } + require.config({ baseUrl: `${baseUrl}/out`, recordStats: true, @@ -48,7 +61,8 @@ return value; } }), - paths: self.webPackagePaths + paths: self.webPackagePaths, + ...nlsConfig });