Merge branch 'main' into fix-183445

This commit is contained in:
Megan Rogge 2023-05-26 10:40:09 -05:00 committed by GitHub
commit fcfef87f08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 2904 additions and 265 deletions

View file

@ -28,6 +28,7 @@ properties:
settings:
Name: 'yarn'
Global: true
PackageDirectory: '${WinGetConfigRoot}\..\'
- resource: Microsoft.WinGet.DSC/WinGetPackage
directives:
description: Install Python 3.10
@ -55,4 +56,12 @@ properties:
includeRecommended: true
components:
- Microsoft.VisualStudio.Workload.VCTools
- resource: YarnDsc/YarnInstall
dependsOn:
- npm
directives:
description: Install dependencies
allowPrerelease: true
settings:
PackageDirectory: '${WinGetConfigRoot}\..\'
configurationVersion: 0.2.0

View file

@ -51,6 +51,8 @@
"--vscode-commandCenter-foreground",
"--vscode-commandCenter-inactiveBorder",
"--vscode-commandCenter-inactiveForeground",
"--vscode-commentsView-resolvedIcon",
"--vscode-commentsView-unresolvedIcon",
"--vscode-contrastActiveBorder",
"--vscode-contrastBorder",
"--vscode-debugConsole-errorForeground",
@ -97,6 +99,7 @@
"--vscode-diffEditor-removedLineBackground",
"--vscode-diffEditor-removedTextBackground",
"--vscode-diffEditor-removedTextBorder",
"--vscode-diffEditor-unchangedRegionBackground",
"--vscode-diffEditorGutter-insertedLineBackground",
"--vscode-diffEditorGutter-removedLineBackground",
"--vscode-diffEditorOverview-insertedForeground",
@ -233,6 +236,8 @@
"--vscode-editorOverviewRuler-background",
"--vscode-editorOverviewRuler-border",
"--vscode-editorOverviewRuler-bracketMatchForeground",
"--vscode-editorOverviewRuler-commentForeground",
"--vscode-editorOverviewRuler-commentUnresolvedForeground",
"--vscode-editorOverviewRuler-commonContentForeground",
"--vscode-editorOverviewRuler-currentContentForeground",
"--vscode-editorOverviewRuler-deletedForeground",
@ -309,6 +314,8 @@
"--vscode-interactiveEditor-border",
"--vscode-interactiveEditor-regionHighlight",
"--vscode-interactiveEditor-shadow",
"--vscode-interactiveEditorDiff-inserted",
"--vscode-interactiveEditorDiff-removed",
"--vscode-interactiveEditorInput-background",
"--vscode-interactiveEditorInput-border",
"--vscode-interactiveEditorInput-focusBorder",
@ -457,7 +464,6 @@
"--vscode-problemsErrorIcon-foreground",
"--vscode-problemsInfoIcon-foreground",
"--vscode-problemsWarningIcon-foreground",
"--vscode-problemsSuccessIcon-foreground",
"--vscode-profileBadge-background",
"--vscode-profileBadge-foreground",
"--vscode-progressBar-background",
@ -520,6 +526,8 @@
"--vscode-statusBar-noFolderBackground",
"--vscode-statusBar-noFolderBorder",
"--vscode-statusBar-noFolderForeground",
"--vscode-statusBar-offlineBackground",
"--vscode-statusBar-offlineForeground",
"--vscode-statusBarItem-activeBackground",
"--vscode-statusBarItem-compactHoverBackground",
"--vscode-statusBarItem-errorBackground",
@ -733,4 +741,4 @@
"--z-index-run-button-container",
"--zoom-factor"
]
}
}

View file

