Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts
4798 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IDiffChange, LcsDiff } from '../../../../../base/common/diff/diff.js';
7
import { getLeadingWhitespace } from '../../../../../base/common/strings.js';
8
import { Position } from '../../../../common/core/position.js';
9
import { Range } from '../../../../common/core/range.js';
10
import { TextReplacement } from '../../../../common/core/edits/textEdit.js';
11
import { ITextModel } from '../../../../common/model.js';
12
import { GhostText, GhostTextPart } from './ghostText.js';
13
import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';
14
15
/**
16
* @param previewSuffixLength Sets where to split `inlineCompletion.text`.
17
* If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
18
*/
19
export function computeGhostText(
20
edit: TextReplacement,
21
model: ITextModel,
22
mode: 'prefix' | 'subword' | 'subwordSmart',
23
cursorPosition?: Position,
24
previewSuffixLength = 0
25
): GhostText | undefined {
26
let e = singleTextRemoveCommonPrefix(edit, model);
27
28
if (e.range.endLineNumber !== e.range.startLineNumber) {
29
// This edit might span multiple lines, but the first lines must be a common prefix.
30
return undefined;
31
}
32
33
const sourceLine = model.getLineContent(e.range.startLineNumber);
34
const sourceIndentationLength = getLeadingWhitespace(sourceLine).length;
35
36
const suggestionTouchesIndentation = e.range.startColumn - 1 <= sourceIndentationLength;
37
if (suggestionTouchesIndentation) {
38
// source: ··········[······abc]
39
// ^^^^^^^^^ inlineCompletion.range
40
// ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
41
// ^^^^^^ replacedIndentation.length
42
// ^^^ rangeThatDoesNotReplaceIndentation
43
// inlineCompletion.text: '··foo'
44
// ^^ suggestionAddedIndentationLength
45
const suggestionAddedIndentationLength = getLeadingWhitespace(e.text).length;
46
47
const replacedIndentation = sourceLine.substring(e.range.startColumn - 1, sourceIndentationLength);
48
49
const [startPosition, endPosition] = [e.range.getStartPosition(), e.range.getEndPosition()];
50
const newStartPosition = startPosition.column + replacedIndentation.length <= endPosition.column
51
? startPosition.delta(0, replacedIndentation.length)
52
: endPosition;
53
const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition);
54
55
const suggestionWithoutIndentationChange = e.text.startsWith(replacedIndentation)
56
// Adds more indentation without changing existing indentation: We can add ghost text for this
57
? e.text.substring(replacedIndentation.length)
58
// Changes or removes existing indentation. Only add ghost text for the non-indentation part.
59
: e.text.substring(suggestionAddedIndentationLength);
60
61
e = new TextReplacement(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange);
62
}
63
64
// This is a single line string
65
const valueToBeReplaced = model.getValueInRange(e.range);
66
67
const changes = cachingDiff(valueToBeReplaced, e.text);
68
69
if (!changes) {
70
// No ghost text in case the diff would be too slow to compute
71
return undefined;
72
}
73
74
const lineNumber = e.range.startLineNumber;
75
76
const parts = new Array<GhostTextPart>();
77
78
if (mode === 'prefix') {
79
const filteredChanges = changes.filter(c => c.originalLength === 0);
80
if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
81
// Prefixes only have a single change.
82
return undefined;
83
}
84
}
85
86
const previewStartInCompletionText = e.text.length - previewSuffixLength;
87
88
for (const c of changes) {
89
const insertColumn = e.range.startColumn + c.originalStart + c.originalLength;
90
91
if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === e.range.startLineNumber && insertColumn < cursorPosition.column) {
92
// No ghost text before cursor
93
return undefined;
94
}
95
96
if (c.originalLength > 0) {
97
return undefined;
98
}
99
100
if (c.modifiedLength === 0) {
101
continue;
102
}
103
104
const modifiedEnd = c.modifiedStart + c.modifiedLength;
105
const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
106
const nonPreviewText = e.text.substring(c.modifiedStart, nonPreviewTextEnd);
107
const italicText = e.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
108
109
if (nonPreviewText.length > 0) {
110
parts.push(new GhostTextPart(insertColumn, nonPreviewText, false));
111
}
112
if (italicText.length > 0) {
113
parts.push(new GhostTextPart(insertColumn, italicText, true));
114
}
115
}
116
117
return new GhostText(lineNumber, parts);
118
}
119
120
let lastRequest: { originalValue: string; newValue: string; changes: readonly IDiffChange[] | undefined } | undefined = undefined;
121
function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] | undefined {
122
if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) {
123
return lastRequest?.changes;
124
} else {
125
let changes = smartDiff(originalValue, newValue, true);
126
if (changes) {
127
const deletedChars = deletedCharacters(changes);
128
if (deletedChars > 0) {
129
// For performance reasons, don't compute diff if there is nothing to improve
130
const newChanges = smartDiff(originalValue, newValue, false);
131
if (newChanges && deletedCharacters(newChanges) < deletedChars) {
132
// Disabling smartness seems to be better here
133
changes = newChanges;
134
}
135
}
136
}
137
lastRequest = {
138
originalValue,
139
newValue,
140
changes
141
};
142
return changes;
143
}
144
}
145
146
function deletedCharacters(changes: readonly IDiffChange[]): number {
147
let sum = 0;
148
for (const c of changes) {
149
sum += c.originalLength;
150
}
151
return sum;
152
}
153
154
/**
155
* When matching `if ()` with `if (f() = 1) { g(); }`,
156
* align it like this: `if ( )`
157
* Not like this: `if ( )`
158
* Also not like this: `if ( )`.
159
*
160
* The parenthesis are preprocessed to ensure that they match correctly.
161
*/
162
export function smartDiff(originalValue: string, newValue: string, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined {
163
if (originalValue.length > 5000 || newValue.length > 5000) {
164
// We don't want to work on strings that are too big
165
return undefined;
166
}
167
168
function getMaxCharCode(val: string): number {
169
let maxCharCode = 0;
170
for (let i = 0, len = val.length; i < len; i++) {
171
const charCode = val.charCodeAt(i);
172
if (charCode > maxCharCode) {
173
maxCharCode = charCode;
174
}
175
}
176
return maxCharCode;
177
}
178
179
const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
180
function getUniqueCharCode(id: number): number {
181
if (id < 0) {
182
throw new Error('unexpected');
183
}
184
return maxCharCode + id + 1;
185
}
186
187
function getElements(source: string): Int32Array {
188
let level = 0;
189
let group = 0;
190
const characters = new Int32Array(source.length);
191
for (let i = 0, len = source.length; i < len; i++) {
192
// TODO support more brackets
193
if (smartBracketMatching && source[i] === '(') {
194
const id = group * 100 + level;
195
characters[i] = getUniqueCharCode(2 * id);
196
level++;
197
} else if (smartBracketMatching && source[i] === ')') {
198
level = Math.max(level - 1, 0);
199
const id = group * 100 + level;
200
characters[i] = getUniqueCharCode(2 * id + 1);
201
if (level === 0) {
202
group++;
203
}
204
} else {
205
characters[i] = source.charCodeAt(i);
206
}
207
}
208
return characters;
209
}
210
211
const elements1 = getElements(originalValue);
212
const elements2 = getElements(newValue);
213
214
return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
215
}
216
217