From 1a8d9f546971bdf5f6f45600d7820597dd6401a6 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 17 Jul 2024 16:23:06 -0400 Subject: [PATCH] Add operator highlighting, partially fix search --- src/common/all_test.ts | 59 ++++++++++++-------- src/common/document.ts | 70 +++++++++--------------- src/common/editor.ts | 20 +------ src/common/filetype/filetype.ts | 91 ++++++++++++++++++++++++------- src/common/fns.ts | 2 + src/common/highlight.ts | 8 ++- src/common/row.ts | 97 +++++++++++++++++++++++++-------- 7 files changed, 219 insertions(+), 128 deletions(-) diff --git a/src/common/all_test.ts b/src/common/all_test.ts index 19a1407..cb4376d 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -13,6 +13,8 @@ import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts'; import { getTestRunner } from './runtime/mod.ts'; import { SearchDirection } from './types.ts'; +import fs from 'node:fs'; + const { assertEquals, assertEquivalent, @@ -27,6 +29,7 @@ const { } = await getTestRunner(); const THIS_FILE = './src/common/all_test.ts'; +const KILO_FILE = './demo/kilo.c'; // ---------------------------------------------------------------------------- // Helper Function Tests @@ -304,35 +307,45 @@ const DocumentTest = { assertTrue(doc.dirty); await doc.save('test.file'); + + fs.rm('test.file', (err: any) => { + assertNone(Option.from(err)); + }); + assertFalse(doc.dirty); }, '.find': async () => { - const doc = await Document.default().open(THIS_FILE); + const doc = await Document.default().open(KILO_FILE); + // First search forward from the beginning of the file const query1 = doc.find( - 'dessert', + 'editor', Position.default(), SearchDirection.Forward, ); assertTrue(query1.isSome()); - // const pos1 = query1.unwrap(); - // - // const query2 = doc.find( - // 'dessert', - // Position.at(pos1.x, 400), - // SearchDirection.Backward, - // ); - // assertTrue(query2.isSome()); - // const pos2 = query2.unwrap(); + const pos1 = query1.unwrap(); + assertEquivalent(pos1, Position.at(5, 27)); - // assertEquivalent(pos2, pos1); - }, - '.insertRow': () => { - const doc = Document.default(); - doc.insertRow(undefined, 'foobar'); - assertEquals(doc.numRows, 1); - assertFalse(doc.isEmpty()); - assertInstanceOf(doc.row(0).unwrap(), Row); + // Now search backwards from line 400 + const query2 = doc.find( + 'realloc', + Position.at(44, 400), + SearchDirection.Backward, + ); + assertTrue(query2.isSome()); + const pos2 = query2.unwrap(); + assertEquivalent(pos2, Position.at(11, 330)); + + // And backwards again + const query3 = doc.find( + 'editor', + Position.from(pos2), + SearchDirection.Backward, + ); + assertTrue(query3.isSome()); + const pos3 = query3.unwrap(); + assertEquivalent(pos3, Position.at(5, 328)); }, '.insert': () => { const doc = Document.default(); @@ -522,15 +535,15 @@ const RowTest = { assertEquals(row.split(3, FileType.default()).toString(), row2.toString()); }, '.find': () => { - const normalRow = Row.from('For whom the bell tolls'); + const normalRow = Row.from('\tFor whom the bell tolls'); assertEquivalent( normalRow.find('who', 0, SearchDirection.Forward), - Some(4), + Some(8), ); assertEquals(normalRow.find('foo', 0, SearchDirection.Forward), None); - const emojiRow = Row.from('šŸ˜ŗšŸ˜øšŸ˜¹'); - assertEquivalent(emojiRow.find('šŸ˜¹', 0, SearchDirection.Forward), Some(2)); + const emojiRow = Row.from('\tšŸ˜ŗšŸ˜øšŸ˜¹'); + assertEquivalent(emojiRow.find('šŸ˜¹', 0, SearchDirection.Forward), Some(6)); assertEquals(emojiRow.find('šŸ¤°šŸ¼', 10, SearchDirection.Forward), None); }, '.find backwards': () => { diff --git a/src/common/document.ts b/src/common/document.ts index 057cd2e..77cf7af 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -1,8 +1,8 @@ import Row from './row.ts'; import { FileType } from './filetype/mod.ts'; -import { arrayInsert, maxAdd, minSub } from './fns.ts'; +import { arrayInsert } from './fns.ts'; import Option, { None, Some } from './option.ts'; -import { getRuntime, logDebug, logWarning } from './runtime/mod.ts'; +import { getRuntime, logWarning } from './runtime/mod.ts'; import { Position, SearchDirection } from './types.ts'; export class Document { @@ -55,13 +55,12 @@ export class Document { } this.type = FileType.from(filename); - let startWithComment = false; const rawFile = await file.openFile(filename); rawFile.split(/\r?\n/) - .forEach((row) => - startWithComment = this.insertRow(this.numRows, row, startWithComment) - ); + .forEach((row) => this.insertRow(this.numRows, row)); + + this.highlight(None); this.dirty = false; @@ -75,13 +74,10 @@ export class Document { const { file } = await getRuntime(); await file.saveFile(filename, this.rowsToString()); - let startWithComment = false; this.type = FileType.from(filename); // Re-highlight the file - this.#rows.forEach((row) => { - startWithComment = row.update(None, this.type, startWithComment); - }); + this.highlight(None); this.dirty = false; } @@ -101,12 +97,7 @@ export class Document { const position = Position.from(at); - const start = (direction === SearchDirection.Forward) ? at.y : 0; - const end = (direction === SearchDirection.Forward) - ? this.numRows - : maxAdd(at.y, 1, this.numRows); - - for (let y = start; y < end; y++) { + for (let y = at.y; y >= 0 && y < this.numRows; y += direction) { if (this.row(position.y).isNone()) { logWarning('Invalid Search location', { position, @@ -123,14 +114,13 @@ export class Document { } if (direction === SearchDirection.Forward) { - position.y = maxAdd(position.y, 1, this.numRows - 1); + position.y += 1; position.x = 0; - } else { - position.y = minSub(position.y, 1, 0); + } else if (direction === SearchDirection.Backward) { + position.y -= 1; + position.x = this.#rows[position.y].size; console.assert(position.y < this.numRows); - - position.x = this.#rows[position.y].size - 1; } } @@ -148,8 +138,10 @@ export class Document { this.insertRow(this.numRows, c); } else { this.#rows[at.y].insertChar(at.x, c); - this.#rows[at.y].update(None, this.type, false); } + + // Re-highlight the file + this.highlight(None); } /** @@ -173,9 +165,10 @@ export class Document { // row with the leftovers const currentRow = this.#rows[at.y]; const newRow = currentRow.split(at.x, this.type); - currentRow.update(None, this.type, false); - newRow.update(None, this.type, false); this.#rows = arrayInsert(this.#rows, at.y + 1, newRow); + + // Re-highlight the file + this.highlight(None); } /** @@ -199,12 +192,6 @@ export class Document { const mergeNextRow = at.x === row.size && this.row(at.y + 1).isSome(); - logDebug('Document.delete', { - method: 'Document.delete', - at, - mergeNextRow, - }); - // If we are at the end of a line, and press delete, // add the contents of the next row, and delete // the merged row object (This also works for pressing @@ -220,7 +207,8 @@ export class Document { row.delete(at.x); } - row.update(None, this.type, false); + // Re-highlight the file + this.highlight(None); } public row(i: number): Option { @@ -231,18 +219,6 @@ export class Document { return Option.from(this.#rows.at(i)); } - public insertRow( - at: number = this.numRows, - s: string = '', - startWithComment: boolean = false, - ): boolean { - this.#rows = arrayInsert(this.#rows, at, Row.from(s)); - - this.dirty = true; - - return this.#rows[at].update(None, this.type, startWithComment); - } - public highlight(searchMatch: Option): void { let startWithComment = false; this.#rows.forEach((row) => { @@ -250,6 +226,14 @@ export class Document { }); } + protected insertRow( + at: number = this.numRows, + s: string = '', + ): void { + this.#rows = arrayInsert(this.#rows, at, Row.from(s)); + this.dirty = true; + } + /** * Delete the specified row * @param at - the index of the row to delete diff --git a/src/common/editor.ts b/src/common/editor.ts index d302cca..8544e6c 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -265,6 +265,7 @@ export default class Editor { */ public async find(): Promise { const savedCursor = Position.from(this.cursor); + const savedOffset = Position.from(this.offset); let direction = SearchDirection.Forward; const result = await this.prompt( @@ -308,7 +309,7 @@ export default class Editor { // when you cancel the search (press the escape key) if (result.isNone()) { this.cursor = Position.from(savedCursor); - // this.offset = Position.from(savedOffset); + this.offset = Position.from(savedOffset); this.scroll(); } @@ -345,15 +346,6 @@ export default class Editor { const height = this.numRows; let width = (this.row(y).isSome()) ? this.row(y).unwrap().size : 0; - logDebug('Editor.moveCursor - start', { - char, - cursor: this.cursor, - renderX: this.renderX, - screen: this.screen, - height, - width, - }); - switch (char) { case KeyCommand.ArrowUp: if (y > 0) { @@ -404,14 +396,6 @@ export default class Editor { } this.cursor = Position.at(x, y); - - logDebug('Editor.moveCursor - end', { - cursor: this.cursor, - renderX: this.renderX, - screen: this.screen, - height, - width, - }); } protected scroll(): void { diff --git a/src/common/filetype/filetype.ts b/src/common/filetype/filetype.ts index 20fa6c6..761d19c 100644 --- a/src/common/filetype/filetype.ts +++ b/src/common/filetype/filetype.ts @@ -25,6 +25,7 @@ interface IFileType { readonly multiLineCommentEnd: Option; readonly keywords1: string[]; readonly keywords2: string[]; + readonly operators: string[]; readonly hlOptions: HighlightingOptions; get flags(): HighlightingOptions; get primaryKeywords(): string[]; @@ -42,6 +43,7 @@ export abstract class AbstractFileType implements IFileType { public readonly multiLineCommentEnd: Option = None; public readonly keywords1: string[] = []; public readonly keywords2: string[] = []; + public readonly operators: string[] = []; public readonly hlOptions: HighlightingOptions = { numbers: false, strings: false, @@ -79,6 +81,8 @@ class JavaScriptFile extends AbstractFileType { public readonly multiLineCommentStart: Option = Some('/*'); public readonly multiLineCommentEnd: Option = Some('*/'); public readonly keywords1 = [ + '=>', + 'await', 'break', 'case', 'catch', @@ -100,9 +104,11 @@ class JavaScriptFile extends AbstractFileType { 'import', 'in', 'instanceof', + 'let', 'new', 'null', 'return', + 'static', 'super', 'switch', 'this', @@ -114,27 +120,72 @@ class JavaScriptFile extends AbstractFileType { 'void', 'while', 'with', - 'let', - 'static', 'yield', - 'await', ]; public readonly keywords2 = [ 'arguments', 'as', 'async', + 'BigInt', + 'Boolean', 'eval', 'from', 'get', + 'JSON', + 'Math', + 'Number', + 'Object', 'of', 'set', - '=>', - 'Number', 'String', - 'Object', - 'Math', - 'JSON', - 'Boolean', + 'Symbol', + 'undefined', + ]; + public readonly operators = [ + '>>>=', + '**=', + '<<=', + '>>=', + '&&=', + '||=', + '??=', + '===', + '!==', + '>>>', + '+=', + '-=', + '*=', + '/=', + '%=', + '&=', + '^=', + '|=', + '==', + '!=', + '>=', + '<=', + '++', + '--', + '**', + '<<', + '>>', + '&&', + '||', + '??', + '?.', + '?', + ':', + '=', + '>', + '<', + '%', + '-', + '+', + '&', + '|', + '^', + '~', + '!', ]; public readonly hlOptions: HighlightingOptions = { ...defaultHighlightOptions, @@ -148,18 +199,19 @@ class TypeScriptFile extends JavaScriptFile { public readonly keywords2 = [ ...super.secondaryKeywords, // Typescript-specific - 'keyof', - 'interface', - 'enum', - 'public', - 'protected', - 'private', - 'string', - 'number', - 'boolean', 'any', - 'unknown', + 'bigint', + 'boolean', + 'enum', + 'interface', + 'keyof', + 'number', + 'private', + 'protected', + 'public', + 'string', 'type', + 'unknown', ]; } @@ -186,6 +238,7 @@ class ShellFile extends AbstractFileType { 'declare', ]; public readonly keywords2 = ['set']; + public readonly operators = ['[[', ']]']; public readonly hlOptions: HighlightingOptions = { ...defaultHighlightOptions, numbers: false, diff --git a/src/common/fns.ts b/src/common/fns.ts index 8e651c4..fb4add6 100644 --- a/src/common/fns.ts +++ b/src/common/fns.ts @@ -63,6 +63,7 @@ export function readKey(raw: Uint8Array): string { /** * Insert a value into an array at the specified index + * * @param arr - the array * @param at - the index to insert at * @param value - what to add into the array @@ -87,6 +88,7 @@ export function arrayInsert( /** * Subtract two numbers, returning a zero if the result is negative + * * @param l * @param s */ diff --git a/src/common/highlight.ts b/src/common/highlight.ts index 3d3abe0..7c154ac 100644 --- a/src/common/highlight.ts +++ b/src/common/highlight.ts @@ -9,6 +9,7 @@ export enum HighlightType { MultiLineComment, Keyword1, Keyword2, + Operator, } export function highlightToColor(type: HighlightType): string { @@ -23,15 +24,20 @@ export function highlightToColor(type: HighlightType): string { return Ansi.color256(45); case HighlightType.SingleLineComment: - case HighlightType.MultiLineComment: return Ansi.color256(201); + case HighlightType.MultiLineComment: + return Ansi.color256(240); + case HighlightType.Keyword1: return Ansi.color256(226); case HighlightType.Keyword2: return Ansi.color256(118); + case HighlightType.Operator: + return Ansi.color256(215); + default: return Ansi.ResetFormatting; } diff --git a/src/common/row.ts b/src/common/row.ts index 2d5c553..e56293a 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -277,7 +277,8 @@ export class Row { .orElse(() => this.highlightPrimaryKeywords(i, syntax)) .orElse(() => this.highlightSecondaryKeywords(i, syntax)) .orElse(() => this.highlightString(i, syntax, ch)) - .orElse(() => this.highlightNumber(i, syntax, ch)); + .orElse(() => this.highlightNumber(i, syntax, ch)) + .orElse(() => this.highlightOperators(i, syntax)); if (maybeNext.isSome()) { const next = maybeNext.unwrap(); @@ -313,9 +314,10 @@ export class Row { // Find matches for the current search if (word.isSome()) { + const query = word.unwrap(); while (true) { const match = this.find( - word.unwrap(), + query, searchIndex, SearchDirection.Forward, ); @@ -324,7 +326,8 @@ export class Row { } const index = match.unwrap(); - const nextPossible = index + strlen(word.unwrap()); + const matchSize = strlen(query); + const nextPossible = index + matchSize; if (nextPossible < this.rsize) { let i = index; for (const _ in strChars(word.unwrap())) { @@ -438,6 +441,35 @@ export class Row { ); } + protected highlightOperators( + i: number, + syntax: FileType, + ): Option { + // Search the list of operators + outer: for (const op of syntax.operators) { + const chars = strChars(op); + + // See if this operator (chars[j]) exists at this index + for (const [j, ch] of chars.entries()) { + // Make sure the next character of this operator matches too + const nextChar = this.rchars[i + j]; + if (nextChar !== ch) { + continue outer; + } + } + + // This operator matches, highlight it + for (const _ of chars) { + this.hl.push(HighlightType.Operator); + i += 1; + } + + return Some(i); + } + + return None; + } + protected highlightString( i: number, syntax: FileType, @@ -470,24 +502,24 @@ export class Row { syntax: FileType, ch: string, ): Option { - if (syntax.hasMultilineComments()) { - const startChars = syntax.multiLineCommentStart.unwrap(); - const endChars = syntax.multiLineCommentEnd.unwrap(); - if (ch === startChars[0] && this.rchars[i + 1] == startChars[1]) { - const maybeEnd = this.rIndexOf(endChars, i); - const end = (maybeEnd.isSome()) - ? maybeEnd.unwrap() + strlen(endChars) + 1 - : this.rsize; - - for (; i < end; i++) { - this.hl.push(HighlightType.MultiLineComment); - } - return Some(i); - } - + if (!syntax.hasMultilineComments()) { return None; } + const startChars = syntax.multiLineCommentStart.unwrap(); + const endChars = syntax.multiLineCommentEnd.unwrap(); + if (ch === startChars[0] && this.rchars[i + 1] == startChars[1]) { + const maybeEnd = this.rIndexOf(endChars, i); + const end = (maybeEnd.isSome()) + ? maybeEnd.unwrap() + strlen(endChars) + 2 + : this.rsize; + + for (; i <= end; i++) { + this.hl.push(HighlightType.MultiLineComment); + } + return Some(i); + } + return None; } @@ -505,12 +537,29 @@ export class Row { while (true) { this.hl.push(HighlightType.Number); i += 1; - if (i < this.rsize) { - const nextChar = this.rchars[i]; - if (nextChar !== '.' && nextChar !== 'x' && !isAsciiDigit(nextChar)) { - break; - } - } else { + if (i >= this.rsize) { + break; + } + + const nextChar = this.rchars[i]; + // deno-fmt-ignore + const validChars = [ + // Decimal + '.', + // Octal Notation + 'o','O', + // Hex Notation + 'x','X', + // Hex digits + 'a','A','c','C','d','D','e','E','f','F', + // Binary Notation/Hex digit + 'b','B', + // BigInt + 'n', + ]; + if ( + !(validChars.includes(nextChar) || isAsciiDigit(nextChar)) + ) { break; } }