Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/minimap/minimap.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 './minimap.css';
7
import * as dom from '../../../../base/browser/dom.js';
8
import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';
9
import { GlobalPointerMoveMonitor } from '../../../../base/browser/globalPointerMoveMonitor.js';
10
import { CharCode } from '../../../../base/common/charCode.js';
11
import { IDisposable, Disposable } from '../../../../base/common/lifecycle.js';
12
import * as platform from '../../../../base/common/platform.js';
13
import * as strings from '../../../../base/common/strings.js';
14
import { ILine, RenderedLinesCollection } from '../../view/viewLayer.js';
15
import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js';
16
import { RenderMinimap, EditorOption, MINIMAP_GUTTER_WIDTH, EditorLayoutInfoComputer } from '../../../common/config/editorOptions.js';
17
import { Range } from '../../../common/core/range.js';
18
import { RGBA8 } from '../../../common/core/misc/rgba.js';
19
import { ScrollType } from '../../../common/editorCommon.js';
20
import { IEditorConfiguration } from '../../../common/config/editorConfiguration.js';
21
import { ColorId } from '../../../common/encodedTokenAttributes.js';
22
import { MinimapCharRenderer } from './minimapCharRenderer.js';
23
import { Constants } from './minimapCharSheet.js';
24
import { MinimapTokensColorTracker } from '../../../common/viewModel/minimapTokensColorTracker.js';
25
import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';
26
import { ViewContext } from '../../../common/viewModel/viewContext.js';
27
import { EditorTheme } from '../../../common/editorTheme.js';
28
import * as viewEvents from '../../../common/viewEvents.js';
29
import { ViewLineData } from '../../../common/viewModel.js';
30
import { minimapSelection, minimapBackground, minimapForegroundOpacity, editorForeground } from '../../../../platform/theme/common/colorRegistry.js';
31
import { ModelDecorationMinimapOptions } from '../../../common/model/textModel.js';
32
import { Selection } from '../../../common/core/selection.js';
33
import { Color } from '../../../../base/common/color.js';
34
import { GestureEvent, EventType, Gesture } from '../../../../base/browser/touch.js';
35
import { MinimapCharRendererFactory } from './minimapCharRendererFactory.js';
36
import { MinimapPosition, MinimapSectionHeaderStyle, TextModelResolvedOptions } from '../../../common/model.js';
37
import { createSingleCallFunction } from '../../../../base/common/functional.js';
38
import { LRUCache } from '../../../../base/common/map.js';
39
import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js';
40
import { ViewModelDecoration } from '../../../common/viewModel/viewModelDecoration.js';
41
import { RunOnceScheduler } from '../../../../base/common/async.js';
42
43
/**
44
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
45
*/
46
const POINTER_DRAG_RESET_DISTANCE = 140;
47
48
const GUTTER_DECORATION_WIDTH = 2;
49
50
class MinimapOptions {
51
52
public readonly renderMinimap: RenderMinimap;
53
public readonly size: 'proportional' | 'fill' | 'fit';
54
public readonly minimapHeightIsEditorHeight: boolean;
55
public readonly scrollBeyondLastLine: boolean;
56
public readonly paddingTop: number;
57
public readonly paddingBottom: number;
58
public readonly showSlider: 'always' | 'mouseover';
59
public readonly autohide: 'none' | 'mouseover' | 'scroll';
60
public readonly pixelRatio: number;
61
public readonly typicalHalfwidthCharacterWidth: number;
62
public readonly lineHeight: number;
63
/**
64
* container dom node left position (in CSS px)
65
*/
66
public readonly minimapLeft: number;
67
/**
68
* container dom node width (in CSS px)
69
*/
70
public readonly minimapWidth: number;
71
/**
72
* container dom node height (in CSS px)
73
*/
74
public readonly minimapHeight: number;
75
/**
76
* canvas backing store width (in device px)
77
*/
78
public readonly canvasInnerWidth: number;
79
/**
80
* canvas backing store height (in device px)
81
*/
82
public readonly canvasInnerHeight: number;
83
/**
84
* canvas width (in CSS px)
85
*/
86
public readonly canvasOuterWidth: number;
87
/**
88
* canvas height (in CSS px)
89
*/
90
public readonly canvasOuterHeight: number;
91
92
public readonly isSampling: boolean;
93
public readonly editorHeight: number;
94
public readonly fontScale: number;
95
public readonly minimapLineHeight: number;
96
public readonly minimapCharWidth: number;
97
public readonly sectionHeaderFontFamily: string;
98
public readonly sectionHeaderFontSize: number;
99
/**
100
* Space in between the characters of the section header (in CSS px)
101
*/
102
public readonly sectionHeaderLetterSpacing: number;
103
public readonly sectionHeaderFontColor: RGBA8;
104
105
public readonly charRenderer: () => MinimapCharRenderer;
106
public readonly defaultBackgroundColor: RGBA8;
107
public readonly backgroundColor: RGBA8;
108
/**
109
* foreground alpha: integer in [0-255]
110
*/
111
public readonly foregroundAlpha: number;
112
113
constructor(configuration: IEditorConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) {
114
const options = configuration.options;
115
const pixelRatio = options.get(EditorOption.pixelRatio);
116
const layoutInfo = options.get(EditorOption.layoutInfo);
117
const minimapLayout = layoutInfo.minimap;
118
const fontInfo = options.get(EditorOption.fontInfo);
119
const minimapOpts = options.get(EditorOption.minimap);
120
121
this.renderMinimap = minimapLayout.renderMinimap;
122
this.size = minimapOpts.size;
123
this.minimapHeightIsEditorHeight = minimapLayout.minimapHeightIsEditorHeight;
124
this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine);
125
this.paddingTop = options.get(EditorOption.padding).top;
126
this.paddingBottom = options.get(EditorOption.padding).bottom;
127
this.showSlider = minimapOpts.showSlider;
128
this.autohide = minimapOpts.autohide;
129
this.pixelRatio = pixelRatio;
130
this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
131
this.lineHeight = options.get(EditorOption.lineHeight);
132
this.minimapLeft = minimapLayout.minimapLeft;
133
this.minimapWidth = minimapLayout.minimapWidth;
134
this.minimapHeight = layoutInfo.height;
135
136
this.canvasInnerWidth = minimapLayout.minimapCanvasInnerWidth;
137
this.canvasInnerHeight = minimapLayout.minimapCanvasInnerHeight;
138
this.canvasOuterWidth = minimapLayout.minimapCanvasOuterWidth;
139
this.canvasOuterHeight = minimapLayout.minimapCanvasOuterHeight;
140
141
this.isSampling = minimapLayout.minimapIsSampling;
142
this.editorHeight = layoutInfo.height;
143
this.fontScale = minimapLayout.minimapScale;
144
this.minimapLineHeight = minimapLayout.minimapLineHeight;
145
this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale;
146
this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY;
147
this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio;
148
this.sectionHeaderLetterSpacing = minimapOpts.sectionHeaderLetterSpacing; // intentionally not multiplying by pixelRatio
149
this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground));
150
151
this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily));
152
this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground);
153
this.backgroundColor = MinimapOptions._getMinimapBackground(theme, this.defaultBackgroundColor);
154
this.foregroundAlpha = MinimapOptions._getMinimapForegroundOpacity(theme);
155
}
156
157
private static _getMinimapBackground(theme: EditorTheme, defaultBackgroundColor: RGBA8): RGBA8 {
158
const themeColor = theme.getColor(minimapBackground);
159
if (themeColor) {
160
return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));
161
}
162
return defaultBackgroundColor;
163
}
164
165
private static _getMinimapForegroundOpacity(theme: EditorTheme): number {
166
const themeColor = theme.getColor(minimapForegroundOpacity);
167
if (themeColor) {
168
return RGBA8._clamp(Math.round(255 * themeColor.rgba.a));
169
}
170
return 255;
171
}
172
173
private static _getSectionHeaderColor(theme: EditorTheme, defaultForegroundColor: RGBA8): RGBA8 {
174
const themeColor = theme.getColor(editorForeground);
175
if (themeColor) {
176
return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));
177
}
178
return defaultForegroundColor;
179
}
180
181
public equals(other: MinimapOptions): boolean {
182
return (this.renderMinimap === other.renderMinimap
183
&& this.size === other.size
184
&& this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight
185
&& this.scrollBeyondLastLine === other.scrollBeyondLastLine
186
&& this.paddingTop === other.paddingTop
187
&& this.paddingBottom === other.paddingBottom
188
&& this.showSlider === other.showSlider
189
&& this.autohide === other.autohide
190
&& this.pixelRatio === other.pixelRatio
191
&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth
192
&& this.lineHeight === other.lineHeight
193
&& this.minimapLeft === other.minimapLeft
194
&& this.minimapWidth === other.minimapWidth
195
&& this.minimapHeight === other.minimapHeight
196
&& this.canvasInnerWidth === other.canvasInnerWidth
197
&& this.canvasInnerHeight === other.canvasInnerHeight
198
&& this.canvasOuterWidth === other.canvasOuterWidth
199
&& this.canvasOuterHeight === other.canvasOuterHeight
200
&& this.isSampling === other.isSampling
201
&& this.editorHeight === other.editorHeight
202
&& this.fontScale === other.fontScale
203
&& this.minimapLineHeight === other.minimapLineHeight
204
&& this.minimapCharWidth === other.minimapCharWidth
205
&& this.sectionHeaderFontSize === other.sectionHeaderFontSize
206
&& this.sectionHeaderLetterSpacing === other.sectionHeaderLetterSpacing
207
&& this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor)
208
&& this.backgroundColor && this.backgroundColor.equals(other.backgroundColor)
209
&& this.foregroundAlpha === other.foregroundAlpha
210
);
211
}
212
}
213
214
class MinimapLayout {
215
216
constructor(
217
/**
218
* The given editor scrollTop (input).
219
*/
220
public readonly scrollTop: number,
221
/**
222
* The given editor scrollHeight (input).
223
*/
224
public readonly scrollHeight: number,
225
public readonly sliderNeeded: boolean,
226
private readonly _computedSliderRatio: number,
227
/**
228
* slider dom node top (in CSS px)
229
*/
230
public readonly sliderTop: number,
231
/**
232
* slider dom node height (in CSS px)
233
*/
234
public readonly sliderHeight: number,
235
/**
236
* empty lines to reserve at the top of the minimap.
237
*/
238
public readonly topPaddingLineCount: number,
239
/**
240
* minimap render start line number.
241
*/
242
public readonly startLineNumber: number,
243
/**
244
* minimap render end line number.
245
*/
246
public readonly endLineNumber: number
247
) { }
248
249
/**
250
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
251
*/
252
public getDesiredScrollTopFromDelta(delta: number): number {
253
return Math.round(this.scrollTop + delta / this._computedSliderRatio);
254
}
255
256
public getDesiredScrollTopFromTouchLocation(pageY: number): number {
257
return Math.round((pageY - this.sliderHeight / 2) / this._computedSliderRatio);
258
}
259
260
/**
261
* Intersect a line range with `this.startLineNumber` and `this.endLineNumber`.
262
*/
263
public intersectWithViewport(range: Range): [number, number] | null {
264
const startLineNumber = Math.max(this.startLineNumber, range.startLineNumber);
265
const endLineNumber = Math.min(this.endLineNumber, range.endLineNumber);
266
if (startLineNumber > endLineNumber) {
267
// entirely outside minimap's viewport
268
return null;
269
}
270
return [startLineNumber, endLineNumber];
271
}
272
273
/**
274
* Get the inner minimap y coordinate for a line number.
275
*/
276
public getYForLineNumber(lineNumber: number, minimapLineHeight: number): number {
277
return + (lineNumber - this.startLineNumber + this.topPaddingLineCount) * minimapLineHeight;
278
}
279
280
public static create(
281
options: MinimapOptions,
282
viewportStartLineNumber: number,
283
viewportEndLineNumber: number,
284
viewportStartLineNumberVerticalOffset: number,
285
viewportHeight: number,
286
viewportContainsWhitespaceGaps: boolean,
287
lineCount: number,
288
realLineCount: number,
289
scrollTop: number,
290
scrollHeight: number,
291
previousLayout: MinimapLayout | null
292
): MinimapLayout {
293
const pixelRatio = options.pixelRatio;
294
const minimapLineHeight = options.minimapLineHeight;
295
const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
296
const lineHeight = options.lineHeight;
297
298
if (options.minimapHeightIsEditorHeight) {
299
let logicalScrollHeight = (
300
realLineCount * options.lineHeight
301
+ options.paddingTop
302
+ options.paddingBottom
303
);
304
if (options.scrollBeyondLastLine) {
305
logicalScrollHeight += Math.max(0, viewportHeight - options.lineHeight - options.paddingBottom);
306
}
307
const sliderHeight = Math.max(1, Math.floor(viewportHeight * viewportHeight / logicalScrollHeight));
308
const maxMinimapSliderTop = Math.max(0, options.minimapHeight - sliderHeight);
309
// The slider can move from 0 to `maxMinimapSliderTop`
310
// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.
311
const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);
312
const sliderTop = (scrollTop * computedSliderRatio);
313
const sliderNeeded = (maxMinimapSliderTop > 0);
314
const maxLinesFitting = Math.floor(options.canvasInnerHeight / options.minimapLineHeight);
315
const topPaddingLineCount = Math.floor(options.paddingTop / options.lineHeight);
316
return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, topPaddingLineCount, 1, Math.min(lineCount, maxLinesFitting));
317
}
318
319
// The visible line count in a viewport can change due to a number of reasons:
320
// a) with the same viewport width, different scroll positions can result in partial lines being visible:
321
// e.g. for a line height of 20, and a viewport height of 600
322
// * scrollTop = 0 => visible lines are [1, 30]
323
// * scrollTop = 10 => visible lines are [1, 31] (with lines 1 and 31 partially visible)
324
// * scrollTop = 20 => visible lines are [2, 31]
325
// b) whitespace gaps might make their way in the viewport (which results in a decrease in the visible line count)
326
// c) we could be in the scroll beyond last line case (which also results in a decrease in the visible line count, down to possibly only one line being visible)
327
328
// We must first establish a desirable slider height.
329
let sliderHeight: number;
330
if (viewportContainsWhitespaceGaps && viewportEndLineNumber !== lineCount) {
331
// case b) from above: there are whitespace gaps in the viewport.
332
// In this case, the height of the slider directly reflects the visible line count.
333
const viewportLineCount = viewportEndLineNumber - viewportStartLineNumber + 1;
334
sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
335
} else {
336
// The slider has a stable height
337
const expectedViewportLineCount = viewportHeight / lineHeight;
338
sliderHeight = Math.floor(expectedViewportLineCount * minimapLineHeight / pixelRatio);
339
}
340
341
const extraLinesAtTheTop = Math.floor(options.paddingTop / lineHeight);
342
let extraLinesAtTheBottom = Math.floor(options.paddingBottom / lineHeight);
343
if (options.scrollBeyondLastLine) {
344
const expectedViewportLineCount = viewportHeight / lineHeight;
345
extraLinesAtTheBottom = Math.max(extraLinesAtTheBottom, expectedViewportLineCount - 1);
346
}
347
348
let maxMinimapSliderTop: number;
349
if (extraLinesAtTheBottom > 0) {
350
const expectedViewportLineCount = viewportHeight / lineHeight;
351
// The minimap slider, when dragged all the way down, will contain the last line at its top
352
maxMinimapSliderTop = (extraLinesAtTheTop + lineCount + extraLinesAtTheBottom - expectedViewportLineCount - 1) * minimapLineHeight / pixelRatio;
353
} else {
354
// The minimap slider, when dragged all the way down, will contain the last line at its bottom
355
maxMinimapSliderTop = Math.max(0, (extraLinesAtTheTop + lineCount) * minimapLineHeight / pixelRatio - sliderHeight);
356
}
357
maxMinimapSliderTop = Math.min(options.minimapHeight - sliderHeight, maxMinimapSliderTop);
358
359
// The slider can move from 0 to `maxMinimapSliderTop`
360
// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.
361
const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);
362
const sliderTop = (scrollTop * computedSliderRatio);
363
364
if (minimapLinesFitting >= extraLinesAtTheTop + lineCount + extraLinesAtTheBottom) {
365
// All lines fit in the minimap
366
const sliderNeeded = (maxMinimapSliderTop > 0);
367
return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, extraLinesAtTheTop, 1, lineCount);
368
} else {
369
let consideringStartLineNumber: number;
370
if (viewportStartLineNumber > 1) {
371
consideringStartLineNumber = viewportStartLineNumber + extraLinesAtTheTop;
372
} else {
373
consideringStartLineNumber = Math.max(1, scrollTop / lineHeight);
374
}
375
376
let topPaddingLineCount: number;
377
let startLineNumber = Math.max(1, Math.floor(consideringStartLineNumber - sliderTop * pixelRatio / minimapLineHeight));
378
if (startLineNumber < extraLinesAtTheTop) {
379
topPaddingLineCount = extraLinesAtTheTop - startLineNumber + 1;
380
startLineNumber = 1;
381
} else {
382
topPaddingLineCount = 0;
383
startLineNumber = Math.max(1, startLineNumber - extraLinesAtTheTop);
384
}
385
386
// Avoid flickering caused by a partial viewport start line
387
// by being consistent w.r.t. the previous layout decision
388
if (previousLayout && previousLayout.scrollHeight === scrollHeight) {
389
if (previousLayout.scrollTop > scrollTop) {
390
// Scrolling up => never increase `startLineNumber`
391
startLineNumber = Math.min(startLineNumber, previousLayout.startLineNumber);
392
topPaddingLineCount = Math.max(topPaddingLineCount, previousLayout.topPaddingLineCount);
393
}
394
if (previousLayout.scrollTop < scrollTop) {
395
// Scrolling down => never decrease `startLineNumber`
396
startLineNumber = Math.max(startLineNumber, previousLayout.startLineNumber);
397
topPaddingLineCount = Math.min(topPaddingLineCount, previousLayout.topPaddingLineCount);
398
}
399
}
400
401
const endLineNumber = Math.min(lineCount, startLineNumber - topPaddingLineCount + minimapLinesFitting - 1);
402
const partialLine = (scrollTop - viewportStartLineNumberVerticalOffset) / lineHeight;
403
404
let sliderTopAligned: number;
405
if (scrollTop >= options.paddingTop) {
406
sliderTopAligned = (viewportStartLineNumber - startLineNumber + topPaddingLineCount + partialLine) * minimapLineHeight / pixelRatio;
407
} else {
408
sliderTopAligned = (scrollTop / options.paddingTop) * (topPaddingLineCount + partialLine) * minimapLineHeight / pixelRatio;
409
}
410
411
return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTopAligned, sliderHeight, topPaddingLineCount, startLineNumber, endLineNumber);
412
}
413
}
414
}
415
416
class MinimapLine implements ILine {
417
418
public static readonly INVALID = new MinimapLine(-1);
419
420
dy: number;
421
422
constructor(dy: number) {
423
this.dy = dy;
424
}
425
426
public onContentChanged(): void {
427
this.dy = -1;
428
}
429
430
public onTokensChanged(): void {
431
this.dy = -1;
432
}
433
}
434
435
class RenderData {
436
/**
437
* last rendered layout.
438
*/
439
public readonly renderedLayout: MinimapLayout;
440
private readonly _imageData: ImageData;
441
private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;
442
443
constructor(
444
renderedLayout: MinimapLayout,
445
imageData: ImageData,
446
lines: MinimapLine[]
447
) {
448
this.renderedLayout = renderedLayout;
449
this._imageData = imageData;
450
this._renderedLines = new RenderedLinesCollection({
451
createLine: () => MinimapLine.INVALID
452
});
453
this._renderedLines._set(renderedLayout.startLineNumber, lines);
454
}
455
456
/**
457
* Check if the current RenderData matches accurately the new desired layout and no painting is needed.
458
*/
459
public linesEquals(layout: MinimapLayout): boolean {
460
if (!this.scrollEquals(layout)) {
461
return false;
462
}
463
464
const tmp = this._renderedLines._get();
465
const lines = tmp.lines;
466
for (let i = 0, len = lines.length; i < len; i++) {
467
if (lines[i].dy === -1) {
468
// This line is invalid
469
return false;
470
}
471
}
472
473
return true;
474
}
475
476
/**
477
* Check if the current RenderData matches the new layout's scroll position
478
*/
479
public scrollEquals(layout: MinimapLayout): boolean {
480
return this.renderedLayout.startLineNumber === layout.startLineNumber
481
&& this.renderedLayout.endLineNumber === layout.endLineNumber;
482
}
483
484
_get(): { imageData: ImageData; rendLineNumberStart: number; lines: MinimapLine[] } {
485
const tmp = this._renderedLines._get();
486
return {
487
imageData: this._imageData,
488
rendLineNumberStart: tmp.rendLineNumberStart,
489
lines: tmp.lines
490
};
491
}
492
493
public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {
494
return this._renderedLines.onLinesChanged(changeFromLineNumber, changeCount);
495
}
496
public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): void {
497
this._renderedLines.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber);
498
}
499
public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): void {
500
this._renderedLines.onLinesInserted(insertFromLineNumber, insertToLineNumber);
501
}
502
public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {
503
return this._renderedLines.onTokensChanged(ranges);
504
}
505
}
506
507
/**
508
* Some sort of double buffering.
509
*
510
* Keeps two buffers around that will be rotated for painting.
511
* Always gives a buffer that is filled with the background color.
512
*/
513
class MinimapBuffers {
514
515
private readonly _backgroundFillData: Uint8ClampedArray;
516
private readonly _buffers: [ImageData, ImageData];
517
private _lastUsedBuffer: number;
518
519
constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA8) {
520
this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background);
521
this._buffers = [
522
ctx.createImageData(WIDTH, HEIGHT),
523
ctx.createImageData(WIDTH, HEIGHT)
524
];
525
this._lastUsedBuffer = 0;
526
}
527
528
public getBuffer(): ImageData {
529
// rotate buffers
530
this._lastUsedBuffer = 1 - this._lastUsedBuffer;
531
const result = this._buffers[this._lastUsedBuffer];
532
533
// fill with background color
534
result.data.set(this._backgroundFillData);
535
536
return result;
537
}
538
539
private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA8): Uint8ClampedArray {
540
const backgroundR = background.r;
541
const backgroundG = background.g;
542
const backgroundB = background.b;
543
const backgroundA = background.a;
544
545
const result = new Uint8ClampedArray(WIDTH * HEIGHT * 4);
546
let offset = 0;
547
for (let i = 0; i < HEIGHT; i++) {
548
for (let j = 0; j < WIDTH; j++) {
549
result[offset] = backgroundR;
550
result[offset + 1] = backgroundG;
551
result[offset + 2] = backgroundB;
552
result[offset + 3] = backgroundA;
553
offset += 4;
554
}
555
}
556
557
return result;
558
}
559
}
560
561
export interface IMinimapModel {
562
readonly tokensColorTracker: MinimapTokensColorTracker;
563
readonly options: MinimapOptions;
564
565
getLineCount(): number;
566
getRealLineCount(): number;
567
getLineContent(lineNumber: number): string;
568
getLineMaxColumn(lineNumber: number): number;
569
getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[];
570
getSelections(): Selection[];
571
getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];
572
getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];
573
getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null;
574
getOptions(): TextModelResolvedOptions;
575
revealLineNumber(lineNumber: number): void;
576
setScrollTop(scrollTop: number): void;
577
}
578
579
interface IMinimapRenderingContext {
580
readonly viewportContainsWhitespaceGaps: boolean;
581
582
readonly scrollWidth: number;
583
readonly scrollHeight: number;
584
585
readonly viewportStartLineNumber: number;
586
readonly viewportEndLineNumber: number;
587
readonly viewportStartLineNumberVerticalOffset: number;
588
589
readonly scrollTop: number;
590
readonly scrollLeft: number;
591
592
readonly viewportWidth: number;
593
readonly viewportHeight: number;
594
}
595
596
interface SamplingStateLinesDeletedEvent {
597
type: 'deleted';
598
_oldIndex: number;
599
deleteFromLineNumber: number;
600
deleteToLineNumber: number;
601
}
602
603
interface SamplingStateLinesInsertedEvent {
604
type: 'inserted';
605
_i: number;
606
insertFromLineNumber: number;
607
insertToLineNumber: number;
608
}
609
610
interface SamplingStateFlushEvent {
611
type: 'flush';
612
}
613
614
type SamplingStateEvent = SamplingStateLinesInsertedEvent | SamplingStateLinesDeletedEvent | SamplingStateFlushEvent;
615
616
class MinimapSamplingState {
617
618
public static compute(options: MinimapOptions, viewLineCount: number, oldSamplingState: MinimapSamplingState | null): [MinimapSamplingState | null, SamplingStateEvent[]] {
619
if (options.renderMinimap === RenderMinimap.None || !options.isSampling) {
620
return [null, []];
621
}
622
623
// ratio is intentionally not part of the layout to avoid the layout changing all the time
624
// so we need to recompute it again...
625
const { minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({
626
viewLineCount: viewLineCount,
627
scrollBeyondLastLine: options.scrollBeyondLastLine,
628
paddingTop: options.paddingTop,
629
paddingBottom: options.paddingBottom,
630
height: options.editorHeight,
631
lineHeight: options.lineHeight,
632
pixelRatio: options.pixelRatio
633
});
634
const ratio = viewLineCount / minimapLineCount;
635
const halfRatio = ratio / 2;
636
637
if (!oldSamplingState || oldSamplingState.minimapLines.length === 0) {
638
const result: number[] = [];
639
result[0] = 1;
640
if (minimapLineCount > 1) {
641
for (let i = 0, lastIndex = minimapLineCount - 1; i < lastIndex; i++) {
642
result[i] = Math.round(i * ratio + halfRatio);
643
}
644
result[minimapLineCount - 1] = viewLineCount;
645
}
646
return [new MinimapSamplingState(ratio, result), []];
647
}
648
649
const oldMinimapLines = oldSamplingState.minimapLines;
650
const oldLength = oldMinimapLines.length;
651
const result: number[] = [];
652
let oldIndex = 0;
653
let oldDeltaLineCount = 0;
654
let minViewLineNumber = 1;
655
const MAX_EVENT_COUNT = 10; // generate at most 10 events, if there are more than 10 changes, just flush all previous data
656
let events: SamplingStateEvent[] = [];
657
let lastEvent: SamplingStateEvent | null = null;
658
for (let i = 0; i < minimapLineCount; i++) {
659
const fromViewLineNumber = Math.max(minViewLineNumber, Math.round(i * ratio));
660
const toViewLineNumber = Math.max(fromViewLineNumber, Math.round((i + 1) * ratio));
661
662
while (oldIndex < oldLength && oldMinimapLines[oldIndex] < fromViewLineNumber) {
663
if (events.length < MAX_EVENT_COUNT) {
664
const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;
665
if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) {
666
lastEvent.deleteToLineNumber++;
667
} else {
668
lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber };
669
events.push(lastEvent);
670
}
671
oldDeltaLineCount--;
672
}
673
oldIndex++;
674
}
675
676
let selectedViewLineNumber: number;
677
if (oldIndex < oldLength && oldMinimapLines[oldIndex] <= toViewLineNumber) {
678
// reuse the old sampled line
679
selectedViewLineNumber = oldMinimapLines[oldIndex];
680
oldIndex++;
681
} else {
682
if (i === 0) {
683
selectedViewLineNumber = 1;
684
} else if (i + 1 === minimapLineCount) {
685
selectedViewLineNumber = viewLineCount;
686
} else {
687
selectedViewLineNumber = Math.round(i * ratio + halfRatio);
688
}
689
if (events.length < MAX_EVENT_COUNT) {
690
const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;
691
if (lastEvent && lastEvent.type === 'inserted' && lastEvent._i === i - 1) {
692
lastEvent.insertToLineNumber++;
693
} else {
694
lastEvent = { type: 'inserted', _i: i, insertFromLineNumber: oldMinimapLineNumber, insertToLineNumber: oldMinimapLineNumber };
695
events.push(lastEvent);
696
}
697
oldDeltaLineCount++;
698
}
699
}
700
701
result[i] = selectedViewLineNumber;
702
minViewLineNumber = selectedViewLineNumber;
703
}
704
705
if (events.length < MAX_EVENT_COUNT) {
706
while (oldIndex < oldLength) {
707
const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;
708
if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) {
709
lastEvent.deleteToLineNumber++;
710
} else {
711
lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber };
712
events.push(lastEvent);
713
}
714
oldDeltaLineCount--;
715
oldIndex++;
716
}
717
} else {
718
// too many events, just give up
719
events = [{ type: 'flush' }];
720
}
721
722
return [new MinimapSamplingState(ratio, result), events];
723
}
724
725
constructor(
726
public readonly samplingRatio: number,
727
public readonly minimapLines: number[] // a map of 0-based minimap line indexes to 1-based view line numbers
728
) {
729
}
730
731
public modelLineToMinimapLine(lineNumber: number): number {
732
return Math.min(this.minimapLines.length, Math.max(1, Math.round(lineNumber / this.samplingRatio)));
733
}
734
735
/**
736
* Will return null if the model line ranges are not intersecting with a sampled model line.
737
*/
738
public modelLineRangeToMinimapLineRange(fromLineNumber: number, toLineNumber: number): [number, number] | null {
739
let fromLineIndex = this.modelLineToMinimapLine(fromLineNumber) - 1;
740
while (fromLineIndex > 0 && this.minimapLines[fromLineIndex - 1] >= fromLineNumber) {
741
fromLineIndex--;
742
}
743
let toLineIndex = this.modelLineToMinimapLine(toLineNumber) - 1;
744
while (toLineIndex + 1 < this.minimapLines.length && this.minimapLines[toLineIndex + 1] <= toLineNumber) {
745
toLineIndex++;
746
}
747
if (fromLineIndex === toLineIndex) {
748
const sampledLineNumber = this.minimapLines[fromLineIndex];
749
if (sampledLineNumber < fromLineNumber || sampledLineNumber > toLineNumber) {
750
// This line is not part of the sampled lines ==> nothing to do
751
return null;
752
}
753
}
754
return [fromLineIndex + 1, toLineIndex + 1];
755
}
756
757
/**
758
* Will always return a range, even if it is not intersecting with a sampled model line.
759
*/
760
public decorationLineRangeToMinimapLineRange(startLineNumber: number, endLineNumber: number): [number, number] {
761
let minimapLineStart = this.modelLineToMinimapLine(startLineNumber);
762
let minimapLineEnd = this.modelLineToMinimapLine(endLineNumber);
763
if (startLineNumber !== endLineNumber && minimapLineEnd === minimapLineStart) {
764
if (minimapLineEnd === this.minimapLines.length) {
765
if (minimapLineStart > 1) {
766
minimapLineStart--;
767
}
768
} else {
769
minimapLineEnd++;
770
}
771
}
772
return [minimapLineStart, minimapLineEnd];
773
}
774
775
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): [number, number] {
776
// have the mapping be sticky
777
const deletedLineCount = e.toLineNumber - e.fromLineNumber + 1;
778
let changeStartIndex = this.minimapLines.length;
779
let changeEndIndex = 0;
780
for (let i = this.minimapLines.length - 1; i >= 0; i--) {
781
if (this.minimapLines[i] < e.fromLineNumber) {
782
break;
783
}
784
if (this.minimapLines[i] <= e.toLineNumber) {
785
// this line got deleted => move to previous available
786
this.minimapLines[i] = Math.max(1, e.fromLineNumber - 1);
787
changeStartIndex = Math.min(changeStartIndex, i);
788
changeEndIndex = Math.max(changeEndIndex, i);
789
} else {
790
this.minimapLines[i] -= deletedLineCount;
791
}
792
}
793
return [changeStartIndex, changeEndIndex];
794
}
795
796
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {
797
// have the mapping be sticky
798
const insertedLineCount = e.toLineNumber - e.fromLineNumber + 1;
799
for (let i = this.minimapLines.length - 1; i >= 0; i--) {
800
if (this.minimapLines[i] < e.fromLineNumber) {
801
break;
802
}
803
this.minimapLines[i] += insertedLineCount;
804
}
805
}
806
}
807
808
/**
809
* The minimap appears beside the editor scroll bar and visualizes a zoomed out
810
* view of the file.
811
*/
812
export class Minimap extends ViewPart implements IMinimapModel {
813
814
public readonly tokensColorTracker: MinimapTokensColorTracker;
815
816
private _selections: Selection[];
817
private _minimapSelections: Selection[] | null;
818
819
public options: MinimapOptions;
820
821
private _samplingState: MinimapSamplingState | null;
822
private _shouldCheckSampling: boolean;
823
824
private _sectionHeaderCache = new LRUCache<string, string>(10, 1.5);
825
826
private _actual: InnerMinimap;
827
828
constructor(context: ViewContext) {
829
super(context);
830
831
this.tokensColorTracker = MinimapTokensColorTracker.getInstance();
832
833
this._selections = [];
834
this._minimapSelections = null;
835
836
this.options = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker);
837
const [samplingState,] = MinimapSamplingState.compute(this.options, this._context.viewModel.getLineCount(), null);
838
this._samplingState = samplingState;
839
this._shouldCheckSampling = false;
840
841
this._actual = new InnerMinimap(context.theme, this);
842
}
843
844
public override dispose(): void {
845
this._actual.dispose();
846
super.dispose();
847
}
848
849
public getDomNode(): FastDomNode<HTMLElement> {
850
return this._actual.getDomNode();
851
}
852
853
private _onOptionsMaybeChanged(): boolean {
854
const opts = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker);
855
if (this.options.equals(opts)) {
856
return false;
857
}
858
this.options = opts;
859
this._recreateLineSampling();
860
this._actual.onDidChangeOptions();
861
return true;
862
}
863
864
// ---- begin view event handlers
865
866
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
867
return this._onOptionsMaybeChanged();
868
}
869
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
870
this._selections = e.selections;
871
this._minimapSelections = null;
872
return this._actual.onSelectionChanged();
873
}
874
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
875
if (e.affectsMinimap) {
876
return this._actual.onDecorationsChanged();
877
}
878
return false;
879
}
880
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
881
if (this._samplingState) {
882
this._shouldCheckSampling = true;
883
}
884
return this._actual.onFlushed();
885
}
886
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
887
if (this._samplingState) {
888
const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(e.fromLineNumber, e.fromLineNumber + e.count - 1);
889
if (minimapLineRange) {
890
return this._actual.onLinesChanged(minimapLineRange[0], minimapLineRange[1] - minimapLineRange[0] + 1);
891
} else {
892
return false;
893
}
894
} else {
895
return this._actual.onLinesChanged(e.fromLineNumber, e.count);
896
}
897
}
898
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
899
if (this._samplingState) {
900
const [changeStartIndex, changeEndIndex] = this._samplingState.onLinesDeleted(e);
901
if (changeStartIndex <= changeEndIndex) {
902
this._actual.onLinesChanged(changeStartIndex + 1, changeEndIndex - changeStartIndex + 1);
903
}
904
this._shouldCheckSampling = true;
905
return true;
906
} else {
907
return this._actual.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
908
}
909
}
910
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
911
if (this._samplingState) {
912
this._samplingState.onLinesInserted(e);
913
this._shouldCheckSampling = true;
914
return true;
915
} else {
916
return this._actual.onLinesInserted(e.fromLineNumber, e.toLineNumber);
917
}
918
}
919
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
920
return this._actual.onScrollChanged(e);
921
}
922
public override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
923
this._actual.onThemeChanged();
924
this._onOptionsMaybeChanged();
925
return true;
926
}
927
public override onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
928
if (this._samplingState) {
929
const ranges: { fromLineNumber: number; toLineNumber: number }[] = [];
930
for (const range of e.ranges) {
931
const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(range.fromLineNumber, range.toLineNumber);
932
if (minimapLineRange) {
933
ranges.push({ fromLineNumber: minimapLineRange[0], toLineNumber: minimapLineRange[1] });
934
}
935
}
936
if (ranges.length) {
937
return this._actual.onTokensChanged(ranges);
938
} else {
939
return false;
940
}
941
} else {
942
return this._actual.onTokensChanged(e.ranges);
943
}
944
}
945
public override onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {
946
this._onOptionsMaybeChanged();
947
return this._actual.onTokensColorsChanged();
948
}
949
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
950
return this._actual.onZonesChanged();
951
}
952
953
// --- end event handlers
954
955
public prepareRender(ctx: RenderingContext): void {
956
if (this._shouldCheckSampling) {
957
this._shouldCheckSampling = false;
958
this._recreateLineSampling();
959
}
960
}
961
962
public render(ctx: RestrictedRenderingContext): void {
963
let viewportStartLineNumber = ctx.visibleRange.startLineNumber;
964
let viewportEndLineNumber = ctx.visibleRange.endLineNumber;
965
966
if (this._samplingState) {
967
viewportStartLineNumber = this._samplingState.modelLineToMinimapLine(viewportStartLineNumber);
968
viewportEndLineNumber = this._samplingState.modelLineToMinimapLine(viewportEndLineNumber);
969
}
970
971
const minimapCtx: IMinimapRenderingContext = {
972
viewportContainsWhitespaceGaps: (ctx.viewportData.whitespaceViewportData.length > 0),
973
974
scrollWidth: ctx.scrollWidth,
975
scrollHeight: ctx.scrollHeight,
976
977
viewportStartLineNumber: viewportStartLineNumber,
978
viewportEndLineNumber: viewportEndLineNumber,
979
viewportStartLineNumberVerticalOffset: ctx.getVerticalOffsetForLineNumber(viewportStartLineNumber),
980
981
scrollTop: ctx.scrollTop,
982
scrollLeft: ctx.scrollLeft,
983
984
viewportWidth: ctx.viewportWidth,
985
viewportHeight: ctx.viewportHeight,
986
};
987
this._actual.render(minimapCtx);
988
}
989
990
//#region IMinimapModel
991
992
private _recreateLineSampling(): void {
993
this._minimapSelections = null;
994
995
const wasSampling = Boolean(this._samplingState);
996
const [samplingState, events] = MinimapSamplingState.compute(this.options, this._context.viewModel.getLineCount(), this._samplingState);
997
this._samplingState = samplingState;
998
999
if (wasSampling && this._samplingState) {
1000
// was sampling, is sampling
1001
for (const event of events) {
1002
switch (event.type) {
1003
case 'deleted':
1004
this._actual.onLinesDeleted(event.deleteFromLineNumber, event.deleteToLineNumber);
1005
break;
1006
case 'inserted':
1007
this._actual.onLinesInserted(event.insertFromLineNumber, event.insertToLineNumber);
1008
break;
1009
case 'flush':
1010
this._actual.onFlushed();
1011
break;
1012
}
1013
}
1014
}
1015
}
1016
1017
public getLineCount(): number {
1018
if (this._samplingState) {
1019
return this._samplingState.minimapLines.length;
1020
}
1021
return this._context.viewModel.getLineCount();
1022
}
1023
1024
public getRealLineCount(): number {
1025
return this._context.viewModel.getLineCount();
1026
}
1027
1028
public getLineContent(lineNumber: number): string {
1029
if (this._samplingState) {
1030
return this._context.viewModel.getLineContent(this._samplingState.minimapLines[lineNumber - 1]);
1031
}
1032
return this._context.viewModel.getLineContent(lineNumber);
1033
}
1034
1035
public getLineMaxColumn(lineNumber: number): number {
1036
if (this._samplingState) {
1037
return this._context.viewModel.getLineMaxColumn(this._samplingState.minimapLines[lineNumber - 1]);
1038
}
1039
return this._context.viewModel.getLineMaxColumn(lineNumber);
1040
}
1041
1042
public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[] {
1043
if (this._samplingState) {
1044
const result: (ViewLineData | null)[] = [];
1045
for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
1046
if (needed[lineIndex]) {
1047
result[lineIndex] = this._context.viewModel.getViewLineData(this._samplingState.minimapLines[startLineNumber + lineIndex - 1]);
1048
} else {
1049
result[lineIndex] = null;
1050
}
1051
}
1052
return result;
1053
}
1054
return this._context.viewModel.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed).data;
1055
}
1056
1057
public getSelections(): Selection[] {
1058
if (this._minimapSelections === null) {
1059
if (this._samplingState) {
1060
this._minimapSelections = [];
1061
for (const selection of this._selections) {
1062
const [minimapLineStart, minimapLineEnd] = this._samplingState.decorationLineRangeToMinimapLineRange(selection.startLineNumber, selection.endLineNumber);
1063
this._minimapSelections.push(new Selection(minimapLineStart, selection.startColumn, minimapLineEnd, selection.endColumn));
1064
}
1065
} else {
1066
this._minimapSelections = this._selections;
1067
}
1068
}
1069
return this._minimapSelections;
1070
}
1071
1072
public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {
1073
return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)
1074
.filter(decoration => !decoration.options.minimap?.sectionHeaderStyle);
1075
}
1076
1077
public getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {
1078
const headerHeightInMinimapLines = this.options.sectionHeaderFontSize / this.options.minimapLineHeight;
1079
startLineNumber = Math.floor(Math.max(1, startLineNumber - headerHeightInMinimapLines));
1080
return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)
1081
.filter(decoration => !!decoration.options.minimap?.sectionHeaderStyle);
1082
}
1083
1084
private _getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number) {
1085
let visibleRange: Range;
1086
if (this._samplingState) {
1087
const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1];
1088
const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1];
1089
visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber));
1090
} else {
1091
visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber));
1092
}
1093
const decorations = this._context.viewModel.getMinimapDecorationsInRange(visibleRange);
1094
1095
if (this._samplingState) {
1096
const result: ViewModelDecoration[] = [];
1097
for (const decoration of decorations) {
1098
if (!decoration.options.minimap) {
1099
continue;
1100
}
1101
const range = decoration.range;
1102
const minimapStartLineNumber = this._samplingState.modelLineToMinimapLine(range.startLineNumber);
1103
const minimapEndLineNumber = this._samplingState.modelLineToMinimapLine(range.endLineNumber);
1104
result.push(new ViewModelDecoration(new Range(minimapStartLineNumber, range.startColumn, minimapEndLineNumber, range.endColumn), decoration.options));
1105
}
1106
return result;
1107
}
1108
1109
return decorations;
1110
}
1111
1112
public getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null {
1113
const headerText = decoration.options.minimap?.sectionHeaderText;
1114
if (!headerText) {
1115
return null;
1116
}
1117
const cachedText = this._sectionHeaderCache.get(headerText);
1118
if (cachedText) {
1119
return cachedText;
1120
}
1121
const fittedText = fitWidth(headerText);
1122
this._sectionHeaderCache.set(headerText, fittedText);
1123
return fittedText;
1124
}
1125
1126
public getOptions(): TextModelResolvedOptions {
1127
return this._context.viewModel.model.getOptions();
1128
}
1129
1130
public revealLineNumber(lineNumber: number): void {
1131
if (this._samplingState) {
1132
lineNumber = this._samplingState.minimapLines[lineNumber - 1];
1133
}
1134
this._context.viewModel.revealRange(
1135
'mouse',
1136
false,
1137
new Range(lineNumber, 1, lineNumber, 1),
1138
viewEvents.VerticalRevealType.Center,
1139
ScrollType.Smooth
1140
);
1141
}
1142
1143
public setScrollTop(scrollTop: number): void {
1144
this._context.viewModel.viewLayout.setScrollPosition({
1145
scrollTop: scrollTop
1146
}, ScrollType.Immediate);
1147
}
1148
1149
//#endregion
1150
}
1151
1152
class InnerMinimap extends Disposable {
1153
1154
private readonly _theme: EditorTheme;
1155
private readonly _model: IMinimapModel;
1156
1157
private readonly _domNode: FastDomNode<HTMLElement>;
1158
private readonly _shadow: FastDomNode<HTMLElement>;
1159
private readonly _canvas: FastDomNode<HTMLCanvasElement>;
1160
private readonly _decorationsCanvas: FastDomNode<HTMLCanvasElement>;
1161
private readonly _slider: FastDomNode<HTMLElement>;
1162
private readonly _sliderHorizontal: FastDomNode<HTMLElement>;
1163
private readonly _pointerDownListener: IDisposable;
1164
private readonly _sliderPointerMoveMonitor: GlobalPointerMoveMonitor;
1165
private readonly _sliderPointerDownListener: IDisposable;
1166
private readonly _gestureDisposable: IDisposable;
1167
private readonly _sliderTouchStartListener: IDisposable;
1168
private readonly _sliderTouchMoveListener: IDisposable;
1169
private readonly _sliderTouchEndListener: IDisposable;
1170
1171
private _lastRenderData: RenderData | null;
1172
private _selectionColor: Color | undefined;
1173
private _renderDecorations: boolean = false;
1174
private _gestureInProgress: boolean = false;
1175
private _buffers: MinimapBuffers | null;
1176
private _isMouseOverMinimap: boolean = false;
1177
private _hideDelayedScheduler: RunOnceScheduler;
1178
1179
constructor(
1180
theme: EditorTheme,
1181
model: IMinimapModel
1182
) {
1183
super();
1184
1185
this._theme = theme;
1186
this._model = model;
1187
1188
this._lastRenderData = null;
1189
this._buffers = null;
1190
this._selectionColor = this._theme.getColor(minimapSelection);
1191
1192
this._domNode = createFastDomNode(document.createElement('div'));
1193
PartFingerprints.write(this._domNode, PartFingerprint.Minimap);
1194
this._domNode.setClassName(this._getMinimapDomNodeClassName());
1195
this._domNode.setPosition('absolute');
1196
this._domNode.setAttribute('role', 'presentation');
1197
this._domNode.setAttribute('aria-hidden', 'true');
1198
1199
this._shadow = createFastDomNode(document.createElement('div'));
1200
this._shadow.setClassName('minimap-shadow-hidden');
1201
this._domNode.appendChild(this._shadow);
1202
1203
this._canvas = createFastDomNode(document.createElement('canvas'));
1204
this._canvas.setPosition('absolute');
1205
this._canvas.setLeft(0);
1206
this._domNode.appendChild(this._canvas);
1207
1208
this._decorationsCanvas = createFastDomNode(document.createElement('canvas'));
1209
this._decorationsCanvas.setPosition('absolute');
1210
this._decorationsCanvas.setClassName('minimap-decorations-layer');
1211
this._decorationsCanvas.setLeft(0);
1212
this._domNode.appendChild(this._decorationsCanvas);
1213
1214
this._slider = createFastDomNode(document.createElement('div'));
1215
this._slider.setPosition('absolute');
1216
this._slider.setClassName('minimap-slider');
1217
this._slider.setLayerHinting(true);
1218
this._slider.setContain('strict');
1219
this._domNode.appendChild(this._slider);
1220
1221
this._sliderHorizontal = createFastDomNode(document.createElement('div'));
1222
this._sliderHorizontal.setPosition('absolute');
1223
this._sliderHorizontal.setClassName('minimap-slider-horizontal');
1224
this._slider.appendChild(this._sliderHorizontal);
1225
1226
this._applyLayout();
1227
1228
this._hideDelayedScheduler = this._register(new RunOnceScheduler(() => this._hideImmediatelyIfMouseIsOutside(), 500));
1229
1230
this._register(dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.MOUSE_OVER, () => {
1231
this._isMouseOverMinimap = true;
1232
}));
1233
this._register(dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.MOUSE_LEAVE, () => {
1234
this._isMouseOverMinimap = false;
1235
}));
1236
1237
this._pointerDownListener = dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.POINTER_DOWN, (e) => {
1238
e.preventDefault();
1239
1240
const isMouse = (e.pointerType === 'mouse');
1241
const isLeftClick = (e.button === 0);
1242
1243
const renderMinimap = this._model.options.renderMinimap;
1244
if (renderMinimap === RenderMinimap.None) {
1245
return;
1246
}
1247
if (!this._lastRenderData) {
1248
return;
1249
}
1250
if (this._model.options.size !== 'proportional') {
1251
if (isLeftClick && this._lastRenderData) {
1252
// pretend the click occurred in the center of the slider
1253
const position = dom.getDomNodePagePosition(this._slider.domNode);
1254
const initialPosY = position.top + position.height / 2;
1255
this._startSliderDragging(e, initialPosY, this._lastRenderData.renderedLayout);
1256
}
1257
return;
1258
}
1259
1260
if (isLeftClick || !isMouse) {
1261
const minimapLineHeight = this._model.options.minimapLineHeight;
1262
const internalOffsetY = (this._model.options.canvasInnerHeight / this._model.options.canvasOuterHeight) * e.offsetY;
1263
const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);
1264
1265
let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber - this._lastRenderData.renderedLayout.topPaddingLineCount;
1266
lineNumber = Math.min(lineNumber, this._model.getLineCount());
1267
1268
this._model.revealLineNumber(lineNumber);
1269
}
1270
});
1271
1272
this._sliderPointerMoveMonitor = new GlobalPointerMoveMonitor();
1273
1274
this._sliderPointerDownListener = dom.addStandardDisposableListener(this._slider.domNode, dom.EventType.POINTER_DOWN, (e) => {
1275
e.preventDefault();
1276
e.stopPropagation();
1277
if (e.button === 0 && this._lastRenderData) {
1278
this._startSliderDragging(e, e.pageY, this._lastRenderData.renderedLayout);
1279
}
1280
});
1281
1282
this._gestureDisposable = Gesture.addTarget(this._domNode.domNode);
1283
this._sliderTouchStartListener = dom.addDisposableListener(this._domNode.domNode, EventType.Start, (e: GestureEvent) => {
1284
e.preventDefault();
1285
e.stopPropagation();
1286
if (this._lastRenderData) {
1287
this._slider.toggleClassName('active', true);
1288
this._gestureInProgress = true;
1289
this.scrollDueToTouchEvent(e);
1290
}
1291
}, { passive: false });
1292
1293
this._sliderTouchMoveListener = dom.addDisposableListener(this._domNode.domNode, EventType.Change, (e: GestureEvent) => {
1294
e.preventDefault();
1295
e.stopPropagation();
1296
if (this._lastRenderData && this._gestureInProgress) {
1297
this.scrollDueToTouchEvent(e);
1298
}
1299
}, { passive: false });
1300
1301
this._sliderTouchEndListener = dom.addStandardDisposableListener(this._domNode.domNode, EventType.End, (e: GestureEvent) => {
1302
e.preventDefault();
1303
e.stopPropagation();
1304
this._gestureInProgress = false;
1305
this._slider.toggleClassName('active', false);
1306
});
1307
}
1308
1309
private _hideSoon() {
1310
this._hideDelayedScheduler.cancel();
1311
this._hideDelayedScheduler.schedule();
1312
}
1313
1314
private _hideImmediatelyIfMouseIsOutside() {
1315
if (this._isMouseOverMinimap) {
1316
this._hideSoon();
1317
return;
1318
}
1319
this._domNode.toggleClassName('active', false);
1320
}
1321
1322
private _startSliderDragging(e: PointerEvent, initialPosY: number, initialSliderState: MinimapLayout): void {
1323
if (!e.target || !(e.target instanceof Element)) {
1324
return;
1325
}
1326
const initialPosX = e.pageX;
1327
1328
this._slider.toggleClassName('active', true);
1329
1330
const handlePointerMove = (posy: number, posx: number) => {
1331
const minimapPosition = dom.getDomNodePagePosition(this._domNode.domNode);
1332
const pointerOrthogonalDelta = Math.min(
1333
Math.abs(posx - initialPosX),
1334
Math.abs(posx - minimapPosition.left),
1335
Math.abs(posx - minimapPosition.left - minimapPosition.width)
1336
);
1337
1338
if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
1339
// The pointer has wondered away from the scrollbar => reset dragging
1340
this._model.setScrollTop(initialSliderState.scrollTop);
1341
return;
1342
}
1343
1344
const pointerDelta = posy - initialPosY;
1345
this._model.setScrollTop(initialSliderState.getDesiredScrollTopFromDelta(pointerDelta));
1346
};
1347
1348
if (e.pageY !== initialPosY) {
1349
handlePointerMove(e.pageY, initialPosX);
1350
}
1351
1352
this._sliderPointerMoveMonitor.startMonitoring(
1353
e.target,
1354
e.pointerId,
1355
e.buttons,
1356
pointerMoveData => handlePointerMove(pointerMoveData.pageY, pointerMoveData.pageX),
1357
() => {
1358
this._slider.toggleClassName('active', false);
1359
}
1360
);
1361
}
1362
1363
private scrollDueToTouchEvent(touch: GestureEvent) {
1364
const startY = this._domNode.domNode.getBoundingClientRect().top;
1365
const scrollTop = this._lastRenderData!.renderedLayout.getDesiredScrollTopFromTouchLocation(touch.pageY - startY);
1366
this._model.setScrollTop(scrollTop);
1367
}
1368
1369
public override dispose(): void {
1370
this._pointerDownListener.dispose();
1371
this._sliderPointerMoveMonitor.dispose();
1372
this._sliderPointerDownListener.dispose();
1373
this._gestureDisposable.dispose();
1374
this._sliderTouchStartListener.dispose();
1375
this._sliderTouchMoveListener.dispose();
1376
this._sliderTouchEndListener.dispose();
1377
super.dispose();
1378
}
1379
1380
private _getMinimapDomNodeClassName(): string {
1381
const class_ = ['minimap'];
1382
if (this._model.options.showSlider === 'always') {
1383
class_.push('slider-always');
1384
} else {
1385
class_.push('slider-mouseover');
1386
}
1387
1388
if (this._model.options.autohide === 'mouseover') {
1389
class_.push('minimap-autohide-mouseover');
1390
} else if (this._model.options.autohide === 'scroll') {
1391
class_.push('minimap-autohide-scroll');
1392
}
1393
1394
return class_.join(' ');
1395
}
1396
1397
public getDomNode(): FastDomNode<HTMLElement> {
1398
return this._domNode;
1399
}
1400
1401
private _applyLayout(): void {
1402
this._domNode.setLeft(this._model.options.minimapLeft);
1403
this._domNode.setWidth(this._model.options.minimapWidth);
1404
this._domNode.setHeight(this._model.options.minimapHeight);
1405
this._shadow.setHeight(this._model.options.minimapHeight);
1406
1407
this._canvas.setWidth(this._model.options.canvasOuterWidth);
1408
this._canvas.setHeight(this._model.options.canvasOuterHeight);
1409
this._canvas.domNode.width = this._model.options.canvasInnerWidth;
1410
this._canvas.domNode.height = this._model.options.canvasInnerHeight;
1411
1412
this._decorationsCanvas.setWidth(this._model.options.canvasOuterWidth);
1413
this._decorationsCanvas.setHeight(this._model.options.canvasOuterHeight);
1414
this._decorationsCanvas.domNode.width = this._model.options.canvasInnerWidth;
1415
this._decorationsCanvas.domNode.height = this._model.options.canvasInnerHeight;
1416
1417
this._slider.setWidth(this._model.options.minimapWidth);
1418
}
1419
1420
private _getBuffer(): ImageData | null {
1421
if (!this._buffers) {
1422
if (this._model.options.canvasInnerWidth > 0 && this._model.options.canvasInnerHeight > 0) {
1423
this._buffers = new MinimapBuffers(
1424
this._canvas.domNode.getContext('2d')!,
1425
this._model.options.canvasInnerWidth,
1426
this._model.options.canvasInnerHeight,
1427
this._model.options.backgroundColor
1428
);
1429
}
1430
}
1431
return this._buffers ? this._buffers.getBuffer() : null;
1432
}
1433
1434
// ---- begin view event handlers
1435
1436
public onDidChangeOptions(): void {
1437
this._lastRenderData = null;
1438
this._buffers = null;
1439
this._applyLayout();
1440
this._domNode.setClassName(this._getMinimapDomNodeClassName());
1441
}
1442
public onSelectionChanged(): boolean {
1443
this._renderDecorations = true;
1444
return true;
1445
}
1446
public onDecorationsChanged(): boolean {
1447
this._renderDecorations = true;
1448
return true;
1449
}
1450
public onFlushed(): boolean {
1451
this._lastRenderData = null;
1452
return true;
1453
}
1454
public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {
1455
if (this._lastRenderData) {
1456
return this._lastRenderData.onLinesChanged(changeFromLineNumber, changeCount);
1457
}
1458
return false;
1459
}
1460
public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): boolean {
1461
this._lastRenderData?.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber);
1462
return true;
1463
}
1464
public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): boolean {
1465
this._lastRenderData?.onLinesInserted(insertFromLineNumber, insertToLineNumber);
1466
return true;
1467
}
1468
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
1469
if (this._model.options.autohide === 'scroll' && (e.scrollTopChanged || e.scrollHeightChanged)) {
1470
this._domNode.toggleClassName('active', true);
1471
this._hideSoon();
1472
}
1473
this._renderDecorations = true;
1474
return true;
1475
}
1476
public onThemeChanged(): boolean {
1477
this._selectionColor = this._theme.getColor(minimapSelection);
1478
this._renderDecorations = true;
1479
return true;
1480
}
1481
public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {
1482
if (this._lastRenderData) {
1483
return this._lastRenderData.onTokensChanged(ranges);
1484
}
1485
return false;
1486
}
1487
public onTokensColorsChanged(): boolean {
1488
this._lastRenderData = null;
1489
this._buffers = null;
1490
return true;
1491
}
1492
public onZonesChanged(): boolean {
1493
this._lastRenderData = null;
1494
return true;
1495
}
1496
1497
// --- end event handlers
1498
1499
public render(renderingCtx: IMinimapRenderingContext): void {
1500
const renderMinimap = this._model.options.renderMinimap;
1501
if (renderMinimap === RenderMinimap.None) {
1502
this._shadow.setClassName('minimap-shadow-hidden');
1503
this._sliderHorizontal.setWidth(0);
1504
this._sliderHorizontal.setHeight(0);
1505
return;
1506
}
1507
if (renderingCtx.scrollLeft + renderingCtx.viewportWidth >= renderingCtx.scrollWidth) {
1508
this._shadow.setClassName('minimap-shadow-hidden');
1509
} else {
1510
this._shadow.setClassName('minimap-shadow-visible');
1511
}
1512
1513
const layout = MinimapLayout.create(
1514
this._model.options,
1515
renderingCtx.viewportStartLineNumber,
1516
renderingCtx.viewportEndLineNumber,
1517
renderingCtx.viewportStartLineNumberVerticalOffset,
1518
renderingCtx.viewportHeight,
1519
renderingCtx.viewportContainsWhitespaceGaps,
1520
this._model.getLineCount(),
1521
this._model.getRealLineCount(),
1522
renderingCtx.scrollTop,
1523
renderingCtx.scrollHeight,
1524
this._lastRenderData ? this._lastRenderData.renderedLayout : null
1525
);
1526
this._slider.setDisplay(layout.sliderNeeded ? 'block' : 'none');
1527
this._slider.setTop(layout.sliderTop);
1528
this._slider.setHeight(layout.sliderHeight);
1529
1530
// Compute horizontal slider coordinates
1531
this._sliderHorizontal.setLeft(0);
1532
this._sliderHorizontal.setWidth(this._model.options.minimapWidth);
1533
this._sliderHorizontal.setTop(0);
1534
this._sliderHorizontal.setHeight(layout.sliderHeight);
1535
1536
this.renderDecorations(layout);
1537
this._lastRenderData = this.renderLines(layout);
1538
}
1539
1540
private renderDecorations(layout: MinimapLayout) {
1541
if (this._renderDecorations) {
1542
this._renderDecorations = false;
1543
const selections = this._model.getSelections();
1544
selections.sort(Range.compareRangesUsingStarts);
1545
1546
const decorations = this._model.getMinimapDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);
1547
decorations.sort((a, b) => (a.options.zIndex || 0) - (b.options.zIndex || 0));
1548
1549
const { canvasInnerWidth, canvasInnerHeight } = this._model.options;
1550
const minimapLineHeight = this._model.options.minimapLineHeight;
1551
const minimapCharWidth = this._model.options.minimapCharWidth;
1552
const tabSize = this._model.getOptions().tabSize;
1553
const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;
1554
1555
canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight);
1556
1557
// We first need to render line highlights and then render decorations on top of those.
1558
// But we need to pick a single color for each line, and use that as a line highlight.
1559
// This needs to be the color of the decoration with the highest `zIndex`, but priority
1560
// is given to the selection.
1561
1562
const highlightedLines = new ContiguousLineMap<boolean>(layout.startLineNumber, layout.endLineNumber, false);
1563
this._renderSelectionLineHighlights(canvasContext, selections, highlightedLines, layout, minimapLineHeight);
1564
this._renderDecorationsLineHighlights(canvasContext, decorations, highlightedLines, layout, minimapLineHeight);
1565
1566
const lineOffsetMap = new ContiguousLineMap<number[] | null>(layout.startLineNumber, layout.endLineNumber, null);
1567
this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);
1568
this._renderDecorationsHighlights(canvasContext, decorations, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);
1569
this._renderSectionHeaders(layout);
1570
}
1571
}
1572
1573
private _renderSelectionLineHighlights(
1574
canvasContext: CanvasRenderingContext2D,
1575
selections: Selection[],
1576
highlightedLines: ContiguousLineMap<boolean>,
1577
layout: MinimapLayout,
1578
minimapLineHeight: number
1579
): void {
1580
if (!this._selectionColor || this._selectionColor.isTransparent()) {
1581
return;
1582
}
1583
1584
canvasContext.fillStyle = this._selectionColor.transparent(0.5).toString();
1585
1586
let y1 = 0;
1587
let y2 = 0;
1588
1589
for (const selection of selections) {
1590
const intersection = layout.intersectWithViewport(selection);
1591
if (!intersection) {
1592
// entirely outside minimap's viewport
1593
continue;
1594
}
1595
const [startLineNumber, endLineNumber] = intersection;
1596
1597
for (let line = startLineNumber; line <= endLineNumber; line++) {
1598
highlightedLines.set(line, true);
1599
}
1600
1601
const yy1 = layout.getYForLineNumber(startLineNumber, minimapLineHeight);
1602
const yy2 = layout.getYForLineNumber(endLineNumber, minimapLineHeight);
1603
1604
if (y2 >= yy1) {
1605
// merge into previous
1606
y2 = yy2;
1607
} else {
1608
if (y2 > y1) {
1609
// flush
1610
canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1);
1611
}
1612
y1 = yy1;
1613
y2 = yy2;
1614
}
1615
}
1616
1617
if (y2 > y1) {
1618
// flush
1619
canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1);
1620
}
1621
}
1622
1623
private _renderDecorationsLineHighlights(
1624
canvasContext: CanvasRenderingContext2D,
1625
decorations: ViewModelDecoration[],
1626
highlightedLines: ContiguousLineMap<boolean>,
1627
layout: MinimapLayout,
1628
minimapLineHeight: number
1629
): void {
1630
1631
const highlightColors = new Map<string, string>();
1632
1633
// Loop backwards to hit first decorations with higher `zIndex`
1634
for (let i = decorations.length - 1; i >= 0; i--) {
1635
const decoration = decorations[i];
1636
1637
const minimapOptions = <ModelDecorationMinimapOptions | null | undefined>decoration.options.minimap;
1638
if (!minimapOptions || minimapOptions.position !== MinimapPosition.Inline) {
1639
continue;
1640
}
1641
1642
const intersection = layout.intersectWithViewport(decoration.range);
1643
if (!intersection) {
1644
// entirely outside minimap's viewport
1645
continue;
1646
}
1647
const [startLineNumber, endLineNumber] = intersection;
1648
1649
const decorationColor = minimapOptions.getColor(this._theme.value);
1650
if (!decorationColor || decorationColor.isTransparent()) {
1651
continue;
1652
}
1653
1654
let highlightColor = highlightColors.get(decorationColor.toString());
1655
if (!highlightColor) {
1656
highlightColor = decorationColor.transparent(0.5).toString();
1657
highlightColors.set(decorationColor.toString(), highlightColor);
1658
}
1659
1660
canvasContext.fillStyle = highlightColor;
1661
for (let line = startLineNumber; line <= endLineNumber; line++) {
1662
if (highlightedLines.has(line)) {
1663
continue;
1664
}
1665
highlightedLines.set(line, true);
1666
const y = layout.getYForLineNumber(startLineNumber, minimapLineHeight);
1667
canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y, canvasContext.canvas.width, minimapLineHeight);
1668
}
1669
}
1670
}
1671
1672
private _renderSelectionsHighlights(
1673
canvasContext: CanvasRenderingContext2D,
1674
selections: Selection[],
1675
lineOffsetMap: ContiguousLineMap<number[] | null>,
1676
layout: MinimapLayout,
1677
lineHeight: number,
1678
tabSize: number,
1679
characterWidth: number,
1680
canvasInnerWidth: number
1681
): void {
1682
if (!this._selectionColor || this._selectionColor.isTransparent()) {
1683
return;
1684
}
1685
for (const selection of selections) {
1686
const intersection = layout.intersectWithViewport(selection);
1687
if (!intersection) {
1688
// entirely outside minimap's viewport
1689
continue;
1690
}
1691
const [startLineNumber, endLineNumber] = intersection;
1692
1693
for (let line = startLineNumber; line <= endLineNumber; line++) {
1694
this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth, canvasInnerWidth);
1695
}
1696
}
1697
}
1698
1699
private _renderDecorationsHighlights(
1700
canvasContext: CanvasRenderingContext2D,
1701
decorations: ViewModelDecoration[],
1702
lineOffsetMap: ContiguousLineMap<number[] | null>,
1703
layout: MinimapLayout,
1704
minimapLineHeight: number,
1705
tabSize: number,
1706
characterWidth: number,
1707
canvasInnerWidth: number
1708
): void {
1709
// Loop forwards to hit first decorations with lower `zIndex`
1710
for (const decoration of decorations) {
1711
1712
const minimapOptions = <ModelDecorationMinimapOptions | null | undefined>decoration.options.minimap;
1713
if (!minimapOptions) {
1714
continue;
1715
}
1716
1717
const intersection = layout.intersectWithViewport(decoration.range);
1718
if (!intersection) {
1719
// entirely outside minimap's viewport
1720
continue;
1721
}
1722
const [startLineNumber, endLineNumber] = intersection;
1723
1724
const decorationColor = minimapOptions.getColor(this._theme.value);
1725
if (!decorationColor || decorationColor.isTransparent()) {
1726
continue;
1727
}
1728
1729
for (let line = startLineNumber; line <= endLineNumber; line++) {
1730
switch (minimapOptions.position) {
1731
1732
case MinimapPosition.Inline:
1733
this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration.range, decorationColor, layout, line, minimapLineHeight, minimapLineHeight, tabSize, characterWidth, canvasInnerWidth);
1734
continue;
1735
1736
case MinimapPosition.Gutter: {
1737
const y = layout.getYForLineNumber(line, minimapLineHeight);
1738
const x = 2;
1739
this.renderDecoration(canvasContext, decorationColor, x, y, GUTTER_DECORATION_WIDTH, minimapLineHeight);
1740
continue;
1741
}
1742
}
1743
}
1744
}
1745
}
1746
1747
private renderDecorationOnLine(
1748
canvasContext: CanvasRenderingContext2D,
1749
lineOffsetMap: ContiguousLineMap<number[] | null>,
1750
decorationRange: Range,
1751
decorationColor: Color | undefined,
1752
layout: MinimapLayout,
1753
lineNumber: number,
1754
height: number,
1755
minimapLineHeight: number,
1756
tabSize: number,
1757
charWidth: number,
1758
canvasInnerWidth: number
1759
): void {
1760
const y = layout.getYForLineNumber(lineNumber, minimapLineHeight);
1761
1762
// Skip rendering the line if it's vertically outside our viewport
1763
if (y + height < 0 || y > this._model.options.canvasInnerHeight) {
1764
return;
1765
}
1766
1767
const { startLineNumber, endLineNumber } = decorationRange;
1768
const startColumn = (startLineNumber === lineNumber ? decorationRange.startColumn : 1);
1769
const endColumn = (endLineNumber === lineNumber ? decorationRange.endColumn : this._model.getLineMaxColumn(lineNumber));
1770
1771
const x1 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, startColumn, tabSize, charWidth, canvasInnerWidth);
1772
const x2 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, endColumn, tabSize, charWidth, canvasInnerWidth);
1773
1774
this.renderDecoration(canvasContext, decorationColor, x1, y, x2 - x1, height);
1775
}
1776
1777
private getXOffsetForPosition(
1778
lineOffsetMap: ContiguousLineMap<number[] | null>,
1779
lineNumber: number,
1780
column: number,
1781
tabSize: number,
1782
charWidth: number,
1783
canvasInnerWidth: number
1784
): number {
1785
if (column === 1) {
1786
return MINIMAP_GUTTER_WIDTH;
1787
}
1788
1789
const minimumXOffset = (column - 1) * charWidth;
1790
if (minimumXOffset >= canvasInnerWidth) {
1791
// there is no need to look at actual characters,
1792
// as this column is certainly after the minimap width
1793
return canvasInnerWidth;
1794
}
1795
1796
// Cache line offset data so that it is only read once per line
1797
let lineIndexToXOffset = lineOffsetMap.get(lineNumber);
1798
if (!lineIndexToXOffset) {
1799
const lineData = this._model.getLineContent(lineNumber);
1800
lineIndexToXOffset = [MINIMAP_GUTTER_WIDTH];
1801
let prevx = MINIMAP_GUTTER_WIDTH;
1802
for (let i = 1; i < lineData.length + 1; i++) {
1803
const charCode = lineData.charCodeAt(i - 1);
1804
const dx = charCode === CharCode.Tab
1805
? tabSize * charWidth
1806
: strings.isFullWidthCharacter(charCode)
1807
? 2 * charWidth
1808
: charWidth;
1809
1810
const x = prevx + dx;
1811
if (x >= canvasInnerWidth) {
1812
// no need to keep on going, as we've hit the canvas width
1813
lineIndexToXOffset[i] = canvasInnerWidth;
1814
break;
1815
}
1816
1817
lineIndexToXOffset[i] = x;
1818
prevx = x;
1819
}
1820
1821
lineOffsetMap.set(lineNumber, lineIndexToXOffset);
1822
}
1823
1824
if (column - 1 < lineIndexToXOffset.length) {
1825
return lineIndexToXOffset[column - 1];
1826
}
1827
// goes over the canvas width
1828
return canvasInnerWidth;
1829
}
1830
1831
private renderDecoration(canvasContext: CanvasRenderingContext2D, decorationColor: Color | undefined, x: number, y: number, width: number, height: number) {
1832
canvasContext.fillStyle = decorationColor && decorationColor.toString() || '';
1833
canvasContext.fillRect(x, y, width, height);
1834
}
1835
1836
private _renderSectionHeaders(layout: MinimapLayout) {
1837
const minimapLineHeight = this._model.options.minimapLineHeight;
1838
const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize;
1839
const sectionHeaderLetterSpacing = this._model.options.sectionHeaderLetterSpacing;
1840
const backgroundFillHeight = sectionHeaderFontSize * 1.5;
1841
const { canvasInnerWidth } = this._model.options;
1842
1843
const backgroundColor = this._model.options.backgroundColor;
1844
const backgroundFill = `rgb(${backgroundColor.r} ${backgroundColor.g} ${backgroundColor.b} / .7)`;
1845
const foregroundColor = this._model.options.sectionHeaderFontColor;
1846
const foregroundFill = `rgb(${foregroundColor.r} ${foregroundColor.g} ${foregroundColor.b})`;
1847
const separatorStroke = foregroundFill;
1848
1849
const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;
1850
canvasContext.letterSpacing = sectionHeaderLetterSpacing + 'px';
1851
canvasContext.font = '500 ' + sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily;
1852
canvasContext.strokeStyle = separatorStroke;
1853
canvasContext.lineWidth = 0.4;
1854
1855
const decorations = this._model.getSectionHeaderDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);
1856
decorations.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber);
1857
1858
const fitWidth = InnerMinimap._fitSectionHeader.bind(null, canvasContext,
1859
canvasInnerWidth - MINIMAP_GUTTER_WIDTH);
1860
1861
for (const decoration of decorations) {
1862
const y = layout.getYForLineNumber(decoration.range.startLineNumber, minimapLineHeight) + sectionHeaderFontSize;
1863
const backgroundFillY = y - sectionHeaderFontSize;
1864
const separatorY = backgroundFillY + 2;
1865
const headerText = this._model.getSectionHeaderText(decoration, fitWidth);
1866
1867
InnerMinimap._renderSectionLabel(
1868
canvasContext,
1869
headerText,
1870
decoration.options.minimap?.sectionHeaderStyle === MinimapSectionHeaderStyle.Underlined,
1871
backgroundFill,
1872
foregroundFill,
1873
canvasInnerWidth,
1874
backgroundFillY,
1875
backgroundFillHeight,
1876
y,
1877
separatorY);
1878
}
1879
}
1880
1881
private static _fitSectionHeader(
1882
target: CanvasRenderingContext2D,
1883
maxWidth: number,
1884
headerText: string,
1885
): string {
1886
if (!headerText) {
1887
return headerText;
1888
}
1889
1890
const ellipsis = '…';
1891
const width = target.measureText(headerText).width;
1892
const ellipsisWidth = target.measureText(ellipsis).width;
1893
1894
if (width <= maxWidth || width <= ellipsisWidth) {
1895
return headerText;
1896
}
1897
1898
const len = headerText.length;
1899
const averageCharWidth = width / headerText.length;
1900
const maxCharCount = Math.floor((maxWidth - ellipsisWidth) / averageCharWidth) - 1;
1901
1902
// Find a halfway point that isn't after whitespace
1903
let halfCharCount = Math.ceil(maxCharCount / 2);
1904
while (halfCharCount > 0 && /\s/.test(headerText[halfCharCount - 1])) {
1905
--halfCharCount;
1906
}
1907
1908
// Split with ellipsis
1909
return headerText.substring(0, halfCharCount)
1910
+ ellipsis + headerText.substring(len - (maxCharCount - halfCharCount));
1911
}
1912
1913
private static _renderSectionLabel(
1914
target: CanvasRenderingContext2D,
1915
headerText: string | null,
1916
hasSeparatorLine: boolean,
1917
backgroundFill: string,
1918
foregroundFill: string,
1919
minimapWidth: number,
1920
backgroundFillY: number,
1921
backgroundFillHeight: number,
1922
textY: number,
1923
separatorY: number
1924
): void {
1925
if (headerText) {
1926
target.fillStyle = backgroundFill;
1927
target.fillRect(0, backgroundFillY, minimapWidth, backgroundFillHeight);
1928
1929
target.fillStyle = foregroundFill;
1930
target.fillText(headerText, MINIMAP_GUTTER_WIDTH, textY);
1931
}
1932
1933
if (hasSeparatorLine) {
1934
target.beginPath();
1935
target.moveTo(0, separatorY);
1936
target.lineTo(minimapWidth, separatorY);
1937
target.closePath();
1938
target.stroke();
1939
}
1940
}
1941
1942
private renderLines(layout: MinimapLayout): RenderData | null {
1943
const startLineNumber = layout.startLineNumber;
1944
const endLineNumber = layout.endLineNumber;
1945
const minimapLineHeight = this._model.options.minimapLineHeight;
1946
1947
// Check if nothing changed w.r.t. lines from last frame
1948
if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {
1949
const _lastData = this._lastRenderData._get();
1950
// Nice!! Nothing changed from last frame
1951
return new RenderData(layout, _lastData.imageData, _lastData.lines);
1952
}
1953
1954
// Oh well!! We need to repaint some lines...
1955
1956
const imageData = this._getBuffer();
1957
if (!imageData) {
1958
// 0 width or 0 height canvas, nothing to do
1959
return null;
1960
}
1961
1962
// Render untouched lines by using last rendered data.
1963
const [_dirtyY1, _dirtyY2, needed] = InnerMinimap._renderUntouchedLines(
1964
imageData,
1965
layout.topPaddingLineCount,
1966
startLineNumber,
1967
endLineNumber,
1968
minimapLineHeight,
1969
this._lastRenderData
1970
);
1971
1972
// Fetch rendering info from view model for rest of lines that need rendering.
1973
const lineInfo = this._model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);
1974
const tabSize = this._model.getOptions().tabSize;
1975
const defaultBackground = this._model.options.defaultBackgroundColor;
1976
const background = this._model.options.backgroundColor;
1977
const foregroundAlpha = this._model.options.foregroundAlpha;
1978
const tokensColorTracker = this._model.tokensColorTracker;
1979
const useLighterFont = tokensColorTracker.backgroundIsLight();
1980
const renderMinimap = this._model.options.renderMinimap;
1981
const charRenderer = this._model.options.charRenderer();
1982
const fontScale = this._model.options.fontScale;
1983
const minimapCharWidth = this._model.options.minimapCharWidth;
1984
1985
const baseCharHeight = (renderMinimap === RenderMinimap.Text ? Constants.BASE_CHAR_HEIGHT : Constants.BASE_CHAR_HEIGHT + 1);
1986
const renderMinimapLineHeight = baseCharHeight * fontScale;
1987
const innerLinePadding = (minimapLineHeight > renderMinimapLineHeight ? Math.floor((minimapLineHeight - renderMinimapLineHeight) / 2) : 0);
1988
1989
// Render the rest of lines
1990
const backgroundA = background.a / 255;
1991
const renderBackground = new RGBA8(
1992
Math.round((background.r - defaultBackground.r) * backgroundA + defaultBackground.r),
1993
Math.round((background.g - defaultBackground.g) * backgroundA + defaultBackground.g),
1994
Math.round((background.b - defaultBackground.b) * backgroundA + defaultBackground.b),
1995
255
1996
);
1997
let dy = layout.topPaddingLineCount * minimapLineHeight;
1998
const renderedLines: MinimapLine[] = [];
1999
for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
2000
if (needed[lineIndex]) {
2001
InnerMinimap._renderLine(
2002
imageData,
2003
renderBackground,
2004
background.a,
2005
useLighterFont,
2006
renderMinimap,
2007
minimapCharWidth,
2008
tokensColorTracker,
2009
foregroundAlpha,
2010
charRenderer,
2011
dy,
2012
innerLinePadding,
2013
tabSize,
2014
lineInfo[lineIndex]!,
2015
fontScale,
2016
minimapLineHeight
2017
);
2018
}
2019
renderedLines[lineIndex] = new MinimapLine(dy);
2020
dy += minimapLineHeight;
2021
}
2022
2023
const dirtyY1 = (_dirtyY1 === -1 ? 0 : _dirtyY1);
2024
const dirtyY2 = (_dirtyY2 === -1 ? imageData.height : _dirtyY2);
2025
const dirtyHeight = dirtyY2 - dirtyY1;
2026
2027
// Finally, paint to the canvas
2028
const ctx = this._canvas.domNode.getContext('2d')!;
2029
ctx.putImageData(imageData, 0, 0, 0, dirtyY1, imageData.width, dirtyHeight);
2030
2031
// Save rendered data for reuse on next frame if possible
2032
return new RenderData(
2033
layout,
2034
imageData,
2035
renderedLines
2036
);
2037
}
2038
2039
private static _renderUntouchedLines(
2040
target: ImageData,
2041
topPaddingLineCount: number,
2042
startLineNumber: number,
2043
endLineNumber: number,
2044
minimapLineHeight: number,
2045
lastRenderData: RenderData | null,
2046
): [number, number, boolean[]] {
2047
2048
const needed: boolean[] = [];
2049
if (!lastRenderData) {
2050
for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {
2051
needed[i] = true;
2052
}
2053
return [-1, -1, needed];
2054
}
2055
2056
const _lastData = lastRenderData._get();
2057
const lastTargetData = _lastData.imageData.data;
2058
const lastStartLineNumber = _lastData.rendLineNumberStart;
2059
const lastLines = _lastData.lines;
2060
const lastLinesLength = lastLines.length;
2061
const WIDTH = target.width;
2062
const targetData = target.data;
2063
2064
const maxDestPixel = (endLineNumber - startLineNumber + 1) * minimapLineHeight * WIDTH * 4;
2065
let dirtyPixel1 = -1; // the pixel offset up to which all the data is equal to the prev frame
2066
let dirtyPixel2 = -1; // the pixel offset after which all the data is equal to the prev frame
2067
2068
let copySourceStart = -1;
2069
let copySourceEnd = -1;
2070
let copyDestStart = -1;
2071
let copyDestEnd = -1;
2072
2073
let dest_dy = topPaddingLineCount * minimapLineHeight;
2074
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
2075
const lineIndex = lineNumber - startLineNumber;
2076
const lastLineIndex = lineNumber - lastStartLineNumber;
2077
const source_dy = (lastLineIndex >= 0 && lastLineIndex < lastLinesLength ? lastLines[lastLineIndex].dy : -1);
2078
2079
if (source_dy === -1) {
2080
needed[lineIndex] = true;
2081
dest_dy += minimapLineHeight;
2082
continue;
2083
}
2084
2085
const sourceStart = source_dy * WIDTH * 4;
2086
const sourceEnd = (source_dy + minimapLineHeight) * WIDTH * 4;
2087
const destStart = dest_dy * WIDTH * 4;
2088
const destEnd = (dest_dy + minimapLineHeight) * WIDTH * 4;
2089
2090
if (copySourceEnd === sourceStart && copyDestEnd === destStart) {
2091
// contiguous zone => extend copy request
2092
copySourceEnd = sourceEnd;
2093
copyDestEnd = destEnd;
2094
} else {
2095
if (copySourceStart !== -1) {
2096
// flush existing copy request
2097
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
2098
if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {
2099
dirtyPixel1 = copySourceEnd;
2100
}
2101
if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {
2102
dirtyPixel2 = copySourceStart;
2103
}
2104
}
2105
copySourceStart = sourceStart;
2106
copySourceEnd = sourceEnd;
2107
copyDestStart = destStart;
2108
copyDestEnd = destEnd;
2109
}
2110
2111
needed[lineIndex] = false;
2112
dest_dy += minimapLineHeight;
2113
}
2114
2115
if (copySourceStart !== -1) {
2116
// flush existing copy request
2117
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
2118
if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {
2119
dirtyPixel1 = copySourceEnd;
2120
}
2121
if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {
2122
dirtyPixel2 = copySourceStart;
2123
}
2124
}
2125
2126
const dirtyY1 = (dirtyPixel1 === -1 ? -1 : dirtyPixel1 / (WIDTH * 4));
2127
const dirtyY2 = (dirtyPixel2 === -1 ? -1 : dirtyPixel2 / (WIDTH * 4));
2128
2129
return [dirtyY1, dirtyY2, needed];
2130
}
2131
2132
private static _renderLine(
2133
target: ImageData,
2134
backgroundColor: RGBA8,
2135
backgroundAlpha: number,
2136
useLighterFont: boolean,
2137
renderMinimap: RenderMinimap,
2138
charWidth: number,
2139
colorTracker: MinimapTokensColorTracker,
2140
foregroundAlpha: number,
2141
minimapCharRenderer: MinimapCharRenderer,
2142
dy: number,
2143
innerLinePadding: number,
2144
tabSize: number,
2145
lineData: ViewLineData,
2146
fontScale: number,
2147
minimapLineHeight: number
2148
): void {
2149
const content = lineData.content;
2150
const tokens = lineData.tokens;
2151
const maxDx = target.width - charWidth;
2152
const force1pxHeight = (minimapLineHeight === 1);
2153
2154
let dx = MINIMAP_GUTTER_WIDTH;
2155
let charIndex = 0;
2156
let tabsCharDelta = 0;
2157
2158
for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
2159
const tokenEndIndex = tokens.getEndOffset(tokenIndex);
2160
const tokenColorId = tokens.getForeground(tokenIndex);
2161
const tokenColor = colorTracker.getColor(tokenColorId);
2162
2163
for (; charIndex < tokenEndIndex; charIndex++) {
2164
if (dx > maxDx) {
2165
// hit edge of minimap
2166
return;
2167
}
2168
const charCode = content.charCodeAt(charIndex);
2169
2170
if (charCode === CharCode.Tab) {
2171
const insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
2172
tabsCharDelta += insertSpacesCount - 1;
2173
// No need to render anything since tab is invisible
2174
dx += insertSpacesCount * charWidth;
2175
} else if (charCode === CharCode.Space) {
2176
// No need to render anything since space is invisible
2177
dx += charWidth;
2178
} else {
2179
// Render twice for a full width character
2180
const count = strings.isFullWidthCharacter(charCode) ? 2 : 1;
2181
2182
for (let i = 0; i < count; i++) {
2183
if (renderMinimap === RenderMinimap.Blocks) {
2184
minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, force1pxHeight);
2185
} else { // RenderMinimap.Text
2186
minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, fontScale, useLighterFont, force1pxHeight);
2187
}
2188
2189
dx += charWidth;
2190
2191
if (dx > maxDx) {
2192
// hit edge of minimap
2193
return;
2194
}
2195
}
2196
}
2197
}
2198
}
2199
}
2200
}
2201
2202
class ContiguousLineMap<T> {
2203
2204
private readonly _startLineNumber: number;
2205
private readonly _endLineNumber: number;
2206
private readonly _defaultValue: T;
2207
private readonly _values: T[];
2208
2209
constructor(startLineNumber: number, endLineNumber: number, defaultValue: T) {
2210
this._startLineNumber = startLineNumber;
2211
this._endLineNumber = endLineNumber;
2212
this._defaultValue = defaultValue;
2213
this._values = [];
2214
for (let i = 0, count = this._endLineNumber - this._startLineNumber + 1; i < count; i++) {
2215
this._values[i] = defaultValue;
2216
}
2217
}
2218
2219
public has(lineNumber: number): boolean {
2220
return (this.get(lineNumber) !== this._defaultValue);
2221
}
2222
2223
public set(lineNumber: number, value: T): void {
2224
if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) {
2225
return;
2226
}
2227
this._values[lineNumber - this._startLineNumber] = value;
2228
}
2229
2230
public get(lineNumber: number): T {
2231
if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) {
2232
return this._defaultValue;
2233
}
2234
return this._values[lineNumber - this._startLineNumber];
2235
}
2236
}
2237
2238
2239