Path: blob/main/src/vs/editor/common/model/guidesTextModelPart.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { findLast } from '../../../base/common/arraysFind.js';6import * as strings from '../../../base/common/strings.js';7import { CursorColumns } from '../core/cursorColumns.js';8import { IPosition, Position } from '../core/position.js';9import { Range } from '../core/range.js';10import type { TextModel } from './textModel.js';11import { TextModelPart } from './textModelPart.js';12import { computeIndentLevel } from './utils.js';13import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from '../languages/languageConfigurationRegistry.js';14import { BracketGuideOptions, HorizontalGuidesState, IActiveIndentGuideInfo, IGuidesTextModelPart, IndentGuide, IndentGuideHorizontalLine } from '../textModelGuides.js';15import { BugIndicatingError } from '../../../base/common/errors.js';1617export class GuidesTextModelPart extends TextModelPart implements IGuidesTextModelPart {18constructor(19private readonly textModel: TextModel,20private readonly languageConfigurationService: ILanguageConfigurationService21) {22super();23}2425private getLanguageConfiguration(26languageId: string27): ResolvedLanguageConfiguration {28return this.languageConfigurationService.getLanguageConfiguration(29languageId30);31}3233private _computeIndentLevel(lineIndex: number): number {34return computeIndentLevel(35this.textModel.getLineContent(lineIndex + 1),36this.textModel.getOptions().tabSize37);38}3940public getActiveIndentGuide(41lineNumber: number,42minLineNumber: number,43maxLineNumber: number44): IActiveIndentGuideInfo {45this.assertNotDisposed();46const lineCount = this.textModel.getLineCount();4748if (lineNumber < 1 || lineNumber > lineCount) {49throw new BugIndicatingError('Illegal value for lineNumber');50}5152const foldingRules = this.getLanguageConfiguration(53this.textModel.getLanguageId()54).foldingRules;55const offSide = Boolean(foldingRules && foldingRules.offSide);5657let up_aboveContentLineIndex =58-2; /* -2 is a marker for not having computed it */59let up_aboveContentLineIndent = -1;60let up_belowContentLineIndex =61-2; /* -2 is a marker for not having computed it */62let up_belowContentLineIndent = -1;63const up_resolveIndents = (lineNumber: number) => {64if (65up_aboveContentLineIndex !== -1 &&66(up_aboveContentLineIndex === -2 ||67up_aboveContentLineIndex > lineNumber - 1)68) {69up_aboveContentLineIndex = -1;70up_aboveContentLineIndent = -1;7172// must find previous line with content73for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {74const indent = this._computeIndentLevel(lineIndex);75if (indent >= 0) {76up_aboveContentLineIndex = lineIndex;77up_aboveContentLineIndent = indent;78break;79}80}81}8283if (up_belowContentLineIndex === -2) {84up_belowContentLineIndex = -1;85up_belowContentLineIndent = -1;8687// must find next line with content88for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {89const indent = this._computeIndentLevel(lineIndex);90if (indent >= 0) {91up_belowContentLineIndex = lineIndex;92up_belowContentLineIndent = indent;93break;94}95}96}97};9899let down_aboveContentLineIndex =100-2; /* -2 is a marker for not having computed it */101let down_aboveContentLineIndent = -1;102let down_belowContentLineIndex =103-2; /* -2 is a marker for not having computed it */104let down_belowContentLineIndent = -1;105const down_resolveIndents = (lineNumber: number) => {106if (down_aboveContentLineIndex === -2) {107down_aboveContentLineIndex = -1;108down_aboveContentLineIndent = -1;109110// must find previous line with content111for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {112const indent = this._computeIndentLevel(lineIndex);113if (indent >= 0) {114down_aboveContentLineIndex = lineIndex;115down_aboveContentLineIndent = indent;116break;117}118}119}120121if (122down_belowContentLineIndex !== -1 &&123(down_belowContentLineIndex === -2 ||124down_belowContentLineIndex < lineNumber - 1)125) {126down_belowContentLineIndex = -1;127down_belowContentLineIndent = -1;128129// must find next line with content130for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {131const indent = this._computeIndentLevel(lineIndex);132if (indent >= 0) {133down_belowContentLineIndex = lineIndex;134down_belowContentLineIndent = indent;135break;136}137}138}139};140141let startLineNumber = 0;142let goUp = true;143let endLineNumber = 0;144let goDown = true;145let indent = 0;146147let initialIndent = 0;148149for (let distance = 0; goUp || goDown; distance++) {150const upLineNumber = lineNumber - distance;151const downLineNumber = lineNumber + distance;152153if (distance > 1 && (upLineNumber < 1 || upLineNumber < minLineNumber)) {154goUp = false;155}156if (157distance > 1 &&158(downLineNumber > lineCount || downLineNumber > maxLineNumber)159) {160goDown = false;161}162if (distance > 50000) {163// stop processing164goUp = false;165goDown = false;166}167168let upLineIndentLevel: number = -1;169if (goUp && upLineNumber >= 1) {170// compute indent level going up171const currentIndent = this._computeIndentLevel(upLineNumber - 1);172if (currentIndent >= 0) {173// This line has content (besides whitespace)174// Use the line's indent175up_belowContentLineIndex = upLineNumber - 1;176up_belowContentLineIndent = currentIndent;177upLineIndentLevel = Math.ceil(178currentIndent / this.textModel.getOptions().indentSize179);180} else {181up_resolveIndents(upLineNumber);182upLineIndentLevel = this._getIndentLevelForWhitespaceLine(183offSide,184up_aboveContentLineIndent,185up_belowContentLineIndent186);187}188}189190let downLineIndentLevel = -1;191if (goDown && downLineNumber <= lineCount) {192// compute indent level going down193const currentIndent = this._computeIndentLevel(downLineNumber - 1);194if (currentIndent >= 0) {195// This line has content (besides whitespace)196// Use the line's indent197down_aboveContentLineIndex = downLineNumber - 1;198down_aboveContentLineIndent = currentIndent;199downLineIndentLevel = Math.ceil(200currentIndent / this.textModel.getOptions().indentSize201);202} else {203down_resolveIndents(downLineNumber);204downLineIndentLevel = this._getIndentLevelForWhitespaceLine(205offSide,206down_aboveContentLineIndent,207down_belowContentLineIndent208);209}210}211212if (distance === 0) {213initialIndent = upLineIndentLevel;214continue;215}216217if (distance === 1) {218if (219downLineNumber <= lineCount &&220downLineIndentLevel >= 0 &&221initialIndent + 1 === downLineIndentLevel222) {223// This is the beginning of a scope, we have special handling here, since we want the224// child scope indent to be active, not the parent scope225goUp = false;226startLineNumber = downLineNumber;227endLineNumber = downLineNumber;228indent = downLineIndentLevel;229continue;230}231232if (233upLineNumber >= 1 &&234upLineIndentLevel >= 0 &&235upLineIndentLevel - 1 === initialIndent236) {237// This is the end of a scope, just like above238goDown = false;239startLineNumber = upLineNumber;240endLineNumber = upLineNumber;241indent = upLineIndentLevel;242continue;243}244245startLineNumber = lineNumber;246endLineNumber = lineNumber;247indent = initialIndent;248if (indent === 0) {249// No need to continue250return { startLineNumber, endLineNumber, indent };251}252}253254if (goUp) {255if (upLineIndentLevel >= indent) {256startLineNumber = upLineNumber;257} else {258goUp = false;259}260}261if (goDown) {262if (downLineIndentLevel >= indent) {263endLineNumber = downLineNumber;264} else {265goDown = false;266}267}268}269270return { startLineNumber, endLineNumber, indent };271}272273public getLinesBracketGuides(274startLineNumber: number,275endLineNumber: number,276activePosition: IPosition | null,277options: BracketGuideOptions278): IndentGuide[][] {279const result: IndentGuide[][] = [];280for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {281result.push([]);282}283284// If requested, this could be made configurable.285const includeSingleLinePairs = true;286287const bracketPairs =288this.textModel.bracketPairs.getBracketPairsInRangeWithMinIndentation(289new Range(290startLineNumber,2911,292endLineNumber,293this.textModel.getLineMaxColumn(endLineNumber)294)295).toArray();296297let activeBracketPairRange: Range | undefined = undefined;298if (activePosition && bracketPairs.length > 0) {299const bracketsContainingActivePosition = (300startLineNumber <= activePosition.lineNumber &&301activePosition.lineNumber <= endLineNumber302// We don't need to query the brackets again if the cursor is in the viewport303? bracketPairs304: this.textModel.bracketPairs.getBracketPairsInRange(305Range.fromPositions(activePosition)306).toArray()307).filter((bp) => Range.strictContainsPosition(bp.range, activePosition));308309activeBracketPairRange = findLast(310bracketsContainingActivePosition,311(i) => includeSingleLinePairs || i.range.startLineNumber !== i.range.endLineNumber312)?.range;313}314315const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions.independentColorPoolPerBracketType;316const colorProvider = new BracketPairGuidesClassNames();317318for (const pair of bracketPairs) {319/*320321322{323|324}325326{327|328----}329330____{331|test332----}333334renderHorizontalEndLineAtTheBottom:335{336|337|x}338--339renderHorizontalEndLineAtTheBottom:340____{341|test342| x }343----344*/345346if (!pair.closingBracketRange) {347continue;348}349350const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange);351352if (!isActive && !options.includeInactive) {353continue;354}355356const className =357colorProvider.getInlineClassName(pair.nestingLevel, pair.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) +358(options.highlightActive && isActive359? ' ' + colorProvider.activeClassName360: '');361362363const start = pair.openingBracketRange.getStartPosition();364const end = pair.closingBracketRange.getStartPosition();365366const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive);367368if (pair.range.startLineNumber === pair.range.endLineNumber) {369if (includeSingleLinePairs && horizontalGuides) {370371result[pair.range.startLineNumber - startLineNumber].push(372new IndentGuide(373-1,374pair.openingBracketRange.getEndPosition().column,375className,376new IndentGuideHorizontalLine(false, end.column),377-1,378-1,379)380);381382}383continue;384}385386const endVisibleColumn = this.getVisibleColumnFromPosition(end);387const startVisibleColumn = this.getVisibleColumnFromPosition(388pair.openingBracketRange.getStartPosition()389);390const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1);391392let renderHorizontalEndLineAtTheBottom = false;393394395const firstNonWsIndex = strings.firstNonWhitespaceIndex(396this.textModel.getLineContent(397pair.closingBracketRange.startLineNumber398)399);400const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1;401if (hasTextBeforeClosingBracket) {402renderHorizontalEndLineAtTheBottom = true;403}404405406const visibleGuideStartLineNumber = Math.max(start.lineNumber, startLineNumber);407const visibleGuideEndLineNumber = Math.min(end.lineNumber, endLineNumber);408409const offset = renderHorizontalEndLineAtTheBottom ? 1 : 0;410411for (let l = visibleGuideStartLineNumber; l < visibleGuideEndLineNumber + offset; l++) {412result[l - startLineNumber].push(413new IndentGuide(414guideVisibleColumn,415-1,416className,417null,418l === start.lineNumber ? start.column : -1,419l === end.lineNumber ? end.column : -1420)421);422}423424if (horizontalGuides) {425if (start.lineNumber >= startLineNumber && startVisibleColumn > guideVisibleColumn) {426result[start.lineNumber - startLineNumber].push(427new IndentGuide(428guideVisibleColumn,429-1,430className,431new IndentGuideHorizontalLine(false, start.column),432-1,433-1,434)435);436}437438if (end.lineNumber <= endLineNumber && endVisibleColumn > guideVisibleColumn) {439result[end.lineNumber - startLineNumber].push(440new IndentGuide(441guideVisibleColumn,442-1,443className,444new IndentGuideHorizontalLine(!renderHorizontalEndLineAtTheBottom, end.column),445-1,446-1,447)448);449}450}451}452453for (const guides of result) {454guides.sort((a, b) => a.visibleColumn - b.visibleColumn);455}456457return result;458}459460private getVisibleColumnFromPosition(position: Position): number {461return (462CursorColumns.visibleColumnFromColumn(463this.textModel.getLineContent(position.lineNumber),464position.column,465this.textModel.getOptions().tabSize466) + 1467);468}469470public getLinesIndentGuides(471startLineNumber: number,472endLineNumber: number473): number[] {474this.assertNotDisposed();475const lineCount = this.textModel.getLineCount();476477if (startLineNumber < 1 || startLineNumber > lineCount) {478throw new Error('Illegal value for startLineNumber');479}480if (endLineNumber < 1 || endLineNumber > lineCount) {481throw new Error('Illegal value for endLineNumber');482}483484const options = this.textModel.getOptions();485const foldingRules = this.getLanguageConfiguration(486this.textModel.getLanguageId()487).foldingRules;488const offSide = Boolean(foldingRules && foldingRules.offSide);489490const result: number[] = new Array<number>(491endLineNumber - startLineNumber + 1492);493494let aboveContentLineIndex =495-2; /* -2 is a marker for not having computed it */496let aboveContentLineIndent = -1;497498let belowContentLineIndex =499-2; /* -2 is a marker for not having computed it */500let belowContentLineIndent = -1;501502for (503let lineNumber = startLineNumber;504lineNumber <= endLineNumber;505lineNumber++506) {507const resultIndex = lineNumber - startLineNumber;508509const currentIndent = this._computeIndentLevel(lineNumber - 1);510if (currentIndent >= 0) {511// This line has content (besides whitespace)512// Use the line's indent513aboveContentLineIndex = lineNumber - 1;514aboveContentLineIndent = currentIndent;515result[resultIndex] = Math.ceil(currentIndent / options.indentSize);516continue;517}518519if (aboveContentLineIndex === -2) {520aboveContentLineIndex = -1;521aboveContentLineIndent = -1;522523// must find previous line with content524for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {525const indent = this._computeIndentLevel(lineIndex);526if (indent >= 0) {527aboveContentLineIndex = lineIndex;528aboveContentLineIndent = indent;529break;530}531}532}533534if (535belowContentLineIndex !== -1 &&536(belowContentLineIndex === -2 || belowContentLineIndex < lineNumber - 1)537) {538belowContentLineIndex = -1;539belowContentLineIndent = -1;540541// must find next line with content542for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {543const indent = this._computeIndentLevel(lineIndex);544if (indent >= 0) {545belowContentLineIndex = lineIndex;546belowContentLineIndent = indent;547break;548}549}550}551552result[resultIndex] = this._getIndentLevelForWhitespaceLine(553offSide,554aboveContentLineIndent,555belowContentLineIndent556);557}558return result;559}560561private _getIndentLevelForWhitespaceLine(562offSide: boolean,563aboveContentLineIndent: number,564belowContentLineIndent: number565): number {566const options = this.textModel.getOptions();567568if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) {569// At the top or bottom of the file570return 0;571} else if (aboveContentLineIndent < belowContentLineIndent) {572// we are inside the region above573return 1 + Math.floor(aboveContentLineIndent / options.indentSize);574} else if (aboveContentLineIndent === belowContentLineIndent) {575// we are in between two regions576return Math.ceil(belowContentLineIndent / options.indentSize);577} else {578if (offSide) {579// same level as region below580return Math.ceil(belowContentLineIndent / options.indentSize);581} else {582// we are inside the region that ends below583return 1 + Math.floor(belowContentLineIndent / options.indentSize);584}585}586}587}588589export class BracketPairGuidesClassNames {590public readonly activeClassName = 'indent-active';591592getInlineClassName(nestingLevel: number, nestingLevelOfEqualBracketType: number, independentColorPoolPerBracketType: boolean): string {593return this.getInlineClassNameOfLevel(independentColorPoolPerBracketType ? nestingLevelOfEqualBracketType : nestingLevel);594}595596getInlineClassNameOfLevel(level: number): string {597// To support a dynamic amount of colors up to 6 colors,598// we use a number that is a lcm of all numbers from 1 to 6.599return `bracket-indent-guide lvl-${level % 30}`;600}601}602603604