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 #225400 #227547

Merged
merged 2 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 11 additions & 2 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4115,6 +4115,8 @@ export interface IInlineSuggestOptions {

showToolbar?: 'always' | 'onHover' | 'never';

syntaxHighlightingEnabled?: boolean;

suppressSuggestions?: boolean;

/**
Expand Down Expand Up @@ -4144,7 +4146,8 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
showToolbar: 'onHover',
suppressSuggestions: false,
keepOnBlur: false,
fontFamily: 'default'
fontFamily: 'default',
syntaxHighlightingEnabled: false,
};

super(
Expand All @@ -4166,6 +4169,11 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
],
description: nls.localize('inlineSuggest.showToolbar', "Controls when to show the inline suggestion toolbar."),
},
'editor.inlineSuggest.syntaxHighlightingEnabled': {
type: 'boolean',
default: defaults.syntaxHighlightingEnabled,
description: nls.localize('inlineSuggest.syntaxHighlightingEnabled', "Controls whether to show syntax highlighting for inline suggestions in the editor."),
},
'editor.inlineSuggest.suppressSuggestions': {
type: 'boolean',
default: defaults.suppressSuggestions,
Expand All @@ -4191,7 +4199,8 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
showToolbar: stringSet(input.showToolbar, this.defaultValue.showToolbar, ['always', 'onHover', 'never']),
suppressSuggestions: boolean(input.suppressSuggestions, this.defaultValue.suppressSuggestions),
keepOnBlur: boolean(input.keepOnBlur, this.defaultValue.keepOnBlur),
fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily)
fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily),
syntaxHighlightingEnabled: boolean(input.syntaxHighlightingEnabled, this.defaultValue.syntaxHighlightingEnabled),
};
}
}
Expand Down
322 changes: 322 additions & 0 deletions src/vs/editor/common/core/offsetEdit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 './offsetRange.js';

/**
* Describes an edit to a (0-based) string.
* Use `TextEdit` to describe edits for a 1-based line/column text.
*/
export class OffsetEdit {
public static readonly empty = new OffsetEdit([]);

public static fromJson(data: IOffsetEdit): OffsetEdit {
return new OffsetEdit(data.map(SingleOffsetEdit.fromJson));
}

public static replace(
range: OffsetRange,
newText: string,
): OffsetEdit {
return new OffsetEdit([new SingleOffsetEdit(range, newText)]);
}

public static insert(
offset: number,
insertText: string,
): OffsetEdit {
return OffsetEdit.replace(OffsetRange.emptyAt(offset), insertText);
}

constructor(
public readonly edits: readonly SingleOffsetEdit[],
) {
let lastEndEx = -1;
for (const edit of edits) {
if (!(edit.replaceRange.start >= lastEndEx)) {
throw new BugIndicatingError(`Edits must be disjoint and sorted. Found ${edit} after ${lastEndEx}`);
}
lastEndEx = edit.replaceRange.endExclusive;
}
}

normalize(): OffsetEdit {
const edits: SingleOffsetEdit[] = [];
let lastEdit: SingleOffsetEdit | undefined;
for (const edit of this.edits) {
if (edit.newText.length === 0 && edit.replaceRange.length === 0) {
continue;
}
if (lastEdit && lastEdit.replaceRange.endExclusive === edit.replaceRange.start) {
lastEdit = new SingleOffsetEdit(
lastEdit.replaceRange.join(edit.replaceRange),
lastEdit.newText + edit.newText,
);
} else {
if (lastEdit) {
edits.push(lastEdit);
}
lastEdit = edit;
}
}
if (lastEdit) {
edits.push(lastEdit);
}
return new OffsetEdit(edits);
}

toString() {
const edits = this.edits.map(e => e.toString()).join(', ');
return `[${edits}]`;
}

apply(str: string): string {
const resultText: string[] = [];
let pos = 0;
for (const edit of this.edits) {
resultText.push(str.substring(pos, edit.replaceRange.start));
resultText.push(edit.newText);
pos = edit.replaceRange.endExclusive;
}
resultText.push(str.substring(pos));
return resultText.join('');
}

compose(other: OffsetEdit): OffsetEdit {
return joinEdits(this, other);
}

/**
* Creates an edit that reverts this edit.
*/
inverse(originalStr: string): OffsetEdit {
const edits: SingleOffsetEdit[] = [];
let offset = 0;
for (const e of this.edits) {
edits.push(new SingleOffsetEdit(
OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length),
originalStr.substring(e.replaceRange.start, e.replaceRange.endExclusive),
));
offset += e.newText.length - e.replaceRange.length;
}
return new OffsetEdit(edits);
}

getNewTextRanges(): OffsetRange[] {
const ranges: OffsetRange[] = [];
let offset = 0;
for (const e of this.edits) {
ranges.push(OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length),);
offset += e.newText.length - e.replaceRange.length;
}
return ranges;
}

