src/vs/editor/common/model/guidesTextModelPart.ts

602 LOC · 375 covered · 227 uncovered · 69 ranges · 1708 concepts · 15 introducers · 861 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- textModel.ts ×190
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, textModel.ts ×20
21 > private readonly languageConfigurationService: ILanguageConfigurationService
22 > ) {
23 > super();
24 > }
26 > private getLanguageConfiguration(
27 > languageId: string guidesTextModelPart.ts ×17
28 > ): ResolvedLanguageConfiguration {
29 > return this.languageConfigurationService.getLanguageConfiguration(
30 > languageId
31 > );
32 > }
34 > private _computeIndentLevel(lineIndex: number): number {
35 > return computeIndentLevel( guidesTextModelPart.ts ×17
36 > this.textModel.getLineContent(lineIndex + 1),
37 > this.textModel.getOptions().tabSize
38 > );
39 > }
41 > public getActiveIndentGuide(
42 > lineNumber: number, guidesTextModelPart.ts ×17
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 }
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) => {
66 > up_aboveContentLineIndex !== -1 &&
67 > (up_aboveContentLineIndex === -2 ||
68 > up_aboveContentLineIndex > lineNumber - 1) guidesTextModelPart.ts ×5
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 > };
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) { guidesTextModelPart.ts ×14
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) guidesTextModelPart.ts ×5
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 > };
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; guidesTextModelPart.ts ×7
156 > }
158 > distance > 1 &&
159 > (downLineNumber > lineCount || downLineNumber > maxLineNumber) guidesTextModelPart.ts ×7
161 > goDown = false; guidesTextModelPart.ts ×1
162 > }
163 > if (distance > 50000) { guidesTextModelPart.ts ×17
164 // stop processing
165 goUp = false;
166 goDown = false;
167 }
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); guidesTextModelPart.ts ×14
183 > upLineIndentLevel = this._getIndentLevelForWhitespaceLine(
184 > offSide,
185 > up_aboveContentLineIndent,
186 > up_belowContentLineIndent
187 > );
188 > }
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); guidesTextModelPart.ts ×14
205 > downLineIndentLevel = this._getIndentLevelForWhitespaceLine(
206 > offSide,
207 > down_aboveContentLineIndent,
208 > down_belowContentLineIndent
209 > );
210 > }
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 guidesTextModelPart.ts ×1
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 > }
233 > if (
234 > upLineNumber >= 1 &&
235 > upLineIndentLevel >= 0 &&
236 > upLineIndentLevel - 1 === initialIndent
237 > ) {
238 > // This is the end of a scope, just like above guidesTextModelPart.ts ×1
239 > goDown = false;
240 > startLineNumber = upLineNumber;
241 > endLineNumber = upLineNumber;
242 > indent = upLineIndentLevel;
243 > continue;
244 > }
246 > startLineNumber = lineNumber;
247 > endLineNumber = lineNumber;
248 > indent = initialIndent;
249 > if (indent === 0) {
250 > // No need to continue guidesTextModelPart.ts ×1
251 > return { startLineNumber, endLineNumber, indent };
252 > }
255 > if (goUp) {
256 > if (upLineIndentLevel >= indent) {
257 > startLineNumber = upLineNumber; guidesTextModelPart.ts ×1
259 > goUp = false;
260 > }
261 > }
262 > if (goDown) {
263 > if (downLineIndentLevel >= indent) {
264 > endLineNumber = downLineNumber; guidesTextModelPart.ts ×1
266 > goDown = false; guidesTextModelPart.ts ×1
267 > }
271 > return { startLineNumber, endLineNumber, indent };
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 }
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 }
471 > public getLinesIndentGuides(
472 > startLineNumber: number, guidesTextModelPart.ts ×5
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) { guidesTextModelPart.ts ×5
482 throw new Error('Illegal value for endLineNumber');
483 }
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 > }
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 }
535 > if (
536 > belowContentLineIndex !== -1 &&
537 > (belowContentLineIndex === -2 || belowContentLineIndex < lineNumber - 1)
539 > belowContentLineIndex = -1; guidesTextModelPart.ts ×14
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; guidesTextModelPart.ts ×5
560 > }
562 > private _getIndentLevelForWhitespaceLine(
563 > offSide: boolean, guidesTextModelPart.ts ×14
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) { guidesTextModelPart.ts ×14
573 > // we are inside the region above guidesTextModelPart.ts ×5
574 > return 1 + Math.floor(aboveContentLineIndent / options.indentSize);
575 > } else if (aboveContentLineIndent === belowContentLineIndent) { guidesTextModelPart.ts ×14
576 > // we are in between two regions guidesTextModelPart.ts ×1
577 > return Math.ceil(belowContentLineIndent / options.indentSize);
579 > if (offSide) { guidesTextModelPart.ts ×5
580 // same level as region below
581 return Math.ceil(belowContentLineIndent / options.indentSize);
583 > // we are inside the region that ends below
584 > return 1 + Math.floor(belowContentLineIndent / options.indentSize);
585 > }
586 > }
589 >
590 > export class BracketPairGuidesClassNames {
591 public readonly activeClassName = 'indent-active';
593 > getInlineClassName(nestingLevel: number, nestingLevelOfEqualBracketType: number, independentColorPoolPerBracketType: boolean): string {
594 return this.getInlineClassNameOfLevel(independentColorPoolPerBracketType ? nestingLevelOfEqualBracketType : nestingLevel);
595 }
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 }