/*---------------------------------------------------------------------------------------------
* 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 {HorizontalRange, IConfigurationChangedEvent, IModelDecoration} from 'vs/editor/common/editorCommon';
import {LineParts, createLineParts, getColumnOfLinePartOffset} from 'vs/editor/common/viewLayout/viewLineParts';
import {renderLine, RenderLineInput} from 'vs/editor/common/viewLayout/viewLineRenderer';
import {ClassNames, IViewContext} from 'vs/editor/browser/editorBrowser';
import {IVisibleLineData} from 'vs/editor/browser/view/viewLayer';
import {RangeUtil} from 'vs/editor/browser/viewParts/lines/rangeUtil';
export class ViewLine implements IVisibleLineData {
protected _context:IViewContext;
private _renderWhitespace: boolean;
private _spaceWidth: number;
private _lineHeight: number;
private _stopRenderingLineAfter: number;
protected _fontLigatures: boolean;
private _domNode: FastDomNode;
private _lineParts: LineParts;
private _isInvalid: boolean;
private _isMaybeInvalid: boolean;
protected _charOffsetInPart:number[];
private _lastRenderedPartIndex:number;
private _cachedWidth: number;
constructor(context:IViewContext) {
this._context = context;
this._renderWhitespace = this._context.configuration.editor.renderWhitespace;
this._spaceWidth = this._context.configuration.editor.spaceWidth;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._stopRenderingLineAfter = this._context.configuration.editor.stopRenderingLineAfter;
this._fontLigatures = this._context.configuration.editor.fontLigatures;
this._domNode = null;
this._isInvalid = true;
this._isMaybeInvalid = false;
this._lineParts = null;
this._charOffsetInPart = [];
this._lastRenderedPartIndex = 0;
}
// --- begin IVisibleLineData
public getDomNode(): HTMLElement {
if (!this._domNode) {
return null;
}
return this._domNode.domNode;
}
public setDomNode(domNode:HTMLElement): void {
this._domNode = createFastDomNode(domNode);
}
public onContentChanged(): void {
this._isInvalid = true;
}
public onLinesInsertedAbove(): void {
this._isMaybeInvalid = true;
}
public onLinesDeletedAbove(): void {
this._isMaybeInvalid = true;
}
public onLineChangedAbove(): void {
this._isMaybeInvalid = true;
}
public onTokensChanged(): void {
this._isMaybeInvalid = true;
}
public onModelDecorationsChanged(): void {
this._isMaybeInvalid = true;
}
public onConfigurationChanged(e:IConfigurationChangedEvent): void {
if (e.renderWhitespace) {
this._renderWhitespace = this._context.configuration.editor.renderWhitespace;
}
if (e.spaceWidth) {
this._spaceWidth = this._context.configuration.editor.spaceWidth;
}
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.stopRenderingLineAfter) {
this._stopRenderingLineAfter = this._context.configuration.editor.stopRenderingLineAfter;
}
if (e.fontLigatures) {
this._fontLigatures = this._context.configuration.editor.fontLigatures;
}
this._isInvalid = true;
}
public shouldUpdateHTML(startLineNumber:number, lineNumber:number, inlineDecorations:IModelDecoration[]): boolean {
let newLineParts:LineParts = null;
if (this._isMaybeInvalid || this._isInvalid) {
// Compute new line parts only if there is some evidence that something might have changed
newLineParts = createLineParts(
lineNumber,
this._context.model.getLineMinColumn(lineNumber),
this._context.model.getLineContent(lineNumber),
this._context.model.getTabSize(),
this._context.model.getLineTokens(lineNumber),
inlineDecorations,
this._renderWhitespace
);
}
// Decide if isMaybeInvalid flips isInvalid to true
if (this._isMaybeInvalid) {
if (!this._isInvalid) {
if (!this._lineParts || !this._lineParts.equals(newLineParts)) {
this._isInvalid = true;
}
}
this._isMaybeInvalid = false;
}
if (this._isInvalid) {
this._lineParts = newLineParts;
}
return this._isInvalid;
}
public getLineOuterHTML(out:string[], lineNumber:number, deltaTop:number): void {
out.push('
');
out.push(this.getLineInnerHTML(lineNumber));
out.push('
');
}
public getLineInnerHTML(lineNumber: number): string {
this._isInvalid = false;
return this._render(lineNumber, this._lineParts);
}
public layoutLine(lineNumber:number, deltaTop:number): void {
this._domNode.setLineNumber(String(lineNumber));
this._domNode.setTop(deltaTop);
this._domNode.setHeight(this._lineHeight);
}
// --- end IVisibleLineData
private _render(lineNumber:number, lineParts:LineParts): string {
this._cachedWidth = -1;
let r = renderLine(new RenderLineInput(
this._context.model.getLineContent(lineNumber),
this._context.model.getTabSize(),
this._spaceWidth,
this._stopRenderingLineAfter,
this._renderWhitespace,
lineParts.getParts()
));
this._charOffsetInPart = r.charOffsetInPart;
this._lastRenderedPartIndex = r.lastRenderedPartIndex;
return r.output;
}
// --- 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._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;
}
return this._readVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
}
protected _readVisibleRangesForRange(startColumn:number, endColumn:number, clientRectDeltaLeft:number, endNode:HTMLElement): HorizontalRange[] {
if (startColumn === endColumn) {
return this._readRawVisibleRangesForPosition(startColumn, clientRectDeltaLeft, endNode);
} else {
return this._readRawVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
}
}
protected _readRawVisibleRangesForPosition(column:number, clientRectDeltaLeft:number, endNode:HTMLElement): HorizontalRange[] {
if (this._charOffsetInPart.length === 0) {
// This line is empty
return [new HorizontalRange(0, 0)];
}
let partIndex = findIndexInArrayWithMax(this._lineParts, column - 1, this._lastRenderedPartIndex);
let charOffsetInPart = this._charOffsetInPart[column - 1];
return RangeUtil.readHorizontalRanges(this._getReadingTarget(), partIndex, charOffsetInPart, partIndex, charOffsetInPart, clientRectDeltaLeft, this._getScaleRatio(), endNode);
}
private _readRawVisibleRangesForRange(startColumn:number, endColumn:number, clientRectDeltaLeft:number, endNode:HTMLElement): HorizontalRange[] {
if (startColumn === 1 && endColumn === this._charOffsetInPart.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 startPartIndex = findIndexInArrayWithMax(this._lineParts, startColumn - 1, this._lastRenderedPartIndex);
let startCharOffsetInPart = this._charOffsetInPart[startColumn - 1];
let endPartIndex = findIndexInArrayWithMax(this._lineParts, endColumn - 1, this._lastRenderedPartIndex);
let endCharOffsetInPart = this._charOffsetInPart[endColumn - 1];
return RangeUtil.readHorizontalRanges(this._getReadingTarget(), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, clientRectDeltaLeft, this._getScaleRatio(), endNode);
}
protected _getScaleRatio(): number {
return 1;
}
/**
* 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 lineParts = this._lineParts.getParts();
return getColumnOfLinePartOffset(
this._stopRenderingLineAfter,
lineParts,
this._context.model.getLineMaxColumn(lineNumber),
this._charOffsetInPart,
spanIndex,
spanNodeTextContentLength,
offset
);
}
}
class IEViewLine extends ViewLine {
constructor(context:IViewContext) {
super(context);
}
protected _getScaleRatio(): number {
return screen.logicalXDPI / screen.deviceXDPI;
}
}
class WebKitViewLine extends ViewLine {
constructor(context:IViewContext) {
super(context);
}
protected _readVisibleRangesForRange(startColumn:number, endColumn:number, clientRectDeltaLeft:number, endNode:HTMLElement): HorizontalRange[] {
let output = super._readVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode);
if (this._fontLigatures && output.length === 1 && endColumn > 1 && endColumn === this._charOffsetInPart.length) {
let lastSpanBoundingClientRect = (this._getReadingTarget().lastChild).getBoundingClientRect();
let lastSpanBoundingClientRectRight = lastSpanBoundingClientRect.right - clientRectDeltaLeft;
if (startColumn === endColumn) {
output[0].left = lastSpanBoundingClientRectRight;
output[0].width = 0;
} else {
output[0].width = lastSpanBoundingClientRectRight - output[0].left;
}
return output;
}
if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._charOffsetInPart.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 beforeEndVisibleRanges = this._readRawVisibleRangesForPosition(endColumn - 1, clientRectDeltaLeft, endNode);
// Find position of last column
let endVisibleRanges = this._readRawVisibleRangesForPosition(endColumn, clientRectDeltaLeft, endNode);
if (beforeEndVisibleRanges && beforeEndVisibleRanges.length > 0 && endVisibleRanges && endVisibleRanges.length > 0) {
let beforeEndVisibleRange = beforeEndVisibleRanges[0];
let endVisibleRange = endVisibleRanges[0];
let isLTR = (beforeEndVisibleRange.left <= endVisibleRange.left);
let lastRange = output[output.length - 1];
if (isLTR && lastRange.left < endVisibleRange.left) {
// Trim down the width of the last visible range to not go after the last column's position
lastRange.width = endVisibleRange.left - lastRange.left;
}
}
return output;
}
}
function findIndexInArrayWithMax(lineParts:LineParts, desiredIndex: number, maxResult:number): number {
let r = lineParts.findIndexOfOffset(desiredIndex);
return r <= maxResult ? r : maxResult;
}
export let createLine: (context: IViewContext) => ViewLine = (function() {
if (window.screen && window.screen.deviceXDPI && (navigator.userAgent.indexOf('Trident/6.0') >= 0 || navigator.userAgent.indexOf('Trident/5.0') >= 0)) {
// IE11 doesn't need the screen.logicalXDPI / screen.deviceXDPI ratio multiplication
// for TextRange.getClientRects() anymore
return createIELine;
} else if (browser.isWebKit) {
return createWebKitLine;
}
return createNormalLine;
})();
function createIELine(context: IViewContext): ViewLine {
return new IEViewLine(context);
}
function createWebKitLine(context: IViewContext): ViewLine {
return new WebKitViewLine(context);
}
function createNormalLine(context: IViewContext): ViewLine {
return new ViewLine(context);
}