Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements experimental word replacement and insertion views. #236618

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/vs/editor/browser/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { BugIndicatingError } from '../../base/common/errors.js';
import { OffsetRange } from '../common/core/offsetRange.js';
import { Point } from './point.js';

Expand Down Expand Up @@ -49,7 +50,7 @@ export class Rect {
public readonly bottom: number,
) {
if (left > right || top > bottom) {
throw new Error('Invalid arguments');
throw new BugIndicatingError('Invalid arguments');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { InlineDecoration, ViewLineRenderingData } from '../../../../../common/v

const ttPolicy = createTrustedTypesPolicy('diffEditorWidget', { createHTML: value => value });

export function renderLines(source: LineSource, options: RenderOptions, decorations: InlineDecoration[], domNode: HTMLElement): RenderLinesResult {
export function renderLines(source: LineSource, options: RenderOptions, decorations: InlineDecoration[], domNode: HTMLElement, noExtra = false): RenderLinesResult {
applyFontInfo(domNode, options.fontInfo);

const hasCharChanges = (decorations.length > 0);
Expand All @@ -44,7 +44,8 @@ export function renderLines(source: LineSource, options: RenderOptions, decorati
source.mightContainNonBasicASCII,
source.mightContainRTL,
options,
sb
sb,
noExtra,
));
renderedLineCount++;
lastBreakOffset = breakOffset;
Expand All @@ -61,6 +62,7 @@ export function renderLines(source: LineSource, options: RenderOptions, decorati
source.mightContainRTL,
options,
sb,
noExtra,
));
renderedLineCount++;
}
Expand Down Expand Up @@ -125,7 +127,25 @@ export class RenderOptions {
public readonly renderWhitespace: FindComputedEditorOptionValueById<EditorOption.renderWhitespace>,
public readonly renderControlCharacters: boolean,
public readonly fontLigatures: FindComputedEditorOptionValueById<EditorOption.fontLigatures>,
public readonly setWidth = true,
) { }

public withSetWidth(setWidth: boolean): RenderOptions {
return new RenderOptions(
this.tabSize,
this.fontInfo,
this.disableMonospaceOptimizations,
this.typicalHalfwidthCharacterWidth,
this.scrollBeyondLastColumn,
this.lineHeight,
this.lineDecorationsWidth,
this.stopRenderingLineAfter,
this.renderWhitespace,
this.renderControlCharacters,
this.fontLigatures,
setWidth,
);
}
}

export interface RenderLinesResult {
Expand All @@ -143,16 +163,21 @@ function renderOriginalLine(
mightContainRTL: boolean,
options: RenderOptions,
sb: StringBuilder,
noExtra: boolean,
): number {

sb.appendString('<div class="view-line');
if (!hasCharChanges) {
if (!noExtra && !hasCharChanges) {
// No char changes
sb.appendString(' char-delete');
}
sb.appendString('" style="top:');
sb.appendString(String(viewLineIdx * options.lineHeight));
sb.appendString('px;width:1000000px;">');
if (options.setWidth) {
sb.appendString('px;width:1000000px;">');
} else {
sb.appendString('px;">');
}

const lineContent = lineTokens.getLineContent();
const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, mightContainNonBasicASCII);
Expand Down
19 changes: 19 additions & 0 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4197,6 +4197,9 @@ export interface IInlineSuggestOptions {
enabled?: boolean;
useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible';
useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump';
useWordInsertionView?: 'never' | 'whenPossible';
useWordReplacementView?: 'never' | 'whenPossible';

onlyShowWhenCloseToCursor?: boolean;
useGutterIndicator?: boolean;
};
Expand Down Expand Up @@ -4230,6 +4233,8 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
enabled: true,
useMixedLinesDiff: 'forStableInsertions',
useInterleavedLinesDiff: 'never',
useWordInsertionView: 'never',
useWordReplacementView: 'never',
onlyShowWhenCloseToCursor: true,
useGutterIndicator: false,
},
Expand Down Expand Up @@ -4287,6 +4292,18 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
description: nls.localize('inlineSuggest.edits.experimental.useInterleavedLinesDiff', "Controls whether to enable experimental interleaved lines diff in inline suggestions."),
enum: ['never', 'always', 'afterJump'],
},
'editor.inlineSuggest.edits.experimental.useWordInsertionView': {
type: 'string',
default: defaults.edits.experimental.useWordInsertionView,
description: nls.localize('inlineSuggest.edits.experimental.useWordInsertionView', "Controls whether to enable experimental word insertion view in inline suggestions."),
enum: ['never', 'whenPossible'],
},
'editor.inlineSuggest.edits.experimental.useWordReplacementView': {
type: 'string',
default: defaults.edits.experimental.useWordReplacementView,
description: nls.localize('inlineSuggest.edits.experimental.useWordReplacementView', "Controls whether to enable experimental word replacement view in inline suggestions."),
enum: ['never', 'whenPossible'],
},
'editor.inlineSuggest.edits.experimental.onlyShowWhenCloseToCursor': {
type: 'boolean',
default: defaults.edits.experimental.onlyShowWhenCloseToCursor,
Expand Down Expand Up @@ -4319,6 +4336,8 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
enabled: boolean(input.edits?.experimental?.enabled, this.defaultValue.edits.experimental.enabled),
useMixedLinesDiff: stringSet(input.edits?.experimental?.useMixedLinesDiff, this.defaultValue.edits.experimental.useMixedLinesDiff, ['never', 'whenPossible', 'forStableInsertions', 'afterJumpWhenPossible']),
useInterleavedLinesDiff: stringSet(input.edits?.experimental?.useInterleavedLinesDiff, this.defaultValue.edits.experimental.useInterleavedLinesDiff, ['never', 'always', 'afterJump']),
useWordInsertionView: stringSet(input.edits?.experimental?.useWordInsertionView, this.defaultValue.edits.experimental.useWordInsertionView, ['never', 'whenPossible']),
useWordReplacementView: stringSet(input.edits?.experimental?.useWordReplacementView, this.defaultValue.edits.experimental.useWordReplacementView, ['never', 'whenPossible']),
onlyShowWhenCloseToCursor: boolean(input.edits?.experimental?.onlyShowWhenCloseToCursor, this.defaultValue.edits.experimental.onlyShowWhenCloseToCursor),
useGutterIndicator: boolean(input.edits?.experimental?.useGutterIndicator, this.defaultValue.edits.experimental.useGutterIndicator),
},
Expand Down
4 changes: 4 additions & 0 deletions src/vs/editor/common/core/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@ export class Range {
return new Range(this.startLineNumber + lineCount, this.startColumn, this.endLineNumber + lineCount, this.endColumn);
}

