Path: blob/main/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts
3296 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 './indentGuides.css';6import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';7import { editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketPairGuideActiveBackground1, editorBracketPairGuideActiveBackground2, editorBracketPairGuideActiveBackground3, editorBracketPairGuideActiveBackground4, editorBracketPairGuideActiveBackground5, editorBracketPairGuideActiveBackground6, editorBracketPairGuideBackground1, editorBracketPairGuideBackground2, editorBracketPairGuideBackground3, editorBracketPairGuideBackground4, editorBracketPairGuideBackground5, editorBracketPairGuideBackground6, editorIndentGuide1, editorIndentGuide2, editorIndentGuide3, editorIndentGuide4, editorIndentGuide5, editorIndentGuide6, editorActiveIndentGuide1, editorActiveIndentGuide2, editorActiveIndentGuide3, editorActiveIndentGuide4, editorActiveIndentGuide5, editorActiveIndentGuide6 } from '../../../common/core/editorColorRegistry.js';8import { RenderingContext } from '../../view/renderingContext.js';9import { ViewContext } from '../../../common/viewModel/viewContext.js';10import * as viewEvents from '../../../common/viewEvents.js';11import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';12import { EditorOption, InternalGuidesOptions } from '../../../common/config/editorOptions.js';13import { Position } from '../../../common/core/position.js';14import { ArrayQueue } from '../../../../base/common/arrays.js';15import { Color } from '../../../../base/common/color.js';16import { isDefined } from '../../../../base/common/types.js';17import { BracketPairGuidesClassNames } from '../../../common/model/guidesTextModelPart.js';18import { IndentGuide, HorizontalGuidesState } from '../../../common/textModelGuides.js';1920/**21* Indent guides are vertical lines that help identify the indentation level of22* the code.23*/24export class IndentGuidesOverlay extends DynamicViewOverlay {2526private readonly _context: ViewContext;27private _primaryPosition: Position | null;28private _spaceWidth: number;29private _renderResult: string[] | null;30private _maxIndentLeft: number;31private _bracketPairGuideOptions: InternalGuidesOptions;3233constructor(context: ViewContext) {34super();35this._context = context;36this._primaryPosition = null;3738const options = this._context.configuration.options;39const wrappingInfo = options.get(EditorOption.wrappingInfo);40const fontInfo = options.get(EditorOption.fontInfo);4142this._spaceWidth = fontInfo.spaceWidth;43this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);44this._bracketPairGuideOptions = options.get(EditorOption.guides);4546this._renderResult = null;4748this._context.addEventHandler(this);49}5051public override dispose(): void {52this._context.removeEventHandler(this);53this._renderResult = null;54super.dispose();55}5657// --- begin event handlers5859public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {60const options = this._context.configuration.options;61const wrappingInfo = options.get(EditorOption.wrappingInfo);62const fontInfo = options.get(EditorOption.fontInfo);6364this._spaceWidth = fontInfo.spaceWidth;65this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);66this._bracketPairGuideOptions = options.get(EditorOption.guides);6768return true;69}70public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {71const selection = e.selections[0];72const newPosition = selection.getPosition();73if (!this._primaryPosition?.equals(newPosition)) {74this._primaryPosition = newPosition;75return true;76}7778return false;79}80public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {81// true for inline decorations82return true;83}84public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {85return true;86}87public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {88return true;89}90public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {91return true;92}93public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {94return true;95}96public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {97return e.scrollTopChanged;// || e.scrollWidthChanged;98}99public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {100return true;101}102public override onLanguageConfigurationChanged(e: viewEvents.ViewLanguageConfigurationEvent): boolean {103return true;104}105106// --- end event handlers107108public prepareRender(ctx: RenderingContext): void {109if (!this._bracketPairGuideOptions.indentation && this._bracketPairGuideOptions.bracketPairs === false) {110this._renderResult = null;111return;112}113114const visibleStartLineNumber = ctx.visibleRange.startLineNumber;115const visibleEndLineNumber = ctx.visibleRange.endLineNumber;116const scrollWidth = ctx.scrollWidth;117118const activeCursorPosition = this._primaryPosition;119120const indents = this.getGuidesByLine(121visibleStartLineNumber,122Math.min(visibleEndLineNumber + 1, this._context.viewModel.getLineCount()),123activeCursorPosition124);125126const output: string[] = [];127for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {128const lineIndex = lineNumber - visibleStartLineNumber;129const indent = indents[lineIndex];130let result = '';131const leftOffset = ctx.visibleRangeForPosition(new Position(lineNumber, 1))?.left ?? 0;132for (const guide of indent) {133const left =134guide.column === -1135? leftOffset + (guide.visibleColumn - 1) * this._spaceWidth136: ctx.visibleRangeForPosition(137new Position(lineNumber, guide.column)138)!.left;139140if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) {141break;142}143144const className = guide.horizontalLine ? (guide.horizontalLine.top ? 'horizontal-top' : 'horizontal-bottom') : 'vertical';145146const width = guide.horizontalLine147? (ctx.visibleRangeForPosition(148new Position(lineNumber, guide.horizontalLine.endColumn)149)?.left ?? (left + this._spaceWidth)) - left150: this._spaceWidth;151152result += `<div class="core-guide ${guide.className} ${className}" style="left:${left}px;width:${width}px"></div>`;153}154output[lineIndex] = result;155}156this._renderResult = output;157}158159private getGuidesByLine(160visibleStartLineNumber: number,161visibleEndLineNumber: number,162activeCursorPosition: Position | null163): IndentGuide[][] {164const bracketGuides = this._bracketPairGuideOptions.bracketPairs !== false165? this._context.viewModel.getBracketGuidesInRangeByLine(166visibleStartLineNumber,167visibleEndLineNumber,168activeCursorPosition,169{170highlightActive: this._bracketPairGuideOptions.highlightActiveBracketPair,171horizontalGuides: this._bracketPairGuideOptions.bracketPairsHorizontal === true172? HorizontalGuidesState.Enabled173: this._bracketPairGuideOptions.bracketPairsHorizontal === 'active'174? HorizontalGuidesState.EnabledForActive175: HorizontalGuidesState.Disabled,176includeInactive: this._bracketPairGuideOptions.bracketPairs === true,177}178)179: null;180181const indentGuides = this._bracketPairGuideOptions.indentation182? this._context.viewModel.getLinesIndentGuides(183visibleStartLineNumber,184visibleEndLineNumber185)186: null;187188let activeIndentStartLineNumber = 0;189let activeIndentEndLineNumber = 0;190let activeIndentLevel = 0;191192if (this._bracketPairGuideOptions.highlightActiveIndentation !== false && activeCursorPosition) {193const activeIndentInfo = this._context.viewModel.getActiveIndentGuide(activeCursorPosition.lineNumber, visibleStartLineNumber, visibleEndLineNumber);194activeIndentStartLineNumber = activeIndentInfo.startLineNumber;195activeIndentEndLineNumber = activeIndentInfo.endLineNumber;196activeIndentLevel = activeIndentInfo.indent;197}198199const { indentSize } = this._context.viewModel.model.getOptions();200201const result: IndentGuide[][] = [];202for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {203const lineGuides = new Array<IndentGuide>();204result.push(lineGuides);205206const bracketGuidesInLine = bracketGuides ? bracketGuides[lineNumber - visibleStartLineNumber] : [];207const bracketGuidesInLineQueue = new ArrayQueue(bracketGuidesInLine);208209const indentGuidesInLine = indentGuides ? indentGuides[lineNumber - visibleStartLineNumber] : 0;210211for (let indentLvl = 1; indentLvl <= indentGuidesInLine; indentLvl++) {212const indentGuide = (indentLvl - 1) * indentSize + 1;213const isActive =214// Disable active indent guide if there are bracket guides.215(this._bracketPairGuideOptions.highlightActiveIndentation === 'always' || bracketGuidesInLine.length === 0) &&216activeIndentStartLineNumber <= lineNumber &&217lineNumber <= activeIndentEndLineNumber &&218indentLvl === activeIndentLevel;219lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => g.visibleColumn < indentGuide) || []);220const peeked = bracketGuidesInLineQueue.peek();221if (!peeked || peeked.visibleColumn !== indentGuide || peeked.horizontalLine) {222lineGuides.push(223new IndentGuide(224indentGuide,225-1,226`core-guide-indent lvl-${(indentLvl - 1) % 30}` + (isActive ? ' indent-active' : ''),227null,228-1,229-1,230)231);232}233}234235lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => true) || []);236}237238return result;239}240241public render(startLineNumber: number, lineNumber: number): string {242if (!this._renderResult) {243return '';244}245const lineIndex = lineNumber - startLineNumber;246if (lineIndex < 0 || lineIndex >= this._renderResult.length) {247return '';248}249return this._renderResult[lineIndex];250}251}252253function transparentToUndefined(color: Color | undefined): Color | undefined {254if (color && color.isTransparent()) {255return undefined;256}257return color;258}259260registerThemingParticipant((theme, collector) => {261262const colors = [263{ bracketColor: editorBracketHighlightingForeground1, guideColor: editorBracketPairGuideBackground1, guideColorActive: editorBracketPairGuideActiveBackground1 },264{ bracketColor: editorBracketHighlightingForeground2, guideColor: editorBracketPairGuideBackground2, guideColorActive: editorBracketPairGuideActiveBackground2 },265{ bracketColor: editorBracketHighlightingForeground3, guideColor: editorBracketPairGuideBackground3, guideColorActive: editorBracketPairGuideActiveBackground3 },266{ bracketColor: editorBracketHighlightingForeground4, guideColor: editorBracketPairGuideBackground4, guideColorActive: editorBracketPairGuideActiveBackground4 },267{ bracketColor: editorBracketHighlightingForeground5, guideColor: editorBracketPairGuideBackground5, guideColorActive: editorBracketPairGuideActiveBackground5 },268{ bracketColor: editorBracketHighlightingForeground6, guideColor: editorBracketPairGuideBackground6, guideColorActive: editorBracketPairGuideActiveBackground6 }269];270const colorProvider = new BracketPairGuidesClassNames();271272const indentColors = [273{ indentColor: editorIndentGuide1, indentColorActive: editorActiveIndentGuide1 },274{ indentColor: editorIndentGuide2, indentColorActive: editorActiveIndentGuide2 },275{ indentColor: editorIndentGuide3, indentColorActive: editorActiveIndentGuide3 },276{ indentColor: editorIndentGuide4, indentColorActive: editorActiveIndentGuide4 },277{ indentColor: editorIndentGuide5, indentColorActive: editorActiveIndentGuide5 },278{ indentColor: editorIndentGuide6, indentColorActive: editorActiveIndentGuide6 },279];280281const colorValues = colors282.map(c => {283const bracketColor = theme.getColor(c.bracketColor);284const guideColor = theme.getColor(c.guideColor);285const guideColorActive = theme.getColor(c.guideColorActive);286287const effectiveGuideColor = transparentToUndefined(transparentToUndefined(guideColor) ?? bracketColor?.transparent(0.3));288const effectiveGuideColorActive = transparentToUndefined(transparentToUndefined(guideColorActive) ?? bracketColor);289290if (!effectiveGuideColor || !effectiveGuideColorActive) {291return undefined;292}293294return {295guideColor: effectiveGuideColor,296guideColorActive: effectiveGuideColorActive,297};298})299.filter(isDefined);300301const indentColorValues = indentColors302.map(c => {303const indentColor = theme.getColor(c.indentColor);304const indentColorActive = theme.getColor(c.indentColorActive);305306const effectiveIndentColor = transparentToUndefined(indentColor);307const effectiveIndentColorActive = transparentToUndefined(indentColorActive);308309if (!effectiveIndentColor || !effectiveIndentColorActive) {310return undefined;311}312313return {314indentColor: effectiveIndentColor,315indentColorActive: effectiveIndentColorActive,316};317})318.filter(isDefined);319320if (colorValues.length > 0) {321for (let level = 0; level < 30; level++) {322const colors = colorValues[level % colorValues.length];323collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')} { --guide-color: ${colors.guideColor}; --guide-color-active: ${colors.guideColorActive}; }`);324}325326collector.addRule(`.monaco-editor .vertical { box-shadow: 1px 0 0 0 var(--guide-color) inset; }`);327collector.addRule(`.monaco-editor .horizontal-top { border-top: 1px solid var(--guide-color); }`);328collector.addRule(`.monaco-editor .horizontal-bottom { border-bottom: 1px solid var(--guide-color); }`);329330collector.addRule(`.monaco-editor .vertical.${colorProvider.activeClassName} { box-shadow: 1px 0 0 0 var(--guide-color-active) inset; }`);331collector.addRule(`.monaco-editor .horizontal-top.${colorProvider.activeClassName} { border-top: 1px solid var(--guide-color-active); }`);332collector.addRule(`.monaco-editor .horizontal-bottom.${colorProvider.activeClassName} { border-bottom: 1px solid var(--guide-color-active); }`);333}334335if (indentColorValues.length > 0) {336for (let level = 0; level < 30; level++) {337const colors = indentColorValues[level % indentColorValues.length];338collector.addRule(`.monaco-editor .lines-content .core-guide-indent.lvl-${level} { --indent-color: ${colors.indentColor}; --indent-color-active: ${colors.indentColorActive}; }`);339}340341collector.addRule(`.monaco-editor .lines-content .core-guide-indent { box-shadow: 1px 0 0 0 var(--indent-color) inset; }`);342collector.addRule(`.monaco-editor .lines-content .core-guide-indent.indent-active { box-shadow: 1px 0 0 0 var(--indent-color-active) inset; }`);343}344});345346347