@ -25,6 +25,7 @@
["`", "`"]
],
"folding": {
"offSide": true,
"markers": {
"start": "^\\s*--\\s*#region\\b",
"end": "^\\s*--\\s*#endregion\\b"

View file

@ -1865,7 +1865,7 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
el.appendChild(c);
} else if (typeof c === 'string') {
el.append(c);
} else {
} else if ('root' in c) {
Object.assign(result, c);
el.appendChild(c.root);
}

View file

@ -23,6 +23,41 @@ export function autorunHandleChanges<TChangeSummary>(
return new AutorunObserver(debugName, fn, options.createEmptyChangeSummary, options.handleChange);
}
// TODO@hediet rename to autorunWithStore
export function autorunWithStore2(
debugName: string,
fn: (reader: IReader, store: DisposableStore) => void,
): IDisposable {
return autorunWithStore(fn, debugName);
}
export function autorunWithStoreHandleChanges<TChangeSummary>(
debugName: string,
options: {
createEmptyChangeSummary?: () => TChangeSummary;
handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
},
fn: (reader: IReader, changeSummary: TChangeSummary, store: DisposableStore) => void
): IDisposable {
const store = new DisposableStore();
const disposable = autorunHandleChanges(
debugName,
{
createEmptyChangeSummary: options.createEmptyChangeSummary,
handleChange: options.handleChange,
},
(reader, changeSummary) => {
store.clear();
fn(reader, changeSummary, store);
}
);
return toDisposable(() => {
disposable.dispose();
store.dispose();
});
}
// TODO@hediet deprecate, rename to autorunWithStoreEx
export function autorunWithStore(
fn: (reader: IReader, store: DisposableStore) => void,
debugName: string
@ -144,13 +179,13 @@ export class AutorunObserver<TChangeSummary = any> implements IObserver, IReader
}
public handlePossibleChange(observable: IObservable<any>): void {
if (this.state === AutorunState.upToDate && this.dependencies.has(observable)) {
if (this.state === AutorunState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
this.state = AutorunState.dependenciesMightHaveChanged;
}
}
public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
if (this.dependencies.has(observable)) {
if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,

View file

@ -7,6 +7,11 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import type { derived } from 'vs/base/common/observableImpl/derived';
import { getLogger } from 'vs/base/common/observableImpl/logging';
/**
* Represents an observable value.
* @template T The type of the value.
* @template TChange The type of delta information (usually `void` and only used in advanced scenarios).
*/
export interface IObservable<T, TChange = unknown> {
/**
* Returns the current value.
@ -248,6 +253,10 @@ export function getFunctionName(fn: Function): string | undefined {
export interface ISettableObservable<T, TChange = void> extends IObservable<T, TChange>, ISettable<T, TChange> {
}
/**
* Creates an observable value.
* Observers get informed when the value changes.
*/
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange> {
return new ObservableValue(name, initialValue);
}

View file

@ -196,7 +196,7 @@ export class Derived<T, TChangeSummary = any> extends BaseObservable<T, void> im
public handlePossibleChange<T>(observable: IObservable<T, unknown>): void {
// In all other states, observers already know that we might have changed.
if (this.state === DerivedState.upToDate && this.dependencies.has(observable)) {
if (this.state === DerivedState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
this.state = DerivedState.dependenciesMightHaveChanged;
for (const r of this.observers) {
r.handlePossibleChange(this);
@ -205,22 +205,19 @@ export class Derived<T, TChangeSummary = any> extends BaseObservable<T, void> im
}
public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
const isUpToDate = this.state === DerivedState.upToDate;
let shouldReact = true;
if (this._handleChange && this.dependencies.has(observable)) {
shouldReact = this._handleChange({
if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,
didChange: o => o === observable as any,
}, this.changeSummary!);
}
if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || isUpToDate) && this.dependencies.has(observable)) {
this.state = DerivedState.stale;
if (isUpToDate) {
for (const r of this.observers) {
r.handlePossibleChange(this);
}, this.changeSummary!) : true;
const wasUpToDate = this.state === DerivedState.upToDate;
if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) {
this.state = DerivedState.stale;
if (wasUpToDate) {
for (const r of this.observers) {
r.handlePossibleChange(this);
}
}
}
}

View file

@ -198,6 +198,9 @@ class FromEventObservableSignal extends BaseObservable<void> {
}
}
/**
* Creates a signal that can be triggered to invalidate observers.
*/
export function observableSignal<TDelta = void>(
debugName: string
): IObservableSignal<TDelta> {
@ -287,6 +290,10 @@ export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number,
export function keepAlive(observable: IObservable<any>, forceRecompute?: boolean): IDisposable {
const o = new KeepAliveObserver(forceRecompute ?? false);
observable.addObserver(o);
if (forceRecompute) {
observable.reportChanges();
}
return toDisposable(() => {
observable.removeObserver(o);
});

View file

@ -37,8 +37,13 @@ export enum RimRafMode {
* - `UNLINK`: direct removal from disk
* - `MOVE`: faster variant that first moves the target to temp dir and then
* deletes it in the background without waiting for that to finish.
* the optional `moveToPath` allows to override where to rename the
* path to before deleting it.
*/
async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<void> {
async function rimraf(path: string, mode: RimRafMode.UNLINK): Promise<void>;
async function rimraf(path: string, mode: RimRafMode.MOVE, moveToPath?: string): Promise<void>;
async function rimraf(path: string, mode?: RimRafMode, moveToPath?: string): Promise<void>;
async function rimraf(path: string, mode = RimRafMode.UNLINK, moveToPath?: string): Promise<void> {
if (isRootOrDriveLetter(path)) {
throw new Error('rimraf - will refuse to recursively delete root');
}
@ -49,12 +54,11 @@ async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<void> {
}
// delete: via move
return rimrafMove(path);
return rimrafMove(path, moveToPath);
}
async function rimrafMove(path: string): Promise<void> {
async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Promise<void> {
try {
const pathInTemp = randomPath(tmpdir());
try {
// Intentionally using `fs.promises` here to skip
// the patched graceful-fs method that can result
@ -64,7 +68,7 @@ async function rimrafMove(path: string): Promise<void> {
// than necessary and we have a fallback to delete
// via unlink.
// https://github.com/microsoft/vscode/issues/139908
await fs.promises.rename(path, pathInTemp);
await fs.promises.rename(path, moveToPath);
} catch (error) {
if (error.code === 'ENOENT') {
return; // ignore - path to delete did not exist
@ -74,7 +78,7 @@ async function rimrafMove(path: string): Promise<void> {
}
// Delete but do not return as promise
rimrafUnlink(pathInTemp).catch(error => {/* ignore */ });
rimrafUnlink(moveToPath).catch(error => {/* ignore */ });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;

View file

@ -10,7 +10,7 @@ import { timeout } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { randomPath } from 'vs/base/common/extpath';
import { FileAccess } from 'vs/base/common/network';
import { join, sep } from 'vs/base/common/path';
import { basename, dirname, join, sep } from 'vs/base/common/path';
import { isWindows } from 'vs/base/common/platform';
import { configureFlushOnWrite, Promises, RimRafMode, rimrafSync, SymlinkSupport, writeFileSync } from 'vs/base/node/pfs';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
@ -91,6 +91,14 @@ flakySuite('PFS', function () {
assert.ok(!fs.existsSync(testDir));
});
test('rimraf - simple - move (with moveToPath)', async () => {
fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents');
fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents');
await Promises.rm(testDir, RimRafMode.MOVE, join(dirname(testDir), `${basename(testDir)}.vsctmp`));
assert.ok(!fs.existsSync(testDir));
});
test('rimraf - path does not exist - move', async () => {
const nonExistingDir = join(testDir, 'unknown-move');
await Promises.rm(nonExistingDir, RimRafMode.MOVE);

View file

@ -21,6 +21,7 @@ import { IEditorWhitespace, IViewModel } from 'vs/editor/common/viewModel';
import { InjectedText } from 'vs/editor/common/modelLineProjectionData';
import { ILineChange, IDiffComputationResult } from 'vs/editor/common/diff/smartLinesDiffComputer';
import { IDimension } from 'vs/editor/common/core/dimension';
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
/**
* A view zone is a full horizontal rectangle that 'pushes' text down.
@ -1210,6 +1211,21 @@ export interface IDiffEditor extends editorCommon.IEditor {
* Update the editor's options after the editor has been created.
*/
updateOptions(newOptions: IDiffEditorOptions): void;
/**
* @internal
*/
setBoundarySashes(sashes: IBoundarySashes): void;
/**
* @internal
*/
goToDiff(target: 'next' | 'previous'): void;
/**
* @internal
*/
revealFirstDiff(): unknown;
}
/**

View file

@ -450,9 +450,13 @@ export abstract class EditorAction2 extends Action2 {
// precondition does hold
return editor.invokeWithinContext((editorAccessor) => {
const kbService = editorAccessor.get(IContextKeyService);
if (kbService.contextMatchesRules(withNullAsUndefined(this.desc.precondition))) {
return this.runEditorCommand(editorAccessor, editor!, ...args);
const logService = editorAccessor.get(ILogService);
const enabled = kbService.contextMatchesRules(withNullAsUndefined(this.desc.precondition));
if (!enabled) {
logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize());
return;
}
return this.runEditorCommand(editorAccessor, editor!, ...args);
});
}

View file

@ -60,6 +60,7 @@ import { getThemeTypeSelector, IColorTheme, IThemeService, registerThemingPartic
import { ThemeIcon } from 'vs/base/common/themables';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator';
export interface IDiffCodeEditorWidgetOptions {
originalEditor?: ICodeEditorWidgetOptions;
@ -242,6 +243,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
private isEmbeddedDiffEditorKey: IContextKey<boolean>;
private _diffNavigator: DiffNavigator | undefined;
constructor(
domElement: HTMLElement,
options: Readonly<editorBrowser.IDiffEditorConstructionOptions>,
@ -289,7 +292,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
renderOverviewRuler: true,
diffWordWrap: 'inherit',
diffAlgorithm: 'advanced',
accessibilityVerbose: false
accessibilityVerbose: false,
experimental: {
collapseUnchangedRegions: false,
},
});
this.isEmbeddedDiffEditorKey = EditorContextKeys.isEmbeddedDiffEditor.bindTo(this._contextKeyService);
@ -860,12 +866,20 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
this._layoutOverviewViewport();
this._onDidChangeModel.fire();
// Diff navigator
this._diffNavigator = this._register(this._instantiationService.createInstance(DiffNavigator, this, {
alwaysRevealFirst: false,
findResultLoop: this.getModifiedEditor().getOption(EditorOption.find).loop
}));
}
public getContainerDomNode(): HTMLElement {
return this._domElement;
}
// #region editorBrowser.IDiffEditor: Delegating to modified Editor
public getVisibleColumnFromPosition(position: IPosition): number {
return this._modifiedEditor.getVisibleColumnFromPosition(position);
}
@ -978,6 +992,24 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
return this._modifiedEditor.getSupportedActions();
}
public focus(): void {
this._modifiedEditor.focus();
}
public trigger(source: string | null | undefined, handlerId: string, payload: any): void {
this._modifiedEditor.trigger(source, handlerId, payload);
}
public createDecorationsCollection(decorations?: IModelDeltaDecoration[]): editorCommon.IEditorDecorationsCollection {
return this._modifiedEditor.createDecorationsCollection(decorations);
}
public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any {
return this._modifiedEditor.changeDecorations(callback);
}
// #endregion
public saveViewState(): editorCommon.IDiffEditorViewState {
const originalViewState = this._originalEditor.saveViewState();
const modifiedViewState = this._modifiedEditor.saveViewState();
@ -999,9 +1031,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
this._elementSizeObserver.observe(dimension);
}
public focus(): void {
this._modifiedEditor.focus();
}
public hasTextFocus(): boolean {
return this._originalEditor.hasTextFocus() || this._modifiedEditor.hasTextFocus();
@ -1023,18 +1052,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
this._cleanViewZonesAndDecorations();
}
public trigger(source: string | null | undefined, handlerId: string, payload: any): void {
this._modifiedEditor.trigger(source, handlerId, payload);
}
public createDecorationsCollection(decorations?: IModelDeltaDecoration[]): editorCommon.IEditorDecorationsCollection {
return this._modifiedEditor.createDecorationsCollection(decorations);
}
public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any {
return this._modifiedEditor.changeDecorations(callback);
}
//------------ end IDiffEditor methods
@ -1531,6 +1548,21 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
equivalentLineNumber: this._getEquivalentLineForModifiedLineNumber(lineNumber)
};
}
public goToDiff(target: 'previous' | 'next'): void {
if (target === 'next') {
this._diffNavigator?.next();
} else {
this._diffNavigator?.previous();
}
}
public revealFirstDiff(): void {
// This is a hack, but it works.
if (this._diffNavigator) {
this._diffNavigator.revealFirst = true;
}
}
}
interface IDataSource {
@ -2780,6 +2812,9 @@ function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaul
diffWordWrap: validateDiffWordWrap(options.diffWordWrap, defaults.diffWordWrap),
diffAlgorithm: validateStringSetOption(options.diffAlgorithm, defaults.diffAlgorithm, ['legacy', 'advanced'], { 'smart': 'legacy', 'experimental': 'advanced' }),
accessibilityVerbose: validateBooleanOption(options.accessibilityVerbose, defaults.accessibilityVerbose),
experimental: {
collapseUnchangedRegions: false,
},
};
}

View file

@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
export const diffFullLineAddDecoration = ModelDecorationOptions.register({
className: 'line-insert',
description: 'line-insert',
isWholeLine: true,
});
export const diffFullLineDeleteDecoration = ModelDecorationOptions.register({
className: 'line-delete',
description: 'line-delete',
isWholeLine: true,
});
export const diffAddDecoration = ModelDecorationOptions.register({
className: 'char-insert',
description: 'char-insert',
});
export const diffDeleteDecoration = ModelDecorationOptions.register({
className: 'char-delete',
description: 'char-delete',
});

View file

@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IDimension } from 'vs/editor/common/core/dimension';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IEditor, IEditorAction, IEditorDecorationsCollection, IEditorModel, IEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration } from 'vs/editor/common/model';
export abstract class DelegatingEditor extends Disposable implements IEditor {
private static idCounter = 0;
private readonly _id = ++DelegatingEditor.idCounter;
private readonly _onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this._onDidDispose.event;
protected abstract get _targetEditor(): CodeEditorWidget;
getId(): string { return this.getEditorType() + ':' + this._id; }
abstract getEditorType(): string;
abstract updateOptions(newOptions: IEditorOptions): void;
abstract onVisible(): void;
abstract onHide(): void;
abstract layout(dimension?: IDimension | undefined): void;
abstract hasTextFocus(): boolean;
abstract saveViewState(): IEditorViewState | null;
abstract restoreViewState(state: IEditorViewState | null): void;
abstract getModel(): IEditorModel | null;
abstract setModel(model: IEditorModel | null): void;
// #region editorBrowser.IDiffEditor: Delegating to modified Editor
public getVisibleColumnFromPosition(position: IPosition): number {
return this._targetEditor.getVisibleColumnFromPosition(position);
}
public getStatusbarColumn(position: IPosition): number {
return this._targetEditor.getStatusbarColumn(position);
}
public getPosition(): Position | null {
return this._targetEditor.getPosition();
}
public setPosition(position: IPosition, source: string = 'api'): void {
this._targetEditor.setPosition(position, source);
}
public revealLine(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLine(lineNumber, scrollType);
}
public revealLineInCenter(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLineInCenter(lineNumber, scrollType);
}
public revealLineInCenterIfOutsideViewport(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLineInCenterIfOutsideViewport(lineNumber, scrollType);
}
public revealLineNearTop(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLineNearTop(lineNumber, scrollType);
}
public revealPosition(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPosition(position, scrollType);
}
public revealPositionInCenter(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPositionInCenter(position, scrollType);
}
public revealPositionInCenterIfOutsideViewport(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPositionInCenterIfOutsideViewport(position, scrollType);
}
public revealPositionNearTop(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPositionNearTop(position, scrollType);
}
public getSelection(): Selection | null {
return this._targetEditor.getSelection();
}
public getSelections(): Selection[] | null {
return this._targetEditor.getSelections();
}
public setSelection(range: IRange, source?: string): void;
public setSelection(editorRange: Range, source?: string): void;
public setSelection(selection: ISelection, source?: string): void;
public setSelection(editorSelection: Selection, source?: string): void;
public setSelection(something: any, source: string = 'api'): void {
this._targetEditor.setSelection(something, source);
}
public setSelections(ranges: readonly ISelection[], source: string = 'api'): void {
this._targetEditor.setSelections(ranges, source);
}
public revealLines(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLines(startLineNumber, endLineNumber, scrollType);
}
public revealLinesInCenter(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLinesInCenter(startLineNumber, endLineNumber, scrollType);
}
public revealLinesInCenterIfOutsideViewport(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLinesInCenterIfOutsideViewport(startLineNumber, endLineNumber, scrollType);
}
public revealLinesNearTop(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLinesNearTop(startLineNumber, endLineNumber, scrollType);
}
public revealRange(range: IRange, scrollType: ScrollType = ScrollType.Smooth, revealVerticalInCenter: boolean = false, revealHorizontal: boolean = true): void {
this._targetEditor.revealRange(range, scrollType, revealVerticalInCenter, revealHorizontal);
}
public revealRangeInCenter(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeInCenter(range, scrollType);
}
public revealRangeInCenterIfOutsideViewport(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeInCenterIfOutsideViewport(range, scrollType);
}
public revealRangeNearTop(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeNearTop(range, scrollType);
}
public revealRangeNearTopIfOutsideViewport(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeNearTopIfOutsideViewport(range, scrollType);
}
public revealRangeAtTop(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeAtTop(range, scrollType);
}
public getSupportedActions(): IEditorAction[] {
return this._targetEditor.getSupportedActions();
}
public focus(): void {
this._targetEditor.focus();
}
public trigger(source: string | null | undefined, handlerId: string, payload: any): void {
this._targetEditor.trigger(source, handlerId, payload);
}
public createDecorationsCollection(decorations?: IModelDeltaDecoration[]): IEditorDecorationsCollection {
return this._targetEditor.createDecorationsCollection(decorations);
}
public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any {
return this._targetEditor.changeDecorations(callback);
}
// #endregion
}

View file

@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Sash, Orientation, ISashEvent, IBoundarySashes, SashState } from 'vs/base/browser/ui/sash/sash';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, autorun, derived, observableValue } from 'vs/base/common/observable';
export class DiffEditorSash extends Disposable {
private readonly _sashRatio = observableValue<number | undefined>('sashRatio', undefined);
public readonly sashLeft = derived('sashLeft', reader => {
const ratio = this._sashRatio.read(reader) ?? this._defaultSashRatio.read(reader);
return this._computeSashLeft(ratio, reader);
});
private readonly _sash = this._register(new Sash(this._domNode, {
getVerticalSashTop: (_sash: Sash): number => 0,
getVerticalSashLeft: (_sash: Sash): number => this.sashLeft.get(),
getVerticalSashHeight: (_sash: Sash): number => this._dimensions.height.get(),
}, { orientation: Orientation.VERTICAL }));
private _startSashPosition: number | undefined = undefined;
constructor(
private readonly _enableSplitViewResizing: IObservable<boolean>,
private readonly _defaultSashRatio: IObservable<number>,
private readonly _domNode: HTMLElement,
private readonly _dimensions: { height: IObservable<number>; width: IObservable<number> },
) {
super();
this._register(this._sash.onDidStart(() => {
this._startSashPosition = this.sashLeft.get();
}));
this._register(this._sash.onDidChange((e: ISashEvent) => {
const contentWidth = this._dimensions.width.get();
const sashPosition = this._computeSashLeft((this._startSashPosition! + (e.currentX - e.startX)) / contentWidth, undefined);
this._sashRatio.set(sashPosition / contentWidth, undefined);
}));
this._register(this._sash.onDidEnd(() => this._sash.layout()));
this._register(this._sash.onDidReset(() => this._sashRatio.set(undefined, undefined)));
this._register(autorun('update sash layout', (reader) => {
const enabled = this._enableSplitViewResizing.read(reader);
this._sash.state = enabled ? SashState.Enabled : SashState.Disabled;
this.sashLeft.read(reader);
this._sash.layout();
}));
}
setBoundarySashes(sashes: IBoundarySashes): void {
this._sash.orthogonalEndSash = sashes.bottom;
}
private _computeSashLeft(desiredRatio: number, reader: IReader | undefined): number {
const contentWidth = this._dimensions.width.read(reader);
const midPoint = Math.floor(this._defaultSashRatio.read(reader) * contentWidth);
const sashLeft = this._enableSplitViewResizing.read(reader) ? Math.floor(desiredRatio * contentWidth) : midPoint;
const MINIMUM_EDITOR_WIDTH = 100;
if (contentWidth <= MINIMUM_EDITOR_WIDTH * 2) {
return midPoint;
}
if (sashLeft < MINIMUM_EDITOR_WIDTH) {
return MINIMUM_EDITOR_WIDTH;
}
if (sashLeft > contentWidth - MINIMUM_EDITOR_WIDTH) {
return contentWidth - MINIMUM_EDITOR_WIDTH;
}
return sashLeft;
}
}

View file

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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 { ThemeIcon } from 'vs/base/common/themables';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
export class ToggleCollapseUnchangedRegions extends Action2 {
constructor() {
super({
id: 'diffEditor.toggleCollapseUnchangedRegions',
title: { value: localize('toggleCollapseUnchangedRegions', "Toggle Collapse Unchanged Regions"), original: 'Toggle Collapse Unchanged Regions' },
icon: Codicon.map,
precondition: ContextKeyEqualsExpr.create('diffEditorVersion', 2),
});
}
run(accessor: ServicesAccessor, ...args: unknown[]): void {
const configurationService = accessor.get(IConfigurationService);
const newValue = !configurationService.getValue<boolean>('diffEditor.experimental.collapseUnchangedRegions');
configurationService.updateValue('diffEditor.experimental.collapseUnchangedRegions', newValue);
}
}
registerAction2(ToggleCollapseUnchangedRegions);
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: new ToggleCollapseUnchangedRegions().desc.id,
title: localize('collapseUnchangedRegions', "Collapse Unchanged Regions"),
icon: Codicon.map
},
group: 'navigation',
when: ContextKeyExpr.and(
ContextKeyExpr.has('config.diffEditor.experimental.collapseUnchangedRegions'),
ContextKeyEqualsExpr.create('diffEditorVersion', 2)
)
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: new ToggleCollapseUnchangedRegions().desc.id,
title: localize('showUnchangedRegions', "Show Unchanged Regions"),
icon: ThemeIcon.modify(Codicon.map, 'disabled'),
},
group: 'navigation',
when: ContextKeyExpr.and(
ContextKeyExpr.has('config.diffEditor.experimental.collapseUnchangedRegions').negate(),
ContextKeyEqualsExpr.create('diffEditorVersion', 2)
)
});

View file

@ -0,0 +1,499 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { h } from 'vs/base/browser/dom';
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
import { findLast } from 'vs/base/common/arrays';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { IObservable, ISettableObservable, derived, keepAlive, observableValue, waitForState } from 'vs/base/common/observable';
import { Constants } from 'vs/base/common/uint';
import 'vs/css!./style';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IDiffLineInformation } from 'vs/editor/browser/editorBrowser';
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget';
import { diffAddDecoration, diffDeleteDecoration, diffFullLineAddDecoration, diffFullLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditorWidget2/decorations';
import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash';
import { ViewZoneAlignment } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment';
import { OverviewRulerPart } from 'vs/editor/browser/widget/diffEditorWidget2/overviewRulerPart';
import { UnchangedRangesFeature } from 'vs/editor/browser/widget/diffEditorWidget2/unchangedRanges';
import { ObservableElementSizeObserver, applyObservableDecorations } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { EditorOptions, IDiffEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from 'vs/editor/common/config/editorOptions';
import { IDimension } from 'vs/editor/common/core/dimension';
import { Position } from 'vs/editor/common/core/position';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
import { EditorType, IContentSizeChangedEvent, IDiffEditorModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { DelegatingEditor } from './delegatingEditorImpl';
import { DiffModel } from './diffModel';
const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = {
enableSplitViewResizing: true,
splitViewDefaultRatio: 0.5,
renderSideBySide: true,
renderMarginRevertIcon: true,
maxComputationTime: 5000,
maxFileSize: 50,
ignoreTrimWhitespace: true,
renderIndicators: true,
originalEditable: false,
diffCodeLens: false,
renderOverviewRuler: true,
diffWordWrap: 'inherit',
diffAlgorithm: 'advanced',
accessibilityVerbose: false,
experimental: {
collapseUnchangedRegions: false,
}
};
export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [
h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }),
h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%' } }),
]);
private readonly _model = observableValue<IDiffEditorModel | null>('diffEditorModel', null);
public readonly onDidChangeModel = Event.fromObservableLight(this._model);
private readonly _diffModel = observableValue<DiffModel | null>('diffModel', null);
private readonly _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
public readonly onDidContentSizeChange = this._onDidContentSizeChange.event;
private readonly _modifiedEditor: CodeEditorWidget;
private readonly _originalEditor: CodeEditorWidget;
private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement));
private readonly _instantiationService = this._parentInstantiationService.createChild(
new ServiceCollection([IContextKeyService, this._contextKeyService])
);
private readonly _rootSizeObserver: ObservableElementSizeObserver;
private readonly _options: ISettableObservable<ValidDiffEditorBaseOptions>;
private _isHandlingScrollEvent = false;
private readonly _sash: DiffEditorSash;
private readonly _renderOverviewRuler: IObservable<boolean>;
constructor(
private readonly _domElement: HTMLElement,
options: Readonly<IDiffEditorConstructionOptions>,
codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions,
@IContextKeyService private readonly _parentContextKeyService: IContextKeyService,
@IInstantiationService private readonly _parentInstantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
) {
super();
codeEditorService.willCreateDiffEditor();
this._contextKeyService.createKey('isInDiffEditor', true);
this._contextKeyService.createKey('diffEditorVersion', 2);
this._contextKeyService.createKey('isInEmbeddedDiffEditor',
typeof options.isInEmbeddedEditor !== 'undefined' ? options.isInEmbeddedEditor : false
);
this._options = observableValue<ValidDiffEditorBaseOptions>('options', validateDiffEditorOptions(options || {}, diffEditorDefaultOptions));
this._domElement.appendChild(this.elements.root);
this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension));
this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false);
this._originalEditor = this._createLeftHandSideEditor(options, codeEditorWidgetOptions.originalEditor || {});
this._modifiedEditor = this._createRightHandSideEditor(options, codeEditorWidgetOptions.modifiedEditor || {});
this._register(applyObservableDecorations(this._originalEditor, this._decorations.map(d => d?.originalDecorations || [])));
this._register(applyObservableDecorations(this._modifiedEditor, this._decorations.map(d => d?.modifiedDecorations || [])));
this._renderOverviewRuler = this._options.map(o => o.renderOverviewRuler);
this._sash = this._register(new DiffEditorSash(
this._options.map(o => o.enableSplitViewResizing),
this._options.map(o => o.splitViewDefaultRatio),
this.elements.root,
{
height: this._rootSizeObserver.height,
width: this._rootSizeObserver.width.map((w, reader) => w - (this._renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),
}
));
this._register(new UnchangedRangesFeature(this._originalEditor, this._modifiedEditor, this._diffModel));
this._register(new ViewZoneAlignment(this._originalEditor, this._modifiedEditor, this._diffModel));
this._register(this._instantiationService.createInstance(OverviewRulerPart,
this._originalEditor,
this._modifiedEditor,
this.elements.root,
this._diffModel,
this._rootSizeObserver.width,
this._rootSizeObserver.height,
this._layoutInfo.map(i => i.modifiedEditor),
this._renderOverviewRuler,
));
this._createDiffEditorContributions();
codeEditorService.addDiffEditor(this);
this._register(keepAlive(this._layoutInfo, true));
}
private readonly _layoutInfo = derived('modifiedEditorLayoutInfo', (reader) => {
const width = this._rootSizeObserver.width.read(reader);
const height = this._rootSizeObserver.height.read(reader);
const sashLeft = this._sash.sashLeft.read(reader);
this.elements.original.style.width = sashLeft + 'px';
this.elements.original.style.left = '0px';
this.elements.modified.style.width = (width - sashLeft) + 'px';
this.elements.modified.style.left = sashLeft + 'px';
this._originalEditor.layout({ width: sashLeft, height: height });
this._modifiedEditor.layout({
width: width - sashLeft -
(this._renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0),
height
});
return { modifiedEditor: this._modifiedEditor.getLayoutInfo() };
});
private readonly _decorations = derived('decorations', (reader) => {
const diff = this._diffModel.read(reader)?.diff.read(reader);
if (!diff) {
return null;
}
const originalDecorations: IModelDeltaDecoration[] = [];
const modifiedDecorations: IModelDeltaDecoration[] = [];
for (const c of diff.changes) {
const fullRangeOriginal = c.originalRange.toInclusiveRange();
if (fullRangeOriginal) {
originalDecorations.push({ range: fullRangeOriginal, options: diffFullLineDeleteDecoration });
}
const fullRangeModified = c.modifiedRange.toInclusiveRange();
if (fullRangeModified) {
modifiedDecorations.push({ range: fullRangeModified, options: diffFullLineAddDecoration });
}
for (const i of c.innerChanges || []) {
originalDecorations.push({ range: i.originalRange, options: diffDeleteDecoration });
modifiedDecorations.push({ range: i.modifiedRange, options: diffAddDecoration });
}
}
return { originalDecorations, modifiedDecorations };
});
private _createDiffEditorContributions() {
const contributions: IDiffEditorContributionDescription[] = EditorExtensionsRegistry.getDiffEditorContributions();
for (const desc of contributions) {
try {
this._register(this._instantiationService.createInstance(desc.ctor, this));
} catch (err) {
onUnexpectedError(err);
}
}
}
private _createLeftHandSideEditor(options: Readonly<IDiffEditorConstructionOptions>, codeEditorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(this._instantiationService, this.elements.original, this._adjustOptionsForLeftHandSide(options), codeEditorWidgetOptions);
const isInDiffLeftEditorKey = this._contextKeyService.createKey<boolean>('isInDiffLeftEditor', editor.hasWidgetFocus());
this._register(editor.onDidFocusEditorWidget(() => isInDiffLeftEditorKey.set(true)));
this._register(editor.onDidBlurEditorWidget(() => isInDiffLeftEditorKey.set(false)));
return editor;
}
private _createRightHandSideEditor(options: Readonly<IDiffEditorConstructionOptions>, codeEditorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(this._instantiationService, this.elements.modified, this._adjustOptionsForRightHandSide(options), codeEditorWidgetOptions);
const isInDiffRightEditorKey = this._contextKeyService.createKey<boolean>('isInDiffRightEditor', editor.hasWidgetFocus());
this._register(editor.onDidFocusEditorWidget(() => isInDiffRightEditorKey.set(true)));
this._register(editor.onDidBlurEditorWidget(() => isInDiffRightEditorKey.set(false)));
// Revert change when an arrow is clicked.
/*TODO
this._register(editor.onMouseDown(event => {
if (!event.event.rightButton && event.target.position && event.target.element?.className.includes('arrow-revert-change')) {
const lineNumber = event.target.position.lineNumber;
const viewZone = event.target as editorBrowser.IMouseTargetViewZone | undefined;
const change = this._diffComputationResult?.changes.find(c =>
// delete change
viewZone?.detail.afterLineNumber === c.modifiedStartLineNumber ||
// other changes
(c.modifiedEndLineNumber > 0 && c.modifiedStartLineNumber === lineNumber));
if (change) {
this.revertChange(change);
}
event.event.stopPropagation();
this._updateDecorations();
return;
}
}));*/
return editor;
}
protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorConstructionOptions>, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions);
this._register(editor.onDidContentSizeChange(e => {
const width = this._originalEditor.getContentWidth() + this._modifiedEditor.getContentWidth() + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH;
const height = Math.max(this._modifiedEditor.getContentHeight(), this._originalEditor.getContentHeight());
this._onDidContentSizeChange.fire({
contentHeight: height,
contentWidth: width,
contentHeightChanged: e.contentHeightChanged,
contentWidthChanged: e.contentWidthChanged
});
}));
this._register(editor.onDidScrollChange((e) => {
if (this._isHandlingScrollEvent) {
return;
}
if (!e.scrollTopChanged && !e.scrollLeftChanged && !e.scrollHeightChanged) {
return;
}
this._isHandlingScrollEvent = true;
try {
const otherEditor = editor === this._originalEditor ? this._modifiedEditor : this._originalEditor;
otherEditor.setScrollPosition({
scrollLeft: e.scrollLeft,
scrollTop: e.scrollTop
});
} finally {
this._isHandlingScrollEvent = false;
}
}));
return editor;
}
private _adjustOptionsForLeftHandSide(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const result = this._adjustOptionsForSubEditor(options);
if (!options.renderSideBySide) {
// never wrap hidden editor
result.wordWrapOverride1 = 'off';
result.wordWrapOverride2 = 'off';
} else {
result.wordWrapOverride1 = this._options.get().diffWordWrap;
}
if (options.originalAriaLabel) {
result.ariaLabel = options.originalAriaLabel;
}
result.ariaLabel = this._updateAriaLabel(result.ariaLabel);
result.readOnly = !options.originalEditable;
result.dropIntoEditor = { enabled: !result.readOnly };
result.extraEditorClassName = 'original-in-monaco-diff-editor';
return result;
}
private _adjustOptionsForRightHandSide(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const result = this._adjustOptionsForSubEditor(options);
if (options.modifiedAriaLabel) {
result.ariaLabel = options.modifiedAriaLabel;
}
result.ariaLabel = this._updateAriaLabel(result.ariaLabel);
result.wordWrapOverride1 = this._options.get().diffWordWrap;
result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH;
result.scrollbar!.verticalHasArrows = false;
result.extraEditorClassName = 'modified-in-monaco-diff-editor';
return result;
}
private _adjustOptionsForSubEditor(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const clonedOptions = {
...options,
dimension: {
height: 0,
width: 0
},
};
clonedOptions.inDiffEditor = true;
clonedOptions.automaticLayout = false;
// Clone scrollbar options before changing them
clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) };
clonedOptions.scrollbar.vertical = 'visible';
clonedOptions.folding = false;
clonedOptions.codeLens = this._options.get().diffCodeLens;
clonedOptions.fixedOverflowWidgets = true;
// clonedOptions.lineDecorationsWidth = '2ch';
// Clone minimap options before changing them
clonedOptions.minimap = { ...(clonedOptions.minimap || {}) };
clonedOptions.minimap.enabled = false;
return clonedOptions;
}
private _updateAriaLabel(ariaLabel: string | undefined): string | undefined {
const ariaNavigationTip = localize('diff-aria-navigation-tip', ' use Shift + F7 to navigate changes');
if (this._options.get().accessibilityVerbose) {
return ariaLabel + ariaNavigationTip;
} else if (ariaLabel) {
return ariaLabel.replaceAll(ariaNavigationTip, '');
}
return undefined;
}
protected override get _targetEditor(): CodeEditorWidget { return this._modifiedEditor; }
override getEditorType(): string { return EditorType.IDiffEditor; }
override onVisible(): void {
// TODO: Only compute diffs when diff editor is visible
this._originalEditor.onVisible();
this._modifiedEditor.onVisible();
}
override onHide(): void {
this._originalEditor.onHide();
this._modifiedEditor.onHide();
}
override layout(dimension?: IDimension | undefined): void {
this._rootSizeObserver.observe(dimension);
}
override hasTextFocus(): boolean {
return this._originalEditor.hasTextFocus() || this._modifiedEditor.hasTextFocus();
}
override saveViewState(): IDiffEditorViewState | null {
return null;
//throw new Error('Method not implemented.');
}
override restoreViewState(state: IDiffEditorViewState | null): void {
//throw new Error('Method not implemented.');
}
override getModel(): IDiffEditorModel | null { return this._model.get(); }
override setModel(model: IDiffEditorModel | null): void {
this._originalEditor.setModel(model ? model.original : null);
this._modifiedEditor.setModel(model ? model.modified : null);
this._model.set(model, undefined);
this._diffModel.set(model ? new DiffModel(
model,
this._options.map(o => o.ignoreTrimWhitespace),
this._options.map(o => o.maxComputationTime),
this._options.map(o => o.experimental.collapseUnchangedRegions!),
this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider, this._options.get())
) : null, undefined);
}
override updateOptions(_newOptions: IDiffEditorOptions): void {
const newOptions = validateDiffEditorOptions(_newOptions, this._options.get());
this._options.set(newOptions, undefined);
this._modifiedEditor.updateOptions(this._adjustOptionsForRightHandSide(_newOptions));
this._originalEditor.updateOptions(this._adjustOptionsForLeftHandSide(_newOptions));
}
getContainerDomNode(): HTMLElement { return this._domElement; }
getOriginalEditor(): ICodeEditor { return this._originalEditor; }
getModifiedEditor(): ICodeEditor { return this._modifiedEditor; }
setBoundarySashes(sashes: IBoundarySashes): void {
this._sash.setBoundarySashes(sashes);
}
readonly onDidUpdateDiff: Event<void> = e => {
return { dispose: () => { } };
};
get ignoreTrimWhitespace(): boolean {
return this._options.get().ignoreTrimWhitespace;
}
get maxComputationTime(): number {
return this._options.get().maxComputationTime;
}
get renderSideBySide(): boolean {
return this._options.get().renderSideBySide;
}
getLineChanges(): ILineChange[] | null {
return null;
//throw new Error('Method not implemented.');
}
getDiffComputationResult(): IDiffComputationResult | null {
return null;
//throw new Error('Method not implemented.');
}
getDiffLineInformationForOriginal(lineNumber: number): IDiffLineInformation | null {
return null;
//throw new Error('Method not implemented.');
}
getDiffLineInformationForModified(lineNumber: number): IDiffLineInformation | null {
return null;
//throw new Error('Method not implemented.');
}
private _goTo(diff: LineRangeMapping): void {
this._modifiedEditor.setPosition(new Position(diff.modifiedRange.startLineNumber, 1));
this._modifiedEditor.revealRangeInCenter(diff.modifiedRange.toExclusiveRange());
}
goToDiff(target: 'previous' | 'next'): void {
const diffs = this._diffModel.get()?.diff.get()?.changes;
if (!diffs || diffs.length === 0) {
return;
}
const curLineNumber = this._modifiedEditor.getPosition()!.lineNumber;
let diff: LineRangeMapping | undefined;
if (target === 'next') {
diff = diffs.find(d => d.modifiedRange.startLineNumber > curLineNumber) ?? diffs[0];
} else {
diff = findLast(diffs, d => d.modifiedRange.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1];
}
this._goTo(diff);
}
revealFirstDiff(): void {
const diffModel = this._diffModel.get();
if (!diffModel) {
return;
}
// wait for the diff computation to finish
waitForState(diffModel.isDiffUpToDate, s => s).then(() => {
const diffs = diffModel.diff.get()?.changes;
if (!diffs || diffs.length === 0) {
return;
}
this._goTo(diffs[0]);
});
}
}
function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions {
return {
enableSplitViewResizing: validateBooleanOption(options.enableSplitViewResizing, defaults.enableSplitViewResizing),
splitViewDefaultRatio: clampedFloat(options.splitViewDefaultRatio, 0.5, 0.1, 0.9),
renderSideBySide: validateBooleanOption(options.renderSideBySide, defaults.renderSideBySide),
renderMarginRevertIcon: validateBooleanOption(options.renderMarginRevertIcon, defaults.renderMarginRevertIcon),
maxComputationTime: clampedInt(options.maxComputationTime, defaults.maxComputationTime, 0, Constants.MAX_SAFE_SMALL_INTEGER),
maxFileSize: clampedInt(options.maxFileSize, defaults.maxFileSize, 0, Constants.MAX_SAFE_SMALL_INTEGER),
ignoreTrimWhitespace: validateBooleanOption(options.ignoreTrimWhitespace, defaults.ignoreTrimWhitespace),
renderIndicators: validateBooleanOption(options.renderIndicators, defaults.renderIndicators),
originalEditable: validateBooleanOption(options.originalEditable, defaults.originalEditable),
diffCodeLens: validateBooleanOption(options.diffCodeLens, defaults.diffCodeLens),
renderOverviewRuler: validateBooleanOption(options.renderOverviewRuler, defaults.renderOverviewRuler),
diffWordWrap: validateStringSetOption<'off' | 'on' | 'inherit'>(options.diffWordWrap, defaults.diffWordWrap, ['off', 'on', 'inherit']),
diffAlgorithm: validateStringSetOption(options.diffAlgorithm, defaults.diffAlgorithm, ['legacy', 'advanced'], { 'smart': 'legacy', 'experimental': 'advanced' }),
accessibilityVerbose: validateBooleanOption(options.accessibilityVerbose, defaults.accessibilityVerbose),
experimental: {
collapseUnchangedRegions: validateBooleanOption(options.experimental?.collapseUnchangedRegions, defaults.experimental.collapseUnchangedRegions!),
},
};
}

View file

@ -0,0 +1,344 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, ITransaction, derived, observableSignal, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable';
import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Range } from 'vs/editor/common/core/range';
import { IDocumentDiff, IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/standardLinesDiffComputer';
import { IDiffEditorModel } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper';
import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos';
import { lengthAdd, lengthDiffNonNegative, lengthOfRange, lengthToPosition, lengthZero, positionToLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length';
export class DiffModel extends Disposable {
private readonly _isDiffUpToDate = observableValue<boolean>('isDiffUpToDate', false);
public readonly isDiffUpToDate: IObservable<boolean> = this._isDiffUpToDate;
private readonly _diff = observableValue<IDocumentDiff | undefined>('diff', undefined);
public readonly diff: IObservable<IDocumentDiff | undefined> = this._diff;
private readonly _unchangedRegions = observableValue<{ regions: UnchangedRegion[]; originalDecorationIds: string[]; modifiedDecorationIds: string[] }>('unchangedRegion', { regions: [], originalDecorationIds: [], modifiedDecorationIds: [] });
public readonly unchangedRegions: IObservable<UnchangedRegion[]> = derived('unchangedRegions', r =>
this.hideUnchangedRegions.read(r) ? this._unchangedRegions.read(r).regions : []
);
constructor(
model: IDiffEditorModel,
ignoreTrimWhitespace: IObservable<boolean>,
maxComputationTimeMs: IObservable<number>,
private readonly hideUnchangedRegions: IObservable<boolean>,
documentDiffProvider: IDocumentDiffProvider,
) {
super();
const contentChangedSignal = observableSignal('contentChangedSignal');
const debouncer = this._register(new RunOnceScheduler(() => contentChangedSignal.trigger(undefined), 200));
this._register(model.modified.onDidChangeContent((e) => {
const diff = this._diff.get();
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
this._diff.set(
applyModifiedEdits(diff, textEdits, model.original, model.modified),
undefined
);
debouncer.schedule();
}));
this._register(model.original.onDidChangeContent((e) => {
const diff = this._diff.get();
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
this._diff.set(
applyOriginalEdits(diff, textEdits, model.original, model.modified),
undefined
);
debouncer.schedule();
}));
const documentDiffProviderOptionChanged = observableSignalFromEvent('documentDiffProviderOptionChanged', documentDiffProvider.onDidChange);
this._register(autorunWithStore2('compute diff', async (reader, store) => {
debouncer.cancel();
contentChangedSignal.read(reader);
documentDiffProviderOptionChanged.read(reader);
const ignoreTrimWhitespaceVal = ignoreTrimWhitespace.read(reader);
const maxComputationTimeMsVal = maxComputationTimeMs.read(reader);
this._isDiffUpToDate.set(false, undefined);
let originalTextEditInfos: TextEditInfo[] = [];
store.add(model.original.onDidChangeContent((e) => {
const edits = TextEditInfo.fromModelContentChanges(e.changes);
originalTextEditInfos = combineTextEditInfos(originalTextEditInfos, edits);
}));
let modifiedTextEditInfos: TextEditInfo[] = [];
store.add(model.modified.onDidChangeContent((e) => {
const edits = TextEditInfo.fromModelContentChanges(e.changes);
modifiedTextEditInfos = combineTextEditInfos(modifiedTextEditInfos, edits);
}));
let result = await documentDiffProvider.computeDiff(model.original, model.modified, {
ignoreTrimWhitespace: ignoreTrimWhitespaceVal,
maxComputationTimeMs: maxComputationTimeMsVal,
});
result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified);
result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified);
const newUnchangedRegions = UnchangedRegion.fromDiffs(result.changes, model.original.getLineCount(), model.modified.getLineCount());
// Transfer state from cur state
const lastUnchangedRegions = this._unchangedRegions.get();
const lastUnchangedRegionsOrigRanges = lastUnchangedRegions.originalDecorationIds
.map(id => model.original.getDecorationRange(id))
.filter(r => !!r)
.map(r => LineRange.fromRange(r!));
const lastUnchangedRegionsModRanges = lastUnchangedRegions.modifiedDecorationIds
.map(id => model.modified.getDecorationRange(id))
.filter(r => !!r)
.map(r => LineRange.fromRange(r!));
for (const r of newUnchangedRegions) {
for (let i = 0; i < lastUnchangedRegions.regions.length; i++) {
if (r.originalRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i])
&& r.modifiedRange.intersectsStrict(lastUnchangedRegionsModRanges[i])) {
r.setState(
lastUnchangedRegions.regions[i].visibleLineCountTop.get(),
lastUnchangedRegions.regions[i].visibleLineCountBottom.get(),
undefined,
);
break;
}
}
}
const originalDecorationIds = model.original.deltaDecorations(
lastUnchangedRegions.originalDecorationIds,
newUnchangedRegions.map(r => ({ range: r.originalRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
);
const modifiedDecorationIds = model.modified.deltaDecorations(
lastUnchangedRegions.modifiedDecorationIds,
newUnchangedRegions.map(r => ({ range: r.modifiedRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
);
transaction(tx => {
this._diff.set(result, tx);
this._isDiffUpToDate.set(true, tx);
this._unchangedRegions.set(
{
regions: newUnchangedRegions,
originalDecorationIds,
modifiedDecorationIds
},
tx
);
});
}));
}
public revealModifiedLine(lineNumber: number, tx: ITransaction): void {
const unchangedRegions = this._unchangedRegions.get().regions;
for (const r of unchangedRegions) {
if (r.getHiddenModifiedRange(undefined).contains(lineNumber)) {
r.showAll(tx); // TODO only unhide what is needed
return;
}
}
}
public revealOriginalLine(lineNumber: number, tx: ITransaction): void {
const unchangedRegions = this._unchangedRegions.get().regions;
for (const r of unchangedRegions) {
if (r.getHiddenOriginalRange(undefined).contains(lineNumber)) {
r.showAll(tx); // TODO only unhide what is needed
return;
}
}
}
}
export class UnchangedRegion {
public static fromDiffs(changes: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): UnchangedRegion[] {
const inversedMappings = LineRangeMapping.inverse(changes, originalLineCount, modifiedLineCount);
const result: UnchangedRegion[] = [];
const minHiddenLineCount = 3;
const minContext = 3;
for (const mapping of inversedMappings) {
let origStart = mapping.originalRange.startLineNumber;
let modStart = mapping.modifiedRange.startLineNumber;
let length = mapping.originalRange.length;
if (origStart === 1 && length > minContext + minHiddenLineCount) {
length -= minContext;
result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
} else if (origStart + length === originalLineCount + 1 && length > minContext + minHiddenLineCount) {
origStart += minContext;
modStart += minContext;
length -= minContext;
result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
} else if (length > minContext * 2 + minHiddenLineCount) {
origStart += minContext;
modStart += minContext;
length -= minContext * 2;
result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
}
}
return result;
}
public get originalRange(): LineRange {
return LineRange.ofLength(this.originalLineNumber, this.lineCount);
}
public get modifiedRange(): LineRange {
return LineRange.ofLength(this.modifiedLineNumber, this.lineCount);
}
private readonly _visibleLineCountTop = observableValue<number>('visibleLineCountTop', 0);
public readonly visibleLineCountTop: IObservable<number> = this._visibleLineCountTop;
private readonly _visibleLineCountBottom = observableValue<number>('visibleLineCountBottom', 0);
public readonly visibleLineCountBottom: IObservable<number> = this._visibleLineCountBottom;
constructor(
public readonly originalLineNumber: number,
public readonly modifiedLineNumber: number,
public readonly lineCount: number,
visibleLineCountTop: number,
visibleLineCountBottom: number,
) {
this._visibleLineCountTop.set(visibleLineCountTop, undefined);
this._visibleLineCountBottom.set(visibleLineCountBottom, undefined);
}
public getHiddenOriginalRange(reader: IReader | undefined): LineRange {
return LineRange.ofLength(
this.originalLineNumber + this._visibleLineCountTop.read(reader),
this.lineCount - this._visibleLineCountTop.read(reader) - this._visibleLineCountBottom.read(reader),
);
}
public getHiddenModifiedRange(reader: IReader | undefined): LineRange {
return LineRange.ofLength(
this.modifiedLineNumber + this._visibleLineCountTop.read(reader),
this.lineCount - this._visibleLineCountTop.read(reader) - this._visibleLineCountBottom.read(reader),
);
}
public showMoreAbove(tx: ITransaction | undefined): void {
const maxVisibleLineCountTop = this.lineCount - this._visibleLineCountBottom.get();
this._visibleLineCountTop.set(Math.min(this._visibleLineCountTop.get() + 10, maxVisibleLineCountTop), tx);
}
public showMoreBelow(tx: ITransaction | undefined): void {
const maxVisibleLineCountBottom = this.lineCount - this._visibleLineCountTop.get();
this._visibleLineCountBottom.set(Math.min(this._visibleLineCountBottom.get() + 10, maxVisibleLineCountBottom), tx);
}
public showAll(tx: ITransaction | undefined): void {
this._visibleLineCountBottom.set(this.lineCount - this._visibleLineCountTop.get(), tx);
}
public setState(visibleLineCountTop: number, visibleLineCountBottom: number, tx: ITransaction | undefined): void {
visibleLineCountTop = Math.min(visibleLineCountTop, this.lineCount);
visibleLineCountBottom = Math.min(visibleLineCountBottom, this.lineCount - visibleLineCountTop);
this._visibleLineCountTop.set(visibleLineCountTop, tx);
this._visibleLineCountBottom.set(visibleLineCountBottom, tx);
}
}
function applyOriginalEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], originalTextModel: ITextModel, modifiedTextModel: ITextModel): IDocumentDiff {
if (textEdits.length === 0) {
return diff;
}
const diffTextEdits = diff.changes.flatMap(c => c.innerChanges!.map(c => new TextEditInfo(
positionToLength(c.modifiedRange.getStartPosition()),
positionToLength(c.modifiedRange.getEndPosition()),
lengthOfRange(c.originalRange).toLength(),
)));
const combined = combineTextEditInfos(diffTextEdits, textEdits);
let lastModifiedEndOffset = lengthZero;
let lastOriginalEndOffset = lengthZero;
const rangeMappings = combined.map(c => {
const originalStartOffset = lengthAdd(lastOriginalEndOffset, lengthDiffNonNegative(lastModifiedEndOffset, c.startOffset));
lastModifiedEndOffset = c.endOffset;
lastOriginalEndOffset = lengthAdd(originalStartOffset, c.newLength);
return new RangeMapping(
Range.fromPositions(lengthToPosition(originalStartOffset), lengthToPosition(lastOriginalEndOffset)),
Range.fromPositions(lengthToPosition(c.startOffset), lengthToPosition(c.endOffset)),
);
});
const changes = lineRangeMappingFromRangeMappings(
rangeMappings,
originalTextModel.getLinesContent(),
modifiedTextModel.getLinesContent(),
);
return {
identical: false,
quitEarly: false,
changes,
};
}
function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], originalTextModel: ITextModel, modifiedTextModel: ITextModel): IDocumentDiff {
if (textEdits.length === 0) {
return diff;
}
const diffTextEdits = diff.changes.flatMap(c => c.innerChanges!.map(c => new TextEditInfo(
positionToLength(c.originalRange.getStartPosition()),
positionToLength(c.originalRange.getEndPosition()),
lengthOfRange(c.modifiedRange).toLength(),
)));
const combined = combineTextEditInfos(diffTextEdits, textEdits);
let lastOriginalEndOffset = lengthZero;
let lastModifiedEndOffset = lengthZero;
const rangeMappings = combined.map(c => {
const modifiedStartOffset = lengthAdd(lastModifiedEndOffset, lengthDiffNonNegative(lastOriginalEndOffset, c.startOffset));
lastOriginalEndOffset = c.endOffset;
lastModifiedEndOffset = lengthAdd(modifiedStartOffset, c.newLength);
return new RangeMapping(
Range.fromPositions(lengthToPosition(c.startOffset), lengthToPosition(c.endOffset)),
Range.fromPositions(lengthToPosition(modifiedStartOffset), lengthToPosition(lastModifiedEndOffset)),
);
});
const changes = lineRangeMappingFromRangeMappings(
rangeMappings,
originalTextModel.getLinesContent(),
modifiedTextModel.getLinesContent(),
);
return {
identical: false,
quitEarly: false,
changes,
};
}

View file

@ -0,0 +1,237 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ArrayQueue } from 'vs/base/common/arrays';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, observableSignalFromEvent, derived } from 'vs/base/common/observable';
import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { IViewZone } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
import { joinCombine } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
export class ViewZoneAlignment extends Disposable {
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diffModel: IObservable<DiffModel | null>,
) {
super();
let isChangingViewZones = false;
const origViewZonesChanged = observableSignalFromEvent(
'origViewZonesChanged',
e => this._originalEditor.onDidChangeViewZones((args) => { if (!isChangingViewZones) { e(args); } })
);
const modViewZonesChanged = observableSignalFromEvent(
'modViewZonesChanged',
e => this._modifiedEditor.onDidChangeViewZones((args) => { if (!isChangingViewZones) { e(args); } })
);
const alignmentViewZoneIdsOrig = new Set<string>();
const alignmentViewZoneIdsMod = new Set<string>();
const alignments = derived<IRangeAlignment[] | null>('alignments', (reader) => {
const diff = this._diffModel.read(reader)?.diff.read(reader);
if (!diff) { return null; }
origViewZonesChanged.read(reader);
modViewZonesChanged.read(reader);
return computeRangeAlignment(this._originalEditor, this._modifiedEditor, diff.changes, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod);
});
function createFakeLinesDiv(): HTMLElement {
const r = document.createElement('div');
r.className = 'diagonal-fill';
return r;
}
const alignmentViewZones = derived<{ orig: IViewZone[]; mod: IViewZone[] }>('alignment viewzones', (reader) => {
const alignments_ = alignments.read(reader);
const origViewZones: IViewZone[] = [];
const modViewZones: IViewZone[] = [];
if (alignments_) {
for (const a of alignments_) {
const delta = a.modifiedHeightInPx - a.originalHeightInPx;
if (delta > 0) {
origViewZones.push({
afterLineNumber: a.originalRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: delta,
});
} else {
modViewZones.push({
afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: -delta,
});
}
}
}
return { orig: origViewZones, mod: modViewZones };
});
this._register(autorunWithStore2('alignment viewzones', (reader) => {
const alignmentViewZones_ = alignmentViewZones.read(reader);
isChangingViewZones = true;
this._originalEditor.changeViewZones((aOrig) => {
for (const id of alignmentViewZoneIdsOrig) { aOrig.removeZone(id); }
alignmentViewZoneIdsOrig.clear();
for (const z of alignmentViewZones_.orig) { alignmentViewZoneIdsOrig.add(aOrig.addZone(z)); }
});
this._modifiedEditor.changeViewZones(aMod => {
for (const id of alignmentViewZoneIdsMod) { aMod.removeZone(id); }
alignmentViewZoneIdsMod.clear();
for (const z of alignmentViewZones_.mod) { alignmentViewZoneIdsMod.add(aMod.addZone(z)); }
});
isChangingViewZones = false;
}));
}
}
interface AdditionalLineHeightInfo {
lineNumber: number;
heightInPx: number;
}
function getAdditionalLineHeights(editor: CodeEditorWidget, viewZonesToIgnore: ReadonlySet<string>): readonly AdditionalLineHeightInfo[] {
const viewZoneHeights: { lineNumber: number; heightInPx: number }[] = [];
const wrappingZoneHeights: { lineNumber: number; heightInPx: number }[] = [];
const hasWrapping = editor.getOption(EditorOption.wrappingInfo).wrappingColumn !== -1;
const coordinatesConverter = editor._getViewModel()!.coordinatesConverter;
if (hasWrapping) {
for (let i = 1; i <= editor.getModel()!.getLineCount(); i++) {
const lineCount = coordinatesConverter.getModelLineViewLineCount(i);
if (lineCount > 1) {
wrappingZoneHeights.push({ lineNumber: i, heightInPx: lineCount - 1 });
}
}
}
for (const w of editor.getWhitespaces()) {
if (viewZonesToIgnore.has(w.id)) {
continue;
}
const modelLineNumber = coordinatesConverter.convertViewPositionToModelPosition(
new Position(w.afterLineNumber, 1)
).lineNumber;
viewZoneHeights.push({ lineNumber: modelLineNumber, heightInPx: w.height });
}
const result = joinCombine(
viewZoneHeights,
wrappingZoneHeights,
v => v.lineNumber,
(v1, v2) => ({ lineNumber: v1.lineNumber, heightInPx: v1.heightInPx + v2.heightInPx })
);
return result;
}
interface IRangeAlignment {
originalRange: LineRange;
modifiedRange: LineRange;
// accounts for foreign viewzones and line wrapping
originalHeightInPx: number;
modifiedHeightInPx: number;
}
function computeRangeAlignment(
originalEditor: CodeEditorWidget,
modifiedEditor: CodeEditorWidget,
diffs: LineRangeMapping[],
originalEditorAlignmentViewZones: ReadonlySet<string>,
modifiedEditorAlignmentViewZones: ReadonlySet<string>,
): IRangeAlignment[] {
const originalLineHeightOverrides = new ArrayQueue(getAdditionalLineHeights(originalEditor, originalEditorAlignmentViewZones));
const modifiedLineHeightOverrides = new ArrayQueue(getAdditionalLineHeights(modifiedEditor, modifiedEditorAlignmentViewZones));
const origLineHeight = originalEditor.getOption(EditorOption.lineHeight);
const modLineHeight = modifiedEditor.getOption(EditorOption.lineHeight);
const result: IRangeAlignment[] = [];
let lastOriginalLineNumber = 0;
let lastModifiedLineNumber = 0;
function handleAlignmentsOutsideOfDiffs(untilOriginalLineNumberExclusive: number, untilModifiedLineNumberExclusive: number) {
while (true) {
let origNext = originalLineHeightOverrides.peek();
let modNext = modifiedLineHeightOverrides.peek();
if (origNext && origNext.lineNumber >= untilOriginalLineNumberExclusive) {
origNext = undefined;
}
if (modNext && modNext.lineNumber >= untilModifiedLineNumberExclusive) {
modNext = undefined;
}
if (!origNext && !modNext) {
break;
}
const distOrig = origNext ? origNext.lineNumber - lastOriginalLineNumber : Number.MAX_VALUE;
const distNext = modNext ? modNext.lineNumber - lastModifiedLineNumber : Number.MAX_VALUE;
if (distOrig < distNext) {
originalLineHeightOverrides.dequeue();
modNext = {
lineNumber: origNext!.lineNumber - lastOriginalLineNumber + lastModifiedLineNumber,
heightInPx: 0,
};
} else if (distOrig > distNext) {
modifiedLineHeightOverrides.dequeue();
origNext = {
lineNumber: modNext!.lineNumber - lastModifiedLineNumber + lastOriginalLineNumber,
heightInPx: 0,
};
} else {
originalLineHeightOverrides.dequeue();
modifiedLineHeightOverrides.dequeue();
}
result.push({
originalRange: LineRange.ofLength(origNext!.lineNumber, 1),
modifiedRange: LineRange.ofLength(modNext!.lineNumber, 1),
originalHeightInPx: origLineHeight + origNext!.heightInPx,
modifiedHeightInPx: modLineHeight + modNext!.heightInPx,
});
}
}
for (const c of diffs) {
handleAlignmentsOutsideOfDiffs(c.originalRange.startLineNumber, c.modifiedRange.startLineNumber);
const originalAdditionalHeight = originalLineHeightOverrides
.takeWhile(v => v.lineNumber < c.originalRange.endLineNumberExclusive)
?.reduce((p, c) => p + c.heightInPx, 0) ?? 0;
const modifiedAdditionalHeight = modifiedLineHeightOverrides
.takeWhile(v => v.lineNumber < c.modifiedRange.endLineNumberExclusive)
?.reduce((p, c) => p + c.heightInPx, 0) ?? 0;
result.push({
originalRange: c.originalRange,
modifiedRange: c.modifiedRange,
originalHeightInPx: c.originalRange.length * origLineHeight + originalAdditionalHeight,
modifiedHeightInPx: c.modifiedRange.length * modLineHeight + modifiedAdditionalHeight,
});
lastOriginalLineNumber = c.originalRange.endLineNumberExclusive;
lastModifiedLineNumber = c.modifiedRange.endLineNumberExclusive;
}
handleAlignmentsOutsideOfDiffs(Number.MAX_VALUE, Number.MAX_VALUE);
return result;
}

View file

@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventType, addDisposableListener, addStandardDisposableListener, h } from 'vs/base/browser/dom';
import { createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { Color } from 'vs/base/common/color';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, derived, observableFromEvent } from 'vs/base/common/observable';
import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
import { appendRemoveOnDispose } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager';
import { defaultInsertColor, defaultRemoveColor, diffInserted, diffOverviewRulerInserted, diffOverviewRulerRemoved, diffRemoved } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class OverviewRulerPart extends Disposable {
public static readonly ONE_OVERVIEW_WIDTH = 15;
public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerPart.ONE_OVERVIEW_WIDTH * 2;
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _rootElement: HTMLElement,
private readonly _diffModel: IObservable<DiffModel | null>,
private readonly _rootWidth: IObservable<number>,
private readonly _rootHeight: IObservable<number>,
private readonly _modifiedEditorLayoutInfo: IObservable<EditorLayoutInfo | null>,
public readonly renderOverviewRuler: IObservable<boolean>,
@IThemeService private readonly _themeService: IThemeService,
) {
super();
const currentColorTheme = observableFromEvent(this._themeService.onDidColorThemeChange, () => this._themeService.getColorTheme());
const currentColors = derived('colors', reader => {
const theme = currentColorTheme.read(reader);
const insertColor = theme.getColor(diffOverviewRulerInserted) || (theme.getColor(diffInserted) || defaultInsertColor).transparent(2);
const removeColor = theme.getColor(diffOverviewRulerRemoved) || (theme.getColor(diffRemoved) || defaultRemoveColor).transparent(2);
return { insertColor, removeColor };
});
const scrollTopObservable = observableFromEvent(this._modifiedEditor.onDidScrollChange, () => this._modifiedEditor.getScrollTop());
const scrollHeightObservable = observableFromEvent(this._modifiedEditor.onDidScrollChange, () => this._modifiedEditor.getScrollHeight());
// overview ruler
this._register(autorunWithStore2('create diff editor overview ruler if enabled', (reader, store) => {
if (!this.renderOverviewRuler.read(reader)) {
return;
}
const viewportDomElement = createFastDomNode(document.createElement('div'));
viewportDomElement.setClassName('diffViewport');
viewportDomElement.setPosition('absolute');
const diffOverviewRoot = h('div.diffOverview', {
style: { position: 'absolute', top: '0px', width: OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH + 'px' }
}).root;
store.add(appendRemoveOnDispose(diffOverviewRoot, viewportDomElement.domNode));
store.add(addStandardDisposableListener(diffOverviewRoot, EventType.POINTER_DOWN, (e) => {
this._modifiedEditor.delegateVerticalScrollbarPointerDown(e);
}));
store.add(addDisposableListener(diffOverviewRoot, EventType.MOUSE_WHEEL, (e: IMouseWheelEvent) => {
this._modifiedEditor.delegateScrollFromMouseWheelEvent(e);
}, { passive: false }));
store.add(appendRemoveOnDispose(this._rootElement, diffOverviewRoot));
store.add(autorunWithStore2('recreate overview rules when model changes', (reader, store) => {
const m = this._diffModel.read(reader);
const originalOverviewRuler = this._originalEditor.createOverviewRuler('original diffOverviewRuler');
if (originalOverviewRuler) {
store.add(originalOverviewRuler);
store.add(appendRemoveOnDispose(diffOverviewRoot, originalOverviewRuler.getDomNode()));
}
const modifiedOverviewRuler = this._modifiedEditor.createOverviewRuler('modified diffOverviewRuler');
if (modifiedOverviewRuler) {
store.add(modifiedOverviewRuler);
store.add(appendRemoveOnDispose(diffOverviewRoot, modifiedOverviewRuler.getDomNode()));
}
if (!originalOverviewRuler || !modifiedOverviewRuler) {
// probably no model
return;
}
store.add(autorun('set overview ruler zones', (reader) => {
const colors = currentColors.read(reader);
const diff = m?.diff.read(reader)?.changes;
function createZones(ranges: LineRange[], color: Color) {
return ranges
.filter(d => d.length > 0)
.map(r => new OverviewRulerZone(r.startLineNumber, r.endLineNumberExclusive, r.length, color.toString()));
}
originalOverviewRuler?.setZones(createZones((diff || []).map(d => d.originalRange), colors.removeColor));
modifiedOverviewRuler?.setZones(createZones((diff || []).map(d => d.modifiedRange), colors.insertColor));
}));
store.add(autorun('layout overview ruler', (reader) => {
const height = this._rootHeight.read(reader);
const width = this._rootWidth.read(reader);
const layoutInfo = this._modifiedEditorLayoutInfo.read(reader);
if (layoutInfo) {
const freeSpace = OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH - 2 * OverviewRulerPart.ONE_OVERVIEW_WIDTH;
originalOverviewRuler.setLayout({
top: 0,
height: height,
right: freeSpace + OverviewRulerPart.ONE_OVERVIEW_WIDTH,
width: OverviewRulerPart.ONE_OVERVIEW_WIDTH,
});
modifiedOverviewRuler.setLayout({
top: 0,
height: height,
right: 0,
width: OverviewRulerPart.ONE_OVERVIEW_WIDTH,
});
const scrollTop = scrollTopObservable.read(reader);
const scrollHeight = scrollHeightObservable.read(reader);
const computedAvailableSize = Math.max(0, layoutInfo.height);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * 0);
const computedRatio = scrollHeight > 0 ? (computedRepresentableSize / scrollHeight) : 0;
const computedSliderSize = Math.max(0, Math.floor(layoutInfo.height * computedRatio));
const computedSliderPosition = Math.floor(scrollTop * computedRatio);
viewportDomElement.setTop(computedSliderPosition);
viewportDomElement.setHeight(computedSliderSize);
} else {
viewportDomElement.setTop(0);
viewportDomElement.setHeight(0);
}
diffOverviewRoot.style.height = height + 'px';
diffOverviewRoot.style.left = (width - OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH) + 'px';
viewportDomElement.setWidth(OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH);
}));
}));
}));
}
}

