mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
Merge branch 'main' into fix-183445
This commit is contained in:
commit
fcfef87f08
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@
|
|||
["`", "`"]
|
||||
],
|
||||
"folding": {
|
||||
"offSide": true,
|
||||
"markers": {
|
||||
"start": "^\\s*--\\s*#region\\b",
|
||||
"end": "^\\s*--\\s*#endregion\\b"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
});
|
|
@ -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!),
|
||||
},
|
||||
};
|
||||
}
|
344
src/vs/editor/browser/widget/diffEditorWidget2/diffModel.ts
Normal file
344
src/vs/editor/browser/widget/diffEditorWidget2/diffModel.ts
Normal 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,
|
||||
};
|
||||
}
|
237
src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts
Normal file
237
src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
61
src/vs/editor/browser/widget/diffEditorWidget2/style.css
Normal file
61
src/vs/editor/browser/widget/diffEditorWidget2/style.css
Normal 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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
121
src/vs/editor/browser/widget/diffEditorWidget2/utils.ts
Normal file
121
src/vs/editor/browser/widget/diffEditorWidget2/utils.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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."),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -794,6 +794,13 @@ export interface IDiffEditorBaseOptions {
|
|||
* Whether the diff editor aria label should be verbose.
|
||||
*/
|
||||
accessibilityVerbose?: boolean;
|
||||
|
||||
experimental?: {
|
||||
/**
|
||||
* Defaults to false.
|
||||
*/
|
||||
collapseUnchangedRegions?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -198,7 +198,7 @@ export class ToggleAlwaysShowInlineSuggestionToolbar extends Action2 {
|
|||
group: 'secondary',
|
||||
order: 10,
|
||||
}],
|
||||
toggled: InlineCompletionContextKeys.alwaysShowInlineSuggestionToolbar,
|
||||
toggled: ContextKeyExpr.equals('config.editor.inlineSuggest.showToolbar', 'always')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
15
src/vs/monaco.d.ts
vendored
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'); }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: [{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue