minimap.ts 24.2 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';
A
Alex Dima 已提交
9
import { ViewPart } from 'vs/editor/browser/view/viewPart';
A
Alex Dima 已提交
10 11
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { IRenderingContext, IRestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
A
Alex Dima 已提交
12
import { getOrCreateMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer';
A
Alex Dima 已提交
13
import * as browser from 'vs/base/browser/browser';
A
Alex Dima 已提交
14
import * as dom from 'vs/base/browser/dom';
A
Alex Dima 已提交
15
import { MinimapCharRenderer, MinimapTokensColorTracker, Constants } from 'vs/editor/common/view/minimapCharRenderer';
A
Alex Dima 已提交
16
import * as editorCommon from 'vs/editor/common/editorCommon';
17
import { CharCode } from 'vs/base/common/charCode';
A
Alex Dima 已提交
18
import { IViewLayout, ViewLineData } from 'vs/editor/common/viewModel/viewModel';
A
Alex Dima 已提交
19
import { ColorId } from 'vs/editor/common/modes';
A
Alex Dima 已提交
20
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
21
import { IDisposable } from 'vs/base/common/lifecycle';
22
import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar';
A
Alex Dima 已提交
23
import { RenderedLinesCollection, ILine } from 'vs/editor/browser/view/viewLayer';
A
Alex Dima 已提交
24
import { Range } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
25
import { RGBA } from 'vs/base/common/color';
26
import * as viewEvents from 'vs/editor/common/view/viewEvents';
27

28 29 30
const enum RenderMinimap {
	None = 0,
	Small = 1,
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
	Large = 2,
	Blocks = 3,
}

function getMinimapLineHeight(renderMinimap: RenderMinimap): number {
	if (renderMinimap === RenderMinimap.Large) {
		return Constants.x2_CHAR_HEIGHT;
	}
	if (renderMinimap === RenderMinimap.Small) {
		return Constants.x1_CHAR_HEIGHT;
	}
	// RenderMinimap.Blocks
	return 3;
}

function getMinimapCharWidth(renderMinimap: RenderMinimap): number {
	if (renderMinimap === RenderMinimap.Large) {
		return Constants.x2_CHAR_WIDTH;
	}
	if (renderMinimap === RenderMinimap.Small) {
		return Constants.x1_CHAR_WIDTH;
	}
	// RenderMinimap.Blocks
	return 1;
55 56 57 58 59 60
}

class MinimapOptions {

	public readonly renderMinimap: RenderMinimap;

61 62
	public readonly pixelRatio: number;

63 64
	public readonly lineHeight: number;

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
	/**
	 * 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) {
		const pixelRatio = browser.getPixelRatio();
		const layoutInfo = configuration.editor.layoutInfo;

		this.renderMinimap = layoutInfo.renderMinimap | 0;
97
		this.pixelRatio = pixelRatio;
98
		this.lineHeight = configuration.editor.lineHeight;
99 100 101 102 103 104 105 106 107 108 109 110
		this.minimapWidth = layoutInfo.minimapWidth;
		this.minimapHeight = layoutInfo.height;

		this.canvasInnerWidth = Math.floor(pixelRatio * this.minimapWidth);
		this.canvasInnerHeight = Math.floor(pixelRatio * this.minimapHeight);

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

	public equals(other: MinimapOptions): boolean {
		return (this.renderMinimap === other.renderMinimap
111
			&& this.pixelRatio === other.pixelRatio
112
			&& this.lineHeight === other.lineHeight
113 114 115 116 117 118 119 120 121 122
			&& this.minimapWidth === other.minimapWidth
			&& this.minimapHeight === other.minimapHeight
			&& this.canvasInnerWidth === other.canvasInnerWidth
			&& this.canvasInnerHeight === other.canvasInnerHeight
			&& this.canvasOuterWidth === other.canvasOuterWidth
			&& this.canvasOuterHeight === other.canvasOuterHeight
		);
	}
}

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
class MinimapLayout {

	/**
	 * 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(
A
Alex Dima 已提交
144
		lastRenderData: RenderData,
145 146 147
		options: MinimapOptions,
		viewportStartLineNumber: number,
		viewportEndLineNumber: number,
148
		viewportHeight: number,
149 150
		lineCount: number,
		scrollbarSliderCenter: number
151 152
	) {
		const pixelRatio = options.pixelRatio;
153
		const minimapLineHeight = getMinimapLineHeight(options.renderMinimap);
154
		const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
155
		const lineHeight = options.lineHeight;
156

A
Alex Dima 已提交
157 158 159 160 161 162 163 164 165 166
		// Sometimes, the number of rendered lines varies for a constant viewport height.
		// The reason is that only parts of the viewportStartLineNumber or viewportEndLineNumber are visible.
		// This leads to an apparent tremor in the minimap's slider height.
		// We try here to compensate, making the slider slightly incorrect in these cases, but more pleasing to the eye.
		let viewportLineCount = viewportEndLineNumber - viewportStartLineNumber + 1;
		const expectedViewportLineCount = Math.round(viewportHeight / lineHeight);
		if (viewportLineCount > expectedViewportLineCount) {
			viewportLineCount = expectedViewportLineCount;
		}

167 168 169 170 171
		if (minimapLinesFitting >= lineCount) {
			// All lines fit in the minimap => no minimap scrolling
			this.startLineNumber = 1;
			this.endLineNumber = lineCount;
		} else {
A
Alex Dima 已提交
172
			// The desire is to align (centers) the minimap's slider with the scrollbar's slider
173

A
Alex Dima 已提交
174
			// For a resolved this.startLineNumber, we can compute the minimap's slider's center with the following formula:
175
			// scrollbarSliderCenter = (viewportStartLineNumber - this.startLineNumber + viewportLineCount/2) * minimapLineHeight / pixelRatio;
A
Alex Dima 已提交
176
			// =>
177 178 179 180
			// scrollbarSliderCenter = (viewportStartLineNumber - this.startLineNumber + viewportLineCount/2) * minimapLineHeight / pixelRatio;
			// scrollbarSliderCenter * pixelRatio / minimapLineHeight = viewportStartLineNumber - this.startLineNumber + viewportLineCount/2
			// this.startLineNumber = viewportStartLineNumber + viewportLineCount/2 - scrollbarSliderCenter * pixelRatio / minimapLineHeight
			let desiredStartLineNumber = Math.floor(viewportStartLineNumber + viewportLineCount / 2 - scrollbarSliderCenter * pixelRatio / minimapLineHeight);
A
Alex Dima 已提交
181 182
			let desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;

A
Alex Dima 已提交
183 184 185
			// Aligning the slider's centers can result (correctly) in tremor.
			// i.e. scrolling down might result in the startLineNumber going up.
			// Avoid this tremor by being consistent w.r.t. the previous computed result
A
Alex Dima 已提交
186
			if (lastRenderData) {
A
Alex Dima 已提交
187 188
				const lastLayoutDecision = lastRenderData.renderedLayout;
				if (lastLayoutDecision.viewportStartLineNumber <= viewportStartLineNumber) {
A
Alex Dima 已提交
189
					// going down => make sure we don't go above our previous decision
A
Alex Dima 已提交
190 191
					if (desiredStartLineNumber < lastLayoutDecision.startLineNumber) {
						desiredStartLineNumber = lastLayoutDecision.startLineNumber;
A
Alex Dima 已提交
192 193 194
						desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;
					}
				}
A
Alex Dima 已提交
195
				if (lastLayoutDecision.viewportStartLineNumber >= viewportStartLineNumber) {
A
Alex Dima 已提交
196
					// going up => make sure we don't go below our previous decision
A
Alex Dima 已提交
197 198
					if (desiredEndLineNumber > lastLayoutDecision.endLineNumber) {
						desiredEndLineNumber = lastLayoutDecision.endLineNumber;
A
Alex Dima 已提交
199 200 201 202 203
						desiredStartLineNumber = desiredEndLineNumber - minimapLinesFitting + 1;
					}
				}
			}

A
Alex Dima 已提交
204 205 206 207 208 209 210 211 212 213 214 215
			// Aligning the slider's centers is a very good thing, but this would make
			// the minimap never scroll all the way to the top or to the bottom of the file.
			// We therefore check that the viewport lines are in the minimap viewport.

			// (a) validate on start line number
			if (desiredStartLineNumber < 1) {
				// must start after 1
				desiredStartLineNumber = 1;
				desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;
			}
			if (desiredStartLineNumber > viewportStartLineNumber) {
				// must contain the viewport's start line number
216
				desiredStartLineNumber = viewportStartLineNumber;
A
Alex Dima 已提交
217 218 219 220 221 222 223 224
				desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;
			}

			// (b) validate on end line number
			if (desiredEndLineNumber > lineCount) {
				// must end before line count
				desiredEndLineNumber = lineCount;
				desiredStartLineNumber = desiredEndLineNumber - minimapLinesFitting + 1;
225
			}
A
Alex Dima 已提交
226 227 228 229
			if (desiredEndLineNumber < viewportEndLineNumber) {
				// must contain the viewport's end line number
				desiredEndLineNumber = viewportEndLineNumber;
				desiredStartLineNumber = desiredEndLineNumber - minimapLinesFitting + 1;
230
			}
231

232
			this.startLineNumber = desiredStartLineNumber;
A
Alex Dima 已提交
233
			this.endLineNumber = desiredEndLineNumber;
234
		}
235 236

		this.sliderTop = Math.floor((viewportStartLineNumber - this.startLineNumber) * minimapLineHeight / pixelRatio);
A
Alex Dima 已提交
237
		this.sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
A
Alex Dima 已提交
238 239 240
	}
}

A
Alex Dima 已提交
241
class RenderedLayout {
A
Alex Dima 已提交
242 243 244 245 246 247 248 249 250 251
	/**
	 * editor viewport start line number.
	 */
	public readonly viewportStartLineNumber: number;
	/**
	 * editor viewport end line number.
	 */
	public readonly viewportEndLineNumber: number;

	/**
A
Alex Dima 已提交
252
	 * minimap rendered start line number.
A
Alex Dima 已提交
253 254 255
	 */
	public readonly startLineNumber: number;
	/**
A
Alex Dima 已提交
256
	 * minimap rendered end line number.
A
Alex Dima 已提交
257 258 259 260 261 262 263 264 265
	 */
	public readonly endLineNumber: number;

	constructor(
		viewportStartLineNumber: number,
		viewportEndLineNumber: number,
		startLineNumber: number,
		endLineNumber: number
	) {
A
Alex Dima 已提交
266
		this.viewportStartLineNumber = viewportStartLineNumber;
A
Alex Dima 已提交
267 268 269
		this.viewportEndLineNumber = viewportEndLineNumber;
		this.startLineNumber = startLineNumber;
		this.endLineNumber = endLineNumber;
270 271 272
	}
}

A
Alex Dima 已提交
273 274 275 276
class MinimapLine implements ILine {

	public static INVALID = new MinimapLine(-1);

277
	dy: number;
A
Alex Dima 已提交
278 279

	constructor(dy: number) {
280
		this.dy = dy;
A
Alex Dima 已提交
281 282 283
	}

	public onContentChanged(): void {
284
		this.dy = -1;
A
Alex Dima 已提交
285 286 287
	}

	public onTokensChanged(): void {
288
		this.dy = -1;
A
Alex Dima 已提交
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
	}
}

class RenderData {
	/**
	 * last rendered layout.
	 */
	public readonly renderedLayout: RenderedLayout;
	private readonly _imageData: ImageData;
	private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;

	constructor(
		renderedLayout: RenderedLayout,
		imageData: ImageData,
		lines: MinimapLine[]
	) {
		this.renderedLayout = renderedLayout;
		this._imageData = imageData;
		this._renderedLines = new RenderedLinesCollection(
			() => MinimapLine.INVALID
		);
		this._renderedLines._set(renderedLayout.startLineNumber, lines);
	}

313 314 315 316 317 318 319 320 321
	_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 已提交
322 323
	public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): void {
		this._renderedLines.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
A
Alex Dima 已提交
324
	}
A
Alex Dima 已提交
325 326
	public onLineChanged(e: viewEvents.ViewLineChangedEvent): boolean {
		return this._renderedLines.onLineChanged(e.lineNumber);
A
Alex Dima 已提交
327
	}
A
Alex Dima 已提交
328 329
	public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {
		this._renderedLines.onLinesInserted(e.fromLineNumber, e.toLineNumber);
A
Alex Dima 已提交
330
	}
A
Alex Dima 已提交
331 332
	public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
		return this._renderedLines.onTokensChanged(e.ranges);
A
Alex Dima 已提交
333 334 335
	}
}

