From b5856f063a35a04ad343eef4c03e5b7b0bff9d29 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Tue, 16 Jul 2024 15:57:41 -0400 Subject: [PATCH] Add string highlighting --- src/common/all_test.ts | 7 +-- src/common/document.ts | 33 +++++++++--- src/common/editor.ts | 4 +- src/common/filetype/filetype.ts | 93 +++++++++++++++++++++++++++++++++ src/common/filetype/mod.ts | 1 + src/common/highlight.ts | 4 ++ src/common/row.ts | 58 ++++++++++++++------ tsconfig.json | 4 +- 8 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 src/common/filetype/filetype.ts create mode 100644 src/common/filetype/mod.ts diff --git a/src/common/all_test.ts b/src/common/all_test.ts index d747353..19a1407 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -2,6 +2,7 @@ import Ansi, * as _Ansi from './ansi.ts'; import Buffer from './buffer.ts'; import Document from './document.ts'; import Editor from './editor.ts'; +import { FileType } from './filetype/mod.ts'; import { highlightToColor, HighlightType } from './highlight.ts'; import Option, { None, Some } from './option.ts'; import Position from './position.ts'; @@ -502,7 +503,7 @@ const RowTest = { }, '.append': () => { const row = Row.from('foo'); - row.append('bar'); + row.append('bar', FileType.default()); assertEquals(row.toString(), 'foobar'); }, '.delete': () => { @@ -518,7 +519,7 @@ const RowTest = { // (Kind of like if the string were one-indexed) const row = Row.from('foobar'); const row2 = Row.from('bar'); - assertEquals(row.split(3).toString(), row2.toString()); + assertEquals(row.split(3, FileType.default()).toString(), row2.toString()); }, '.find': () => { const normalRow = Row.from('For whom the bell tolls'); @@ -567,7 +568,7 @@ const RowTest = { }, '.cxToRx, .rxToCx': () => { const row = Row.from('foo\tbar\tbaz'); - row.update(None); + row.update(None, FileType.default()); assertNotEquals(row.chars, row.rchars); assertNotEquals(row.size, row.rsize); assertEquals(row.size, 11); diff --git a/src/common/document.ts b/src/common/document.ts index db7d5bd..7e28037 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -1,10 +1,14 @@ import Row from './row.ts'; +import { FileType } from './filetype/mod.ts'; import { arrayInsert, maxAdd, minSub } from './fns.ts'; import Option, { None, Some } from './option.ts'; import { getRuntime, logDebug, logWarning } from './runtime/mod.ts'; import { Position, SearchDirection } from './types.ts'; export class Document { + /** + * Each line of the current document + */ #rows: Row[]; /** @@ -12,9 +16,19 @@ export class Document { */ public dirty: boolean; + /** + * The meta-data for the file type of the current document + */ + public type: FileType; + private constructor() { this.#rows = []; this.dirty = false; + this.type = FileType.default(); + } + + public get fileType(): string { + return this.type.name; } public get numRows(): number { @@ -40,6 +54,8 @@ export class Document { this.#rows = []; } + this.type = FileType.from(filename); + const rawFile = await file.openFile(filename); rawFile.split(/\r?\n/) .forEach((row) => this.insertRow(this.numRows, row)); @@ -56,6 +72,7 @@ export class Document { const { file } = await getRuntime(); await file.saveFile(filename, this.rowsToString()); + this.type = FileType.from(filename); this.dirty = false; } @@ -122,7 +139,7 @@ export class Document { this.insertRow(this.numRows, c); } else { 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 // row with the leftovers const currentRow = this.#rows[at.y]; - const newRow = currentRow.split(at.x); - currentRow.update(None); - newRow.update(None); + const newRow = currentRow.split(at.x, this.type); + currentRow.update(None, this.type); + newRow.update(None, this.type); 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 // the next line into the current one const rowToAppend = this.#rows[at.y + 1].toString(); - row.append(rowToAppend); + row.append(rowToAppend, this.type); this.deleteRow(at.y + 1); } else { row.delete(at.x); } - row.update(None); + row.update(None, this.type); } public row(i: number): Option { @@ -207,14 +224,14 @@ export class Document { public insertRow(at: number = this.numRows, s: string = ''): void { this.#rows = arrayInsert(this.#rows, at, Row.from(s)); - this.#rows[at].update(None); + this.#rows[at].update(None, this.type); this.dirty = true; } public highlight(searchMatch: Option): void { this.#rows.forEach((row) => { - row.update(searchMatch); + row.update(searchMatch, this.type); }); } diff --git a/src/common/editor.ts b/src/common/editor.ts index f8deef0..d302cca 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -531,7 +531,9 @@ export default class Editor { const name = (this.filename !== '') ? this.filename : '[No Name]'; const modified = (this.document.dirty) ? '(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); this.buffer.append(status, len); diff --git a/src/common/filetype/filetype.ts b/src/common/filetype/filetype.ts new file mode 100644 index 0000000..730f3b1 --- /dev/null +++ b/src/common/filetype/filetype.ts @@ -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(); + } +} diff --git a/src/common/filetype/mod.ts b/src/common/filetype/mod.ts new file mode 100644 index 0000000..4dbdee3 --- /dev/null +++ b/src/common/filetype/mod.ts @@ -0,0 +1 @@ +export * from './filetype.ts'; diff --git a/src/common/highlight.ts b/src/common/highlight.ts index 7553e1e..d4393b9 100644 --- a/src/common/highlight.ts +++ b/src/common/highlight.ts @@ -4,6 +4,7 @@ export enum HighlightType { None, Number, Match, + String, } export function highlightToColor(type: HighlightType): string { @@ -14,6 +15,9 @@ export function highlightToColor(type: HighlightType): string { case HighlightType.Match: return Ansi.color256(21); + case HighlightType.String: + return Ansi.color256(201); + default: return Ansi.ResetFormatting; } diff --git a/src/common/row.ts b/src/common/row.ts index d143276..2566a45 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -8,6 +8,7 @@ import { strChars, strlen, } from './fns.ts'; +import { FileType } from './filetype/mod.ts'; import { highlightToColor, HighlightType } from './highlight.ts'; import Option, { None, Some } from './option.ts'; import { SearchDirection } from './types.ts'; @@ -63,9 +64,9 @@ export class Row { return new Row(s); } - public append(s: string): void { + public append(s: string, syntax: FileType): void { this.chars = this.chars.concat(strChars(s)); - this.update(None); + this.update(None, syntax); } 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 */ - public split(at: number): Row { + public split(at: number, syntax: FileType): Row { const newRow = new Row(this.chars.slice(at)); this.chars = this.chars.slice(0, at); - this.update(None); + this.update(None, syntax); return newRow; } @@ -213,17 +214,17 @@ export class Row { return this.chars.join(''); } - public update(word: Option): void { + public update(word: Option, syntax: FileType): void { const newString = this.chars.join('').replaceAll( '\t', ' '.repeat(SCROLL_TAB_SIZE), ); this.rchars = strChars(newString); - this.highlight(word); + this.highlight(word, syntax); } - public highlight(word: Option): void { + public highlight(word: Option, syntax: FileType): void { const highlighting = []; let searchIndex = 0; const matches = []; @@ -247,8 +248,10 @@ export class Row { } let prevIsSeparator = true; + let inString: string | boolean = false; let i = 0; for (; i < this.rsize;) { + const ch = this.rchars[i]; const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None; // 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 - const ch = this.rchars[i]; - const isNumeric = isAsciiDigit(ch) && - (prevIsSeparator || prevHighlight === HighlightType.Number); - const isDecimalNumeric = ch === '.' && - prevHighlight === HighlightType.Number; - const isHexNumeric = ch === 'x' && prevHighlight === HighlightType.Number; - if (isNumeric || isDecimalNumeric || isHexNumeric) { - highlighting.push(HighlightType.Number); - } else { - highlighting.push(HighlightType.None); + if (syntax.flags.numbers) { + const isNumeric = isAsciiDigit(ch) && (prevIsSeparator || + prevHighlight === HighlightType.Number); + const isDecimalNumeric = ch === '.' && + prevHighlight === HighlightType.Number; + const isHexNumeric = ch === 'x' && + prevHighlight === HighlightType.Number; + if (isNumeric || isDecimalNumeric || isHexNumeric) { + highlighting.push(HighlightType.Number); + } else { + highlighting.push(HighlightType.None); + } } prevIsSeparator = isSeparator(ch); diff --git a/tsconfig.json b/tsconfig.json index 587bec5..7bf8cd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,12 +10,14 @@ "noEmit": true, "strict": true, "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, "skipLibCheck": true, "composite": true, "downlevelIteration": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "isolatedModules": true + "isolatedModules": true, + "strictNullChecks": true }, "exclude": ["src/deno"] }