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 inline edits as part of inline completions #228996

Merged
merged 4 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
23 changes: 22 additions & 1 deletion src/vs/base/common/hotReloadHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { isHotReloadEnabled, registerHotReloadHandler } from './hotReload.js';
import { IReader, observableSignalFromEvent } from './observable.js';
import { constObservable, IObservable, IReader, ISettableObservable, observableSignalFromEvent, observableValue } from './observable.js';

export function readHotReloadableExport<T>(value: T, reader: IReader | undefined): T {
observeHotReloadableExports([value], reader);
Expand All @@ -28,3 +28,24 @@ export function observeHotReloadableExports(values: any[], reader: IReader | und
o.read(reader);
}
}

const classes = new Map<string, ISettableObservable<unknown>>();

export function createHotClass<T>(clazz: T): IObservable<T> {
if (!isHotReloadEnabled()) {
return constObservable(clazz);
}

const id = (clazz as any).name;

let existing = classes.get(id);
if (!existing) {
existing = observableValue(id, clazz);
classes.set(id, existing);
} else {
setTimeout(() => {
existing!.set(clazz, undefined);
}, 0);
}
return existing as IObservable<T>;
}
6 changes: 6 additions & 0 deletions src/vs/base/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ export function lastNonWhitespaceIndex(str: string, startIndex: number = str.len
return -1;
}

export function getIndentationLength(str: string): number {
const idx = firstNonWhitespaceIndex(str);
if (idx === -1) { return str.length; }
return idx;
}

