Add operator highlighting, partially fix search
Some checks failed
timw4mail/scroll/pipeline/head There was a failure building this commit
Some checks failed
timw4mail/scroll/pipeline/head There was a failure building this commit
This commit is contained in:
parent
65ff7e5b79
commit
1a8d9f5469
@ -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': () => {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,16 +502,19 @@ export class Row {
|
||||
syntax: FileType,
|
||||
ch: string,
|
||||
): Option<number> {
|
||||
if (syntax.hasMultilineComments()) {
|
||||
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) + 1
|
||||
? maybeEnd.unwrap() + strlen(endChars) + 2
|
||||
: this.rsize;
|
||||
|
||||
for (; i < end; i++) {
|
||||
for (; i <= end; i++) {
|
||||
this.hl.push(HighlightType.MultiLineComment);
|
||||
}
|
||||
return Some(i);
|
||||
@ -488,9 +523,6 @@ export class Row {
|
||||
return None;
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
protected highlightNumber(
|
||||
i: number,
|
||||
syntax: FileType,
|
||||
@ -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)) {
|
||||
if (i >= this.rsize) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user