View file

@ -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.
*--------------------------------------------------------------------------------------------*/
.diff-hidden-lines.showTop {
margin: -5px 0;
}
.diff-hidden-lines.showTop .top {
height: 7px;
background-color: var(--vscode-diffEditor-unchangedRegionBackground);
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,10 5,0 10,10' style='fill:white' /> </svg>") repeat 0px 0px;
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,10 5,0 10,10' style='fill:white' /> </svg>") repeat 0px 0px;
}
.diff-hidden-lines.showBottom .bottom {
height: 7px;
background-color: var(--vscode-diffEditor-unchangedRegionBackground);
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,0 5,10 10,0' style='fill:white' /> </svg>") repeat 10px 0px;
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,0 5,10 10,0' style='fill:white' /> </svg>") repeat 10px 0px;
}
.diff-hidden-lines .center {
background: var(--vscode-diffEditor-unchangedRegionBackground);
padding: 0px 3px;
overflow: hidden;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
}
.diff-hidden-lines .center > span,
.diff-hidden-lines .center > a {
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
}
.diff-hidden-lines .center > a {
text-decoration: none;
}
.diff-hidden-lines .center > a:hover {
cursor: pointer;
color: var(--vscode-editorLink-activeForeground) !important;
}
.diff-hidden-lines .center > a:hover .codicon {
color: var(--vscode-editorLink-activeForeground) !important;
}
.diff-hidden-lines .center .codicon {
vertical-align: middle;
color: currentColor !important;
color: var(--vscode-editorCodeLens-foreground);
}
.merge-editor-conflict-actions > a:hover .codicon::before {
cursor: pointer;
}

