Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/model/guidesTextModelPart.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 { findLast } from '../../../base/common/arraysFind.js';
7
import * as strings from '../../../base/common/strings.js';
8
import { CursorColumns } from '../core/cursorColumns.js';
9
import { IPosition, Position } from '../core/position.js';
10
import { Range } from '../core/range.js';
11
import type { TextModel } from './textModel.js';
12
import { TextModelPart } from './textModelPart.js';
13
import { computeIndentLevel } from './utils.js';
14
import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from '../languages/languageConfigurationRegistry.js';
15
import { BracketGuideOptions, HorizontalGuidesState, IActiveIndentGuideInfo, IGuidesTextModelPart, IndentGuide, IndentGuideHorizontalLine } from '../textModelGuides.js';
16
import { BugIndicatingError } from '../../../base/common/errors.js';
17
18
export class GuidesTextModelPart extends TextModelPart implements IGuidesTextModelPart {
19
constructor(
20
private readonly textModel: TextModel,
21
private readonly languageConfigurationService: ILanguageConfigurationService
22
) {
23
super();
24
}
25
26
private getLanguageConfiguration(
27
languageId: string
28
): ResolvedLanguageConfiguration {
29
return this.languageConfigurationService.getLanguageConfiguration(
30
languageId
31
);
32
}
33
34
private _computeIndentLevel(lineIndex: number): number {
35
return computeIndentLevel(
36
this.textModel.getLineContent(lineIndex + 1),
37
this.textModel.getOptions().tabSize
38
);
39
}
40
41
public getActiveIndentGuide(
42
lineNumber: number,
43
minLineNumber: number,
44
maxLineNumber: number
45
): IActiveIndentGuideInfo {
46
this.assertNotDisposed();
47
const lineCount = this.textModel.getLineCount();
48
49
if (lineNumber < 1 || lineNumber > lineCount) {
50
throw new BugIndicatingError('Illegal value for lineNumber');
51
}
52
53
const foldingRules = this.getLanguageConfiguration(
54
this.textModel.getLanguageId()
55
).foldingRules;
56
const offSide = Boolean(foldingRules && foldingRules.offSide);
57
58
let up_aboveContentLineIndex =
59
-2; /* -2 is a marker for not having computed it */
60
let up_aboveContentLineIndent = -1;
61
let up_belowContentLineIndex =
62
-2; /* -2 is a marker for not having computed it */
63
let up_belowContentLineIndent = -1;
64
const up_resolveIndents = (lineNumber: number) => {
65
if (
66
up_aboveContentLineIndex !== -1 &&
67
(up_aboveContentLineIndex === -2 ||
68
up_aboveContentLineIndex > lineNumber - 1)
69
) {
70
up_aboveContentLineIndex = -1;
71
up_aboveContentLineIndent = -1;
72
73
// must find previous line with content
74
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
75
const indent = this._computeIndentLevel(lineIndex);
76
if (indent >= 0) {
77
up_aboveContentLineIndex = lineIndex;
78
up_aboveContentLineIndent = indent;
79
break;
80
}
81
}
82
}
83
84
if (up_belowContentLineIndex === -2) {
85
up_belowContentLineIndex = -1;
86
up_belowContentLineIndent = -1;
87
88
// must find next line with content
89
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
90
const indent = this._computeIndentLevel(lineIndex);
91
if (indent >= 0) {
92
up_belowContentLineIndex = lineIndex;
93
up_belowContentLineIndent = indent;
94
break;
95
}
96
}
97
}
98
};
99
100
let down_aboveContentLineIndex =
101
-2; /* -2 is a marker for not having computed it */
102
let down_aboveContentLineIndent = -1;
103
let down_belowContentLineIndex =
104
-2; /* -2 is a marker for not having computed it */
105
let down_belowContentLineIndent = -1;
106
const down_resolveIndents = (lineNumber: number) => {
107
if (down_aboveContentLineIndex === -2) {
108
down_aboveContentLineIndex = -1;
109
down_aboveContentLineIndent = -1;
110
111
// must find previous line with content
112
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
113
const indent = this._computeIndentLevel(lineIndex);
114
if (indent >= 0) {
115
down_aboveContentLineIndex = lineIndex;
116
down_aboveContentLineIndent = indent;
117
break;
118
}
119
}
120
}
121
122
if (
123
down_belowContentLineIndex !== -1 &&
124
(down_belowContentLineIndex === -2 ||
125
down_belowContentLineIndex < lineNumber - 1)
126
) {
127
down_belowContentLineIndex = -1;
128
down_belowContentLineIndent = -1;
129
130
// must find next line with content
131
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
132
const indent = this._computeIndentLevel(lineIndex);
133
if (indent >= 0) {
134
down_belowContentLineIndex = lineIndex;
135
down_belowContentLineIndent = indent;
136
break;
137
}
138
}
139
}
140
};
141
142
let startLineNumber = 0;
143
let goUp = true;
144
let endLineNumber = 0;
145
let goDown = true;
146
let indent = 0;
147
148
let initialIndent = 0;
149
150
for (let distance = 0; goUp || goDown; distance++) {
151
const upLineNumber = lineNumber - distance;
152
const downLineNumber = lineNumber + distance;
153
154
if (distance > 1 && (upLineNumber < 1 || upLineNumber < minLineNumber)) {
155
goUp = false;
156
}
157
if (
158
distance > 1 &&
159
(downLineNumber > lineCount || downLineNumber > maxLineNumber)
160
) {
161
goDown = false;
162
}
163
if (distance > 50000) {
164
// stop processing
165
goUp = false;
166
goDown = false;
167
}
168
169
let upLineIndentLevel: number = -1;
170
if (goUp && upLineNumber >= 1) {
171
// compute indent level going up
172
const currentIndent = this._computeIndentLevel(upLineNumber - 1);
173
if (currentIndent >= 0) {
174
// This line has content (besides whitespace)
175
// Use the line's indent
176
up_belowContentLineIndex = upLineNumber - 1;
177
up_belowContentLineIndent = currentIndent;
178
upLineIndentLevel = Math.ceil(
179
currentIndent / this.textModel.getOptions().indentSize
180
);
181
} else {
182
up_resolveIndents(upLineNumber);
183
upLineIndentLevel = this._getIndentLevelForWhitespaceLine(
184
offSide,
185
up_aboveContentLineIndent,
186
up_belowContentLineIndent
187
);
188
}
189
}
190
191
let downLineIndentLevel = -1;
192
if (goDown && downLineNumber <= lineCount) {
193
// compute indent level going down
194
const currentIndent = this._computeIndentLevel(downLineNumber - 1);
195
if (currentIndent >= 0) {
196
// This line has content (besides whitespace)
197
// Use the line's indent
198
down_aboveContentLineIndex = downLineNumber - 1;
199
down_aboveContentLineIndent = currentIndent;
200
downLineIndentLevel = Math.ceil(
201
currentIndent / this.textModel.getOptions().indentSize
202
);
203
} else {
204
down_resolveIndents(downLineNumber);
205
downLineIndentLevel = this._getIndentLevelForWhitespaceLine(
206
offSide,
207
down_aboveContentLineIndent,
208
down_belowContentLineIndent
209
);
210
}
211
}
212
213
if (distance === 0) {
214
initialIndent = upLineIndentLevel;
215
continue;
216
}
217
218
if (distance === 1) {
219
if (
220
downLineNumber <= lineCount &&
221
downLineIndentLevel >= 0 &&
222
initialIndent + 1 === downLineIndentLevel
223
) {
224
// This is the beginning of a scope, we have special handling here, since we want the
225
// child scope indent to be active, not the parent scope
226
goUp = false;
227
startLineNumber = downLineNumber;
228
endLineNumber = downLineNumber;
229
indent = downLineIndentLevel;
230
continue;
231
}
232
233
if (
234
upLineNumber >= 1 &&
235
upLineIndentLevel >= 0 &&
236
upLineIndentLevel - 1 === initialIndent
237
) {
238
// This is the end of a scope, just like above
239
goDown = false;
240
startLineNumber = upLineNumber;
241
endLineNumber = upLineNumber;
242
indent = upLineIndentLevel;
243
continue;
244
}
245
246
startLineNumber = lineNumber;
247
endLineNumber = lineNumber;
248
indent = initialIndent;
249
if (indent === 0) {
250
// No need to continue
251
return { startLineNumber, endLineNumber, indent };
252
}
253
}
254
255
if (goUp) {
256
if (upLineIndentLevel >= indent) {
257
startLineNumber = upLineNumber;
258
} else {
259
goUp = false;
260
}
261
}
262
if (goDown) {
263
if (downLineIndentLevel >= indent) {
264
endLineNumber = downLineNumber;
265
} else {
266
goDown = false;
267
}
268
}
269
}
270
271
return { startLineNumber, endLineNumber, indent };
272
}
273
274
public getLinesBracketGuides(
275
startLineNumber: number,
276
endLineNumber: number,
277
activePosition: IPosition | null,
278
options: BracketGuideOptions
279
): IndentGuide[][] {
280
const result: IndentGuide[][] = [];
281
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
282
result.push([]);
283
}
284
285
// If requested, this could be made configurable.
286
const includeSingleLinePairs = true;
287
288
const bracketPairs =
289
this.textModel.bracketPairs.getBracketPairsInRangeWithMinIndentation(
290
new Range(
291
startLineNumber,
292
1,
293
endLineNumber,
294
this.textModel.getLineMaxColumn(endLineNumber)
295
)
296
).toArray();
297
298
let activeBracketPairRange: Range | undefined = undefined;
299
if (activePosition && bracketPairs.length > 0) {
300
const bracketsContainingActivePosition = (
301
startLineNumber <= activePosition.lineNumber &&
302
activePosition.lineNumber <= endLineNumber
303
// We don't need to query the brackets again if the cursor is in the viewport
304
? bracketPairs
305
: this.textModel.bracketPairs.getBracketPairsInRange(
306
Range.fromPositions(activePosition)
307
).toArray()
308
).filter((bp) => Range.strictContainsPosition(bp.range, activePosition));
309
310
activeBracketPairRange = findLast(
311
bracketsContainingActivePosition,
312
(i) => includeSingleLinePairs || i.range.startLineNumber !== i.range.endLineNumber
313
)?.range;
314
}
315
316
const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions.independentColorPoolPerBracketType;
317
const colorProvider = new BracketPairGuidesClassNames();
318
319
for (const pair of bracketPairs) {
320
/*
321
322
323
{
324
|
325
}
326
327
{
328
|
329
----}
330
331
____{
332
|test
333
----}
334
335
renderHorizontalEndLineAtTheBottom:
336
{
337
|
338
|x}
339
--
340
renderHorizontalEndLineAtTheBottom:
341
____{
342
|test
343
| x }
344
----
345
*/
346
347
if (!pair.closingBracketRange) {
348
continue;
349
}
350
351
const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange);
352
353
if (!isActive && !options.includeInactive) {
354
continue;
355
}
356
357
const className =
358
colorProvider.getInlineClassName(pair.nestingLevel, pair.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) +
359
(options.highlightActive && isActive
360
? ' ' + colorProvider.activeClassName
361
: '');
362
363
364
const start = pair.openingBracketRange.getStartPosition();
365
const end = pair.closingBracketRange.getStartPosition();
366
367
const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive);
368
369
if (pair.range.startLineNumber === pair.range.endLineNumber) {
370
if (includeSingleLinePairs && horizontalGuides) {
371
372
result[pair.range.startLineNumber - startLineNumber].push(
373
new IndentGuide(
374
-1,
375
pair.openingBracketRange.getEndPosition().column,
376
className,
377
new IndentGuideHorizontalLine(false, end.column),
378
-1,
379
-1,
380
)
381
);
382
383
}
384
continue;
385
}
386
387
const endVisibleColumn = this.getVisibleColumnFromPosition(end);
388
const startVisibleColumn = this.getVisibleColumnFromPosition(
389
pair.openingBracketRange.getStartPosition()
390
);
391
const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1);
392
393
let renderHorizontalEndLineAtTheBottom = false;
394
395
396
const firstNonWsIndex = strings.firstNonWhitespaceIndex(
397
this.textModel.getLineContent(
398
pair.closingBracketRange.startLineNumber
399
)
400
);
401
const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1;
402
if (hasTextBeforeClosingBracket) {
403
renderHorizontalEndLineAtTheBottom = true;
404
}
405
406
407
const visibleGuideStartLineNumber = Math.max(start.lineNumber, startLineNumber);
408
const visibleGuideEndLineNumber = Math.min(end.lineNumber, endLineNumber);
409
410
const offset = renderHorizontalEndLineAtTheBottom ? 1 : 0;
411
412
for (let l = visibleGuideStartLineNumber; l < visibleGuideEndLineNumber + offset; l++) {
413
result[l - startLineNumber].push(
414
new IndentGuide(
415
guideVisibleColumn,
416
-1,
417
className,
418
null,
419
l === start.lineNumber ? start.column : -1,
420
l === end.lineNumber ? end.column : -1
421
)
422
);
423
}
424
425
if (horizontalGuides) {
426
if (start.lineNumber >= startLineNumber && startVisibleColumn > guideVisibleColumn) {
427
result[start.lineNumber - startLineNumber].push(
428
new IndentGuide(
429
guideVisibleColumn,
430
-1,
431
className,
432
new IndentGuideHorizontalLine(false, start.column),
433
-1,
434
-1,
435
)
436
);
437
}
438
439
if (end.lineNumber <= endLineNumber && endVisibleColumn > guideVisibleColumn) {
440
result[end.lineNumber - startLineNumber].push(
441
new IndentGuide(
442
guideVisibleColumn,
443
-1,
444
className,
445
new IndentGuideHorizontalLine(!renderHorizontalEndLineAtTheBottom, end.column),
446
-1,
447
-1,
448
)
449
);
450
}
451
}
452
}
453
454
for (const guides of result) {
455
guides.sort((a, b) => a.visibleColumn - b.visibleColumn);
456
}
457
458
return result;
459
}
460
461
private getVisibleColumnFromPosition(position: Position): number {
462
return (
463
CursorColumns.visibleColumnFromColumn(
464
this.textModel.getLineContent(position.lineNumber),
465
position.column,
466
this.textModel.getOptions().tabSize
467
) + 1
468
);
469
}
470
471
public getLinesIndentGuides(
472
startLineNumber: number,
473
endLineNumber: number
474
): number[] {
475
this.assertNotDisposed();
476
const lineCount = this.textModel.getLineCount();
477
478
if (startLineNumber < 1 || startLineNumber > lineCount) {
479
throw new Error('Illegal value for startLineNumber');
480
}
481
if (endLineNumber < 1 || endLineNumber > lineCount) {
482
throw new Error('Illegal value for endLineNumber');
483
}
484
485
const options = this.textModel.getOptions();
486
const foldingRules = this.getLanguageConfiguration(
487
this.textModel.getLanguageId()
488
).foldingRules;
489
const offSide = Boolean(foldingRules && foldingRules.offSide);
490
491
const result: number[] = new Array<number>(
492
endLineNumber - startLineNumber + 1
493
);
494
495
let aboveContentLineIndex =
496
-2; /* -2 is a marker for not having computed it */
497
let aboveContentLineIndent = -1;
498
499
let belowContentLineIndex =
500
-2; /* -2 is a marker for not having computed it */
501
let belowContentLineIndent = -1;
502
503
for (
504
let lineNumber = startLineNumber;
505
lineNumber <= endLineNumber;
506
lineNumber++
507
) {
508
const resultIndex = lineNumber - startLineNumber;
509
510
const currentIndent = this._computeIndentLevel(lineNumber - 1);
511
if (currentIndent >= 0) {
512
// This line has content (besides whitespace)
513
// Use the line's indent
514
aboveContentLineIndex = lineNumber - 1;
515
aboveContentLineIndent = currentIndent;
516
result[resultIndex] = Math.ceil(currentIndent / options.indentSize);
517
continue;
518
}
519
520
if (aboveContentLineIndex === -2) {
521
aboveContentLineIndex = -1;
522
aboveContentLineIndent = -1;
523
524
// must find previous line with content
525
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
526
const indent = this._computeIndentLevel(lineIndex);
527
if (indent >= 0) {
528
aboveContentLineIndex = lineIndex;
529
aboveContentLineIndent = indent;
530
break;
531
}
532
}
533
}
534
535
if (
536
belowContentLineIndex !== -1 &&
537
(belowContentLineIndex === -2 || belowContentLineIndex < lineNumber - 1)
538
) {
539
belowContentLineIndex = -1;
540
belowContentLineIndent = -1;
541
542
// must find next line with content
543
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
544
const indent = this._computeIndentLevel(lineIndex);
545
if (indent >= 0) {
546
belowContentLineIndex = lineIndex;
547
belowContentLineIndent = indent;
548
break;
549
}
550
}
551
}
552
553
result[resultIndex] = this._getIndentLevelForWhitespaceLine(
554
offSide,
555
aboveContentLineIndent,
556
belowContentLineIndent
557
);
558
}
559
return result;
560
}
561
562
private _getIndentLevelForWhitespaceLine(
563
offSide: boolean,
564
aboveContentLineIndent: number,
565
belowContentLineIndent: number
566
): number {
567
const options = this.textModel.getOptions();
568
569
if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) {
570
// At the top or bottom of the file
571
return 0;
572
} else if (aboveContentLineIndent < belowContentLineIndent) {
573
// we are inside the region above
574
return 1 + Math.floor(aboveContentLineIndent / options.indentSize);
575
} else if (aboveContentLineIndent === belowContentLineIndent) {
576
// we are in between two regions
577
return Math.ceil(belowContentLineIndent / options.indentSize);
578
} else {
579
if (offSide) {
580
// same level as region below
581
return Math.ceil(belowContentLineIndent / options.indentSize);
582
} else {
583
// we are inside the region that ends below
584
return 1 + Math.floor(belowContentLineIndent / options.indentSize);
585
}
586
}
587
}
588
}
589
590
export class BracketPairGuidesClassNames {
591
public readonly activeClassName = 'indent-active';
592
593
getInlineClassName(nestingLevel: number, nestingLevelOfEqualBracketType: number, independentColorPoolPerBracketType: boolean): string {
594
return this.getInlineClassNameOfLevel(independentColorPoolPerBracketType ? nestingLevelOfEqualBracketType : nestingLevel);
595
}
596
597
getInlineClassNameOfLevel(level: number): string {
598
// To support a dynamic amount of colors up to 6 colors,
599
// we use a number that is a lcm of all numbers from 1 to 6.
600
return `bracket-indent-guide lvl-${level % 30}`;
601
}
602
}
603
604