Add operator highlighting, partially fix search
Some checks failed
timw4mail/scroll/pipeline/head There was a failure building this commit

This commit is contained in:
Timothy Warren 2024-07-17 16:23:06 -04:00
parent 65ff7e5b79
commit 1a8d9f5469
7 changed files with 219 additions and 128 deletions

View File

@ -13,6 +13,8 @@ import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts';
import { getTestRunner } from './runtime/mod.ts'; import { getTestRunner } from './runtime/mod.ts';
import { SearchDirection } from './types.ts'; import { SearchDirection } from './types.ts';
import fs from 'node:fs';
const { const {
assertEquals, assertEquals,
assertEquivalent, assertEquivalent,
@ -27,6 +29,7 @@ const {
} = await getTestRunner(); } = await getTestRunner();
const THIS_FILE = './src/common/all_test.ts'; const THIS_FILE = './src/common/all_test.ts';
const KILO_FILE = './demo/kilo.c';
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helper Function Tests // Helper Function Tests
@ -304,35 +307,45 @@ const DocumentTest = {
assertTrue(doc.dirty); assertTrue(doc.dirty);
await doc.save('test.file'); await doc.save('test.file');
fs.rm('test.file', (err: any) => {
assertNone(Option.from(err));
});
assertFalse(doc.dirty); assertFalse(doc.dirty);
}, },
'.find': async () => { '.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( const query1 = doc.find(
'dessert', 'editor',
Position.default(), Position.default(),
SearchDirection.Forward, SearchDirection.Forward,
); );
assertTrue(query1.isSome()); assertTrue(query1.isSome());
// const pos1 = query1.unwrap(); const pos1 = query1.unwrap();
// assertEquivalent(pos1, Position.at(5, 27));
// const query2 = doc.find(
// 'dessert',
// Position.at(pos1.x, 400),
// SearchDirection.Backward,
// );
// assertTrue(query2.isSome());
// const pos2 = query2.unwrap();
// assertEquivalent(pos2, pos1); // Now search backwards from line 400
}, const query2 = doc.find(
'.insertRow': () => { 'realloc',
const doc = Document.default(); Position.at(44, 400),
doc.insertRow(undefined, 'foobar'); SearchDirection.Backward,
assertEquals(doc.numRows, 1); );
assertFalse(doc.isEmpty()); assertTrue(query2.isSome());
assertInstanceOf(doc.row(0).unwrap(), Row); 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': () => { '.insert': () => {
const doc = Document.default(); const doc = Document.default();
@ -522,15 +535,15 @@ const RowTest = {
assertEquals(row.split(3, FileType.default()).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('\tFor whom the bell tolls');
assertEquivalent( assertEquivalent(
normalRow.find('who', 0, SearchDirection.Forward), normalRow.find('who', 0, SearchDirection.Forward),
Some(4), Some(8),
); );
assertEquals(normalRow.find('foo', 0, SearchDirection.Forward), None); assertEquals(normalRow.find('foo', 0, SearchDirection.Forward), None);
const emojiRow = Row.from('😺😸😹'); const emojiRow = Row.from('\t😺😸😹');
assertEquivalent(emojiRow.find('😹', 0, SearchDirection.Forward), Some(2)); assertEquivalent(emojiRow.find('😹', 0, SearchDirection.Forward), Some(6));
assertEquals(emojiRow.find('🤰🏼', 10, SearchDirection.Forward), None); assertEquals(emojiRow.find('🤰🏼', 10, SearchDirection.Forward), None);
}, },
'.find backwards': () => { '.find backwards': () => {

View File

@ -1,8 +1,8 @@
import Row from './row.ts'; import Row from './row.ts';
import { FileType } from './filetype/mod.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 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'; import { Position, SearchDirection } from './types.ts';
export class Document { export class Document {
@ -55,13 +55,12 @@ export class Document {
} }
this.type = FileType.from(filename); this.type = FileType.from(filename);
let startWithComment = false;
const rawFile = await file.openFile(filename); const rawFile = await file.openFile(filename);
rawFile.split(/\r?\n/) rawFile.split(/\r?\n/)
.forEach((row) => .forEach((row) => this.insertRow(this.numRows, row));
startWithComment = this.insertRow(this.numRows, row, startWithComment)
); this.highlight(None);
this.dirty = false; this.dirty = false;
@ -75,13 +74,10 @@ export class Document {
const { file } = await getRuntime(); const { file } = await getRuntime();
await file.saveFile(filename, this.rowsToString()); await file.saveFile(filename, this.rowsToString());
let startWithComment = false;
this.type = FileType.from(filename); this.type = FileType.from(filename);
// Re-highlight the file // Re-highlight the file
this.#rows.forEach((row) => { this.highlight(None);
startWithComment = row.update(None, this.type, startWithComment);
});
this.dirty = false; this.dirty = false;
} }
@ -101,12 +97,7 @@ export class Document {
const position = Position.from(at); const position = Position.from(at);
const start = (direction === SearchDirection.Forward) ? at.y : 0; for (let y = at.y; y >= 0 && y < this.numRows; y += direction) {
const end = (direction === SearchDirection.Forward)
? this.numRows
: maxAdd(at.y, 1, this.numRows);
for (let y = start; y < end; y++) {
if (this.row(position.y).isNone()) { if (this.row(position.y).isNone()) {
logWarning('Invalid Search location', { logWarning('Invalid Search location', {
position, position,
@ -123,14 +114,13 @@ export class Document {
} }
if (direction === SearchDirection.Forward) { if (direction === SearchDirection.Forward) {
position.y = maxAdd(position.y, 1, this.numRows - 1); position.y += 1;
position.x = 0; position.x = 0;
} else { } else if (direction === SearchDirection.Backward) {
position.y = minSub(position.y, 1, 0); position.y -= 1;
position.x = this.#rows[position.y].size;
console.assert(position.y < this.numRows); 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); 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.type, false);
} }
// Re-highlight the file
this.highlight(None);
} }
/** /**
@ -173,9 +165,10 @@ export class Document {
// 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, this.type); 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); 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(); 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, // If we are at the end of a line, and press delete,
// add the contents of the next row, and delete // add the contents of the next row, and delete
// the merged row object (This also works for pressing // the merged row object (This also works for pressing
@ -220,7 +207,8 @@ export class Document {
row.delete(at.x); row.delete(at.x);
} }
row.update(None, this.type, false); // Re-highlight the file
this.highlight(None);
} }
public row(i: number): Option<Row> { public row(i: number): Option<Row> {
@ -231,18 +219,6 @@ export class Document {
return Option.from(this.#rows.at(i)); 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<string>): void { public highlight(searchMatch: Option<string>): void {
let startWithComment = false; let startWithComment = false;
this.#rows.forEach((row) => { 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 * Delete the specified row
* @param at - the index of the row to delete * @param at - the index of the row to delete

View File

@ -265,6 +265,7 @@ export default class Editor {
*/ */
public async find(): Promise<void> { public async find(): Promise<void> {
const savedCursor = Position.from(this.cursor); const savedCursor = Position.from(this.cursor);
const savedOffset = Position.from(this.offset);
let direction = SearchDirection.Forward; let direction = SearchDirection.Forward;
const result = await this.prompt( const result = await this.prompt(
@ -308,7 +309,7 @@ export default class Editor {
// when you cancel the search (press the escape key) // when you cancel the search (press the escape key)
if (result.isNone()) { if (result.isNone()) {
this.cursor = Position.from(savedCursor); this.cursor = Position.from(savedCursor);
// this.offset = Position.from(savedOffset); this.offset = Position.from(savedOffset);
this.scroll(); this.scroll();
} }
@ -345,15 +346,6 @@ export default class Editor {
const height = this.numRows; const height = this.numRows;
let width = (this.row(y).isSome()) ? this.row(y).unwrap().size : 0; 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) { switch (char) {
case KeyCommand.ArrowUp: case KeyCommand.ArrowUp:
if (y > 0) { if (y > 0) {
@ -404,14 +396,6 @@ export default class Editor {
} }
this.cursor = Position.at(x, y); this.cursor = Position.at(x, y);
logDebug('Editor.moveCursor - end', {
cursor: this.cursor,
renderX: this.renderX,
screen: this.screen,
height,
width,
});
} }
protected scroll(): void { protected scroll(): void {

View File

@ -25,6 +25,7 @@ interface IFileType {
readonly multiLineCommentEnd: Option<string>; readonly multiLineCommentEnd: Option<string>;
readonly keywords1: string[]; readonly keywords1: string[];
readonly keywords2: string[]; readonly keywords2: string[];
readonly operators: string[];
readonly hlOptions: HighlightingOptions; readonly hlOptions: HighlightingOptions;
get flags(): HighlightingOptions; get flags(): HighlightingOptions;
get primaryKeywords(): string[]; get primaryKeywords(): string[];
@ -42,6 +43,7 @@ export abstract class AbstractFileType implements IFileType {
public readonly multiLineCommentEnd: Option<string> = None; public readonly multiLineCommentEnd: Option<string> = None;
public readonly keywords1: string[] = []; public readonly keywords1: string[] = [];
public readonly keywords2: string[] = []; public readonly keywords2: string[] = [];
public readonly operators: string[] = [];
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
numbers: false, numbers: false,
strings: false, strings: false,
@ -79,6 +81,8 @@ class JavaScriptFile extends AbstractFileType {
public readonly multiLineCommentStart: Option<string> = Some('/*'); public readonly multiLineCommentStart: Option<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = Some('*/'); public readonly multiLineCommentEnd: Option<string> = Some('*/');
public readonly keywords1 = [ public readonly keywords1 = [
'=>',
'await',
'break', 'break',
'case', 'case',
'catch', 'catch',
@ -100,9 +104,11 @@ class JavaScriptFile extends AbstractFileType {
'import', 'import',
'in', 'in',
'instanceof', 'instanceof',
'let',
'new', 'new',
'null', 'null',
'return', 'return',
'static',
'super', 'super',
'switch', 'switch',
'this', 'this',
@ -114,27 +120,72 @@ class JavaScriptFile extends AbstractFileType {
'void', 'void',
'while', 'while',
'with', 'with',
'let',
'static',
'yield', 'yield',
'await',
]; ];
public readonly keywords2 = [ public readonly keywords2 = [
'arguments', 'arguments',
'as', 'as',
'async', 'async',
'BigInt',
'Boolean',
'eval', 'eval',
'from', 'from',
'get', 'get',
'JSON',
'Math',
'Number',
'Object',
'of', 'of',
'set', 'set',
'=>',
'Number',
'String', 'String',
'Object', 'Symbol',
'Math', 'undefined',
'JSON', ];
'Boolean', public readonly operators = [
'>>>=',
'**=',
'<<=',
'>>=',
'&&=',
'||=',
'??=',
'===',
'!==',
'>>>',
'+=',
'-=',
'*=',
'/=',
'%=',
'&=',
'^=',
'|=',
'==',
'!=',
'>=',
'<=',
'++',
'--',
'**',
'<<',
'>>',
'&&',
'||',
'??',
'?.',
'?',
':',
'=',
'>',
'<',
'%',
'-',
'+',
'&',
'|',
'^',
'~',
'!',
]; ];
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions, ...defaultHighlightOptions,
@ -148,18 +199,19 @@ class TypeScriptFile extends JavaScriptFile {
public readonly keywords2 = [ public readonly keywords2 = [
...super.secondaryKeywords, ...super.secondaryKeywords,
// Typescript-specific // Typescript-specific
'keyof',
'interface',
'enum',
'public',
'protected',
'private',
'string',
'number',
'boolean',
'any', 'any',
'unknown', 'bigint',
'boolean',
'enum',
'interface',
'keyof',
'number',
'private',
'protected',
'public',
'string',
'type', 'type',
'unknown',
]; ];
} }
@ -186,6 +238,7 @@ class ShellFile extends AbstractFileType {
'declare', 'declare',
]; ];
public readonly keywords2 = ['set']; public readonly keywords2 = ['set'];
public readonly operators = ['[[', ']]'];
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions, ...defaultHighlightOptions,
numbers: false, numbers: false,

View File

@ -63,6 +63,7 @@ export function readKey(raw: Uint8Array): string {
/** /**
* Insert a value into an array at the specified index * Insert a value into an array at the specified index
*
* @param arr - the array * @param arr - the array
* @param at - the index to insert at * @param at - the index to insert at
* @param value - what to add into the array * @param value - what to add into the array
@ -87,6 +88,7 @@ export function arrayInsert<T>(
/** /**
* Subtract two numbers, returning a zero if the result is negative * Subtract two numbers, returning a zero if the result is negative
*
* @param l * @param l
* @param s * @param s
*/ */

View File

@ -9,6 +9,7 @@ export enum HighlightType {
MultiLineComment, MultiLineComment,
Keyword1, Keyword1,
Keyword2, Keyword2,
Operator,
} }
export function highlightToColor(type: HighlightType): string { export function highlightToColor(type: HighlightType): string {
@ -23,15 +24,20 @@ export function highlightToColor(type: HighlightType): string {
return Ansi.color256(45); return Ansi.color256(45);
case HighlightType.SingleLineComment: case HighlightType.SingleLineComment:
case HighlightType.MultiLineComment:
return Ansi.color256(201); return Ansi.color256(201);
case HighlightType.MultiLineComment:
return Ansi.color256(240);
case HighlightType.Keyword1: case HighlightType.Keyword1:
return Ansi.color256(226); return Ansi.color256(226);
case HighlightType.Keyword2: case HighlightType.Keyword2:
return Ansi.color256(118); return Ansi.color256(118);
case HighlightType.Operator:
return Ansi.color256(215);
default: default:
return Ansi.ResetFormatting; return Ansi.ResetFormatting;
} }

View File

@ -277,7 +277,8 @@ export class Row {
.orElse(() => this.highlightPrimaryKeywords(i, syntax)) .orElse(() => this.highlightPrimaryKeywords(i, syntax))
.orElse(() => this.highlightSecondaryKeywords(i, syntax)) .orElse(() => this.highlightSecondaryKeywords(i, syntax))
.orElse(() => this.highlightString(i, syntax, ch)) .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()) { if (maybeNext.isSome()) {
const next = maybeNext.unwrap(); const next = maybeNext.unwrap();
@ -313,9 +314,10 @@ export class Row {
// Find matches for the current search // Find matches for the current search
if (word.isSome()) { if (word.isSome()) {
const query = word.unwrap();
while (true) { while (true) {
const match = this.find( const match = this.find(
word.unwrap(), query,
searchIndex, searchIndex,
SearchDirection.Forward, SearchDirection.Forward,
); );
@ -324,7 +326,8 @@ export class Row {
} }
const index = match.unwrap(); const index = match.unwrap();
const nextPossible = index + strlen(word.unwrap()); const matchSize = strlen(query);
const nextPossible = index + matchSize;
if (nextPossible < this.rsize) { if (nextPossible < this.rsize) {
let i = index; let i = index;
for (const _ in strChars(word.unwrap())) { for (const _ in strChars(word.unwrap())) {
@ -438,6 +441,35 @@ export class Row {
); );
} }
protected highlightOperators(
i: number,
syntax: FileType,
): Option<number> {
// 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( protected highlightString(
i: number, i: number,
syntax: FileType, syntax: FileType,
@ -470,24 +502,24 @@ export class Row {
syntax: FileType, syntax: FileType,
ch: string, ch: string,
): Option<number> { ): Option<number> {
if (syntax.hasMultilineComments()) { 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);
}
return None; 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; return None;
} }
@ -505,12 +537,29 @@ export class Row {
while (true) { while (true) {
this.hl.push(HighlightType.Number); this.hl.push(HighlightType.Number);
i += 1; i += 1;
if (i < this.rsize) { if (i >= this.rsize) {
const nextChar = this.rchars[i]; break;
if (nextChar !== '.' && nextChar !== 'x' && !isAsciiDigit(nextChar)) { }
break;
} const nextChar = this.rchars[i];
} else { // 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; break;
} }
} }