View file

@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $, h, reset } from 'vs/base/browser/dom';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, transaction, constObservable } from 'vs/base/common/observable';
import { autorun, autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { isDefined } from 'vs/base/common/types';
import { ICodeEditor, IOverlayWidget, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
export class UnchangedRangesFeature extends Disposable {
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diffModel: IObservable<DiffModel | null>,
) {
super();
const unchangedRegionViewZoneIdsOrig: string[] = [];
const unchangedRegionViewZoneIdsMod: string[] = [];
this._register(this._originalEditor.onDidChangeCursorPosition(e => {
const m = this._diffModel.get();
transaction(tx => {
for (const s of this._originalEditor.getSelections() || []) {
m?.revealOriginalLine(s.getStartPosition().lineNumber, tx);
m?.revealOriginalLine(s.getEndPosition().lineNumber, tx);
}
});
}));
this._register(this._modifiedEditor.onDidChangeCursorPosition(e => {
const m = this._diffModel.get();
transaction(tx => {
for (const s of this._modifiedEditor.getSelections() || []) {
m?.revealModifiedLine(s.getStartPosition().lineNumber, tx);
m?.revealModifiedLine(s.getEndPosition().lineNumber, tx);
}
});
}));
this._register(autorunWithStore2('update folded unchanged regions', (reader, store) => {
const unchangedRegions = this._diffModel.read(reader)?.unchangedRegions.read(reader);
if (!unchangedRegions) {
return;
}
// TODO@hediet This might cause unnecessary updates of alignment viewzones if this runs too late
this._originalEditor.changeViewZones((aOrig) => {
this._modifiedEditor.changeViewZones(aMod => {
for (const id of unchangedRegionViewZoneIdsOrig) {
aOrig.removeZone(id);
}
unchangedRegionViewZoneIdsOrig.length = 0;
for (const id of unchangedRegionViewZoneIdsMod) {
aMod.removeZone(id);
}
unchangedRegionViewZoneIdsMod.length = 0;
for (const r of unchangedRegions) {
const atTop = r.modifiedLineNumber !== 1;
const atBottom = r.modifiedRange.endLineNumberExclusive !== this._modifiedEditor.getModel()!.getLineCount() + 1;
const hiddenOriginalRange = r.getHiddenOriginalRange(reader);
const hiddenModifiedRange = r.getHiddenModifiedRange(reader);
if (hiddenOriginalRange.isEmpty) {
continue;
}
store.add(new CollapsedCodeActionsContentWidget(this._originalEditor, aOrig, hiddenOriginalRange.startLineNumber - 1, 30, constObservable<IContentWidgetAction[]>([
{
text: `${hiddenOriginalRange.length} Lines Hidden`
},
{
text: '$(chevron-up) Show More',
async action() { r.showMoreAbove(undefined); },
},
{
text: '$(chevron-down) Show More',
async action() { r.showMoreBelow(undefined); },
},
{
text: '$(close) Show All',
async action() { r.showAll(undefined); },
}
]), unchangedRegionViewZoneIdsOrig, atTop, atBottom));
store.add(new CollapsedCodeActionsContentWidget(this._modifiedEditor, aMod, hiddenModifiedRange.startLineNumber - 1, 30, constObservable<IContentWidgetAction[]>([
{
text: '$(chevron-up) Show More',
async action() { r.showMoreAbove(undefined); },
},
{
text: '$(chevron-down) Show More',
async action() { r.showMoreBelow(undefined); },
},
{
text: '$(close) Show All',
async action() { r.showAll(undefined); },
}
]), unchangedRegionViewZoneIdsMod, atTop, atBottom));
}
});
});
this._originalEditor.setHiddenAreas(unchangedRegions.map(r => r.getHiddenOriginalRange(reader).toInclusiveRange()).filter(isDefined));
this._modifiedEditor.setHiddenAreas(unchangedRegions.map(r => r.getHiddenModifiedRange(reader).toInclusiveRange()).filter(isDefined));
}));
}
}
// TODO@hediet avoid code duplication with FixedZoneWidget in merge editor
abstract class FixedZoneWidget extends Disposable {
private static counter = 0;
private readonly overlayWidgetId = `fixedZoneWidget-${FixedZoneWidget.counter++}`;
private readonly viewZoneId: string;
protected readonly widgetDomNode = h('div.fixed-zone-widget').root;
private readonly overlayWidget: IOverlayWidget = {
getId: () => this.overlayWidgetId,
getDomNode: () => this.widgetDomNode,
getPosition: () => null
};
constructor(
private readonly editor: ICodeEditor,
viewZoneAccessor: IViewZoneChangeAccessor,
afterLineNumber: number,
height: number,
viewZoneIdsToCleanUp: string[],
) {
super();
this.viewZoneId = viewZoneAccessor.addZone({
domNode: document.createElement('div'),
afterLineNumber: afterLineNumber,
heightInPx: height,
onComputedHeight: (height) => {
this.widgetDomNode.style.height = `${height}px`;
},
onDomNodeTop: (top) => {
this.widgetDomNode.style.top = `${top}px`;
},
showInHiddenAreas: true,
});
viewZoneIdsToCleanUp.push(this.viewZoneId);
this.widgetDomNode.style.left = this.editor.getLayoutInfo().contentLeft + 'px';
this.editor.addOverlayWidget(this.overlayWidget);
this._register({
dispose: () => {
this.editor.removeOverlayWidget(this.overlayWidget);
},
});
}
}
class CollapsedCodeActionsContentWidget extends FixedZoneWidget {
private readonly _domNode = h('div.diff-hidden-lines', { className: [this.showTopZigZag ? 'showTop' : '', this.showBottomZigZag ? 'showBottom' : ''].join(' ') }, [
h('div.top'),
h('div.center@content'),
h('div.bottom'),
]);
constructor(
editor: ICodeEditor,
viewZoneAccessor: IViewZoneChangeAccessor,
afterLineNumber: number,
height: number,
items: IObservable<IContentWidgetAction[]>,
viewZoneIdsToCleanUp: string[],
public readonly showTopZigZag: boolean,
public readonly showBottomZigZag: boolean,
) {
super(editor, viewZoneAccessor, afterLineNumber, height, viewZoneIdsToCleanUp);
this.widgetDomNode.appendChild(this._domNode.root);
this._register(autorun('update commands', (reader) => {
const i = items.read(reader);
this.setState(i);
}));
}
private setState(items: IContentWidgetAction[]) {
const children: HTMLElement[] = [];
let isFirst = true;
for (const item of items) {
if (isFirst) {
isFirst = false;
} else {
children.push($('span', undefined, '\u00a0|\u00a0'));
}
const title = renderLabelWithIcons(item.text);
if (item.action) {
children.push($('a', { title: item.tooltip, role: 'button', onclick: () => item.action!() }, ...title));
} else {
children.push($('span', { title: item.tooltip }, ...title));
}
}
reset(this._domNode.content, ...children);
}
}
interface IContentWidgetAction {
text: string;
tooltip?: string;
action?: () => Promise<void>;
}

View file

@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDimension } from 'vs/base/browser/dom';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, ISettableObservable, autorun, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export function joinCombine<T>(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] {
if (arr1.length === 0) {
return arr2;
}
if (arr2.length === 0) {
return arr1;
}
const result: T[] = [];
let i = 0;
let j = 0;
while (i < arr1.length && j < arr2.length) {
const val1 = arr1[i];
const val2 = arr2[j];
const key1 = keySelector(val1);
const key2 = keySelector(val2);
if (key1 < key2) {
result.push(val1);
i++;
} else if (key1 > key2) {
result.push(val2);
j++;
} else {
result.push(combine(val1, val2));
i++;
j++;
}
}
while (i < arr1.length) {
result.push(arr1[i]);
i++;
}
while (j < arr2.length) {
result.push(arr2[j]);
j++;
}
return result;
}
// TODO make utility
export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
const d = new DisposableStore();
const decorationsCollection = editor.createDecorationsCollection();
d.add(autorun(`Apply decorations from ${decorations.debugName}`, reader => {
const d = decorations.read(reader);
decorationsCollection.set(d);
}));
d.add({
dispose: () => {
decorationsCollection.clear();
}
});
return d;
}
export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) {
parent.appendChild(child);
return toDisposable(() => {
parent.removeChild(child);
});
}
export function observableConfigValue<T>(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable<T> {
return observableFromEvent(
(handleChange) => configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(key)) {
handleChange(e);
}
}),
() => configurationService.getValue<T>(key) ?? defaultValue,
);
}
export class ObservableElementSizeObserver extends Disposable {
private readonly elementSizeObserver: ElementSizeObserver;
private readonly _width: ISettableObservable<number>;
public get width(): ISettableObservable<number> { return this._width; }
private readonly _height: ISettableObservable<number>;
public get height(): ISettableObservable<number> { return this._height; }
constructor(element: HTMLElement | null, dimension: IDimension | undefined) {
super();
this.elementSizeObserver = this._register(new ElementSizeObserver(element, dimension));
this._width = observableValue('width', this.elementSizeObserver.getWidth());
this._height = observableValue('height', this.elementSizeObserver.getHeight());
this._register(this.elementSizeObserver.onDidChange(e => transaction(tx => {
this._width.set(this.elementSizeObserver.getWidth(), tx);
this._height.set(this.elementSizeObserver.getHeight(), tx);
})));
}
public observe(dimension?: IDimension): void {
this.elementSizeObserver.observe(dimension);
}
public setAutomaticLayout(automaticLayout: boolean): void {
if (automaticLayout) {
this.elementSizeObserver.startObserving();
} else {
this.elementSizeObserver.stopObserving();
}
}
}

View file

@ -55,7 +55,7 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
readonly onDidUpdate: Event<this> = this._onDidUpdate.event;
private disposed: boolean;
private revealFirst: boolean;
public revealFirst: boolean;
private nextIdx: number;
private ranges: IDiffRange[];
private ignoreSelectionChange: boolean;
@ -78,8 +78,6 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
this.ignoreSelectionChange = false;
this.revealFirst = Boolean(this._options.alwaysRevealFirst);
// hook up to diff editor for diff, disposal, and caret move
this._register(this._editor.onDidDispose(() => this.dispose()));
this._register(this._editor.onDidUpdateDiff(() => this._onDiffUpdated()));
if (this._options.followsCaret) {
@ -91,11 +89,6 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
this.nextIdx = -1;
}));
}
if (this._options.alwaysRevealFirst) {
this._register(this._editor.getModifiedEditor().onDidChangeModel((e) => {
this.revealFirst = true;
}));
}
// init things
this._init();

View file

@ -6,7 +6,9 @@
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { StopWatch } from 'vs/base/common/stopwatch';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ITextModel } from 'vs/editor/common/model';
import { DiffAlgorithmName, IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -35,6 +37,26 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I
return this.diffAlgorithm.computeDiff(original, modified, options);
}
// This significantly speeds up the case when the original file is empty
if (original.getLineCount() === 1 && original.getLineMaxColumn(1) === 1) {
return {
changes: [
new LineRangeMapping(
new LineRange(1, 1),
new LineRange(1, modified.getLineCount()),
[
new RangeMapping(
original.getFullModelRange(),
modified.getFullModelRange(),
)
]
)
],
identical: false,
quitEarly: false,
};
}
const sw = StopWatch.create(true);
const result = await this.editorWorkerService.computeDiff(original.uri, modified.uri, options, this.diffAlgorithm);
const timeMs = sw.elapsed();

View file

@ -202,6 +202,16 @@ const editorConfiguration: IConfigurationNode = {
],
tags: ['experimental'],
},
'diffEditor.experimental.collapseUnchangedRegions': {
type: 'boolean',
default: false,
description: nls.localize('collapseUnchangedRegions', "Controls whether the diff editor shows unchanged regions. Only works when 'diffEditor.experimental.useVersion2' is set."),
},
'diffEditor.experimental.useVersion2': {
type: 'boolean',
default: false,
description: nls.localize('useVersion2', "Controls whether the diff editor uses the new or the old implementation."),
}
}
};

View file

