Add string highlighting
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good
This commit is contained in:
parent
01b8535c5e
commit
b5856f063a
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
93
src/common/filetype/filetype.ts
Normal file
93
src/common/filetype/filetype.ts
Normal 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();
|
||||
}
|
||||
}
|
1
src/common/filetype/mod.ts
Normal file
1
src/common/filetype/mod.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './filetype.ts';
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user