UNPKG

@21epub/epub-thirdparty

Version:
1,038 lines 71.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import './minimap.css'; import * as dom from '../../../../base/browser/dom.js'; import { createFastDomNode } from '../../../../base/browser/fastDomNode.js'; import { GlobalMouseMoveMonitor, standardMouseMoveMerger } from '../../../../base/browser/globalMouseMoveMonitor.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import * as platform from '../../../../base/common/platform.js'; import * as strings from '../../../../base/common/strings.js'; import { RenderedLinesCollection } from '../../view/viewLayer.js'; import { PartFingerprints, ViewPart } from '../../view/viewPart.js'; import { MINIMAP_GUTTER_WIDTH, EditorLayoutInfoComputer } from '../../../common/config/editorOptions.js'; import { Range } from '../../../common/core/range.js'; import { RGBA8 } from '../../../common/core/rgba.js'; import { MinimapTokensColorTracker } from '../../../common/viewModel/minimapTokensColorTracker.js'; import { ViewModelDecoration } from '../../../common/viewModel/viewModel.js'; import { minimapSelection, scrollbarShadow, minimapBackground, minimapSliderBackground, minimapSliderHoverBackground, minimapSliderActiveBackground, minimapForegroundOpacity } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { Selection } from '../../../common/core/selection.js'; import { EventType, Gesture } from '../../../../base/browser/touch.js'; import { MinimapCharRendererFactory } from './minimapCharRendererFactory.js'; import { MinimapPosition } from '../../../common/model.js'; import { once } from '../../../../base/common/functional.js'; /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" */ const MOUSE_DRAG_RESET_DISTANCE = 140; const GUTTER_DECORATION_WIDTH = 2; class MinimapOptions { constructor(configuration, theme, tokensColorTracker) { const options = configuration.options; const pixelRatio = options.get(127 /* pixelRatio */); const layoutInfo = options.get(129 /* layoutInfo */); const minimapLayout = layoutInfo.minimap; const fontInfo = options.get(43 /* fontInfo */); const minimapOpts = options.get(64 /* minimap */); this.renderMinimap = minimapLayout.renderMinimap; this.size = minimapOpts.size; this.minimapHeightIsEditorHeight = minimapLayout.minimapHeightIsEditorHeight; this.scrollBeyondLastLine = options.get(93 /* scrollBeyondLastLine */); this.showSlider = minimapOpts.showSlider; this.pixelRatio = pixelRatio; this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this.lineHeight = options.get(58 /* 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 = 1 /* BASE_CHAR_WIDTH */ * this.fontScale; this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.defaultBackgroundColor = tokensColorTracker.getColor(2 /* DefaultBackground */); this.backgroundColor = MinimapOptions._getMinimapBackground(theme, this.defaultBackgroundColor); this.foregroundAlpha = MinimapOptions._getMinimapForegroundOpacity(theme); } static _getMinimapBackground(theme, defaultBackgroundColor) { 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; } static _getMinimapForegroundOpacity(theme) { const themeColor = theme.getColor(minimapForegroundOpacity); if (themeColor) { return RGBA8._clamp(Math.round(255 * themeColor.rgba.a)); } return 255; } equals(other) { return (this.renderMinimap === other.renderMinimap && this.size === other.size && this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight && this.scrollBeyondLastLine === other.scrollBeyondLastLine && this.showSlider === other.showSlider && 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.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor) && this.backgroundColor && this.backgroundColor.equals(other.backgroundColor) && this.foregroundAlpha === other.foregroundAlpha); } } class MinimapLayout { constructor(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber) { this.scrollTop = scrollTop; this.scrollHeight = scrollHeight; this.sliderNeeded = sliderNeeded; this._computedSliderRatio = computedSliderRatio; this.sliderTop = sliderTop; this.sliderHeight = sliderHeight; this.startLineNumber = startLineNumber; this.endLineNumber = endLineNumber; } /** * Compute a desired `scrollPosition` such that the slider moves by `delta`. */ getDesiredScrollTopFromDelta(delta) { return Math.round(this.scrollTop + delta / this._computedSliderRatio); } getDesiredScrollTopFromTouchLocation(pageY) { return Math.round((pageY - this.sliderHeight / 2) / this._computedSliderRatio); } static create(options, viewportStartLineNumber, viewportEndLineNumber, viewportStartLineNumberVerticalOffset, viewportHeight, viewportContainsWhitespaceGaps, lineCount, realLineCount, scrollTop, scrollHeight, previousLayout) { const pixelRatio = options.pixelRatio; const minimapLineHeight = options.minimapLineHeight; const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight); const lineHeight = options.lineHeight; if (options.minimapHeightIsEditorHeight) { const logicalScrollHeight = (realLineCount * options.lineHeight + (options.scrollBeyondLastLine ? viewportHeight - options.lineHeight : 0)); 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); return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, 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; 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); } let maxMinimapSliderTop; if (options.scrollBeyondLastLine) { // The minimap slider, when dragged all the way down, will contain the last line at its top maxMinimapSliderTop = (lineCount - 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, 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); let extraLinesAtTheBottom = 0; if (options.scrollBeyondLastLine) { const expectedViewportLineCount = viewportHeight / lineHeight; extraLinesAtTheBottom = expectedViewportLineCount - 1; } if (minimapLinesFitting >= lineCount + extraLinesAtTheBottom) { // All lines fit in the minimap const startLineNumber = 1; const endLineNumber = lineCount; const sliderNeeded = (maxMinimapSliderTop > 0); return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); } else { let startLineNumber = Math.max(1, Math.floor(viewportStartLineNumber - sliderTop * pixelRatio / minimapLineHeight)); // 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); } if (previousLayout.scrollTop < scrollTop) { // Scrolling down => never decrease `startLineNumber` startLineNumber = Math.max(startLineNumber, previousLayout.startLineNumber); } } const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1); const partialLine = (scrollTop - viewportStartLineNumberVerticalOffset) / lineHeight; const sliderTopAligned = (viewportStartLineNumber - startLineNumber + partialLine) * minimapLineHeight / pixelRatio; return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTopAligned, sliderHeight, startLineNumber, endLineNumber); } } } class MinimapLine { constructor(dy) { this.dy = dy; } onContentChanged() { this.dy = -1; } onTokensChanged() { this.dy = -1; } } MinimapLine.INVALID = new MinimapLine(-1); class RenderData { constructor(renderedLayout, imageData, lines) { 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. */ linesEquals(layout) { 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 */ scrollEquals(layout) { return this.renderedLayout.startLineNumber === layout.startLineNumber && this.renderedLayout.endLineNumber === layout.endLineNumber; } _get() { const tmp = this._renderedLines._get(); return { imageData: this._imageData, rendLineNumberStart: tmp.rendLineNumberStart, lines: tmp.lines }; } onLinesChanged(changeFromLineNumber, changeToLineNumber) { return this._renderedLines.onLinesChanged(changeFromLineNumber, changeToLineNumber); } onLinesDeleted(deleteFromLineNumber, deleteToLineNumber) { this._renderedLines.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber); } onLinesInserted(insertFromLineNumber, insertToLineNumber) { this._renderedLines.onLinesInserted(insertFromLineNumber, insertToLineNumber); } onTokensChanged(ranges) { 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 { constructor(ctx, WIDTH, HEIGHT, background) { this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background); this._buffers = [ ctx.createImageData(WIDTH, HEIGHT), ctx.createImageData(WIDTH, HEIGHT) ]; this._lastUsedBuffer = 0; } getBuffer() { // rotate buffers this._lastUsedBuffer = 1 - this._lastUsedBuffer; const result = this._buffers[this._lastUsedBuffer]; // fill with background color result.data.set(this._backgroundFillData); return result; } static _createBackgroundFillData(WIDTH, HEIGHT, background) { 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; } } class MinimapSamplingState { constructor(samplingRatio, minimapLines) { this.samplingRatio = samplingRatio; this.minimapLines = minimapLines; } static compute(options, viewLineCount, oldSamplingState) { if (options.renderMinimap === 0 /* 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 pixelRatio = options.pixelRatio; const lineHeight = options.lineHeight; const scrollBeyondLastLine = options.scrollBeyondLastLine; const { minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({ viewLineCount: viewLineCount, scrollBeyondLastLine: scrollBeyondLastLine, height: options.editorHeight, lineHeight: lineHeight, pixelRatio: pixelRatio }); const ratio = viewLineCount / minimapLineCount; const halfRatio = ratio / 2; if (!oldSamplingState || oldSamplingState.minimapLines.length === 0) { let result = []; 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; let result = []; 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 = []; let lastEvent = 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; 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]; } modelLineToMinimapLine(lineNumber) { 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. */ modelLineRangeToMinimapLineRange(fromLineNumber, toLineNumber) { 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. */ decorationLineRangeToMinimapLineRange(startLineNumber, endLineNumber) { 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]; } onLinesDeleted(e) { // 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]; } onLinesInserted(e) { // 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 { constructor(context) { 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.model.getLineCount(), null); this._samplingState = samplingState; this._shouldCheckSampling = false; this._actual = new InnerMinimap(context.theme, this); } dispose() { this._actual.dispose(); super.dispose(); } getDomNode() { return this._actual.getDomNode(); } _onOptionsMaybeChanged() { 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 onConfigurationChanged(e) { return this._onOptionsMaybeChanged(); } onCursorStateChanged(e) { this._selections = e.selections; this._minimapSelections = null; return this._actual.onSelectionChanged(); } onDecorationsChanged(e) { if (e.affectsMinimap) { return this._actual.onDecorationsChanged(); } return false; } onFlushed(e) { if (this._samplingState) { this._shouldCheckSampling = true; } return this._actual.onFlushed(); } onLinesChanged(e) { if (this._samplingState) { const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(e.fromLineNumber, e.toLineNumber); if (minimapLineRange) { return this._actual.onLinesChanged(minimapLineRange[0], minimapLineRange[1]); } else { return false; } } else { return this._actual.onLinesChanged(e.fromLineNumber, e.toLineNumber); } } onLinesDeleted(e) { if (this._samplingState) { const [changeStartIndex, changeEndIndex] = this._samplingState.onLinesDeleted(e); if (changeStartIndex <= changeEndIndex) { this._actual.onLinesChanged(changeStartIndex + 1, changeEndIndex + 1); } this._shouldCheckSampling = true; return true; } else { return this._actual.onLinesDeleted(e.fromLineNumber, e.toLineNumber); } } onLinesInserted(e) { if (this._samplingState) { this._samplingState.onLinesInserted(e); this._shouldCheckSampling = true; return true; } else { return this._actual.onLinesInserted(e.fromLineNumber, e.toLineNumber); } } onScrollChanged(e) { return this._actual.onScrollChanged(); } onThemeChanged(e) { this._context.model.invalidateMinimapColorCache(); this._actual.onThemeChanged(); this._onOptionsMaybeChanged(); return true; } onTokensChanged(e) { if (this._samplingState) { let ranges = []; 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); } } onTokensColorsChanged(e) { this._onOptionsMaybeChanged(); return this._actual.onTokensColorsChanged(); } onZonesChanged(e) { return this._actual.onZonesChanged(); } // --- end event handlers prepareRender(ctx) { if (this._shouldCheckSampling) { this._shouldCheckSampling = false; this._recreateLineSampling(); } } render(ctx) { 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 = { 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 _recreateLineSampling() { this._minimapSelections = null; const wasSampling = Boolean(this._samplingState); const [samplingState, events] = MinimapSamplingState.compute(this.options, this._context.model.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; } } } } getLineCount() { if (this._samplingState) { return this._samplingState.minimapLines.length; } return this._context.model.getLineCount(); } getRealLineCount() { return this._context.model.getLineCount(); } getLineContent(lineNumber) { if (this._samplingState) { return this._context.model.getLineContent(this._samplingState.minimapLines[lineNumber - 1]); } return this._context.model.getLineContent(lineNumber); } getLineMaxColumn(lineNumber) { if (this._samplingState) { return this._context.model.getLineMaxColumn(this._samplingState.minimapLines[lineNumber - 1]); } return this._context.model.getLineMaxColumn(lineNumber); } getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed) { if (this._samplingState) { let result = []; for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) { if (needed[lineIndex]) { result[lineIndex] = this._context.model.getViewLineData(this._samplingState.minimapLines[startLineNumber + lineIndex - 1]); } else { result[lineIndex] = null; } } return result; } return this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed).data; } getSelections() { 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; } getMinimapDecorationsInViewport(startLineNumber, endLineNumber) { let visibleRange; 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.model.getLineMaxColumn(modelEndLineNumber)); } else { visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.model.getLineMaxColumn(endLineNumber)); } const decorations = this._context.model.getDecorationsInViewport(visibleRange); if (this._samplingState) { let result = []; 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; } getOptions() { return this._context.model.getTextModelOptions(); } revealLineNumber(lineNumber) { if (this._samplingState) { lineNumber = this._samplingState.minimapLines[lineNumber - 1]; } this._context.model.revealRange('mouse', false, new Range(lineNumber, 1, lineNumber, 1), 1 /* Center */, 0 /* Smooth */); } setScrollTop(scrollTop) { this._context.model.setScrollPosition({ scrollTop: scrollTop }, 1 /* Immediate */); } } class InnerMinimap extends Disposable { constructor(theme, model) { super(); this._renderDecorations = false; this._gestureInProgress = false; 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, 8 /* 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._mouseDownListener = dom.addStandardDisposableListener(this._domNode.domNode, 'mousedown', (e) => { e.preventDefault(); const renderMinimap = this._model.options.renderMinimap; if (renderMinimap === 0 /* None */) { return; } if (!this._lastRenderData) { return; } if (this._model.options.size !== 'proportional') { if (e.leftButton && 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.buttons, e.posx, initialPosY, e.posy, this._lastRenderData.renderedLayout); } return; } const minimapLineHeight = this._model.options.minimapLineHeight; const internalOffsetY = (this._model.options.canvasInnerHeight / this._model.options.canvasOuterHeight) * e.browserEvent.offsetY; const lineIndex = Math.floor(internalOffsetY / minimapLineHeight); let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber; lineNumber = Math.min(lineNumber, this._model.getLineCount()); this._model.revealLineNumber(lineNumber); }); this._sliderMouseMoveMonitor = new GlobalMouseMoveMonitor(); this._sliderMouseDownListener = dom.addStandardDisposableListener(this._slider.domNode, 'mousedown', (e) => { e.preventDefault(); e.stopPropagation(); if (e.leftButton && this._lastRenderData) { this._startSliderDragging(e.buttons, e.posx, e.posy, e.posy, this._lastRenderData.renderedLayout); } }); this._gestureDisposable = Gesture.addTarget(this._domNode.domNode); this._sliderTouchStartListener = dom.addDisposableListener(this._domNode.domNode, EventType.Start, (e) => { 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) => { e.preventDefault(); e.stopPropagation(); if (this._lastRenderData && this._gestureInProgress) { this.scrollDueToTouchEvent(e); } }, { passive: false }); this._sliderTouchEndListener = dom.addStandardDisposableListener(this._domNode.domNode, EventType.End, (e) => { e.preventDefault(); e.stopPropagation(); this._gestureInProgress = false; this._slider.toggleClassName('active', false); }); } _startSliderDragging(initialButtons, initialPosX, initialPosY, posy, initialSliderState) { this._slider.toggleClassName('active', true); const handleMouseMove = (posy, posx) => { const mouseOrthogonalDelta = Math.abs(posx - initialPosX); if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { // The mouse has wondered away from the scrollbar => reset dragging this._model.setScrollTop(initialSliderState.scrollTop); return; } const mouseDelta = posy - initialPosY; this._model.setScrollTop(initialSliderState.getDesiredScrollTopFromDelta(mouseDelta)); }; if (posy !== initialPosY) { handleMouseMove(posy, initialPosX); } this._sliderMouseMoveMonitor.startMonitoring(this._slider.domNode, initialButtons, standardMouseMoveMerger, (mouseMoveData) => handleMouseMove(mouseMoveData.posy, mouseMoveData.posx), () => { this._slider.toggleClassName('active', false); }); } scrollDueToTouchEvent(touch) { const startY = this._domNode.domNode.getBoundingClientRect().top; const scrollTop = this._lastRenderData.renderedLayout.getDesiredScrollTopFromTouchLocation(touch.pageY - startY); this._model.setScrollTop(scrollTop); } dispose() { this._mouseDownListener.dispose(); this._sliderMouseMoveMonitor.dispose(); this._sliderMouseDownListener.dispose(); this._gestureDisposable.dispose(); this._sliderTouchStartListener.dispose(); this._sliderTouchMoveListener.dispose(); this._sliderTouchEndListener.dispose(); super.dispose(); } _getMinimapDomNodeClassName() { if (this._model.options.showSlider === 'always') { return 'minimap slider-always'; } return 'minimap slider-mouseover'; } getDomNode() { return this._domNode; } _applyLayout() { 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); } _getBuffer() { 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 onDidChangeOptions() { this._lastRenderData = null; this._buffers = null; this._applyLayout(); this._domNode.setClassName(this._getMinimapDomNodeClassName()); } onSelectionChanged() { this._renderDecorations = true; return true; } onDecorationsChanged() { this._renderDecorations = true; return true; } onFlushed() { this._lastRenderData = null; return true; } onLinesChanged(changeFromLineNumber, changeToLineNumber) { if (this._lastRenderData) { return this._lastRenderData.onLinesChanged(changeFromLineNumber, changeToLineNumber); } return false; } onLinesDeleted(deleteFromLineNumber, deleteToLineNumber) { if (this._lastRenderData) { this._lastRenderData.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber); } return true; } onLinesInserted(insertFromLineNumber, insertToLineNumber) { if (this._lastRenderData) { this._lastRenderData.onLinesInserted(insertFromLineNumber, insertToLineNumber); } return true; } onScrollChanged() { this._renderDecorations = true; return true; } onThemeChanged() { this._selectionColor = this._theme.getColor(minimapSelection); this._renderDecorations = true; return true; } onTokensChanged(ranges) { if (this._lastRenderData) { return this._lastRenderData.onTokensChanged(ranges); } return false; } onTokensColorsChanged() { this._lastRenderData = null; this._buffers = null; return true; } onZonesChanged() { this._lastRenderData = null; return true; } // --- end event handlers render(renderingCtx) { const renderMinimap = this._model.options.renderMinimap; if (renderMinimap === 0 /* 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); } renderDecorations(layout) { 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 lineHeight = this._model.options.minimapLineHeight; const characterWidth = 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(layout.startLineNumber, layout.endLineNumber, false); this._renderSelectionLineHighlights(canvasContext, selections, highlightedLines, layout, lineHeight); this._renderDecorationsLineHighlights(canvasContext, decorations, highlightedLines, layout, lineHeight); const lineOffsetMap = new ContiguousLineMap(layout.startLineNumber, layout.endLineNumber, null); this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, lineHeight, tabSize, characterWidth,