336 337 338 339 340 341 342 343 344 345 346 347
/**
 * 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;

A
Alex Dima 已提交
348
	constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA) {
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
		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;
	}

A
Alex Dima 已提交
368
	private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA): Uint8ClampedArray {
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
		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 已提交
389 390
export class Minimap extends ViewPart {

391
	private readonly _viewLayout: IViewLayout;
392
	private readonly _editorScrollbar: EditorScrollbar;
393

A
Alex Dima 已提交
394 395
	private readonly _domNode: FastDomNode<HTMLElement>;
	private readonly _canvas: FastDomNode<HTMLCanvasElement>;
396
	private readonly _slider: FastDomNode<HTMLElement>;
397
	private readonly _tokensColorTracker: MinimapTokensColorTracker;
A
Alex Dima 已提交
398
	private readonly _mouseDownListener: IDisposable;
A
Alex Dima 已提交
399

A
Alex Dima 已提交
400 401
	private readonly _minimapCharRenderer: MinimapCharRenderer;

402
	private _options: MinimapOptions;
A
Alex Dima 已提交
403
	private _lastRenderData: RenderData;
404
	private _buffers: MinimapBuffers;
A
Alex Dima 已提交
405

406
	constructor(context: ViewContext, viewLayout: IViewLayout, editorScrollbar: EditorScrollbar) {
A
Alex Dima 已提交
407
		super(context);
408
		this._viewLayout = viewLayout;
409
		this._editorScrollbar = editorScrollbar;
A
Alex Dima 已提交
410

411
		this._options = new MinimapOptions(this._context.configuration);
A
Alex Dima 已提交
412
		this._lastRenderData = null;
413
		this._buffers = null;
414 415 416

		this._domNode = createFastDomNode(document.createElement('div'));
		this._domNode.setPosition('absolute');
417
		this._domNode.setRight(this._context.configuration.editor.layoutInfo.verticalScrollbarWidth);
418 419 420 421 422 423

		this._canvas = createFastDomNode(document.createElement('canvas'));
		this._canvas.setPosition('absolute');
		this._canvas.setLeft(0);
		this._domNode.domNode.appendChild(this._canvas.domNode);

424 425 426 427 428
		this._slider = createFastDomNode(document.createElement('div'));
		this._slider.setPosition('absolute');
		this._slider.setClassName('minimap-slider');
		this._domNode.domNode.appendChild(this._slider.domNode);

429 430
		this._tokensColorTracker = MinimapTokensColorTracker.getInstance();

A
Alex Dima 已提交
431 432
		this._minimapCharRenderer = getOrCreateMinimapCharRenderer();

433
		this._applyLayout();
A
Alex Dima 已提交
434 435 436 437 438 439 440 441 442 443 444

		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;
			}
445
			const minimapLineHeight = getMinimapLineHeight(renderMinimap);
A
Alex Dima 已提交
446 447 448 449 450 451
			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());

452
			this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
A
Alex Dima 已提交
453 454 455 456 457
				new Range(lineNumber, 1, lineNumber, 1),
				editorCommon.VerticalRevealType.Center,
				false,
				false
			));
A
Alex Dima 已提交
458
		});
A
Alex Dima 已提交
459 460 461
	}

	public dispose(): void {
A
Alex Dima 已提交
462
		this._mouseDownListener.dispose();
A
Alex Dima 已提交
463
		super.dispose();
464 465 466 467 468 469 470 471 472 473 474
	}

	public getDomNode(): HTMLElement {
		return this._domNode.domNode;
	}

	private _applyLayout(): void {
		this._domNode.setWidth(this._options.minimapWidth);
		this._domNode.setHeight(this._options.minimapHeight);
		this._canvas.setWidth(this._options.canvasOuterWidth);
		this._canvas.setHeight(this._options.canvasOuterHeight);
A
Alex Dima 已提交
475 476
		this._canvas.domNode.width = this._options.canvasInnerWidth;
		this._canvas.domNode.height = this._options.canvasInnerHeight;
477
		this._slider.setWidth(this._options.minimapWidth);
478
	}
479

480 481 482 483 484 485 486 487
	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)
			);
488
		}
489
		return this._buffers.getBuffer();
A
Alex Dima 已提交
490 491
	}

492 493 494 495 496 497
	private _onOptionsMaybeChanged(): boolean {
		let opts = new MinimapOptions(this._context.configuration);
		if (this._options.equals(opts)) {
			return false;
		}
		this._options = opts;
A
Alex Dima 已提交
498
		this._lastRenderData = null;
499
		this._buffers = null;
500
		this._applyLayout();
A
Alex Dima 已提交
501 502
		return true;
	}
A
Alex Dima 已提交
503

504 505 506 507 508
	// ---- begin view event handlers

	public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
		return this._onOptionsMaybeChanged();
	}
A
Alex Dima 已提交
509
	public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
A
Alex Dima 已提交
510
		this._lastRenderData = null;
A
Alex Dima 已提交
511 512
		return true;
	}
A
Alex Dima 已提交
513
	public onLineChanged(e: viewEvents.ViewLineChangedEvent): boolean {
A
Alex Dima 已提交
514
		if (this._lastRenderData) {
A
Alex Dima 已提交
515
			return this._lastRenderData.onLineChanged(e);
A
Alex Dima 已提交
516 517
		}
		return false;
A
Alex Dima 已提交
518
	}
519 520 521 522 523 524
	public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
		if (this._lastRenderData) {
			this._lastRenderData.onLinesDeleted(e);
		}
		return true;
	}
A
Alex Dima 已提交
525
	public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
A
Alex Dima 已提交
526
		if (this._lastRenderData) {
A
Alex Dima 已提交
527
			this._lastRenderData.onLinesInserted(e);
A
Alex Dima 已提交
528
		}
A
Alex Dima 已提交
529 530
		return true;
	}
531 532 533
	public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
		return e.scrollTopChanged || e.scrollHeightChanged;
	}
A
Alex Dima 已提交
534
	public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
A
Alex Dima 已提交
535
		if (this._lastRenderData) {
A
Alex Dima 已提交
536
			return this._lastRenderData.onTokensChanged(e);
A
Alex Dima 已提交
537 538
		}
		return false;
A
Alex Dima 已提交
539
	}
540 541 542 543 544
	public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {
		this._lastRenderData = null;
		this._buffers = null;
		return true;
	}
A
Alex Dima 已提交
545
	public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
A
Alex Dima 已提交
546
		this._lastRenderData = null;
A
Alex Dima 已提交
547 548
		return true;
	}
A
Alex Dima 已提交
549

A
Alex Dima 已提交
550
	// --- end event handlers
A
Alex Dima 已提交
551 552 553 554 555

	public prepareRender(ctx: IRenderingContext): void {
		// Nothing to read
	}

556 557 558 559 560 561
	public render(renderingCtx: IRestrictedRenderingContext): void {
		const renderMinimap = this._options.renderMinimap;
		if (renderMinimap === RenderMinimap.None) {
			return;
		}

A
Alex Dima 已提交
562 563
		const layout = new MinimapLayout(
			this._lastRenderData,
564 565 566
			this._options,
			renderingCtx.visibleRange.startLineNumber,
			renderingCtx.visibleRange.endLineNumber,
567
			renderingCtx.viewportHeight,
568 569
			this._context.model.getLineCount(),
			this._editorScrollbar.getVerticalSliderVerticalCenter()
570
		);
A
Alex Dima 已提交
571 572
		this._slider.setTop(layout.sliderTop);
		this._slider.setHeight(layout.sliderHeight);
A
Alex Dima 已提交
573

A
Alex Dima 已提交
574 575
		const startLineNumber = layout.startLineNumber;
		const endLineNumber = layout.endLineNumber;
576
		const minimapLineHeight = getMinimapLineHeight(renderMinimap);
A
Alex Dima 已提交
577

578
		const imageData = this._getBuffer();
A
Alex Dima 已提交
579 580

		// Render untouched lines by using last rendered data.
581 582 583 584 585 586 587
		let needed = Minimap._renderUntouchedLines(
			imageData,
			startLineNumber,
			endLineNumber,
			minimapLineHeight,
			this._lastRenderData
		);
588

A
Alex Dima 已提交
589 590 591 592
		// 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 已提交
593
		const useLighterFont = this._tokensColorTracker.backgroundIsLight();
A
Alex Dima 已提交
594

A
Alex Dima 已提交
595
		// Render the rest of lines
596
		let dy = 0;
A
Alex Dima 已提交
597
		let renderedLines: MinimapLine[] = [];
598 599 600 601 602
		for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
			if (needed[lineIndex]) {
				Minimap._renderLine(
					imageData,
					background,
A
Alex Dima 已提交
603
					useLighterFont,
604 605 606 607 608
					renderMinimap,
					this._tokensColorTracker,
					this._minimapCharRenderer,
					dy,
					tabSize,
A
Alex Dima 已提交
609
					lineInfo.data[lineIndex]
610 611
				);
			}
A
Alex Dima 已提交
612
			renderedLines[lineIndex] = new MinimapLine(dy);
613
			dy += minimapLineHeight;
A
Alex Dima 已提交
614 615
		}

A
Alex Dima 已提交
616
		// Save rendered data for reuse on next frame if possible
A
Alex Dima 已提交
617
		this._lastRenderData = new RenderData(
618 619 620 621 622 623
			new RenderedLayout(
				renderingCtx.visibleRange.startLineNumber,
				renderingCtx.visibleRange.endLineNumber,
				startLineNumber,
				endLineNumber
			),
A
Alex Dima 已提交
624 625
			imageData,
			renderedLines
A
Alex Dima 已提交
626 627
		);

A
Alex Dima 已提交
628
		// Finally, paint to the canvas
629
		const ctx = this._canvas.domNode.getContext('2d');
630
		ctx.putImageData(imageData, 0, 0);
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
	}

	private static _renderUntouchedLines(
		target: ImageData,
		startLineNumber: number,
		endLineNumber: number,
		minimapLineHeight: number,
		lastRenderData: RenderData,
	): boolean[] {

		let needed: boolean[] = [];
		if (!lastRenderData) {
			for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {
				needed[i] = true;
			}
			return needed;
		}

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

		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);
				}
				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);
		}

		return needed;
A
Alex Dima 已提交
704 705
	}

A
Alex Dima 已提交
706 707
	private static _renderLine(
		target: ImageData,
A
Alex Dima 已提交
708 709
		backgroundColor: RGBA,
		useLighterFont,
A
Alex Dima 已提交
710 711 712 713
		renderMinimap: RenderMinimap,
		colorTracker: MinimapTokensColorTracker,
		minimapCharRenderer: MinimapCharRenderer,
		dy: number,
A
Alex Dima 已提交
714
		tabSize: number,
A
Alex Dima 已提交
715
		lineData: ViewLineData
A
Alex Dima 已提交
716
	): void {
A
Alex Dima 已提交
717 718
		const content = lineData.content;
		const tokens = lineData.tokens;
719
		const charWidth = getMinimapCharWidth(renderMinimap);
720
		const maxDx = target.width - charWidth;
A
Alex Dima 已提交
721 722 723

		let dx = 0;
		let charIndex = 0;
724 725
		let tabsCharDelta = 0;

A
Alex Dima 已提交
726 727 728 729
		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 已提交
730
			const tokenColor = colorTracker.getColor(tokenColorId);
A
Alex Dima 已提交
731 732

			for (; charIndex < tokenEndIndex; charIndex++) {
A
Alex Dima 已提交
733
				if (dx > maxDx) {
A
Alex Dima 已提交
734 735 736 737 738
					// hit edge of minimap
					return;
				}
				const charCode = content.charCodeAt(charIndex);

739 740 741 742
				if (charCode === CharCode.Tab) {
					let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
					tabsCharDelta += insertSpacesCount - 1;
					// No need to render anything since tab is invisible
743
					dx += insertSpacesCount * charWidth;
744 745
				} else if (charCode === CharCode.Space) {
					// No need to render anything since space is invisible
746
					dx += charWidth;
747
				} else {
748
					if (renderMinimap === RenderMinimap.Large) {
A
Alex Dima 已提交
749
						minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
750
					} else if (renderMinimap === RenderMinimap.Small) {
A
Alex Dima 已提交
751
						minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
752 753
					} else {
						minimapCharRenderer.blockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
754 755
					}
					dx += charWidth;
756
				}
A
Alex Dima 已提交
757 758 759 760
			}
		}
	}
}