public isSingleLine(): boolean {
return this.startLineNumber === this.endLineNumber;
}

// ---

public static fromPositions(start: IPosition, end: IPosition = start): Range {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
const characterDiffs = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
new OffsetRange(seq1Offset, seq1Offset + 1),
new OffsetRange(seq2Offset, seq2Offset + 1),
), timeout, considerWhitespaceChanges);
), timeout, considerWhitespaceChanges, options);
for (const a of characterDiffs.mappings) {
alignments.push(a);
}
Expand All @@ -130,7 +130,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
seq1LastStart = diff.seq1Range.endExclusive;
seq2LastStart = diff.seq2Range.endExclusive;

const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges);
const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges, options);
if (characterDiffs.hitTimeout) {
hitTimeout = true;
}
Expand All @@ -145,7 +145,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {

let moves: MovedText[] = [];
if (options.computeMoves) {
moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges);
moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges, options);
}

// Make sure all ranges are valid
Expand Down Expand Up @@ -190,6 +190,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
hashedModifiedLines: number[],
timeout: ITimeout,
considerWhitespaceChanges: boolean,
options: ILinesDiffComputerOptions,
): MovedText[] {
const moves = computeMovedLines(
changes,
Expand All @@ -203,14 +204,14 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
m.original.toOffsetRange(),
m.modified.toOffsetRange(),
), timeout, considerWhitespaceChanges);
), timeout, considerWhitespaceChanges, options);
const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, new ArrayText(originalLines), new ArrayText(modifiedLines), true);
return new MovedText(m, mappings);
});
return movesWithDiffs;
}

private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } {
private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean, options: ILinesDiffComputerOptions): { mappings: RangeMapping[]; hitTimeout: boolean } {
const lineRangeMapping = toLineRangeMapping(diff);
const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines);

Expand All @@ -227,8 +228,14 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
if (check) { SequenceDiff.assertSorted(diffs); }
diffs = optimizeSequenceDiffs(slice1, slice2, diffs);
if (check) { SequenceDiff.assertSorted(diffs); }
diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs);
diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findWordContaining(idx));
if (check) { SequenceDiff.assertSorted(diffs); }

if (options.extendToSubwords) {
diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findSubWordContaining(idx), true);
if (check) { SequenceDiff.assertSorted(diffs); }
}

diffs = removeShortMatches(slice1, slice2, diffs);
if (check) { SequenceDiff.assertSorted(diffs); }
diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,13 @@ export function removeShortMatches(sequence1: ISequence, sequence2: ISequence, s
return result;
}

export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
export function extendDiffsToEntireWordIfAppropriate(
sequence1: LinesSliceCharSequence,
sequence2: LinesSliceCharSequence,
sequenceDiffs: SequenceDiff[],
findParent: (seq: LinesSliceCharSequence, idx: number) => OffsetRange | undefined,
force: boolean = false,
): SequenceDiff[] {
const equalMappings = SequenceDiff.invert(sequenceDiffs, sequence1.length);

const additional: SequenceDiff[] = [];
Expand All @@ -231,8 +237,8 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe
return;
}

