Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/completions/common/parseBlock.ts
13399 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 { StringText } from '../../../util/vs/editor/common/core/text/abstractText';
7
8
// TODO: This should probably be language specific
9
const continuations = [
10
// Brace control
11
'\\{',
12
'\\}',
13
'\\[',
14
'\\]',
15
'\\(',
16
'\\)',
17
].concat(
18
[
19
// Separators in a multi-line list
20
// ",", ";", "\\|",
21
// Multi-line comments
22
// None
23
// Keywords for same-level control flow
24
'then',
25
'else',
26
'elseif',
27
'elif',
28
'catch',
29
'finally',
30
// End keywords
31
'fi',
32
'done',
33
'end',
34
'loop',
35
'until',
36
'where',
37
'when',
38
].map(s => s + '\\b')
39
);
40
const continuationRegex = new RegExp(`^(${continuations.join('|')})`);
41
42
/**
43
* Returns true if the given line is a line where we continue completion where
44
* the indentation level equals the current indentation level.
45
*
46
* TODO: Should probably be language specific
47
*/
48
function isContinuationLine(line: string) {
49
return continuationRegex.test(line.trimLeft().toLowerCase());
50
}
51
52
/**
53
* Return the indentation level of a given single line.
54
*
55
* If the line is blank, return undefined.
56
*
57
* TODO: Possibly support tabs specially?
58
*/
59
function indentationOfLine(line: string): number | undefined {
60
// [^] is used to match any character include '`r', otherwise this regex never matches on
61
// a file containing Windows newlines.
62
// TODO this is a bit of hack and ideally we would be using the "right" newline character at the
63
// point where we split/join lines.
64
const match = /^(\s*)([^]*)$/.exec(line);
65
if (match && match[2] && match[2].length > 0) {
66
return match[1].length;
67
} else {
68
return undefined;
69
}
70
}
71
72
/**
73
* Represents the indentation around the context of a cursor position in the code.
74
*
75
* The indentation level of the current line is the number of leading whitespace
76
* characters. If the current line is blank, we define its indentation level to
77
* be that of the preceding line (recursive if that is also blank).
78
*
79
* The indentation level of the next line is defined analogously, but recurses
80
* forwards until a non-blank line is encountered. It is `undefined` if there
81
* are no non-blank lines after the current.
82
*/
83
export interface ContextIndentation {
84
/**
85
* Next smaller indentation above the current line (guaranteed to be
86
* smaller than `current`, or else undefined).
87
*/
88
prev: number | undefined;
89
/** Indentation at the current line */
90
current: number;
91
/** Indentation at the following line */
92
next: number | undefined;
93
}
94
95
/**
96
* Return the context indentation corresponding to a given position.
97
*/
98
export function contextIndentation(doc: StringText, offset: number, languageId: string): ContextIndentation {
99
return contextIndentationFromText(doc.value, offset, languageId);
100
}
101
102
/**
103
* Return the context indentation corresponding to a given offset in text.
104
*/
105
export function contextIndentationFromText(source: string, offset: number, languageId: string): ContextIndentation {
106
const prevLines = source.slice(0, offset).split('\n');
107
const nextLines = source.slice(offset).split('\n');
108
function seekNonBlank(lines: string[], start: number, direction: -1 | 1): [number | undefined, number | undefined] {
109
let i = start;
110
let ind,
111
indIdx: number | undefined = undefined;
112
while (ind === undefined && i >= 0 && i < lines.length) {
113
ind = indentationOfLine(lines[i]);
114
indIdx = i;
115
i += direction;
116
}
117
if (languageId === 'python' && direction === -1) {
118
// HACK: special case to support multi-statement completions after Python doc comments.
119
// The logic looks for comments formatted as described in PEP 257.
120
121
// The final iteration of the indentation loop will have got us to one before the "current line".
122
i++;
123
const trimmedLine = lines[i].trim();
124
125
if (trimmedLine.endsWith(`"""`)) {
126
const isSingleLineDocString = trimmedLine.startsWith(`"""`) && trimmedLine !== `"""`;
127
if (!isSingleLineDocString) {
128
// Look backwards for the opening """"
129
i--;
130
while (i >= 0 && !lines[i].trim().startsWith(`"""`)) {
131
i--;
132
}
133
}
134
// i should point to the line with the opening """, if found.
135
// If i is negative then we never found the opening """". Give up and use the indentation
136
// we originally calculated.
137
if (i >= 0) {
138
ind = undefined;
139
i--;
140
// This is the same loop as above but specialised for direction = -1
141
while (ind === undefined && i >= 0) {
142
ind = indentationOfLine(lines[i]);
143
indIdx = i;
144
i--;
145
}
146
}
147
}
148
}
149
return [ind, indIdx];
150
}
151
const [current, currentIdx] = seekNonBlank(prevLines, prevLines.length - 1, -1);
152
const prev = (() => {
153
if (current === undefined || currentIdx === undefined) {
154
return undefined;
155
}
156
for (let i = currentIdx - 1; i >= 0; i--) {
157
const ind = indentationOfLine(prevLines[i]);
158
if (ind !== undefined && ind < current) {
159
return ind;
160
}
161
}
162
})();
163
const [next] = seekNonBlank(nextLines, 1, 1); // Skip the current line.
164
return {
165
prev,
166
current: current ?? 0,
167
next,
168
};
169
}
170
171
// If the model thinks we are at the end of a line, do we want to offer a completion
172
// for the next line? For now (05 Oct 2021) we leave it as false to minimise behaviour
173
// changes between parsing and indentation mode.
174
const OfferNextLineCompletion = false;
175
176
/**
177
* Return an offset where the completion ends its current context, or
178
* "continue" if it has not yet ended.
179
*
180
* A completion should be continued if it is:
181
* - A very long line that did not yet end; or
182
* - A multi-line context that is not yet ended.
183
*
184
* We use indentation with continuation patterns to determine whether a context
185
* is ended.
186
*/
187
export function completionCutOrContinue(
188
completion: string,
189
contextIndentation: ContextIndentation,
190
previewText: string | undefined
191
): number | 'continue' {
192
const completionLines = completion.split('\n');
193
const isContinuation = previewText !== undefined;
194
const lastLineOfPreview = previewText?.split('\n').pop();
195
let startLine = 0;
196
if (isContinuation) {
197
if (lastLineOfPreview?.trim() !== '' && completionLines[0].trim() !== '') {
198
// If we're in the middle of a line after the preview, we should at least finish it.
199
startLine++;
200
}
201
}
202
if (!isContinuation && OfferNextLineCompletion && completionLines[0].trim() === '') {
203
// See the comment on `OfferNextLineCompletion` for why we might do this.
204
startLine++;
205
}
206
if (!isContinuation) {
207
// We want to offer at least one line.
208
startLine++;
209
}
210
if (completionLines.length === startLine) {
211
// A single line that did not yet end.
212
return 'continue';
213
}
214
const breakIndentation = Math.max(contextIndentation.current, contextIndentation.next ?? 0);
215
for (let i = startLine; i < completionLines.length; i++) {
216
let line = completionLines[i];
217
if (i === 0 && lastLineOfPreview !== undefined) {
218
line = lastLineOfPreview + line;
219
}
220
const ind = indentationOfLine(line);
221
if (ind !== undefined && (ind < breakIndentation || (ind === breakIndentation && !isContinuationLine(line)))) {
222
return completionLines.slice(0, i).join('\n').length;
223
}
224
}
225
return 'continue';
226
}
227
228
/**
229
* Returns a callback appropriate as `finishedCb` for
230
* `CompletionStream.streamChoices` that terminates a block according to
231
* indentation-logic.
232
*/
233
export function indentationBlockFinished(
234
contextIndentation: ContextIndentation,
235
previewText: string | undefined
236
): (completion: string) => Promise<number | undefined> {
237
// NOTE: The returned callback is only async because streamChoices needs an
238
// async callback
239
return async (completion: string) => {
240
const res = completionCutOrContinue(completion, contextIndentation, previewText);
241
// streamChoices needs a callback with bad type signature where
242
// undefined really means "continue".
243
return res === 'continue' ? undefined : res;
244
};
245
}
246
247