@21epub/epub-thirdparty
Version:
epub-thirdparty
1,038 lines • 71.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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,