Enable multiline-comment-style with starred-block to require JSDoc-style block comments with content on separate lines from the opening delimiter. Auto-fix existing violations across the codebase. Fixes #5899 Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
/**
|
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { IBufferLine, ICellData, CharData } from 'common/Types';
|
|
import { ICharacterJoiner } from 'browser/Types';
|
|
import { AttributeData } from 'common/buffer/AttributeData';
|
|
import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
|
|
import { CellData } from 'common/buffer/CellData';
|
|
import { IBufferService } from 'common/services/Services';
|
|
import { ICharacterJoinerService } from 'browser/services/Services';
|
|
|
|
export class JoinedCellData extends AttributeData implements ICellData {
|
|
private _width: number;
|
|
/*
|
|
* .content carries no meaning for joined CellData, simply nullify it
|
|
* thus we have to overload all other .content accessors
|
|
*/
|
|
public content: number = 0;
|
|
public fg: number;
|
|
public bg: number;
|
|
public combinedData: string = '';
|
|
|
|
constructor(firstCell: ICellData, chars: string, width: number) {
|
|
super();
|
|
this.fg = firstCell.fg;
|
|
this.bg = firstCell.bg;
|
|
this.combinedData = chars;
|
|
this._width = width;
|
|
}
|
|
|
|
public isCombined(): number {
|
|
// always mark joined cell data as combined
|
|
return Content.IS_COMBINED_MASK;
|
|
}
|
|
|
|
public getWidth(): number {
|
|
return this._width;
|
|
}
|
|
|
|
public getChars(): string {
|
|
return this.combinedData;
|
|
}
|
|
|
|
public getCode(): number {
|
|
/*
|
|
* code always gets the highest possible fake codepoint (read as -1)
|
|
* this is needed as code is used by caches as identifier
|
|
*/
|
|
return 0x1FFFFF;
|
|
}
|
|
|
|
public setFromCharData(value: CharData): void {
|
|
throw new Error('not implemented');
|
|
}
|
|
|
|
public getAsCharData(): CharData {
|
|
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
|
}
|
|
}
|
|
|
|
export class CharacterJoinerService implements ICharacterJoinerService {
|
|
public serviceBrand: undefined;
|
|
|
|
private _characterJoiners: ICharacterJoiner[] = [];
|
|
private _nextCharacterJoinerId: number = 0;
|
|
private _workCell: CellData = new CellData();
|
|
|
|
constructor(
|
|
@IBufferService private _bufferService: IBufferService
|
|
) { }
|
|
|
|
public register(handler: (text: string) => [number, number][]): number {
|
|
const joiner: ICharacterJoiner = {
|
|
id: this._nextCharacterJoinerId++,
|
|
handler
|
|
};
|
|
|
|
this._characterJoiners.push(joiner);
|
|
return joiner.id;
|
|
}
|
|
|
|
public deregister(joinerId: number): boolean {
|
|
for (let i = 0; i < this._characterJoiners.length; i++) {
|
|
if (this._characterJoiners[i].id === joinerId) {
|
|
this._characterJoiners.splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public getJoinedCharacters(row: number): [number, number][] {
|
|
if (this._characterJoiners.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const line = this._bufferService.buffer.lines.get(row);
|
|
if (!line || line.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const ranges: [number, number][] = [];
|
|
const lineStr = line.translateToString(true);
|
|
|
|
/*
|
|
* Because some cells can be represented by multiple javascript characters,
|
|
* we track the cell and the string indexes separately. This allows us to
|
|
* translate the string ranges we get from the joiners back into cell ranges
|
|
* for use when rendering
|
|
*/
|
|
let rangeStartColumn = 0;
|
|
let currentStringIndex = 0;
|
|
let rangeStartStringIndex = 0;
|
|
let rangeAttrFG = line.getFg(0);
|
|
let rangeAttrBG = line.getBg(0);
|
|
|
|
for (let x = 0; x < line.getTrimmedLength(); x++) {
|
|
line.loadCell(x, this._workCell);
|
|
|
|
if (this._workCell.getWidth() === 0) {
|
|
// If this character is of width 0, skip it.
|
|
continue;
|
|
}
|
|
|
|
// End of range
|
|
if (this._workCell.fg !== rangeAttrFG || this._workCell.bg !== rangeAttrBG) {
|
|
/*
|
|
* If we ended up with a sequence of more than one character,
|
|
* look for ranges to join.
|
|
*/
|
|
if (x - rangeStartColumn > 1) {
|
|
const joinedRanges = this._getJoinedRanges(
|
|
lineStr,
|
|
rangeStartStringIndex,
|
|
currentStringIndex,
|
|
line,
|
|
rangeStartColumn
|
|
);
|
|
for (let i = 0; i < joinedRanges.length; i++) {
|
|
ranges.push(joinedRanges[i]);
|
|
}
|
|
}
|
|
|
|
// Reset our markers for a new range.
|
|
rangeStartColumn = x;
|
|
rangeStartStringIndex = currentStringIndex;
|
|
rangeAttrFG = this._workCell.fg;
|
|
rangeAttrBG = this._workCell.bg;
|
|
}
|
|
|
|
currentStringIndex += this._workCell.getChars().length || WHITESPACE_CELL_CHAR.length;
|
|
}
|
|
|
|
// Process any trailing ranges.
|
|
if (this._bufferService.cols - rangeStartColumn > 1) {
|
|
const joinedRanges = this._getJoinedRanges(
|
|
lineStr,
|
|
rangeStartStringIndex,
|
|
currentStringIndex,
|
|
line,
|
|
rangeStartColumn
|
|
);
|
|
for (let i = 0; i < joinedRanges.length; i++) {
|
|
ranges.push(joinedRanges[i]);
|
|
}
|
|
}
|
|
|
|
return ranges;
|
|
}
|
|
|
|
/**
|
|
* Given a segment of a line of text, find all ranges of text that should be
|
|
* joined in a single rendering unit. Ranges are internally converted to
|
|
* column ranges, rather than string ranges.
|
|
* @param line String representation of the full line of text
|
|
* @param startIndex Start position of the range to search in the string (inclusive)
|
|
* @param endIndex End position of the range to search in the string (exclusive)
|
|
*/
|
|
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
|
|
const text = line.substring(startIndex, endIndex);
|
|
/*
|
|
* At this point we already know that there is at least one joiner so
|
|
* we can just pull its value and assign it directly rather than
|
|
* merging it into an empty array, which incurs unnecessary writes.
|
|
*/
|
|
let allJoinedRanges: [number, number][] = [];
|
|
try {
|
|
allJoinedRanges = this._characterJoiners[0].handler(text);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
for (let i = 1; i < this._characterJoiners.length; i++) {
|
|
// We merge any overlapping ranges across the different joiners
|
|
try {
|
|
const joinerRanges = this._characterJoiners[i].handler(text);
|
|
for (let j = 0; j < joinerRanges.length; j++) {
|
|
CharacterJoinerService._mergeRanges(allJoinedRanges, joinerRanges[j]);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
this._stringRangesToCellRanges(allJoinedRanges, lineData, startCol);
|
|
return allJoinedRanges;
|
|
}
|
|
|
|
/**
|
|
* Modifies the provided ranges in-place to adjust for variations between
|
|
* string length and cell width so that the range represents a cell range,
|
|
* rather than the string range the joiner provides.
|
|
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
|
|
* @param line Cell data for the relevant line in the terminal
|
|
* @param startCol Offset within the line to start from
|
|
*/
|
|
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
|
|
let currentRangeIndex = 0;
|
|
let currentRangeStarted = false;
|
|
let currentStringIndex = 0;
|
|
let currentRange = ranges[currentRangeIndex];
|
|
|
|
// If we got through all of the ranges, stop searching
|
|
if (!currentRange) {
|
|
return;
|
|
}
|
|
|
|
for (let x = startCol; x < this._bufferService.cols; x++) {
|
|
const width = line.getWidth(x);
|
|
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
|
|
|
|
/*
|
|
* We skip zero-width characters when creating the string to join the text
|
|
* so we do the same here
|
|
*/
|
|
if (width === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Adjust the start of the range
|
|
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
|
|
currentRange[0] = x;
|
|
currentRangeStarted = true;
|
|
}
|
|
|
|
// Adjust the end of the range
|
|
if (currentRange[1] <= currentStringIndex) {
|
|
currentRange[1] = x;
|
|
|
|
// We're finished with this range, so we move to the next one
|
|
currentRange = ranges[++currentRangeIndex];
|
|
|
|
// If there are no more ranges left, stop searching
|
|
if (!currentRange) {
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Ranges can be on adjacent characters. Because the end index of the
|
|
* ranges are exclusive, this means that the index for the start of a
|
|
* range can be the same as the end index of the previous range. To
|
|
* account for the start of the next range, we check here just in case.
|
|
*/
|
|
if (currentRange[0] <= currentStringIndex) {
|
|
currentRange[0] = x;
|
|
currentRangeStarted = true;
|
|
} else {
|
|
currentRangeStarted = false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Adjust the string index based on the character length to line up with
|
|
* the column adjustment
|
|
*/
|
|
currentStringIndex += length;
|
|
}
|
|
|
|
/*
|
|
* If there is still a range left at the end, it must extend all the way to
|
|
* the end of the line.
|
|
*/
|
|
if (currentRange) {
|
|
currentRange[1] = this._bufferService.cols;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges the range defined by the provided start and end into the list of
|
|
* existing ranges. The merge is done in place on the existing range for
|
|
* performance and is also returned.
|
|
* @param ranges Existing range list
|
|
* @param newRange Tuple of two numbers representing the new range to merge in.
|
|
* @returns The ranges input with the new range merged in place
|
|
*/
|
|
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
|
|
let inRange = false;
|
|
for (let i = 0; i < ranges.length; i++) {
|
|
const range = ranges[i];
|
|
if (!inRange) {
|
|
if (newRange[1] <= range[0]) {
|
|
// Case 1: New range is before the search range
|
|
ranges.splice(i, 0, newRange);
|
|
return ranges;
|
|
}
|
|
|
|
if (newRange[1] <= range[1]) {
|
|
/*
|
|
* Case 2: New range is either wholly contained within the
|
|
* search range or overlaps with the front of it
|
|
*/
|
|
range[0] = Math.min(newRange[0], range[0]);
|
|
return ranges;
|
|
}
|
|
|
|
if (newRange[0] < range[1]) {
|
|
/*
|
|
* Case 3: New range either wholly contains the search range
|
|
* or overlaps with the end of it
|
|
*/
|
|
range[0] = Math.min(newRange[0], range[0]);
|
|
inRange = true;
|
|
}
|
|
|
|
// Case 4: New range starts after the search range
|
|
continue;
|
|
} else {
|
|
if (newRange[1] <= range[0]) {
|
|
/*
|
|
* Case 5: New range extends from previous range but doesn't
|
|
* reach the current one
|
|
*/
|
|
ranges[i - 1][1] = newRange[1];
|
|
return ranges;
|
|
}
|
|
|
|
if (newRange[1] <= range[1]) {
|
|
/*
|
|
* Case 6: New range extends from prvious range into the
|
|
* current range
|
|
*/
|
|
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
|
|
ranges.splice(i, 1);
|
|
return ranges;
|
|
}
|
|
|
|
/*
|
|
* Case 7: New range extends from previous range past the
|
|
* end of the current range
|
|
*/
|
|
ranges.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
if (inRange) {
|
|
// Case 8: New range extends past the last existing range
|
|
ranges[ranges.length - 1][1] = newRange[1];
|
|
} else {
|
|
// Case 9: New range starts after the last existing range
|
|
ranges.push(newRange);
|
|
}
|
|
|
|
return ranges;
|
|
}
|
|
}
|