minimap.ts 32.0 KB
Newer Older
A
Alex Dima 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import 'vs/css!./minimap';
9
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
A
Alex Dima 已提交
10
import { ViewContext } from 'vs/editor/common/view/viewContext';
A
Alex Dima 已提交
11
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
A
Alex Dima 已提交
12
import { getOrCreateMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer';
A
Alex Dima 已提交
13
import * as dom from 'vs/base/browser/dom';
A
Alex Dima 已提交
14
import { MinimapCharRenderer, MinimapTokensColorTracker, Constants } from 'vs/editor/common/view/minimapCharRenderer';
A
Alex Dima 已提交
15
import * as editorCommon from 'vs/editor/common/editorCommon';
16
import { CharCode } from 'vs/base/common/charCode';
17
import { ViewLineData } from 'vs/editor/common/viewModel/viewModel';
A
Alex Dima 已提交
18
import { ColorId } from 'vs/editor/common/modes';
A
Alex Dima 已提交
19
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
20
import { IDisposable } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
21
import { RenderedLinesCollection, ILine } from 'vs/editor/browser/view/viewLayer';
A
Alex Dima 已提交
22
import { Range } from 'vs/editor/common/core/range';
B
Benjamin Pasero 已提交
23
import { RGBA8 } from 'vs/editor/common/core/rgba';
24
import * as viewEvents from 'vs/editor/common/view/viewEvents';
25 26
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import * as platform from 'vs/base/common/platform';
27 28
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground, scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
29

30 31 32
const enum RenderMinimap {
	None = 0,
	Small = 1,
33
	Large = 2,
A
Alex Dima 已提交
34 35
	SmallBlocks = 3,
	LargeBlocks = 4,
36 37 38 39 40 41
}

function getMinimapLineHeight(renderMinimap: RenderMinimap): number {
	if (renderMinimap === RenderMinimap.Large) {
		return Constants.x2_CHAR_HEIGHT;
	}
A
Alex Dima 已提交
42 43 44
	if (renderMinimap === RenderMinimap.LargeBlocks) {
		return Constants.x2_CHAR_HEIGHT + 2;
	}
45 46 47
	if (renderMinimap === RenderMinimap.Small) {
		return Constants.x1_CHAR_HEIGHT;
	}
A
Alex Dima 已提交
48 49
	// RenderMinimap.SmallBlocks
	return Constants.x1_CHAR_HEIGHT + 1;
50 51 52 53 54 55
}

function getMinimapCharWidth(renderMinimap: RenderMinimap): number {
	if (renderMinimap === RenderMinimap.Large) {
		return Constants.x2_CHAR_WIDTH;
	}
A
Alex Dima 已提交
56 57 58
	if (renderMinimap === RenderMinimap.LargeBlocks) {
		return Constants.x2_CHAR_WIDTH;
	}
59 60 61
	if (renderMinimap === RenderMinimap.Small) {
		return Constants.x1_CHAR_WIDTH;
	}
A
Alex Dima 已提交
62 63
	// RenderMinimap.SmallBlocks
	return Constants.x1_CHAR_WIDTH;
64 65
}

66 67 68 69 70
/**
 * The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
 */
const MOUSE_DRAG_RESET_DISTANCE = 140;

71 72 73 74
class MinimapOptions {

	public readonly renderMinimap: RenderMinimap;

75 76
	public readonly scrollBeyondLastLine: boolean;

77 78
	public readonly showSlider: 'always' | 'mouseover';

79 80
	public readonly side: 'right' | 'left';

81 82
	public readonly pixelRatio: number;

83 84
	public readonly typicalHalfwidthCharacterWidth: number;

85 86
	public readonly lineHeight: number;

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
	/**
	 * container dom node width (in CSS px)
	 */
	public readonly minimapWidth: number;
	/**
	 * container dom node height (in CSS px)
	 */
	public readonly minimapHeight: number;

	/**
	 * canvas backing store width (in device px)
	 */
	public readonly canvasInnerWidth: number;
	/**
	 * canvas backing store height (in device px)
	 */
	public readonly canvasInnerHeight: number;

	/**
	 * canvas width (in CSS px)
	 */
	public readonly canvasOuterWidth: number;
	/**
	 * canvas height (in CSS px)
	 */
	public readonly canvasOuterHeight: number;

	constructor(configuration: editorCommon.IConfiguration) {
115
		const pixelRatio = configuration.editor.pixelRatio;
116
		const layoutInfo = configuration.editor.layoutInfo;
117
		const viewInfo = configuration.editor.viewInfo;
118
		const fontInfo = configuration.editor.fontInfo;
119 120

		this.renderMinimap = layoutInfo.renderMinimap | 0;
121
		this.scrollBeyondLastLine = viewInfo.scrollBeyondLastLine;
122
		this.showSlider = viewInfo.minimap.showSlider;
123
		this.side = viewInfo.minimap.side;
124
		this.pixelRatio = pixelRatio;
125
		this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
126
		this.lineHeight = configuration.editor.lineHeight;
127 128 129
		this.minimapWidth = layoutInfo.minimapWidth;
		this.minimapHeight = layoutInfo.height;

130 131
		this.canvasInnerWidth = Math.max(1, Math.floor(pixelRatio * this.minimapWidth));
		this.canvasInnerHeight = Math.max(1, Math.floor(pixelRatio * this.minimapHeight));
132 133 134 135 136 137 138

		this.canvasOuterWidth = this.canvasInnerWidth / pixelRatio;
		this.canvasOuterHeight = this.canvasInnerHeight / pixelRatio;
	}

	public equals(other: MinimapOptions): boolean {
		return (this.renderMinimap === other.renderMinimap
139
			&& this.scrollBeyondLastLine === other.scrollBeyondLastLine
140
			&& this.showSlider === other.showSlider
141
			&& this.side === other.side
142
			&& this.pixelRatio === other.pixelRatio
143
			&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth
144
			&& this.lineHeight === other.lineHeight
145 146 147 148 149 150 151 152 153 154
			&& this.minimapWidth === other.minimapWidth
			&& this.minimapHeight === other.minimapHeight
			&& this.canvasInnerWidth === other.canvasInnerWidth
			&& this.canvasInnerHeight === other.canvasInnerHeight
			&& this.canvasOuterWidth === other.canvasOuterWidth
			&& this.canvasOuterHeight === other.canvasOuterHeight
		);
	}
}

155 156
class MinimapLayout {

157 158 159 160 161
	/**
	 * The given editor scrollTop (input).
	 */
	public readonly scrollTop: number;

162 163 164 165 166
	/**
	* The given editor scrollHeight (input).
	*/
	public readonly scrollHeight: number;

167 168
	private readonly _computedSliderRatio: number;

169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
	/**
	 * slider dom node top (in CSS px)
	 */
	public readonly sliderTop: number;
	/**
	 * slider dom node height (in CSS px)
	 */
	public readonly sliderHeight: number;

	/**
	 * minimap render start line number.
	 */
	public readonly startLineNumber: number;
	/**
	 * minimap render end line number.
	 */
	public readonly endLineNumber: number;

	constructor(
188
		scrollTop: number,
189
		scrollHeight: number,
190 191 192 193 194 195 196
		computedSliderRatio: number,
		sliderTop: number,
		sliderHeight: number,
		startLineNumber: number,
		endLineNumber: number
	) {
		this.scrollTop = scrollTop;
197
		this.scrollHeight = scrollHeight;
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
		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`.
	 */
	public getDesiredScrollTopFromDelta(delta: number): number {
		let desiredSliderPosition = this.sliderTop + delta;
		return Math.round(desiredSliderPosition / this._computedSliderRatio);
	}

	public static create(
214 215 216
		options: MinimapOptions,
		viewportStartLineNumber: number,
		viewportEndLineNumber: number,
217
		viewportHeight: number,
218
		viewportContainsWhitespaceGaps: boolean,
219
		lineCount: number,
220
		scrollTop: number,
221 222
		scrollHeight: number,
		previousLayout: MinimapLayout
223
	): MinimapLayout {
224
		const pixelRatio = options.pixelRatio;
225
		const minimapLineHeight = getMinimapLineHeight(options.renderMinimap);
226
		const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
227
		const lineHeight = options.lineHeight;
228

229 230 231 232 233 234 235 236 237 238
		// 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.
239 240
		let sliderHeight: number;
		if (viewportContainsWhitespaceGaps && viewportEndLineNumber !== lineCount) {
241 242 243
			// 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;
244
			sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
245 246 247
		} else {
			// The slider has a stable height
			const expectedViewportLineCount = viewportHeight / lineHeight;
248
			sliderHeight = Math.floor(expectedViewportLineCount * minimapLineHeight / pixelRatio);
A
Alex Dima 已提交
249 250
		}

251 252 253 254 255 256 257 258 259 260
		let maxMinimapSliderTop: number;
		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);

261 262 263 264 265
		// 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);

266
		if (minimapLinesFitting >= lineCount) {
267 268 269 270
			// All lines fit in the minimap
			const startLineNumber = 1;
			const endLineNumber = lineCount;

271
			return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber);
272
		} else {
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
			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);
				}
			}

288
			const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1);
289

290
			return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber);
291
		}
A
Alex Dima 已提交
292 293 294
	}
}

A
Alex Dima 已提交
295 296
class MinimapLine implements ILine {

M
Matt Bierner 已提交
297
	public static readonly INVALID = new MinimapLine(-1);
A
Alex Dima 已提交
298

299
	dy: number;
A
Alex Dima 已提交
300 301

	constructor(dy: number) {
302
		this.dy = dy;
A
Alex Dima 已提交
303 304 305
	}

	public onContentChanged(): void {
306
		this.dy = -1;
A
Alex Dima 已提交
307 308 309
	}

	public onTokensChanged(): void {
310
		this.dy = -1;
A
Alex Dima 已提交
311 312 313 314 315 316 317
	}
}

class RenderData {
	/**
	 * last rendered layout.
	 */
318
	public readonly renderedLayout: MinimapLayout;
A
Alex Dima 已提交
319 320 321 322
	private readonly _imageData: ImageData;
	private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;

	constructor(
323
		renderedLayout: MinimapLayout,
A
Alex Dima 已提交
324 325 326 327 328 329 330 331 332 333 334
		imageData: ImageData,
		lines: MinimapLine[]
	) {
		this.renderedLayout = renderedLayout;
		this._imageData = imageData;
		this._renderedLines = new RenderedLinesCollection(
			() => MinimapLine.INVALID
		);
		this._renderedLines._set(renderedLayout.startLineNumber, lines);
	}

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
	/**
	 * Check if the current RenderData matches accurately the new desired layout and no painting is needed.
	 */
	public linesEquals(layout: MinimapLayout): boolean {
		if (this.renderedLayout.startLineNumber !== layout.startLineNumber) {
			return false;
		}
		if (this.renderedLayout.endLineNumber !== layout.endLineNumber) {
			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;
	}

358 359 360 361 362 363 364 365 366
	_get(): { imageData: ImageData; rendLineNumberStart: number; lines: MinimapLine[]; } {
		let tmp = this._renderedLines._get();
		return {
			imageData: this._imageData,
			rendLineNumberStart: tmp.rendLineNumberStart,
			lines: tmp.lines
		};
	}

A
Alex Dima 已提交
367 368 369
	public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
		return this._renderedLines.onLinesChanged(e.fromLineNumber, e.toLineNumber);
	}
A
Alex Dima 已提交
370 371
	public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): void {
		this._renderedLines.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
A
Alex Dima 已提交
372
	}
A
Alex Dima 已提交
373 374
	public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {
		this._renderedLines.onLinesInserted(e.fromLineNumber, e.toLineNumber);
A
Alex Dima 已提交
375
	}
A
Alex Dima 已提交
376 377
	public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
		return this._renderedLines.onTokensChanged(e.ranges);
A
Alex Dima 已提交
378 379 380
	}
}

381 382 383 384 385 386 387 388 389 390 391 392
/**
 * Some sort of double buffering.
 *
 * Keeps two buffers around that will be rotated for painting.
 * Always gives a buffer that is filled with the background color.
 */
class MinimapBuffers {

	private readonly _backgroundFillData: Uint8ClampedArray;
	private readonly _buffers: [ImageData, ImageData];
	private _lastUsedBuffer: number;

393
	constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA8) {
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
		this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background);
		this._buffers = [
			ctx.createImageData(WIDTH, HEIGHT),
			ctx.createImageData(WIDTH, HEIGHT)
		];
		this._lastUsedBuffer = 0;
	}

	public getBuffer(): ImageData {
		// rotate buffers
		this._lastUsedBuffer = 1 - this._lastUsedBuffer;
		let result = this._buffers[this._lastUsedBuffer];

		// fill with background color
		result.data.set(this._backgroundFillData);

		return result;
	}

413
	private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA8): Uint8ClampedArray {
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
		const backgroundR = background.r;
		const backgroundG = background.g;
		const backgroundB = background.b;

		let 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] = 255;
				offset += 4;
			}
		}

		return result;
	}
}

A
Alex Dima 已提交
434 435
export class Minimap extends ViewPart {

A
Alex Dima 已提交
436
	private readonly _domNode: FastDomNode<HTMLElement>;
437
	private readonly _shadow: FastDomNode<HTMLElement>;
A
Alex Dima 已提交
438
	private readonly _canvas: FastDomNode<HTMLCanvasElement>;
439
	private readonly _slider: FastDomNode<HTMLElement>;
440
	private readonly _sliderHorizontal: FastDomNode<HTMLElement>;
441
	private readonly _tokensColorTracker: MinimapTokensColorTracker;
A
Alex Dima 已提交
442
	private readonly _mouseDownListener: IDisposable;
443 444
	private readonly _sliderMouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
	private readonly _sliderMouseDownListener: IDisposable;
A
Alex Dima 已提交
445

446
	private _options: MinimapOptions;
A
Alex Dima 已提交
447
	private _lastRenderData: RenderData;
448
	private _buffers: MinimapBuffers;
A
Alex Dima 已提交
449

A
Alex Dima 已提交
450
	constructor(context: ViewContext) {
A
Alex Dima 已提交
451 452
		super(context);

453
		this._options = new MinimapOptions(this._context.configuration);
A
Alex Dima 已提交
454
		this._lastRenderData = null;
455
		this._buffers = null;
456 457

		this._domNode = createFastDomNode(document.createElement('div'));
A
Alex Dima 已提交
458
		PartFingerprints.write(this._domNode, PartFingerprint.Minimap);
459
		this._domNode.setClassName(this._getMinimapDomNodeClassName());
460
		this._domNode.setPosition('absolute');
A
Alex Dima 已提交
461 462
		this._domNode.setAttribute('role', 'presentation');
		this._domNode.setAttribute('aria-hidden', 'true');
463 464 465 466 467
		if (this._options.side === 'right') {
			this._domNode.setRight(this._context.configuration.editor.layoutInfo.verticalScrollbarWidth);
		} else {
			this._domNode.setLeft(0);
		}
468

469 470
		this._shadow = createFastDomNode(document.createElement('div'));
		this._shadow.setClassName('minimap-shadow-hidden');
A
Alex Dima 已提交
471
		this._domNode.appendChild(this._shadow);
472

473 474 475
		this._canvas = createFastDomNode(document.createElement('canvas'));
		this._canvas.setPosition('absolute');
		this._canvas.setLeft(0);
A
Alex Dima 已提交
476
		this._domNode.appendChild(this._canvas);
477

478 479 480
		this._slider = createFastDomNode(document.createElement('div'));
		this._slider.setPosition('absolute');
		this._slider.setClassName('minimap-slider');
481
		this._slider.setLayerHinting(true);
A
Alex Dima 已提交
482
		this._domNode.appendChild(this._slider);
483

484 485 486 487 488
		this._sliderHorizontal = createFastDomNode(document.createElement('div'));
		this._sliderHorizontal.setPosition('absolute');
		this._sliderHorizontal.setClassName('minimap-slider-horizontal');
		this._slider.appendChild(this._sliderHorizontal);

489 490 491
		this._tokensColorTracker = MinimapTokensColorTracker.getInstance();

		this._applyLayout();
A
Alex Dima 已提交
492 493 494 495 496 497 498 499 500 501 502

		this._mouseDownListener = dom.addStandardDisposableListener(this._canvas.domNode, 'mousedown', (e) => {
			e.preventDefault();

			const renderMinimap = this._options.renderMinimap;
			if (renderMinimap === RenderMinimap.None) {
				return;
			}
			if (!this._lastRenderData) {
				return;
			}
503
			const minimapLineHeight = getMinimapLineHeight(renderMinimap);
A
Alex Dima 已提交
504 505 506 507 508 509
			const internalOffsetY = this._options.pixelRatio * e.browserEvent.offsetY;
			const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);

			let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber;
			lineNumber = Math.min(lineNumber, this._context.model.getLineCount());

510
			this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
A
Alex Dima 已提交
511
				new Range(lineNumber, 1, lineNumber, 1),
512
				viewEvents.VerticalRevealType.Center,
513 514
				false,
				editorCommon.ScrollType.Smooth
A
Alex Dima 已提交
515
			));
A
Alex Dima 已提交
516
		});
517 518 519 520 521

		this._sliderMouseMoveMonitor = new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>();

		this._sliderMouseDownListener = dom.addStandardDisposableListener(this._slider.domNode, 'mousedown', (e) => {
			e.preventDefault();
522
			if (e.leftButton && this._lastRenderData) {
523

524
				const initialMousePosition = e.posy;
525
				const initialMouseOrthogonalPosition = e.posx;
526
				const initialSliderState = this._lastRenderData.renderedLayout;
527 528 529 530 531
				this._slider.toggleClassName('active', true);

				this._sliderMouseMoveMonitor.startMonitoring(
					standardMouseMoveMerger,
					(mouseMoveData: IStandardMouseMoveEventData) => {
532
						const mouseOrthogonalDelta = Math.abs(mouseMoveData.posx - initialMouseOrthogonalPosition);
533

534 535
						if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
							// The mouse has wondered away from the scrollbar => reset dragging
536
							this._context.viewLayout.setScrollPositionNow({
537
								scrollTop: initialSliderState.scrollTop
538
							});
539
							return;
540
						}
541 542

						const mouseDelta = mouseMoveData.posy - initialMousePosition;
543
						this._context.viewLayout.setScrollPositionNow({
544 545
							scrollTop: initialSliderState.getDesiredScrollTopFromDelta(mouseDelta)
						});
546 547 548 549 550 551 552
					},
					() => {
						this._slider.toggleClassName('active', false);
					}
				);
			}
		});
A
Alex Dima 已提交
553 554 555
	}

	public dispose(): void {
A
Alex Dima 已提交
556
		this._mouseDownListener.dispose();
557 558
		this._sliderMouseMoveMonitor.dispose();
		this._sliderMouseDownListener.dispose();
A
Alex Dima 已提交
559
		super.dispose();
560 561
	}

562 563 564 565 566 567 568
	private _getMinimapDomNodeClassName(): string {
		if (this._options.showSlider === 'always') {
			return 'minimap slider-always';
		}
		return 'minimap slider-mouseover';
	}

A
Alex Dima 已提交
569 570
	public getDomNode(): FastDomNode<HTMLElement> {
		return this._domNode;
571 572 573 574 575
	}

	private _applyLayout(): void {
		this._domNode.setWidth(this._options.minimapWidth);
		this._domNode.setHeight(this._options.minimapHeight);
576
		this._shadow.setHeight(this._options.minimapHeight);
577 578
		this._canvas.setWidth(this._options.canvasOuterWidth);
		this._canvas.setHeight(this._options.canvasOuterHeight);
A
Alex Dima 已提交
579 580
		this._canvas.domNode.width = this._options.canvasInnerWidth;
		this._canvas.domNode.height = this._options.canvasInnerHeight;
581
		this._slider.setWidth(this._options.minimapWidth);
582
	}
583

584 585 586 587 588 589 590 591
	private _getBuffer(): ImageData {
		if (!this._buffers) {
			this._buffers = new MinimapBuffers(
				this._canvas.domNode.getContext('2d'),
				this._options.canvasInnerWidth,
				this._options.canvasInnerHeight,
				this._tokensColorTracker.getColor(ColorId.DefaultBackground)
			);
592
		}
593
		return this._buffers.getBuffer();
A
Alex Dima 已提交
594 595
	}

596 597 598 599 600 601
	private _onOptionsMaybeChanged(): boolean {
		let opts = new MinimapOptions(this._context.configuration);
		if (this._options.equals(opts)) {
			return false;
		}
		this._options = opts;
A
Alex Dima 已提交
602
		this._lastRenderData = null;
603
		this._buffers = null;
604
		this._applyLayout();
605
		this._domNode.setClassName(this._getMinimapDomNodeClassName());
A
Alex Dima 已提交
606 607
		return true;
	}
A
Alex Dima 已提交
608

609 610 611 612 613
	// ---- begin view event handlers

	public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
		return this._onOptionsMaybeChanged();
	}
A
Alex Dima 已提交
614
	public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
A
Alex Dima 已提交
615
		this._lastRenderData = null;
A
Alex Dima 已提交
616 617
		return true;
	}
A
Alex Dima 已提交
618
	public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
A
Alex Dima 已提交
619
		if (this._lastRenderData) {
A
Alex Dima 已提交
620
			return this._lastRenderData.onLinesChanged(e);
A
Alex Dima 已提交
621 622
		}
		return false;
A
Alex Dima 已提交
623
	}
624 625 626 627 628 629
	public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
		if (this._lastRenderData) {
			this._lastRenderData.onLinesDeleted(e);
		}
		return true;
	}
A
Alex Dima 已提交
630
	public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
A
Alex Dima 已提交
631
		if (this._lastRenderData) {
A
Alex Dima 已提交
632
			this._lastRenderData.onLinesInserted(e);
A
Alex Dima 已提交
633
		}
A
Alex Dima 已提交
634 635
		return true;
	}
636
	public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
637
		return true;
638
	}
A
Alex Dima 已提交
639
	public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
A
Alex Dima 已提交
640
		if (this._lastRenderData) {
A
Alex Dima 已提交
641
			return this._lastRenderData.onTokensChanged(e);
A
Alex Dima 已提交
642 643
		}
		return false;
A
Alex Dima 已提交
644
	}
645 646 647 648 649
	public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {
		this._lastRenderData = null;
		this._buffers = null;
		return true;
	}
A
Alex Dima 已提交
650
	public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
A
Alex Dima 已提交
651
		this._lastRenderData = null;
A
Alex Dima 已提交
652 653
		return true;
	}
A
Alex Dima 已提交
654

A
Alex Dima 已提交
655
	// --- end event handlers
A
Alex Dima 已提交
656

A
Alex Dima 已提交
657
	public prepareRender(ctx: RenderingContext): void {
A
Alex Dima 已提交
658 659 660
		// Nothing to read
	}

A
Alex Dima 已提交
661
	public render(renderingCtx: RestrictedRenderingContext): void {
662 663
		const renderMinimap = this._options.renderMinimap;
		if (renderMinimap === RenderMinimap.None) {
664
			this._shadow.setClassName('minimap-shadow-hidden');
665 666
			return;
		}
A
Alex Dima 已提交
667
		if (renderingCtx.scrollLeft + renderingCtx.viewportWidth >= renderingCtx.scrollWidth) {
668 669 670 671
			this._shadow.setClassName('minimap-shadow-hidden');
		} else {
			this._shadow.setClassName('minimap-shadow-visible');
		}
672

673
		const layout = MinimapLayout.create(
674 675 676
			this._options,
			renderingCtx.visibleRange.startLineNumber,
			renderingCtx.visibleRange.endLineNumber,
677
			renderingCtx.viewportHeight,
678
			(renderingCtx.viewportData.whitespaceViewportData.length > 0),
679
			this._context.model.getLineCount(),
680
			renderingCtx.scrollTop,
681 682
			renderingCtx.scrollHeight,
			this._lastRenderData ? this._lastRenderData.renderedLayout : null
683
		);
A
Alex Dima 已提交
684 685
		this._slider.setTop(layout.sliderTop);
		this._slider.setHeight(layout.sliderHeight);
A
Alex Dima 已提交
686

687 688 689 690 691 692 693 694
		// Compute horizontal slider coordinates
		const scrollLeftChars = renderingCtx.scrollLeft / this._options.typicalHalfwidthCharacterWidth;
		const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap) / this._options.pixelRatio));
		this._sliderHorizontal.setLeft(horizontalSliderLeft);
		this._sliderHorizontal.setWidth(this._options.minimapWidth - horizontalSliderLeft);
		this._sliderHorizontal.setTop(0);
		this._sliderHorizontal.setHeight(layout.sliderHeight);

695 696 697 698 699
		this._lastRenderData = this.renderLines(layout);
	}

	private renderLines(layout: MinimapLayout): RenderData {
		const renderMinimap = this._options.renderMinimap;
A
Alex Dima 已提交
700 701
		const startLineNumber = layout.startLineNumber;
		const endLineNumber = layout.endLineNumber;
702
		const minimapLineHeight = getMinimapLineHeight(renderMinimap);
A
Alex Dima 已提交
703

704
		// Check if nothing changed w.r.t. lines from last frame
705
		if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {
706
			const _lastData = this._lastRenderData._get();
707 708
			// Nice!! Nothing changed from last frame
			return new RenderData(layout, _lastData.imageData, _lastData.lines);
709 710 711 712
		}

		// Oh well!! We need to repaint some lines...

713
		const imageData = this._getBuffer();
A
Alex Dima 已提交
714 715

		// Render untouched lines by using last rendered data.
716
		let [_dirtyY1, _dirtyY2, needed] = Minimap._renderUntouchedLines(
717 718 719 720 721 722
			imageData,
			startLineNumber,
			endLineNumber,
			minimapLineHeight,
			this._lastRenderData
		);
723

A
Alex Dima 已提交
724 725 726 727
		// Fetch rendering info from view model for rest of lines that need rendering.
		const lineInfo = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);
		const tabSize = lineInfo.tabSize;
		const background = this._tokensColorTracker.getColor(ColorId.DefaultBackground);
A
Alex Dima 已提交
728
		const useLighterFont = this._tokensColorTracker.backgroundIsLight();
A
Alex Dima 已提交
729

A
Alex Dima 已提交
730
		// Render the rest of lines
731
		let dy = 0;
A
Alex Dima 已提交
732
		let renderedLines: MinimapLine[] = [];
733 734 735 736 737
		for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
			if (needed[lineIndex]) {
				Minimap._renderLine(
					imageData,
					background,
A
Alex Dima 已提交
738
					useLighterFont,
739 740
					renderMinimap,
					this._tokensColorTracker,
741
					getOrCreateMinimapCharRenderer(),
742 743
					dy,
					tabSize,
A
Alex Dima 已提交
744
					lineInfo.data[lineIndex]
745 746
				);
			}
A
Alex Dima 已提交
747
			renderedLines[lineIndex] = new MinimapLine(dy);
748
			dy += minimapLineHeight;
A
Alex Dima 已提交
749 750
		}

751 752 753 754
		const dirtyY1 = (_dirtyY1 === -1 ? 0 : _dirtyY1);
		const dirtyY2 = (_dirtyY2 === -1 ? imageData.height : _dirtyY2);
		const dirtyHeight = dirtyY2 - dirtyY1;

755 756
		// Finally, paint to the canvas
		const ctx = this._canvas.domNode.getContext('2d');
757
		ctx.putImageData(imageData, 0, 0, 0, dirtyY1, imageData.width, dirtyHeight);
758

A
Alex Dima 已提交
759
		// Save rendered data for reuse on next frame if possible
760
		return new RenderData(
761
			layout,
A
Alex Dima 已提交
762 763
			imageData,
			renderedLines
A
Alex Dima 已提交
764
		);
765 766 767 768 769 770 771 772
	}

	private static _renderUntouchedLines(
		target: ImageData,
		startLineNumber: number,
		endLineNumber: number,
		minimapLineHeight: number,
		lastRenderData: RenderData,
773
	): [number, number, boolean[]] {
774 775 776 777 778 779

		let needed: boolean[] = [];
		if (!lastRenderData) {
			for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {
				needed[i] = true;
			}
780
			return [-1, -1, needed];
781 782 783 784 785 786 787 788 789 790
		}

		const _lastData = lastRenderData._get();
		const lastTargetData = _lastData.imageData.data;
		const lastStartLineNumber = _lastData.rendLineNumberStart;
		const lastLines = _lastData.lines;
		const lastLinesLength = lastLines.length;
		const WIDTH = target.width;
		const targetData = target.data;

791 792 793 794
		const maxDestPixel = (endLineNumber - startLineNumber + 1) * minimapLineHeight * WIDTH * 4;
		let dirtyPixel1 = -1; // the pixel offset up to which all the data is equal to the prev frame
		let dirtyPixel2 = -1; // the pixel offset after which all the data is equal to the prev frame

795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824
		let copySourceStart = -1;
		let copySourceEnd = -1;
		let copyDestStart = -1;
		let copyDestEnd = -1;

		let dest_dy = 0;
		for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
			const lineIndex = lineNumber - startLineNumber;
			const lastLineIndex = lineNumber - lastStartLineNumber;
			const source_dy = (lastLineIndex >= 0 && lastLineIndex < lastLinesLength ? lastLines[lastLineIndex].dy : -1);

			if (source_dy === -1) {
				needed[lineIndex] = true;
				dest_dy += minimapLineHeight;
				continue;
			}

			let sourceStart = source_dy * WIDTH * 4;
			let sourceEnd = (source_dy + minimapLineHeight) * WIDTH * 4;
			let destStart = dest_dy * WIDTH * 4;
			let destEnd = (dest_dy + minimapLineHeight) * WIDTH * 4;

			if (copySourceEnd === sourceStart && copyDestEnd === destStart) {
				// contiguous zone => extend copy request
				copySourceEnd = sourceEnd;
				copyDestEnd = destEnd;
			} else {
				if (copySourceStart !== -1) {
					// flush existing copy request
					targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
825 826 827 828 829 830
					if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {
						dirtyPixel1 = copySourceEnd;
					}
					if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {
						dirtyPixel2 = copySourceStart;
					}
831 832 833 834 835 836 837 838 839 840 841 842 843 844
				}
				copySourceStart = sourceStart;
				copySourceEnd = sourceEnd;
				copyDestStart = destStart;
				copyDestEnd = destEnd;
			}

			needed[lineIndex] = false;
			dest_dy += minimapLineHeight;
		}

		if (copySourceStart !== -1) {
			// flush existing copy request
			targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
845 846 847 848 849 850
			if (dirtyPixel1 === -1 && copySourceStart === 0 && copySourceStart === copyDestStart) {
				dirtyPixel1 = copySourceEnd;
			}
			if (dirtyPixel2 === -1 && copySourceEnd === maxDestPixel && copySourceStart === copyDestStart) {
				dirtyPixel2 = copySourceStart;
			}
851 852
		}

853 854 855 856
		const dirtyY1 = (dirtyPixel1 === -1 ? -1 : dirtyPixel1 / (WIDTH * 4));
		const dirtyY2 = (dirtyPixel2 === -1 ? -1 : dirtyPixel2 / (WIDTH * 4));

		return [dirtyY1, dirtyY2, needed];
A
Alex Dima 已提交
857 858
	}

A
Alex Dima 已提交
859 860
	private static _renderLine(
		target: ImageData,
861
		backgroundColor: RGBA8,
862
		useLighterFont: boolean,
A
Alex Dima 已提交
863 864 865 866
		renderMinimap: RenderMinimap,
		colorTracker: MinimapTokensColorTracker,
		minimapCharRenderer: MinimapCharRenderer,
		dy: number,
A
Alex Dima 已提交
867
		tabSize: number,
A
Alex Dima 已提交
868
		lineData: ViewLineData
A
Alex Dima 已提交
869
	): void {
A
Alex Dima 已提交
870 871
		const content = lineData.content;
		const tokens = lineData.tokens;
872
		const charWidth = getMinimapCharWidth(renderMinimap);
873
		const maxDx = target.width - charWidth;
A
Alex Dima 已提交
874 875 876

		let dx = 0;
		let charIndex = 0;
877 878
		let tabsCharDelta = 0;

A
Alex Dima 已提交
879 880 881 882
		for (let tokenIndex = 0, tokensLen = tokens.length; tokenIndex < tokensLen; tokenIndex++) {
			const token = tokens[tokenIndex];
			const tokenEndIndex = token.endIndex;
			const tokenColorId = token.getForeground();
A
Alex Dima 已提交
883
			const tokenColor = colorTracker.getColor(tokenColorId);
A
Alex Dima 已提交
884 885

			for (; charIndex < tokenEndIndex; charIndex++) {
A
Alex Dima 已提交
886
				if (dx > maxDx) {
A
Alex Dima 已提交
887 888 889 890 891
					// hit edge of minimap
					return;
				}
				const charCode = content.charCodeAt(charIndex);

892 893 894 895
				if (charCode === CharCode.Tab) {
					let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
					tabsCharDelta += insertSpacesCount - 1;
					// No need to render anything since tab is invisible
896
					dx += insertSpacesCount * charWidth;
897 898
				} else if (charCode === CharCode.Space) {
					// No need to render anything since space is invisible
899
					dx += charWidth;
900
				} else {
901
					if (renderMinimap === RenderMinimap.Large) {
A
Alex Dima 已提交
902
						minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
903
					} else if (renderMinimap === RenderMinimap.Small) {
A
Alex Dima 已提交
904
						minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
A
Alex Dima 已提交
905 906
					} else if (renderMinimap === RenderMinimap.LargeBlocks) {
						minimapCharRenderer.x2BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
907
					} else {
A
Alex Dima 已提交
908 909
						// RenderMinimap.SmallBlocks
						minimapCharRenderer.x1BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
910 911
					}
					dx += charWidth;
912
				}
A
Alex Dima 已提交
913 914 915 916
			}
		}
	}
}
917 918

registerThemingParticipant((theme, collector) => {
919
	const sliderBackground = theme.getColor(scrollbarSliderBackground);
920
	if (sliderBackground) {
921 922
		const halfSliderBackground = sliderBackground.transparent(0.5);
		collector.addRule(`.monaco-editor .minimap-slider, .monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${halfSliderBackground}; }`);
923
	}
924
	const sliderHoverBackground = theme.getColor(scrollbarSliderHoverBackground);
925
	if (sliderHoverBackground) {
926 927
		const halfSliderHoverBackground = sliderHoverBackground.transparent(0.5);
		collector.addRule(`.monaco-editor .minimap-slider:hover, .monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${halfSliderHoverBackground}; }`);
928
	}
929
	const sliderActiveBackground = theme.getColor(scrollbarSliderActiveBackground);
930
	if (sliderActiveBackground) {
931 932
		const halfSliderActiveBackground = sliderActiveBackground.transparent(0.5);
		collector.addRule(`.monaco-editor .minimap-slider.active, .monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${halfSliderActiveBackground}; }`);
933
	}
934
	const shadow = theme.getColor(scrollbarShadow);
935 936 937 938
	if (shadow) {
		collector.addRule(`.monaco-editor .minimap-shadow-visible { box-shadow: ${shadow} -6px 0 6px -6px inset; }`);
	}
});