@ -794,6 +794,13 @@ export interface IDiffEditorBaseOptions {
* Whether the diff editor aria label should be verbose.
*/
accessibilityVerbose?: boolean;
experimental?: {
/**
* Defaults to false.
*/
collapseUnchangedRegions?: boolean;
};
}
/**

View file

@ -4,11 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import { BugIndicatingError } from 'vs/base/common/errors';
import { Range } from 'vs/editor/common/core/range';
/**
* A range of lines (1-based).
*/
export class LineRange {
public static fromRange(range: Range): LineRange {
return new LineRange(range.startLineNumber, range.endLineNumber);
}
/**
* @param lineRanges An array of sorted line ranges.
*/
@ -78,6 +83,10 @@ export class LineRange {
return result;
}
public static ofLength(startLineNumber: number, length: number): LineRange {
return new LineRange(startLineNumber, startLineNumber + length);
}
/**
* The start line number.
*/
@ -154,6 +163,10 @@ export class LineRange {
return undefined;
}
public intersectsStrict(other: LineRange): boolean {
return this.startLineNumber < other.endLineNumberExclusive && other.startLineNumber < this.endLineNumberExclusive;
}
public overlapOrTouch(other: LineRange): boolean {
return this.startLineNumber <= other.endLineNumberExclusive && other.startLineNumber <= this.endLineNumberExclusive;
}
@ -161,4 +174,15 @@ export class LineRange {
public equals(b: LineRange): boolean {
return this.startLineNumber === b.startLineNumber && this.endLineNumberExclusive === b.endLineNumberExclusive;
}
public toInclusiveRange(): Range | null {
if (this.isEmpty) {
return null;
}
return new Range(this.startLineNumber, 1, this.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER);
}
public toExclusiveRange(): Range {
return new Range(this.startLineNumber, 1, this.endLineNumberExclusive, 1);
}
}

View file

@ -28,7 +28,7 @@ export interface IDocumentDiffProvider {
*/
export interface IDocumentDiffProviderOptions {
/**
* When set to true, the diff should ignore whitespace changes.i
* When set to true, the diff should ignore whitespace changes.
*/
ignoreTrimWhitespace: boolean;

View file

@ -32,6 +32,34 @@ export class LinesDiff {
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
public static inverse(mapping: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[] {
const result: LineRangeMapping[] = [];
let lastOriginalEndLineNumber = 1;
let lastModifiedEndLineNumber = 1;
for (const m of mapping) {
const r = new LineRangeMapping(
new LineRange(lastOriginalEndLineNumber, m.originalRange.startLineNumber),
new LineRange(lastModifiedEndLineNumber, m.modifiedRange.startLineNumber),
undefined
);
if (!r.modifiedRange.isEmpty) {
result.push(r);
}
lastOriginalEndLineNumber = m.originalRange.endLineNumberExclusive;
lastModifiedEndLineNumber = m.modifiedRange.endLineNumberExclusive;
}
const r = new LineRangeMapping(
new LineRange(lastOriginalEndLineNumber, originalLineCount + 1),
new LineRange(lastModifiedEndLineNumber, modifiedLineCount + 1),
undefined
);
if (!r.modifiedRange.isEmpty) {
result.push(r);
}
return result;
}
/**
* The line range in the original text model.
*/

View file

@ -3,9 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthToObj, toLength } from './length';
import { Range } from 'vs/editor/common/core/range';
import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthOfString, lengthToObj, positionToLength, toLength } from './length';
import { IModelContentChange } from 'vs/editor/common/textModelEvents';
export class TextEditInfo {
public static fromModelContentChanges(changes: IModelContentChange[]): TextEditInfo[] {
// Must be sorted in ascending order
const edits = changes.map(c => {
const range = Range.lift(c.range);
return new TextEditInfo(
positionToLength(range.getStartPosition()),
positionToLength(range.getEndPosition()),
lengthOfString(c.text)
);
}).reverse();
return edits;
}
constructor(
public readonly startOffset: Length,
public readonly endOffset: Length,

View file

@ -14,7 +14,7 @@ import { ResolvedLanguageConfiguration } from 'vs/editor/common/languages/langua
import { AstNode, AstNodeKind } from './ast';
import { TextEditInfo } from './beforeEditPositionMapper';
import { LanguageAgnosticBracketTokens } from './brackets';
import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length';
import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthsToRange, lengthZero, positionToLength, toLength } from './length';
import { parseDocument } from './parser';
import { DenseKeyProvider } from './smallImmutableSet';
import { FastTokenizer, TextBufferTokenizer } from './tokenizer';
@ -103,16 +103,7 @@ export class BracketPairsTree extends Disposable {
}
public handleContentChanged(change: IModelContentChangedEvent) {
// Must be sorted in ascending order
const edits = change.changes.map(c => {
const range = Range.lift(c.range);
return new TextEditInfo(
positionToLength(range.getStartPosition()),
positionToLength(range.getEndPosition()),
lengthOfString(c.text)
);
}).reverse();
const edits = TextEditInfo.fromModelContentChanges(change.changes);
this.handleEdits(edits, false);
}

View file

@ -216,6 +216,14 @@ export function lengthsToRange(lengthStart: Length, lengthEnd: Length): Range {
return new Range(lineCount + 1, colCount + 1, lineCount2 + 1, colCount2 + 1);
}
export function lengthOfRange(range: Range): LengthObj {
if (range.startLineNumber === range.endLineNumber) {
return new LengthObj(0, range.endColumn - range.startColumn);
} else {
return new LengthObj(range.endLineNumber - range.startLineNumber, range.endColumn - 1);
}
}
export function lengthCompare(length1: Length, length2: Length): number {
const l1 = length1 as any as number;
const l2 = length2 as any as number;

View file

@ -198,7 +198,7 @@ export class ToggleAlwaysShowInlineSuggestionToolbar extends Action2 {
group: 'secondary',
order: 10,
}],
toggled: InlineCompletionContextKeys.alwaysShowInlineSuggestionToolbar,
toggled: ContextKeyExpr.equals('config.editor.inlineSuggest.showToolbar', 'always')
});
}

View file

@ -15,7 +15,6 @@ export class InlineCompletionContextKeys extends Disposable {
public static readonly inlineSuggestionVisible = new RawContextKey<boolean>('inlineSuggestionVisible', false, localize('inlineSuggestionVisible', "Whether an inline suggestion is visible"));
public static readonly inlineSuggestionHasIndentation = new RawContextKey<boolean>('inlineSuggestionHasIndentation', false, localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace"));
public static readonly inlineSuggestionHasIndentationLessThanTabSize = new RawContextKey<boolean>('inlineSuggestionHasIndentationLessThanTabSize', true, localize('inlineSuggestionHasIndentationLessThanTabSize', "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab"));
public static readonly alwaysShowInlineSuggestionToolbar = new RawContextKey<boolean>('alwaysShowInlineSuggestionToolbar', false, localize('alwaysShowInlineSuggestionToolbar', "Whether the inline suggestion toolbar should always be visible"));
public static readonly suppressSuggestions = new RawContextKey<boolean | undefined>('inlineSuggestionSuppressSuggestions', undefined, localize('suppressSuggestions', "Whether suggestions should be suppressed for the current suggestion"));
public readonly inlineCompletionVisible = InlineCompletionContextKeys.inlineSuggestionVisible.bindTo(this.contextKeyService);

View file

@ -20,7 +20,7 @@ import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds';
import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget';
import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys';
import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider';
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
@ -191,6 +191,8 @@ export class InlineCompletionsController extends Disposable {
});
}
}));
this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this.instantiationService));
}
/**

View file

@ -12,6 +12,7 @@ import { RunOnceScheduler } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, derived, observableFromEvent } from 'vs/base/common/observable';
import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { OS } from 'vs/base/common/platform';
import { ThemeIcon } from 'vs/base/common/themables';
import 'vs/css!./inlineCompletionsHintsWidget';
@ -35,14 +36,14 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
export class InlineCompletionsHintsWidget extends Disposable {
private readonly showToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar);
private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always');
private sessionPosition: Position | undefined = undefined;
private readonly position = derived('position', reader => {
const ghostText = this.model.ghostText.read(reader);
const ghostText = this.model.read(reader)?.ghostText.read(reader);
if (this.showToolbar.read(reader) !== 'always' || !ghostText) {
if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) {
this.sessionPosition = undefined;
return null;
}
@ -53,37 +54,44 @@ export class InlineCompletionsHintsWidget extends Disposable {
}
const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER));
this.sessionPosition = position;
return position;
});
private readonly contentWidget = this._register(this.instantiationService.createInstance(
InlineSuggestionHintsContentWidget,
this.editor,
true,
this.position,
this.model.selectedInlineCompletionIndex,
this.model.inlineCompletionsCount,
this.model.selectedInlineCompletion.map(v => v?.inlineCompletion.source.inlineCompletions.commands ?? []),
));
constructor(
private readonly editor: ICodeEditor,
private readonly model: InlineCompletionsModel,
private readonly model: IObservable<InlineCompletionsModel | undefined>,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
editor.addContentWidget(this.contentWidget);
this._register(toDisposable(() => editor.removeContentWidget(this.contentWidget)));
this._register(autorun('request explicit', reader => {
const position = this.position.read(reader);
if (!position) {
this._register(autorunWithStore2('setup content widget', (reader, store) => {
const model = this.model.read(reader);
if (!model || !this.alwaysShowToolbar.read(reader)) {
return;
}
if (this.model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) {
this.model.triggerExplicitly();
}
const contentWidget = store.add(this.instantiationService.createInstance(
InlineSuggestionHintsContentWidget,
this.editor,
true,
this.position,
model.selectedInlineCompletionIndex,
model.inlineCompletionsCount,
model.selectedInlineCompletion.map(v => v?.inlineCompletion.source.inlineCompletions.commands ?? []),
));
editor.addContentWidget(contentWidget);
store.add(toDisposable(() => editor.removeContentWidget(contentWidget)));
store.add(autorun('request explicit', reader => {
const position = this.position.read(reader);
if (!position) {
return;
}
if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) {
model.triggerExplicitly();
}
}));
}));
}
}

View file

@ -89,10 +89,10 @@ export class InlineCompletionsSource extends Disposable {
}
}
this._updateOperation.clear();
transaction(tx => {
target.set(completions, tx);
});
this._updateOperation.clear();
return true;
})();

15
src/vs/monaco.d.ts vendored
View file

@ -2348,7 +2348,7 @@ declare namespace monaco.editor {
*/
export interface IDocumentDiffProviderOptions {
/**
* When set to true, the diff should ignore whitespace changes.i
* When set to true, the diff should ignore whitespace changes.
*/
ignoreTrimWhitespace: boolean;
/**
@ -2374,10 +2374,12 @@ declare namespace monaco.editor {
*/
readonly changes: LineRangeMapping[];
}
/**
* A range of lines (1-based).
*/
export class LineRange {
static fromRange(range: Range): LineRange;
/**
* @param lineRanges An array of sorted line ranges.
*/
@ -2387,6 +2389,7 @@ declare namespace monaco.editor {
* @param lineRanges2 Must be sorted.
*/
static join(lineRanges1: readonly LineRange[], lineRanges2: readonly LineRange[]): readonly LineRange[];
static ofLength(startLineNumber: number, length: number): LineRange;
/**
* The start line number.
*/
@ -2422,14 +2425,18 @@ declare namespace monaco.editor {
* If the ranges don't even touch, the result is undefined.
*/
intersect(other: LineRange): LineRange | undefined;
intersectsStrict(other: LineRange): boolean;
overlapOrTouch(other: LineRange): boolean;
equals(b: LineRange): boolean;
toInclusiveRange(): Range | null;
toExclusiveRange(): Range;
}
/**
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
static inverse(mapping: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[];
/**
* The line range in the original text model.
*/
@ -3860,6 +3867,12 @@ declare namespace monaco.editor {
* Whether the diff editor aria label should be verbose.
*/
accessibilityVerbose?: boolean;
experimental?: {
/**
* Defaults to false.
*/
collapseUnchangedRegions?: boolean;
};
}
/**

View file

@ -155,7 +155,9 @@ export class MenuEntryActionViewItem extends ActionViewItem {
super.render(container);
container.classList.add('menu-entry');
this._updateItemClass(this._menuItemAction.item);
if (this.options.icon) {
this._updateItemClass(this._menuItemAction.item);
}
let mouseOver = false;

View file

@ -269,8 +269,8 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
const file = await fileHandle.getFile();
const contents = new Uint8Array(await file.arrayBuffer());
await this.writeFile(to, contents, { create: true, overwrite: opts.overwrite, unlock: false });
await this.delete(from, { recursive: false, useTrash: false });
await this.writeFile(to, contents, { create: true, overwrite: opts.overwrite, unlock: false, atomic: false });
await this.delete(from, { recursive: false, useTrash: false, atomic: false });
}
// File API does not support any real rename otherwise

View file

@ -311,7 +311,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
throw createFileSystemProviderError('Cannot rename files with different types', FileSystemProviderErrorCode.Unknown);
}
// delete the target file if exists
await this.delete(to, { recursive: true, useTrash: false });
await this.delete(to, { recursive: true, useTrash: false, atomic: false });
}
const toTargetResource = (path: string): URI => this.extUri.joinPath(to, this.extUri.relativePath(from, from.with({ path })) || '');
@ -339,7 +339,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
await this.bulkWrite(targetFiles);
}
await this.delete(from, { recursive: true, useTrash: false });
await this.delete(from, { recursive: true, useTrash: false, atomic: false });
}
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {

View file

@ -53,6 +53,8 @@ export class DiskFileSystemProviderClient extends Disposable implements
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileAtomicRead |
FileSystemProviderCapabilities.FileAtomicWrite |
FileSystemProviderCapabilities.FileAtomicDelete |
FileSystemProviderCapabilities.FileClone;
if (this.extraCapabilities.pathCaseSensitive) {

View file

@ -14,7 +14,7 @@ import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { Schemas } from 'vs/base/common/network';
import { mark } from 'vs/base/common/performance';
import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/common/resources';
import { basename, dirname, extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath, joinPath } from 'vs/base/common/resources';
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
@ -396,7 +396,17 @@ export class FileService extends Disposable implements IFileService {
// write file: buffered
else {
await this.doWriteBuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
const contents = bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream;
// atomic write
if (options?.atomic !== false && options?.atomic?.postfix) {
await this.doWriteBufferedAtomic(provider, resource, joinPath(dirname(resource), `${basename(resource)}${options.atomic.postfix}`), options, contents);
}
// non-atomic write
else {
await this.doWriteBuffered(provider, resource, options, contents);
}
}
// events
@ -416,6 +426,18 @@ export class FileService extends Disposable implements IFileService {
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
// Validate atomic support
const atomic = !!options?.atomic;
if (atomic) {
if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {
throw new Error(localize('writeFailedAtomicUnsupported', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
if (unlock) {
throw new Error(localize('writeFailedAtomicUnlock', "Unable to unlock file '{0}' because atomic write is enabled.", this.resourceForError(resource)));
}
}
// Validate via file stat meta data
let stat: IStat | undefined = undefined;
try {
@ -579,7 +601,7 @@ export class FileService extends Disposable implements IFileService {
}
if (error instanceof TooLargeFileOperationError) {
return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options);
return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options as IReadFileOptions);
}
return new FileOperationError(message, toFileOperationResult(error), options);
@ -959,6 +981,16 @@ export class FileService extends Disposable implements IFileService {
throw new Error(localize('deleteFailedTrashUnsupported', "Unable to delete file '{0}' via trash because provider does not support it.", this.resourceForError(resource)));
}
// Validate atomic support
const atomic = options?.atomic;
if (atomic && !(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete)) {
throw new Error(localize('deleteFailedAtomicUnsupported', "Unable to delete file '{0}' atomically because provider does not support it.", this.resourceForError(resource)));
}
if (useTrash && atomic) {
throw new Error(localize('deleteFailedTrashAndAtomicUnsupported', "Unable to atomically delete file '{0}' because using trash is enabled.", this.resourceForError(resource)));
}
// Validate delete
let stat: IStat | undefined = undefined;
try {
@ -990,9 +1022,10 @@ export class FileService extends Disposable implements IFileService {
const useTrash = !!options?.useTrash;
const recursive = !!options?.recursive;
const atomic = options?.atomic ?? false;
// Delete through provider
await provider.delete(resource, { recursive, useTrash });
await provider.delete(resource, { recursive, useTrash, atomic });
// Events
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
@ -1122,6 +1155,28 @@ export class FileService extends Disposable implements IFileService {
private readonly writeQueue = this._register(new ResourceQueue());
private async doWriteBufferedAtomic(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, tempResource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
// Write to temp resource first
await this.doWriteBuffered(provider, tempResource, options, readableOrStreamOrBufferedStream);
try {
// Rename over existing to ensure atomic replace
await provider.rename(tempResource, resource, { overwrite: true });
} catch (error) {
// Cleanup in case of rename error
try {
await provider.delete(tempResource, { recursive: false, useTrash: false, atomic: false });
} catch (error) {
// ignore - we want the outer error to bubble up
}
throw error;
}
}
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
@ -1237,7 +1292,7 @@ export class FileService extends Disposable implements IFileService {
}
// Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false });
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false });
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@ -1291,7 +1346,7 @@ export class FileService extends Disposable implements IFileService {
}
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false });
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false, atomic: false });
}
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {

View file

@ -283,7 +283,44 @@ export interface IFileAtomicReadOptions {
* to from a different process. If you need such atomic
* operations, you better use a real database as storage.
*/
readonly atomic: true;
readonly atomic: boolean;
}
export interface IFileAtomicOptions {
/**
* The postfix is used to create a temporary file based
* on the original resource. The resulting temporary
* file will be in the same folder as the resource and
* have `postfix` appended to the resource name.
*
* Example: given a file resource `file:///some/path/foo.txt`
* and a postfix `.vsctmp`, the temporary file will be
* created as `file:///some/path/foo.txt.vsctmp`.
*/
readonly postfix: string;
}
export interface IFileAtomicWriteOptions {
/**
* The optional `atomic` flag can be used to make sure
* the `writeFile` method updates the target file atomically
* by first writing to a temporary file in the same folder
* and then renaming it over the target.
*/
readonly atomic: IFileAtomicOptions | false;
}
export interface IFileAtomicDeleteOptions {
/**
* The optional `atomic` flag can be used to make sure
* the `delete` method deletes the target atomically by
* first renaming it to a temporary resource in the same
* folder and then deleting it.
*/
readonly atomic: IFileAtomicOptions | false;
}
export interface IFileReadLimits {
@ -316,7 +353,7 @@ export interface IFileReadStreamOptions {
readonly limits?: IFileReadLimits;
}
export interface IFileWriteOptions extends IFileOverwriteOptions, IFileUnlockOptions {
export interface IFileWriteOptions extends IFileOverwriteOptions, IFileUnlockOptions, IFileAtomicWriteOptions {
/**
* Set to `true` to create a file when it does not exist. Will
@ -358,10 +395,21 @@ export interface IFileDeleteOptions {
/**
* Set to `true` to attempt to move the file to trash
* instead of deleting it permanently from disk. This
* option maybe not be supported on all providers.
* instead of deleting it permanently from disk.
*
* This option maybe not be supported on all providers.
*/
readonly useTrash: boolean;
/**
* The optional `atomic` flag can be used to make sure
* the `delete` method deletes the target atomically by
* first renaming it to a temporary resource in the same
* folder and then deleting it.
*
* This option maybe not be supported on all providers.
*/
readonly atomic: IFileAtomicOptions | false;
}
export enum FileType {
@ -515,10 +563,21 @@ export const enum FileSystemProviderCapabilities {
*/
FileAtomicRead = 1 << 14,
/**
* Provider support to write files atomically. This implies the
* provider provides the `FileReadWrite` capability too.
*/
FileAtomicWrite = 1 << 15,
/**
* Provider support to delete atomically.
*/
FileAtomicDelete = 1 << 16,
/**
* Provider support to clone files atomically.
*/
FileClone = 1 << 15
FileClone = 1 << 17
}
export interface IFileSystemProvider {
@ -607,6 +666,26 @@ export function hasFileAtomicReadCapability(provider: IFileSystemProvider): prov
return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicRead);
}
export interface IFileSystemProviderWithFileAtomicWriteCapability extends IFileSystemProvider {
writeFile(resource: URI, contents: Uint8Array, opts?: IFileAtomicWriteOptions): Promise<void>;
}
export function hasFileAtomicWriteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicWriteCapability {
if (!hasReadWriteCapability(provider)) {
return false; // we require the `FileReadWrite` capability too
}
return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite);
}
export interface IFileSystemProviderWithFileAtomicDeleteCapability extends IFileSystemProvider {
delete(resource: URI, opts: IFileAtomicDeleteOptions): Promise<void>;
}
export function hasFileAtomicDeleteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileAtomicDeleteCapability {
return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete);
}
export enum FileSystemProviderErrorCode {
FileExists = 'EntryExists',
FileNotFound = 'EntryNotFound',
@ -1146,6 +1225,14 @@ export interface IWriteFileOptions {
* Whether to attempt to unlock a file before writing.
*/
readonly unlock?: boolean;
/**
* The optional `atomic` flag can be used to make sure
* the `writeFile` method updates the target file atomically
* by first writing to a temporary file in the same folder
* and then renaming it over the target.
*/
readonly atomic?: IFileAtomicOptions | false;
}
export interface IResolveFileOptions {
@ -1185,7 +1272,7 @@ export class FileOperationError extends Error {
constructor(
message: string,
readonly fileOperationResult: FileOperationResult,
readonly options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions
readonly options?: IReadFileOptions | IWriteFileOptions | ICreateFileOptions
) {
super(message);
}

View file

@ -12,14 +12,14 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { Event } from 'vs/base/common/event';
import { isEqual } from 'vs/base/common/extpath';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { basename, dirname } from 'vs/base/common/path';
import { basename, dirname, join } from 'vs/base/common/path';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources';
import { extUriBiasedIgnorePathCase, joinPath, basename as resourcesBasename, dirname as resourcesDirname } from 'vs/base/common/resources';
import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs';
import { localize } from 'vs/nls';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission } from 'vs/platform/files/common/files';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage } from 'vs/platform/files/common/watcher';
import { ILogService } from 'vs/platform/log/common/log';
@ -46,6 +46,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability,
IFileSystemProviderWithFileAtomicReadCapability,
IFileSystemProviderWithFileAtomicWriteCapability,
IFileSystemProviderWithFileAtomicDeleteCapability,
IFileSystemProviderWithFileCloneCapability {
private static TRACE_LOG_RESOURCE_LOCKS = false; // not enabled by default because very spammy
@ -71,6 +73,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileAtomicRead |
FileSystemProviderCapabilities.FileAtomicWrite |
FileSystemProviderCapabilities.FileAtomicDelete |
FileSystemProviderCapabilities.FileClone;
if (isLinux) {
@ -101,6 +105,14 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
}
}
private async statIgnoreError(resource: URI): Promise<IStat | undefined> {
try {
return await this.stat(resource);
} catch (error) {
return undefined;
}
}
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true });
@ -231,6 +243,37 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
}
async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
if (opts?.atomic !== false && opts?.atomic?.postfix) {
return this.doWriteFileAtomic(resource, joinPath(resourcesDirname(resource), `${resourcesBasename(resource)}${opts.atomic.postfix}`), content, opts);
} else {
return this.doWriteFile(resource, content, opts);
}
}
private async doWriteFileAtomic(resource: URI, tempResource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
// Write to temp resource first
await this.doWriteFile(tempResource, content, opts);
try {
// Rename over existing to ensure atomic replace
await this.rename(tempResource, resource, { overwrite: true });
} catch (error) {
// Cleanup in case of rename error
try {
await this.delete(tempResource, { recursive: false, useTrash: false, atomic: false });
} catch (error) {
// ignore - we want the outer error to bubble up
}
throw error;
}
}
private async doWriteFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
let handle: number | undefined = undefined;
try {
const filePath = this.toFilePath(resource);
@ -296,7 +339,9 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
await Promises.chmod(filePath, stat.mode | 0o200);
}
} catch (error) {
this.logService.trace(error); // ignore any errors here and try to just write
if (error.code !== 'ENOENT') {
this.logService.trace(error); // ignore any errors here and try to just write
}
}
}
@ -542,7 +587,12 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
try {
const filePath = this.toFilePath(resource);
if (opts.recursive) {
await Promises.rm(filePath, RimRafMode.MOVE);
let rmMoveToPath: string | undefined = undefined;
if (opts?.atomic !== false && opts.atomic.postfix) {
rmMoveToPath = join(dirname(filePath), `${basename(filePath)}${opts.atomic.postfix}`);
}
await Promises.rm(filePath, RimRafMode.MOVE, rmMoveToPath);
} else {
try {
await Promises.unlink(filePath);
@ -587,8 +637,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'move', opts.overwrite);
// Validate the move operation can perform
await this.validateMoveCopy(from, to, 'move', opts.overwrite);
// Move
await Promises.move(fromFilePath, toFilePath);
@ -614,8 +664,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'copy', opts.overwrite);
// Validate the copy operation can perform
await this.validateMoveCopy(from, to, 'copy', opts.overwrite);
// Copy
await Promises.copy(fromFilePath, toFilePath, { preserveSymlinks: true });
@ -631,7 +681,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
}
}
private async validateTargetDeleted(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {
private async validateMoveCopy(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
@ -641,18 +691,44 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);
}
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "'File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);
}
if (isSameResourceWithDifferentPathCase) {
// Handle existing target (unless this is a case change)
if (!isSameResourceWithDifferentPathCase && await Promises.exists(toFilePath)) {
if (!overwrite) {
throw createFileSystemProviderError(localize('fileCopyErrorExists', "File at target already exists"), FileSystemProviderErrorCode.FileExists);
// You cannot copy the same file to the same location with different
// path case unless you are on a case sensitive file system
if (mode === 'copy') {
throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);
}
// Delete target
await this.delete(to, { recursive: true, useTrash: false });
// You can move the same file to the same location with different
// path case on case insensitive file systems
else if (mode === 'move') {
return;
}
}
// Here we have to see if the target to move/copy to exists or not.
// We need to respect the `overwrite` option to throw in case the
// target exists.
const fromStat = await this.statIgnoreError(from);
if (!fromStat) {
throw createFileSystemProviderError(localize('fileMoveCopyErrorNotFound', "File to move/copy does not exist"), FileSystemProviderErrorCode.FileNotFound);
}
const toStat = await this.statIgnoreError(to);
if (!toStat) {
return; // target does not exist so we are good
}
if (!overwrite) {
throw createFileSystemProviderError(localize('fileMoveCopyErrorExists', "File at target already exists and thus will not be moved/copied to unless overwrite is specified"), FileSystemProviderErrorCode.FileExists);
}
// Handle existing target for move/copy
if ((fromStat.type & FileType.File) !== 0 && (toStat.type & FileType.File) !== 0) {
return; // node.js can move/copy a file over an existing file without having to delete it first
} else {
await this.delete(to, { recursive: true, useTrash: false, atomic: false });
}
}

View file

