/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as browser from 'vs/base/browser/browser';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/styleMutator';
import { IConfigurationChangedEvent } from 'vs/editor/common/editorCommon';
import { Decoration } from 'vs/editor/common/viewLayout/viewLineParts';
import { renderViewLine, RenderLineInput, RenderLineOutput, CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { ClassNames } from 'vs/editor/browser/editorBrowser';
import { IVisibleLineData } from 'vs/editor/browser/view/viewLayer';
import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
import { InlineDecoration } from 'vs/editor/common/viewModel/viewModel';
export class ViewLine implements IVisibleLineData {
private _context: ViewContext;
private _renderWhitespace: 'none' | 'boundary' | 'all';
private _renderControlCharacters: boolean;
private _spaceWidth: number;
private _lineHeight: number;
private _stopRenderingLineAfter: number;
private _isMaybeInvalid: boolean;
private _renderedViewLine: RenderedViewLine;
constructor(context: ViewContext) {
this._context = context;
this._renderWhitespace = this._context.configuration.editor.viewInfo.renderWhitespace;
this._renderControlCharacters = this._context.configuration.editor.viewInfo.renderControlCharacters;
this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._stopRenderingLineAfter = this._context.configuration.editor.viewInfo.stopRenderingLineAfter;
this._isMaybeInvalid = true;
this._renderedViewLine = null;
}
// --- begin IVisibleLineData
public getDomNode(): HTMLElement {
if (this._renderedViewLine && this._renderedViewLine.domNode) {
return this._renderedViewLine.domNode.domNode;
}
return null;
}
public setDomNode(domNode: HTMLElement): void {
if (this._renderedViewLine) {
this._renderedViewLine.domNode = createFastDomNode(domNode);
} else {
throw new Error('I have no rendered view line to set the dom node to...');
}
}
public onContentChanged(): void {
this._isMaybeInvalid = true;
}
public onTokensChanged(): void {
this._isMaybeInvalid = true;
}
public onModelDecorationsChanged(): void {
this._isMaybeInvalid = true;
}
public onConfigurationChanged(e: IConfigurationChangedEvent): void {
if (e.viewInfo.renderWhitespace) {
this._isMaybeInvalid = true;
this._renderWhitespace = this._context.configuration.editor.viewInfo.renderWhitespace;
}
if (e.viewInfo.renderControlCharacters) {
this._isMaybeInvalid = true;
this._renderControlCharacters = this._context.configuration.editor.viewInfo.renderControlCharacters;
}
if (e.fontInfo) {
this._isMaybeInvalid = true;
this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth;
}
if (e.lineHeight) {
this._isMaybeInvalid = true;
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.viewInfo.stopRenderingLineAfter) {
this._isMaybeInvalid = true;
this._stopRenderingLineAfter = this._context.configuration.editor.viewInfo.stopRenderingLineAfter;
}
}
public shouldUpdateHTML(startLineNumber: number, lineNumber: number, inlineDecorations: InlineDecoration[]): boolean {
if (this._isMaybeInvalid === false) {
// it appears that nothing relevant has changed
return false;
}
this._isMaybeInvalid = false;
const model = this._context.model;
const actualInlineDecorations = Decoration.filter(inlineDecorations, lineNumber, model.getLineMinColumn(lineNumber), model.getLineMaxColumn(lineNumber));
let renderLineInput = new RenderLineInput(
model.getLineContent(lineNumber),
model.mightContainRTL(),
model.getLineMinColumn(lineNumber) - 1,
model.getLineTokens(lineNumber),
actualInlineDecorations,
model.getTabSize(),
this._spaceWidth,
this._stopRenderingLineAfter,
this._renderWhitespace,
this._renderControlCharacters
);
if (this._renderedViewLine && this._renderedViewLine.input.equals(renderLineInput)) {
// no need to do anything, we have the same render input
return false;
}
let isWhitespaceOnly = /^\s*$/.test(renderLineInput.lineContent);
this._renderedViewLine = createRenderedLine(
this._renderedViewLine ? this._renderedViewLine.domNode : null,
renderLineInput,
this._context.model.mightContainRTL(),
isWhitespaceOnly,
renderViewLine(renderLineInput)
);
return true;
}
public getLineOuterHTML(out: string[], lineNumber: number, deltaTop: number): void {
out.push(`
`);
out.push(this.getLineInnerHTML(lineNumber));
out.push(`
`);
}
public getLineInnerHTML(lineNumber: number): string {
return this._renderedViewLine.html;
}
public layoutLine(lineNumber: number, deltaTop: number): void {
this._renderedViewLine.domNode.setLineNumber(String(lineNumber));
this._renderedViewLine.domNode.setTop(deltaTop);
this._renderedViewLine.domNode.setHeight(this._lineHeight);
}
// --- end IVisibleLineData
public getWidth(): number {
if (!this._renderedViewLine) {
return 0;
}
return this._renderedViewLine.getWidth();
}
public getVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
return this._renderedViewLine.getVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
}
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
return this._renderedViewLine.getColumnOfNodeOffset(lineNumber, spanNode, offset);
}
}
/**
* Every time we render a line, we save what we have rendered in an instance of this class.
*/
class RenderedViewLine {
public domNode: FastDomNode;
public readonly input: RenderLineInput;
public readonly html: string;
protected readonly _characterMapping: CharacterMapping;
private readonly _isWhitespaceOnly: boolean;
private _cachedWidth: number;
/**
* This is a map that is used only when the line is guaranteed to have no RTL text.
*/
private _pixelOffsetCache: number[];
constructor(domNode: FastDomNode, renderLineInput: RenderLineInput, modelContainsRTL: boolean, isWhitespaceOnly: boolean, renderLineOutput: RenderLineOutput) {
this.domNode = domNode;
this.input = renderLineInput;
this.html = renderLineOutput.output;
this._characterMapping = renderLineOutput.characterMapping;
this._isWhitespaceOnly = isWhitespaceOnly;
this._cachedWidth = -1;
this._pixelOffsetCache = null;
if (!modelContainsRTL) {
this._pixelOffsetCache = [];
for (let column = 0, len = this._characterMapping.length; column <= len; column++) {
this._pixelOffsetCache[column] = -1;
}
}
}
// --- Reading from the DOM methods
protected _getReadingTarget(): HTMLElement {
return this.domNode.domNode.firstChild;
}
/**
* Width of the line in pixels
*/
public getWidth(): number {
if (this._cachedWidth === -1) {
this._cachedWidth = this._getReadingTarget().offsetWidth;
}
return this._cachedWidth;
}
/**
* Visible ranges for a model range
*/
public getVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
startColumn = startColumn | 0; // @perf
endColumn = endColumn | 0; // @perf
clientRectDeltaLeft = clientRectDeltaLeft | 0; // @perf
const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) {
// This range is obviously not visible
return null;
}
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) {
startColumn = stopRenderingLineAfter;
}
if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) {
endColumn = stopRenderingLineAfter;
}
if (this._pixelOffsetCache !== null) {
// the text is LTR
let startOffset = this._readPixelOffset(startColumn, clientRectDeltaLeft, endNode);
if (startOffset === -1) {
return null;
}
let endOffset = this._readPixelOffset(endColumn, clientRectDeltaLeft, endNode);
if (endOffset === -1) {
return null;
}
return [new HorizontalRange(startOffset, endOffset - startOffset)];
}
return this._readVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
}
protected _readVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
if (startColumn === endColumn) {
let pixelOffset = this._readPixelOffset(startColumn, clientRectDeltaLeft, endNode);
if (pixelOffset === -1) {
return null;
} else {
return [new HorizontalRange(pixelOffset, 0)];
}
} else {
return this._readRawVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
}
}
protected _readPixelOffset(column: number, clientRectDeltaLeft: number, endNode: HTMLElement): number {
if (this._pixelOffsetCache !== null) {
// the text is LTR
let cachedPixelOffset = this._pixelOffsetCache[column];
if (cachedPixelOffset !== -1) {
return cachedPixelOffset;
}
let result = this._actualReadPixelOffset(column, clientRectDeltaLeft, endNode);
this._pixelOffsetCache[column] = result;
return result;
}
return this._actualReadPixelOffset(column, clientRectDeltaLeft, endNode);
}
private _actualReadPixelOffset(column: number, clientRectDeltaLeft: number, endNode: HTMLElement): number {
if (this._characterMapping.length === 0) {
// This line is empty
return 0;
}
if (column === this._characterMapping.length && this._isWhitespaceOnly) {
// This branch helps in the case of whitespace only lines which have a width set
return this.getWidth();
}
let partData = this._characterMapping.charOffsetToPartData(column - 1);
let partIndex = CharacterMapping.getPartIndex(partData);
let charOffsetInPart = CharacterMapping.getCharIndex(partData);
let r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), partIndex, charOffsetInPart, partIndex, charOffsetInPart, clientRectDeltaLeft, endNode);
if (!r || r.length === 0) {
return -1;
}
return r[0].left;
}
private _readRawVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
if (startColumn === 1 && endColumn === this._characterMapping.length) {
// This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line
return [new HorizontalRange(0, this.getWidth())];
}
let startPartData = this._characterMapping.charOffsetToPartData(startColumn - 1);
let startPartIndex = CharacterMapping.getPartIndex(startPartData);
let startCharOffsetInPart = CharacterMapping.getCharIndex(startPartData);
let endPartData = this._characterMapping.charOffsetToPartData(endColumn - 1);
let endPartIndex = CharacterMapping.getPartIndex(endPartData);
let endCharOffsetInPart = CharacterMapping.getCharIndex(endPartData);
return RangeUtil.readHorizontalRanges(this._getReadingTarget(), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, clientRectDeltaLeft, endNode);
}
/**
* Returns the column for the text found at a specific offset inside a rendered dom node
*/
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
let spanNodeTextContentLength = spanNode.textContent.length;
let spanIndex = -1;
while (spanNode) {
spanNode = spanNode.previousSibling;
spanIndex++;
}
let charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset);
return charOffset + 1;
}
}
class WebKitRenderedViewLine extends RenderedViewLine {
protected _readVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
let output = super._readVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) {
return output;
}
// WebKit is buggy and returns an expanded range (to contain words in some cases)
// The last client rect is enlarged (I think)
// This is an attempt to patch things up
// Find position of previous column
let beforeEndPixelOffset = this._readPixelOffset(endColumn - 1, clientRectDeltaLeft, endNode);
// Find position of last column
let endPixelOffset = this._readPixelOffset(endColumn, clientRectDeltaLeft, endNode);
if (beforeEndPixelOffset !== -1 && endPixelOffset !== -1) {
let isLTR = (beforeEndPixelOffset <= endPixelOffset);
let lastRange = output[output.length - 1];
if (isLTR && lastRange.left < endPixelOffset) {
// Trim down the width of the last visible range to not go after the last column's position
lastRange.width = endPixelOffset - lastRange.left;
}
}
return output;
}
}
const createRenderedLine: (domNode: FastDomNode, renderLineInput: RenderLineInput, modelContainsRTL: boolean, isWhitespaceOnly: boolean, renderLineOutput: RenderLineOutput) => RenderedViewLine = (function () {
if (browser.isWebKit) {
return createWebKitRenderedLine;
}
return createNormalRenderedLine;
})();
function createWebKitRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, modelContainsRTL: boolean, isWhitespaceOnly: boolean, renderLineOutput: RenderLineOutput): RenderedViewLine {
return new WebKitRenderedViewLine(domNode, renderLineInput, modelContainsRTL, isWhitespaceOnly, renderLineOutput);
}
function createNormalRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, modelContainsRTL: boolean, isWhitespaceOnly: boolean, renderLineOutput: RenderLineOutput): RenderedViewLine {
return new RenderedViewLine(domNode, renderLineInput, modelContainsRTL, isWhitespaceOnly, renderLineOutput);
}