diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index faf9661716694..189122c966dd3 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4250,7 +4250,7 @@ class InlineEditorSuggest extends BaseEditorOption, private readonly _uiState: IObservable<{ - edit: InlineEditWithChanges; - originalDisplayRange: LineRange; - widgetStartColumn: number; + originalRange: LineRange; + deletions: Range[]; } | undefined>, ) { super(); @@ -55,7 +54,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); - private readonly _originalDisplayRange = this._uiState.map(s => s?.originalDisplayRange); + private readonly _originalDisplayRange = this._uiState.map(s => s?.originalRange); private readonly _editorMaxContentWidthInRange = derived(this, reader => { const originalDisplayRange = this._originalDisplayRange.read(reader); if (!originalDisplayRange) { @@ -71,6 +70,14 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV }); }).map((v, r) => v.read(r)); + private readonly _maxPrefixTrim = derived(reader => { + const state = this._uiState.read(reader); + if (!state) { + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } + return getPrefixTrim(state.deletions, state.originalRange, [], this._editor); + }); + private readonly _editorLayoutInfo = derived(this, (reader) => { const inlineEdit = this._edit.read(reader); if (!inlineEdit) { @@ -81,7 +88,6 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV return null; } - const w = this._editorObs.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; const editorLayout = this._editorObs.layoutInfo.read(reader); const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); @@ -91,7 +97,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); - const codeLeft = editorLayout.contentLeft + state.widgetStartColumn * w; + const codeLeft = editorLayout.contentLeft + this._maxPrefixTrim.read(reader).prefixLeftOffset; if (left <= codeLeft) { return null; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 541e6e75cef69..6f855179b1eed 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -14,6 +14,7 @@ import { OS } from '../../../../../../base/common/platform.js'; import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; import { MenuEntryActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; import { Rect } from '../../../../../browser/rect.js'; @@ -24,6 +25,7 @@ import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js'; import { RangeMapping } from '../../../../../common/diff/rangeMapping.js'; +import { indentOfLine } from '../../../../../common/model/textModel.js'; export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { editor.layoutInfo.read(reader); @@ -66,6 +68,22 @@ export function getOffsetForPos(editor: ObservableCodeEditor, pos: Position, rea return lineContentWidth; } +export function getPrefixTrim(diffRanges: Range[], originalLinesRange: LineRange, modifiedLines: string[], editor: ICodeEditor): { prefixTrim: number; prefixLeftOffset: number } { + const textModel = editor.getModel(); + if (!textModel) { + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } + + const replacementStart = diffRanges.map(r => r.isSingleLine() ? r.startColumn - 1 : 0); + const originalIndents = originalLinesRange.mapToLineArray(line => indentOfLine(textModel.getLineContent(line))); + const modifiedIndents = modifiedLines.map(line => indentOfLine(line)); + const prefixTrim = Math.min(...replacementStart, ...originalIndents, ...modifiedIndents); + + const prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1); + + return { prefixTrim, prefixLeftOffset }; +} + export class StatusBarViewItem extends MenuEntryActionViewItem { protected readonly _updateLabelListener = this._register(this._contextKeyService.onDidChangeContext(() => { this.updateLabel(); 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 33f71f8d77a72..950247780faea 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -130,9 +130,8 @@ export class InlineEditsView extends Disposable { this._editor, this._edit, this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ - edit: s.edit, - originalDisplayRange: s.originalDisplayRange, - widgetStartColumn: s.state.widgetStartColumn, + originalRange: s.state.originalRange, + deletions: s.state.deletions, }) : undefined), )); @@ -244,7 +243,7 @@ export class InlineEditsView extends Disposable { const numOriginalLines = edit.originalLineRange.length; const numModifiedLines = edit.modifiedLineRange.length; const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1 && useCodeOverlay === 'whenPossible') { + if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { return 'wordReplacements'; } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { return 'lineReplacement'; @@ -282,9 +281,11 @@ export class InlineEditsView extends Disposable { const inner = diff.flatMap(d => d.innerChanges ?? []); if (view === 'deletion') { - const trimLength = getPrefixTrimLength(edit, inner, newText); - const widgetStartColumn = Math.min(trimLength, ...inner.map(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber ? 0 : m.originalRange.startColumn - 1)); - return { kind: 'deletion' as const, widgetStartColumn }; + return { + kind: 'deletion' as const, + originalRange: edit.originalLineRange, + deletions: inner.map(m => m.originalRange), + }; } const replacements = inner.map(m => new SingleTextEdit(m.originalRange, newText.getValueOfRange(m.modifiedRange))); @@ -354,28 +355,3 @@ function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { return true; } } - -function getPrefixTrimLength(edit: InlineEditWithChanges, innerChanges: RangeMapping[], newText: StringText) { - if (innerChanges.some(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber)) { - return 0; - } - - let minTrimLength = Number.MAX_SAFE_INTEGER; - for (let i = 0; i < edit.originalLineRange.length; i++) { - const lineNumber = edit.originalLineRange.startLineNumber + i; - const originalLine = edit.originalText.getLineAt(lineNumber); - const editedLine = newText.getLineAt(lineNumber); - const trimLength = getLinePrefixTrimLength(originalLine, editedLine); - minTrimLength = Math.min(minTrimLength, trimLength); - } - - return Math.min(minTrimLength, ...innerChanges.map(m => m.originalRange.startColumn - 1)); - - function getLinePrefixTrimLength(originalLine: string, editedLine: string) { - let startTrim = 0; - while (originalLine[startTrim] === editedLine[startTrim] && (originalLine[startTrim] === ' ' || originalLine[startTrim] === '\t')) { - startTrim++; - } - return startTrim; - } -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 2173dc88e31dd..91dddc5b9cd06 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -18,7 +18,7 @@ import { SingleTextEdit } from '../../../../../common/core/textEdit.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; -import { mapOutFalsy, n, rectToProps } from './utils.js'; +import { getPrefixTrim, mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; import { IInlineEditsView } from './sideBySideDiff.js'; import { Range } from '../../../../../common/core/range.js'; @@ -274,35 +274,13 @@ export class LineReplacementView extends Disposable implements IInlineEditsView stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; - private readonly maxPrefixTrim = derived(this, reader => { - const maxPrefixTrim = Math.max(...this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]).map(r => r.isSingleLine() ? r.startColumn - 1 : 0)); - if (maxPrefixTrim === 0) { - return 0; - } - - const textModel = this._editor.editor.getModel()!; - - const getLineTrimColLength = (line: string) => { - let i = 0; - while (i < line.length && line[i] === ' ') { i++; } - return i; - }; - - // TODO: make sure this works for tabs - return Math.min( - maxPrefixTrim, - ...this._originalRange.mapToLineArray(line => getLineTrimColLength(textModel.getLineContent(line))), - ...this._modifiedLines.map(line => getLineTrimColLength(line)) - ); - }); + private readonly _maxPrefixTrim = getPrefixTrim(this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]), this._originalRange, this._modifiedLines, this._editor.editor); private readonly _modifiedLineElements = derived(reader => { - - const maxPrefixTrim = this.maxPrefixTrim.read(reader); - const lines = []; let requiredWidth = 0; + const maxPrefixTrim = this._maxPrefixTrim.prefixTrim; const modifiedBubbles = rangesToBubbleRanges(this._replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); const textModel = this._editor.model.get()!; @@ -351,12 +329,12 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const PADDING = 4; const editorModel = this._editor.editor.getModel()!; - const maxPrefixTrim = this.maxPrefixTrim.read(reader); + const { prefixTrim, prefixLeftOffset } = this._maxPrefixTrim; - // TODO, correctly count tabs + // TODO: correctly count tabs const originalLineContents: string[] = []; this._originalRange.forEach(line => originalLineContents.push(editorModel.getLineContent(line))); - const maxOriginalLineLength = Math.max(...originalLineContents.map(l => l.length)) - maxPrefixTrim; + const maxOriginalLineLength = Math.max(...originalLineContents.map(l => l.length)) - prefixTrim; const maxLineWidth = Math.max(maxOriginalLineLength * w, requiredWidth); const startLineNumber = this._originalRange.startLineNumber; @@ -369,11 +347,9 @@ export class LineReplacementView extends Disposable implements IInlineEditsView return undefined; } - const prefixTrimOffset = maxPrefixTrim * w; - // Box Widget positioning const originalLinesOverlay = Rect.fromLeftTopWidthHeight( - editorLeftOffset + prefixTrimOffset, + editorLeftOffset + prefixLeftOffset, topOfOriginalLines, maxLineWidth, bottomOfOriginalLines - topOfOriginalLines + PADDING @@ -407,7 +383,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView lowerBackground, lowerText, padding: PADDING, - minContentWidthRequired: prefixTrimOffset + maxLineWidth + PADDING * 2, + minContentWidthRequired: maxLineWidth + PADDING * 2, }; });