Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts
3296 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 './indentGuides.css';
7
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
8
import { 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';
9
import { RenderingContext } from '../../view/renderingContext.js';
10
import { ViewContext } from '../../../common/viewModel/viewContext.js';
11
import * as viewEvents from '../../../common/viewEvents.js';
12
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
13
import { EditorOption, InternalGuidesOptions } from '../../../common/config/editorOptions.js';
14
import { Position } from '../../../common/core/position.js';
15
import { ArrayQueue } from '../../../../base/common/arrays.js';
16
import { Color } from '../../../../base/common/color.js';
17
import { isDefined } from '../../../../base/common/types.js';
18
import { BracketPairGuidesClassNames } from '../../../common/model/guidesTextModelPart.js';
19
import { IndentGuide, HorizontalGuidesState } from '../../../common/textModelGuides.js';
20
21
/**
22
* Indent guides are vertical lines that help identify the indentation level of
23
* the code.
24
*/
25
export class IndentGuidesOverlay extends DynamicViewOverlay {
26
27
private readonly _context: ViewContext;
28
private _primaryPosition: Position | null;
29
private _spaceWidth: number;
30
private _renderResult: string[] | null;
31
private _maxIndentLeft: number;
32
private _bracketPairGuideOptions: InternalGuidesOptions;
33
34
constructor(context: ViewContext) {
35
super();
36
this._context = context;
37
this._primaryPosition = null;
38
39
const options = this._context.configuration.options;
40
const wrappingInfo = options.get(EditorOption.wrappingInfo);
41
const fontInfo = options.get(EditorOption.fontInfo);
42
43
this._spaceWidth = fontInfo.spaceWidth;
44
this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
45
this._bracketPairGuideOptions = options.get(EditorOption.guides);
46
47
this._renderResult = null;
48
49
this._context.addEventHandler(this);
50
}
51
52
public override dispose(): void {
53
this._context.removeEventHandler(this);
54
this._renderResult = null;
55
super.dispose();
56
}
57
58
// --- begin event handlers
59
60
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
61
const options = this._context.configuration.options;
62
const wrappingInfo = options.get(EditorOption.wrappingInfo);
63
const fontInfo = options.get(EditorOption.fontInfo);
64
65
this._spaceWidth = fontInfo.spaceWidth;
66
this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
67
this._bracketPairGuideOptions = options.get(EditorOption.guides);
68
69
return true;
70
}
71
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
72
const selection = e.selections[0];
73
const newPosition = selection.getPosition();
74
if (!this._primaryPosition?.equals(newPosition)) {
75
this._primaryPosition = newPosition;
76
return true;
77
}
78
79
return false;
80
}
81
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
82
// true for inline decorations
83
return true;
84
}
85
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
86
return true;
87
}
88
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
89
return true;
90
}
91
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
92
return true;
93
}
94
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
95
return true;
96
}
97
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
98
return e.scrollTopChanged;// || e.scrollWidthChanged;
99
}
100
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
101
return true;
102
}
103
public override onLanguageConfigurationChanged(e: viewEvents.ViewLanguageConfigurationEvent): boolean {
104
return true;
105
}
106
107
// --- end event handlers
108
109
public prepareRender(ctx: RenderingContext): void {
110
if (!this._bracketPairGuideOptions.indentation && this._bracketPairGuideOptions.bracketPairs === false) {
111
this._renderResult = null;
112
return;
113
}
114
115
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
116
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
117
const scrollWidth = ctx.scrollWidth;
118
119
const activeCursorPosition = this._primaryPosition;
120
121
const indents = this.getGuidesByLine(
122
visibleStartLineNumber,
123
Math.min(visibleEndLineNumber + 1, this._context.viewModel.getLineCount()),
124
activeCursorPosition
125
);
126
127
const output: string[] = [];
128
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
129
const lineIndex = lineNumber - visibleStartLineNumber;
130
const indent = indents[lineIndex];
131
let result = '';
132
const leftOffset = ctx.visibleRangeForPosition(new Position(lineNumber, 1))?.left ?? 0;
133
for (const guide of indent) {
134
const left =
135
guide.column === -1
136
? leftOffset + (guide.visibleColumn - 1) * this._spaceWidth
137
: ctx.visibleRangeForPosition(
138
new Position(lineNumber, guide.column)
139
)!.left;
140
141
if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) {
142
break;
143
}
144
145
const className = guide.horizontalLine ? (guide.horizontalLine.top ? 'horizontal-top' : 'horizontal-bottom') : 'vertical';
146
147
const width = guide.horizontalLine
148
? (ctx.visibleRangeForPosition(
149
new Position(lineNumber, guide.horizontalLine.endColumn)
150
)?.left ?? (left + this._spaceWidth)) - left
151
: this._spaceWidth;
152
153
result += `<div class="core-guide ${guide.className} ${className}" style="left:${left}px;width:${width}px"></div>`;
154
}
155
output[lineIndex] = result;
156
}
157
this._renderResult = output;
158
}
159
160
private getGuidesByLine(
161
visibleStartLineNumber: number,
162
visibleEndLineNumber: number,
163
activeCursorPosition: Position | null
164
): IndentGuide[][] {
165
const bracketGuides = this._bracketPairGuideOptions.bracketPairs !== false
166
? this._context.viewModel.getBracketGuidesInRangeByLine(
167
visibleStartLineNumber,
168
visibleEndLineNumber,
169
activeCursorPosition,
170
{
171
highlightActive: this._bracketPairGuideOptions.highlightActiveBracketPair,
172
horizontalGuides: this._bracketPairGuideOptions.bracketPairsHorizontal === true
173
? HorizontalGuidesState.Enabled
174
: this._bracketPairGuideOptions.bracketPairsHorizontal === 'active'
175
? HorizontalGuidesState.EnabledForActive
176
: HorizontalGuidesState.Disabled,
177
includeInactive: this._bracketPairGuideOptions.bracketPairs === true,
178
}
179
)
180
: null;
181
182
const indentGuides = this._bracketPairGuideOptions.indentation
183
? this._context.viewModel.getLinesIndentGuides(
184
visibleStartLineNumber,
185
visibleEndLineNumber
186
)
187
: null;
188
189
let activeIndentStartLineNumber = 0;
190
let activeIndentEndLineNumber = 0;
191
let activeIndentLevel = 0;
192
193
if (this._bracketPairGuideOptions.highlightActiveIndentation !== false && activeCursorPosition) {
194
const activeIndentInfo = this._context.viewModel.getActiveIndentGuide(activeCursorPosition.lineNumber, visibleStartLineNumber, visibleEndLineNumber);
195
activeIndentStartLineNumber = activeIndentInfo.startLineNumber;
196
activeIndentEndLineNumber = activeIndentInfo.endLineNumber;
197
activeIndentLevel = activeIndentInfo.indent;
198
}
199
200
const { indentSize } = this._context.viewModel.model.getOptions();
201
202
const result: IndentGuide[][] = [];
203
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
204
const lineGuides = new Array<IndentGuide>();
205
result.push(lineGuides);
206
207
const bracketGuidesInLine = bracketGuides ? bracketGuides[lineNumber - visibleStartLineNumber] : [];
208
const bracketGuidesInLineQueue = new ArrayQueue(bracketGuidesInLine);
209
210
const indentGuidesInLine = indentGuides ? indentGuides[lineNumber - visibleStartLineNumber] : 0;
211
212
for (let indentLvl = 1; indentLvl <= indentGuidesInLine; indentLvl++) {
213
const indentGuide = (indentLvl - 1) * indentSize + 1;
214
const isActive =
215
// Disable active indent guide if there are bracket guides.
216
(this._bracketPairGuideOptions.highlightActiveIndentation === 'always' || bracketGuidesInLine.length === 0) &&
217
activeIndentStartLineNumber <= lineNumber &&
218
lineNumber <= activeIndentEndLineNumber &&
219
indentLvl === activeIndentLevel;
220
lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => g.visibleColumn < indentGuide) || []);
221
const peeked = bracketGuidesInLineQueue.peek();
222
if (!peeked || peeked.visibleColumn !== indentGuide || peeked.horizontalLine) {
223
lineGuides.push(
224
new IndentGuide(
225
indentGuide,
226
-1,
227
`core-guide-indent lvl-${(indentLvl - 1) % 30}` + (isActive ? ' indent-active' : ''),
228
null,
229
-1,
230
-1,
231
)
232
);
233
}
234
}
235
236
lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => true) || []);
237
}
238
239
return result;
240
}
241
242
public render(startLineNumber: number, lineNumber: number): string {
243
if (!this._renderResult) {
244
return '';
245
}
246
const lineIndex = lineNumber - startLineNumber;
247
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
248
return '';
249
}
250
return this._renderResult[lineIndex];
251
}
252
}
253
254
function transparentToUndefined(color: Color | undefined): Color | undefined {
255
if (color && color.isTransparent()) {
256
return undefined;
257
}
258
return color;
259
}
260
261
registerThemingParticipant((theme, collector) => {
262
263
const colors = [
264
{ bracketColor: editorBracketHighlightingForeground1, guideColor: editorBracketPairGuideBackground1, guideColorActive: editorBracketPairGuideActiveBackground1 },
265
{ bracketColor: editorBracketHighlightingForeground2, guideColor: editorBracketPairGuideBackground2, guideColorActive: editorBracketPairGuideActiveBackground2 },
266
{ bracketColor: editorBracketHighlightingForeground3, guideColor: editorBracketPairGuideBackground3, guideColorActive: editorBracketPairGuideActiveBackground3 },
267
{ bracketColor: editorBracketHighlightingForeground4, guideColor: editorBracketPairGuideBackground4, guideColorActive: editorBracketPairGuideActiveBackground4 },
268
{ bracketColor: editorBracketHighlightingForeground5, guideColor: editorBracketPairGuideBackground5, guideColorActive: editorBracketPairGuideActiveBackground5 },
269
{ bracketColor: editorBracketHighlightingForeground6, guideColor: editorBracketPairGuideBackground6, guideColorActive: editorBracketPairGuideActiveBackground6 }
270
];
271
const colorProvider = new BracketPairGuidesClassNames();
272
273
const indentColors = [
274
{ indentColor: editorIndentGuide1, indentColorActive: editorActiveIndentGuide1 },
275
{ indentColor: editorIndentGuide2, indentColorActive: editorActiveIndentGuide2 },
276
{ indentColor: editorIndentGuide3, indentColorActive: editorActiveIndentGuide3 },
277
{ indentColor: editorIndentGuide4, indentColorActive: editorActiveIndentGuide4 },
278
{ indentColor: editorIndentGuide5, indentColorActive: editorActiveIndentGuide5 },
279
{ indentColor: editorIndentGuide6, indentColorActive: editorActiveIndentGuide6 },
280
];
281
282
const colorValues = colors
283
.map(c => {
284
const bracketColor = theme.getColor(c.bracketColor);
285
const guideColor = theme.getColor(c.guideColor);
286
const guideColorActive = theme.getColor(c.guideColorActive);
287
288
const effectiveGuideColor = transparentToUndefined(transparentToUndefined(guideColor) ?? bracketColor?.transparent(0.3));
289
const effectiveGuideColorActive = transparentToUndefined(transparentToUndefined(guideColorActive) ?? bracketColor);
290
291
if (!effectiveGuideColor || !effectiveGuideColorActive) {
292
return undefined;
293
}
294
295
return {
296
guideColor: effectiveGuideColor,
297
guideColorActive: effectiveGuideColorActive,
298
};
299
})
300
.filter(isDefined);
301
302
const indentColorValues = indentColors
303
.map(c => {
304
const indentColor = theme.getColor(c.indentColor);
305
const indentColorActive = theme.getColor(c.indentColorActive);
306
307
const effectiveIndentColor = transparentToUndefined(indentColor);
308
const effectiveIndentColorActive = transparentToUndefined(indentColorActive);
309
310
if (!effectiveIndentColor || !effectiveIndentColorActive) {
311
return undefined;
312
}
313
314
return {
315
indentColor: effectiveIndentColor,
316
indentColorActive: effectiveIndentColorActive,
317
};
318
})
319
.filter(isDefined);
320
321
if (colorValues.length > 0) {
322
for (let level = 0; level < 30; level++) {
323
const colors = colorValues[level % colorValues.length];
324
collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')} { --guide-color: ${colors.guideColor}; --guide-color-active: ${colors.guideColorActive}; }`);
325
}
326
327
collector.addRule(`.monaco-editor .vertical { box-shadow: 1px 0 0 0 var(--guide-color) inset; }`);
328
collector.addRule(`.monaco-editor .horizontal-top { border-top: 1px solid var(--guide-color); }`);
329
collector.addRule(`.monaco-editor .horizontal-bottom { border-bottom: 1px solid var(--guide-color); }`);
330
331
collector.addRule(`.monaco-editor .vertical.${colorProvider.activeClassName} { box-shadow: 1px 0 0 0 var(--guide-color-active) inset; }`);
332
collector.addRule(`.monaco-editor .horizontal-top.${colorProvider.activeClassName} { border-top: 1px solid var(--guide-color-active); }`);
333
collector.addRule(`.monaco-editor .horizontal-bottom.${colorProvider.activeClassName} { border-bottom: 1px solid var(--guide-color-active); }`);
334
}
335
336
if (indentColorValues.length > 0) {
337
for (let level = 0; level < 30; level++) {
338
const colors = indentColorValues[level % indentColorValues.length];
339
collector.addRule(`.monaco-editor .lines-content .core-guide-indent.lvl-${level} { --indent-color: ${colors.indentColor}; --indent-color-active: ${colors.indentColorActive}; }`);
340
}
341
342
collector.addRule(`.monaco-editor .lines-content .core-guide-indent { box-shadow: 1px 0 0 0 var(--indent-color) inset; }`);
343
collector.addRule(`.monaco-editor .lines-content .core-guide-indent.indent-active { box-shadow: 1px 0 0 0 var(--indent-color-active) inset; }`);
344
}
345
});
346
347