Add string highlighting
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-16 15:57:41 -04:00
parent 01b8535c5e
commit b5856f063a
8 changed files with 174 additions and 30 deletions

View File

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

View File

@ -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<Row> {
@ -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<string>): void {
this.#rows.forEach((row) => {
row.update(searchMatch);
row.update(searchMatch, this.type);
});
}

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './filetype.ts';

View File

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

View File

@ -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<string>): void {
public update(word: Option<string>, 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<string>): void {
public highlight(word: Option<string>, 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);

View File

@ -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"]
}