Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/languages/autoIndent.ts
3294 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 * as strings from '../../../base/common/strings.js';
7
import { Range } from '../core/range.js';
8
import { ITextModel } from '../model.js';
9
import { IndentAction } from './languageConfiguration.js';
10
import { IndentConsts } from './supports/indentRules.js';
11
import { EditorAutoIndentStrategy } from '../config/editorOptions.js';
12
import { ILanguageConfigurationService } from './languageConfigurationRegistry.js';
13
import { IViewLineTokens } from '../tokens/lineTokens.js';
14
import { IndentationContextProcessor, isLanguageDifferentFromLineStart, ProcessedIndentRulesSupport } from './supports/indentationLineProcessor.js';
15
import { CursorConfiguration } from '../cursorCommon.js';
16
17
export interface IVirtualModel {
18
tokenization: {
19
getLineTokens(lineNumber: number): IViewLineTokens;
20
getLanguageId(): string;
21
getLanguageIdAtPosition(lineNumber: number, column: number): string;
22
forceTokenization?(lineNumber: number): void;
23
};
24
getLineContent(lineNumber: number): string;
25
}
26
27
export interface IIndentConverter {
28
shiftIndent(indentation: string): string;
29
unshiftIndent(indentation: string): string;
30
normalizeIndentation?(indentation: string): string;
31
}
32
33
/**
34
* Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace.
35
* Result:
36
* -1: run into the boundary of embedded languages
37
* 0: every line above are invalid
38
* else: nearest preceding line of the same language
39
*/
40
function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, processedIndentRulesSupport: ProcessedIndentRulesSupport) {
41
const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0);
42
if (lineNumber > 1) {
43
let lastLineNumber: number;
44
let resultLineNumber = -1;
45
46
for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) {
47
if (model.tokenization.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) {
48
return resultLineNumber;
49
}
50
const text = model.getLineContent(lastLineNumber);
51
if (processedIndentRulesSupport.shouldIgnore(lastLineNumber) || /^\s+$/.test(text) || text === '') {
52
resultLineNumber = lastLineNumber;
53
continue;
54
}
55
56
return lastLineNumber;
57
}
58
}
59
60
return -1;
61
}
62
63
/**
64
* Get inherited indentation from above lines.
65
* 1. Find the nearest preceding line which doesn't match unIndentedLinePattern.
66
* 2. If this line matches indentNextLinePattern or increaseIndentPattern, it means that the indent level of `lineNumber` should be 1 greater than this line.
67
* 3. If this line doesn't match any indent rules
68
* a. check whether the line above it matches indentNextLinePattern
69
* b. If not, the indent level of this line is the result
70
* c. If so, it means the indent of this line is *temporary*, go upward utill we find a line whose indent is not temporary (the same workflow a -> b -> c).
71
* 4. Otherwise, we fail to get an inherited indent from aboves. Return null and we should not touch the indent of `lineNumber`
72
*
73
* This function only return the inherited indent based on above lines, it doesn't check whether current line should decrease or not.
74
*/
75
export function getInheritIndentForLine(
76
autoIndent: EditorAutoIndentStrategy,
77
model: IVirtualModel,
78
lineNumber: number,
79
honorIntentialIndent: boolean = true,
80
languageConfigurationService: ILanguageConfigurationService
81
): { indentation: string; action: IndentAction | null; line?: number } | null {
82
if (autoIndent < EditorAutoIndentStrategy.Full) {
83
return null;
84
}
85
86
const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport;
87
if (!indentRulesSupport) {
88
return null;
89
}
90
const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentRulesSupport, languageConfigurationService);
91
92
if (lineNumber <= 1) {
93
return {
94
indentation: '',
95
action: null
96
};
97
}
98
99
// Use no indent if this is the first non-blank line
100
for (let priorLineNumber = lineNumber - 1; priorLineNumber > 0; priorLineNumber--) {
101
if (model.getLineContent(priorLineNumber) !== '') {
102
break;
103
}
104
if (priorLineNumber === 1) {
105
return {
106
indentation: '',
107
action: null
108
};
109
}
110
}
111
112
const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, processedIndentRulesSupport);
113
if (precedingUnIgnoredLine < 0) {
114
return null;
115
} else if (precedingUnIgnoredLine < 1) {
116
return {
117
indentation: '',
118
action: null
119
};
120
}
121
122
if (processedIndentRulesSupport.shouldIncrease(precedingUnIgnoredLine) || processedIndentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLine)) {
123
const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine);
124
return {
125
indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent),
126
action: IndentAction.Indent,
127
line: precedingUnIgnoredLine
128
};
129
} else if (processedIndentRulesSupport.shouldDecrease(precedingUnIgnoredLine)) {
130
const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine);
131
return {
132
indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent),
133
action: null,
134
line: precedingUnIgnoredLine
135
};
136
} else {
137
// precedingUnIgnoredLine can not be ignored.
138
// it doesn't increase indent of following lines
139
// it doesn't increase just next line
140
// so current line is not affect by precedingUnIgnoredLine
141
// and then we should get a correct inheritted indentation from above lines
142
if (precedingUnIgnoredLine === 1) {
143
return {
144
indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)),
145
action: null,
146
line: precedingUnIgnoredLine
147
};
148
}
149
150
const previousLine = precedingUnIgnoredLine - 1;
151
152
const previousLineIndentMetadata = indentRulesSupport.getIndentMetadata(model.getLineContent(previousLine));
153
if (!(previousLineIndentMetadata & (IndentConsts.INCREASE_MASK | IndentConsts.DECREASE_MASK)) &&
154
(previousLineIndentMetadata & IndentConsts.INDENT_NEXTLINE_MASK)) {
155
let stopLine = 0;
156
for (let i = previousLine - 1; i > 0; i--) {
157
if (processedIndentRulesSupport.shouldIndentNextLine(i)) {
158
continue;
159
}
160
stopLine = i;
161
break;
162
}
163
164
return {
165
indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)),
166
action: null,
167
line: stopLine + 1
168
};
169
}
170
171
if (honorIntentialIndent) {
172
return {
173
indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)),
174
action: null,
175
line: precedingUnIgnoredLine
176
};
177
} else {
178
// search from precedingUnIgnoredLine until we find one whose indent is not temporary
179
for (let i = precedingUnIgnoredLine; i > 0; i--) {
180
if (processedIndentRulesSupport.shouldIncrease(i)) {
181
return {
182
indentation: strings.getLeadingWhitespace(model.getLineContent(i)),
183
action: IndentAction.Indent,
184
line: i
185
};
186
} else if (processedIndentRulesSupport.shouldIndentNextLine(i)) {
187
let stopLine = 0;
188
for (let j = i - 1; j > 0; j--) {
189
if (processedIndentRulesSupport.shouldIndentNextLine(i)) {
190
continue;
191
}
192
stopLine = j;
193
break;
194
}
195
196
return {
197
indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)),
198
action: null,
199
line: stopLine + 1
200
};
201
} else if (processedIndentRulesSupport.shouldDecrease(i)) {
202
return {
203
indentation: strings.getLeadingWhitespace(model.getLineContent(i)),
204
action: null,
205
line: i
206
};
207
}
208
}
209
210
return {
211
indentation: strings.getLeadingWhitespace(model.getLineContent(1)),
212
action: null,
213
line: 1
214
};
215
}
216
}
217
}
218
219
export function getGoodIndentForLine(
220
autoIndent: EditorAutoIndentStrategy,
221
virtualModel: IVirtualModel,
222
languageId: string,
223
lineNumber: number,
224
indentConverter: IIndentConverter,
225
languageConfigurationService: ILanguageConfigurationService
226
): string | null {
227
if (autoIndent < EditorAutoIndentStrategy.Full) {
228
return null;
229
}
230
231
const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId);
232
if (!richEditSupport) {
233
return null;
234
}
235
236
const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;
237
if (!indentRulesSupport) {
238
return null;
239
}
240
241
const processedIndentRulesSupport = new ProcessedIndentRulesSupport(virtualModel, indentRulesSupport, languageConfigurationService);
242
const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService);
243
244
if (indent) {
245
const inheritLine = indent.line;
246
if (inheritLine !== undefined) {
247
// Apply enter action as long as there are only whitespace lines between inherited line and this line.
248
let shouldApplyEnterRules = true;
249
for (let inBetweenLine = inheritLine; inBetweenLine < lineNumber - 1; inBetweenLine++) {
250
if (!/^\s*$/.test(virtualModel.getLineContent(inBetweenLine))) {
251
shouldApplyEnterRules = false;
252
break;
253
}
254
}
255
if (shouldApplyEnterRules) {
256
const enterResult = richEditSupport.onEnter(autoIndent, '', virtualModel.getLineContent(inheritLine), '');
257
258
if (enterResult) {
259
let indentation = strings.getLeadingWhitespace(virtualModel.getLineContent(inheritLine));
260
261
if (enterResult.removeText) {
262
indentation = indentation.substring(0, indentation.length - enterResult.removeText);
263
}
264
265
if (
266
(enterResult.indentAction === IndentAction.Indent) ||
267
(enterResult.indentAction === IndentAction.IndentOutdent)
268
) {
269
indentation = indentConverter.shiftIndent(indentation);
270
} else if (enterResult.indentAction === IndentAction.Outdent) {
271
indentation = indentConverter.unshiftIndent(indentation);
272
}
273
274
if (processedIndentRulesSupport.shouldDecrease(lineNumber)) {
275
indentation = indentConverter.unshiftIndent(indentation);
276
}
277
278
if (enterResult.appendText) {
279
indentation += enterResult.appendText;
280
}
281
282
return strings.getLeadingWhitespace(indentation);
283
}
284
}
285
}
286
287
if (processedIndentRulesSupport.shouldDecrease(lineNumber)) {
288
if (indent.action === IndentAction.Indent) {
289
return indent.indentation;
290
} else {
291
return indentConverter.unshiftIndent(indent.indentation);
292
}
293
} else {
294
if (indent.action === IndentAction.Indent) {
295
return indentConverter.shiftIndent(indent.indentation);
296
} else {
297
return indent.indentation;
298
}
299
}
300
}
301
return null;
302
}
303
304
export function getIndentForEnter(
305
autoIndent: EditorAutoIndentStrategy,
306
model: ITextModel,
307
range: Range,
308
indentConverter: IIndentConverter,
309
languageConfigurationService: ILanguageConfigurationService
310
): { beforeEnter: string; afterEnter: string } | null {
311
if (autoIndent < EditorAutoIndentStrategy.Full) {
312
return null;
313
}
314
const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn);
315
const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;
316
if (!indentRulesSupport) {
317
return null;
318
}
319
320
model.tokenization.forceTokenization(range.startLineNumber);
321
const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService);
322
const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range);
323
const afterEnterProcessedTokens = processedContextTokens.afterRangeProcessedTokens;
324
const beforeEnterProcessedTokens = processedContextTokens.beforeRangeProcessedTokens;
325
const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterProcessedTokens.getLineContent());
326
327
const virtualModel = createVirtualModelWithModifiedTokensAtLine(model, range.startLineNumber, beforeEnterProcessedTokens);
328
const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition());
329
const currentLine = model.getLineContent(range.startLineNumber);
330
const currentLineIndent = strings.getLeadingWhitespace(currentLine);
331
const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService);
332
if (!afterEnterAction) {
333
const beforeEnter = languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent;
334
return {
335
beforeEnter: beforeEnter,
336
afterEnter: beforeEnter
337
};
338
}
339
340
let afterEnterIndent = languageIsDifferentFromLineStart ? currentLineIndent : afterEnterAction.indentation;
341
342
if (afterEnterAction.action === IndentAction.Indent) {
343
afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent);
344
}
345
346
if (indentRulesSupport.shouldDecrease(afterEnterProcessedTokens.getLineContent())) {
347
afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent);
348
}
349
350
return {
351
beforeEnter: languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent,
352
afterEnter: afterEnterIndent
353
};
354
}
355
356
/**
357
* We should always allow intentional indentation. It means, if users change the indentation of `lineNumber` and the content of
358
* this line doesn't match decreaseIndentPattern, we should not adjust the indentation.
359
*/
360
export function getIndentActionForType(
361
cursorConfig: CursorConfiguration,
362
model: ITextModel,
363
range: Range,
364
ch: string,
365
indentConverter: IIndentConverter,
366
languageConfigurationService: ILanguageConfigurationService
367
): string | null {
368
const autoIndent = cursorConfig.autoIndent;
369
if (autoIndent < EditorAutoIndentStrategy.Full) {
370
return null;
371
}
372
const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition());
373
if (languageIsDifferentFromLineStart) {
374
// this line has mixed languages and indentation rules will not work
375
return null;
376
}
377
378
const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn);
379
const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;
380
if (!indentRulesSupport) {
381
return null;
382
}
383
384
const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService);
385
const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range);
386
const beforeRangeText = processedContextTokens.beforeRangeProcessedTokens.getLineContent();
387
const afterRangeText = processedContextTokens.afterRangeProcessedTokens.getLineContent();
388
const textAroundRange = beforeRangeText + afterRangeText;
389
const textAroundRangeWithCharacter = beforeRangeText + ch + afterRangeText;
390
391
// If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted
392
// Users might change the indentation by purpose and we should honor that instead of readjusting.
393
if (!indentRulesSupport.shouldDecrease(textAroundRange) && indentRulesSupport.shouldDecrease(textAroundRangeWithCharacter)) {
394
// after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner.
395
// 1. Get inherited indent action
396
const r = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService);
397
if (!r) {
398
return null;
399
}
400
401
let indentation = r.indentation;
402
if (r.action !== IndentAction.Indent) {
403
indentation = indentConverter.unshiftIndent(indentation);
404
}
405
406
return indentation;
407
}
408
409
const previousLineNumber = range.startLineNumber - 1;
410
if (previousLineNumber > 0) {
411
const previousLine = model.getLineContent(previousLineNumber);
412
if (indentRulesSupport.shouldIndentNextLine(previousLine) && indentRulesSupport.shouldIncrease(textAroundRangeWithCharacter)) {
413
const inheritedIndentationData = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService);
414
const inheritedIndentation = inheritedIndentationData?.indentation;
415
if (inheritedIndentation !== undefined) {
416
const currentLine = model.getLineContent(range.startLineNumber);
417
const actualCurrentIndentation = strings.getLeadingWhitespace(currentLine);
418
const inferredCurrentIndentation = indentConverter.shiftIndent(inheritedIndentation);
419
// If the inferred current indentation is not equal to the actual current indentation, then the indentation has been intentionally changed, in that case keep it
420
const inferredIndentationEqualsActual = inferredCurrentIndentation === actualCurrentIndentation;
421
const textAroundRangeContainsOnlyWhitespace = /^\s*$/.test(textAroundRange);
422
const autoClosingPairs = cursorConfig.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch);
423
const autoClosingPairExists = autoClosingPairs && autoClosingPairs.length > 0;
424
const isChFirstNonWhitespaceCharacterAndInAutoClosingPair = autoClosingPairExists && textAroundRangeContainsOnlyWhitespace;
425
if (inferredIndentationEqualsActual && isChFirstNonWhitespaceCharacterAndInAutoClosingPair) {
426
return inheritedIndentation;
427
}
428
}
429
}
430
}
431
432
return null;
433
}
434
435
export function getIndentMetadata(
436
model: ITextModel,
437
lineNumber: number,
438
languageConfigurationService: ILanguageConfigurationService
439
): number | null {
440
const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport;
441
if (!indentRulesSupport) {
442
return null;
443
}
444
if (lineNumber < 1 || lineNumber > model.getLineCount()) {
445
return null;
446
}
447
return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber));
448
}
449
450
function createVirtualModelWithModifiedTokensAtLine(model: ITextModel, modifiedLineNumber: number, modifiedTokens: IViewLineTokens): IVirtualModel {
451
const virtualModel: IVirtualModel = {
452
tokenization: {
453
getLineTokens: (lineNumber: number): IViewLineTokens => {
454
if (lineNumber === modifiedLineNumber) {
455
return modifiedTokens;
456
} else {
457
return model.tokenization.getLineTokens(lineNumber);
458
}
459
},
460
getLanguageId: (): string => {
461
return model.getLanguageId();
462
},
463
getLanguageIdAtPosition: (lineNumber: number, column: number): string => {
464
return model.getLanguageIdAtPosition(lineNumber, column);
465
},
466
},
467
getLineContent: (lineNumber: number): string => {
468
if (lineNumber === modifiedLineNumber) {
469
return modifiedTokens.getLineContent();
470
} else {
471
return model.getLineContent(lineNumber);
472
}
473
}
474
};
475
return virtualModel;
476
}
477
478
479