const w1 = sequence1.findWordContaining(pair.offset1);
const w2 = sequence2.findWordContaining(pair.offset2);
const w1 = findParent(sequence1, pair.offset1);
const w2 = findParent(sequence2, pair.offset2);
if (!w1 || !w2) {
return;
}
Expand All @@ -252,8 +258,8 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe
break;
}

const v1 = sequence1.findWordContaining(next.seq1Range.start);
const v2 = sequence2.findWordContaining(next.seq2Range.start);
const v1 = findParent(sequence1, next.seq1Range.start);
const v2 = findParent(sequence2, next.seq2Range.start);
// Because there is an intersection, we know that the words are not empty.
const v = new SequenceDiff(v1!, v2!);
const equalPart = v.intersect(next)!;
Expand All @@ -271,7 +277,7 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe
}
}

if (equalChars1 + equalChars2 < (w.seq1Range.length + w.seq2Range.length) * 2 / 3) {
if ((force && equalChars1 + equalChars2 < w.seq1Range.length + w.seq2Range.length) || equalChars1 + equalChars2 < (w.seq1Range.length + w.seq2Range.length) * 2 / 3) {
additional.push(w);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,31 @@ export class LinesSliceCharSequence implements ISequence {
return new OffsetRange(start, end);
}

/** fooBar has the two sub-words foo and bar */
public findSubWordContaining(offset: number): OffsetRange | undefined {
if (offset < 0 || offset >= this.elements.length) {
return undefined;
}

if (!isWordChar(this.elements[offset])) {
return undefined;
}

// find start
let start = offset;
while (start > 0 && isWordChar(this.elements[start - 1]) && !isUpperCase(this.elements[start])) {
start--;
}

// find end
let end = offset;
while (end < this.elements.length && isWordChar(this.elements[end]) && !isUpperCase(this.elements[end])) {
end++;
}

return new OffsetRange(start, end);
}

public countLinesIn(range: OffsetRange): number {
return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber;
}
Expand All @@ -165,6 +190,10 @@ function isWordChar(charCode: number): boolean {
|| charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9;
}

function isUpperCase(charCode: number): boolean {
return charCode >= CharCode.A && charCode <= CharCode.Z;
}

const enum CharBoundaryCategory {
WordLower,
WordUpper,
Expand Down
2 changes: 2 additions & 0 deletions src/vs/editor/common/diff/documentDiffProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface IDocumentDiffProviderOptions {
* If set, the diff computation should compute moves in addition to insertions and deletions.
*/
computeMoves: boolean;

extendToSubwords?: boolean;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/common/diff/linesDiffComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ILinesDiffComputerOptions {
readonly ignoreTrimWhitespace: boolean;
readonly maxComputationTimeMs: number;
readonly computeMoves: boolean;
readonly extendToSubwords?: boolean;
}

export class LinesDiff {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { IObservable, IReader, constObservable, derived, observableFromEvent } from '../../../../../../base/common/observable.js';
import { IObservable, constObservable, derived, observableFromEvent } from '../../../../../../base/common/observable.js';
import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js';
import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js';
import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js';
Expand All @@ -16,7 +16,7 @@ import { LineRange } from '../../../../../common/core/lineRange.js';
import { OffsetRange } from '../../../../../common/core/offsetRange.js';
import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js';
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
import { mapOutFalsy, n } from './utils.js';
import { mapOutFalsy, n, rectToProps } from './utils.js';

export const inlineEditIndicatorPrimaryForeground = registerColor('inlineEdit.gutterIndicator.primaryForeground', buttonForeground, 'Foreground color for the primary inline edit gutter indicator.');
export const inlineEditIndicatorPrimaryBackground = registerColor('inlineEdit.gutterIndicator.primaryBackground', buttonBackground, 'Background color for the primary inline edit gutter indicator.');
Expand Down Expand Up @@ -157,7 +157,7 @@ export class InlineEditsGutterIndicator extends Disposable {
case 'accept': return 'var(--vscode-inlineEdit-gutterIndicator-successfulBackground)';
}
}),
'--vscodeIconForeground': this._tabAction.map(v => {
['--vscodeIconForeground' as any]: this._tabAction.map(v => {
switch (v) {
case 'inactive': return 'var(--vscode-inlineEdit-gutterIndicator-secondaryForeground)';
case 'jump': return 'var(--vscode-inlineEdit-gutterIndicator-primaryForeground)';
Expand Down Expand Up @@ -188,12 +188,3 @@ export class InlineEditsGutterIndicator extends Disposable {
]),
])).keepUpdated(this._store);
}

function rectToProps(fn: (reader: IReader) => Rect): any {
return {
left: derived(reader => fn(reader).left),
top: derived(reader => fn(reader).top),
width: derived(reader => fn(reader).right - fn(reader).left),
height: derived(reader => fn(reader).bottom - fn(reader).top),
};
}
Loading
Loading