/**
* Function that works identically to String.prototype.replace, except, the
* replace function is allowed to be async and return a Promise.
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/browser/observableCodeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export class ObservableCodeEditor extends Disposable {
public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; });
public readonly cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null);
public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null);
public readonly cursorLineNumber = derived<number | null>(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null);

public readonly onDidType = observableSignal<string>(this);

Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,8 +584,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, maxCol, includeViewZones);
}

public setHiddenAreas(ranges: IRange[], source?: unknown): void {
this._modelData?.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)), source);
public setHiddenAreas(ranges: IRange[], source?: unknown, forceUpdate?: boolean): void {
this._modelData?.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)), source, forceUpdate);
}

public getVisibleColumnFromPosition(rawPosition: IPosition): number {
Expand Down
28 changes: 26 additions & 2 deletions src/vs/editor/common/core/textEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class TextEdit {
return new TextEdit([new SingleTextEdit(originalRange, newText)]);
}

public static insert(position: Position, newText: string): TextEdit {
return new TextEdit([new SingleTextEdit(Range.fromPositions(position, position), newText)]);
}

constructor(public readonly edits: readonly SingleTextEdit[]) {
assertFn(() => checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition())));
}
Expand Down Expand Up @@ -43,19 +47,23 @@ export class TextEdit {

for (const edit of this.edits) {
const start = edit.range.getStartPosition();
const end = edit.range.getEndPosition();

if (position.isBeforeOrEqual(start)) {
break;
}

const end = edit.range.getEndPosition();
const len = TextLength.ofText(edit.text);
if (position.isBefore(end)) {
const startPos = new Position(start.lineNumber + lineDelta, start.column + (start.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0));
const endPos = len.addToPosition(startPos);
return rangeFromPositions(startPos, endPos);
}

if (start.lineNumber + lineDelta !== curLine) {
columnDeltaInCurLine = 0;
}

lineDelta += len.lineCount - (edit.range.endLineNumber - edit.range.startLineNumber);

if (len.lineCount === 0) {
Expand Down Expand Up @@ -173,6 +181,14 @@ export class SingleTextEdit {
text: this.text,
};
}

public toEdit(): TextEdit {
return new TextEdit([this]);
}

public equals(other: SingleTextEdit): boolean {
return SingleTextEdit.equals(this, other);
}
}

function rangeFromPositions(start: Position, end: Position): Range {
Expand All @@ -195,6 +211,10 @@ export abstract class AbstractText {
getValue() {
return this.getValueOfRange(this.length.toRange());
}

getLineLength(lineNumber: number): number {
return this.getValueOfRange(new Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER)).length;
}
}

export class LineBasedText extends AbstractText {
Expand All @@ -207,7 +227,7 @@ export class LineBasedText extends AbstractText {
super();
}

getValueOfRange(range: Range): string {
override getValueOfRange(range: Range): string {
if (range.startLineNumber === range.endLineNumber) {
return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1);
}
Expand All @@ -219,6 +239,10 @@ export class LineBasedText extends AbstractText {
return result;
}

override getLineLength(lineNumber: number): number {
return this._getLineContent(lineNumber).length;
}

get length(): TextLength {
const lastLine = this._getLineContent(this._lineCount);
return new TextLength(this._lineCount - 1, lastLine.length);
Expand Down
11 changes: 11 additions & 0 deletions src/vs/editor/common/core/textLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class TextLength {
}
}

public static fromPosition(pos: Position): TextLength {
return new TextLength(pos.lineNumber - 1, pos.column - 1);
}

public static ofRange(range: Range) {
return TextLength.betweenPositions(range.getStartPosition(), range.getEndPosition());
}
Expand Down Expand Up @@ -117,6 +121,13 @@ export class TextLength {
}
}

public addToRange(range: Range): Range {
return Range.fromPositions(
this.addToPosition(range.getStartPosition()),
this.addToPosition(range.getEndPosition())
);
}

toString() {
return `${this.lineCount},${this.columnCount}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { equals, groupAdjacentBy } from '../../../../base/common/arrays.js';
import { assertFn, checkAdjacentItems } from '../../../../base/common/assert.js';
import { equals } from '../../../../base/common/arrays.js';
import { assertFn } from '../../../../base/common/assert.js';
import { LineRange } from '../../core/lineRange.js';
import { OffsetRange } from '../../core/offsetRange.js';
import { Position } from '../../core/position.js';
import { Range } from '../../core/range.js';
import { DateTimeout, ITimeout, InfiniteTimeout, SequenceDiff } from './algorithms/diffAlgorithm.js';
import { ArrayText } from '../../core/textEdit.js';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js';
import { DetailedLineRangeMapping, LineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../rangeMapping.js';
import { DateTimeout, InfiniteTimeout, ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js';
import { DynamicProgrammingDiffing } from './algorithms/dynamicProgrammingDiffing.js';
import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js';
import { computeMovedLines } from './computeMovedLines.js';
import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from './heuristicSequenceOptimizations.js';
import { LineSequence } from './lineSequence.js';
import { LinesSliceCharSequence } from './linesSliceCharSequence.js';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js';
import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../rangeMapping.js';

export class DefaultLinesDiffComputer implements ILinesDiffComputer {
private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing();
Expand Down Expand Up @@ -140,7 +141,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {

scanForWhitespaceChanges(originalLines.length - seq1LastStart);

const changes = lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines);
const changes = lineRangeMappingFromRangeMappings(alignments, new ArrayText(originalLines), new ArrayText(modifiedLines));

let moves: MovedText[] = [];
if (options.computeMoves) {
Expand Down Expand Up @@ -203,7 +204,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
m.original.toOffsetRange(),
m.modified.toOffsetRange(),
), timeout, considerWhitespaceChanges);
const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true);
const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, new ArrayText(originalLines), new ArrayText(modifiedLines), true);
return new MovedText(m, mappings);
});
return movesWithDiffs;
Expand Down Expand Up @@ -252,81 +253,6 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer {
}
}

export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[], originalLines: string[], modifiedLines: string[], dontAssertStartLine: boolean = false): DetailedLineRangeMapping[] {
const changes: DetailedLineRangeMapping[] = [];
for (const g of groupAdjacentBy(
alignments.map(a => getLineRangeMapping(a, originalLines, modifiedLines)),
(a1, a2) =>
a1.original.overlapOrTouch(a2.original)
|| a1.modified.overlapOrTouch(a2.modified)
)) {
const first = g[0];
const last = g[g.length - 1];

changes.push(new DetailedLineRangeMapping(
first.original.join(last.original),
first.modified.join(last.modified),
g.map(a => a.innerChanges![0]),
));
}

assertFn(() => {
if (!dontAssertStartLine && changes.length > 0) {
if (changes[0].modified.startLineNumber !== changes[0].original.startLineNumber) {
return false;
}
if (modifiedLines.length - changes[changes.length - 1].modified.endLineNumberExclusive !== originalLines.length - changes[changes.length - 1].original.endLineNumberExclusive) {
return false;
}
}
return checkAdjacentItems(changes,
(m1, m2) => m2.original.startLineNumber - m1.original.endLineNumberExclusive === m2.modified.startLineNumber - m1.modified.endLineNumberExclusive &&
// There has to be an unchanged line in between (otherwise both diffs should have been joined)
m1.original.endLineNumberExclusive < m2.original.startLineNumber &&
m1.modified.endLineNumberExclusive < m2.modified.startLineNumber,
);
});

return changes;
}

export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: string[], modifiedLines: string[]): DetailedLineRangeMapping {
let lineStartDelta = 0;
let lineEndDelta = 0;

// rangeMapping describes the edit that replaces `rangeMapping.originalRange` with `newText := getText(modifiedLines, rangeMapping.modifiedRange)`.

// original: ]xxx \n <- this line is not modified
// modified: ]xx \n
if (rangeMapping.modifiedRange.endColumn === 1 && rangeMapping.originalRange.endColumn === 1
&& rangeMapping.originalRange.startLineNumber + lineStartDelta <= rangeMapping.originalRange.endLineNumber
&& rangeMapping.modifiedRange.startLineNumber + lineStartDelta <= rangeMapping.modifiedRange.endLineNumber) {
// We can only do this if the range is not empty yet
lineEndDelta = -1;
}

// original: xxx[ \n <- this line is not modified
// modified: xxx[ \n
if (rangeMapping.modifiedRange.startColumn - 1 >= modifiedLines[rangeMapping.modifiedRange.startLineNumber - 1].length
&& rangeMapping.originalRange.startColumn - 1 >= originalLines[rangeMapping.originalRange.startLineNumber - 1].length
&& rangeMapping.originalRange.startLineNumber <= rangeMapping.originalRange.endLineNumber + lineEndDelta
&& rangeMapping.modifiedRange.startLineNumber <= rangeMapping.modifiedRange.endLineNumber + lineEndDelta) {
// We can only do this if the range is not empty yet
lineStartDelta = 1;
}

const originalLineRange = new LineRange(
rangeMapping.originalRange.startLineNumber + lineStartDelta,
rangeMapping.originalRange.endLineNumber + 1 + lineEndDelta
);
const modifiedLineRange = new LineRange(
rangeMapping.modifiedRange.startLineNumber + lineStartDelta,
rangeMapping.modifiedRange.endLineNumber + 1 + lineEndDelta
);

return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]);
}

function toLineRangeMapping(sequenceDiff: SequenceDiff) {
return new LineRangeMapping(
new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1),
Expand Down
Loading
Loading