diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index a29d78b2167..cf74aac44be 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -74,6 +74,10 @@ "name": "vs/workbench/contrib/dialogs", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/multiDiffEditor", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/emmet", "project": "vscode-workbench" diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 0baec502572..e2203ec77b1 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -417,6 +417,7 @@ "--vscode-minimapSlider-activeBackground", "--vscode-minimapSlider-background", "--vscode-minimapSlider-hoverBackground", + "--vscode-multiDiffEditor-headerBackground", "--vscode-notebook-cellBorderColor", "--vscode-notebook-cellEditorBackground", "--vscode-notebook-cellHoverBackground", diff --git a/extensions/git/package.json b/extensions/git/package.json index 8b1df39dd43..7a20e7ffef1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -694,6 +694,12 @@ "title": "%command.timelineCompareWithSelected%", "category": "Git" }, + { + "command": "git.timeline.openCommit", + "title": "%command.timelineOpenCommit%", + "icon": "$(wand)", + "category": "Git" + }, { "command": "git.rebaseAbort", "title": "%command.rebaseAbort%", @@ -753,6 +759,18 @@ "command": "git.openRepositoriesInParentFolders", "title": "%command.openRepositoriesInParentFolders%", "category": "Git" + }, + { + "command": "git.viewChanges", + "title": "View Changes", + "icon": "$(search)", + "category": "Git" + }, + { + "command": "git.viewStagedChanges", + "title": "View Staged Changes", + "icon": "$(search)", + "category": "Git" } ], "continueEditSession": [ @@ -1197,6 +1215,10 @@ "command": "git.timeline.compareWithSelected", "when": "false" }, + { + "command": "git.timeline.openCommit", + "when": "false" + }, { "command": "git.closeAllDiffEditors", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1224,6 +1246,14 @@ { "command": "git.openRepositoriesInParentFolders", "when": "config.git.enabled && !git.missing && git.parentRepositoryCount != 0" + }, + { + "command": "git.viewChanges", + "when": "false" + }, + { + "command": "git.viewStagedChanges", + "when": "false" } ], "scm/title": [ @@ -1390,6 +1420,16 @@ "command": "git.stageAllUntracked", "when": "scmProvider == git && scmResourceGroup == untracked", "group": "inline@2" + }, + { + "command": "git.viewStagedChanges", + "when": "scmProvider == git && scmResourceGroup == index", + "group": "inline@1" + }, + { + "command": "git.viewChanges", + "when": "scmProvider == git && scmResourceGroup == workingTree", + "group": "inline@1" } ], "scm/resourceFolder/context": [ @@ -1767,11 +1807,21 @@ } ], "timeline/item/context": [ + { + "command": "git.timeline.openCommit", + "group": "inline", + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection" + }, { "command": "git.timeline.openDiff", - "group": "1_actions", + "group": "1_actions@1", "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file\\b/ && !listMultiSelection" }, + { + "command": "git.timeline.openCommit", + "group": "1_actions@2", + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection" + }, { "command": "git.timeline.compareWithSelected", "group": "3_compare@1", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 20a807be2cb..5f454ae4008 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -104,6 +104,7 @@ "command.stashDrop": "Drop Stash...", "command.stashDropAll": "Drop All Stashes...", "command.timelineOpenDiff": "Open Changes", + "command.timelineOpenCommit": "Open Commit", "command.timelineCopyCommitId": "Copy Commit ID", "command.timelineCopyCommitMessage": "Copy Commit Message", "command.timelineSelectForCompare": "Select for Compare", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f6b625a71de..89ca2519c5a 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3426,6 +3426,54 @@ export class CommandCenter { }; } + @command('git.timeline.openCommit', { repository: false }) + async timelineOpenCommit(item: TimelineItem, uri: Uri | undefined, _source: string) { + console.log('timelineOpenCommit', item); + if (!GitTimelineItem.is(item)) { + return; + } + + const cmd = await this._resolveTimelineOpenCommitCommand( + item, uri, + { + preserveFocus: true, + preview: true, + viewColumn: ViewColumn.Active + }, + ); + if (cmd === undefined) { + return undefined; + } + + return commands.executeCommand(cmd.command, ...(cmd.arguments ?? [])); + } + + private async _resolveTimelineOpenCommitCommand(item: TimelineItem, uri: Uri | undefined, options?: TextDocumentShowOptions): Promise { + if (uri === undefined || uri === null || !GitTimelineItem.is(item)) { + return undefined; + } + + const repository = await this.model.getRepository(uri.fsPath); + if (!repository) { + return undefined; + } + + const commit = await repository.getCommit(item.ref); + const commitFiles = await repository.getCommitFiles(item.ref); + + const args: [Uri, Uri | undefined, Uri | undefined][] = []; + for (const commitFile of commitFiles) { + const commitFileUri = Uri.file(path.join(repository.root, commitFile)); + args.push([commitFileUri, toGitUri(commitFileUri, item.previousRef), toGitUri(commitFileUri, item.ref)]); + } + + return { + command: 'vscode.changes', + title: l10n.t('Open Commit'), + arguments: [`${item.shortRef} - ${commit.message}`, args, options] + }; + } + @command('git.timeline.copyCommitId', { repository: false }) async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) { if (!GitTimelineItem.is(item)) { @@ -3603,6 +3651,26 @@ export class CommandCenter { repository.generateCommitMessageCancel(); } + @command('git.viewChanges', { repository: true }) + viewChanges(repository: Repository): void { + this._viewChanges('Changes', repository.workingTreeGroup.resourceStates); + } + + @command('git.viewStagedChanges', { repository: true }) + viewStagedChanges(repository: Repository): void { + this._viewChanges('Staged Changes', repository.indexGroup.resourceStates); + } + + private _viewChanges(title: string, resources: Resource[]): void { + const args: [Uri, Uri | undefined, Uri | undefined][] = []; + + for (const resource of resources) { + args.push([resource.resourceUri, resource.leftUri, resource.rightUri]); + } + + commands.executeCommand('vscode.changes', title, args); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 421cc14b752..f072af20935 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2552,6 +2552,11 @@ export class Repository { return commits[0]; } + async getCommitFiles(ref: string): Promise { + const result = await this.exec(['diff-tree', '--no-commit-id', '--name-only', '-r', ref]); + return result.stdout.split('\n').filter(l => !!l); + } + async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { const result = await this.exec(['rev-list', '--count', '--left-right', range]); const [ahead, behind] = result.stdout.trim().split('\t'); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 21859a62434..cebecfd239a 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1658,6 +1658,10 @@ export class Repository implements Disposable { return await this.repository.getCommit(ref); } + async getCommitFiles(ref: string): Promise { + return await this.repository.getCommitFiles(ref); + } + async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { return await this.run(Operation.RevList, () => this.repository.getCommitCount(range)); } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 37914a70335..c38ac1d72cd 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -358,6 +358,11 @@ export class ViewZones extends ViewPart { let hasVisibleZone = false; for (const visibleWhitespace of visibleWhitespaces) { + if (!this._zones[visibleWhitespace.id]) { + // This zone got deleted in the meantime? + // TODO@hediet: This should not happen. + continue; + } if (this._zones[visibleWhitespace.id].isInHiddenArea) { continue; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index b4cd16d44bd..7ec1c797edb 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1491,7 +1491,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } this._overlayWidgets[widget.getId()] = widgetData; - if (this._modelData && this._modelData.hasRealView) { this._modelData.view.addOverlayWidget(widgetData); } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts index b6d518d4c1c..1b76ed9df95 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorEditors.ts @@ -143,7 +143,6 @@ export class DiffEditorEditors extends Disposable { // Clone scrollbar options before changing them clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) }; - clonedOptions.scrollbar.vertical = 'visible'; clonedOptions.folding = false; clonedOptions.codeLens = this._options.diffCodeLens.get(); clonedOptions.fixedOverflowWidgets = true; diff --git a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts index 3fe7ebb61d4..6f548bbfa5e 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts @@ -17,6 +17,7 @@ export interface IDocumentDiffProviderOptions { export interface IDiffProviderFactoryService { readonly _serviceBrand: undefined; + // TODO, don't include IDiffEditor createDiffProvider(editor: IDiffEditor, options: IDocumentDiffProviderOptions): IDocumentDiffProvider; } diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/colors.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/colors.ts new file mode 100644 index 00000000000..5b69dd6c1df --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/colors.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 { localize } from 'vs/nls'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; + +export const multiDiffEditorHeaderBackground = registerColor( + 'multiDiffEditor.headerBackground', + { dark: '#808080', light: '#b4b4b4', hcDark: '#808080', hcLight: '#b4b4b4', }, + localize('multiDiffEditor.headerBackground', 'The background color of the diff editor\'s header') +); diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/model.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/model.ts new file mode 100644 index 00000000000..4de8c1a0280 --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/model.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { ITextModel } from 'vs/editor/common/model'; + +export interface IMultiDocumentDiffEditorModel { + readonly diffs: LazyPromise[]; + readonly onDidChange: Event; +} + +export interface LazyPromise { + request(): Promise; + readonly value: T | undefined; + readonly onHasValueDidChange: Event; +} + +export class ConstLazyPromise implements LazyPromise { + public readonly onHasValueDidChange = Event.None; + + constructor( + private readonly _value: T + ) { } + + public request(): Promise { + return Promise.resolve(this._value); + } + + public get value(): T { + return this._value; + } +} + +export interface IDiffEntry { + readonly title: string; + readonly original: ITextModel | undefined; // undefined if the file was created. + readonly modified: ITextModel | undefined; // undefined if the file was deleted. +} diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts new file mode 100644 index 00000000000..e59063af53b --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension } from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; +import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; +import { IMultiDocumentDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/model'; +import { MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import './colors'; + +export class MultiDiffEditorWidget extends Disposable { + private readonly _dimension = observableValue(this, undefined); + private readonly _model = observableValue(this, undefined); + + private readonly widgetImpl = derivedWithStore(this, (reader, store) => { + return store.add(this._instantiationService.createInstance(( + readHotReloadableExport(MultiDiffEditorWidgetImpl, reader)), + this._element, + this._dimension, + this._model, + )); + }); + + constructor( + private readonly _element: HTMLElement, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._register(recomputeInitiallyAndOnChange(this.widgetImpl)); + } + + public setModel(model: IMultiDocumentDiffEditorModel): void { + this._model.set(model, undefined); + } + + public layout(dimension: Dimension): void { + this._dimension.set(dimension, undefined); + } +} diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts new file mode 100644 index 00000000000..c1c3d74dd52 --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts @@ -0,0 +1,292 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension, getWindow, h, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Disposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, IReader, ISettableObservable, autorun, constObservable, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { globalTransaction } from 'vs/base/common/observableInternal/base'; +import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import 'vs/css!./style'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { ObservableElementSizeObserver } from 'vs/editor/browser/widget/diffEditor/utils'; +import { IDiffEntry, IMultiDocumentDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditorWidget/model'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ObjectPool } from './objectPool'; + +export class MultiDiffEditorWidgetImpl extends Disposable { + private readonly _elements = h('div', { + style: { + overflowY: 'hidden', + } + }, [ + h('div@content', { + style: { + overflow: 'hidden', + } + }) + ]); + + private readonly _sizeObserver = new ObservableElementSizeObserver(this._element, undefined); + private readonly _documentsObs = this._model.map(this, m => !m ? constObservable([]) : observableFromEvent(m.onDidChange, /** @description Documents changed */() => m.diffs)); + private readonly _documents = this._documentsObs.map(this, (m, reader) => m.read(reader)); + + private readonly _objectPool = new ObjectPool(() => this._instantiationService.createInstance(DiffEditorItemTemplate, this._elements.content)); + + private readonly _hiddenContainer = document.createElement('div'); + + private readonly _editor = this._register(this._instantiationService.createInstance(DiffEditorWidget, this._hiddenContainer, { + hideUnchangedRegions: { + enabled: true, + }, + }, {})); + + private readonly _viewItems = this._documents.map(this, + docs => docs.map(d => new DiffEditorItem(this._objectPool, d, this._editor)) + ); + + private readonly _totalHeight = this._viewItems.map(this, (items, reader) => items.reduce((r, i) => r + i.contentHeight.read(reader), 0)); + + private readonly scrollTop: IObservable; + + constructor( + private readonly _element: HTMLElement, + private readonly _dimension: IObservable, + private readonly _model: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._sizeObserver.setAutomaticLayout(true); + + this._register(autorun((reader) => { + /** @description Update widget dimension */ + const dimension = this._dimension.read(reader); + this._sizeObserver.observe(dimension); + })); + + const scrollable = this._register(new Scrollable({ + forceIntegerValues: false, + scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(_element), cb), + smoothScrollDuration: 100, + })); + + this._elements.content.style.position = 'relative'; + + const scrollableElement = this._register(new SmoothScrollableElement(this._elements.root, { + vertical: ScrollbarVisibility.Auto, + className: 'monaco-component', + }, scrollable)); + + this.scrollTop = observableFromEvent(scrollableElement.onScroll, () => /** @description onScroll */ scrollableElement.getScrollPosition().scrollTop); + + this._register(autorun((reader) => { + /** @description Update scroll dimensions */ + const height = this._sizeObserver.height.read(reader); + this._elements.root.style.height = `${height}px`; + const totalHeight = this._totalHeight.read(reader); + this._elements.content.style.height = `${totalHeight}px`; + scrollableElement.setScrollDimensions({ + width: _element.clientWidth, + height: height, + scrollHeight: totalHeight, + scrollWidth: _element.clientWidth, + }); + })); + + _element.replaceChildren(scrollableElement.getDomNode()); + this._register(toDisposable(() => { + _element.replaceChildren(); + })); + + this._register(this._register(autorun(reader => { + /** @description Render all */ + this.render(reader); + }))); + } + + private render(reader: IReader | undefined) { + const scrollTop = this.scrollTop.read(reader); + let contentScrollOffsetToScrollOffset = 0; + let itemHeightSumBefore = 0; + let itemContentHeightSumBefore = 0; + const viewPortHeight = this._elements.root.clientHeight; + const contentViewPort = OffsetRange.ofStartAndLength(scrollTop, viewPortHeight); + + const width = this._elements.content.clientWidth; + + for (const v of this._viewItems.read(reader)) { + const itemContentHeight = v.contentHeight.read(reader); + const itemHeight = Math.min(itemContentHeight, viewPortHeight); + const itemRange = OffsetRange.ofStartAndLength(itemHeightSumBefore, itemHeight); + const itemContentRange = OffsetRange.ofStartAndLength(itemContentHeightSumBefore, itemContentHeight); + + if (itemContentRange.isBefore(contentViewPort)) { + contentScrollOffsetToScrollOffset -= itemContentHeight - itemHeight; + v.hide(); + } else if (itemContentRange.isAfter(contentViewPort)) { + v.hide(); + } else { + const scroll = Math.max(0, Math.min(contentViewPort.start - itemContentRange.start, itemContentHeight - itemHeight)); + v.render(itemRange, scroll, width); + contentScrollOffsetToScrollOffset -= scroll; + } + + itemHeightSumBefore += itemHeight; + itemContentHeightSumBefore += itemContentHeight; + } + + this._elements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; + } +} + +class DiffEditorItem { + private readonly _height = observableValue(this, 500); + public readonly contentHeight: IObservable = this._height; + private _templateRef: IReference | undefined; + + private _vm: IDiffEditorViewModel | undefined; + + constructor( + private readonly _objectPool: ObjectPool, + private readonly _entry: LazyPromise, + baseDiffEditorWidget: DiffEditorWidget, + ) { + this._vm = baseDiffEditorWidget.createViewModel({ + original: _entry.value!.original!, + modified: _entry.value!.modified!, + }); + } + + public toString(): string { + return `ViewItem(${this._entry.value!.title})`; + } + + public hide(): void { + this._templateRef?.object.hide(); + this._templateRef?.dispose(); + this._templateRef = undefined; + } + + public render(verticalSpace: OffsetRange, offset: number, width: number): void { + if (!this._templateRef) { + this._templateRef = this._objectPool.getUnusedObj(); + this._templateRef.object.setData({ height: this._height, viewModel: this._vm!, entry: this._entry.value! }); + } + this._templateRef.object.render(verticalSpace, width, offset); + } +} + +class DiffEditorItemTemplate extends Disposable { + private _height: number = 500; + private _heightObs: ISettableObservable | undefined = undefined; + + private readonly _elements = h('div', { + style: { + display: 'flex', + flexDirection: 'column', + + } + }, [ + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + + flex: '1', + border: '1px solid #4d4d4d', + borderRadius: '5px', + overflow: 'hidden', + margin: '10px 10px 10px 10px', + } + }, [ + + h('div', { style: { display: 'flex', alignItems: 'center', padding: '8px 5px', background: 'var(--vscode-multiDiffEditor-headerBackground)', color: 'black' } }, [ + //h('div.expand-button@collapseButton', { style: { margin: '0 5px' } }), + h('div@title', { style: { fontSize: '14px' } }, ['Title'] as any), + ]), + + h('div', { + style: { + flex: '1', + display: 'flex', + flexDirection: 'column', + } + }, [ + h('div@editor', { style: { flex: '1' } }), + ]) + + ]) + ]); + + private readonly editor = this._register(this._instantiationService.createInstance(DiffEditorWidget, this._elements.editor, { + automaticLayout: true, + scrollBeyondLastLine: false, + hideUnchangedRegions: { + enabled: true, + }, + scrollbar: { + vertical: 'hidden', + handleMouseWheel: false, + }, + renderOverviewRuler: false, + }, { + modifiedEditor: { + contributions: [], + }, + originalEditor: { + contributions: [], + } + })); + + constructor( + private readonly _container: HTMLElement, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService, + ) { + super(); + + // TODO@hediet + /* + const btn = new Button(this._elements.collapseButton, {}); + btn.icon = Codicon.chevronDown; + */ + + this._register(this.editor.onDidContentSizeChange(e => { + this._height = e.contentHeight + this._elements.root.clientHeight - this._elements.editor.clientHeight; + globalTransaction(tx => { + this._heightObs?.set(this._height, tx); + }); + })); + + this._container.appendChild(this._elements.root); + } + + public setData(data: { height: ISettableObservable; viewModel: IDiffEditorViewModel; entry: IDiffEntry }) { + this._heightObs = data.height; + globalTransaction(tx => { + this.editor.setModel(data.viewModel, tx); + this._heightObs!.set(this._height, tx); + }); + this._elements.title.innerText = this._labelService.getUriLabel(data.viewModel.model.modified.uri, { relative: true }); // data.entry.title; + } + + public hide(): void { + this._elements.root.style.top = `-100000px`; + this._elements.root.style.visibility = 'hidden'; // Some editor parts are still visible + } + + public render(verticalRange: OffsetRange, width: number, editorScroll: number): void { + this._elements.root.style.visibility = 'visible'; + this._elements.root.style.top = `${verticalRange.start}px`; + this._elements.root.style.height = `${verticalRange.length}px`; + this._elements.root.style.width = `${width}px`; + this._elements.root.style.position = 'absolute'; + this.editor.getOriginalEditor().setScrollTop(editorScroll); + } +} diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/objectPool.ts b/src/vs/editor/browser/widget/multiDiffEditorWidget/objectPool.ts new file mode 100644 index 00000000000..95954589a40 --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/objectPool.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 { IDisposable, IReference } from 'vs/base/common/lifecycle'; + +export class ObjectPool { + private readonly _unused = new Set(); + private readonly _used = new Set(); + + constructor( + private readonly _create: () => T + ) { } + + public getUnusedObj(): IReference { + let obj: T; + if (this._unused.size === 0) { + obj = this._create(); + } else { + obj = this._unused.values().next().value; + this._unused.delete(obj); + } + this._used.add(obj); + return { + object: obj, + dispose: () => { + this._used.delete(obj); + if (this._unused.size > 5) { + obj.dispose(); + } else { + this._unused.add(obj); + } + } + }; + } +} diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css b/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css new file mode 100644 index 00000000000..7206dda8cf6 --- /dev/null +++ b/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-component .expand-button a { + display: block; +} diff --git a/src/vs/editor/common/core/offsetRange.ts b/src/vs/editor/common/core/offsetRange.ts index 7e78d30b591..5efa09faecc 100644 --- a/src/vs/editor/common/core/offsetRange.ts +++ b/src/vs/editor/common/core/offsetRange.ts @@ -43,6 +43,10 @@ export class OffsetRange implements IOffsetRange { return new OffsetRange(0, length); } + public static ofStartAndLength(start: number, length: number): OffsetRange { + return new OffsetRange(start, start + length); + } + constructor(public readonly start: number, public readonly endExclusive: number) { if (start > endExclusive) { throw new BugIndicatingError(`Invalid range: ${this.toString()}`); @@ -114,6 +118,14 @@ export class OffsetRange implements IOffsetRange { return start <= end; } + public isBefore(other: OffsetRange): boolean { + return this.endExclusive <= other.start; + } + + public isAfter(other: OffsetRange): boolean { + return this.start >= other.endExclusive; + } + public slice(arr: T[]): T[] { return arr.slice(this.start, this.endExclusive); } diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 92cf32cf2d5..903e20e1151 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -38,6 +38,7 @@ import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarker, IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget'; /** * Create a new editor under `domElement`. @@ -98,6 +99,11 @@ export function createDiffEditor(domElement: HTMLElement, options?: IStandaloneD return instantiationService.createInstance(StandaloneDiffEditor2, domElement, options); } +export function createMultiFileDiffEditor(domElement: HTMLElement, override?: IEditorOverrideServices) { + const instantiationService = StandaloneServices.initialize(override || {}); + return new MultiDiffEditorWidget(domElement, instantiationService); +} + /** * Description of a command contribution */ @@ -567,6 +573,8 @@ export function createMonacoEditorAPI(): typeof monaco.editor { ApplyUpdateResult: ApplyUpdateResult, EditorZoom: EditorZoom, + createMultiFileDiffEditor: createMultiFileDiffEditor, + // vars EditorType: EditorType, EditorOptions: EditorOptions diff --git a/src/vs/editor/standalone/browser/standaloneThemeService.ts b/src/vs/editor/standalone/browser/standaloneThemeService.ts index 0d1810873da..5fc765ed2d1 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeService.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeService.ts @@ -393,7 +393,7 @@ export class StandaloneThemeService extends Disposable implements IStandaloneThe colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`); } } - ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor { ${colorVariables.join('\n')} }`); + ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { ${colorVariables.join('\n')} }`); const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e45bb5af784..eee22193298 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -974,6 +974,8 @@ declare namespace monaco.editor { */ export function createDiffEditor(domElement: HTMLElement, options?: IStandaloneDiffEditorConstructionOptions, override?: IEditorOverrideServices): IStandaloneDiffEditor; + export function createMultiFileDiffEditor(domElement: HTMLElement, override?: IEditorOverrideServices): any; + /** * Description of a command contribution */ diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index e79c39ceee6..7412cb9168a 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -432,6 +432,31 @@ const newCommands: ApiCommand[] = [ ], ApiCommandResult.Void ), + new ApiCommand( + 'vscode.changes', '_workbench.changes', 'Opens a list of resources in the changes editor to compare their contents.', + [ + ApiCommandArgument.String.with('title', 'Human readable title for the changes editor'), + new ApiCommandArgument<[URI, URI?, URI?][]>('resourceList', 'List of resources to compare', + resources => { + for (const resource of resources) { + if (resource.length !== 3) { + return false; + } + + const [label, left, right] = resource; + if (!URI.isUri(label) || + (!URI.isUri(left) && left !== undefined && left !== null) || + (!URI.isUri(right) && right !== undefined && right !== null)) { + return false; + } + } + + return true; + }, + v => v) + ], + ApiCommandResult.Void + ), // --- type hierarchy new ApiCommand( 'vscode.prepareTypeHierarchy', '_executePrepareTypeHierarchy', 'Prepare type hierarchy at a position inside a document', diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index fe6da7f00c0..6a7132ad998 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -461,7 +461,7 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG let command: ICommandDto | undefined; if (r.command) { - if (r.command.command === 'vscode.open' || r.command.command === 'vscode.diff') { + if (r.command.command === 'vscode.open' || r.command.command === 'vscode.diff' || r.command.command === 'vscode.changes') { const disposables = new DisposableStore(); command = this._commands.converter.toInternal(r.command, disposables); this._resourceStatesDisposablesMap.set(handle, disposables); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index f8c4178a374..74bcd105029 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { isObject, isString, isUndefined, isNumber } from 'vs/base/common/types'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IEditorIdentifier, IEditorCommandsContext, CloseDirection, IVisibleEditorPane, EditorsOrder, EditorInputCapabilities, isEditorIdentifier, isEditorInputWithOptionsAndGroup, IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { IEditorIdentifier, IEditorCommandsContext, CloseDirection, IVisibleEditorPane, EditorsOrder, EditorInputCapabilities, isEditorIdentifier, isEditorInputWithOptionsAndGroup, IUntitledTextResourceEditorInput, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { TextCompareEditorVisibleContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, ActiveEditorStickyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, TextCompareEditorActiveContext, SideBySideEditorActiveContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorGroupColumn, columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; @@ -632,6 +632,39 @@ function registerOpenEditorAPICommands(): void { await editorService.openEditor({ resource: URI.from(resource, true), options: { ...optionsArg, pinned: true, override: id } }, columnToEditorGroup(editorGroupsService, configurationService, columnArg)); }); + + // partial, renderer-side API command to open diff editor + // complements https://github.com/microsoft/vscode/blob/2b164efb0e6a5de3826bff62683eaeafe032284f/src/vs/workbench/api/common/extHostApiCommands.ts#L397 + CommandsRegistry.registerCommand({ + id: 'vscode.changes', + handler: (accessor, title: string, resources: [UriComponents, UriComponents?, UriComponents?][]) => { + accessor.get(ICommandService).executeCommand('_workbench.changes', title, resources); + }, + metadata: { + description: 'Opens a list of resources in the changes editor to compare their contents.', + args: [ + { name: 'title', description: 'Human readable title for the diff editor' }, + { name: 'resources', description: 'List of resources to open in the changes editor' } + ] + } + }); + + CommandsRegistry.registerCommand('_workbench.changes', async (accessor: ServicesAccessor, title: string, resources: [UriComponents, UriComponents?, UriComponents?][]) => { + const editorService = accessor.get(IEditorService); + // const editorGroupService = accessor.get(IEditorGroupsService); + // const configurationService = accessor.get(IConfigurationService); + + const editor: (IResourceDiffEditorInput & { resource: URI })[] = []; + for (const [label, original, modified] of resources) { + editor.push({ + resource: URI.revive(label), + original: { resource: URI.revive(original) }, + modified: { resource: URI.revive(modified) }, + }); + } + + await editorService.openEditor({ resources: editor, label: title }); + }); } function registerOpenEditorAtIndexCommands(): void { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index bb796afcae8..c812971023b 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -489,6 +489,17 @@ export interface IResourceDiffEditorInput extends IBaseUntypedEditorInput { readonly modified: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; } +/** + * A resource list diff editor input compares multiple resources side by side + * highlighting the differences. + */ +export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { + /** + * The list of resources to compare. + */ + readonly resources: (IResourceDiffEditorInput & { resource: URI })[]; +} + export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; /** @@ -541,6 +552,16 @@ export function isResourceDiffEditorInput(editor: unknown): editor is IResourceD return candidate?.original !== undefined && candidate.modified !== undefined; } +export function isResourceDiffListEditorInput(editor: unknown): editor is IResourceMultiDiffEditorInput { + if (isEditorInput(editor)) { + return false; // make sure to not accidentally match on typed editor inputs + } + + const candidate = editor as IResourceMultiDiffEditorInput | undefined; + + return Array.isArray(candidate?.resources); +} + export function isResourceSideBySideEditorInput(editor: unknown): editor is IResourceSideBySideEditorInput { if (isEditorInput(editor)) { return false; // make sure to not accidentally match on typed editor inputs @@ -760,7 +781,7 @@ export const enum EditorInputCapabilities { AuxWindowUnsupported = 1 << 10 } -export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceSideBySideEditorInput | IResourceMergeEditorInput; +export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceMultiDiffEditorInput | IResourceSideBySideEditorInput | IResourceMergeEditorInput; export abstract class AbstractEditorInput extends Disposable { // Marker class for implementing `isEditorInput` @@ -1260,7 +1281,7 @@ class EditorResourceAccessorImpl { } } - if (isResourceDiffEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { + if (isResourceDiffEditorInput(editor) || isResourceDiffListEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { return undefined; } @@ -1329,7 +1350,7 @@ class EditorResourceAccessorImpl { } } - if (isResourceDiffEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { + if (isResourceDiffEditorInput(editor) || isResourceDiffListEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { return undefined; } diff --git a/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/src/vs/workbench/common/editor/sideBySideEditorInput.ts index beee39963a2..e852dd7b2bb 100644 --- a/src/vs/workbench/common/editor/sideBySideEditorInput.ts +++ b/src/vs/workbench/common/editor/sideBySideEditorInput.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, ISideBySideEditorInput, IUntypedEditorInput, isResourceSideBySideEditorInput, isDiffEditorInput, isResourceDiffEditorInput, IResourceSideBySideEditorInput, findViewStateForEditor, IMoveResult, isEditorInput, isResourceEditorInput, Verbosity, isResourceMergeEditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, ISideBySideEditorInput, IUntypedEditorInput, isResourceSideBySideEditorInput, isDiffEditorInput, isResourceDiffEditorInput, IResourceSideBySideEditorInput, findViewStateForEditor, IMoveResult, isEditorInput, isResourceEditorInput, Verbosity, isResourceMergeEditorInput, isResourceDiffListEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -190,7 +190,7 @@ export class SideBySideEditorInput extends EditorInput implements ISideBySideEdi return new SideBySideEditorInput(this.preferredName, this.preferredDescription, primarySaveResult, primarySaveResult, this.editorService); } - if (!isResourceDiffEditorInput(primarySaveResult) && !isResourceSideBySideEditorInput(primarySaveResult) && !isResourceMergeEditorInput(primarySaveResult)) { + if (!isResourceDiffEditorInput(primarySaveResult) && !isResourceDiffListEditorInput(primarySaveResult) && !isResourceSideBySideEditorInput(primarySaveResult) && !isResourceMergeEditorInput(primarySaveResult)) { return { primary: primarySaveResult, secondary: primarySaveResult, @@ -259,6 +259,7 @@ export class SideBySideEditorInput extends EditorInput implements ISideBySideEdi if ( primaryResourceEditorInput && secondaryResourceEditorInput && !isResourceDiffEditorInput(primaryResourceEditorInput) && !isResourceDiffEditorInput(secondaryResourceEditorInput) && + !isResourceDiffListEditorInput(primaryResourceEditorInput) && !isResourceDiffListEditorInput(secondaryResourceEditorInput) && !isResourceSideBySideEditorInput(primaryResourceEditorInput) && !isResourceSideBySideEditorInput(secondaryResourceEditorInput) && !isResourceMergeEditorInput(primaryResourceEditorInput) && !isResourceMergeEditorInput(secondaryResourceEditorInput) ) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index a6a66ffd952..666ea74dd6a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -43,7 +43,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget; if (!isEmbeddedDiffEditor) { - const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => /** diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); + const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); const onlyWhiteSpaceChange = computationResult.map(r => r && !r.identical && r.changes2.length === 0); this._register(autorunWithStore((reader, store) => { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts new file mode 100644 index 00000000000..19a8cc28e66 --- /dev/null +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable } from 'vs/base/common/lifecycle'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorExtensions, EditorInputWithOptions, IEditorFactoryRegistry, IEditorSerializer, IResourceMultiDiffEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { parse } from 'vs/base/common/marshalling'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { MultiDiffEditorInput, MultiDiffEditorInputData } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { MultiDiffEditor } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor'; + +class MultiDiffEditorResolverContribution extends Disposable { + + constructor( + @IEditorResolverService editorResolverService: IEditorResolverService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this._register(editorResolverService.registerEditor( + `*`, + { + id: DEFAULT_EDITOR_ASSOCIATION.id, + label: DEFAULT_EDITOR_ASSOCIATION.displayName, + detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, + priority: RegisteredEditorPriority.builtin + }, + {}, + { + createMultiDiffEditorInput: (diffListEditor: IResourceMultiDiffEditorInput): EditorInputWithOptions => { + return { + editor: instantiationService.createInstance( + MultiDiffEditorInput, + diffListEditor.label, + diffListEditor.resources.map(resource => { + return new MultiDiffEditorInputData( + resource.resource, + resource.original.resource, + resource.modified.resource + ); + })) + }; + } + } + )); + } +} + +Registry + .as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(MultiDiffEditorResolverContribution, LifecyclePhase.Starting); + +class MultiDiffEditorSerializer implements IEditorSerializer { + + canSerialize(editor: EditorInput): boolean { + return true; + } + + serialize(editor: MultiDiffEditorInput): string | undefined { + return JSON.stringify({ label: editor.label, resources: editor.resources }); + } + + deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined { + try { + const data = parse(serializedEditor) as { label: string | undefined; resources: MultiDiffEditorInputData[] }; + return instantiationService.createInstance(MultiDiffEditorInput, data.label, data.resources); + } catch (err) { + onUnexpectedError(err); + return undefined; + } + } +} + +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + MultiDiffEditor, + MultiDiffEditor.ID, + localize('name', "Multi Diff Editor") + ), + [ + new SyncDescriptor(MultiDiffEditorInput) + ] +); + +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( + MultiDiffEditorInput.ID, + MultiDiffEditorSerializer +); diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts new file mode 100644 index 00000000000..6ca92292e4c --- /dev/null +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { ConstLazyPromise, IDiffEntry } from 'vs/editor/browser/widget/multiDiffEditorWidget/model'; + +export class MultiDiffEditor extends EditorPane { + static readonly ID = 'multiDiffEditor'; + + private _multiDiffEditorWidget: MultiDiffEditorWidget | undefined = undefined; + + constructor( + @IInstantiationService private readonly instantiationService: InstantiationService, + @ITelemetryService telemetryService: ITelemetryService, + @ITextModelService private readonly textModelService: ITextModelService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService + ) { + super(MultiDiffEditor.ID, telemetryService, themeService, storageService); + } + + protected createEditor(parent: HTMLElement): void { + this._multiDiffEditorWidget = this._register(this.instantiationService.createInstance(MultiDiffEditorWidget, parent)); + } + + override async setInput(input: MultiDiffEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + const rs = await Promise.all(input.resources.map(async r => ({ + originalRef: await this.textModelService.createModelReference(r.original!), + modifiedRef: await this.textModelService.createModelReference(r.modified!), + title: r.resource.fsPath, + }))); + + this._multiDiffEditorWidget?.setModel({ + onDidChange: () => toDisposable(() => { }), + diffs: rs.map(r => new ConstLazyPromise({ + original: r.originalRef.object.textEditorModel, + modified: r.modifiedRef.object.textEditorModel, + title: r.title, + })), + }); + } + + layout(dimension: DOM.Dimension): void { + this._multiDiffEditorWidget?.layout(dimension); + } +} diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts new file mode 100644 index 00000000000..59b39f6e714 --- /dev/null +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; + +export class MultiDiffEditorInput extends EditorInput { + static readonly ID: string = 'workbench.input.multiDiffEditor'; + + get resource(): URI | undefined { + return undefined; + } + + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly; + } + + override get typeId(): string { + return MultiDiffEditorInput.ID; + } + + override getName(): string { + return this.label ?? localize('name', "Multi Diff Editor"); + } + + override get editorId(): string { + return DEFAULT_EDITOR_ASSOCIATION.id; + } + + constructor(readonly label: string | undefined, readonly resources: readonly MultiDiffEditorInputData[]) { + super(); + } +} + +export class MultiDiffEditorInputData { + constructor( + readonly resource: URI, + readonly original: URI | undefined, + readonly modified: URI | undefined + ) { } +} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 5f36dd36800..61262c27ef7 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -227,3 +227,7 @@ export interface ISCMViewService { readonly onDidFocusRepository: Event; focus(repository: ISCMRepository): void; } + +export const SCM_CHANGES_EDITOR_ID = 'workbench.editor.scmChangesEditor'; + +export interface ISCMChangesEditor { } diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index a8b940dcf9a..b8d99e5772a 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -10,7 +10,7 @@ import { basename, extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorActivation, EditorResolution, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, EditorInputWithOptions, IResourceSideBySideEditorInput, isEditorInputWithOptions, isEditorInputWithOptionsAndGroup, isResourceDiffEditorInput, isResourceSideBySideEditorInput, isUntitledResourceEditorInput, isResourceMergeEditorInput, IUntypedEditorInput, SideBySideEditor } from 'vs/workbench/common/editor'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, EditorInputWithOptions, IResourceSideBySideEditorInput, isEditorInputWithOptions, isEditorInputWithOptionsAndGroup, isResourceDiffEditorInput, isResourceSideBySideEditorInput, isUntitledResourceEditorInput, isResourceMergeEditorInput, IUntypedEditorInput, SideBySideEditor, isResourceDiffListEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Schemas } from 'vs/base/common/network'; @@ -463,6 +463,15 @@ export class EditorResolverService extends Disposable implements IEditorResolver return { editor: inputWithOptions.editor, options: inputWithOptions.options ?? options }; } + // If it's a diff list editor we trigger the create diff list editor input + if (isResourceDiffListEditorInput(editor)) { + if (!selectedEditor.editorFactoryObject.createMultiDiffEditorInput) { + return; + } + const inputWithOptions = await selectedEditor.editorFactoryObject.createMultiDiffEditorInput(editor, group); + return { editor: inputWithOptions.editor, options: inputWithOptions.options ?? options }; + } + if (isResourceSideBySideEditorInput(editor)) { throw new Error(`Untyped side by side editor input not supported here.`); } diff --git a/src/vs/workbench/services/editor/common/editorResolverService.ts b/src/vs/workbench/services/editor/common/editorResolverService.ts index 9c5d16f9abd..cd571b4d9e2 100644 --- a/src/vs/workbench/services/editor/common/editorResolverService.ts +++ b/src/vs/workbench/services/editor/common/editorResolverService.ts @@ -16,7 +16,7 @@ import { Extensions as ConfigurationExtensions, IConfigurationNode, IConfigurati import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInputWithOptions, EditorInputWithOptionsAndGroup, IResourceDiffEditorInput, IResourceMergeEditorInput, IUntitledTextResourceEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInputWithOptions, EditorInputWithOptionsAndGroup, IResourceDiffEditorInput, IResourceMultiDiffEditorInput, IResourceMergeEditorInput, IUntitledTextResourceEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { PreferredGroup } from 'vs/workbench/services/editor/common/editorService'; import { AtLeastOne } from 'vs/base/common/types'; @@ -108,12 +108,15 @@ export type UntitledEditorInputFactoryFunction = (untitledEditorInput: IUntitled export type DiffEditorInputFactoryFunction = (diffEditorInput: IResourceDiffEditorInput, group: IEditorGroup) => EditorInputFactoryResult; +export type DiffListEditorInputFactoryFunction = (diffEditorInput: IResourceMultiDiffEditorInput, group: IEditorGroup) => EditorInputFactoryResult; + export type MergeEditorInputFactoryFunction = (mergeEditorInput: IResourceMergeEditorInput, group: IEditorGroup) => EditorInputFactoryResult; type EditorInputFactories = { createEditorInput?: EditorInputFactoryFunction; createUntitledEditorInput?: UntitledEditorInputFactoryFunction; createDiffEditorInput?: DiffEditorInputFactoryFunction; + createMultiDiffEditorInput?: DiffListEditorInputFactoryFunction; createMergeEditorInput?: MergeEditorInputFactoryFunction; }; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 6175f84405c..30c9f21e421 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -233,6 +233,9 @@ import 'vs/workbench/contrib/markers/browser/markers.contribution'; // Merge Editor import 'vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution'; +// Multi Diff Editor +import 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution'; + // Mapped Edits import 'vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index f590f1f8815..85a55686e30 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -165,6 +165,9 @@ import 'vs/workbench/contrib/localHistory/electron-sandbox/localHistory.contribu // Merge Editor import 'vs/workbench/contrib/mergeEditor/electron-sandbox/mergeEditor.contribution'; +// Multi Diff Editor +import 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution'; + // Remote Tunnel import 'vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution';