211 lines
4.2 KiB
JavaScript
211 lines
4.2 KiB
JavaScript
import { SCROLL_TAB_SIZE } from './config.ts';
|
|
import { arrayInsert, isAsciiDigit, strChars } from './fns.ts';
|
|
import { highlightToColor, HighlightType } from './highlight.ts';
|
|
import Ansi from './ansi.ts';
|
|
|
|
/**
|
|
* One row of text in the current document. In order to handle
|
|
* multi-byte graphemes, all operations are done on an
|
|
* array of 'character' strings.
|
|
*/
|
|
export class Row {
|
|
/**
|
|
* The actual characters in the current row
|
|
*/
|
|
chars: string[] = [];
|
|
|
|
/**
|
|
* The characters rendered for the current row
|
|
* (like replacing tabs with spaces)
|
|
*/
|
|
rchars: string[] = [];
|
|
|
|
/**
|
|
* The syntax highlighting map
|
|
*/
|
|
hl: HighlightType[] = [];
|
|
|
|
private constructor(s: string | string[] = '') {
|
|
this.chars = Array.isArray(s) ? s : strChars(s);
|
|
this.rchars = [];
|
|
}
|
|
|
|
public get size(): number {
|
|
return this.chars.length;
|
|
}
|
|
|
|
public get rsize(): number {
|
|
return this.rchars.length;
|
|
}
|
|
|
|
public rstring(offset: number = 0): string {
|
|
return this.rchars.slice(offset).join('');
|
|
}
|
|
|
|
public static default(): Row {
|
|
return new Row();
|
|
}
|
|
|
|
public static from(s: string | string[] | Row): Row {
|
|
if (s instanceof Row) {
|
|
return s;
|
|
}
|
|
|
|
return new Row(s);
|
|
}
|
|
|
|
public append(s: string): void {
|
|
this.chars = this.chars.concat(strChars(s));
|
|
this.update();
|
|
}
|
|
|
|
public insertChar(at: number, c: string): void {
|
|
const newSlice = strChars(c);
|
|
if (at >= this.size) {
|
|
this.chars = this.chars.concat(newSlice);
|
|
} else {
|
|
this.chars = arrayInsert(this.chars, at + 1, newSlice);
|
|
}
|
|
}
|
|
|
|
public split(at: number): Row {
|
|
const newRow = new Row(this.chars.slice(at));
|
|
this.chars = this.chars.slice(0, at);
|
|
this.update();
|
|
|
|
return newRow;
|
|
}
|
|
|
|
public delete(at: number): void {
|
|
if (at >= this.size) {
|
|
return;
|
|
}
|
|
|
|
this.chars.splice(at, 1);
|
|
}
|
|
|
|
public find(s: string, offset: number = 0): number | null {
|
|
const thisStr = this.toString();
|
|
if (!this.toString().includes(s)) {
|
|
return null;
|
|
}
|
|
|
|
const byteCount = thisStr.indexOf(s, this.charIndexToByteIndex(offset));
|
|
|
|
// In many cases, the string length will
|
|
// equal the number of characters. So
|
|
// searching is fairly easy
|
|
if (thisStr.length === this.chars.length) {
|
|
return byteCount;
|
|
}
|
|
|
|
// Emoji/Extended Unicode-friendly search
|
|
return this.byteIndexToCharIndex(byteCount);
|
|
}
|
|
|
|
public cxToRx(cx: number): number {
|
|
let rx = 0;
|
|
let j;
|
|
for (j = 0; j < cx; j++) {
|
|
if (this.chars[j] === '\t') {
|
|
rx += (SCROLL_TAB_SIZE - 1) - (rx % SCROLL_TAB_SIZE);
|
|
}
|
|
rx++;
|
|
}
|
|
|
|
return rx;
|
|
}
|
|
|
|
public rxToCx(rx: number): number {
|
|
let curRx = 0;
|
|
let cx = 0;
|
|
for (; cx < this.size; cx++) {
|
|
if (this.chars[cx] === '\t') {
|
|
curRx += (SCROLL_TAB_SIZE - 1) - (curRx % SCROLL_TAB_SIZE);
|
|
}
|
|
curRx++;
|
|
|
|
if (curRx > rx) {
|
|
return cx;
|
|
}
|
|
}
|
|
|
|
return cx;
|
|
}
|
|
|
|
public byteIndexToCharIndex(byteIndex: number): number {
|
|
if (this.toString().length === this.chars.length) {
|
|
return byteIndex;
|
|
}
|
|
|
|
let n = 0;
|
|
let byteCount = 0;
|
|
for (; n < this.chars.length; n++) {
|
|
byteCount += this.chars[n].length;
|
|
if (byteCount > byteIndex) {
|
|
return n;
|
|
}
|
|
}
|
|
|
|
return this.chars.length;
|
|
}
|
|
|
|
public charIndexToByteIndex(charIndex: number): number {
|
|
if (charIndex === 0 || this.toString().length === this.chars.length) {
|
|
return charIndex;
|
|
}
|
|
|
|
return this.chars.slice(0, charIndex).reduce(
|
|
(prev, current) => prev += current.length,
|
|
0,
|
|
);
|
|
}
|
|
|
|
public toString(): string {
|
|
return this.chars.join('');
|
|
}
|
|
|
|
public update(): void {
|
|
const newString = this.chars.join('').replaceAll(
|
|
'\t',
|
|
' '.repeat(SCROLL_TAB_SIZE),
|
|
);
|
|
|
|
this.rchars = strChars(newString);
|
|
this.highlight();
|
|
}
|
|
|
|
public render(offset: number, len: number): string {
|
|
const end = Math.min(len, this.rsize);
|
|
const start = Math.min(offset, len);
|
|
let result = '';
|
|
|
|
for (let i = start; i < end; i++) {
|
|
// if (this.chars[i] === '\t') {
|
|
// result += ' '.repeat(SCROLL_TAB_SIZE);
|
|
// } else {
|
|
result += highlightToColor(this.hl[i]);
|
|
result += this.rchars[i];
|
|
result += Ansi.ResetFormatting;
|
|
}
|
|
// }
|
|
|
|
return result;
|
|
}
|
|
|
|
private highlight(): void {
|
|
const highlighting = [];
|
|
for (const ch of this.rchars) {
|
|
if (isAsciiDigit(ch)) {
|
|
highlighting.push(HighlightType.Number);
|
|
} else {
|
|
highlighting.push(HighlightType.None);
|
|
}
|
|
}
|
|
|
|
this.hl = highlighting;
|
|
}
|
|
}
|
|
|
|
export default Row;
|