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.
/*---------------------------------------------------------------------------------------------
textModel.ts ×190
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLast } from '../../../base/common/arraysFind.js';
import * as strings from '../../../base/common/strings.js';
import { CursorColumns } from '../core/cursorColumns.js';
import { IPosition, Position } from '../core/position.js';
import { Range } from '../core/range.js';
import type { TextModel } from './textModel.js';
import { TextModelPart } from './textModelPart.js';
import { computeIndentLevel } from './utils.js';
import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from '../languages/languageConfigurationRegistry.js';
import { BracketGuideOptions, HorizontalGuidesState, IActiveIndentGuideInfo, IGuidesTextModelPart, IndentGuide, IndentGuideHorizontalLine } from '../textModelGuides.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
export class GuidesTextModelPart extends TextModelPart implements IGuidesTextModelPart {
constructor(
private readonly languageConfigurationService: ILanguageConfigurationService
) {
super();
}
private getLanguageConfiguration(
): ResolvedLanguageConfiguration {
return this.languageConfigurationService.getLanguageConfiguration(
languageId
);
}
private _computeIndentLevel(lineIndex: number): number {
this.textModel.getLineContent(lineIndex + 1),
this.textModel.getOptions().tabSize
);
}
public getActiveIndentGuide(
minLineNumber: number,
maxLineNumber: number
): IActiveIndentGuideInfo {
this.assertNotDisposed();
const lineCount = this.textModel.getLineCount();
if (lineNumber < 1 || lineNumber > lineCount) {
throw new BugIndicatingError('Illegal value for lineNumber');
}
const foldingRules = this.getLanguageConfiguration(
this.textModel.getLanguageId()
).foldingRules;
const offSide = Boolean(foldingRules && foldingRules.offSide);
let up_aboveContentLineIndex =
-2; /* -2 is a marker for not having computed it */
let up_aboveContentLineIndent = -1;
let up_belowContentLineIndex =
-2; /* -2 is a marker for not having computed it */
let up_belowContentLineIndent = -1;
const up_resolveIndents = (lineNumber: number) => {
up_aboveContentLineIndex !== -1 &&
(up_aboveContentLineIndex === -2 ||
up_aboveContentLineIndex = -1;
up_aboveContentLineIndent = -1;
// must find previous line with content
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
up_aboveContentLineIndex = lineIndex;
up_aboveContentLineIndent = indent;
break;
}
}
}
if (up_belowContentLineIndex === -2) {
up_belowContentLineIndex = -1;
up_belowContentLineIndent = -1;
// must find next line with content
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
up_belowContentLineIndex = lineIndex;
up_belowContentLineIndent = indent;
break;
}
}
}
};
let down_aboveContentLineIndex =
-2; /* -2 is a marker for not having computed it */
let down_aboveContentLineIndent = -1;
let down_belowContentLineIndex =
-2; /* -2 is a marker for not having computed it */
let down_belowContentLineIndent = -1;
const down_resolveIndents = (lineNumber: number) => {
down_aboveContentLineIndex = -1;
down_aboveContentLineIndent = -1;
// must find previous line with content
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
down_aboveContentLineIndex = lineIndex;
down_aboveContentLineIndent = indent;
break;
}
}
}
if (
down_belowContentLineIndex !== -1 &&
(down_belowContentLineIndex === -2 ||
down_belowContentLineIndex = -1;
down_belowContentLineIndent = -1;
// must find next line with content
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
down_belowContentLineIndex = lineIndex;
down_belowContentLineIndent = indent;
break;
}
}
}
};
let startLineNumber = 0;
let goUp = true;
let endLineNumber = 0;
let goDown = true;
let indent = 0;
let initialIndent = 0;
for (let distance = 0; goUp || goDown; distance++) {
const upLineNumber = lineNumber - distance;
const downLineNumber = lineNumber + distance;
if (distance > 1 && (upLineNumber < 1 || upLineNumber < minLineNumber)) {
}
distance > 1 &&
}
// stop processing
goUp = false;
goDown = false;
}
let upLineIndentLevel: number = -1;
if (goUp && upLineNumber >= 1) {
// compute indent level going up
const currentIndent = this._computeIndentLevel(upLineNumber - 1);
if (currentIndent >= 0) {
// This line has content (besides whitespace)
// Use the line's indent
up_belowContentLineIndex = upLineNumber - 1;
up_belowContentLineIndent = currentIndent;
upLineIndentLevel = Math.ceil(
currentIndent / this.textModel.getOptions().indentSize
);
} else {
upLineIndentLevel = this._getIndentLevelForWhitespaceLine(
offSide,
up_aboveContentLineIndent,
up_belowContentLineIndent
);
}
let downLineIndentLevel = -1;
if (goDown && downLineNumber <= lineCount) {
// compute indent level going down
const currentIndent = this._computeIndentLevel(downLineNumber - 1);
if (currentIndent >= 0) {
// This line has content (besides whitespace)
// Use the line's indent
down_aboveContentLineIndex = downLineNumber - 1;
down_aboveContentLineIndent = currentIndent;
downLineIndentLevel = Math.ceil(
currentIndent / this.textModel.getOptions().indentSize
);
} else {
downLineIndentLevel = this._getIndentLevelForWhitespaceLine(
offSide,
down_aboveContentLineIndent,
down_belowContentLineIndent
);
}
if (distance === 0) {
initialIndent = upLineIndentLevel;
continue;
}
if (distance === 1) {
if (
downLineNumber <= lineCount &&
downLineIndentLevel >= 0 &&
initialIndent + 1 === downLineIndentLevel
) {
// This is the beginning of a scope, we have special handling here, since we want the
guidesTextModelPart.ts ×1
// child scope indent to be active, not the parent scope
goUp = false;
startLineNumber = downLineNumber;
endLineNumber = downLineNumber;
indent = downLineIndentLevel;
continue;
}
if (
upLineNumber >= 1 &&
upLineIndentLevel >= 0 &&
upLineIndentLevel - 1 === initialIndent
) {
goDown = false;
startLineNumber = upLineNumber;
endLineNumber = upLineNumber;
indent = upLineIndentLevel;
continue;
}
startLineNumber = lineNumber;
endLineNumber = lineNumber;
indent = initialIndent;
if (indent === 0) {
return { startLineNumber, endLineNumber, indent };
}
if (goUp) {
if (upLineIndentLevel >= indent) {
goUp = false;
}
}
if (goDown) {
if (downLineIndentLevel >= indent) {
}
return { startLineNumber, endLineNumber, indent };
public getLinesBracketGuides(
startLineNumber: number,
endLineNumber: number,
activePosition: IPosition | null,
options: BracketGuideOptions
): IndentGuide[][] {
const result: IndentGuide[][] = [];
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
result.push([]);
}
// If requested, this could be made configurable.
const includeSingleLinePairs = true;
const bracketPairs =
this.textModel.bracketPairs.getBracketPairsInRangeWithMinIndentation(
new Range(
startLineNumber,
1,
endLineNumber,
this.textModel.getLineMaxColumn(endLineNumber)
)
).toArray();
let activeBracketPairRange: Range | undefined = undefined;
if (activePosition && bracketPairs.length > 0) {
const bracketsContainingActivePosition = (
startLineNumber <= activePosition.lineNumber &&
activePosition.lineNumber <= endLineNumber
// We don't need to query the brackets again if the cursor is in the viewport
? bracketPairs
: this.textModel.bracketPairs.getBracketPairsInRange(
Range.fromPositions(activePosition)
).toArray()
).filter((bp) => Range.strictContainsPosition(bp.range, activePosition));
activeBracketPairRange = findLast(
bracketsContainingActivePosition,
(i) => includeSingleLinePairs || i.range.startLineNumber !== i.range.endLineNumber
)?.range;
}
const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions.independentColorPoolPerBracketType;
const colorProvider = new BracketPairGuidesClassNames();
for (const pair of bracketPairs) {
/*
{
|
}
{
|
----}
____{
|test
----}
renderHorizontalEndLineAtTheBottom:
{
|
|x}
--
renderHorizontalEndLineAtTheBottom:
____{
|test
| x }
----
*/
if (!pair.closingBracketRange) {
continue;
}
const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange);
if (!isActive && !options.includeInactive) {
continue;
}
const className =
colorProvider.getInlineClassName(pair.nestingLevel, pair.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) +
(options.highlightActive && isActive
? ' ' + colorProvider.activeClassName
: '');
const start = pair.openingBracketRange.getStartPosition();
const end = pair.closingBracketRange.getStartPosition();
const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive);
if (pair.range.startLineNumber === pair.range.endLineNumber) {
if (includeSingleLinePairs && horizontalGuides) {
result[pair.range.startLineNumber - startLineNumber].push(
new IndentGuide(
-1,
pair.openingBracketRange.getEndPosition().column,
className,
new IndentGuideHorizontalLine(false, end.column),
-1,
-1,
)
);
}
continue;
}
const endVisibleColumn = this.getVisibleColumnFromPosition(end);
const startVisibleColumn = this.getVisibleColumnFromPosition(
pair.openingBracketRange.getStartPosition()
);
const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1);
let renderHorizontalEndLineAtTheBottom = false;
const firstNonWsIndex = strings.firstNonWhitespaceIndex(
this.textModel.getLineContent(
pair.closingBracketRange.startLineNumber
)
);
const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1;
if (hasTextBeforeClosingBracket) {
renderHorizontalEndLineAtTheBottom = true;
}
const visibleGuideStartLineNumber = Math.max(start.lineNumber, startLineNumber);
const visibleGuideEndLineNumber = Math.min(end.lineNumber, endLineNumber);
const offset = renderHorizontalEndLineAtTheBottom ? 1 : 0;
for (let l = visibleGuideStartLineNumber; l < visibleGuideEndLineNumber + offset; l++) {
result[l - startLineNumber].push(
new IndentGuide(
guideVisibleColumn,
-1,
className,
null,
l === start.lineNumber ? start.column : -1,
l === end.lineNumber ? end.column : -1
)
);
}
if (horizontalGuides) {
if (start.lineNumber >= startLineNumber && startVisibleColumn > guideVisibleColumn) {
result[start.lineNumber - startLineNumber].push(
new IndentGuide(
guideVisibleColumn,
-1,
className,
new IndentGuideHorizontalLine(false, start.column),
-1,
-1,
)
);
}
if (end.lineNumber <= endLineNumber && endVisibleColumn > guideVisibleColumn) {
result[end.lineNumber - startLineNumber].push(
new IndentGuide(
guideVisibleColumn,
-1,
className,
new IndentGuideHorizontalLine(!renderHorizontalEndLineAtTheBottom, end.column),
-1,
-1,
)
);
}
}
}
for (const guides of result) {
guides.sort((a, b) => a.visibleColumn - b.visibleColumn);
}
return result;
}
private getVisibleColumnFromPosition(position: Position): number {
return (
CursorColumns.visibleColumnFromColumn(
this.textModel.getLineContent(position.lineNumber),
position.column,
this.textModel.getOptions().tabSize
) + 1
);
}
public getLinesIndentGuides(
endLineNumber: number
): number[] {
this.assertNotDisposed();
const lineCount = this.textModel.getLineCount();
if (startLineNumber < 1 || startLineNumber > lineCount) {
throw new Error('Illegal value for startLineNumber');
}
throw new Error('Illegal value for endLineNumber');
}
const options = this.textModel.getOptions();
const foldingRules = this.getLanguageConfiguration(
this.textModel.getLanguageId()
).foldingRules;
const offSide = Boolean(foldingRules && foldingRules.offSide);
const result: number[] = new Array<number>(
endLineNumber - startLineNumber + 1
);
let aboveContentLineIndex =
-2; /* -2 is a marker for not having computed it */
let aboveContentLineIndent = -1;
let belowContentLineIndex =
-2; /* -2 is a marker for not having computed it */
let belowContentLineIndent = -1;
for (
let lineNumber = startLineNumber;
lineNumber <= endLineNumber;
lineNumber++
) {
const resultIndex = lineNumber - startLineNumber;
const currentIndent = this._computeIndentLevel(lineNumber - 1);
if (currentIndent >= 0) {
// This line has content (besides whitespace)
// Use the line's indent
aboveContentLineIndex = lineNumber - 1;
aboveContentLineIndent = currentIndent;
result[resultIndex] = Math.ceil(currentIndent / options.indentSize);
continue;
}
if (aboveContentLineIndex === -2) {
aboveContentLineIndex = -1;
aboveContentLineIndent = -1;
// must find previous line with content
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
aboveContentLineIndex = lineIndex;
aboveContentLineIndent = indent;
break;
}
}
}
if (
belowContentLineIndex !== -1 &&
(belowContentLineIndex === -2 || belowContentLineIndex < lineNumber - 1)
belowContentLineIndent = -1;
// must find next line with content
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
belowContentLineIndex = lineIndex;
belowContentLineIndent = indent;
break;
}
}
}
result[resultIndex] = this._getIndentLevelForWhitespaceLine(
offSide,
aboveContentLineIndent,
belowContentLineIndent
);
}
}
private _getIndentLevelForWhitespaceLine(
aboveContentLineIndent: number,
belowContentLineIndent: number
): number {
const options = this.textModel.getOptions();
if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) {
// At the top or bottom of the file
return 0;
return 1 + Math.floor(aboveContentLineIndent / options.indentSize);
return Math.ceil(belowContentLineIndent / options.indentSize);
// same level as region below
return Math.ceil(belowContentLineIndent / options.indentSize);
// we are inside the region that ends below
return 1 + Math.floor(belowContentLineIndent / options.indentSize);
}
}
export class BracketPairGuidesClassNames {
public readonly activeClassName = 'indent-active';
getInlineClassName(nestingLevel: number, nestingLevelOfEqualBracketType: number, independentColorPoolPerBracketType: boolean): string {
return this.getInlineClassNameOfLevel(independentColorPoolPerBracketType ? nestingLevelOfEqualBracketType : nestingLevel);
}
getInlineClassNameOfLevel(level: number): string {
// To support a dynamic amount of colors up to 6 colors,
// we use a number that is a lcm of all numbers from 1 to 6.
return `bracket-indent-guide lvl-${level % 30}`;
}