/*---------------------------------------------------------------------------------------------
* 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 { ViewLineToken } from 'vs/editor/common/core/viewLineToken';
import { CharCode } from 'vs/base/common/charCode';
import { LineParts } from 'vs/editor/common/core/lineParts';
export class RenderLineInput {
_renderLineInputBrand: void;
lineContent: string;
tabSize: number;
spaceWidth: number;
stopRenderingLineAfter: number;
renderWhitespace: 'none' | 'boundary' | 'all';
renderControlCharacters: boolean;
lineParts: LineParts;
constructor(
lineContent: string,
tabSize: number,
spaceWidth: number,
stopRenderingLineAfter: number,
renderWhitespace: 'none' | 'boundary' | 'all',
renderControlCharacters: boolean,
lineParts: LineParts
) {
this.lineContent = lineContent;
this.tabSize = tabSize;
this.spaceWidth = spaceWidth;
this.stopRenderingLineAfter = stopRenderingLineAfter;
this.renderWhitespace = renderWhitespace;
this.renderControlCharacters = renderControlCharacters;
this.lineParts = lineParts;
}
public equals(other: RenderLineInput): boolean {
return (
this.lineContent === other.lineContent
&& this.tabSize === other.tabSize
&& this.spaceWidth === other.spaceWidth
&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
&& this.renderWhitespace === other.renderWhitespace
&& this.renderControlCharacters === other.renderControlCharacters
&& this.lineParts.equals(other.lineParts)
);
}
}
export const enum CharacterMappingConstants {
PART_INDEX_MASK = 0b11111111111111110000000000000000,
CHAR_INDEX_MASK = 0b00000000000000001111111111111111,
CHAR_INDEX_OFFSET = 0,
PART_INDEX_OFFSET = 16
}
/**
* Provides a both direction mapping between a line's character and its rendered position.
*/
export class CharacterMapping {
public static getPartIndex(partData: number): number {
return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET;
}
public static getCharIndex(partData: number): number {
return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET;
}
private readonly _data: Uint32Array;
public readonly length: number;
constructor(length: number) {
this.length = length;
this._data = new Uint32Array(this.length);
}
public setPartData(charOffset: number, partIndex: number, charIndex: number): void {
let partData = (
(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
) >>> 0;
this._data[charOffset] = partData;
}
public charOffsetToPartData(charOffset: number): number {
if (this.length === 0) {
return 0;
}
if (charOffset < 0) {
return this._data[0];
}
if (charOffset >= this.length) {
return this._data[this.length - 1];
}
return this._data[charOffset];
}
public partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number {
if (this.length === 0) {
return 0;
}
let searchEntry = (
(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
) >>> 0;
let min = 0;
let max = this.length - 1;
while (min + 1 < max) {
let mid = ((min + max) >>> 1);
let midEntry = this._data[mid];
if (midEntry === searchEntry) {
return mid;
} else if (midEntry > searchEntry) {
max = mid;
} else {
min = mid;
}
}
if (min === max) {
return min;
}
let minEntry = this._data[min];
let maxEntry = this._data[max];
if (minEntry === searchEntry) {
return min;
}
if (maxEntry === searchEntry) {
return max;
}
let minPartIndex = CharacterMapping.getPartIndex(minEntry);
let minCharIndex = CharacterMapping.getCharIndex(minEntry);
let maxPartIndex = CharacterMapping.getPartIndex(maxEntry);
let maxCharIndex: number;
if (minPartIndex !== maxPartIndex) {
// sitting between parts
maxCharIndex = partLength;
} else {
maxCharIndex = CharacterMapping.getCharIndex(maxEntry);
}
let minEntryDistance = charIndex - minCharIndex;
let maxEntryDistance = maxCharIndex - charIndex;
if (minEntryDistance <= maxEntryDistance) {
return min;
}
return max;
}
}
export class RenderLineOutput {
_renderLineOutputBrand: void;
readonly characterMapping: CharacterMapping;
readonly output: string;
readonly isWhitespaceOnly: boolean;
constructor(characterMapping: CharacterMapping, output: string, isWhitespaceOnly: boolean) {
this.characterMapping = characterMapping;
this.output = output;
this.isWhitespaceOnly = isWhitespaceOnly;
}
}
export function renderLine(input: RenderLineInput): RenderLineOutput {
const lineText = input.lineContent;
const lineTextLength = lineText.length;
const tabSize = input.tabSize;
const spaceWidth = input.spaceWidth;
const actualLineParts = input.lineParts.parts;
const renderWhitespace = input.renderWhitespace;
const renderControlCharacters = input.renderControlCharacters;
const charBreakIndex = (input.stopRenderingLineAfter === -1 ? lineTextLength : input.stopRenderingLineAfter - 1);
if (lineTextLength === 0) {
return new RenderLineOutput(
new CharacterMapping(0),
// This is basically for IE's hit test to work
' ',
true
);
}
if (actualLineParts.length === 0) {
throw new Error('Cannot render non empty line without line parts!');
}
return renderLineActual(lineText, lineTextLength, tabSize, spaceWidth, actualLineParts, renderWhitespace, renderControlCharacters, charBreakIndex);
}
function isWhitespace(type: string): boolean {
return (type.indexOf('vs-whitespace') >= 0);
}
function isControlCharacter(characterCode: number): boolean {
return characterCode < 32;
}
const _controlCharacterSequenceConversionStart = 9216;
function controlCharacterToPrintable(characterCode: number): string {
return String.fromCharCode(_controlCharacterSequenceConversionStart + characterCode);
}
function renderLineActual(lineText: string, lineTextLength: number, tabSize: number, spaceWidth: number, actualLineParts: ViewLineToken[], renderWhitespace: 'none' | 'boundary' | 'all', renderControlCharacters: boolean, charBreakIndex: number): RenderLineOutput {
lineTextLength = +lineTextLength;
tabSize = +tabSize;
charBreakIndex = +charBreakIndex;
let charIndex = 0;
let out = '';
let charOffsetInPart = 0;
let tabsCharDelta = 0;
let isWhitespaceOnly = /^\s*$/.test(lineText);
let characterMapping = new CharacterMapping(Math.min(lineTextLength, charBreakIndex) + 1);
out += '';
for (let partIndex = 0, partIndexLen = actualLineParts.length; partIndex < partIndexLen; partIndex++) {
let part = actualLineParts[partIndex];
let parsRendersWhitespace = (renderWhitespace !== 'none' && isWhitespace(part.type));
let toCharIndex = lineTextLength;
if (partIndex + 1 < partIndexLen) {
let nextPart = actualLineParts[partIndex + 1];
toCharIndex = Math.min(lineTextLength, nextPart.startIndex);
}
charOffsetInPart = 0;
if (parsRendersWhitespace) {
let partContentCnt = 0;
let partContent = '';
for (; charIndex < toCharIndex; charIndex++) {
characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
let charCode = lineText.charCodeAt(charIndex);
if (charCode === CharCode.Tab) {
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
charOffsetInPart += insertSpacesCount - 1;
if (insertSpacesCount > 0) {
partContent += '→';
partContentCnt++;
insertSpacesCount--;
}
while (insertSpacesCount > 0) {
partContent += ' ';
partContentCnt++;
insertSpacesCount--;
}
} else {
// must be CharCode.Space
partContent += '·';
partContentCnt++;
}
charOffsetInPart++;
if (charIndex >= charBreakIndex) {
out += `${partContent}…`;
characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
return new RenderLineOutput(
characterMapping,
out,
isWhitespaceOnly
);
}
}
out += `${partContent}`;
} else {
out += ``;
for (; charIndex < toCharIndex; charIndex++) {
characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
let charCode = lineText.charCodeAt(charIndex);
switch (charCode) {
case CharCode.Tab:
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
charOffsetInPart += insertSpacesCount - 1;
while (insertSpacesCount > 0) {
out += ' ';
insertSpacesCount--;
}
break;
case CharCode.Space:
out += ' ';
break;
case CharCode.LessThan:
out += '<';
break;
case CharCode.GreaterThan:
out += '>';
break;
case CharCode.Ampersand:
out += '&';
break;
case CharCode.Null:
out += '';
break;
case CharCode.UTF8_BOM:
case CharCode.LINE_SEPARATOR_2028:
out += '\ufffd';
break;
case CharCode.CarriageReturn:
// zero width space, because carriage return would introduce a line break
out += '';
break;
default:
if (renderControlCharacters && isControlCharacter(charCode)) {
out += controlCharacterToPrintable(charCode);
} else {
out += lineText.charAt(charIndex);
}
}
charOffsetInPart++;
if (charIndex >= charBreakIndex) {
out += '…';
characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
return new RenderLineOutput(
characterMapping,
out,
isWhitespaceOnly
);
}
}
out += '';
}
}
out += '';
// When getting client rects for the last character, we will position the
// text range at the end of the span, insteaf of at the beginning of next span
characterMapping.setPartData(lineTextLength, actualLineParts.length - 1, charOffsetInPart);
return new RenderLineOutput(
characterMapping,
out,
isWhitespaceOnly
);
}