patched-vscode/src/vs/editor/browser/viewParts/minimap/minimap.ts (1,481 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./minimap';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { GlobalPointerMoveMonitor } from 'vs/base/browser/globalPointerMoveMonitor';
import { CharCode } from 'vs/base/common/charCode';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import { ILine, RenderedLinesCollection } from 'vs/editor/browser/view/viewLayer';
import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart';
import { RenderMinimap, EditorOption, MINIMAP_GUTTER_WIDTH, EditorLayoutInfoComputer } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { RGBA8 } from 'vs/editor/common/core/rgba';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
import { ColorId } from 'vs/editor/common/encodedTokenAttributes';
import { MinimapCharRenderer } from 'vs/editor/browser/viewParts/minimap/minimapCharRenderer';
import { Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet';
import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import { EditorTheme } from 'vs/editor/common/editorTheme';
import * as viewEvents from 'vs/editor/common/viewEvents';
import { ViewLineData, ViewModelDecoration } from 'vs/editor/common/viewModel';
import { minimapSelection, minimapBackground, minimapForegroundOpacity, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel';
import { Selection } from 'vs/editor/common/core/selection';
import { Color } from 'vs/base/common/color';
import { GestureEvent, EventType, Gesture } from 'vs/base/browser/touch';
import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory';
import { MinimapPosition, MinimapSectionHeaderStyle, TextModelResolvedOptions } from 'vs/editor/common/model';
import { createSingleCallFunction } from 'vs/base/common/functional';
import { LRUCache } from 'vs/base/common/map';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
*/
const POINTER_DRAG_RESET_DISTANCE = 140;
const GUTTER_DECORATION_WIDTH = 2;
class MinimapOptions {
public readonly renderMinimap: RenderMinimap;
public readonly size: 'proportional' | 'fill' | 'fit';
public readonly minimapHeightIsEditorHeight: boolean;
public readonly scrollBeyondLastLine: boolean;
public readonly paddingTop: number;
public readonly paddingBottom: number;
public readonly showSlider: 'always' | 'mouseover';
public readonly autohide: boolean;
public readonly pixelRatio: number;
public readonly typicalHalfwidthCharacterWidth: number;
public readonly lineHeight: number;
/**
* container dom node left position (in CSS px)
*/
public readonly minimapLeft: number;
/**
* container dom node width (in CSS px)
*/
public readonly minimapWidth: number;
/**
* container dom node height (in CSS px)
*/
public readonly minimapHeight: number;
/**
* canvas backing store width (in device px)
*/
public readonly canvasInnerWidth: number;
/**
* canvas backing store height (in device px)
*/
public readonly canvasInnerHeight: number;
/**
* canvas width (in CSS px)
*/
public readonly canvasOuterWidth: number;
/**
* canvas height (in CSS px)
*/
public readonly canvasOuterHeight: number;
public readonly isSampling: boolean;
public readonly editorHeight: number;
public readonly fontScale: number;
public readonly minimapLineHeight: number;
public readonly minimapCharWidth: number;
public readonly sectionHeaderFontFamily: string;
public readonly sectionHeaderFontSize: number;
/**
* Space in between the characters of the section header (in CSS px)
*/
public readonly sectionHeaderLetterSpacing: number;
public readonly sectionHeaderFontColor: RGBA8;
public readonly charRenderer: () => MinimapCharRenderer;
public readonly defaultBackgroundColor: RGBA8;
public readonly backgroundColor: RGBA8;
/**
* foreground alpha: integer in [0-255]
*/
public readonly foregroundAlpha: number;
constructor(configuration: IEditorConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) {
const options = configuration.options;
const pixelRatio = options.get(EditorOption.pixelRatio);
const layoutInfo = options.get(EditorOption.layoutInfo);
const minimapLayout = layoutInfo.minimap;
const fontInfo = options.get(EditorOption.fontInfo);
const minimapOpts = options.get(EditorOption.minimap);
this.renderMinimap = minimapLayout.renderMinimap;
this.size = minimapOpts.size;
this.minimapHeightIsEditorHeight = minimapLayout.minimapHeightIsEditorHeight;
this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine);
this.paddingTop = options.get(EditorOption.padding).top;
this.paddingBottom = options.get(EditorOption.padding).bottom;
this.showSlider = minimapOpts.showSlider;
this.autohide = minimapOpts.autohide;
this.pixelRatio = pixelRatio;
this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
this.lineHeight = options.get(EditorOption.lineHeight);
this.minimapLeft = minimapLayout.minimapLeft;
this.minimapWidth = minimapLayout.minimapWidth;
this.minimapHeight = layoutInfo.height;
this.canvasInnerWidth = minimapLayout.minimapCanvasInnerWidth;
this.canvasInnerHeight = minimapLayout.minimapCanvasInnerHeight;
this.canvasOuterWidth = minimapLayout.minimapCanvasOuterWidth;
this.canvasOuterHeight = minimapLayout.minimapCanvasOuterHeight;
this.isSampling = minimapLayout.minimapIsSampling;
this.editorHeight = layoutInfo.height;
this.fontScale = minimapLayout.minimapScale;
this.minimapLineHeight = minimapLayout.minimapLineHeight;
this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale;
this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY;
this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio;
this.sectionHeaderLetterSpacing = minimapOpts.sectionHeaderLetterSpacing; // intentionally not multiplying by pixelRatio
this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground));
this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily));
this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground);
this.backgroundColor = MinimapOptions._getMinimapBackground(theme, this.defaultBackgroundColor);
this.foregroundAlpha = MinimapOptions._getMinimapForegroundOpacity(theme);
}
private static _getMinimapBackground(theme: EditorTheme, defaultBackgroundColor: RGBA8): RGBA8 {
const themeColor = theme.getColor(minimapBackground);
if (themeColor) {
return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));
}
return defaultBackgroundColor;
}
private static _getMinimapForegroundOpacity(theme: EditorTheme): number {
const themeColor = theme.getColor(minimapForegroundOpacity);
if (themeColor) {
return RGBA8._clamp(Math.round(255 * themeColor.rgba.a));
}
return 255;
}
private static _getSectionHeaderColor(theme: EditorTheme, defaultForegroundColor: RGBA8): RGBA8 {
const themeColor = theme.getColor(editorForeground);
if (themeColor) {
return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));
}
return defaultForegroundColor;
}
public equals(other: MinimapOptions): boolean {
return (this.renderMinimap === other.renderMinimap
&& this.size === other.size
&& this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight
&& this.scrollBeyondLastLine === other.scrollBeyondLastLine
&& this.paddingTop === other.paddingTop
&& this.paddingBottom === other.paddingBottom
&& this.showSlider === other.showSlider
&& this.autohide === other.autohide
&& this.pixelRatio === other.pixelRatio
&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth
&& this.lineHeight === other.lineHeight
&& this.minimapLeft === other.minimapLeft
&& this.minimapWidth === other.minimapWidth
&& this.minimapHeight === other.minimapHeight
&& this.canvasInnerWidth === other.canvasInnerWidth
&& this.canvasInnerHeight === other.canvasInnerHeight
&& this.canvasOuterWidth === other.canvasOuterWidth
&& this.canvasOuterHeight === other.canvasOuterHeight
&& this.isSampling === other.isSampling
&& this.editorHeight === other.editorHeight
&& this.fontScale === other.fontScale
&& this.minimapLineHeight === other.minimapLineHeight
&& this.minimapCharWidth === other.minimapCharWidth
&& this.sectionHeaderFontSize === other.sectionHeaderFontSize
&& this.sectionHeaderLetterSpacing === other.sectionHeaderLetterSpacing
&& this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor)
&& this.backgroundColor && this.backgroundColor.equals(other.backgroundColor)
&& this.foregroundAlpha === other.foregroundAlpha
);
}
}
class MinimapLayout {
constructor(
/**
* The given editor scrollTop (input).
*/
public readonly scrollTop: number,
/**
* The given editor scrollHeight (input).
*/
public readonly scrollHeight: number,
public readonly sliderNeeded: boolean,
private readonly _computedSliderRatio: number,
/**
* slider dom node top (in CSS px)
*/
public readonly sliderTop: number,
/**
* slider dom node height (in CSS px)
*/
public readonly sliderHeight: number,
/**
* empty lines to reserve at the top of the minimap.
*/
public readonly topPaddingLineCount: number,
/**
* minimap render start line number.
*/
public readonly startLineNumber: number,
/**
* minimap render end line number.
*/
public readonly endLineNumber: number
) { }
/**
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
*/
public getDesiredScrollTopFromDelta(delta: number): number {
return Math.round(this.scrollTop + delta / this._computedSliderRatio);
}
public getDesiredScrollTopFromTouchLocation(pageY: number): number {
return Math.round((pageY - this.sliderHeight / 2) / this._computedSliderRatio);
}
/**
* Intersect a line range with `this.startLineNumber` and `this.endLineNumber`.
*/
public intersectWithViewport(range: Range): [number, number] | null {
const startLineNumber = Math.max(this.startLineNumber, range.startLineNumber);
const endLineNumber = Math.min(this.endLineNumber, range.endLineNumber);
if (startLineNumber > endLineNumber) {
// entirely outside minimap's viewport
return null;
}
return [startLineNumber, endLineNumber];
}
/**
* Get the inner minimap y coordinate for a line number.
*/
public getYForLineNumber(lineNumber: number, minimapLineHeight: number): number {
return + (lineNumber - this.startLineNumber + this.topPaddingLineCount) * minimapLineHeight;
}
public static create(
options: MinimapOptions,
viewportStartLineNumber: number,
viewportEndLineNumber: number,
viewportStartLineNumberVerticalOffset: number,
viewportHeight: number,
viewportContainsWhitespaceGaps: boolean,
lineCount: number,
realLineCount: number,
scrollTop: number,
scrollHeight: number,
previousLayout: MinimapLayout | null
): MinimapLayout {
const pixelRatio = options.pixelRatio;
const minimapLineHeight = options.minimapLineHeight;
const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
const lineHeight = options.lineHeight;
if (options.minimapHeightIsEditorHeight) {
let logicalScrollHeight = (
realLineCount * options.lineHeight
+ options.paddingTop
+ options.paddingBottom
);
if (options.scrollBeyondLastLine) {
logicalScrollHeight += Math.max(0, viewportHeight - options.lineHeight - options.paddingBottom);
}
const sliderHeight = Math.max(1, Math.floor(viewportHeight * viewportHeight / logicalScrollHeight));
const maxMinimapSliderTop = Math.max(0, options.minimapHeight - sliderHeight);
// The slider can move from 0 to `maxMinimapSliderTop`
// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.
const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);
const sliderTop = (scrollTop * computedSliderRatio);
const sliderNeeded = (maxMinimapSliderTop > 0);
const maxLinesFitting = Math.floor(options.canvasInnerHeight / options.minimapLineHeight);
const topPaddingLineCount = Math.floor(options.paddingTop / options.lineHeight);
return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, topPaddingLineCount, 1, Math.min(lineCount, maxLinesFitting));
}
// The visible line count in a viewport can change due to a number of reasons:
// a) with the same viewport width, different scroll positions can result in partial lines being visible:
// e.g. for a line height of 20, and a viewport height of 600
// * scrollTop = 0 => visible lines are [1, 30]
// * scrollTop = 10 => visible lines are [1, 31] (with lines 1 and 31 partially visible)
// * scrollTop = 20 => visible lines are [2, 31]
// b) whitespace gaps might make their way in the viewport (which results in a decrease in the visible line count)
// 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)
// We must first establish a desirable slider height.
let sliderHeight: number;
if (viewportContainsWhitespaceGaps && viewportEndLineNumber !== lineCount) {
// case b) from above: there are whitespace gaps in the viewport.
// In this case, the height of the slider directly reflects the visible line count.
const viewportLineCount = viewportEndLineNumber - viewportStartLineNumber + 1;
sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
} else {
// The slider has a stable height
const expectedViewportLineCount = viewportHeight / lineHeight;
sliderHeight = Math.floor(expectedViewportLineCount * minimapLineHeight / pixelRatio);
}
const extraLinesAtTheTop = Math.floor(options.paddingTop / lineHeight);
let extraLinesAtTheBottom = Math.floor(options.paddingBottom / lineHeight);
if (options.scrollBeyondLastLine) {
const expectedViewportLineCount = viewportHeight / lineHeight;
extraLinesAtTheBottom = Math.max(extraLinesAtTheBottom, expectedViewportLineCount - 1);
}
let maxMinimapSliderTop: number;
if (extraLinesAtTheBottom > 0) {
const expectedViewportLineCount = viewportHeight / lineHeight;
// The minimap slider, when dragged all the way down, will contain the last line at its top
maxMinimapSliderTop = (extraLinesAtTheTop + lineCount + extraLinesAtTheBottom - expectedViewportLineCount - 1) * minimapLineHeight / pixelRatio;
} else {
// The minimap slider, when dragged all the way down, will contain the last line at its bottom
maxMinimapSliderTop = Math.max(0, (extraLinesAtTheTop + lineCount) * minimapLineHeight / pixelRatio - sliderHeight);
}
maxMinimapSliderTop = Math.min(options.minimapHeight - sliderHeight, maxMinimapSliderTop);
// The slider can move from 0 to `maxMinimapSliderTop`
// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.
const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);
const sliderTop = (scrollTop * computedSliderRatio);
if (minimapLinesFitting >= extraLinesAtTheTop + lineCount + extraLinesAtTheBottom) {
// All lines fit in the minimap
const sliderNeeded = (maxMinimapSliderTop > 0);
return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, extraLinesAtTheTop, 1, lineCount);
} else {
let consideringStartLineNumber: number;
if (viewportStartLineNumber > 1) {
consideringStartLineNumber = viewportStartLineNumber + extraLinesAtTheTop;
} else {
consideringStartLineNumber = Math.max(1, scrollTop / lineHeight);
}
let topPaddingLineCount: number;
let startLineNumber = Math.max(1, Math.floor(consideringStartLineNumber - sliderTop * pixelRatio / minimapLineHeight));
if (startLineNumber < extraLinesAtTheTop) {
topPaddingLineCount = extraLinesAtTheTop - startLineNumber + 1;
startLineNumber = 1;
} else {
topPaddingLineCount = 0;
startLineNumber = Math.max(1, startLineNumber - extraLinesAtTheTop);
}
// Avoid flickering caused by a partial viewport start line
// by being consistent w.r.t. the previous layout decision
if (previousLayout && previousLayout.scrollHeight === scrollHeight) {
if (previousLayout.scrollTop > scrollTop) {
// Scrolling up => never increase `startLineNumber`
startLineNumber = Math.min(startLineNumber, previousLayout.startLineNumber);
topPaddingLineCount = Math.max(topPaddingLineCount, previousLayout.topPaddingLineCount);
}
if (previousLayout.scrollTop < scrollTop) {
// Scrolling down => never decrease `startLineNumber`
startLineNumber = Math.max(startLineNumber, previousLayout.startLineNumber);
topPaddingLineCount = Math.min(topPaddingLineCount, previousLayout.topPaddingLineCount);
}
}
const endLineNumber = Math.min(lineCount, startLineNumber - topPaddingLineCount + minimapLinesFitting - 1);
const partialLine = (scrollTop - viewportStartLineNumberVerticalOffset) / lineHeight;
let sliderTopAligned: number;
if (scrollTop >= options.paddingTop) {
sliderTopAligned = (viewportStartLineNumber - startLineNumber + topPaddingLineCount + partialLine) * minimapLineHeight / pixelRatio;
} else {
sliderTopAligned = (scrollTop / options.paddingTop) * (topPaddingLineCount + partialLine) * minimapLineHeight / pixelRatio;
}
return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTopAligned, sliderHeight, topPaddingLineCount, startLineNumber, endLineNumber);
}
}
}
class MinimapLine implements ILine {
public static readonly INVALID = new MinimapLine(-1);
dy: number;
constructor(dy: number) {
this.dy = dy;
}
public onContentChanged(): void {
this.dy = -1;
}
public onTokensChanged(): void {
this.dy = -1;
}
}
class RenderData {
/**
* last rendered layout.
*/
public readonly renderedLayout: MinimapLayout;
private readonly _imageData: ImageData;
private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;
constructor(
renderedLayout: MinimapLayout,
imageData: ImageData,
lines: MinimapLine[]
) {
this.renderedLayout = renderedLayout;
this._imageData = imageData;
this._renderedLines = new RenderedLinesCollection(
() => MinimapLine.INVALID
);
this._renderedLines._set(renderedLayout.startLineNumber, lines);
}
/**
* Check if the current RenderData matches accurately the new desired layout and no painting is needed.
*/
public linesEquals(layout: MinimapLayout): boolean {
if (!this.scrollEquals(layout)) {
return false;
}
const tmp = this._renderedLines._get();
const lines = tmp.lines;
for (let i = 0, len = lines.length; i < len; i++) {
if (lines[i].dy === -1) {
// This line is invalid
return false;
}
}
return true;
}
/**
* Check if the current RenderData matches the new layout's scroll position
*/
public scrollEquals(layout: MinimapLayout): boolean {
return this.renderedLayout.startLineNumber === layout.startLineNumber
&& this.renderedLayout.endLineNumber === layout.endLineNumber;
}
_get(): { imageData: ImageData; rendLineNumberStart: number; lines: MinimapLine[] } {
const tmp = this._renderedLines._get();
return {
imageData: this._imageData,
rendLineNumberStart: tmp.rendLineNumberStart,
lines: tmp.lines
};
}
public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {
return this._renderedLines.onLinesChanged(changeFromLineNumber, changeCount);
}
public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): void {
this._renderedLines.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber);
}
public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): void {
this._renderedLines.onLinesInserted(insertFromLineNumber, insertToLineNumber);
}
public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {
return this._renderedLines.onTokensChanged(ranges);
}
}
/**
* Some sort of double buffering.
*
* Keeps two buffers around that will be rotated for painting.
* Always gives a buffer that is filled with the background color.
*/
class MinimapBuffers {
private readonly _backgroundFillData: Uint8ClampedArray;
private readonly _buffers: [ImageData, ImageData];
private _lastUsedBuffer: number;
constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA8) {
this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background);
this._buffers = [
ctx.createImageData(WIDTH, HEIGHT),
ctx.createImageData(WIDTH, HEIGHT)
];
this._lastUsedBuffer = 0;
}
public getBuffer(): ImageData {
// rotate buffers
this._lastUsedBuffer = 1 - this._lastUsedBuffer;
const result = this._buffers[this._lastUsedBuffer];
// fill with background color
result.data.set(this._backgroundFillData);
return result;
}
private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA8): Uint8ClampedArray {
const backgroundR = background.r;
const backgroundG = background.g;
const backgroundB = background.b;
const backgroundA = background.a;
const result = new Uint8ClampedArray(WIDTH * HEIGHT * 4);
let offset = 0;
for (let i = 0; i < HEIGHT; i++) {
for (let j = 0; j < WIDTH; j++) {
result[offset] = backgroundR;
result[offset + 1] = backgroundG;
result[offset + 2] = backgroundB;
result[offset + 3] = backgroundA;
offset += 4;
}
}
return result;
}
}
export interface IMinimapModel {
readonly tokensColorTracker: MinimapTokensColorTracker;
readonly options: MinimapOptions;
getLineCount(): number;
getRealLineCount(): number;
getLineContent(lineNumber: number): string;
getLineMaxColumn(lineNumber: number): number;
getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[];
getSelections(): Selection[];
getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];
getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];
getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null;
getOptions(): TextModelResolvedOptions;
revealLineNumber(lineNumber: number): void;
setScrollTop(scrollTop: number): void;
}
interface IMinimapRenderingContext {
readonly viewportContainsWhitespaceGaps: boolean;
readonly scrollWidth: number;
readonly scrollHeight: number;
readonly viewportStartLineNumber: number;
readonly viewportEndLineNumber: number;
readonly viewportStartLineNumberVerticalOffset: number;
readonly scrollTop: number;
readonly scrollLeft: number;
readonly viewportWidth: number;
readonly viewportHeight: number;
}
interface SamplingStateLinesDeletedEvent {
type: 'deleted';
_oldIndex: number;
deleteFromLineNumber: number;
deleteToLineNumber: number;
}
interface SamplingStateLinesInsertedEvent {
type: 'inserted';
_i: number;
insertFromLineNumber: number;
insertToLineNumber: number;
}
interface SamplingStateFlushEvent {
type: 'flush';
}
type SamplingStateEvent = SamplingStateLinesInsertedEvent | SamplingStateLinesDeletedEvent | SamplingStateFlushEvent;
class MinimapSamplingState {
public static compute(options: MinimapOptions, viewLineCount: number, oldSamplingState: MinimapSamplingState | null): [MinimapSamplingState | null, SamplingStateEvent[]] {
if (options.renderMinimap === RenderMinimap.None || !options.isSampling) {
return [null, []];
}
// ratio is intentionally not part of the layout to avoid the layout changing all the time
// so we need to recompute it again...
const { minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({
viewLineCount: viewLineCount,
scrollBeyondLastLine: options.scrollBeyondLastLine,
paddingTop: options.paddingTop,
paddingBottom: options.paddingBottom,
height: options.editorHeight,
lineHeight: options.lineHeight,
pixelRatio: options.pixelRatio
});
const ratio = viewLineCount / minimapLineCount;
const halfRatio = ratio / 2;
if (!oldSamplingState || oldSamplingState.minimapLines.length === 0) {
const result: number[] = [];
result[0] = 1;
if (minimapLineCount > 1) {
for (let i = 0, lastIndex = minimapLineCount - 1; i < lastIndex; i++) {
result[i] = Math.round(i * ratio + halfRatio);
}
result[minimapLineCount - 1] = viewLineCount;
}
return [new MinimapSamplingState(ratio, result), []];
}
const oldMinimapLines = oldSamplingState.minimapLines;
const oldLength = oldMinimapLines.length;
const result: number[] = [];
let oldIndex = 0;
let oldDeltaLineCount = 0;
let minViewLineNumber = 1;
const MAX_EVENT_COUNT = 10; // generate at most 10 events, if there are more than 10 changes, just flush all previous data
let events: SamplingStateEvent[] = [];
let lastEvent: SamplingStateEvent | null = null;
for (let i = 0; i < minimapLineCount; i++) {
const fromViewLineNumber = Math.max(minViewLineNumber, Math.round(i * ratio));
const toViewLineNumber = Math.max(fromViewLineNumber, Math.round((i + 1) * ratio));
while (oldIndex < oldLength && oldMinimapLines[oldIndex] < fromViewLineNumber) {
if (events.length < MAX_EVENT_COUNT) {
const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;
if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) {
lastEvent.deleteToLineNumber++;
} else {
lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber };
events.push(lastEvent);
}
oldDeltaLineCount--;
}
oldIndex++;
}
let selectedViewLineNumber: number;
if (oldIndex < oldLength && oldMinimapLines[oldIndex] <= toViewLineNumber) {
// reuse the old sampled line
selectedViewLineNumber = oldMinimapLines[oldIndex];
oldIndex++;
} else {
if (i === 0) {
selectedViewLineNumber = 1;
} else if (i + 1 === minimapLineCount) {
selectedViewLineNumber = viewLineCount;
} else {
selectedViewLineNumber = Math.round(i * ratio + halfRatio);
}
if (events.length < MAX_EVENT_COUNT) {
const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;
if (lastEvent && lastEvent.type === 'inserted' && lastEvent._i === i - 1) {
lastEvent.insertToLineNumber++;
} else {
lastEvent = { type: 'inserted', _i: i, insertFromLineNumber: oldMinimapLineNumber, insertToLineNumber: oldMinimapLineNumber };
events.push(lastEvent);
}
oldDeltaLineCount++;
}
}
result[i] = selectedViewLineNumber;
minViewLineNumber = selectedViewLineNumber;
}
if (events.length < MAX_EVENT_COUNT) {
while (oldIndex < oldLength) {
const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;
if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) {
lastEvent.deleteToLineNumber++;
} else {
lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber };
events.push(lastEvent);
}
oldDeltaLineCount--;
oldIndex++;
}
} else {
// too many events, just give up
events = [{ type: 'flush' }];
}
return [new MinimapSamplingState(ratio, result), events];
}
constructor(
public readonly samplingRatio: number,
public readonly minimapLines: number[] // a map of 0-based minimap line indexes to 1-based view line numbers
) {
}
public modelLineToMinimapLine(lineNumber: number): number {
return Math.min(this.minimapLines.length, Math.max(1, Math.round(lineNumber / this.samplingRatio)));
}
/**
* Will return null if the model line ranges are not intersecting with a sampled model line.
*/
public modelLineRangeToMinimapLineRange(fromLineNumber: number, toLineNumber: number): [number, number] | null {
let fromLineIndex = this.modelLineToMinimapLine(fromLineNumber) - 1;
while (fromLineIndex > 0 && this.minimapLines[fromLineIndex - 1] >= fromLineNumber) {
fromLineIndex--;
}
let toLineIndex = this.modelLineToMinimapLine(toLineNumber) - 1;
while (toLineIndex + 1 < this.minimapLines.length && this.minimapLines[toLineIndex + 1] <= toLineNumber) {
toLineIndex++;
}
if (fromLineIndex === toLineIndex) {
const sampledLineNumber = this.minimapLines[fromLineIndex];
if (sampledLineNumber < fromLineNumber || sampledLineNumber > toLineNumber) {
// This line is not part of the sampled lines ==> nothing to do
return null;
}
}
return [fromLineIndex + 1, toLineIndex + 1];
}
/**
* Will always return a range, even if it is not intersecting with a sampled model line.
*/
public decorationLineRangeToMinimapLineRange(startLineNumber: number, endLineNumber: number): [number, number] {
let minimapLineStart = this.modelLineToMinimapLine(startLineNumber);
let minimapLineEnd = this.modelLineToMinimapLine(endLineNumber);
if (startLineNumber !== endLineNumber && minimapLineEnd === minimapLineStart) {
if (minimapLineEnd === this.minimapLines.length) {
if (minimapLineStart > 1) {
minimapLineStart--;
}
} else {
minimapLineEnd++;
}
}
return [minimapLineStart, minimapLineEnd];
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): [number, number] {
// have the mapping be sticky
const deletedLineCount = e.toLineNumber - e.fromLineNumber + 1;
let changeStartIndex = this.minimapLines.length;
let changeEndIndex = 0;
for (let i = this.minimapLines.length - 1; i >= 0; i--) {
if (this.minimapLines[i] < e.fromLineNumber) {
break;
}
if (this.minimapLines[i] <= e.toLineNumber) {
// this line got deleted => move to previous available
this.minimapLines[i] = Math.max(1, e.fromLineNumber - 1);
changeStartIndex = Math.min(changeStartIndex, i);
changeEndIndex = Math.max(changeEndIndex, i);
} else {
this.minimapLines[i] -= deletedLineCount;
}
}
return [changeStartIndex, changeEndIndex];
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {
// have the mapping be sticky
const insertedLineCount = e.toLineNumber - e.fromLineNumber + 1;
for (let i = this.minimapLines.length - 1; i >= 0; i--) {
if (this.minimapLines[i] < e.fromLineNumber) {
break;
}
this.minimapLines[i] += insertedLineCount;
}
}
}
export class Minimap extends ViewPart implements IMinimapModel {
public readonly tokensColorTracker: MinimapTokensColorTracker;
private _selections: Selection[];
private _minimapSelections: Selection[] | null;
public options: MinimapOptions;
private _samplingState: MinimapSamplingState | null;
private _shouldCheckSampling: boolean;
private _sectionHeaderCache = new LRUCache<string, string>(10, 1.5);
private _actual: InnerMinimap;
constructor(context: ViewContext) {
super(context);
this.tokensColorTracker = MinimapTokensColorTracker.getInstance();
this._selections = [];
this._minimapSelections = null;
this.options = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker);
const [samplingState,] = MinimapSamplingState.compute(this.options, this._context.viewModel.getLineCount(), null);
this._samplingState = samplingState;
this._shouldCheckSampling = false;
this._actual = new InnerMinimap(context.theme, this);
}
public override dispose(): void {
this._actual.dispose();
super.dispose();
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._actual.getDomNode();
}
private _onOptionsMaybeChanged(): boolean {
const opts = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker);
if (this.options.equals(opts)) {
return false;
}
this.options = opts;
this._recreateLineSampling();
this._actual.onDidChangeOptions();
return true;
}
// ---- begin view event handlers
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
return this._onOptionsMaybeChanged();
}
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._selections = e.selections;
this._minimapSelections = null;
return this._actual.onSelectionChanged();
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
if (e.affectsMinimap) {
return this._actual.onDecorationsChanged();
}
return false;
}
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
if (this._samplingState) {
this._shouldCheckSampling = true;
}
return this._actual.onFlushed();
}
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
if (this._samplingState) {
const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(e.fromLineNumber, e.fromLineNumber + e.count - 1);
if (minimapLineRange) {
return this._actual.onLinesChanged(minimapLineRange[0], minimapLineRange[1] - minimapLineRange[0] + 1);
} else {
return false;
}
} else {
return this._actual.onLinesChanged(e.fromLineNumber, e.count);
}
}
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
if (this._samplingState) {
const [changeStartIndex, changeEndIndex] = this._samplingState.onLinesDeleted(e);
if (changeStartIndex <= changeEndIndex) {
this._actual.onLinesChanged(changeStartIndex + 1, changeEndIndex - changeStartIndex + 1);
}
this._shouldCheckSampling = true;
return true;
} else {
return this._actual.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
}
}
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
if (this._samplingState) {
this._samplingState.onLinesInserted(e);
this._shouldCheckSampling = true;
return true;
} else {
return this._actual.onLinesInserted(e.fromLineNumber, e.toLineNumber);
}
}
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return this._actual.onScrollChanged();
}
public override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
this._actual.onThemeChanged();
this._onOptionsMaybeChanged();
return true;
}
public override onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
if (this._samplingState) {
const ranges: { fromLineNumber: number; toLineNumber: number }[] = [];
for (const range of e.ranges) {
const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(range.fromLineNumber, range.toLineNumber);
if (minimapLineRange) {
ranges.push({ fromLineNumber: minimapLineRange[0], toLineNumber: minimapLineRange[1] });
}
}
if (ranges.length) {
return this._actual.onTokensChanged(ranges);
} else {
return false;
}
} else {
return this._actual.onTokensChanged(e.ranges);
}
}
public override onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {
this._onOptionsMaybeChanged();
return this._actual.onTokensColorsChanged();
}
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return this._actual.onZonesChanged();
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
if (this._shouldCheckSampling) {
this._shouldCheckSampling = false;
this._recreateLineSampling();
}
}
public render(ctx: RestrictedRenderingContext): void {
let viewportStartLineNumber = ctx.visibleRange.startLineNumber;
let viewportEndLineNumber = ctx.visibleRange.endLineNumber;
if (this._samplingState) {
viewportStartLineNumber = this._samplingState.modelLineToMinimapLine(viewportStartLineNumber);
viewportEndLineNumber = this._samplingState.modelLineToMinimapLine(viewportEndLineNumber);
}
const minimapCtx: IMinimapRenderingContext = {
viewportContainsWhitespaceGaps: (ctx.viewportData.whitespaceViewportData.length > 0),
scrollWidth: ctx.scrollWidth,
scrollHeight: ctx.scrollHeight,
viewportStartLineNumber: viewportStartLineNumber,
viewportEndLineNumber: viewportEndLineNumber,
viewportStartLineNumberVerticalOffset: ctx.getVerticalOffsetForLineNumber(viewportStartLineNumber),
scrollTop: ctx.scrollTop,
scrollLeft: ctx.scrollLeft,
viewportWidth: ctx.viewportWidth,
viewportHeight: ctx.viewportHeight,
};
this._actual.render(minimapCtx);
}
//#region IMinimapModel
private _recreateLineSampling(): void {
this._minimapSelections = null;
const wasSampling = Boolean(this._samplingState);
const [samplingState, events] = MinimapSamplingState.compute(this.options, this._context.viewModel.getLineCount(), this._samplingState);
this._samplingState = samplingState;
if (wasSampling && this._samplingState) {
// was sampling, is sampling
for (const event of events) {
switch (event.type) {
case 'deleted':
this._actual.onLinesDeleted(event.deleteFromLineNumber, event.deleteToLineNumber);
break;
case 'inserted':
this._actual.onLinesInserted(event.insertFromLineNumber, event.insertToLineNumber);
break;
case 'flush':
this._actual.onFlushed();
break;
}
}
}
}
public getLineCount(): number {
if (this._samplingState) {
return this._samplingState.minimapLines.length;
}
return this._context.viewModel.getLineCount();
}
public getRealLineCount(): number {
return this._context.viewModel.getLineCount();
}
public getLineContent(lineNumber: number): string {
if (this._samplingState) {
return this._context.viewModel.getLineContent(this._samplingState.minimapLines[lineNumber - 1]);
}
return this._context.viewModel.getLineContent(lineNumber);
}
public getLineMaxColumn(lineNumber: number): number {
if (this._samplingState) {
return this._context.viewModel.getLineMaxColumn(this._samplingState.minimapLines[lineNumber - 1]);
}
return this._context.viewModel.getLineMaxColumn(lineNumber);
}
public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[] {
if (this._samplingState) {
const result: (ViewLineData | null)[] = [];
for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
if (needed[lineIndex]) {
result[lineIndex] = this._context.viewModel.getViewLineData(this._samplingState.minimapLines[startLineNumber + lineIndex - 1]);
} else {
result[lineIndex] = null;
}
}
return result;
}
return this._context.viewModel.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed).data;
}
public getSelections(): Selection[] {
if (this._minimapSelections === null) {
if (this._samplingState) {
this._minimapSelections = [];
for (const selection of this._selections) {
const [minimapLineStart, minimapLineEnd] = this._samplingState.decorationLineRangeToMinimapLineRange(selection.startLineNumber, selection.endLineNumber);
this._minimapSelections.push(new Selection(minimapLineStart, selection.startColumn, minimapLineEnd, selection.endColumn));
}
} else {
this._minimapSelections = this._selections;
}
}
return this._minimapSelections;
}
public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {
const decorations = this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)
.filter(decoration => !decoration.options.minimap?.sectionHeaderStyle);
if (this._samplingState) {
const result: ViewModelDecoration[] = [];
for (const decoration of decorations) {
if (!decoration.options.minimap) {
continue;
}
const range = decoration.range;
const minimapStartLineNumber = this._samplingState.modelLineToMinimapLine(range.startLineNumber);
const minimapEndLineNumber = this._samplingState.modelLineToMinimapLine(range.endLineNumber);
result.push(new ViewModelDecoration(new Range(minimapStartLineNumber, range.startColumn, minimapEndLineNumber, range.endColumn), decoration.options));
}
return result;
}
return decorations;
}
public getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {
const minimapLineHeight = this.options.minimapLineHeight;
const sectionHeaderFontSize = this.options.sectionHeaderFontSize;
const headerHeightInMinimapLines = sectionHeaderFontSize / minimapLineHeight;
startLineNumber = Math.floor(Math.max(1, startLineNumber - headerHeightInMinimapLines));
return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)
.filter(decoration => !!decoration.options.minimap?.sectionHeaderStyle);
}
private _getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number) {
let visibleRange: Range;
if (this._samplingState) {
const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1];
const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1];
visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber));
} else {
visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber));
}
return this._context.viewModel.getMinimapDecorationsInRange(visibleRange);
}
public getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null {
const headerText = decoration.options.minimap?.sectionHeaderText;
if (!headerText) {
return null;
}
const cachedText = this._sectionHeaderCache.get(headerText);
if (cachedText) {
return cachedText;
}
const fittedText = fitWidth(headerText);
this._sectionHeaderCache.set(headerText, fittedText);
return fittedText;
}
public getOptions(): TextModelResolvedOptions {
return this._context.viewModel.model.getOptions();
}
public revealLineNumber(lineNumber: number): void {
if (this._samplingState) {
lineNumber = this._samplingState.minimapLines[lineNumber - 1];
}
this._context.viewModel.revealRange(
'mouse',
false,
new Range(lineNumber, 1, lineNumber, 1),
viewEvents.VerticalRevealType.Center,
ScrollType.Smooth
);
}
public setScrollTop(scrollTop: number): void {
this._context.viewModel.viewLayout.setScrollPosition({
scrollTop: scrollTop
}, ScrollType.Immediate);
}
//#endregion
}
class InnerMinimap extends Disposable {
private readonly _theme: EditorTheme;
private readonly _model: IMinimapModel;
private readonly _domNode: FastDomNode<HTMLElement>;
private readonly _shadow: FastDomNode<HTMLElement>;
private readonly _canvas: FastDomNode<HTMLCanvasElement>;
private readonly _decorationsCanvas: FastDomNode<HTMLCanvasElement>;
private readonly _slider: FastDomNode<HTMLElement>;
private readonly _sliderHorizontal: FastDomNode<HTMLElement>;
private readonly _pointerDownListener: IDisposable;
private readonly _sliderPointerMoveMonitor: GlobalPointerMoveMonitor;
private readonly _sliderPointerDownListener: IDisposable;
private readonly _gestureDisposable: IDisposable;
private readonly _sliderTouchStartListener: IDisposable;
private readonly _sliderTouchMoveListener: IDisposable;
private readonly _sliderTouchEndListener: IDisposable;
private _lastRenderData: RenderData | null;
private _selectionColor: Color | undefined;
private _renderDecorations: boolean = false;
private _gestureInProgress: boolean = false;
private _buffers: MinimapBuffers | null;
constructor(
theme: EditorTheme,
model: IMinimapModel
) {
super();
this._theme = theme;
this._model = model;
this._lastRenderData = null;
this._buffers = null;
this._selectionColor = this._theme.getColor(minimapSelection);
this._domNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this._domNode, PartFingerprint.Minimap);
this._domNode.setClassName(this._getMinimapDomNodeClassName());
this._domNode.setPosition('absolute');
this._domNode.setAttribute('role', 'presentation');
this._domNode.setAttribute('aria-hidden', 'true');
this._shadow = createFastDomNode(document.createElement('div'));
this._shadow.setClassName('minimap-shadow-hidden');
this._domNode.appendChild(this._shadow);
this._canvas = createFastDomNode(document.createElement('canvas'));
this._canvas.setPosition('absolute');
this._canvas.setLeft(0);
this._domNode.appendChild(this._canvas);
this._decorationsCanvas = createFastDomNode(document.createElement('canvas'));
this._decorationsCanvas.setPosition('absolute');
this._decorationsCanvas.setClassName('minimap-decorations-layer');
this._decorationsCanvas.setLeft(0);
this._domNode.appendChild(this._decorationsCanvas);
this._slider = createFastDomNode(document.createElement('div'));
this._slider.setPosition('absolute');
this._slider.setClassName('minimap-slider');
this._slider.setLayerHinting(true);
this._slider.setContain('strict');
this._domNode.appendChild(this._slider);
this._sliderHorizontal = createFastDomNode(document.createElement('div'));
this._sliderHorizontal.setPosition('absolute');
this._sliderHorizontal.setClassName('minimap-slider-horizontal');
this._slider.appendChild(this._sliderHorizontal);
this._applyLayout();
this._pointerDownListener = dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.POINTER_DOWN, (e) => {
e.preventDefault();
const renderMinimap = this._model.options.renderMinimap;
if (renderMinimap === RenderMinimap.None) {
return;
}
if (!this._lastRenderData) {
return;
}
if (this._model.options.size !== 'proportional') {
if (e.button === 0 && this._lastRenderData) {
// pretend the click occurred in the center of the slider
const position = dom.getDomNodePagePosition(this._slider.domNode);
const initialPosY = position.top + position.height / 2;
this._startSliderDragging(e, initialPosY, this._lastRenderData.renderedLayout);
}
return;
}
const minimapLineHeight = this._model.options.minimapLineHeight;
const internalOffsetY = (this._model.options.canvasInnerHeight / this._model.options.canvasOuterHeight) * e.offsetY;
const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);
let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber - this._lastRenderData.renderedLayout.topPaddingLineCount;
lineNumber = Math.min(lineNumber, this._model.getLineCount());
this._model.revealLineNumber(lineNumber);
});
this._sliderPointerMoveMonitor = new GlobalPointerMoveMonitor();
this._sliderPointerDownListener = dom.addStandardDisposableListener(this._slider.domNode, dom.EventType.POINTER_DOWN, (e) => {
e.preventDefault();
e.stopPropagation();
if (e.button === 0 && this._lastRenderData) {
this._startSliderDragging(e, e.pageY, this._lastRenderData.renderedLayout);
}
});
this._gestureDisposable = Gesture.addTarget(this._domNode.domNode);
this._sliderTouchStartListener = dom.addDisposableListener(this._domNode.domNode, EventType.Start, (e: GestureEvent) => {
e.preventDefault();
e.stopPropagation();
if (this._lastRenderData) {
this._slider.toggleClassName('active', true);
this._gestureInProgress = true;
this.scrollDueToTouchEvent(e);
}
}, { passive: false });
this._sliderTouchMoveListener = dom.addDisposableListener(this._domNode.domNode, EventType.Change, (e: GestureEvent) => {
e.preventDefault();
e.stopPropagation();
if (this._lastRenderData && this._gestureInProgress) {
this.scrollDueToTouchEvent(e);
}
}, { passive: false });
this._sliderTouchEndListener = dom.addStandardDisposableListener(this._domNode.domNode, EventType.End, (e: GestureEvent) => {
e.preventDefault();
e.stopPropagation();
this._gestureInProgress = false;
this._slider.toggleClassName('active', false);
});
}
private _startSliderDragging(e: PointerEvent, initialPosY: number, initialSliderState: MinimapLayout): void {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const initialPosX = e.pageX;
this._slider.toggleClassName('active', true);
const handlePointerMove = (posy: number, posx: number) => {
const minimapPosition = dom.getDomNodePagePosition(this._domNode.domNode);
const pointerOrthogonalDelta = Math.min(
Math.abs(posx - initialPosX),
Math.abs(posx - minimapPosition.left),
Math.abs(posx - minimapPosition.left - minimapPosition.width)
);
if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
// The pointer has wondered away from the scrollbar => reset dragging
this._model.setScrollTop(initialSliderState.scrollTop);
return;
}
const pointerDelta = posy - initialPosY;
this._model.setScrollTop(initialSliderState.getDesiredScrollTopFromDelta(pointerDelta));
};
if (e.pageY !== initialPosY) {
handlePointerMove(e.pageY, initialPosX);
}
this._sliderPointerMoveMonitor.startMonitoring(
e.target,
e.pointerId,
e.buttons,
pointerMoveData => handlePointerMove(pointerMoveData.pageY, pointerMoveData.pageX),
() => {
this._slider.toggleClassName('active', false);
}
);
}
private scrollDueToTouchEvent(touch: GestureEvent) {
const startY = this._domNode.domNode.getBoundingClientRect().top;
const scrollTop = this._lastRenderData!.renderedLayout.getDesiredScrollTopFromTouchLocation(touch.pageY - startY);
this._model.setScrollTop(scrollTop);
}
public override dispose(): void {
this._pointerDownListener.dispose();
this._sliderPointerMoveMonitor.dispose();
this._sliderPointerDownListener.dispose();
this._gestureDisposable.dispose();
this._sliderTouchStartListener.dispose();
this._sliderTouchMoveListener.dispose();
this._sliderTouchEndListener.dispose();
super.dispose();
}
private _getMinimapDomNodeClassName(): string {
const class_ = ['minimap'];
if (this._model.options.showSlider === 'always') {
class_.push('slider-always');
} else {
class_.push('slider-mouseover');
}
if (this._model.options.autohide) {
class_.push('autohide');
}
return class_.join(' ');
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
private _applyLayout(): void {
this._domNode.setLeft(this._model.options.minimapLeft);
this._domNode.setWidth(this._model.options.minimapWidth);
this._domNode.setHeight(this._model.options.minimapHeight);
this._shadow.setHeight(this._model.options.minimapHeight);
this._canvas.setWidth(this._model.options.canvasOuterWidth);
this._canvas.setHeight(this._model.options.canvasOuterHeight);
this._canvas.domNode.width = this._model.options.canvasInnerWidth;
this._canvas.domNode.height = this._model.options.canvasInnerHeight;
this._decorationsCanvas.setWidth(this._model.options.canvasOuterWidth);
this._decorationsCanvas.setHeight(this._model.options.canvasOuterHeight);
this._decorationsCanvas.domNode.width = this._model.options.canvasInnerWidth;
this._decorationsCanvas.domNode.height = this._model.options.canvasInnerHeight;
this._slider.setWidth(this._model.options.minimapWidth);
}
private _getBuffer(): ImageData | null {
if (!this._buffers) {
if (this._model.options.canvasInnerWidth > 0 && this._model.options.canvasInnerHeight > 0) {
this._buffers = new MinimapBuffers(
this._canvas.domNode.getContext('2d')!,
this._model.options.canvasInnerWidth,
this._model.options.canvasInnerHeight,
this._model.options.backgroundColor
);
}
}
return this._buffers ? this._buffers.getBuffer() : null;
}
// ---- begin view event handlers
public onDidChangeOptions(): void {
this._lastRenderData = null;
this._buffers = null;
this._applyLayout();
this._domNode.setClassName(this._getMinimapDomNodeClassName());
}
public onSelectionChanged(): boolean {
this._renderDecorations = true;
return true;
}
public onDecorationsChanged(): boolean {
this._renderDecorations = true;
return true;
}
public onFlushed(): boolean {
this._lastRenderData = null;
return true;
}
public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {
if (this._lastRenderData) {
return this._lastRenderData.onLinesChanged(changeFromLineNumber, changeCount);
}
return false;
}
public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): boolean {
this._lastRenderData?.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber);
return true;
}
public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): boolean {
this._lastRenderData?.onLinesInserted(insertFromLineNumber, insertToLineNumber);
return true;
}
public onScrollChanged(): boolean {
this._renderDecorations = true;
return true;
}
public onThemeChanged(): boolean {
this._selectionColor = this._theme.getColor(minimapSelection);
this._renderDecorations = true;
return true;
}
public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {
if (this._lastRenderData) {
return this._lastRenderData.onTokensChanged(ranges);
}
return false;
}
public onTokensColorsChanged(): boolean {
this._lastRenderData = null;
this._buffers = null;
return true;
}
public onZonesChanged(): boolean {
this._lastRenderData = null;
return true;
}
// --- end event handlers
public render(renderingCtx: IMinimapRenderingContext): void {
const renderMinimap = this._model.options.renderMinimap;
if (renderMinimap === RenderMinimap.None) {
this._shadow.setClassName('minimap-shadow-hidden');
this._sliderHorizontal.setWidth(0);
this._sliderHorizontal.setHeight(0);
return;
}
if (renderingCtx.scrollLeft + renderingCtx.viewportWidth >= renderingCtx.scrollWidth) {
this._shadow.setClassName('minimap-shadow-hidden');
} else {
this._shadow.setClassName('minimap-shadow-visible');
}
const layout = MinimapLayout.create(
this._model.options,
renderingCtx.viewportStartLineNumber,
renderingCtx.viewportEndLineNumber,
renderingCtx.viewportStartLineNumberVerticalOffset,
renderingCtx.viewportHeight,
renderingCtx.viewportContainsWhitespaceGaps,
this._model.getLineCount(),
this._model.getRealLineCount(),
renderingCtx.scrollTop,
renderingCtx.scrollHeight,
this._lastRenderData ? this._lastRenderData.renderedLayout : null
);
this._slider.setDisplay(layout.sliderNeeded ? 'block' : 'none');
this._slider.setTop(layout.sliderTop);
this._slider.setHeight(layout.sliderHeight);
// Compute horizontal slider coordinates
this._sliderHorizontal.setLeft(0);
this._sliderHorizontal.setWidth(this._model.options.minimapWidth);
this._sliderHorizontal.setTop(0);
this._sliderHorizontal.setHeight(layout.sliderHeight);
this.renderDecorations(layout);
this._lastRenderData = this.renderLines(layout);
}
private renderDecorations(layout: MinimapLayout) {
if (this._renderDecorations) {
this._renderDecorations = false;
const selections = this._model.getSelections();
selections.sort(Range.compareRangesUsingStarts);
const decorations = this._model.getMinimapDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);
decorations.sort((a, b) => (a.options.zIndex || 0) - (b.options.zIndex || 0));
const { canvasInnerWidth, canvasInnerHeight } = this._model.options;
const minimapLineHeight = this._model.options.minimapLineHeight;
const minimapCharWidth = this._model.options.minimapCharWidth;
const tabSize = this._model.getOptions().tabSize;
const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;
canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight);
// We first need to render line highlights and then render decorations on top of those.
// But we need to pick a single color for each line, and use that as a line highlight.
// This needs to be the color of the decoration with the highest `zIndex`, but priority
// is given to the selection.
const highlightedLines = new ContiguousLineMap<boolean>(layout.startLineNumber, layout.endLineNumber, false);
this._renderSelectionLineHighlights(canvasContext, selections, highlightedLines, layout, minimapLineHeight);
this._renderDecorationsLineHighlights(canvasContext, decorations, highlightedLines, layout, minimapLineHeight);
const lineOffsetMap = new ContiguousLineMap<number[] | null>(layout.startLineNumber, layout.endLineNumber, null);
this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);
this._renderDecorationsHighlights(canvasContext, decorations, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);
this._renderSectionHeaders(layout);
}
}
private _renderSelectionLineHighlights(
canvasContext: CanvasRenderingContext2D,
selections: Selection[],
highlightedLines: ContiguousLineMap<boolean>,
layout: MinimapLayout,
minimapLineHeight: number
): void {
if (!this._selectionColor || this._selectionColor.isTransparent()) {
return;
}
canvasContext.fillStyle = this._selectionColor.transparent(0.5).toString();
let y1 = 0;
let y2 = 0;
for (const selection of selections) {
const intersection = layout.intersectWithViewport(selection);
if (!intersection) {
// entirely outside minimap's viewport
continue;
}
const [startLineNumber, endLineNumber] = intersection;
for (let line = startLineNumber; line <= endLineNumber; line++) {
highlightedLines.set(line, true);
}
const yy1 = layout.getYForLineNumber(startLineNumber, minimapLineHeight);
const yy2 = layout.getYForLineNumber(endLineNumber, minimapLineHeight);
if (y2 >= yy1) {
// merge into previous
y2 = yy2;
} else {
if (y2 > y1) {
// flush
canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1);
}
y1 = yy1;
y2 = yy2;
}
}
if (y2 > y1) {
// flush
canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1);
}
}
private _renderDecorationsLineHighlights(
canvasContext: CanvasRenderingContext2D,
decorations: ViewModelDecoration[],
highlightedLines: ContiguousLineMap<boolean>,
layout: MinimapLayout,
minimapLineHeight: number
): void {
const highlightColors = new Map<string, string>();
// Loop backwards to hit first decorations with higher `zIndex`
for (let i = decorations.length - 1; i >= 0; i--) {
const decoration = decorations[i];
const minimapOptions = <ModelDecorationMinimapOptions | null | undefined>decoration.options.minimap;
if (!minimapOptions || minimapOptions.position !== MinimapPosition.Inline) {
continue;
}
const intersection = layout.intersectWithViewport(decoration.range);
if (!intersection) {
// entirely outside minimap's viewport
continue;
}
const [startLineNumber, endLineNumber] = intersection;
const decorationColor = minimapOptions.getColor(this._theme.value);
if (!decorationColor || decorationColor.isTransparent()) {
continue;
}
let highlightColor = highlightColors.get(decorationColor.toString());
if (!highlightColor) {
highlightColor = decorationColor.transparent(0.5).toString();
highlightColors.set(decorationColor.toString(), highlightColor);
}
canvasContext.fillStyle = highlightColor;
for (let line = startLineNumber; line <= endLineNumber; line++) {
if (highlightedLines.has(line)) {
continue;
}
highlightedLines.set(line, true);
const y = layout.getYForLineNumber(startLineNumber, minimapLineHeight);
canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y, canvasContext.canvas.width, minimapLineHeight);
}
}
}
private _renderSelectionsHighlights(
canvasContext: CanvasRenderingContext2D,
selections: Selection[],
lineOffsetMap: ContiguousLineMap<number[] | null>,
layout: MinimapLayout,
lineHeight: number,
tabSize: number,
characterWidth: number,
canvasInnerWidth: number
): void {
if (!this._selectionColor || this._selectionColor.isTransparent()) {
return;
}
for (const selection of selections) {
const intersection = layout.intersectWithViewport(selection);
if (!intersection) {
// entirely outside minimap's viewport
continue;
}
const [startLineNumber, endLineNumber] = intersection;
for (let line = startLineNumber; line <= endLineNumber; line++) {
this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth, canvasInnerWidth);
}
}
}
private _renderDecorationsHighlights(
canvasContext: CanvasRenderingContext2D,
decorations: ViewModelDecoration[],
lineOffsetMap: ContiguousLineMap<number[] | null>,
layout: MinimapLayout,
minimapLineHeight: number,
tabSize: number,
characterWidth: number,
canvasInnerWidth: number
): void {
// Loop forwards to hit first decorations with lower `zIndex`
for (const decoration of decorations) {
const minimapOptions = <ModelDecorationMinimapOptions | null | undefined>decoration.options.minimap;
if (!minimapOptions) {
continue;
}
const intersection = layout.intersectWithViewport(decoration.range);
if (!intersection) {
// entirely outside minimap's viewport
continue;
}
const [startLineNumber, endLineNumber] = intersection;
const decorationColor = minimapOptions.getColor(this._theme.value);
if (!decorationColor || decorationColor.isTransparent()) {
continue;
}
for (let line = startLineNumber; line <= endLineNumber; line++) {
switch (minimapOptions.position) {
case MinimapPosition.Inline:
this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration.range, decorationColor, layout, line, minimapLineHeight, minimapLineHeight, tabSize, characterWidth, canvasInnerWidth);
continue;
case MinimapPosition.Gutter: {
const y = layout.getYForLineNumber(line, minimapLineHeight);
const x = 2;
this.renderDecoration(canvasContext, decorationColor, x, y, GUTTER_DECORATION_WIDTH, minimapLineHeight);
continue;
}
}
}
}
}
private renderDecorationOnLine(
canvasContext: CanvasRenderingContext2D,
lineOffsetMap: ContiguousLineMap<number[] | null>,
decorationRange: Range,
decorationColor: Color | undefined,
layout: MinimapLayout,
lineNumber: number,
height: number,
minimapLineHeight: number,
tabSize: number,
charWidth: number,
canvasInnerWidth: number
): void {
const y = layout.getYForLineNumber(lineNumber, minimapLineHeight);
// Skip rendering the line if it's vertically outside our viewport
if (y + height < 0 || y > this._model.options.canvasInnerHeight) {
return;
}
const { startLineNumber, endLineNumber } = decorationRange;
const startColumn = (startLineNumber === lineNumber ? decorationRange.startColumn : 1);
const endColumn = (endLineNumber === lineNumber ? decorationRange.endColumn : this._model.getLineMaxColumn(lineNumber));
const x1 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, startColumn, tabSize, charWidth, canvasInnerWidth);
const x2 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, endColumn, tabSize, charWidth, canvasInnerWidth);
this.renderDecoration(canvasContext, decorationColor, x1, y, x2 - x1, height);
}
private getXOffsetForPosition(
lineOffsetMap: ContiguousLineMap<number[] | null>,
lineNumber: number,
column: number,
tabSize: number,
charWidth: number,
canvasInnerWidth: number
): number {
if (column === 1) {
return MINIMAP_GUTTER_WIDTH;
}
const minimumXOffset = (column - 1) * charWidth;
if (minimumXOffset >= canvasInnerWidth) {
// there is no need to look at actual characters,
// as this column is certainly after the minimap width
return canvasInnerWidth;
}
// Cache line offset data so that it is only read once per line
let lineIndexToXOffset = lineOffsetMap.get(lineNumber);
if (!lineIndexToXOffset) {
const lineData = this._model.getLineContent(lineNumber);
lineIndexToXOffset = [MINIMAP_GUTTER_WIDTH];
let prevx = MINIMAP_GUTTER_WIDTH;
for (let i = 1; i < lineData.length + 1; i++) {
const charCode = lineData.charCodeAt(i - 1);
const dx = charCode === CharCode.Tab
? tabSize * charWidth
: strings.isFullWidthCharacter(charCode)
? 2 * charWidth
: charWidth;
const x = prevx + dx;
if (x >= canvasInnerWidth) {
// no need to keep on going, as we've hit the canvas width
lineIndexToXOffset[i] = canvasInnerWidth;
break;
}
lineIndexToXOffset[i] = x;
prevx = x;
}
lineOffsetMap.set(lineNumber, lineIndexToXOffset);
}
if (column - 1 < lineIndexToXOffset.length) {
return lineIndexToXOffset[column - 1];
}
// goes over the canvas width
return canvasInnerWidth;
}
private renderDecoration(canvasContext: CanvasRenderingContext2D, decorationColor: Color | undefined, x: number, y: number, width: number, height: number) {
canvasContext.fillStyle = decorationColor && decorationColor.toString() || '';
canvasContext.fillRect(x, y, width, height);
}
private _renderSectionHeaders(layout: MinimapLayout) {
const minimapLineHeight = this._model.options.minimapLineHeight;
const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize;
const sectionHeaderLetterSpacing = this._model.options.sectionHeaderLetterSpacing;
const backgroundFillHeight = sectionHeaderFontSize * 1.5;
const { canvasInnerWidth } = this._model.options;
const backgroundColor = this._model.options.backgroundColor;
const backgroundFill = `rgb(${backgroundColor.r} ${backgroundColor.g} ${backgroundColor.b} / .7)`;
const foregroundColor = this._model.options.sectionHeaderFontColor;
const foregroundFill = `rgb(${foregroundColor.r} ${foregroundColor.g} ${foregroundColor.b})`;
const separatorStroke = foregroundFill;
const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;
canvasContext.letterSpacing = sectionHeaderLetterSpacing + 'px';
canvasContext.font = '500 ' + sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily;
canvasContext.strokeStyle = separatorStroke;
canvasContext.lineWidth = 0.2;
const decorations = this._model.getSectionHeaderDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);
decorations.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber);
const fitWidth = InnerMinimap._fitSectionHeader.bind(null, canvasContext,
canvasInnerWidth - MINIMAP_GUTTER_WIDTH);
for (const decoration of decorations) {
const y = layout.getYForLineNumber(decoration.range.startLineNumber, minimapLineHeight) + sectionHeaderFontSize;
const backgroundFillY = y - sectionHeaderFontSize;
const separatorY = backgroundFillY + 2;
const headerText = this._model.getSectionHeaderText(decoration, fitWidth);
InnerMinimap._renderSectionLabel(
canvasContext,
headerText,
decoration.options.minimap?.sectionHeaderStyle === MinimapSectionHeaderStyle.Underlined,
backgroundFill,
foregroundFill,
canvasInnerWidth,
backgroundFillY,
backgroundFillHeight,
y,
separatorY);
}
}
private static _fitSectionHeader(
target: CanvasRenderingContext2D,
maxWidth: number,
headerText: string,
): string {
if (!headerText) {
return headerText;
}
const ellipsis = '…';
const width = target.measureText(headerText).width;
const ellipsisWidth = target.measureText(ellipsis).width;
if (width <= maxWidth || width <= ellipsisWidth) {
return headerText;
}
const len = headerText.length;
const averageCharWidth = width / headerText.length;
const maxCharCount = Math.floor((maxWidth - ellipsisWidth) / averageCharWidth) - 1;
// Find a halfway point that isn't after whitespace
let halfCharCount = Math.ceil(maxCharCount / 2);
while (halfCharCount > 0 && /\s/.test(headerText[halfCharCount - 1])) {
--halfCharCount;
}
// Split with ellipsis
return headerText.substring(0, halfCharCount)
+ ellipsis + headerText.substring(len - (maxCharCount - halfCharCount));
}
private static _renderSectionLabel(
target: CanvasRenderingContext2D,
headerText: string | null,
hasSeparatorLine: boolean,
backgroundFill: string,
foregroundFill: string,
minimapWidth: number,
backgroundFillY: number,
backgroundFillHeight: number,
textY: number,
separatorY: number
): void {
if (headerText) {
target.fillStyle = backgroundFill;
target.fillRect(0, backgroundFillY, minimapWidth, backgroundFillHeight);
target.fillStyle = foregroundFill;
target.fillText(headerText, MINIMAP_GUTTER_WIDTH, textY);
}
if (hasSeparatorLine) {
target.beginPath();
target.moveTo(0, separatorY);
target.lineTo(minimapWidth, separatorY);
target.closePath();
target.stroke();
}
}
private renderLines(layout: MinimapLayout): RenderData | null {
const startLineNumber = layout.startLineNumber;
const endLineNumber = layout.endLineNumber;
const minimapLineHeight = this._model.options.minimapLineHeight;
// Check if nothing changed w.r.t. lines from last frame
if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {
const _lastData = this._lastRenderData._get();
// Nice!! Nothing changed from last frame
return new RenderData(layout, _lastData.imageData, _lastData.lines);
}
// Oh well!! We need to repaint some lines...
const imageData = this._getBuffer();
if (!imageData) {
// 0 width or 0 height canvas, nothing to do
return null;
}
// Render untouched lines by using last rendered data.
const [_dirtyY1, _dirtyY2, needed] = InnerMinimap._renderUntouchedLines(
imageData,
layout.topPaddingLineCount,
startLineNumber,
endLineNumber,
minimapLineHeight,
this._lastRenderData
);
// Fetch rendering info from view model for rest of lines that need rendering.
const lineInfo = this._model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);
const tabSize = this._model.getOptions().tabSize;
const defaultBackground = this._model.options.defaultBackgroundColor;
const background = this._model.options.backgroundColor;
const foregroundAlpha = this._model.options.foregroundAlpha;
const tokensColorTracker = this._model.tokensColorTracker;
const useLighterFont = tokensColorTracker.backgroundIsLight();
const renderMinimap = this._model.options.renderMinimap;
const charRenderer = this._model.options.charRenderer();
const fontScale = this._model.options.fontScale;
const minimapCharWidth = this._model.options.minimapCharWidth;
const baseCharHeight = (renderMinimap === RenderMinimap.Text ? Constants.BASE_CHAR_HEIGHT : Constants.BASE_CHAR_HEIGHT + 1);
const renderMinimapLineHeight = baseCharHeight * fontScale;
const innerLinePadding = (minimapLineHeight > renderMinimapLineHeight ? Math.floor((minimapLineHeight - renderMinimapLineHeight) / 2) : 0);
// Render the rest of lines
const backgroundA = background.a / 255;
const renderBackground = new RGBA8(
Math.round((background.r - defaultBackground.r) * backgroundA + defaultBackground.r),
Math.round((background.g - defaultBackground.g) * backgroundA + defaultBackground.g),
Math.round((background.b - defaultBackground.b) * backgroundA + defaultBackground.b),
255
);
let dy = layout.topPaddingLineCount * minimapLineHeight;
const renderedLines: MinimapLine[] = [];
for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
if (needed[lineIndex]) {
InnerMinimap._renderLine(
imageData,
renderBackground,
background.a,
useLighterFont,
renderMinimap,
minimapCharWidth,
tokensColorTracker,
foregroundAlpha,
charRenderer,
dy,
innerLinePadding,
tabSize,
lineInfo[lineIndex]!,
fontScale,
minimapLineHeight
);
}
renderedLines[lineIndex] = new MinimapLine(dy);
dy += minimapLineHeight;
}
const dirtyY1 = (_dirtyY1 === -1 ? 0 : _dirtyY1);
const dirtyY2 = (_dirtyY2 === -1 ? imageData.height : _dirtyY2);
const dirtyHeight = dirtyY2 - dirtyY1;
// Finally, paint to the canvas
const ctx = this._canvas.domNode.getContext('2d')!;
ctx.putImageData(imageData, 0, 0, 0, dirtyY1, imageData.width, dirtyHeight);
// Save rendered data for reuse on next frame if possible
return new RenderData(
layout,
imageData,
renderedLines
);
}
private static _renderUntouchedLines(
target: ImageData,
topPaddingLineCount: number,
startLineNumber: number,
endLineNumber: number,
minimapLineHeight: number,
lastRenderData: RenderData | null,
): [number, number, boolean[]] {
const needed: boolean[] = [];
if (!lastRenderData) {
for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {
needed[i] = true;
}
return [-1, -1, needed];
}
const _lastData = lastRenderData._get();
const lastTargetData = _lastData.imageData.data;
const lastStartLineNumber = _lastData.rendLineNumberStart;
const lastLines = _lastData.lines;
const lastLinesLength = lastLines.length;
const WIDTH = target.width;
const targetData = target.data;
const maxDestPixel = (endLineNumber - startLineNumber + 1) * minimapLineHeight * WIDTH * 4;
let dirtyPixel1 = -1; // the pixel offset up to which all the data is equal to the prev frame
let dirtyPixel2 = -1; // the pixel offset after which all the data is equal to the prev frame
let copySourceStart = -1;
let copySourceEnd = -1;
let copyDestStart = -1;
let copyDestEnd = -1;
let dest_dy = topPaddingLineCount * minimapLineHeight;
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const lineIndex = lineNumber - startLineNumber;
const lastLineIndex = lineNumber - lastStartLineNumber;
const source_dy = (lastLineIndex >= 0 && lastLineIndex < lastLinesLength ? lastLines[lastLineIndex].dy : -1);
if (source_dy === -1) {
needed[lineIndex] = true;
dest_dy += minimapLineHeight;
continue;
}
const sourceStart = source_dy * WIDTH * 4;
const sourceEnd = (source_dy + minimapLineHeight) * WIDTH * 4;
const destStart = dest_dy * WIDTH * 4;
const destEnd = (dest_dy + minimapLineHeight) * WIDTH * 4;
if (copySourceEnd === sourceStart && copyDestEnd === destStart) {
// contiguous zone => extend copy request
copySourceEnd = sourceEnd;
copyDestEnd = destEnd;
} else {
if (copySourceStart !== -1) {
// flush existing copy request
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {
dirtyPixel1 = copySourceEnd;
}
if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {
dirtyPixel2 = copySourceStart;
}
}
copySourceStart = sourceStart;
copySourceEnd = sourceEnd;
copyDestStart = destStart;
copyDestEnd = destEnd;
}
needed[lineIndex] = false;
dest_dy += minimapLineHeight;
}
if (copySourceStart !== -1) {
// flush existing copy request
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {
dirtyPixel1 = copySourceEnd;
}
if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {
dirtyPixel2 = copySourceStart;
}
}
const dirtyY1 = (dirtyPixel1 === -1 ? -1 : dirtyPixel1 / (WIDTH * 4));
const dirtyY2 = (dirtyPixel2 === -1 ? -1 : dirtyPixel2 / (WIDTH * 4));
return [dirtyY1, dirtyY2, needed];
}
private static _renderLine(
target: ImageData,
backgroundColor: RGBA8,
backgroundAlpha: number,
useLighterFont: boolean,
renderMinimap: RenderMinimap,
charWidth: number,
colorTracker: MinimapTokensColorTracker,
foregroundAlpha: number,
minimapCharRenderer: MinimapCharRenderer,
dy: number,
innerLinePadding: number,
tabSize: number,
lineData: ViewLineData,
fontScale: number,
minimapLineHeight: number
): void {
const content = lineData.content;
const tokens = lineData.tokens;
const maxDx = target.width - charWidth;
const force1pxHeight = (minimapLineHeight === 1);
let dx = MINIMAP_GUTTER_WIDTH;
let charIndex = 0;
let tabsCharDelta = 0;
for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
const tokenEndIndex = tokens.getEndOffset(tokenIndex);
const tokenColorId = tokens.getForeground(tokenIndex);
const tokenColor = colorTracker.getColor(tokenColorId);
for (; charIndex < tokenEndIndex; charIndex++) {
if (dx > maxDx) {
// hit edge of minimap
return;
}
const charCode = content.charCodeAt(charIndex);
if (charCode === CharCode.Tab) {
const insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
// No need to render anything since tab is invisible
dx += insertSpacesCount * charWidth;
} else if (charCode === CharCode.Space) {
// No need to render anything since space is invisible
dx += charWidth;
} else {
// Render twice for a full width character
const count = strings.isFullWidthCharacter(charCode) ? 2 : 1;
for (let i = 0; i < count; i++) {
if (renderMinimap === RenderMinimap.Blocks) {
minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, force1pxHeight);
} else { // RenderMinimap.Text
minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, fontScale, useLighterFont, force1pxHeight);
}
dx += charWidth;
if (dx > maxDx) {
// hit edge of minimap
return;
}
}
}
}
}
}
}
class ContiguousLineMap<T> {
private readonly _startLineNumber: number;
private readonly _endLineNumber: number;
private readonly _defaultValue: T;
private readonly _values: T[];
constructor(startLineNumber: number, endLineNumber: number, defaultValue: T) {
this._startLineNumber = startLineNumber;
this._endLineNumber = endLineNumber;
this._defaultValue = defaultValue;
this._values = [];
for (let i = 0, count = this._endLineNumber - this._startLineNumber + 1; i < count; i++) {
this._values[i] = defaultValue;
}
}
public has(lineNumber: number): boolean {
return (this.get(lineNumber) !== this._defaultValue);
}
public set(lineNumber: number, value: T): void {
if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) {
return;
}
this._values[lineNumber - this._startLineNumber] = value;
}
public get(lineNumber: number): T {
if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) {
return this._defaultValue;
}
return this._values[lineNumber - this._startLineNumber];
}
}