minimap.ts 17.1 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 { MinimapCharRenderer, ParsedColor, 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';
A
Alex Dima 已提交
17
import { IViewLayout, ViewLineData } from 'vs/editor/common/viewModel/viewModel';
A
Alex Dima 已提交
18
import { ColorId } from 'vs/editor/common/modes';
19 20
import { FastDomNode, createFastDomNode } from 'vs/base/browser/styleMutator';
import { IDisposable } from 'vs/base/common/lifecycle';
21
import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar';
22

23 24 25 26 27 28 29 30 31 32
const enum RenderMinimap {
	None = 0,
	Small = 1,
	Large = 2
}

class MinimapOptions {

	public readonly renderMinimap: RenderMinimap;

33 34
	public readonly pixelRatio: number;

35 36
	public readonly lineHeight: number;

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
	/**
	 * 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;
69
		this.pixelRatio = pixelRatio;
70
		this.lineHeight = configuration.editor.lineHeight;
71 72 73 74 75 76 77 78 79 80 81 82
		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
83
			&& this.pixelRatio === other.pixelRatio
84
			&& this.lineHeight === other.lineHeight
85 86 87 88 89 90 91 92 93 94
			&& this.minimapWidth === other.minimapWidth
			&& this.minimapHeight === other.minimapHeight
			&& this.canvasInnerWidth === other.canvasInnerWidth
			&& this.canvasInnerHeight === other.canvasInnerHeight
			&& this.canvasOuterWidth === other.canvasOuterWidth
			&& this.canvasOuterHeight === other.canvasOuterHeight
		);
	}
}

95 96
class MinimapLayout {

A
Alex Dima 已提交
97 98
	public readonly viewportStartLineNumber: number;

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
	/**
	 * 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 已提交
118
		prevLayout: MinimapLayout,
119 120 121
		options: MinimapOptions,
		viewportStartLineNumber: number,
		viewportEndLineNumber: number,
122
		viewportHeight: number,
123 124
		lineCount: number,
		scrollbarSliderCenter: number
125 126 127
	) {
		const pixelRatio = options.pixelRatio;
		const minimapLineHeight = (options.renderMinimap === RenderMinimap.Large ? Constants.x2_CHAR_HEIGHT : Constants.x1_CHAR_HEIGHT);
128
		const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
129
		const lineHeight = options.lineHeight;
130

A
Alex Dima 已提交
131 132 133 134 135 136 137 138 139 140
		// 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;
		}

141 142 143 144 145
		if (minimapLinesFitting >= lineCount) {
			// All lines fit in the minimap => no minimap scrolling
			this.startLineNumber = 1;
			this.endLineNumber = lineCount;
		} else {
A
Alex Dima 已提交
146
			// The desire is to align (centers) the minimap's slider with the scrollbar's slider
147

A
Alex Dima 已提交
148
			// For a resolved this.startLineNumber, we can compute the minimap's slider's center with the following formula:
149
			// scrollbarSliderCenter = (viewportStartLineNumber - this.startLineNumber + viewportLineCount/2) * minimapLineHeight / pixelRatio;
A
Alex Dima 已提交
150
			// =>
151 152 153 154
			// 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 已提交
155 156
			let desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;

A
Alex Dima 已提交
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
			// 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
			if (prevLayout) {
				if (prevLayout.viewportStartLineNumber <= viewportStartLineNumber) {
					// going down => make sure we don't go above our previous decision
					if (desiredStartLineNumber < prevLayout.startLineNumber) {
						desiredStartLineNumber = prevLayout.startLineNumber;
						desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;
					}
				}
				if (prevLayout.viewportStartLineNumber >= viewportStartLineNumber) {
					// going up => make sure we don't go below our previous decision
					if (desiredEndLineNumber > prevLayout.endLineNumber) {
						desiredEndLineNumber = prevLayout.endLineNumber;
						desiredStartLineNumber = desiredEndLineNumber - minimapLinesFitting + 1;
					}
				}
			}

A
Alex Dima 已提交
177 178 179 180 181 182 183 184 185 186 187 188
			// 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
189
				desiredStartLineNumber = viewportStartLineNumber;
A
Alex Dima 已提交
190 191 192 193 194 195 196 197
				desiredEndLineNumber = desiredStartLineNumber + minimapLinesFitting - 1;
			}

			// (b) validate on end line number
			if (desiredEndLineNumber > lineCount) {
				// must end before line count
				desiredEndLineNumber = lineCount;
				desiredStartLineNumber = desiredEndLineNumber - minimapLinesFitting + 1;
198
			}
A
Alex Dima 已提交
199 200 201 202
			if (desiredEndLineNumber < viewportEndLineNumber) {
				// must contain the viewport's end line number
				desiredEndLineNumber = viewportEndLineNumber;
				desiredStartLineNumber = desiredEndLineNumber - minimapLinesFitting + 1;
203
			}
204

205
			this.startLineNumber = desiredStartLineNumber;
A
Alex Dima 已提交
206
			this.endLineNumber = desiredEndLineNumber;
207
		}
208 209

		this.sliderTop = Math.floor((viewportStartLineNumber - this.startLineNumber) * minimapLineHeight / pixelRatio);
A
Alex Dima 已提交
210 211
		this.sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
		this.viewportStartLineNumber = viewportStartLineNumber;
212 213 214
	}
}

A
Alex Dima 已提交
215 216
export class Minimap extends ViewPart {

217
	private readonly _viewLayout: IViewLayout;
218
	private readonly _editorScrollbar: EditorScrollbar;
219

A
Alex Dima 已提交
220 221
	private readonly _domNode: FastDomNode<HTMLElement>;
	private readonly _canvas: FastDomNode<HTMLCanvasElement>;
222
	private readonly _slider: FastDomNode<HTMLElement>;
223 224
	private readonly _tokensColorTracker: MinimapTokensColorTracker;
	private readonly _tokensColorTrackerListener: IDisposable;
A
Alex Dima 已提交
225

A
Alex Dima 已提交
226 227
	private readonly _minimapCharRenderer: MinimapCharRenderer;

228
	private _options: MinimapOptions;
A
Alex Dima 已提交
229
	private _lastLayout: MinimapLayout;
230
	private _backgroundFillData: Uint8ClampedArray;
A
Alex Dima 已提交
231

232
	constructor(context: ViewContext, viewLayout: IViewLayout, editorScrollbar: EditorScrollbar) {
A
Alex Dima 已提交
233
		super(context);
234
		this._viewLayout = viewLayout;
235
		this._editorScrollbar = editorScrollbar;
A
Alex Dima 已提交
236

237
		this._options = new MinimapOptions(this._context.configuration);
A
Alex Dima 已提交
238
		this._lastLayout = null;
239 240 241 242 243 244 245 246 247 248 249
		this._backgroundFillData = null;

		this._domNode = createFastDomNode(document.createElement('div'));
		this._domNode.setPosition('absolute');
		this._domNode.setRight(0);

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

250 251 252 253 254
		this._slider = createFastDomNode(document.createElement('div'));
		this._slider.setPosition('absolute');
		this._slider.setClassName('minimap-slider');
		this._domNode.domNode.appendChild(this._slider.domNode);

255 256 257
		this._tokensColorTracker = MinimapTokensColorTracker.getInstance();
		this._tokensColorTrackerListener = this._tokensColorTracker.onDidChange(() => this._backgroundFillData = null);

A
Alex Dima 已提交
258 259
		this._minimapCharRenderer = getOrCreateMinimapCharRenderer();

260
		this._applyLayout();
A
Alex Dima 已提交
261 262 263
	}

	public dispose(): void {
A
Alex Dima 已提交
264
		this._tokensColorTrackerListener.dispose();
A
Alex Dima 已提交
265
		super.dispose();
266 267 268 269 270 271 272 273 274 275 276
	}

	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 已提交
277 278
		this._canvas.domNode.width = this._options.canvasInnerWidth;
		this._canvas.domNode.height = this._options.canvasInnerHeight;
279
		this._slider.setWidth(this._options.minimapWidth);
280 281 282 283 284 285 286 287
		this._backgroundFillData = null;
	}

	private _getBackgroundFillData(): Uint8ClampedArray {
		if (this._backgroundFillData === null) {
			const WIDTH = this._options.canvasInnerWidth;
			const HEIGHT = this._options.canvasInnerHeight;

A
Alex Dima 已提交
288
			const background = this._tokensColorTracker.getColor(ColorId.DefaultBackground);
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
			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;
				}
			}

			this._backgroundFillData = result;
		}
		return this._backgroundFillData;
A
Alex Dima 已提交
308 309 310 311
	}

	// ---- begin view event handlers

312 313 314 315 316 317
	private _onOptionsMaybeChanged(): boolean {
		let opts = new MinimapOptions(this._context.configuration);
		if (this._options.equals(opts)) {
			return false;
		}
		this._options = opts;
A
Alex Dima 已提交
318
		this._lastLayout = null;
319
		this._applyLayout();
A
Alex Dima 已提交
320 321
		return true;
	}
A
Alex Dima 已提交
322 323 324 325 326 327 328 329 330 331

	public onLineMappingChanged(): boolean {
		this._lastLayout = null;
		return true;
	}
	public onModelFlushed(): boolean {
		this._lastLayout = null;
		return true;
	}
	public onModelLinesDeleted(e: editorCommon.IViewLinesDeletedEvent): boolean {
A
Alex Dima 已提交
332
		// TODO@minimap: only do so when the lines are painted in the minimap
A
Alex Dima 已提交
333 334 335 336
		this._lastLayout = null;
		return true;
	}
	public onModelLineChanged(e: editorCommon.IViewLineChangedEvent): boolean {
A
Alex Dima 已提交
337
		// TODO@minimap: only do so when the lines are painted in the minimap
A
Alex Dima 已提交
338 339 340
		return true;
	}
	public onModelLinesInserted(e: editorCommon.IViewLinesInsertedEvent): boolean {
A
Alex Dima 已提交
341
		// TODO@minimap: only do so when the lines are painted in the minimap
A
Alex Dima 已提交
342 343 344 345 346 347 348
		this._lastLayout = null;
		return true;
	}
	public onModelTokensChanged(e: editorCommon.IViewTokensChangedEvent): boolean {
		// TODO@minimap: only do so when the lines are painted in the minimap
		return true;
	}
349 350 351 352 353 354
	public onConfigurationChanged(e: editorCommon.IConfigurationChangedEvent): boolean {
		return this._onOptionsMaybeChanged();
	}
	public onLayoutChanged(layoutInfo: editorCommon.EditorLayoutInfo): boolean {
		return this._onOptionsMaybeChanged();
	}
A
Alex Dima 已提交
355
	public onScrollChanged(e: editorCommon.IScrollEvent): boolean {
A
Alex Dima 已提交
356
		return e.scrollTopChanged || e.scrollHeightChanged;
357
	}
A
Alex Dima 已提交
358
	public onZonesChanged(): boolean {
A
Alex Dima 已提交
359
		this._lastLayout = null;
A
Alex Dima 已提交
360 361
		return true;
	}
A
Alex Dima 已提交
362

A
Alex Dima 已提交
363
	// --- end event handlers
A
Alex Dima 已提交
364 365 366 367 368 369 370 371

	public prepareRender(ctx: IRenderingContext): void {
		// Nothing to read
		if (!this.shouldRender()) {
			throw new Error('I did not ask to render!');
		}
	}

372 373 374 375 376 377 378 379
	public render(renderingCtx: IRestrictedRenderingContext): void {
		const renderMinimap = this._options.renderMinimap;
		if (renderMinimap === RenderMinimap.None) {
			return;
		}

		const WIDTH = this._options.canvasInnerWidth;
		const HEIGHT = this._options.canvasInnerHeight;
A
Alex Dima 已提交
380
		const ctx = this._canvas.domNode.getContext('2d');
381 382
		const minimapLineHeight = (renderMinimap === RenderMinimap.Large ? Constants.x2_CHAR_HEIGHT : Constants.x1_CHAR_HEIGHT);
		const charWidth = (renderMinimap === RenderMinimap.Large ? Constants.x2_CHAR_WIDTH : Constants.x1_CHAR_WIDTH);
383

A
Alex Dima 已提交
384 385
		this._lastLayout = new MinimapLayout(
			this._lastLayout,
386 387 388
			this._options,
			renderingCtx.visibleRange.startLineNumber,
			renderingCtx.visibleRange.endLineNumber,
389
			renderingCtx.viewportHeight,
390 391
			this._context.model.getLineCount(),
			this._editorScrollbar.getVerticalSliderVerticalCenter()
392
		);
A
Alex Dima 已提交
393

A
Alex Dima 已提交
394 395
		this._slider.setTop(this._lastLayout.sliderTop);
		this._slider.setHeight(this._lastLayout.sliderHeight);
A
Alex Dima 已提交
396

A
Alex Dima 已提交
397 398
		const startLineNumber = this._lastLayout.startLineNumber;
		const endLineNumber = this._lastLayout.endLineNumber;
A
Alex Dima 已提交
399 400


401 402 403 404
		// Prepare image data (fill with background color)
		let imageData = ctx.createImageData(WIDTH, HEIGHT);
		imageData.data.set(this._getBackgroundFillData());

A
Alex Dima 已提交
405
		let background = this._tokensColorTracker.getColor(ColorId.DefaultBackground);
406

A
Alex Dima 已提交
407 408
		let start = performance.now();
		let needed: boolean[] = [];
409
		for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
A
Alex Dima 已提交
410
			needed[lineNumber - startLineNumber] = true;
A
Alex Dima 已提交
411
		}
A
Alex Dima 已提交
412 413 414 415
		const data2 = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);
		const tabSize = data2.tabSize;
		let end = performance.now();
		console.log(`FETCHING MINIMAP DATA TOOK ${end - start} ms.`);
A
Alex Dima 已提交
416

A
Alex Dima 已提交
417
		// let start2 = performance.now();
418
		let dy = 0;
419
		for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
A
Alex Dima 已提交
420
			Minimap._renderLine(imageData, background, renderMinimap, charWidth, this._tokensColorTracker, this._minimapCharRenderer, dy, tabSize, data2.data[lineNumber - startLineNumber]);
421
			dy += minimapLineHeight;
A
Alex Dima 已提交
422
		}
A
Alex Dima 已提交
423 424
		// let end2 = performance.now();
		// console.log(`PAINTING MINIMAP TOOK ${end2 - start2} ms.`);
A
Alex Dima 已提交
425

426
		ctx.putImageData(imageData, 0, 0);
A
Alex Dima 已提交
427 428
	}

A
Alex Dima 已提交
429 430 431 432 433 434 435 436
	private static _renderLine(
		target: ImageData,
		backgroundColor: ParsedColor,
		renderMinimap: RenderMinimap,
		charWidth: number,
		colorTracker: MinimapTokensColorTracker,
		minimapCharRenderer: MinimapCharRenderer,
		dy: number,
A
Alex Dima 已提交
437
		tabSize: number,
A
Alex Dima 已提交
438
		lineData: ViewLineData
A
Alex Dima 已提交
439
	): void {
A
Alex Dima 已提交
440 441
		const content = lineData.content;
		const tokens = lineData.tokens;
442
		const maxDx = target.width - charWidth;
A
Alex Dima 已提交
443 444 445

		let dx = 0;
		let charIndex = 0;
446 447
		let tabsCharDelta = 0;

A
Alex Dima 已提交
448 449 450 451
		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 已提交
452
			const tokenColor = colorTracker.getColor(tokenColorId);
A
Alex Dima 已提交
453 454

			for (; charIndex < tokenEndIndex; charIndex++) {
A
Alex Dima 已提交
455
				if (dx > maxDx) {
A
Alex Dima 已提交
456 457 458 459 460
					// hit edge of minimap
					return;
				}
				const charCode = content.charCodeAt(charIndex);

461 462 463 464
				if (charCode === CharCode.Tab) {
					let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
					tabsCharDelta += insertSpacesCount - 1;
					// No need to render anything since tab is invisible
465
					dx += insertSpacesCount * charWidth;
466 467
				} else if (charCode === CharCode.Space) {
					// No need to render anything since space is invisible
468
					dx += charWidth;
469
				} else {
470
					if (renderMinimap === RenderMinimap.Large) {
A
Alex Dima 已提交
471
						minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor);
472
					} else {
A
Alex Dima 已提交
473
						minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor);
474 475
					}
					dx += charWidth;
476
				}
A
Alex Dima 已提交
477 478 479 480
			}
		}
	}
}