Path: blob/main/src/vs/editor/browser/viewParts/minimap/minimap.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './minimap.css';6import * as dom from '../../../../base/browser/dom.js';7import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';8import { GlobalPointerMoveMonitor } from '../../../../base/browser/globalPointerMoveMonitor.js';9import { CharCode } from '../../../../base/common/charCode.js';10import { IDisposable, Disposable } from '../../../../base/common/lifecycle.js';11import * as platform from '../../../../base/common/platform.js';12import * as strings from '../../../../base/common/strings.js';13import { ILine, RenderedLinesCollection } from '../../view/viewLayer.js';14import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js';15import { RenderMinimap, EditorOption, MINIMAP_GUTTER_WIDTH, EditorLayoutInfoComputer } from '../../../common/config/editorOptions.js';16import { Range } from '../../../common/core/range.js';17import { RGBA8 } from '../../../common/core/misc/rgba.js';18import { ScrollType } from '../../../common/editorCommon.js';19import { IEditorConfiguration } from '../../../common/config/editorConfiguration.js';20import { ColorId } from '../../../common/encodedTokenAttributes.js';21import { MinimapCharRenderer } from './minimapCharRenderer.js';22import { Constants } from './minimapCharSheet.js';23import { MinimapTokensColorTracker } from '../../../common/viewModel/minimapTokensColorTracker.js';24import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';25import { ViewContext } from '../../../common/viewModel/viewContext.js';26import { EditorTheme } from '../../../common/editorTheme.js';27import * as viewEvents from '../../../common/viewEvents.js';28import { ViewLineData } from '../../../common/viewModel.js';29import { minimapSelection, minimapBackground, minimapForegroundOpacity, editorForeground } from '../../../../platform/theme/common/colorRegistry.js';30import { ModelDecorationMinimapOptions } from '../../../common/model/textModel.js';31import { Selection } from '../../../common/core/selection.js';32import { Color } from '../../../../base/common/color.js';33import { GestureEvent, EventType, Gesture } from '../../../../base/browser/touch.js';34import { MinimapCharRendererFactory } from './minimapCharRendererFactory.js';35import { MinimapPosition, MinimapSectionHeaderStyle, TextModelResolvedOptions } from '../../../common/model.js';36import { createSingleCallFunction } from '../../../../base/common/functional.js';37import { LRUCache } from '../../../../base/common/map.js';38import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js';39import { ViewModelDecoration } from '../../../common/viewModel/viewModelDecoration.js';40import { RunOnceScheduler } from '../../../../base/common/async.js';4142/**43* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"44*/45const POINTER_DRAG_RESET_DISTANCE = 140;4647const GUTTER_DECORATION_WIDTH = 2;4849class MinimapOptions {5051public readonly renderMinimap: RenderMinimap;52public readonly size: 'proportional' | 'fill' | 'fit';53public readonly minimapHeightIsEditorHeight: boolean;54public readonly scrollBeyondLastLine: boolean;55public readonly paddingTop: number;56public readonly paddingBottom: number;57public readonly showSlider: 'always' | 'mouseover';58public readonly autohide: 'none' | 'mouseover' | 'scroll';59public readonly pixelRatio: number;60public readonly typicalHalfwidthCharacterWidth: number;61public readonly lineHeight: number;62/**63* container dom node left position (in CSS px)64*/65public readonly minimapLeft: number;66/**67* container dom node width (in CSS px)68*/69public readonly minimapWidth: number;70/**71* container dom node height (in CSS px)72*/73public readonly minimapHeight: number;74/**75* canvas backing store width (in device px)76*/77public readonly canvasInnerWidth: number;78/**79* canvas backing store height (in device px)80*/81public readonly canvasInnerHeight: number;82/**83* canvas width (in CSS px)84*/85public readonly canvasOuterWidth: number;86/**87* canvas height (in CSS px)88*/89public readonly canvasOuterHeight: number;9091public readonly isSampling: boolean;92public readonly editorHeight: number;93public readonly fontScale: number;94public readonly minimapLineHeight: number;95public readonly minimapCharWidth: number;96public readonly sectionHeaderFontFamily: string;97public readonly sectionHeaderFontSize: number;98/**99* Space in between the characters of the section header (in CSS px)100*/101public readonly sectionHeaderLetterSpacing: number;102public readonly sectionHeaderFontColor: RGBA8;103104public readonly charRenderer: () => MinimapCharRenderer;105public readonly defaultBackgroundColor: RGBA8;106public readonly backgroundColor: RGBA8;107/**108* foreground alpha: integer in [0-255]109*/110public readonly foregroundAlpha: number;111112constructor(configuration: IEditorConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) {113const options = configuration.options;114const pixelRatio = options.get(EditorOption.pixelRatio);115const layoutInfo = options.get(EditorOption.layoutInfo);116const minimapLayout = layoutInfo.minimap;117const fontInfo = options.get(EditorOption.fontInfo);118const minimapOpts = options.get(EditorOption.minimap);119120this.renderMinimap = minimapLayout.renderMinimap;121this.size = minimapOpts.size;122this.minimapHeightIsEditorHeight = minimapLayout.minimapHeightIsEditorHeight;123this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine);124this.paddingTop = options.get(EditorOption.padding).top;125this.paddingBottom = options.get(EditorOption.padding).bottom;126this.showSlider = minimapOpts.showSlider;127this.autohide = minimapOpts.autohide;128this.pixelRatio = pixelRatio;129this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;130this.lineHeight = options.get(EditorOption.lineHeight);131this.minimapLeft = minimapLayout.minimapLeft;132this.minimapWidth = minimapLayout.minimapWidth;133this.minimapHeight = layoutInfo.height;134135this.canvasInnerWidth = minimapLayout.minimapCanvasInnerWidth;136this.canvasInnerHeight = minimapLayout.minimapCanvasInnerHeight;137this.canvasOuterWidth = minimapLayout.minimapCanvasOuterWidth;138this.canvasOuterHeight = minimapLayout.minimapCanvasOuterHeight;139140this.isSampling = minimapLayout.minimapIsSampling;141this.editorHeight = layoutInfo.height;142this.fontScale = minimapLayout.minimapScale;143this.minimapLineHeight = minimapLayout.minimapLineHeight;144this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale;145this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY;146this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio;147this.sectionHeaderLetterSpacing = minimapOpts.sectionHeaderLetterSpacing; // intentionally not multiplying by pixelRatio148this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground));149150this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily));151this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground);152this.backgroundColor = MinimapOptions._getMinimapBackground(theme, this.defaultBackgroundColor);153this.foregroundAlpha = MinimapOptions._getMinimapForegroundOpacity(theme);154}155156private static _getMinimapBackground(theme: EditorTheme, defaultBackgroundColor: RGBA8): RGBA8 {157const themeColor = theme.getColor(minimapBackground);158if (themeColor) {159return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));160}161return defaultBackgroundColor;162}163164private static _getMinimapForegroundOpacity(theme: EditorTheme): number {165const themeColor = theme.getColor(minimapForegroundOpacity);166if (themeColor) {167return RGBA8._clamp(Math.round(255 * themeColor.rgba.a));168}169return 255;170}171172private static _getSectionHeaderColor(theme: EditorTheme, defaultForegroundColor: RGBA8): RGBA8 {173const themeColor = theme.getColor(editorForeground);174if (themeColor) {175return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));176}177return defaultForegroundColor;178}179180public equals(other: MinimapOptions): boolean {181return (this.renderMinimap === other.renderMinimap182&& this.size === other.size183&& this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight184&& this.scrollBeyondLastLine === other.scrollBeyondLastLine185&& this.paddingTop === other.paddingTop186&& this.paddingBottom === other.paddingBottom187&& this.showSlider === other.showSlider188&& this.autohide === other.autohide189&& this.pixelRatio === other.pixelRatio190&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth191&& this.lineHeight === other.lineHeight192&& this.minimapLeft === other.minimapLeft193&& this.minimapWidth === other.minimapWidth194&& this.minimapHeight === other.minimapHeight195&& this.canvasInnerWidth === other.canvasInnerWidth196&& this.canvasInnerHeight === other.canvasInnerHeight197&& this.canvasOuterWidth === other.canvasOuterWidth198&& this.canvasOuterHeight === other.canvasOuterHeight199&& this.isSampling === other.isSampling200&& this.editorHeight === other.editorHeight201&& this.fontScale === other.fontScale202&& this.minimapLineHeight === other.minimapLineHeight203&& this.minimapCharWidth === other.minimapCharWidth204&& this.sectionHeaderFontSize === other.sectionHeaderFontSize205&& this.sectionHeaderLetterSpacing === other.sectionHeaderLetterSpacing206&& this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor)207&& this.backgroundColor && this.backgroundColor.equals(other.backgroundColor)208&& this.foregroundAlpha === other.foregroundAlpha209);210}211}212213class MinimapLayout {214215constructor(216/**217* The given editor scrollTop (input).218*/219public readonly scrollTop: number,220/**221* The given editor scrollHeight (input).222*/223public readonly scrollHeight: number,224public readonly sliderNeeded: boolean,225private readonly _computedSliderRatio: number,226/**227* slider dom node top (in CSS px)228*/229public readonly sliderTop: number,230/**231* slider dom node height (in CSS px)232*/233public readonly sliderHeight: number,234/**235* empty lines to reserve at the top of the minimap.236*/237public readonly topPaddingLineCount: number,238/**239* minimap render start line number.240*/241public readonly startLineNumber: number,242/**243* minimap render end line number.244*/245public readonly endLineNumber: number246) { }247248/**249* Compute a desired `scrollPosition` such that the slider moves by `delta`.250*/251public getDesiredScrollTopFromDelta(delta: number): number {252return Math.round(this.scrollTop + delta / this._computedSliderRatio);253}254255public getDesiredScrollTopFromTouchLocation(pageY: number): number {256return Math.round((pageY - this.sliderHeight / 2) / this._computedSliderRatio);257}258259/**260* Intersect a line range with `this.startLineNumber` and `this.endLineNumber`.261*/262public intersectWithViewport(range: Range): [number, number] | null {263const startLineNumber = Math.max(this.startLineNumber, range.startLineNumber);264const endLineNumber = Math.min(this.endLineNumber, range.endLineNumber);265if (startLineNumber > endLineNumber) {266// entirely outside minimap's viewport267return null;268}269return [startLineNumber, endLineNumber];270}271272/**273* Get the inner minimap y coordinate for a line number.274*/275public getYForLineNumber(lineNumber: number, minimapLineHeight: number): number {276return + (lineNumber - this.startLineNumber + this.topPaddingLineCount) * minimapLineHeight;277}278279public static create(280options: MinimapOptions,281viewportStartLineNumber: number,282viewportEndLineNumber: number,283viewportStartLineNumberVerticalOffset: number,284viewportHeight: number,285viewportContainsWhitespaceGaps: boolean,286lineCount: number,287realLineCount: number,288scrollTop: number,289scrollHeight: number,290previousLayout: MinimapLayout | null291): MinimapLayout {292const pixelRatio = options.pixelRatio;293const minimapLineHeight = options.minimapLineHeight;294const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);295const lineHeight = options.lineHeight;296297if (options.minimapHeightIsEditorHeight) {298let logicalScrollHeight = (299realLineCount * options.lineHeight300+ options.paddingTop301+ options.paddingBottom302);303if (options.scrollBeyondLastLine) {304logicalScrollHeight += Math.max(0, viewportHeight - options.lineHeight - options.paddingBottom);305}306const sliderHeight = Math.max(1, Math.floor(viewportHeight * viewportHeight / logicalScrollHeight));307const maxMinimapSliderTop = Math.max(0, options.minimapHeight - sliderHeight);308// The slider can move from 0 to `maxMinimapSliderTop`309// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.310const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);311const sliderTop = (scrollTop * computedSliderRatio);312const sliderNeeded = (maxMinimapSliderTop > 0);313const maxLinesFitting = Math.floor(options.canvasInnerHeight / options.minimapLineHeight);314const topPaddingLineCount = Math.floor(options.paddingTop / options.lineHeight);315return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, topPaddingLineCount, 1, Math.min(lineCount, maxLinesFitting));316}317318// The visible line count in a viewport can change due to a number of reasons:319// a) with the same viewport width, different scroll positions can result in partial lines being visible:320// e.g. for a line height of 20, and a viewport height of 600321// * scrollTop = 0 => visible lines are [1, 30]322// * scrollTop = 10 => visible lines are [1, 31] (with lines 1 and 31 partially visible)323// * scrollTop = 20 => visible lines are [2, 31]324// b) whitespace gaps might make their way in the viewport (which results in a decrease in the visible line count)325// 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)326327// We must first establish a desirable slider height.328let sliderHeight: number;329if (viewportContainsWhitespaceGaps && viewportEndLineNumber !== lineCount) {330// case b) from above: there are whitespace gaps in the viewport.331// In this case, the height of the slider directly reflects the visible line count.332const viewportLineCount = viewportEndLineNumber - viewportStartLineNumber + 1;333sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);334} else {335// The slider has a stable height336const expectedViewportLineCount = viewportHeight / lineHeight;337sliderHeight = Math.floor(expectedViewportLineCount * minimapLineHeight / pixelRatio);338}339340const extraLinesAtTheTop = Math.floor(options.paddingTop / lineHeight);341let extraLinesAtTheBottom = Math.floor(options.paddingBottom / lineHeight);342if (options.scrollBeyondLastLine) {343const expectedViewportLineCount = viewportHeight / lineHeight;344extraLinesAtTheBottom = Math.max(extraLinesAtTheBottom, expectedViewportLineCount - 1);345}346347let maxMinimapSliderTop: number;348if (extraLinesAtTheBottom > 0) {349const expectedViewportLineCount = viewportHeight / lineHeight;350// The minimap slider, when dragged all the way down, will contain the last line at its top351maxMinimapSliderTop = (extraLinesAtTheTop + lineCount + extraLinesAtTheBottom - expectedViewportLineCount - 1) * minimapLineHeight / pixelRatio;352} else {353// The minimap slider, when dragged all the way down, will contain the last line at its bottom354maxMinimapSliderTop = Math.max(0, (extraLinesAtTheTop + lineCount) * minimapLineHeight / pixelRatio - sliderHeight);355}356maxMinimapSliderTop = Math.min(options.minimapHeight - sliderHeight, maxMinimapSliderTop);357358// The slider can move from 0 to `maxMinimapSliderTop`359// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.360const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);361const sliderTop = (scrollTop * computedSliderRatio);362363if (minimapLinesFitting >= extraLinesAtTheTop + lineCount + extraLinesAtTheBottom) {364// All lines fit in the minimap365const sliderNeeded = (maxMinimapSliderTop > 0);366return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, extraLinesAtTheTop, 1, lineCount);367} else {368let consideringStartLineNumber: number;369if (viewportStartLineNumber > 1) {370consideringStartLineNumber = viewportStartLineNumber + extraLinesAtTheTop;371} else {372consideringStartLineNumber = Math.max(1, scrollTop / lineHeight);373}374375let topPaddingLineCount: number;376let startLineNumber = Math.max(1, Math.floor(consideringStartLineNumber - sliderTop * pixelRatio / minimapLineHeight));377if (startLineNumber < extraLinesAtTheTop) {378topPaddingLineCount = extraLinesAtTheTop - startLineNumber + 1;379startLineNumber = 1;380} else {381topPaddingLineCount = 0;382startLineNumber = Math.max(1, startLineNumber - extraLinesAtTheTop);383}384385// Avoid flickering caused by a partial viewport start line386// by being consistent w.r.t. the previous layout decision387if (previousLayout && previousLayout.scrollHeight === scrollHeight) {388if (previousLayout.scrollTop > scrollTop) {389// Scrolling up => never increase `startLineNumber`390startLineNumber = Math.min(startLineNumber, previousLayout.startLineNumber);391topPaddingLineCount = Math.max(topPaddingLineCount, previousLayout.topPaddingLineCount);392}393if (previousLayout.scrollTop < scrollTop) {394// Scrolling down => never decrease `startLineNumber`395startLineNumber = Math.max(startLineNumber, previousLayout.startLineNumber);396topPaddingLineCount = Math.min(topPaddingLineCount, previousLayout.topPaddingLineCount);397}398}399400const endLineNumber = Math.min(lineCount, startLineNumber - topPaddingLineCount + minimapLinesFitting - 1);401const partialLine = (scrollTop - viewportStartLineNumberVerticalOffset) / lineHeight;402403let sliderTopAligned: number;404if (scrollTop >= options.paddingTop) {405sliderTopAligned = (viewportStartLineNumber - startLineNumber + topPaddingLineCount + partialLine) * minimapLineHeight / pixelRatio;406} else {407sliderTopAligned = (scrollTop / options.paddingTop) * (topPaddingLineCount + partialLine) * minimapLineHeight / pixelRatio;408}409410return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTopAligned, sliderHeight, topPaddingLineCount, startLineNumber, endLineNumber);411}412}413}414415class MinimapLine implements ILine {416417public static readonly INVALID = new MinimapLine(-1);418419dy: number;420421constructor(dy: number) {422this.dy = dy;423}424425public onContentChanged(): void {426this.dy = -1;427}428429public onTokensChanged(): void {430this.dy = -1;431}432}433434class RenderData {435/**436* last rendered layout.437*/438public readonly renderedLayout: MinimapLayout;439private readonly _imageData: ImageData;440private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;441442constructor(443renderedLayout: MinimapLayout,444imageData: ImageData,445lines: MinimapLine[]446) {447this.renderedLayout = renderedLayout;448this._imageData = imageData;449this._renderedLines = new RenderedLinesCollection({450createLine: () => MinimapLine.INVALID451});452this._renderedLines._set(renderedLayout.startLineNumber, lines);453}454455/**456* Check if the current RenderData matches accurately the new desired layout and no painting is needed.457*/458public linesEquals(layout: MinimapLayout): boolean {459if (!this.scrollEquals(layout)) {460return false;461}462463const tmp = this._renderedLines._get();464const lines = tmp.lines;465for (let i = 0, len = lines.length; i < len; i++) {466if (lines[i].dy === -1) {467// This line is invalid468return false;469}470}471472return true;473}474475/**476* Check if the current RenderData matches the new layout's scroll position477*/478public scrollEquals(layout: MinimapLayout): boolean {479return this.renderedLayout.startLineNumber === layout.startLineNumber480&& this.renderedLayout.endLineNumber === layout.endLineNumber;481}482483_get(): { imageData: ImageData; rendLineNumberStart: number; lines: MinimapLine[] } {484const tmp = this._renderedLines._get();485return {486imageData: this._imageData,487rendLineNumberStart: tmp.rendLineNumberStart,488lines: tmp.lines489};490}491492public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {493return this._renderedLines.onLinesChanged(changeFromLineNumber, changeCount);494}495public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): void {496this._renderedLines.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber);497}498public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): void {499this._renderedLines.onLinesInserted(insertFromLineNumber, insertToLineNumber);500}501public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {502return this._renderedLines.onTokensChanged(ranges);503}504}505506/**507* Some sort of double buffering.508*509* Keeps two buffers around that will be rotated for painting.510* Always gives a buffer that is filled with the background color.511*/512class MinimapBuffers {513514private readonly _backgroundFillData: Uint8ClampedArray;515private readonly _buffers: [ImageData, ImageData];516private _lastUsedBuffer: number;517518constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA8) {519this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background);520this._buffers = [521ctx.createImageData(WIDTH, HEIGHT),522ctx.createImageData(WIDTH, HEIGHT)523];524this._lastUsedBuffer = 0;525}526527public getBuffer(): ImageData {528// rotate buffers529this._lastUsedBuffer = 1 - this._lastUsedBuffer;530const result = this._buffers[this._lastUsedBuffer];531532// fill with background color533result.data.set(this._backgroundFillData);534535return result;536}537538private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA8): Uint8ClampedArray {539const backgroundR = background.r;540const backgroundG = background.g;541const backgroundB = background.b;542const backgroundA = background.a;543544const result = new Uint8ClampedArray(WIDTH * HEIGHT * 4);545let offset = 0;546for (let i = 0; i < HEIGHT; i++) {547for (let j = 0; j < WIDTH; j++) {548result[offset] = backgroundR;549result[offset + 1] = backgroundG;550result[offset + 2] = backgroundB;551result[offset + 3] = backgroundA;552offset += 4;553}554}555556return result;557}558}559560export interface IMinimapModel {561readonly tokensColorTracker: MinimapTokensColorTracker;562readonly options: MinimapOptions;563564getLineCount(): number;565getRealLineCount(): number;566getLineContent(lineNumber: number): string;567getLineMaxColumn(lineNumber: number): number;568getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[];569getSelections(): Selection[];570getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];571getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];572getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null;573getOptions(): TextModelResolvedOptions;574revealLineNumber(lineNumber: number): void;575setScrollTop(scrollTop: number): void;576}577578interface IMinimapRenderingContext {579readonly viewportContainsWhitespaceGaps: boolean;580581readonly scrollWidth: number;582readonly scrollHeight: number;583584readonly viewportStartLineNumber: number;585readonly viewportEndLineNumber: number;586readonly viewportStartLineNumberVerticalOffset: number;587588readonly scrollTop: number;589readonly scrollLeft: number;590591readonly viewportWidth: number;592readonly viewportHeight: number;593}594595interface SamplingStateLinesDeletedEvent {596type: 'deleted';597_oldIndex: number;598deleteFromLineNumber: number;599deleteToLineNumber: number;600}601602interface SamplingStateLinesInsertedEvent {603type: 'inserted';604_i: number;605insertFromLineNumber: number;606insertToLineNumber: number;607}608609interface SamplingStateFlushEvent {610type: 'flush';611}612613type SamplingStateEvent = SamplingStateLinesInsertedEvent | SamplingStateLinesDeletedEvent | SamplingStateFlushEvent;614615class MinimapSamplingState {616617public static compute(options: MinimapOptions, viewLineCount: number, oldSamplingState: MinimapSamplingState | null): [MinimapSamplingState | null, SamplingStateEvent[]] {618if (options.renderMinimap === RenderMinimap.None || !options.isSampling) {619return [null, []];620}621622// ratio is intentionally not part of the layout to avoid the layout changing all the time623// so we need to recompute it again...624const { minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({625viewLineCount: viewLineCount,626scrollBeyondLastLine: options.scrollBeyondLastLine,627paddingTop: options.paddingTop,628paddingBottom: options.paddingBottom,629height: options.editorHeight,630lineHeight: options.lineHeight,631pixelRatio: options.pixelRatio632});633const ratio = viewLineCount / minimapLineCount;634const halfRatio = ratio / 2;635636if (!oldSamplingState || oldSamplingState.minimapLines.length === 0) {637const result: number[] = [];638result[0] = 1;639if (minimapLineCount > 1) {640for (let i = 0, lastIndex = minimapLineCount - 1; i < lastIndex; i++) {641result[i] = Math.round(i * ratio + halfRatio);642}643result[minimapLineCount - 1] = viewLineCount;644}645return [new MinimapSamplingState(ratio, result), []];646}647648const oldMinimapLines = oldSamplingState.minimapLines;649const oldLength = oldMinimapLines.length;650const result: number[] = [];651let oldIndex = 0;652let oldDeltaLineCount = 0;653let minViewLineNumber = 1;654const MAX_EVENT_COUNT = 10; // generate at most 10 events, if there are more than 10 changes, just flush all previous data655let events: SamplingStateEvent[] = [];656let lastEvent: SamplingStateEvent | null = null;657for (let i = 0; i < minimapLineCount; i++) {658const fromViewLineNumber = Math.max(minViewLineNumber, Math.round(i * ratio));659const toViewLineNumber = Math.max(fromViewLineNumber, Math.round((i + 1) * ratio));660661while (oldIndex < oldLength && oldMinimapLines[oldIndex] < fromViewLineNumber) {662if (events.length < MAX_EVENT_COUNT) {663const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;664if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) {665lastEvent.deleteToLineNumber++;666} else {667lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber };668events.push(lastEvent);669}670oldDeltaLineCount--;671}672oldIndex++;673}674675let selectedViewLineNumber: number;676if (oldIndex < oldLength && oldMinimapLines[oldIndex] <= toViewLineNumber) {677// reuse the old sampled line678selectedViewLineNumber = oldMinimapLines[oldIndex];679oldIndex++;680} else {681if (i === 0) {682selectedViewLineNumber = 1;683} else if (i + 1 === minimapLineCount) {684selectedViewLineNumber = viewLineCount;685} else {686selectedViewLineNumber = Math.round(i * ratio + halfRatio);687}688if (events.length < MAX_EVENT_COUNT) {689const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;690if (lastEvent && lastEvent.type === 'inserted' && lastEvent._i === i - 1) {691lastEvent.insertToLineNumber++;692} else {693lastEvent = { type: 'inserted', _i: i, insertFromLineNumber: oldMinimapLineNumber, insertToLineNumber: oldMinimapLineNumber };694events.push(lastEvent);695}696oldDeltaLineCount++;697}698}699700result[i] = selectedViewLineNumber;701minViewLineNumber = selectedViewLineNumber;702}703704if (events.length < MAX_EVENT_COUNT) {705while (oldIndex < oldLength) {706const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount;707if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) {708lastEvent.deleteToLineNumber++;709} else {710lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber };711events.push(lastEvent);712}713oldDeltaLineCount--;714oldIndex++;715}716} else {717// too many events, just give up718events = [{ type: 'flush' }];719}720721return [new MinimapSamplingState(ratio, result), events];722}723724constructor(725public readonly samplingRatio: number,726public readonly minimapLines: number[] // a map of 0-based minimap line indexes to 1-based view line numbers727) {728}729730public modelLineToMinimapLine(lineNumber: number): number {731return Math.min(this.minimapLines.length, Math.max(1, Math.round(lineNumber / this.samplingRatio)));732}733734/**735* Will return null if the model line ranges are not intersecting with a sampled model line.736*/737public modelLineRangeToMinimapLineRange(fromLineNumber: number, toLineNumber: number): [number, number] | null {738let fromLineIndex = this.modelLineToMinimapLine(fromLineNumber) - 1;739while (fromLineIndex > 0 && this.minimapLines[fromLineIndex - 1] >= fromLineNumber) {740fromLineIndex--;741}742let toLineIndex = this.modelLineToMinimapLine(toLineNumber) - 1;743while (toLineIndex + 1 < this.minimapLines.length && this.minimapLines[toLineIndex + 1] <= toLineNumber) {744toLineIndex++;745}746if (fromLineIndex === toLineIndex) {747const sampledLineNumber = this.minimapLines[fromLineIndex];748if (sampledLineNumber < fromLineNumber || sampledLineNumber > toLineNumber) {749// This line is not part of the sampled lines ==> nothing to do750return null;751}752}753return [fromLineIndex + 1, toLineIndex + 1];754}755756/**757* Will always return a range, even if it is not intersecting with a sampled model line.758*/759public decorationLineRangeToMinimapLineRange(startLineNumber: number, endLineNumber: number): [number, number] {760let minimapLineStart = this.modelLineToMinimapLine(startLineNumber);761let minimapLineEnd = this.modelLineToMinimapLine(endLineNumber);762if (startLineNumber !== endLineNumber && minimapLineEnd === minimapLineStart) {763if (minimapLineEnd === this.minimapLines.length) {764if (minimapLineStart > 1) {765minimapLineStart--;766}767} else {768minimapLineEnd++;769}770}771return [minimapLineStart, minimapLineEnd];772}773774public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): [number, number] {775// have the mapping be sticky776const deletedLineCount = e.toLineNumber - e.fromLineNumber + 1;777let changeStartIndex = this.minimapLines.length;778let changeEndIndex = 0;779for (let i = this.minimapLines.length - 1; i >= 0; i--) {780if (this.minimapLines[i] < e.fromLineNumber) {781break;782}783if (this.minimapLines[i] <= e.toLineNumber) {784// this line got deleted => move to previous available785this.minimapLines[i] = Math.max(1, e.fromLineNumber - 1);786changeStartIndex = Math.min(changeStartIndex, i);787changeEndIndex = Math.max(changeEndIndex, i);788} else {789this.minimapLines[i] -= deletedLineCount;790}791}792return [changeStartIndex, changeEndIndex];793}794795public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {796// have the mapping be sticky797const insertedLineCount = e.toLineNumber - e.fromLineNumber + 1;798for (let i = this.minimapLines.length - 1; i >= 0; i--) {799if (this.minimapLines[i] < e.fromLineNumber) {800break;801}802this.minimapLines[i] += insertedLineCount;803}804}805}806807/**808* The minimap appears beside the editor scroll bar and visualizes a zoomed out809* view of the file.810*/811export class Minimap extends ViewPart implements IMinimapModel {812813public readonly tokensColorTracker: MinimapTokensColorTracker;814815private _selections: Selection[];816private _minimapSelections: Selection[] | null;817818public options: MinimapOptions;819820private _samplingState: MinimapSamplingState | null;821private _shouldCheckSampling: boolean;822823private _sectionHeaderCache = new LRUCache<string, string>(10, 1.5);824825private _actual: InnerMinimap;826827constructor(context: ViewContext) {828super(context);829830this.tokensColorTracker = MinimapTokensColorTracker.getInstance();831832this._selections = [];833this._minimapSelections = null;834835this.options = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker);836const [samplingState,] = MinimapSamplingState.compute(this.options, this._context.viewModel.getLineCount(), null);837this._samplingState = samplingState;838this._shouldCheckSampling = false;839840this._actual = new InnerMinimap(context.theme, this);841}842843public override dispose(): void {844this._actual.dispose();845super.dispose();846}847848public getDomNode(): FastDomNode<HTMLElement> {849return this._actual.getDomNode();850}851852private _onOptionsMaybeChanged(): boolean {853const opts = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker);854if (this.options.equals(opts)) {855return false;856}857this.options = opts;858this._recreateLineSampling();859this._actual.onDidChangeOptions();860return true;861}862863// ---- begin view event handlers864865public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {866return this._onOptionsMaybeChanged();867}868public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {869this._selections = e.selections;870this._minimapSelections = null;871return this._actual.onSelectionChanged();872}873public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {874if (e.affectsMinimap) {875return this._actual.onDecorationsChanged();876}877return false;878}879public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {880if (this._samplingState) {881this._shouldCheckSampling = true;882}883return this._actual.onFlushed();884}885public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {886if (this._samplingState) {887const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(e.fromLineNumber, e.fromLineNumber + e.count - 1);888if (minimapLineRange) {889return this._actual.onLinesChanged(minimapLineRange[0], minimapLineRange[1] - minimapLineRange[0] + 1);890} else {891return false;892}893} else {894return this._actual.onLinesChanged(e.fromLineNumber, e.count);895}896}897public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {898if (this._samplingState) {899const [changeStartIndex, changeEndIndex] = this._samplingState.onLinesDeleted(e);900if (changeStartIndex <= changeEndIndex) {901this._actual.onLinesChanged(changeStartIndex + 1, changeEndIndex - changeStartIndex + 1);902}903this._shouldCheckSampling = true;904return true;905} else {906return this._actual.onLinesDeleted(e.fromLineNumber, e.toLineNumber);907}908}909public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {910if (this._samplingState) {911this._samplingState.onLinesInserted(e);912this._shouldCheckSampling = true;913return true;914} else {915return this._actual.onLinesInserted(e.fromLineNumber, e.toLineNumber);916}917}918public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {919return this._actual.onScrollChanged(e);920}921public override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {922this._actual.onThemeChanged();923this._onOptionsMaybeChanged();924return true;925}926public override onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {927if (this._samplingState) {928const ranges: { fromLineNumber: number; toLineNumber: number }[] = [];929for (const range of e.ranges) {930const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(range.fromLineNumber, range.toLineNumber);931if (minimapLineRange) {932ranges.push({ fromLineNumber: minimapLineRange[0], toLineNumber: minimapLineRange[1] });933}934}935if (ranges.length) {936return this._actual.onTokensChanged(ranges);937} else {938return false;939}940} else {941return this._actual.onTokensChanged(e.ranges);942}943}944public override onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {945this._onOptionsMaybeChanged();946return this._actual.onTokensColorsChanged();947}948public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {949return this._actual.onZonesChanged();950}951952// --- end event handlers953954public prepareRender(ctx: RenderingContext): void {955if (this._shouldCheckSampling) {956this._shouldCheckSampling = false;957this._recreateLineSampling();958}959}960961public render(ctx: RestrictedRenderingContext): void {962let viewportStartLineNumber = ctx.visibleRange.startLineNumber;963let viewportEndLineNumber = ctx.visibleRange.endLineNumber;964965if (this._samplingState) {966viewportStartLineNumber = this._samplingState.modelLineToMinimapLine(viewportStartLineNumber);967viewportEndLineNumber = this._samplingState.modelLineToMinimapLine(viewportEndLineNumber);968}969970const minimapCtx: IMinimapRenderingContext = {971viewportContainsWhitespaceGaps: (ctx.viewportData.whitespaceViewportData.length > 0),972973scrollWidth: ctx.scrollWidth,974scrollHeight: ctx.scrollHeight,975976viewportStartLineNumber: viewportStartLineNumber,977viewportEndLineNumber: viewportEndLineNumber,978viewportStartLineNumberVerticalOffset: ctx.getVerticalOffsetForLineNumber(viewportStartLineNumber),979980scrollTop: ctx.scrollTop,981scrollLeft: ctx.scrollLeft,982983viewportWidth: ctx.viewportWidth,984viewportHeight: ctx.viewportHeight,985};986this._actual.render(minimapCtx);987}988989//#region IMinimapModel990991private _recreateLineSampling(): void {992this._minimapSelections = null;993994const wasSampling = Boolean(this._samplingState);995const [samplingState, events] = MinimapSamplingState.compute(this.options, this._context.viewModel.getLineCount(), this._samplingState);996this._samplingState = samplingState;997998if (wasSampling && this._samplingState) {999// was sampling, is sampling1000for (const event of events) {1001switch (event.type) {1002case 'deleted':1003this._actual.onLinesDeleted(event.deleteFromLineNumber, event.deleteToLineNumber);1004break;1005case 'inserted':1006this._actual.onLinesInserted(event.insertFromLineNumber, event.insertToLineNumber);1007break;1008case 'flush':1009this._actual.onFlushed();1010break;1011}1012}1013}1014}10151016public getLineCount(): number {1017if (this._samplingState) {1018return this._samplingState.minimapLines.length;1019}1020return this._context.viewModel.getLineCount();1021}10221023public getRealLineCount(): number {1024return this._context.viewModel.getLineCount();1025}10261027public getLineContent(lineNumber: number): string {1028if (this._samplingState) {1029return this._context.viewModel.getLineContent(this._samplingState.minimapLines[lineNumber - 1]);1030}1031return this._context.viewModel.getLineContent(lineNumber);1032}10331034public getLineMaxColumn(lineNumber: number): number {1035if (this._samplingState) {1036return this._context.viewModel.getLineMaxColumn(this._samplingState.minimapLines[lineNumber - 1]);1037}1038return this._context.viewModel.getLineMaxColumn(lineNumber);1039}10401041public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[] {1042if (this._samplingState) {1043const result: (ViewLineData | null)[] = [];1044for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {1045if (needed[lineIndex]) {1046result[lineIndex] = this._context.viewModel.getViewLineData(this._samplingState.minimapLines[startLineNumber + lineIndex - 1]);1047} else {1048result[lineIndex] = null;1049}1050}1051return result;1052}1053return this._context.viewModel.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed).data;1054}10551056public getSelections(): Selection[] {1057if (this._minimapSelections === null) {1058if (this._samplingState) {1059this._minimapSelections = [];1060for (const selection of this._selections) {1061const [minimapLineStart, minimapLineEnd] = this._samplingState.decorationLineRangeToMinimapLineRange(selection.startLineNumber, selection.endLineNumber);1062this._minimapSelections.push(new Selection(minimapLineStart, selection.startColumn, minimapLineEnd, selection.endColumn));1063}1064} else {1065this._minimapSelections = this._selections;1066}1067}1068return this._minimapSelections;1069}10701071public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {1072return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)1073.filter(decoration => !decoration.options.minimap?.sectionHeaderStyle);1074}10751076public getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {1077const headerHeightInMinimapLines = this.options.sectionHeaderFontSize / this.options.minimapLineHeight;1078startLineNumber = Math.floor(Math.max(1, startLineNumber - headerHeightInMinimapLines));1079return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)1080.filter(decoration => !!decoration.options.minimap?.sectionHeaderStyle);1081}10821083private _getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number) {1084let visibleRange: Range;1085if (this._samplingState) {1086const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1];1087const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1];1088visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber));1089} else {1090visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber));1091}1092const decorations = this._context.viewModel.getMinimapDecorationsInRange(visibleRange);10931094if (this._samplingState) {1095const result: ViewModelDecoration[] = [];1096for (const decoration of decorations) {1097if (!decoration.options.minimap) {1098continue;1099}1100const range = decoration.range;1101const minimapStartLineNumber = this._samplingState.modelLineToMinimapLine(range.startLineNumber);1102const minimapEndLineNumber = this._samplingState.modelLineToMinimapLine(range.endLineNumber);1103result.push(new ViewModelDecoration(new Range(minimapStartLineNumber, range.startColumn, minimapEndLineNumber, range.endColumn), decoration.options));1104}1105return result;1106}11071108return decorations;1109}11101111public getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null {1112const headerText = decoration.options.minimap?.sectionHeaderText;1113if (!headerText) {1114return null;1115}1116const cachedText = this._sectionHeaderCache.get(headerText);1117if (cachedText) {1118return cachedText;1119}1120const fittedText = fitWidth(headerText);1121this._sectionHeaderCache.set(headerText, fittedText);1122return fittedText;1123}11241125public getOptions(): TextModelResolvedOptions {1126return this._context.viewModel.model.getOptions();1127}11281129public revealLineNumber(lineNumber: number): void {1130if (this._samplingState) {1131lineNumber = this._samplingState.minimapLines[lineNumber - 1];1132}1133this._context.viewModel.revealRange(1134'mouse',1135false,1136new Range(lineNumber, 1, lineNumber, 1),1137viewEvents.VerticalRevealType.Center,1138ScrollType.Smooth1139);1140}11411142public setScrollTop(scrollTop: number): void {1143this._context.viewModel.viewLayout.setScrollPosition({1144scrollTop: scrollTop1145}, ScrollType.Immediate);1146}11471148//#endregion1149}11501151class InnerMinimap extends Disposable {11521153private readonly _theme: EditorTheme;1154private readonly _model: IMinimapModel;11551156private readonly _domNode: FastDomNode<HTMLElement>;1157private readonly _shadow: FastDomNode<HTMLElement>;1158private readonly _canvas: FastDomNode<HTMLCanvasElement>;1159private readonly _decorationsCanvas: FastDomNode<HTMLCanvasElement>;1160private readonly _slider: FastDomNode<HTMLElement>;1161private readonly _sliderHorizontal: FastDomNode<HTMLElement>;1162private readonly _pointerDownListener: IDisposable;1163private readonly _sliderPointerMoveMonitor: GlobalPointerMoveMonitor;1164private readonly _sliderPointerDownListener: IDisposable;1165private readonly _gestureDisposable: IDisposable;1166private readonly _sliderTouchStartListener: IDisposable;1167private readonly _sliderTouchMoveListener: IDisposable;1168private readonly _sliderTouchEndListener: IDisposable;11691170private _lastRenderData: RenderData | null;1171private _selectionColor: Color | undefined;1172private _renderDecorations: boolean = false;1173private _gestureInProgress: boolean = false;1174private _buffers: MinimapBuffers | null;1175private _isMouseOverMinimap: boolean = false;1176private _hideDelayedScheduler: RunOnceScheduler;11771178constructor(1179theme: EditorTheme,1180model: IMinimapModel1181) {1182super();11831184this._theme = theme;1185this._model = model;11861187this._lastRenderData = null;1188this._buffers = null;1189this._selectionColor = this._theme.getColor(minimapSelection);11901191this._domNode = createFastDomNode(document.createElement('div'));1192PartFingerprints.write(this._domNode, PartFingerprint.Minimap);1193this._domNode.setClassName(this._getMinimapDomNodeClassName());1194this._domNode.setPosition('absolute');1195this._domNode.setAttribute('role', 'presentation');1196this._domNode.setAttribute('aria-hidden', 'true');11971198this._shadow = createFastDomNode(document.createElement('div'));1199this._shadow.setClassName('minimap-shadow-hidden');1200this._domNode.appendChild(this._shadow);12011202this._canvas = createFastDomNode(document.createElement('canvas'));1203this._canvas.setPosition('absolute');1204this._canvas.setLeft(0);1205this._domNode.appendChild(this._canvas);12061207this._decorationsCanvas = createFastDomNode(document.createElement('canvas'));1208this._decorationsCanvas.setPosition('absolute');1209this._decorationsCanvas.setClassName('minimap-decorations-layer');1210this._decorationsCanvas.setLeft(0);1211this._domNode.appendChild(this._decorationsCanvas);12121213this._slider = createFastDomNode(document.createElement('div'));1214this._slider.setPosition('absolute');1215this._slider.setClassName('minimap-slider');1216this._slider.setLayerHinting(true);1217this._slider.setContain('strict');1218this._domNode.appendChild(this._slider);12191220this._sliderHorizontal = createFastDomNode(document.createElement('div'));1221this._sliderHorizontal.setPosition('absolute');1222this._sliderHorizontal.setClassName('minimap-slider-horizontal');1223this._slider.appendChild(this._sliderHorizontal);12241225this._applyLayout();12261227this._hideDelayedScheduler = this._register(new RunOnceScheduler(() => this._hideImmediatelyIfMouseIsOutside(), 500));12281229this._register(dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.MOUSE_OVER, () => {1230this._isMouseOverMinimap = true;1231}));1232this._register(dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.MOUSE_LEAVE, () => {1233this._isMouseOverMinimap = false;1234}));12351236this._pointerDownListener = dom.addStandardDisposableListener(this._domNode.domNode, dom.EventType.POINTER_DOWN, (e) => {1237e.preventDefault();12381239const isMouse = (e.pointerType === 'mouse');1240const isLeftClick = (e.button === 0);12411242const renderMinimap = this._model.options.renderMinimap;1243if (renderMinimap === RenderMinimap.None) {1244return;1245}1246if (!this._lastRenderData) {1247return;1248}1249if (this._model.options.size !== 'proportional') {1250if (isLeftClick && this._lastRenderData) {1251// pretend the click occurred in the center of the slider1252const position = dom.getDomNodePagePosition(this._slider.domNode);1253const initialPosY = position.top + position.height / 2;1254this._startSliderDragging(e, initialPosY, this._lastRenderData.renderedLayout);1255}1256return;1257}12581259if (isLeftClick || !isMouse) {1260const minimapLineHeight = this._model.options.minimapLineHeight;1261const internalOffsetY = (this._model.options.canvasInnerHeight / this._model.options.canvasOuterHeight) * e.offsetY;1262const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);12631264let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber - this._lastRenderData.renderedLayout.topPaddingLineCount;1265lineNumber = Math.min(lineNumber, this._model.getLineCount());12661267this._model.revealLineNumber(lineNumber);1268}1269});12701271this._sliderPointerMoveMonitor = new GlobalPointerMoveMonitor();12721273this._sliderPointerDownListener = dom.addStandardDisposableListener(this._slider.domNode, dom.EventType.POINTER_DOWN, (e) => {1274e.preventDefault();1275e.stopPropagation();1276if (e.button === 0 && this._lastRenderData) {1277this._startSliderDragging(e, e.pageY, this._lastRenderData.renderedLayout);1278}1279});12801281this._gestureDisposable = Gesture.addTarget(this._domNode.domNode);1282this._sliderTouchStartListener = dom.addDisposableListener(this._domNode.domNode, EventType.Start, (e: GestureEvent) => {1283e.preventDefault();1284e.stopPropagation();1285if (this._lastRenderData) {1286this._slider.toggleClassName('active', true);1287this._gestureInProgress = true;1288this.scrollDueToTouchEvent(e);1289}1290}, { passive: false });12911292this._sliderTouchMoveListener = dom.addDisposableListener(this._domNode.domNode, EventType.Change, (e: GestureEvent) => {1293e.preventDefault();1294e.stopPropagation();1295if (this._lastRenderData && this._gestureInProgress) {1296this.scrollDueToTouchEvent(e);1297}1298}, { passive: false });12991300this._sliderTouchEndListener = dom.addStandardDisposableListener(this._domNode.domNode, EventType.End, (e: GestureEvent) => {1301e.preventDefault();1302e.stopPropagation();1303this._gestureInProgress = false;1304this._slider.toggleClassName('active', false);1305});1306}13071308private _hideSoon() {1309this._hideDelayedScheduler.cancel();1310this._hideDelayedScheduler.schedule();1311}13121313private _hideImmediatelyIfMouseIsOutside() {1314if (this._isMouseOverMinimap) {1315this._hideSoon();1316return;1317}1318this._domNode.toggleClassName('active', false);1319}13201321private _startSliderDragging(e: PointerEvent, initialPosY: number, initialSliderState: MinimapLayout): void {1322if (!e.target || !(e.target instanceof Element)) {1323return;1324}1325const initialPosX = e.pageX;13261327this._slider.toggleClassName('active', true);13281329const handlePointerMove = (posy: number, posx: number) => {1330const minimapPosition = dom.getDomNodePagePosition(this._domNode.domNode);1331const pointerOrthogonalDelta = Math.min(1332Math.abs(posx - initialPosX),1333Math.abs(posx - minimapPosition.left),1334Math.abs(posx - minimapPosition.left - minimapPosition.width)1335);13361337if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {1338// The pointer has wondered away from the scrollbar => reset dragging1339this._model.setScrollTop(initialSliderState.scrollTop);1340return;1341}13421343const pointerDelta = posy - initialPosY;1344this._model.setScrollTop(initialSliderState.getDesiredScrollTopFromDelta(pointerDelta));1345};13461347if (e.pageY !== initialPosY) {1348handlePointerMove(e.pageY, initialPosX);1349}13501351this._sliderPointerMoveMonitor.startMonitoring(1352e.target,1353e.pointerId,1354e.buttons,1355pointerMoveData => handlePointerMove(pointerMoveData.pageY, pointerMoveData.pageX),1356() => {1357this._slider.toggleClassName('active', false);1358}1359);1360}13611362private scrollDueToTouchEvent(touch: GestureEvent) {1363const startY = this._domNode.domNode.getBoundingClientRect().top;1364const scrollTop = this._lastRenderData!.renderedLayout.getDesiredScrollTopFromTouchLocation(touch.pageY - startY);1365this._model.setScrollTop(scrollTop);1366}13671368public override dispose(): void {1369this._pointerDownListener.dispose();1370this._sliderPointerMoveMonitor.dispose();1371this._sliderPointerDownListener.dispose();1372this._gestureDisposable.dispose();1373this._sliderTouchStartListener.dispose();1374this._sliderTouchMoveListener.dispose();1375this._sliderTouchEndListener.dispose();1376super.dispose();1377}13781379private _getMinimapDomNodeClassName(): string {1380const class_ = ['minimap'];1381if (this._model.options.showSlider === 'always') {1382class_.push('slider-always');1383} else {1384class_.push('slider-mouseover');1385}13861387if (this._model.options.autohide === 'mouseover') {1388class_.push('minimap-autohide-mouseover');1389} else if (this._model.options.autohide === 'scroll') {1390class_.push('minimap-autohide-scroll');1391}13921393return class_.join(' ');1394}13951396public getDomNode(): FastDomNode<HTMLElement> {1397return this._domNode;1398}13991400private _applyLayout(): void {1401this._domNode.setLeft(this._model.options.minimapLeft);1402this._domNode.setWidth(this._model.options.minimapWidth);1403this._domNode.setHeight(this._model.options.minimapHeight);1404this._shadow.setHeight(this._model.options.minimapHeight);14051406this._canvas.setWidth(this._model.options.canvasOuterWidth);1407this._canvas.setHeight(this._model.options.canvasOuterHeight);1408this._canvas.domNode.width = this._model.options.canvasInnerWidth;1409this._canvas.domNode.height = this._model.options.canvasInnerHeight;14101411this._decorationsCanvas.setWidth(this._model.options.canvasOuterWidth);1412this._decorationsCanvas.setHeight(this._model.options.canvasOuterHeight);1413this._decorationsCanvas.domNode.width = this._model.options.canvasInnerWidth;1414this._decorationsCanvas.domNode.height = this._model.options.canvasInnerHeight;14151416this._slider.setWidth(this._model.options.minimapWidth);1417}14181419private _getBuffer(): ImageData | null {1420if (!this._buffers) {1421if (this._model.options.canvasInnerWidth > 0 && this._model.options.canvasInnerHeight > 0) {1422this._buffers = new MinimapBuffers(1423this._canvas.domNode.getContext('2d')!,1424this._model.options.canvasInnerWidth,1425this._model.options.canvasInnerHeight,1426this._model.options.backgroundColor1427);1428}1429}1430return this._buffers ? this._buffers.getBuffer() : null;1431}14321433// ---- begin view event handlers14341435public onDidChangeOptions(): void {1436this._lastRenderData = null;1437this._buffers = null;1438this._applyLayout();1439this._domNode.setClassName(this._getMinimapDomNodeClassName());1440}1441public onSelectionChanged(): boolean {1442this._renderDecorations = true;1443return true;1444}1445public onDecorationsChanged(): boolean {1446this._renderDecorations = true;1447return true;1448}1449public onFlushed(): boolean {1450this._lastRenderData = null;1451return true;1452}1453public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {1454if (this._lastRenderData) {1455return this._lastRenderData.onLinesChanged(changeFromLineNumber, changeCount);1456}1457return false;1458}1459public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): boolean {1460this._lastRenderData?.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber);1461return true;1462}1463public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): boolean {1464this._lastRenderData?.onLinesInserted(insertFromLineNumber, insertToLineNumber);1465return true;1466}1467public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {1468if (this._model.options.autohide === 'scroll' && (e.scrollTopChanged || e.scrollHeightChanged)) {1469this._domNode.toggleClassName('active', true);1470this._hideSoon();1471}1472this._renderDecorations = true;1473return true;1474}1475public onThemeChanged(): boolean {1476this._selectionColor = this._theme.getColor(minimapSelection);1477this._renderDecorations = true;1478return true;1479}1480public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {1481if (this._lastRenderData) {1482return this._lastRenderData.onTokensChanged(ranges);1483}1484return false;1485}1486public onTokensColorsChanged(): boolean {1487this._lastRenderData = null;1488this._buffers = null;1489return true;1490}1491public onZonesChanged(): boolean {1492this._lastRenderData = null;1493return true;1494}14951496// --- end event handlers14971498public render(renderingCtx: IMinimapRenderingContext): void {1499const renderMinimap = this._model.options.renderMinimap;1500if (renderMinimap === RenderMinimap.None) {1501this._shadow.setClassName('minimap-shadow-hidden');1502this._sliderHorizontal.setWidth(0);1503this._sliderHorizontal.setHeight(0);1504return;1505}1506if (renderingCtx.scrollLeft + renderingCtx.viewportWidth >= renderingCtx.scrollWidth) {1507this._shadow.setClassName('minimap-shadow-hidden');1508} else {1509this._shadow.setClassName('minimap-shadow-visible');1510}15111512const layout = MinimapLayout.create(1513this._model.options,1514renderingCtx.viewportStartLineNumber,1515renderingCtx.viewportEndLineNumber,1516renderingCtx.viewportStartLineNumberVerticalOffset,1517renderingCtx.viewportHeight,1518renderingCtx.viewportContainsWhitespaceGaps,1519this._model.getLineCount(),1520this._model.getRealLineCount(),1521renderingCtx.scrollTop,1522renderingCtx.scrollHeight,1523this._lastRenderData ? this._lastRenderData.renderedLayout : null1524);1525this._slider.setDisplay(layout.sliderNeeded ? 'block' : 'none');1526this._slider.setTop(layout.sliderTop);1527this._slider.setHeight(layout.sliderHeight);15281529// Compute horizontal slider coordinates1530this._sliderHorizontal.setLeft(0);1531this._sliderHorizontal.setWidth(this._model.options.minimapWidth);1532this._sliderHorizontal.setTop(0);1533this._sliderHorizontal.setHeight(layout.sliderHeight);15341535this.renderDecorations(layout);1536this._lastRenderData = this.renderLines(layout);1537}15381539private renderDecorations(layout: MinimapLayout) {1540if (this._renderDecorations) {1541this._renderDecorations = false;1542const selections = this._model.getSelections();1543selections.sort(Range.compareRangesUsingStarts);15441545const decorations = this._model.getMinimapDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);1546decorations.sort((a, b) => (a.options.zIndex || 0) - (b.options.zIndex || 0));15471548const { canvasInnerWidth, canvasInnerHeight } = this._model.options;1549const minimapLineHeight = this._model.options.minimapLineHeight;1550const minimapCharWidth = this._model.options.minimapCharWidth;1551const tabSize = this._model.getOptions().tabSize;1552const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;15531554canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight);15551556// We first need to render line highlights and then render decorations on top of those.1557// But we need to pick a single color for each line, and use that as a line highlight.1558// This needs to be the color of the decoration with the highest `zIndex`, but priority1559// is given to the selection.15601561const highlightedLines = new ContiguousLineMap<boolean>(layout.startLineNumber, layout.endLineNumber, false);1562this._renderSelectionLineHighlights(canvasContext, selections, highlightedLines, layout, minimapLineHeight);1563this._renderDecorationsLineHighlights(canvasContext, decorations, highlightedLines, layout, minimapLineHeight);15641565const lineOffsetMap = new ContiguousLineMap<number[] | null>(layout.startLineNumber, layout.endLineNumber, null);1566this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);1567this._renderDecorationsHighlights(canvasContext, decorations, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);1568this._renderSectionHeaders(layout);1569}1570}15711572private _renderSelectionLineHighlights(1573canvasContext: CanvasRenderingContext2D,1574selections: Selection[],1575highlightedLines: ContiguousLineMap<boolean>,1576layout: MinimapLayout,1577minimapLineHeight: number1578): void {1579if (!this._selectionColor || this._selectionColor.isTransparent()) {1580return;1581}15821583canvasContext.fillStyle = this._selectionColor.transparent(0.5).toString();15841585let y1 = 0;1586let y2 = 0;15871588for (const selection of selections) {1589const intersection = layout.intersectWithViewport(selection);1590if (!intersection) {1591// entirely outside minimap's viewport1592continue;1593}1594const [startLineNumber, endLineNumber] = intersection;15951596for (let line = startLineNumber; line <= endLineNumber; line++) {1597highlightedLines.set(line, true);1598}15991600const yy1 = layout.getYForLineNumber(startLineNumber, minimapLineHeight);1601const yy2 = layout.getYForLineNumber(endLineNumber, minimapLineHeight);16021603if (y2 >= yy1) {1604// merge into previous1605y2 = yy2;1606} else {1607if (y2 > y1) {1608// flush1609canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1);1610}1611y1 = yy1;1612y2 = yy2;1613}1614}16151616if (y2 > y1) {1617// flush1618canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1);1619}1620}16211622private _renderDecorationsLineHighlights(1623canvasContext: CanvasRenderingContext2D,1624decorations: ViewModelDecoration[],1625highlightedLines: ContiguousLineMap<boolean>,1626layout: MinimapLayout,1627minimapLineHeight: number1628): void {16291630const highlightColors = new Map<string, string>();16311632// Loop backwards to hit first decorations with higher `zIndex`1633for (let i = decorations.length - 1; i >= 0; i--) {1634const decoration = decorations[i];16351636const minimapOptions = <ModelDecorationMinimapOptions | null | undefined>decoration.options.minimap;1637if (!minimapOptions || minimapOptions.position !== MinimapPosition.Inline) {1638continue;1639}16401641const intersection = layout.intersectWithViewport(decoration.range);1642if (!intersection) {1643// entirely outside minimap's viewport1644continue;1645}1646const [startLineNumber, endLineNumber] = intersection;16471648const decorationColor = minimapOptions.getColor(this._theme.value);1649if (!decorationColor || decorationColor.isTransparent()) {1650continue;1651}16521653let highlightColor = highlightColors.get(decorationColor.toString());1654if (!highlightColor) {1655highlightColor = decorationColor.transparent(0.5).toString();1656highlightColors.set(decorationColor.toString(), highlightColor);1657}16581659canvasContext.fillStyle = highlightColor;1660for (let line = startLineNumber; line <= endLineNumber; line++) {1661if (highlightedLines.has(line)) {1662continue;1663}1664highlightedLines.set(line, true);1665const y = layout.getYForLineNumber(startLineNumber, minimapLineHeight);1666canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y, canvasContext.canvas.width, minimapLineHeight);1667}1668}1669}16701671private _renderSelectionsHighlights(1672canvasContext: CanvasRenderingContext2D,1673selections: Selection[],1674lineOffsetMap: ContiguousLineMap<number[] | null>,1675layout: MinimapLayout,1676lineHeight: number,1677tabSize: number,1678characterWidth: number,1679canvasInnerWidth: number1680): void {1681if (!this._selectionColor || this._selectionColor.isTransparent()) {1682return;1683}1684for (const selection of selections) {1685const intersection = layout.intersectWithViewport(selection);1686if (!intersection) {1687// entirely outside minimap's viewport1688continue;1689}1690const [startLineNumber, endLineNumber] = intersection;16911692for (let line = startLineNumber; line <= endLineNumber; line++) {1693this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth, canvasInnerWidth);1694}1695}1696}16971698private _renderDecorationsHighlights(1699canvasContext: CanvasRenderingContext2D,1700decorations: ViewModelDecoration[],1701lineOffsetMap: ContiguousLineMap<number[] | null>,1702layout: MinimapLayout,1703minimapLineHeight: number,1704tabSize: number,1705characterWidth: number,1706canvasInnerWidth: number1707): void {1708// Loop forwards to hit first decorations with lower `zIndex`1709for (const decoration of decorations) {17101711const minimapOptions = <ModelDecorationMinimapOptions | null | undefined>decoration.options.minimap;1712if (!minimapOptions) {1713continue;1714}17151716const intersection = layout.intersectWithViewport(decoration.range);1717if (!intersection) {1718// entirely outside minimap's viewport1719continue;1720}1721const [startLineNumber, endLineNumber] = intersection;17221723const decorationColor = minimapOptions.getColor(this._theme.value);1724if (!decorationColor || decorationColor.isTransparent()) {1725continue;1726}17271728for (let line = startLineNumber; line <= endLineNumber; line++) {1729switch (minimapOptions.position) {17301731case MinimapPosition.Inline:1732this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration.range, decorationColor, layout, line, minimapLineHeight, minimapLineHeight, tabSize, characterWidth, canvasInnerWidth);1733continue;17341735case MinimapPosition.Gutter: {1736const y = layout.getYForLineNumber(line, minimapLineHeight);1737const x = 2;1738this.renderDecoration(canvasContext, decorationColor, x, y, GUTTER_DECORATION_WIDTH, minimapLineHeight);1739continue;1740}1741}1742}1743}1744}17451746private renderDecorationOnLine(1747canvasContext: CanvasRenderingContext2D,1748lineOffsetMap: ContiguousLineMap<number[] | null>,1749decorationRange: Range,1750decorationColor: Color | undefined,1751layout: MinimapLayout,1752lineNumber: number,1753height: number,1754minimapLineHeight: number,1755tabSize: number,1756charWidth: number,1757canvasInnerWidth: number1758): void {1759const y = layout.getYForLineNumber(lineNumber, minimapLineHeight);17601761// Skip rendering the line if it's vertically outside our viewport1762if (y + height < 0 || y > this._model.options.canvasInnerHeight) {1763return;1764}17651766const { startLineNumber, endLineNumber } = decorationRange;1767const startColumn = (startLineNumber === lineNumber ? decorationRange.startColumn : 1);1768const endColumn = (endLineNumber === lineNumber ? decorationRange.endColumn : this._model.getLineMaxColumn(lineNumber));17691770const x1 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, startColumn, tabSize, charWidth, canvasInnerWidth);1771const x2 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, endColumn, tabSize, charWidth, canvasInnerWidth);17721773this.renderDecoration(canvasContext, decorationColor, x1, y, x2 - x1, height);1774}17751776private getXOffsetForPosition(1777lineOffsetMap: ContiguousLineMap<number[] | null>,1778lineNumber: number,1779column: number,1780tabSize: number,1781charWidth: number,1782canvasInnerWidth: number1783): number {1784if (column === 1) {1785return MINIMAP_GUTTER_WIDTH;1786}17871788const minimumXOffset = (column - 1) * charWidth;1789if (minimumXOffset >= canvasInnerWidth) {1790// there is no need to look at actual characters,1791// as this column is certainly after the minimap width1792return canvasInnerWidth;1793}17941795// Cache line offset data so that it is only read once per line1796let lineIndexToXOffset = lineOffsetMap.get(lineNumber);1797if (!lineIndexToXOffset) {1798const lineData = this._model.getLineContent(lineNumber);1799lineIndexToXOffset = [MINIMAP_GUTTER_WIDTH];1800let prevx = MINIMAP_GUTTER_WIDTH;1801for (let i = 1; i < lineData.length + 1; i++) {1802const charCode = lineData.charCodeAt(i - 1);1803const dx = charCode === CharCode.Tab1804? tabSize * charWidth1805: strings.isFullWidthCharacter(charCode)1806? 2 * charWidth1807: charWidth;18081809const x = prevx + dx;1810if (x >= canvasInnerWidth) {1811// no need to keep on going, as we've hit the canvas width1812lineIndexToXOffset[i] = canvasInnerWidth;1813break;1814}18151816lineIndexToXOffset[i] = x;1817prevx = x;1818}18191820lineOffsetMap.set(lineNumber, lineIndexToXOffset);1821}18221823if (column - 1 < lineIndexToXOffset.length) {1824return lineIndexToXOffset[column - 1];1825}1826// goes over the canvas width1827return canvasInnerWidth;1828}18291830private renderDecoration(canvasContext: CanvasRenderingContext2D, decorationColor: Color | undefined, x: number, y: number, width: number, height: number) {1831canvasContext.fillStyle = decorationColor && decorationColor.toString() || '';1832canvasContext.fillRect(x, y, width, height);1833}18341835private _renderSectionHeaders(layout: MinimapLayout) {1836const minimapLineHeight = this._model.options.minimapLineHeight;1837const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize;1838const sectionHeaderLetterSpacing = this._model.options.sectionHeaderLetterSpacing;1839const backgroundFillHeight = sectionHeaderFontSize * 1.5;1840const { canvasInnerWidth } = this._model.options;18411842const backgroundColor = this._model.options.backgroundColor;1843const backgroundFill = `rgb(${backgroundColor.r} ${backgroundColor.g} ${backgroundColor.b} / .7)`;1844const foregroundColor = this._model.options.sectionHeaderFontColor;1845const foregroundFill = `rgb(${foregroundColor.r} ${foregroundColor.g} ${foregroundColor.b})`;1846const separatorStroke = foregroundFill;18471848const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;1849canvasContext.letterSpacing = sectionHeaderLetterSpacing + 'px';1850canvasContext.font = '500 ' + sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily;1851canvasContext.strokeStyle = separatorStroke;1852canvasContext.lineWidth = 0.4;18531854const decorations = this._model.getSectionHeaderDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);1855decorations.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber);18561857const fitWidth = InnerMinimap._fitSectionHeader.bind(null, canvasContext,1858canvasInnerWidth - MINIMAP_GUTTER_WIDTH);18591860for (const decoration of decorations) {1861const y = layout.getYForLineNumber(decoration.range.startLineNumber, minimapLineHeight) + sectionHeaderFontSize;1862const backgroundFillY = y - sectionHeaderFontSize;1863const separatorY = backgroundFillY + 2;1864const headerText = this._model.getSectionHeaderText(decoration, fitWidth);18651866InnerMinimap._renderSectionLabel(1867canvasContext,1868headerText,1869decoration.options.minimap?.sectionHeaderStyle === MinimapSectionHeaderStyle.Underlined,1870backgroundFill,1871foregroundFill,1872canvasInnerWidth,1873backgroundFillY,1874backgroundFillHeight,1875y,1876separatorY);1877}1878}18791880private static _fitSectionHeader(1881target: CanvasRenderingContext2D,1882maxWidth: number,1883headerText: string,1884): string {1885if (!headerText) {1886return headerText;1887}18881889const ellipsis = '…';1890const width = target.measureText(headerText).width;1891const ellipsisWidth = target.measureText(ellipsis).width;18921893if (width <= maxWidth || width <= ellipsisWidth) {1894return headerText;1895}18961897const len = headerText.length;1898const averageCharWidth = width / headerText.length;1899const maxCharCount = Math.floor((maxWidth - ellipsisWidth) / averageCharWidth) - 1;19001901// Find a halfway point that isn't after whitespace1902let halfCharCount = Math.ceil(maxCharCount / 2);1903while (halfCharCount > 0 && /\s/.test(headerText[halfCharCount - 1])) {1904--halfCharCount;1905}19061907// Split with ellipsis1908return headerText.substring(0, halfCharCount)1909+ ellipsis + headerText.substring(len - (maxCharCount - halfCharCount));1910}19111912private static _renderSectionLabel(1913target: CanvasRenderingContext2D,1914headerText: string | null,1915hasSeparatorLine: boolean,1916backgroundFill: string,1917foregroundFill: string,1918minimapWidth: number,1919backgroundFillY: number,1920backgroundFillHeight: number,1921textY: number,1922separatorY: number1923): void {1924if (headerText) {1925target.fillStyle = backgroundFill;1926target.fillRect(0, backgroundFillY, minimapWidth, backgroundFillHeight);19271928target.fillStyle = foregroundFill;1929target.fillText(headerText, MINIMAP_GUTTER_WIDTH, textY);1930}19311932if (hasSeparatorLine) {1933target.beginPath();1934target.moveTo(0, separatorY);1935target.lineTo(minimapWidth, separatorY);1936target.closePath();1937target.stroke();1938}1939}19401941private renderLines(layout: MinimapLayout): RenderData | null {1942const startLineNumber = layout.startLineNumber;1943const endLineNumber = layout.endLineNumber;1944const minimapLineHeight = this._model.options.minimapLineHeight;19451946// Check if nothing changed w.r.t. lines from last frame1947if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {1948const _lastData = this._lastRenderData._get();1949// Nice!! Nothing changed from last frame1950return new RenderData(layout, _lastData.imageData, _lastData.lines);1951}19521953// Oh well!! We need to repaint some lines...19541955const imageData = this._getBuffer();1956if (!imageData) {1957// 0 width or 0 height canvas, nothing to do1958return null;1959}19601961// Render untouched lines by using last rendered data.1962const [_dirtyY1, _dirtyY2, needed] = InnerMinimap._renderUntouchedLines(1963imageData,1964layout.topPaddingLineCount,1965startLineNumber,1966endLineNumber,1967minimapLineHeight,1968this._lastRenderData1969);19701971// Fetch rendering info from view model for rest of lines that need rendering.1972const lineInfo = this._model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);1973const tabSize = this._model.getOptions().tabSize;1974const defaultBackground = this._model.options.defaultBackgroundColor;1975const background = this._model.options.backgroundColor;1976const foregroundAlpha = this._model.options.foregroundAlpha;1977const tokensColorTracker = this._model.tokensColorTracker;1978const useLighterFont = tokensColorTracker.backgroundIsLight();1979const renderMinimap = this._model.options.renderMinimap;1980const charRenderer = this._model.options.charRenderer();1981const fontScale = this._model.options.fontScale;1982const minimapCharWidth = this._model.options.minimapCharWidth;19831984const baseCharHeight = (renderMinimap === RenderMinimap.Text ? Constants.BASE_CHAR_HEIGHT : Constants.BASE_CHAR_HEIGHT + 1);1985const renderMinimapLineHeight = baseCharHeight * fontScale;1986const innerLinePadding = (minimapLineHeight > renderMinimapLineHeight ? Math.floor((minimapLineHeight - renderMinimapLineHeight) / 2) : 0);19871988// Render the rest of lines1989const backgroundA = background.a / 255;1990const renderBackground = new RGBA8(1991Math.round((background.r - defaultBackground.r) * backgroundA + defaultBackground.r),1992Math.round((background.g - defaultBackground.g) * backgroundA + defaultBackground.g),1993Math.round((background.b - defaultBackground.b) * backgroundA + defaultBackground.b),19942551995);1996let dy = layout.topPaddingLineCount * minimapLineHeight;1997const renderedLines: MinimapLine[] = [];1998for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {1999if (needed[lineIndex]) {2000InnerMinimap._renderLine(2001imageData,2002renderBackground,2003background.a,2004useLighterFont,2005renderMinimap,2006minimapCharWidth,2007tokensColorTracker,2008foregroundAlpha,2009charRenderer,2010dy,2011innerLinePadding,2012tabSize,2013lineInfo[lineIndex]!,2014fontScale,2015minimapLineHeight2016);2017}2018renderedLines[lineIndex] = new MinimapLine(dy);2019dy += minimapLineHeight;2020}20212022const dirtyY1 = (_dirtyY1 === -1 ? 0 : _dirtyY1);2023const dirtyY2 = (_dirtyY2 === -1 ? imageData.height : _dirtyY2);2024const dirtyHeight = dirtyY2 - dirtyY1;20252026// Finally, paint to the canvas2027const ctx = this._canvas.domNode.getContext('2d')!;2028ctx.putImageData(imageData, 0, 0, 0, dirtyY1, imageData.width, dirtyHeight);20292030// Save rendered data for reuse on next frame if possible2031return new RenderData(2032layout,2033imageData,2034renderedLines2035);2036}20372038private static _renderUntouchedLines(2039target: ImageData,2040topPaddingLineCount: number,2041startLineNumber: number,2042endLineNumber: number,2043minimapLineHeight: number,2044lastRenderData: RenderData | null,2045): [number, number, boolean[]] {20462047const needed: boolean[] = [];2048if (!lastRenderData) {2049for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {2050needed[i] = true;2051}2052return [-1, -1, needed];2053}20542055const _lastData = lastRenderData._get();2056const lastTargetData = _lastData.imageData.data;2057const lastStartLineNumber = _lastData.rendLineNumberStart;2058const lastLines = _lastData.lines;2059const lastLinesLength = lastLines.length;2060const WIDTH = target.width;2061const targetData = target.data;20622063const maxDestPixel = (endLineNumber - startLineNumber + 1) * minimapLineHeight * WIDTH * 4;2064let dirtyPixel1 = -1; // the pixel offset up to which all the data is equal to the prev frame2065let dirtyPixel2 = -1; // the pixel offset after which all the data is equal to the prev frame20662067let copySourceStart = -1;2068let copySourceEnd = -1;2069let copyDestStart = -1;2070let copyDestEnd = -1;20712072let dest_dy = topPaddingLineCount * minimapLineHeight;2073for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {2074const lineIndex = lineNumber - startLineNumber;2075const lastLineIndex = lineNumber - lastStartLineNumber;2076const source_dy = (lastLineIndex >= 0 && lastLineIndex < lastLinesLength ? lastLines[lastLineIndex].dy : -1);20772078if (source_dy === -1) {2079needed[lineIndex] = true;2080dest_dy += minimapLineHeight;2081continue;2082}20832084const sourceStart = source_dy * WIDTH * 4;2085const sourceEnd = (source_dy + minimapLineHeight) * WIDTH * 4;2086const destStart = dest_dy * WIDTH * 4;2087const destEnd = (dest_dy + minimapLineHeight) * WIDTH * 4;20882089if (copySourceEnd === sourceStart && copyDestEnd === destStart) {2090// contiguous zone => extend copy request2091copySourceEnd = sourceEnd;2092copyDestEnd = destEnd;2093} else {2094if (copySourceStart !== -1) {2095// flush existing copy request2096targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);2097if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {2098dirtyPixel1 = copySourceEnd;2099}2100if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {2101dirtyPixel2 = copySourceStart;2102}2103}2104copySourceStart = sourceStart;2105copySourceEnd = sourceEnd;2106copyDestStart = destStart;2107copyDestEnd = destEnd;2108}21092110needed[lineIndex] = false;2111dest_dy += minimapLineHeight;2112}21132114if (copySourceStart !== -1) {2115// flush existing copy request2116targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);2117if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {2118dirtyPixel1 = copySourceEnd;2119}2120if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {2121dirtyPixel2 = copySourceStart;2122}2123}21242125const dirtyY1 = (dirtyPixel1 === -1 ? -1 : dirtyPixel1 / (WIDTH * 4));2126const dirtyY2 = (dirtyPixel2 === -1 ? -1 : dirtyPixel2 / (WIDTH * 4));21272128return [dirtyY1, dirtyY2, needed];2129}21302131private static _renderLine(2132target: ImageData,2133backgroundColor: RGBA8,2134backgroundAlpha: number,2135useLighterFont: boolean,2136renderMinimap: RenderMinimap,2137charWidth: number,2138colorTracker: MinimapTokensColorTracker,2139foregroundAlpha: number,2140minimapCharRenderer: MinimapCharRenderer,2141dy: number,2142innerLinePadding: number,2143tabSize: number,2144lineData: ViewLineData,2145fontScale: number,2146minimapLineHeight: number2147): void {2148const content = lineData.content;2149const tokens = lineData.tokens;2150const maxDx = target.width - charWidth;2151const force1pxHeight = (minimapLineHeight === 1);21522153let dx = MINIMAP_GUTTER_WIDTH;2154let charIndex = 0;2155let tabsCharDelta = 0;21562157for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {2158const tokenEndIndex = tokens.getEndOffset(tokenIndex);2159const tokenColorId = tokens.getForeground(tokenIndex);2160const tokenColor = colorTracker.getColor(tokenColorId);21612162for (; charIndex < tokenEndIndex; charIndex++) {2163if (dx > maxDx) {2164// hit edge of minimap2165return;2166}2167const charCode = content.charCodeAt(charIndex);21682169if (charCode === CharCode.Tab) {2170const insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;2171tabsCharDelta += insertSpacesCount - 1;2172// No need to render anything since tab is invisible2173dx += insertSpacesCount * charWidth;2174} else if (charCode === CharCode.Space) {2175// No need to render anything since space is invisible2176dx += charWidth;2177} else {2178// Render twice for a full width character2179const count = strings.isFullWidthCharacter(charCode) ? 2 : 1;21802181for (let i = 0; i < count; i++) {2182if (renderMinimap === RenderMinimap.Blocks) {2183minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, force1pxHeight);2184} else { // RenderMinimap.Text2185minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, fontScale, useLighterFont, force1pxHeight);2186}21872188dx += charWidth;21892190if (dx > maxDx) {2191// hit edge of minimap2192return;2193}2194}2195}2196}2197}2198}2199}22002201class ContiguousLineMap<T> {22022203private readonly _startLineNumber: number;2204private readonly _endLineNumber: number;2205private readonly _defaultValue: T;2206private readonly _values: T[];22072208constructor(startLineNumber: number, endLineNumber: number, defaultValue: T) {2209this._startLineNumber = startLineNumber;2210this._endLineNumber = endLineNumber;2211this._defaultValue = defaultValue;2212this._values = [];2213for (let i = 0, count = this._endLineNumber - this._startLineNumber + 1; i < count; i++) {2214this._values[i] = defaultValue;2215}2216}22172218public has(lineNumber: number): boolean {2219return (this.get(lineNumber) !== this._defaultValue);2220}22212222public set(lineNumber: number, value: T): void {2223if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) {2224return;2225}2226this._values[lineNumber - this._startLineNumber] = value;2227}22282229public get(lineNumber: number): T {2230if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) {2231return this._defaultValue;2232}2233return this._values[lineNumber - this._startLineNumber];2234}2235}2236223722382239