Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/editKind.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
import { Position } from '../../../../common/core/position.js';
6
import { StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js';
7
import { ITextModel } from '../../../../common/model.js';
8
9
const syntacticalChars = new Set([';', ',', '=', '+', '-', '*', '/', '{', '}', '(', ')', '[', ']', '<', '>', ':', '.', '!', '?', '&', '|', '^', '%', '@', '#', '~', '`', '\\', '\'', '"', '$']);
10
11
function isSyntacticalChar(char: string): boolean {
12
return syntacticalChars.has(char);
13
}
14
15
function isIdentifierChar(char: string): boolean {
16
return /[a-zA-Z0-9_]/.test(char);
17
}
18
19
function isWhitespaceChar(char: string): boolean {
20
return char === ' ' || char === '\t';
21
}
22
23
type SingleCharacterKind = 'syntactical' | 'identifier' | 'whitespace';
24
25
interface SingleLineTextShape {
26
readonly kind: 'singleLine';
27
readonly isSingleCharacter: boolean;
28
readonly singleCharacterKind: SingleCharacterKind | undefined;
29
readonly isWord: boolean;
30
readonly isMultipleWords: boolean;
31
readonly isMultipleWhitespace: boolean;
32
readonly hasDuplicatedWhitespace: boolean;
33
}
34
35
interface MultiLineTextShape {
36
readonly kind: 'multiLine';
37
readonly lineCount: number;
38
}
39
40
type TextShape = SingleLineTextShape | MultiLineTextShape;
41
42
function analyzeTextShape(text: string): TextShape {
43
const lines = text.split(/\r\n|\r|\n/);
44
if (lines.length > 1) {
45
return {
46
kind: 'multiLine',
47
lineCount: lines.length,
48
};
49
}
50
51
const isSingleChar = text.length === 1;
52
let singleCharKind: SingleCharacterKind | undefined;
53
if (isSingleChar) {
54
if (isSyntacticalChar(text)) {
55
singleCharKind = 'syntactical';
56
} else if (isIdentifierChar(text)) {
57
singleCharKind = 'identifier';
58
} else if (isWhitespaceChar(text)) {
59
singleCharKind = 'whitespace';
60
}
61
}
62
63
// Analyze whitespace patterns
64
const whitespaceMatches = text.match(/[ \t]+/g) || [];
65
const isMultipleWhitespace = whitespaceMatches.some(ws => ws.length > 1);
66
const hasDuplicatedWhitespace = whitespaceMatches.some(ws =>
67
(ws.includes(' ') || ws.includes('\t\t'))
68
);
69
70
// Analyze word patterns
71
const words = text.split(/\s+/).filter(w => w.length > 0);
72
const isWord = words.length === 1 && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(words[0]);
73
const isMultipleWords = words.length > 1;
74
75
return {
76
kind: 'singleLine',
77
isSingleCharacter: isSingleChar,
78
singleCharacterKind: singleCharKind,
79
isWord,
80
isMultipleWords,
81
isMultipleWhitespace,
82
hasDuplicatedWhitespace,
83
};
84
}
85
86
type InsertLocationShape = 'endOfLine' | 'emptyLine' | 'startOfLine' | 'middleOfLine';
87
88
interface InsertLocationRelativeToCursor {
89
readonly atCursor: boolean;
90
readonly beforeCursorOnSameLine: boolean;
91
readonly afterCursorOnSameLine: boolean;
92
readonly linesAbove: number | undefined;
93
readonly linesBelow: number | undefined;
94
}
95
96
export interface InsertProperties {
97
readonly textShape: TextShape;
98
readonly locationShape: InsertLocationShape;
99
readonly relativeToCursor: InsertLocationRelativeToCursor | undefined;
100
}
101
102
export interface DeleteProperties {
103
readonly textShape: TextShape;
104
readonly isAtEndOfLine: boolean;
105
readonly deletesEntireLineContent: boolean;
106
}
107
108
export interface ReplaceProperties {
109
readonly isWordToWordReplacement: boolean;
110
readonly isAdditive: boolean;
111
readonly isSubtractive: boolean;
112
readonly isSingleLineToSingleLine: boolean;
113
readonly isSingleLineToMultiLine: boolean;
114
readonly isMultiLineToSingleLine: boolean;
115
}
116
117
type EditOperation = 'insert' | 'delete' | 'replace';
118
119
interface IInlineSuggestionEditKindEdit {
120
readonly operation: EditOperation;
121
readonly properties: InsertProperties | DeleteProperties | ReplaceProperties;
122
readonly charactersInserted: number;
123
readonly charactersDeleted: number;
124
readonly linesInserted: number;
125
readonly linesDeleted: number;
126
}
127
export class InlineSuggestionEditKind {
128
constructor(readonly edits: IInlineSuggestionEditKindEdit[]) { }
129
toString(): string {
130
return JSON.stringify({ edits: this.edits });
131
}
132
}
133
134
export function computeEditKind(edit: StringEdit, textModel: ITextModel, cursorPosition?: Position): InlineSuggestionEditKind | undefined {
135
if (edit.replacements.length === 0) {
136
// Empty edit - return undefined as there's no edit to classify
137
return undefined;
138
}
139
140
return new InlineSuggestionEditKind(edit.replacements.map(rep => computeSingleEditKind(rep, textModel, cursorPosition)));
141
}
142
143
function countLines(text: string): number {
144
if (text.length === 0) {
145
return 0;
146
}
147
return text.split(/\r\n|\r|\n/).length - 1;
148
}
149
150
function computeSingleEditKind(replacement: StringReplacement, textModel: ITextModel, cursorPosition?: Position): IInlineSuggestionEditKindEdit {
151
const replaceRange = replacement.replaceRange;
152
const newText = replacement.newText;
153
const deletedLength = replaceRange.length;
154
const insertedLength = newText.length;
155
const linesInserted = countLines(newText);
156
157
const kind = replaceRange.isEmpty ? 'insert' : (newText.length === 0 ? 'delete' : 'replace');
158
switch (kind) {
159
case 'insert':
160
return {
161
operation: 'insert',
162
properties: computeInsertProperties(replaceRange.start, newText, textModel, cursorPosition),
163
charactersInserted: insertedLength,
164
charactersDeleted: 0,
165
linesInserted,
166
linesDeleted: 0,
167
};
168
case 'delete': {
169
const deletedText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive);
170
return {
171
operation: 'delete',
172
properties: computeDeleteProperties(replaceRange.start, replaceRange.endExclusive, textModel),
173
charactersInserted: 0,
174
charactersDeleted: deletedLength,
175
linesInserted: 0,
176
linesDeleted: countLines(deletedText),
177
};
178
}
179
case 'replace': {
180
const oldText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive);
181
return {
182
operation: 'replace',
183
properties: computeReplaceProperties(oldText, newText),
184
charactersInserted: insertedLength,
185
charactersDeleted: deletedLength,
186
linesInserted,
187
linesDeleted: countLines(oldText),
188
};
189
}
190
}
191
}
192
193
function computeInsertProperties(offset: number, newText: string, textModel: ITextModel, cursorPosition?: Position): InsertProperties {
194
const textShape = analyzeTextShape(newText);
195
const insertPosition = textModel.getPositionAt(offset);
196
const lineContent = textModel.getLineContent(insertPosition.lineNumber);
197
const lineLength = lineContent.length;
198
199
// Determine location shape
200
let locationShape: InsertLocationShape;
201
const isLineEmpty = lineContent.trim().length === 0;
202
const isAtEndOfLine = insertPosition.column > lineLength;
203
const isAtStartOfLine = insertPosition.column === 1;
204
205
if (isLineEmpty) {
206
locationShape = 'emptyLine';
207
} else if (isAtEndOfLine) {
208
locationShape = 'endOfLine';
209
} else if (isAtStartOfLine) {
210
locationShape = 'startOfLine';
211
} else {
212
locationShape = 'middleOfLine';
213
}
214
215
// Compute relative to cursor if cursor position is provided
216
let relativeToCursor: InsertLocationRelativeToCursor | undefined;
217
if (cursorPosition) {
218
const cursorLine = cursorPosition.lineNumber;
219
const insertLine = insertPosition.lineNumber;
220
const cursorColumn = cursorPosition.column;
221
const insertColumn = insertPosition.column;
222
223
const atCursor = cursorLine === insertLine && cursorColumn === insertColumn;
224
const beforeCursorOnSameLine = cursorLine === insertLine && insertColumn < cursorColumn;
225
const afterCursorOnSameLine = cursorLine === insertLine && insertColumn > cursorColumn;
226
const linesAbove = insertLine < cursorLine ? cursorLine - insertLine : undefined;
227
const linesBelow = insertLine > cursorLine ? insertLine - cursorLine : undefined;
228
229
relativeToCursor = {
230
atCursor,
231
beforeCursorOnSameLine,
232
afterCursorOnSameLine,
233
linesAbove,
234
linesBelow,
235
};
236
}
237
238
return {
239
textShape,
240
locationShape,
241
relativeToCursor,
242
};
243
}
244
245
function computeDeleteProperties(startOffset: number, endOffset: number, textModel: ITextModel): DeleteProperties {
246
const deletedText = textModel.getValue().substring(startOffset, endOffset);
247
const textShape = analyzeTextShape(deletedText);
248
249
const startPosition = textModel.getPositionAt(startOffset);
250
const endPosition = textModel.getPositionAt(endOffset);
251
252
// Check if delete is at end of line
253
const lineContent = textModel.getLineContent(endPosition.lineNumber);
254
const isAtEndOfLine = endPosition.column > lineContent.length;
255
256
// Check if entire line content is deleted
257
const deletesEntireLineContent =
258
startPosition.lineNumber === endPosition.lineNumber &&
259
startPosition.column === 1 &&
260
endPosition.column > lineContent.length;
261
262
return {
263
textShape,
264
isAtEndOfLine,
265
deletesEntireLineContent,
266
};
267
}
268
269
function computeReplaceProperties(oldText: string, newText: string): ReplaceProperties {
270
const oldShape = analyzeTextShape(oldText);
271
const newShape = analyzeTextShape(newText);
272
273
const oldIsWord = oldShape.kind === 'singleLine' && oldShape.isWord;
274
const newIsWord = newShape.kind === 'singleLine' && newShape.isWord;
275
const isWordToWordReplacement = oldIsWord && newIsWord;
276
277
const isAdditive = newText.length > oldText.length;
278
const isSubtractive = newText.length < oldText.length;
279
280
const isSingleLineToSingleLine = oldShape.kind === 'singleLine' && newShape.kind === 'singleLine';
281
const isSingleLineToMultiLine = oldShape.kind === 'singleLine' && newShape.kind === 'multiLine';
282
const isMultiLineToSingleLine = oldShape.kind === 'multiLine' && newShape.kind === 'singleLine';
283
284
return {
285
isWordToWordReplacement,
286
isAdditive,
287
isSubtractive,
288
isSingleLineToSingleLine,
289
isSingleLineToMultiLine,
290
isMultiLineToSingleLine,
291
};
292
}
293
294