get isEmpty(): boolean {
return this.edits.length === 0;
}

/**
* Consider `t1 := text o base` and `t2 := text o this`.
* We are interested in `tm := tryMerge(t1, t2, base: text)`.
* For that, we compute `tm' := t1 o base o this.rebase(base)`
* such that `tm' === tm`.
*/
tryRebase(base: OffsetEdit): OffsetEdit {
const newEdits: SingleOffsetEdit[] = [];

let baseIdx = 0;
let ourIdx = 0;
let offset = 0;

while (ourIdx < this.edits.length || baseIdx < base.edits.length) {
// take the edit that starts first
const baseEdit = base.edits[baseIdx];
const ourEdit = this.edits[ourIdx];

if (!ourEdit) {
// We processed all our edits
break;
} else if (!baseEdit) {
// no more edits from base
newEdits.push(new SingleOffsetEdit(
ourEdit.replaceRange.delta(offset),
ourEdit.newText,
));
ourIdx++;
} else if (ourEdit.replaceRange.intersects(baseEdit.replaceRange)) {
ourIdx++; // Don't take our edit, as it is conflicting -> skip
} else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start) {
// Our edit starts first
newEdits.push(new SingleOffsetEdit(
ourEdit.replaceRange.delta(offset),
ourEdit.newText,
));
ourIdx++;
} else {
baseIdx++;
offset += baseEdit.newText.length - baseEdit.replaceRange.length;
}
}

return new OffsetEdit(newEdits);
}

applyToOffset(originalOffset: number): number {
let accumulatedDelta = 0;
for (const edit of this.edits) {
if (edit.replaceRange.start <= originalOffset) {
if (originalOffset < edit.replaceRange.endExclusive) {
// the offset is in the replaced range
return edit.replaceRange.start + accumulatedDelta;
}
accumulatedDelta += edit.newText.length - edit.replaceRange.length;
} else {
break;
}
}
return originalOffset + accumulatedDelta;
}

applyToOffsetRange(originalRange: OffsetRange): OffsetRange {
return new OffsetRange(
this.applyToOffset(originalRange.start),
this.applyToOffset(originalRange.endExclusive)
);
}

applyInverseToOffset(postEditsOffset: number): number {
let accumulatedDelta = 0;
for (const edit of this.edits) {
const editLength = edit.newText.length;
if (edit.replaceRange.start <= postEditsOffset - accumulatedDelta) {
if (postEditsOffset - accumulatedDelta < edit.replaceRange.start + editLength) {
// the offset is in the replaced range
return edit.replaceRange.start;
}
accumulatedDelta += editLength - edit.replaceRange.length;
} else {
break;
}
}
return postEditsOffset - accumulatedDelta;
}
}

export type IOffsetEdit = ISingleOffsetEdit[];

export interface ISingleOffsetEdit {
txt: string;
pos: number;
len: number;
}

export class SingleOffsetEdit {
public static fromJson(data: ISingleOffsetEdit): SingleOffsetEdit {
return new SingleOffsetEdit(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt);
}

public static insert(offset: number, text: string): SingleOffsetEdit {
return new SingleOffsetEdit(OffsetRange.emptyAt(offset), text);
}

constructor(
public readonly replaceRange: OffsetRange,
public readonly newText: string,
) { }

toString(): string {
return `${this.replaceRange} -> "${this.newText}"`;
}

get isEmpty() {
return this.newText.length === 0 && this.replaceRange.length === 0;
}
}

