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 { 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': () => {

View File

@ -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<Row> {
@ -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<string>): 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

View File

@ -265,6 +265,7 @@ export default class Editor {
*/
public async find(): Promise<void> {
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 {

View File

@ -25,6 +25,7 @@ interface IFileType {
readonly multiLineCommentEnd: Option<string>;
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<string> = 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<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = 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,

View File

@ -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<T>(
/**
* Subtract two numbers, returning a zero if the result is negative
*
* @param l
* @param s
*/

View File

@ -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;
}

View File

@ -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<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(
i: number,
syntax: FileType,
@ -470,24 +502,24 @@ export class Row {
syntax: FileType,
ch: string,
): Option<number> {
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;
}
}