Add string highlighting
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-16 15:57:41 -04:00
parent 01b8535c5e
commit b5856f063a
8 changed files with 174 additions and 30 deletions

View File

@ -2,6 +2,7 @@ import Ansi, * as _Ansi from './ansi.ts';
import Buffer from './buffer.ts'; import Buffer from './buffer.ts';
import Document from './document.ts'; import Document from './document.ts';
import Editor from './editor.ts'; import Editor from './editor.ts';
import { FileType } from './filetype/mod.ts';
import { highlightToColor, HighlightType } from './highlight.ts'; import { highlightToColor, HighlightType } from './highlight.ts';
import Option, { None, Some } from './option.ts'; import Option, { None, Some } from './option.ts';
import Position from './position.ts'; import Position from './position.ts';
@ -502,7 +503,7 @@ const RowTest = {
}, },
'.append': () => { '.append': () => {
const row = Row.from('foo'); const row = Row.from('foo');
row.append('bar'); row.append('bar', FileType.default());
assertEquals(row.toString(), 'foobar'); assertEquals(row.toString(), 'foobar');
}, },
'.delete': () => { '.delete': () => {
@ -518,7 +519,7 @@ const RowTest = {
// (Kind of like if the string were one-indexed) // (Kind of like if the string were one-indexed)
const row = Row.from('foobar'); const row = Row.from('foobar');
const row2 = Row.from('bar'); const row2 = Row.from('bar');
assertEquals(row.split(3).toString(), row2.toString()); assertEquals(row.split(3, FileType.default()).toString(), row2.toString());
}, },
'.find': () => { '.find': () => {
const normalRow = Row.from('For whom the bell tolls'); const normalRow = Row.from('For whom the bell tolls');
@ -567,7 +568,7 @@ const RowTest = {
}, },
'.cxToRx, .rxToCx': () => { '.cxToRx, .rxToCx': () => {
const row = Row.from('foo\tbar\tbaz'); const row = Row.from('foo\tbar\tbaz');
row.update(None); row.update(None, FileType.default());
assertNotEquals(row.chars, row.rchars); assertNotEquals(row.chars, row.rchars);
assertNotEquals(row.size, row.rsize); assertNotEquals(row.size, row.rsize);
assertEquals(row.size, 11); assertEquals(row.size, 11);

View File

@ -1,10 +1,14 @@
import Row from './row.ts'; import Row from './row.ts';
import { FileType } from './filetype/mod.ts';
import { arrayInsert, maxAdd, minSub } from './fns.ts'; import { arrayInsert, maxAdd, minSub } from './fns.ts';
import Option, { None, Some } from './option.ts'; import Option, { None, Some } from './option.ts';
import { getRuntime, logDebug, logWarning } from './runtime/mod.ts'; import { getRuntime, logDebug, logWarning } from './runtime/mod.ts';
import { Position, SearchDirection } from './types.ts'; import { Position, SearchDirection } from './types.ts';
export class Document { export class Document {
/**
* Each line of the current document
*/
#rows: Row[]; #rows: Row[];
/** /**
@ -12,9 +16,19 @@ export class Document {
*/ */
public dirty: boolean; public dirty: boolean;
/**
* The meta-data for the file type of the current document
*/
public type: FileType;
private constructor() { private constructor() {
this.#rows = []; this.#rows = [];
this.dirty = false; this.dirty = false;
this.type = FileType.default();
}
public get fileType(): string {
return this.type.name;
} }
public get numRows(): number { public get numRows(): number {
@ -40,6 +54,8 @@ export class Document {
this.#rows = []; this.#rows = [];
} }
this.type = FileType.from(filename);
const rawFile = await file.openFile(filename); const rawFile = await file.openFile(filename);
rawFile.split(/\r?\n/) rawFile.split(/\r?\n/)
.forEach((row) => this.insertRow(this.numRows, row)); .forEach((row) => this.insertRow(this.numRows, row));
@ -56,6 +72,7 @@ export class Document {
const { file } = await getRuntime(); const { file } = await getRuntime();
await file.saveFile(filename, this.rowsToString()); await file.saveFile(filename, this.rowsToString());
this.type = FileType.from(filename);
this.dirty = false; this.dirty = false;
} }
@ -122,7 +139,7 @@ export class Document {
this.insertRow(this.numRows, c); this.insertRow(this.numRows, c);
} else { } else {
this.#rows[at.y].insertChar(at.x, c); this.#rows[at.y].insertChar(at.x, c);
this.#rows[at.y].update(None); this.#rows[at.y].update(None, this.type);
} }
} }
@ -146,9 +163,9 @@ export class Document {
// Split the current row, and insert a new // Split the current row, and insert a new
// row with the leftovers // row with the leftovers
const currentRow = this.#rows[at.y]; const currentRow = this.#rows[at.y];
const newRow = currentRow.split(at.x); const newRow = currentRow.split(at.x, this.type);
currentRow.update(None); currentRow.update(None, this.type);
newRow.update(None); newRow.update(None, this.type);
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow); this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
} }
@ -188,13 +205,13 @@ export class Document {
// At the end of a line, pressing delete will merge // At the end of a line, pressing delete will merge
// the next line into the current one // the next line into the current one
const rowToAppend = this.#rows[at.y + 1].toString(); const rowToAppend = this.#rows[at.y + 1].toString();
row.append(rowToAppend); row.append(rowToAppend, this.type);
this.deleteRow(at.y + 1); this.deleteRow(at.y + 1);
} else { } else {
row.delete(at.x); row.delete(at.x);
} }
row.update(None); row.update(None, this.type);
} }
public row(i: number): Option<Row> { public row(i: number): Option<Row> {
@ -207,14 +224,14 @@ export class Document {
public insertRow(at: number = this.numRows, s: string = ''): void { public insertRow(at: number = this.numRows, s: string = ''): void {
this.#rows = arrayInsert(this.#rows, at, Row.from(s)); this.#rows = arrayInsert(this.#rows, at, Row.from(s));
this.#rows[at].update(None); this.#rows[at].update(None, this.type);
this.dirty = true; this.dirty = true;
} }
public highlight(searchMatch: Option<string>): void { public highlight(searchMatch: Option<string>): void {
this.#rows.forEach((row) => { this.#rows.forEach((row) => {
row.update(searchMatch); row.update(searchMatch, this.type);
}); });
} }

View File

@ -531,7 +531,9 @@ export default class Editor {
const name = (this.filename !== '') ? this.filename : '[No Name]'; const name = (this.filename !== '') ? this.filename : '[No Name]';
const modified = (this.document.dirty) ? '(modified)' : ''; const modified = (this.document.dirty) ? '(modified)' : '';
const status = `${truncate(name, 25)} - ${this.numRows} lines ${modified}`; const status = `${truncate(name, 25)} - ${this.numRows} lines ${modified}`;
const rStatus = `${this.cursor.y + 1},${this.cursor.x + 1}/${this.numRows}`; const rStatus = `${this.document.fileType} | ${this.cursor.y + 1},${
this.cursor.x + 1
}/${this.numRows}`;
let len = Math.min(status.length, this.screen.cols); let len = Math.min(status.length, this.screen.cols);
this.buffer.append(status, len); this.buffer.append(status, len);

View File

@ -0,0 +1,93 @@
import { node_path as path } from '../runtime/mod.ts';
// ----------------------------------------------------------------------------
// File-related types
// ----------------------------------------------------------------------------
export enum FileLang {
TypeScript = 'TypeScript',
JavaScript = 'JavaScript',
CSS = 'CSS',
Plain = 'Plain Text',
}
export interface HighlightingOptions {
numbers: boolean;
strings: boolean;
}
interface IFileType {
readonly name: FileLang;
readonly hlOptions: HighlightingOptions;
get flags(): HighlightingOptions;
}
/**
* The base class for File Types
*/
export abstract class AbstractFileType implements IFileType {
public readonly name: FileLang = FileLang.Plain;
public readonly hlOptions: HighlightingOptions = {
numbers: false,
strings: false,
};
get flags(): HighlightingOptions {
return this.hlOptions;
}
}
// ----------------------------------------------------------------------------
// FileType implementations
// ----------------------------------------------------------------------------
const defaultHighlightOptions: HighlightingOptions = {
numbers: true,
strings: true,
};
class TypeScriptFile extends AbstractFileType {
public readonly name: FileLang = FileLang.TypeScript;
public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions,
};
}
class JavaScriptFile extends AbstractFileType {
public readonly name: FileLang = FileLang.JavaScript;
public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions,
};
}
class CSSFile extends AbstractFileType {
public readonly name: FileLang = FileLang.CSS;
public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions,
};
}
// ----------------------------------------------------------------------------
// External interface
// ----------------------------------------------------------------------------
export class FileType extends AbstractFileType {
static #fileTypeMap = new Map([
['.css', CSSFile],
['.js', JavaScriptFile],
['.jsx', JavaScriptFile],
['.mjs', JavaScriptFile],
['.ts', TypeScriptFile],
['.tsx', TypeScriptFile],
]);
public static default(): FileType {
return new FileType();
}
public static from(filename: string): FileType {
const ext = path.extname(filename);
const type = FileType.#fileTypeMap.get(ext) ?? FileType;
return new type();
}
}

View File

@ -0,0 +1 @@
export * from './filetype.ts';

View File

@ -4,6 +4,7 @@ export enum HighlightType {
None, None,
Number, Number,
Match, Match,
String,
} }
export function highlightToColor(type: HighlightType): string { export function highlightToColor(type: HighlightType): string {
@ -14,6 +15,9 @@ export function highlightToColor(type: HighlightType): string {
case HighlightType.Match: case HighlightType.Match:
return Ansi.color256(21); return Ansi.color256(21);
case HighlightType.String:
return Ansi.color256(201);
default: default:
return Ansi.ResetFormatting; return Ansi.ResetFormatting;
} }

View File

@ -8,6 +8,7 @@ import {
strChars, strChars,
strlen, strlen,
} from './fns.ts'; } from './fns.ts';
import { FileType } from './filetype/mod.ts';
import { highlightToColor, HighlightType } from './highlight.ts'; import { highlightToColor, HighlightType } from './highlight.ts';
import Option, { None, Some } from './option.ts'; import Option, { None, Some } from './option.ts';
import { SearchDirection } from './types.ts'; import { SearchDirection } from './types.ts';
@ -63,9 +64,9 @@ export class Row {
return new Row(s); return new Row(s);
} }
public append(s: string): void { public append(s: string, syntax: FileType): void {
this.chars = this.chars.concat(strChars(s)); this.chars = this.chars.concat(strChars(s));
this.update(None); this.update(None, syntax);
} }
public insertChar(at: number, c: string): void { public insertChar(at: number, c: string): void {
@ -80,10 +81,10 @@ export class Row {
/** /**
* Truncate the current row, and return a new one at the specified index * Truncate the current row, and return a new one at the specified index
*/ */
public split(at: number): Row { public split(at: number, syntax: FileType): Row {
const newRow = new Row(this.chars.slice(at)); const newRow = new Row(this.chars.slice(at));
this.chars = this.chars.slice(0, at); this.chars = this.chars.slice(0, at);
this.update(None); this.update(None, syntax);
return newRow; return newRow;
} }
@ -213,17 +214,17 @@ export class Row {
return this.chars.join(''); return this.chars.join('');
} }
public update(word: Option<string>): void { public update(word: Option<string>, syntax: FileType): void {
const newString = this.chars.join('').replaceAll( const newString = this.chars.join('').replaceAll(
'\t', '\t',
' '.repeat(SCROLL_TAB_SIZE), ' '.repeat(SCROLL_TAB_SIZE),
); );
this.rchars = strChars(newString); this.rchars = strChars(newString);
this.highlight(word); this.highlight(word, syntax);
} }
public highlight(word: Option<string>): void { public highlight(word: Option<string>, syntax: FileType): void {
const highlighting = []; const highlighting = [];
let searchIndex = 0; let searchIndex = 0;
const matches = []; const matches = [];
@ -247,8 +248,10 @@ export class Row {
} }
let prevIsSeparator = true; let prevIsSeparator = true;
let inString: string | boolean = false;
let i = 0; let i = 0;
for (; i < this.rsize;) { for (; i < this.rsize;) {
const ch = this.rchars[i];
const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None; const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None;
// Highlight search matches // Highlight search matches
@ -263,17 +266,38 @@ export class Row {
} }
} }
// Highlight strings
if (syntax.flags.strings) {
if (inString) {
highlighting.push(HighlightType.String);
if (ch === inString) {
inString = false;
}
i += 1;
prevIsSeparator = true;
continue;
} else if (prevIsSeparator && ch === '"' || ch === "'") {
highlighting.push(HighlightType.String);
inString = ch;
prevIsSeparator = true;
i += 1;
continue;
}
}
// Highlight numbers // Highlight numbers
const ch = this.rchars[i]; if (syntax.flags.numbers) {
const isNumeric = isAsciiDigit(ch) && const isNumeric = isAsciiDigit(ch) && (prevIsSeparator ||
(prevIsSeparator || prevHighlight === HighlightType.Number); prevHighlight === HighlightType.Number);
const isDecimalNumeric = ch === '.' && const isDecimalNumeric = ch === '.' &&
prevHighlight === HighlightType.Number; prevHighlight === HighlightType.Number;
const isHexNumeric = ch === 'x' && prevHighlight === HighlightType.Number; const isHexNumeric = ch === 'x' &&
if (isNumeric || isDecimalNumeric || isHexNumeric) { prevHighlight === HighlightType.Number;
highlighting.push(HighlightType.Number); if (isNumeric || isDecimalNumeric || isHexNumeric) {
} else { highlighting.push(HighlightType.Number);
highlighting.push(HighlightType.None); } else {
highlighting.push(HighlightType.None);
}
} }
prevIsSeparator = isSeparator(ch); prevIsSeparator = isSeparator(ch);

View File

@ -10,12 +10,14 @@
"noEmit": true, "noEmit": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"skipLibCheck": true, "skipLibCheck": true,
"composite": true, "composite": true,
"downlevelIteration": true, "downlevelIteration": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"isolatedModules": true "isolatedModules": true,
"strictNullChecks": true
}, },
"exclude": ["src/deno"] "exclude": ["src/deno"]
} }