From bd786750d9f1750cdeea7902a222d7ec9e347279 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Fri, 13 Dec 2024 18:53:24 +0100 Subject: [PATCH] Add `useMixedLinesDiff: forStableInsertions` (#236090) --- src/vs/editor/common/config/editorOptions.ts | 8 +- .../browser/model/inlineCompletionsModel.ts | 10 +-- .../view/inlineEdits/inlineDiffView.ts | 6 +- .../browser/view/inlineEdits/view.ts | 78 +++++++++++++++---- .../view/inlineEdits/viewAndDiffProducer.ts | 6 +- src/vs/monaco.d.ts | 2 +- 6 files changed, 79 insertions(+), 31 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 8bd08d2ee1a19..0c25596358196 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4195,7 +4195,7 @@ export interface IInlineSuggestOptions { edits?: { experimental?: { enabled?: boolean; - useMixedLinesDiff?: 'never' | 'whenPossible' | 'afterJumpWhenPossible'; + useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; onlyShowWhenCloseToCursor?: boolean; }; @@ -4227,7 +4227,7 @@ class InlineEditorSuggest extends BaseEditorOption(this, undefined); - private readonly _primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); + public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); private _isAcceptingPartially = false; public get isAcceptingPartially() { return this._isAcceptingPartially; } @@ -194,7 +194,7 @@ export class InlineCompletionsModel extends Disposable { }); } - const cursorPosition = this._primaryPosition.get(); + const cursorPosition = this.primaryPosition.get(); if (changeSummary.dontRefetch) { return Promise.resolve(true); } @@ -257,7 +257,7 @@ export class InlineCompletionsModel extends Disposable { private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => { const c = this._source.inlineCompletions.read(reader); if (!c) { return undefined; } - const cursorPosition = this._primaryPosition.read(reader); + const cursorPosition = this.primaryPosition.read(reader); let inlineEdit: InlineCompletionWithUpdatedRange | undefined = undefined; const visibleCompletions: InlineCompletionWithUpdatedRange[] = []; for (const completion of c.inlineCompletions) { @@ -355,10 +355,10 @@ export class InlineCompletionsModel extends Disposable { let edit = item.inlineEdit.toSingleTextEdit(reader); edit = singleTextRemoveCommonPrefix(edit, model); - const cursorPos = this._primaryPosition.read(reader); + const cursorPos = this.primaryPosition.read(reader); const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber); - const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this._primaryPosition.read(reader).lineNumber); + const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber); if (this._onlyShowWhenCloseToCursor.read(reader) && cursorDist > 3 && !item.inlineEdit.request.isExplicitRequest && !this._inAcceptFlow.read(reader)) { return undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index faddbf04b093a..38343311a3e26 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -23,7 +23,7 @@ import { classNames } from './utils.js'; export interface IOriginalEditorInlineDiffViewState { diff: DetailedLineRangeMapping[]; modifiedText: AbstractText; - mode: 'mixedLines' | 'interleavedLines' | 'sideBySide'; + mode: 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide'; modifiedCodeEditor: ICodeEditor; } @@ -111,7 +111,7 @@ export class OriginalEditorInlineDiffView extends Disposable { if (!diff) { return undefined; } const modified = diff.modifiedText; - const showInline = diff.mode === 'mixedLines'; + const showInline = diff.mode === 'mixedLines' || diff.mode === 'ghostText'; const showEmptyDecorations = true; @@ -214,7 +214,7 @@ export class OriginalEditorInlineDiffView extends Disposable { description: 'inserted-text', before: { content: insertedText, - inlineClassName: 'inlineCompletions-char-insert', + inlineClassName: diff.mode === 'ghostText' ? 'ghost-text-decoration' : 'inlineCompletions-char-insert', }, zIndex: 2, showIfCollapsed: true, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 391cf7516b77e..945c91b9e130a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -4,22 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { derived, IObservable } from '../../../../../../base/common/observable.js'; +import { derived, IObservable, IReader } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; +import { Position } from '../../../../../common/core/position.js'; import { StringText } from '../../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; -import './view.css'; +import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { IInlineEditsIndicatorState, InlineEditsIndicator } from './indicatorView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineDiffView.js'; +import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; -import { IInlineEditsIndicatorState, InlineEditsIndicator } from './indicatorView.js'; -import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import './view.css'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; -import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); @@ -37,7 +38,7 @@ export class InlineEditsView extends Disposable { } private readonly _uiState = derived<{ - state: 'collapsed' | 'mixedLines' | 'interleavedLines' | 'sideBySide'; + state: 'collapsed' | 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide'; diff: DetailedLineRangeMapping[]; edit: InlineEditWithChanges; newText: string; @@ -55,17 +56,7 @@ export class InlineEditsView extends Disposable { let newText = edit.edit.apply(edit.originalText); let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); - let state: 'collapsed' | 'mixedLines' | 'interleavedLines' | 'sideBySide'; - if (edit.isCollapsed) { - state = 'collapsed'; - } else if (diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) && - (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible'))) { - state = 'mixedLines'; - } else if ((this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump'))) { - state = 'interleavedLines'; - } else { - state = 'sideBySide'; - } + const state = this.determinRenderState(edit, reader, diff); if (state === 'sideBySide') { const indentationAdjustmentEdit = createReindentEdit(newText, edit.modifiedLineRange); @@ -140,4 +131,57 @@ export class InlineEditsView extends Disposable { }), this._model, )); + + private determinRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[]) { + if (edit.isCollapsed) { + return 'collapsed'; + } + + if ( + (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) + && diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) + ) { + return 'mixedLines'; + } + + if ( + this._useMixedLinesDiff.read(reader) === 'forStableInsertions' + && isInsertionAfterPosition(diff, edit.cursorPosition) + ) { + return 'ghostText'; + } + + if (this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) { + return 'interleavedLines'; + } + + return 'sideBySide'; + } +} + +function isInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Position | null) { + if (!position) { + return false; + } + const pos = position; + + return diff.every(m => m.innerChanges!.every(r => isStableWordInsertion(r))); + + function isStableWordInsertion(r: RangeMapping) { + if (!r.originalRange.isEmpty()) { + return false; + } + const isInsertionWithinLine = r.modifiedRange.startLineNumber === r.modifiedRange.endLineNumber; + if (!isInsertionWithinLine) { + return false; + } + const insertPosition = r.originalRange.getStartPosition(); + if (pos.isBeforeOrEqual(insertPosition)) { + return true; + } + if (insertPosition.lineNumber < pos.lineNumber) { + return true; + } + return false; + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts index 3f31d826857a7..ed40c6c0edbd5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts @@ -53,6 +53,8 @@ export class InlineEditsViewAndDiffProducer extends Disposable { }); private readonly _inlineEditPromise = derived | undefined>(this, (reader) => { + const model = this._model.read(reader); + if (!model) { return undefined; } const inlineEdit = this._edit.read(reader); if (!inlineEdit) { return undefined; } @@ -86,7 +88,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { )); const diffEdits = new TextEdit(edits); - return new InlineEditWithChanges(text, diffEdits, inlineEdit.isCollapsed, inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); //inlineEdit.showInlineIfPossible); + return new InlineEditWithChanges(text, diffEdits, inlineEdit.isCollapsed, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); //inlineEdit.showInlineIfPossible); }); }); @@ -116,6 +118,7 @@ export class InlineEditWithChanges { public readonly originalText: AbstractText, public readonly edit: TextEdit, public readonly isCollapsed: boolean, + public readonly cursorPosition: Position, public readonly userJumpedToIt: boolean, public readonly commands: readonly Command[], public readonly inlineCompletion: InlineCompletionItem, @@ -126,6 +129,7 @@ export class InlineEditWithChanges { return this.originalText.getValue() === other.originalText.getValue() && this.edit.equals(other.edit) && this.isCollapsed === other.isCollapsed && + this.cursorPosition.equals(other.cursorPosition) && this.userJumpedToIt === other.userJumpedToIt && this.commands === other.commands && this.inlineCompletion === other.inlineCompletion; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index eba21bfb56126..77cd7ae49a531 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4602,7 +4602,7 @@ declare namespace monaco.editor { edits?: { experimental?: { enabled?: boolean; - useMixedLinesDiff?: 'never' | 'whenPossible' | 'afterJumpWhenPossible'; + useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; onlyShowWhenCloseToCursor?: boolean; };