@ -82,7 +82,7 @@ flakySuite('IndexedDBFileSystemProvider', function () {
test('root is always present', async () => {
assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory);
await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false });
await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false, atomic: false });
assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory);
});
@ -230,7 +230,7 @@ flakySuite('IndexedDBFileSystemProvider', function () {
let creationPromises: Promise<any> | undefined = undefined;
return {
async create() {
return creationPromises = Promise.all(batch.map(entry => userdataFileProvider.writeFile(entry.resource, VSBuffer.fromString(entry.contents).buffer, { create: true, overwrite: true, unlock: false })));
return creationPromises = Promise.all(batch.map(entry => userdataFileProvider.writeFile(entry.resource, VSBuffer.fromString(entry.contents).buffer, { create: true, overwrite: true, unlock: false, atomic: false })));
},
async assertContentsCorrect() {
if (!creationPromises) { throw Error('read called before create'); }

View file

@ -16,7 +16,7 @@ import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Promises } from 'vs/base/node/pfs';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { etag, IFileAtomicReadOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, hasFileAtomicReadCapability, hasOpenReadWriteCloseCapability, IFileStat, IFileStatWithMetadata, IReadFileOptions, IStat, NotModifiedSinceFileOperationError, TooLargeFileOperationError } from 'vs/platform/files/common/files';
import { etag, IFileAtomicReadOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, hasFileAtomicReadCapability, hasOpenReadWriteCloseCapability, IFileStat, IFileStatWithMetadata, IReadFileOptions, IStat, NotModifiedSinceFileOperationError, TooLargeFileOperationError, IFileAtomicOptions } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { NullLogService } from 'vs/platform/log/common/log';
@ -70,6 +70,8 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileAtomicRead |
FileSystemProviderCapabilities.FileAtomicWrite |
FileSystemProviderCapabilities.FileAtomicDelete |
FileSystemProviderCapabilities.FileClone;
if (isLinux) {
@ -569,22 +571,26 @@ flakySuite('Disk File Service', function () {
});
test('deleteFolder (recursive)', async () => {
return testDeleteFolderRecursive(false);
return testDeleteFolderRecursive(false, false);
});
test('deleteFolder (recursive, atomic)', async () => {
return testDeleteFolderRecursive(false, { postfix: '.vsctmp' });
});
(isLinux /* trash is unreliable on Linux */ ? test.skip : test)('deleteFolder (recursive, useTrash)', async () => {
return testDeleteFolderRecursive(true);
return testDeleteFolderRecursive(true, false);
});
async function testDeleteFolderRecursive(useTrash: boolean): Promise<void> {
async function testDeleteFolderRecursive(useTrash: boolean, atomic: IFileAtomicOptions | false): Promise<void> {
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
const resource = URI.file(join(testDir, 'deep'));
const source = await service.resolve(resource);
assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash }), true);
await service.del(source.resource, { recursive: true, useTrash });
assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash, atomic }), true);
await service.del(source.resource, { recursive: true, useTrash, atomic });
assert.strictEqual(existsSync(source.resource.fsPath), false);
assert.ok(event!);
@ -1772,13 +1778,13 @@ flakySuite('Disk File Service', function () {
});
test('writeFile - default', async () => {
return testWriteFile();
return testWriteFile(false);
});
test('writeFile - flush on write', async () => {
DiskFileSystemProvider.configureFlushOnWrite(true);
try {
return await testWriteFile();
return await testWriteFile(false);
} finally {
DiskFileSystemProvider.configureFlushOnWrite(false);
}
@ -1787,16 +1793,41 @@ flakySuite('Disk File Service', function () {
test('writeFile - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testWriteFile();
return testWriteFile(false);
});
test('writeFile - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testWriteFile();
return testWriteFile(false);
});
async function testWriteFile() {
test('writeFile - default (atomic)', async () => {
return testWriteFile(true);
});
test('writeFile - flush on write (atomic)', async () => {
DiskFileSystemProvider.configureFlushOnWrite(true);
try {
return await testWriteFile(true);
} finally {
DiskFileSystemProvider.configureFlushOnWrite(false);
}
});
test('writeFile - buffered (atomic)', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAtomicWrite);
return testWriteFile(true);
});
test('writeFile - unbuffered (atomic)', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAtomicWrite);
return testWriteFile(true);
});
async function testWriteFile(atomic: boolean) {
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
@ -1806,7 +1837,7 @@ flakySuite('Disk File Service', function () {
assert.strictEqual(content, 'Small File');
const newContent = 'Updates to the small file';
await service.writeFile(resource, VSBuffer.fromString(newContent));
await service.writeFile(resource, VSBuffer.fromString(newContent), { atomic: atomic ? { postfix: '.vsctmp' } : false });
assert.ok(event!);
assert.strictEqual(event!.resource.fsPath, resource.fsPath);
@ -1816,28 +1847,44 @@ flakySuite('Disk File Service', function () {
}
test('writeFile (large file) - default', async () => {
return testWriteFileLarge();
return testWriteFileLarge(false);
});
test('writeFile (large file) - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testWriteFileLarge();
return testWriteFileLarge(false);
});
test('writeFile (large file) - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testWriteFileLarge();
return testWriteFileLarge(false);
});
async function testWriteFileLarge() {
test('writeFile (large file) - default (atomic)', async () => {
return testWriteFileLarge(true);
});
test('writeFile (large file) - buffered (atomic)', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAtomicWrite);
return testWriteFileLarge(true);
});
test('writeFile (large file) - unbuffered (atomic)', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAtomicWrite);
return testWriteFileLarge(true);
});
async function testWriteFileLarge(atomic: boolean) {
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent));
const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent), { atomic: atomic ? { postfix: '.vsctmp' } : false });
assert.strictEqual(fileStat.name, 'lorem.txt');
assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent);

View file

@ -139,7 +139,7 @@ export class FileStorage {
// Write to disk
try {
await this.fileService.writeFile(this.storagePath, VSBuffer.fromString(serializedDatabase));
await this.fileService.writeFile(this.storagePath, VSBuffer.fromString(serializedDatabase), { atomic: { postfix: '.vsctmp' } });
this.lastSavedStorageContents = serializedDatabase;
} catch (error) {
this.logService.error(error);

View file

@ -424,6 +424,8 @@ export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder',
export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.'));
export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: '#cccccc33', light: '#22222233', hcDark: null, hcLight: null }, nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views."));
export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', { dark: '#000000', light: '#e4e4e4', hcDark: null, hcLight: null }, nls.localize('diffEditor.unchangedRegionBackground', "The color of unchanged blocks in diff editor."));
/**
* List and tree colors
*/

View file

@ -107,7 +107,7 @@ export class ExtHostConsumerFileSystem {
await that._proxy.$ensureActivation(uri.scheme);
return await provider.delete(uri, { recursive: false, ...options });
} else {
return await that._proxy.$delete(uri, { recursive: false, useTrash: false, ...options });
return await that._proxy.$delete(uri, { recursive: false, useTrash: false, atomic: false, ...options });
}
} catch (err) {
return ExtHostConsumerFileSystem._handleError(err);

View file

@ -55,11 +55,11 @@ class DiskFileSystemProviderAdapter implements vscode.FileSystemProvider {
}
writeFile(uri: vscode.Uri, content: Uint8Array, options: { readonly create: boolean; readonly overwrite: boolean }): Promise<void> {
return this.impl.writeFile(uri, content, { ...options, unlock: false });
return this.impl.writeFile(uri, content, { ...options, unlock: false, atomic: false });
}
delete(uri: vscode.Uri, options: { readonly recursive: boolean }): Promise<void> {
return this.impl.delete(uri, { ...options, useTrash: false });
return this.impl.delete(uri, { ...options, useTrash: false, atomic: false });
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { readonly overwrite: boolean }): Promise<void> {

View file

@ -68,6 +68,7 @@ import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorH
import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration';
import { AccessibilityStatus } from 'vs/workbench/browser/parts/editor/accessibilityStatus';
import { ToggleTabsVisibilityAction } from 'vs/workbench/browser/actions/layoutActions';
import 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution';
//#region Editor Registrations

View file

@ -385,10 +385,7 @@ function registerDiffEditorCommands(): void {
const activeTextDiffEditor = getActiveTextDiffEditor(accessor);
if (activeTextDiffEditor) {
const navigator = activeTextDiffEditor.getDiffNavigator();
if (navigator) {
next ? navigator.next() : navigator.previous();
}
activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous');
}
}

View file

@ -7,13 +7,12 @@ import { localize } from 'vs/nls';
import { deepClone } from 'vs/base/common/objects';
import { isObject, assertIsDefined, withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types';
import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffEditorOptions, EditorOption, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions';
import { AbstractTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor';
import { TEXT_DIFF_EDITOR_ID, IEditorFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorOpenContext, EditorInputCapabilities, isEditorInput, isTextEditorViewState, createTooLargeFileError } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -37,17 +36,17 @@ import { ByteSize, FileOperationError, FileOperationResult, IFileService, TooLar
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { StopWatch } from 'vs/base/common/stopwatch';
import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
/**
* The text editor that leverages the diff text editor for the editing experience.
*/
export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> implements ITextDiffEditorPane {
static readonly ID = TEXT_DIFF_EDITOR_ID;
private static widgetCounter = 0; // Just for debugging
private diffEditorControl: DiffEditorWidget | undefined = undefined;
private diffEditorControl: IDiffEditor | undefined = undefined;
private diffNavigator: DiffNavigator | undefined;
private readonly diffNavigatorDisposables = this._register(new DisposableStore());
private inputLifecycleStopWatch: StopWatch | undefined = undefined;
@ -86,7 +85,18 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
}
protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}));
TextDiffEditor.widgetCounter++;
let useVersion2 = this.textResourceConfigurationService.getValue(undefined, 'diffEditor.experimental.useVersion2');
if (useVersion2 === 'first') {
// This allows to have both the old and new diff editor next to each other - just for debugging
useVersion2 = TextDiffEditor.widgetCounter === 1;
}
if (useVersion2) {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget2, parent, configuration, {}));
} else {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}));
}
}
protected updateEditorControlOptions(options: ICodeEditorOptions): void {
@ -137,12 +147,9 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
optionsGotApplied = applyTextEditorOptions(options, control, ScrollType.Immediate);
}
// Diff navigator
this.diffNavigator = this.instantiationService.createInstance(DiffNavigator, control, {
alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState, // only reveal first change if we had no options or viewstate
findResultLoop: this.getMainControl()?.getOption(EditorOption.find).loop
});
this.diffNavigatorDisposables.add(this.diffNavigator);
if (!optionsGotApplied && !hasPreviousViewState) {
control.revealFirstDiff();
}
// Since the resolved model provides information about being readonly
// or not, we apply it here to the editor even though the editor input
@ -335,10 +342,6 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
});
}
getDiffNavigator(): DiffNavigator | undefined {
return this.diffNavigator;
}
override getControl(): IDiffEditor | undefined {
return this.diffEditorControl;
}

View file

@ -8,7 +8,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
import { basename, isEqual } from 'vs/base/common/resources';
import { Action } from 'vs/base/common/actions';
import { URI } from 'vs/base/common/uri';
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { FileOperationError, FileOperationResult, IWriteFileOptions } from 'vs/platform/files/common/files';
import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
@ -134,7 +134,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa
// Any other save error
else {
const isWriteLocked = fileOperationError.fileOperationResult === FileOperationResult.FILE_WRITE_LOCKED;
const triedToUnlock = isWriteLocked && fileOperationError.options?.unlock;
const triedToUnlock = isWriteLocked && (fileOperationError.options as IWriteFileOptions | undefined)?.unlock;
const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
const canSaveElevated = resource.scheme === Schemas.file; // currently only supported for local schemes (https://github.com/microsoft/vscode/issues/48659)

View file

@ -11,11 +11,15 @@ import { IInteractiveEditorService, INTERACTIVE_EDITOR_ID } from 'vs/workbench/c
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { InteractiveEditorServiceImpl } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditorServiceImpl';
import { IInteractiveEditorSessionService, InteractiveEditorSessionService } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { InteractiveEditorNotebookContribution } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorNotebook';
registerSingleton(IInteractiveEditorService, InteractiveEditorServiceImpl, InstantiationType.Delayed);
registerSingleton(IInteractiveEditorSessionService, InteractiveEditorSessionService, InstantiationType.Delayed);
registerEditorContribution(INTERACTIVE_EDITOR_ID, InteractiveEditorController, EditorContributionInstantiation.Lazy);
registerEditorContribution(INTERACTIVE_EDITOR_ID, InteractiveEditorController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors
registerAction2(interactiveEditorActions.StartSessionAction);
registerAction2(interactiveEditorActions.UnstashSessionAction);
@ -42,3 +46,7 @@ registerAction2(interactiveEditorActions.FeebackUnhelpfulCommand);
registerAction2(interactiveEditorActions.ApplyPreviewEdits);
registerAction2(interactiveEditorActions.CopyRecordings);
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench)
.registerWorkbenchContribution(InteractiveEditorNotebookContribution, LifecyclePhase.Restored);

View file

@ -90,28 +90,43 @@
/* status */
.monaco-editor .interactive-editor .status {
padding: 6px 0px 2px 0px;
margin-top: 3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.monaco-editor .interactive-editor .status.actions {
margin-top: 6px;
}
.monaco-editor .interactive-editor .status .actions.hidden {
display: none;
}
.monaco-editor .interactive-editor .status .label {
overflow: hidden;
padding-left: 10px;
padding-right: 4px;
margin-left: auto;
color: var(--vscode-descriptionForeground);
font-size: 11px;
align-self: baseline;
display: flex;
}
.monaco-editor .interactive-editor .status .label.hidden {
display: none;
}
.monaco-editor .interactive-editor .status .label.info {
margin-right: auto;
padding-left: 2px;
}
.monaco-editor .interactive-editor .status .label.status {
padding-left: 10px;
padding-right: 4px;
margin-left: auto;
}
.monaco-editor .interactive-editor .markdownMessage {
padding-top: 10px;
}
@ -161,6 +176,18 @@
outline: 1px solid var(--vscode-inputOption-activeBorder);
}
.monaco-editor .interactive-editor .status .monaco-toolbar .action-item.button-item .action-label {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
border-radius: 2px;
padding: 4px 6px;
}
.monaco-editor .interactive-editor .status .monaco-toolbar .action-item.button-item .action-label>.codicon {
color: unset;
font-size: 14px;
}
/* preview */
.monaco-editor .interactive-editor .preview {

View file

@ -10,7 +10,7 @@ import { EditorAction2 } from 'vs/editor/browser/editorExtensions';
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { InteractiveEditorController, InteractiveEditorRunOptions } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController';
import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_SHOWING_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED, CTX_INTERACTIVE_EDITOR_DID_EDIT, CTX_INTERACTIVE_EDITOR_HAS_STASHED_SESSION } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_SHOWING_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED, CTX_INTERACTIVE_EDITOR_DID_EDIT, CTX_INTERACTIVE_EDITOR_HAS_STASHED_SESSION, MENU_INTERACTIVE_EDITOR_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { localize } from 'vs/nls';
import { IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
@ -306,6 +306,7 @@ export class DiscardAction extends AbstractInteractiveEditorAction {
},
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD,
group: '0_main',
order: 0
}
});
@ -330,6 +331,7 @@ export class DiscardToClipboardAction extends AbstractInteractiveEditorAction {
// },
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD,
group: '0_main',
order: 1
}
});
@ -353,6 +355,7 @@ export class DiscardUndoToNewFileAction extends AbstractInteractiveEditorAction
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_DID_EDIT),
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD,
group: '0_main',
order: 2
}
});
@ -377,7 +380,7 @@ export class FeebackHelpfulCommand extends AbstractInteractiveEditorAction {
precondition: CTX_INTERACTIVE_EDITOR_VISIBLE,
toggled: CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK.isEqualTo('helpful'),
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS,
id: MENU_INTERACTIVE_EDITOR_WIDGET_FEEDBACK,
when: CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE.notEqualsTo(undefined),
group: '2_feedback',
order: 1
@ -399,7 +402,7 @@ export class FeebackUnhelpfulCommand extends AbstractInteractiveEditorAction {
precondition: CTX_INTERACTIVE_EDITOR_VISIBLE,
toggled: CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK.isEqualTo('unhelpful'),
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS,
id: MENU_INTERACTIVE_EDITOR_WIDGET_FEEDBACK,
when: CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE.notEqualsTo(undefined),
group: '2_feedback',
order: 2
@ -419,13 +422,13 @@ export class ToggleInlineDiff extends AbstractInteractiveEditorAction {
id: 'interactiveEditor.toggleDiff',
title: localize('toggleDiff', 'Toggle Diff'),
icon: Codicon.diff,
precondition: CTX_INTERACTIVE_EDITOR_VISIBLE,
toggled: CTX_INTERACTIVE_EDITOR_SHOWING_DIFF,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_DID_EDIT),
toggled: { condition: CTX_INTERACTIVE_EDITOR_SHOWING_DIFF, title: localize('toggleDiff2', "Show Inline Diff") },
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS,
when: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INTERACTIVE_EDITOR_DID_EDIT),
group: '0_main',
order: 10
id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD,
when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview),
group: '1_config',
order: 9
}
});
}
@ -439,8 +442,9 @@ export class ApplyPreviewEdits extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.applyEdits',
title: localize('applyEdits', 'Apply Changes'),
id: ACTION_ACCEPT_CHANGES,
title: localize('apply1', 'Accept Changes'),
shortTitle: localize('apply2', 'Accept'),
icon: Codicon.check,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, ContextKeyExpr.or(CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED.toNegated(), CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview))),
keybinding: [{

View file

@ -130,16 +130,26 @@ export class InteractiveEditorController implements IEditorContribution {
return;
}
this._logService.trace('[IE] session RESUMING');
this._log('session RESUMING', e);
await this._nextState(State.CREATE_SESSION, { existingSession });
this._logService.trace('[IE] session done or paused');
this._log('session done or paused');
}));
this._log('NEW controller');
}
dispose(): void {
this._stashedSession.clear();
this._finishExistingSession();
this._store.dispose();
this._log('controller disposed');
}
private _log(message: string | Error, ...more: any[]): void {
if (message instanceof Error) {
this._logService.error(message, ...more);
} else {
this._logService.trace(`[IE] (editor:${this._editor.getId()})${message}`, ...more);
}
}
getId(): string {
@ -161,21 +171,21 @@ export class InteractiveEditorController implements IEditorContribution {
}
async run(options: InteractiveEditorRunOptions | undefined): Promise<void> {
this._logService.trace('[IE] session starting');
this._log('session starting');
await this._finishExistingSession();
this._stashedSession.clear();
await this._nextState(State.CREATE_SESSION, options);
this._logService.trace('[IE] session done or paused');
this._log('session done or paused');
}
private async _finishExistingSession(): Promise<void> {
if (this._activeSession) {
if (this._activeSession.editMode === EditMode.Preview) {
this._logService.trace('[IE] finishing existing session, using CANCEL', this._activeSession.editMode);
this._log('finishing existing session, using CANCEL', this._activeSession.editMode);
await this.cancelSession();
} else {
this._logService.trace('[IE] finishing existing session, using APPLY', this._activeSession.editMode);
this._log('finishing existing session, using APPLY', this._activeSession.editMode);
await this.applyChanges();
}
}
@ -184,7 +194,7 @@ export class InteractiveEditorController implements IEditorContribution {
// ---- state machine
protected async _nextState(state: State, options: InteractiveEditorRunOptions | undefined): Promise<void> {
this._logService.trace('[IE] setState to ', state);
this._log('setState to ', state);
const nextState = await this[state](options);
if (nextState) {
await this._nextState(nextState, options);
@ -200,7 +210,7 @@ export class InteractiveEditorController implements IEditorContribution {
if (!session) {
const createSessionCts = new CancellationTokenSource();
const msgListener = Event.once(this._messages.event)(m => {
this._logService.trace('[IE](state=_createSession) message received', m);
this._log('state=_createSession) message received', m);
createSessionCts.cancel();
});
@ -257,14 +267,16 @@ export class InteractiveEditorController implements IEditorContribution {
this._zone.widget.updateSlashCommands(this._activeSession.session.slashCommands ?? []);
this._zone.widget.placeholder = this._getPlaceholderText();
this._zone.widget.updateStatus(this._activeSession.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"));
this._zone.widget.value = this._activeSession.lastInput ?? '';
this._zone.widget.updateInfo(this._activeSession.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"));
this._zone.show(this._activeSession.wholeRange.getEndPosition());
this._sessionStore.add(this._editor.onDidChangeModel(() => {
this._messages.fire(this._activeSession?.lastExchange
this._sessionStore.add(this._editor.onDidChangeModel((e) => {
const msg = this._activeSession?.lastExchange
? Message.PAUSE_SESSION // pause when switching models/tabs and when having a previous exchange
: Message.CANCEL_SESSION
);
: Message.CANCEL_SESSION;
this._log('model changed, pause or cancel session', msg, e);
this._messages.fire(msg);
}));
this._sessionStore.add(this._editor.onDidChangeModelContent(e => {
@ -281,7 +293,7 @@ export class InteractiveEditorController implements IEditorContribution {
this._activeSession!.recordExternalEditOccurred(editIsOutsideOfWholeRange);
if (editIsOutsideOfWholeRange) {
this._logService.info('[IE] text changed outside of whole range, FINISH session');
this._log('text changed outside of whole range, FINISH session');
this._finishExistingSession();
}
}));
@ -362,7 +374,7 @@ export class InteractiveEditorController implements IEditorContribution {
} else {
const barrier = new Barrier();
const msgListener = Event.once(this._messages.event)(m => {
this._logService.trace('[IE](state=_waitForInput) message received', m);
this._log('state=_waitForInput) message received', m);
message = m;
barrier.open();
});
@ -396,7 +408,7 @@ export class InteractiveEditorController implements IEditorContribution {
const refer = this._activeSession.session.slashCommands?.some(value => value.refer && input!.startsWith(`/${value.command}`));
if (refer) {
this._logService.info('[IE] seeing refer command, continuing outside editor', this._activeSession.provider.debugName);
this._log('[IE] seeing refer command, continuing outside editor', this._activeSession.provider.debugName);
this._editor.setSelection(this._activeSession.wholeRange);
this._instaService.invokeFunction(sendRequest, input);
@ -420,7 +432,7 @@ export class InteractiveEditorController implements IEditorContribution {
let message = Message.NONE;
const msgListener = Event.once(this._messages.event)(m => {
this._logService.trace('[IE](state=_makeRequest) message received', m);
this._log('state=_makeRequest) message received', m);
message = m;
requestCts.cancel();
});
@ -437,12 +449,13 @@ export class InteractiveEditorController implements IEditorContribution {
attempt: 0,
};
const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, requestCts.token);
this._logService.trace('[IE] request started', this._activeSession.provider.debugName, this._activeSession.session, request);
this._log('request started', this._activeSession.provider.debugName, this._activeSession.session, request);
let response: EditResponse | MarkdownResponse | ErrorResponse | EmptyResponse;
let reply: IInteractiveEditorResponse | null | undefined;
try {
this._zone.widget.updateProgress(true);
this._zone.widget.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : '');
this._ctxHasActiveRequest.set(true);
reply = await raceCancellationError(Promise.resolve(task), requestCts.token);
@ -460,7 +473,8 @@ export class InteractiveEditorController implements IEditorContribution {
} finally {
this._ctxHasActiveRequest.set(false);
this._zone.widget.updateProgress(false);
this._logService.trace('[IE] request took', sw.elapsed(), this._activeSession.provider.debugName);
this._zone.widget.updateInfo('');
this._log('request took', sw.elapsed(), this._activeSession.provider.debugName);
}
@ -494,7 +508,7 @@ export class InteractiveEditorController implements IEditorContribution {
}
const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(this._activeSession.textModelN.uri, response.localEdits));
const editOperations = (moreMinimalEdits ?? response.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, response.localEdits, moreMinimalEdits);
this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, response.localEdits, moreMinimalEdits);
const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(this._activeSession.textModelN.createSnapshot()), null, undefined, true);
textModelNplus1.applyEdits(editOperations);
@ -556,6 +570,7 @@ export class InteractiveEditorController implements IEditorContribution {
try {
this._ignoreModelContentChanged = true;
await this._strategy.renderChanges(response);
this._ctxDidEdit.set(this._activeSession.hasChangedText);
} finally {
this._ignoreModelContentChanged = false;
}
@ -678,8 +693,8 @@ export class InteractiveEditorController implements IEditorContribution {
await strategy?.apply();
} catch (err) {
this._dialogService.error(localize('err.apply', "Failed to apply changes.", toErrorMessage(err)));
this._logService.error('[IE] FAILED to apply changes');
this._logService.error(err);
this._log('FAILED to apply changes');
this._log(err);
}
strategy?.dispose();
this._messages.fire(Message.ACCEPT_SESSION);
@ -698,8 +713,8 @@ export class InteractiveEditorController implements IEditorContribution {
await strategy?.cancel();
} catch (err) {
this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err)));
this._logService.error('[IE] FAILED to discard changes');
this._logService.error(err);
this._log('FAILED to discard changes');
this._log(err);
}
strategy?.dispose();
this._messages.fire(Message.CANCEL_SESSION);

View file

@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { illegalState } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import { IInteractiveEditorSessionService } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession';
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService';
import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
export class InteractiveEditorNotebookContribution {
constructor(
@IInteractiveEditorSessionService sessionService: IInteractiveEditorSessionService,
@INotebookEditorService notebookEditorService: INotebookEditorService,
) {
sessionService.registerSessionKeyComputer(Schemas.vscodeNotebookCell, {
getComparisonKey: (_editor, uri) => {
const data = CellUri.parse(uri);
if (!data) {
throw illegalState('Expected notebook');
}
for (const editor of notebookEditorService.listNotebookEditors()) {
if (isEqual(editor.textModel?.uri, data.notebook)) {
return `<notebook>${editor.getId()}#${uri}`;
}
}
throw illegalState('Expected notebook');
}
});
}
}

View file

@ -13,7 +13,6 @@ import { EditMode, IInteractiveEditorSessionProvider, IInteractiveEditorSession,
import { IRange, Range } from 'vs/editor/common/core/range';
import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ResourceMap } from 'vs/base/common/map';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IModelService } from 'vs/editor/common/services/model';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
@ -266,6 +265,10 @@ export class EditResponse {
}
}
export interface ISessionKeyComputer {
getComparisonKey(editor: ICodeEditor, uri: URI): string;
}
export const IInteractiveEditorSessionService = createDecorator<IInteractiveEditorSessionService>('IInteractiveEditorSessionService');
export interface IInteractiveEditorSessionService {
@ -277,6 +280,8 @@ export interface IInteractiveEditorSessionService {
releaseSession(session: Session): void;
registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable;
//
recordings(): readonly Recording[];
@ -291,7 +296,8 @@ export class InteractiveEditorSessionService implements IInteractiveEditorSessio
declare _serviceBrand: undefined;
private readonly _sessions = new Map<ICodeEditor, ResourceMap<SessionData>>();
private readonly _sessions = new Map<string, SessionData>();
private readonly _keyComputers = new Map<string, ISessionKeyComputer>();
private _recordings: Recording[] = [];
constructor(
@ -360,35 +366,27 @@ export class InteractiveEditorSessionService implements IInteractiveEditorSessio
const session = new Session(options.editMode, editor, textModel0, textModel, provider, raw, wholeRangeDecorationId);
// store: editor -> uri -> session
let map = this._sessions.get(editor);
if (!map) {
map = new ResourceMap<SessionData>();
this._sessions.set(editor, map);
// store: key -> session
const key = this._key(editor, textModel.uri);
if (this._sessions.has(key)) {
store.dispose();
throw new Error(`Session already stored for ${key}`);
}
if (map.has(textModel.uri)) {
throw new Error(`Session already stored for ${textModel.uri}`);
}
map.set(textModel.uri, { session, store });
this._sessions.set(key, { session, store });
return session;
}
releaseSession(session: Session): void {
const { editor, textModelN } = session;
const { editor } = session;
// cleanup
const map = this._sessions.get(editor);
if (map) {
const data = map.get(textModelN.uri);
if (data) {
data.store.dispose();
data.session.session.dispose?.();
map.delete(textModelN.uri);
}
if (map.size === 0) {
this._sessions.delete(editor);
for (const [key, value] of this._sessions) {
if (value.session === session) {
value.store.dispose();
this._sessions.delete(key);
this._logService.trace(`[IE] did RELEASED session for ${editor.getId()}, ${session.provider.debugName}`);
break;
}
}
@ -403,7 +401,21 @@ export class InteractiveEditorSessionService implements IInteractiveEditorSessio
}
getSession(editor: ICodeEditor, uri: URI): Session | undefined {
return this._sessions.get(editor)?.get(uri)?.session;
const key = this._key(editor, uri);
return this._sessions.get(key)?.session;
}
private _key(editor: ICodeEditor, uri: URI): string {
const item = this._keyComputers.get(uri.scheme);
return item
? item.getComparisonKey(editor, uri)
: `${editor.getId()}@${uri.toString()}`;
}
registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable {
this._keyComputers.set(scheme, value);
return toDisposable(() => this._keyComputers.delete(scheme));
}
// --- debug

View file

@ -326,11 +326,11 @@ export class LiveStrategy extends EditModeStrategy {
}
let message: string;
if (linesChanged === 0) {
message = localize('lines.0', "Generated reply");
message = localize('lines.0', "Nothing changed");
} else if (linesChanged === 1) {
message = localize('lines.1', "Generated reply and changed 1 line");
message = localize('lines.1', "Changed 1 line");
} else {
message = localize('lines.N', "Generated reply and changed {0} lines", linesChanged);
message = localize('lines.N', "Changed {0} lines", linesChanged);
}
this._widget.updateStatus(message);
}

View file

@ -12,7 +12,7 @@ import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, IInteractiveEditorSlashCommand } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, IInteractiveEditorSlashCommand, MENU_INTERACTIVE_EDITOR_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event';
@ -28,7 +28,7 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { DropdownWithDefaultActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { DropdownWithDefaultActionViewItem, IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language';
@ -44,10 +44,12 @@ import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/interact
import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution';
import { assertType } from 'vs/base/common/types';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
const defaultAriaLabel = localize('aria-label', "Interactive Editor Input");
@ -136,8 +138,10 @@ export class InteractiveEditorWidget {
h('div.previewCreateTitle.show-file-icons@previewCreateTitle'),
h('div.previewCreate.hidden@previewCreate'),
h('div.status@status', [
h('div.label.info.hidden@infoLabel'),
h('div.actions.hidden@statusToolbar'),
h('div.label.hidden@statusLabel')
h('div.label.status.hidden@statusLabel'),
h('div.actions.hidden@feedbackToolbar'),
]),
h('div.markdownMessage.hidden@markdownMessage', [
h('div.message@message'),
@ -286,6 +290,31 @@ export class InteractiveEditorWidget {
return this._instantiationService.createInstance(DropdownWithDefaultActionViewItem, action, { ...options, renderKeybindingWithDefaultActionLabel: true, persistLastActionId: false });
}
if (action.id === ACTION_ACCEPT_CHANGES) {
const ButtonLikeActionViewItem = class extends MenuEntryActionViewItem {
override render(container: HTMLElement): void {
this.options.icon = false;
super.render(container);
assertType(this.element);
this.element.classList.add('button-item');
}
protected override updateLabel(): void {
assertType(this.label);
assertType(this.action instanceof MenuItemAction);
const label = MenuItemAction.label(this.action.item, { renderShortTitle: true });
const labelElements = renderLabelWithIcons(`$(check)${label}`);
reset(this.label, ...labelElements);
}
protected override updateClass(): void {
// noop
}
};
return this._instantiationService.createInstance(ButtonLikeActionViewItem, <MenuItemAction>action, <IMenuEntryActionViewItemOptions>options);
}
return createActionViewItem(this._instantiationService, action, options);
}
};
@ -293,6 +322,10 @@ export class InteractiveEditorWidget {
this._store.add(statusToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
this._store.add(statusToolbar);
const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, MENU_INTERACTIVE_EDITOR_WIDGET_FEEDBACK, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore });
this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
this._store.add(feedbackToolbar);
// preview editors
this._previewDiffEditor = this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, _previewEditorEditorOptions, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor));
@ -389,6 +422,9 @@ export class InteractiveEditorWidget {
updateToolbar(show: boolean) {
this._elements.statusToolbar.classList.toggle('hidden', !show);
this._elements.feedbackToolbar.classList.toggle('hidden', !show);
this._elements.status.classList.toggle('actions', show);
this._elements.infoLabel.classList.toggle('hidden', show);
this._onDidChangeHeight.fire();
}
@ -409,6 +445,12 @@ export class InteractiveEditorWidget {
this._onDidChangeHeight.fire();
}
updateInfo(message: string): void {
this._elements.infoLabel.classList.toggle('hidden', !message);
this._elements.infoLabel.innerText = message;
this._onDidChangeHeight.fire();
}
updateStatus(message: string, ops: { classes?: string[]; resetAfter?: number; keepMessage?: boolean } = {}) {
const isTempMessage = typeof ops.resetAfter === 'number';
if (isTempMessage && !this._elements.statusLabel.dataset['state']) {
@ -419,7 +461,7 @@ export class InteractiveEditorWidget {
}, ops.resetAfter);
}
reset(this._elements.statusLabel, message);
this._elements.statusLabel.className = `label ${(ops.classes ?? []).join(' ')}`;
this._elements.statusLabel.className = `label status ${(ops.classes ?? []).join(' ')}`;
this._elements.statusLabel.classList.toggle('hidden', !message);
if (isTempMessage) {
this._elements.statusLabel.dataset['state'] = 'temp';
@ -437,6 +479,7 @@ export class InteractiveEditorWidget {
reset(this._elements.statusLabel);
this._elements.statusLabel.classList.toggle('hidden', true);
this._elements.statusToolbar.classList.add('hidden');
this._elements.feedbackToolbar.classList.add('hidden');
this.hideCreatePreview();
this.hideEditsPreview();
this._onDidChangeHeight.fire();

View file

@ -100,7 +100,6 @@ export interface IInteractiveEditorService {
export const INTERACTIVE_EDITOR_ID = 'interactiveEditor';
export const CTX_INTERACTIVE_EDITOR_HAS_PROVIDER = new RawContextKey<boolean>('interactiveEditorHasProvider', false, localize('interactiveEditorHasProvider', "Whether a provider for interactive editors exists"));
export const CTX_INTERACTIVE_EDITOR_VISIBLE = new RawContextKey<boolean>('interactiveEditorVisible', false, localize('interactiveEditorVisible', "Whether the interactive editor input is visible"));
export const CTX_INTERACTIVE_EDITOR_FOCUSED = new RawContextKey<boolean>('interactiveEditorFocused', false, localize('interactiveEditorFocused', "Whether the interactive editor input is focused"));
@ -118,11 +117,16 @@ export const CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK = new RawContextKey<'unhelpful
export const CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED = new RawContextKey<boolean>('interactiveEditorDocumentChanged', false, localize('interactiveEditorDocumentChanged', "Whether the document has changed concurrently"));
export const CTX_INTERACTIVE_EDITOR_EDIT_MODE = new RawContextKey<EditMode>('config.interactiveEditor.editMode', EditMode.Live);
// --- (select) action identifier
export const ACTION_ACCEPT_CHANGES = 'interactive.acceptChanges';
// --- menus
export const MENU_INTERACTIVE_EDITOR_WIDGET = MenuId.for('interactiveEditorWidget');
export const MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE = MenuId.for('interactiveEditorWidget.markdownMessage');
export const MENU_INTERACTIVE_EDITOR_WIDGET_STATUS = MenuId.for('interactiveEditorWidget.status');
export const MENU_INTERACTIVE_EDITOR_WIDGET_FEEDBACK = MenuId.for('interactiveEditorWidget.feedback');
export const MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD = MenuId.for('interactiveEditorWidget.undo');
// --- colors

View file

@ -437,7 +437,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
private renderRemoteStatusIndicator(initialText: string, initialTooltip?: string | MarkdownString, command?: string, showProgress?: boolean): void {
const { text, tooltip, ariaLabel } = this.withNetworkStatus(initialText, initialTooltip);
const { text, tooltip, ariaLabel } = this.withNetworkStatus(initialText, initialTooltip, showProgress);
const properties: IStatusbarEntry = {
name: nls.localize('remoteHost', "Remote Host"),
@ -457,35 +457,37 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
}
private withNetworkStatus(initialText: string, initialTooltip?: string | MarkdownString): { text: string; tooltip: string | IMarkdownString | undefined; ariaLabel: string } {
private withNetworkStatus(initialText: string, initialTooltip?: string | MarkdownString, showProgress?: boolean): { text: string; tooltip: string | IMarkdownString | undefined; ariaLabel: string } {
let text = initialText;
let tooltip = initialTooltip;
let ariaLabel = getCodiconAriaLabel(text);
// `initialText` can have a "$(remote)" codicon in the beginning
// but it may not have it depending on the environment.
// the following function will replace the codicon in the beginning with
// another icon or add it to the beginning if no icon
function textWithAlert(): string {
function insertOrReplaceCodicon(target: string, codicon: string): string {
if (target.startsWith('$(remote)')) {
return target.replace('$(remote)', codicon);
// `initialText` can have a codicon in the beginning that already
// indicates some kind of status, or we may have been asked to
// show progress, where a spinning codicon appears. we only want
// to replace with an alert icon for when a normal remote indicator
// is shown.
if (!showProgress && initialText.startsWith('$(remote)')) {
return initialText.replace('$(remote)', '$(alert)');
}
return `${codicon} ${target}`;
return initialText;
}
switch (this.networkState) {
case 'offline': {
text = insertOrReplaceCodicon(initialText, '$(alert)');
const offlineMessage = nls.localize('networkStatusOfflineTooltip', "Network appears to be offline, certain features might be unavailable.");
text = textWithAlert();
tooltip = this.appendTooltipLine(tooltip, offlineMessage);
ariaLabel = `${ariaLabel}, ${offlineMessage}`;
break;
}
case 'high-latency':
text = insertOrReplaceCodicon(initialText, '$(alert)');
text = textWithAlert();
tooltip = this.appendTooltipLine(tooltip, nls.localize('networkStatusHighLatencyTooltip', "Network appears to have high latency ({0}ms last, {1}ms average), certain features may be slow to respond.", remoteConnectionLatencyMeasurer.latency?.current?.toFixed(2), remoteConnectionLatencyMeasurer.latency?.average?.toFixed(2)));
break;
}

View file

@ -9,7 +9,7 @@ import { isUNC } from 'vs/base/common/extpath';
import { Schemas } from 'vs/base/common/network';
import { normalize, sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
import { FileOperationError, FileOperationResult, IFileService, IWriteFileOptions } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
@ -73,7 +73,7 @@ export async function loadLocalResource(
// NotModified status is expected and can be handled gracefully
if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
return new WebviewResourceResponse.NotModified(mime, err.options?.mtime);
return new WebviewResourceResponse.NotModified(mime, (err.options as IWriteFileOptions | undefined)?.mtime);
}
}

View file

@ -57,7 +57,7 @@ if (isWeb) {
await fileProvider.writeFile(
URI.file(join(testDir, fileName)),
files[fileName],
{ create: true, overwrite: false, unlock: false }
{ create: true, overwrite: false, unlock: false, atomic: false }
);
}

View file

@ -53,7 +53,7 @@ suite('Files - NativeTextFileService i/o', function () {
await fileProvider.writeFile(
URI.file(join(testDir, fileName)),
files[fileName],
{ create: true, overwrite: false, unlock: false }
{ create: true, overwrite: false, unlock: false, atomic: false }
);
}

View file

@ -1074,7 +1074,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
// Any other save error
else {
const isWriteLocked = fileOperationError.fileOperationResult === FileOperationResult.FILE_WRITE_LOCKED;
const triedToUnlock = isWriteLocked && fileOperationError.options?.unlock;
const triedToUnlock = isWriteLocked && (fileOperationError.options as IWriteFileOptions | undefined)?.unlock;
const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
const canSaveElevated = this.elevatedFileService.isSupported(this.resource);