/**
* Invariant:
* ```
* edits2.apply(edits1.apply(str)) = join(edits1, edits2).apply(str)
* ```
*/
function joinEdits(edits1: OffsetEdit, edits2: OffsetEdit): OffsetEdit {
edits1 = edits1.normalize();
edits2 = edits2.normalize();

if (edits1.isEmpty) { return edits2; }
if (edits2.isEmpty) { return edits1; }

const edit1Queue = [...edits1.edits];
const result: SingleOffsetEdit[] = [];

let edit1ToEdit2 = 0;

for (const edit2 of edits2.edits) {
// Copy over edit1 unmodified until it touches edit2.
while (true) {
const edit1 = edit1Queue[0]!;
if (!edit1 || edit1.replaceRange.start + edit1ToEdit2 + edit1.newText.length >= edit2.replaceRange.start) {
break;
}
edit1Queue.shift();

result.push(edit1);
edit1ToEdit2 += edit1.newText.length - edit1.replaceRange.length;
}

const firstEdit1ToEdit2 = edit1ToEdit2;
let firstIntersecting: SingleOffsetEdit | undefined; // or touching
let lastIntersecting: SingleOffsetEdit | undefined; // or touching

while (true) {
const edit1 = edit1Queue[0];
if (!edit1 || edit1.replaceRange.start + edit1ToEdit2 > edit2.replaceRange.endExclusive) {
break;
}
// else we intersect, because the new end of edit1 is after or equal to our start

if (!firstIntersecting) {
firstIntersecting = edit1;
}
lastIntersecting = edit1;
edit1Queue.shift();

edit1ToEdit2 += edit1.newText.length - edit1.replaceRange.length;
}

if (!firstIntersecting) {
result.push(new SingleOffsetEdit(edit2.replaceRange.delta(-edit1ToEdit2), edit2.newText));
} else {
let prefix = '';
const prefixLength = edit2.replaceRange.start - (firstIntersecting.replaceRange.start + firstEdit1ToEdit2);
if (prefixLength > 0) {
prefix = firstIntersecting.newText.slice(0, prefixLength);
}
const suffixLength = (lastIntersecting!.replaceRange.endExclusive + edit1ToEdit2) - edit2.replaceRange.endExclusive;
if (suffixLength > 0) {
const e = new SingleOffsetEdit(OffsetRange.ofStartAndLength(lastIntersecting!.replaceRange.endExclusive, 0), lastIntersecting!.newText.slice(-suffixLength));
edit1Queue.unshift(e);
edit1ToEdit2 -= e.newText.length - e.replaceRange.length;
}
const newText = prefix + edit2.newText;

const newReplaceRange = new OffsetRange(
Math.min(firstIntersecting.replaceRange.start, edit2.replaceRange.start - firstEdit1ToEdit2),
edit2.replaceRange.endExclusive - edit1ToEdit2
);
result.push(new SingleOffsetEdit(newReplaceRange, newText));
}
}

while (true) {
const item = edit1Queue.shift();
if (!item) { break; }
result.push(item);
}

return new OffsetEdit(result).normalize();
}
10 changes: 10 additions & 0 deletions src/vs/editor/common/core/offsetRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export class OffsetRange implements IOffsetRange {
return new OffsetRange(start, start + length);
}

public static emptyAt(offset: number): OffsetRange {
return new OffsetRange(offset, offset);
}

constructor(public readonly start: number, public readonly endExclusive: number) {
if (start > endExclusive) {
throw new BugIndicatingError(`Invalid range: ${this.toString()}`);
Expand Down Expand Up @@ -112,6 +116,12 @@ export class OffsetRange implements IOffsetRange {
return undefined;
}

public intersectionLength(range: OffsetRange): number {
const start = Math.max(this.start, range.start);
const end = Math.min(this.endExclusive, range.endExclusive);
return Math.max(0, end - start);
}

public intersects(other: OffsetRange): boolean {
const start = Math.max(this.start, other.start);
const end = Math.min(this.endExclusive, other.endExclusive);
Expand Down
Loading
Loading