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 Buffer from './buffer.ts';
|
||||||
import Document from './document.ts';
|
import Document from './document.ts';
|
||||||
import Editor from './editor.ts';
|
import Editor from './editor.ts';
|
||||||
|
import { FileType } from './filetype/mod.ts';
|
||||||
import { highlightToColor, HighlightType } from './highlight.ts';
|
import { highlightToColor, HighlightType } from './highlight.ts';
|
||||||
import Option, { None, Some } from './option.ts';
|
import Option, { None, Some } from './option.ts';
|
||||||
import Position from './position.ts';
|
import Position from './position.ts';
|
||||||
@ -502,7 +503,7 @@ const RowTest = {
|
|||||||
},
|
},
|
||||||
'.append': () => {
|
'.append': () => {
|
||||||
const row = Row.from('foo');
|
const row = Row.from('foo');
|
||||||
row.append('bar');
|
row.append('bar', FileType.default());
|
||||||
assertEquals(row.toString(), 'foobar');
|
assertEquals(row.toString(), 'foobar');
|
||||||
},
|
},
|
||||||
'.delete': () => {
|
'.delete': () => {
|
||||||
@ -518,7 +519,7 @@ const RowTest = {
|
|||||||
// (Kind of like if the string were one-indexed)
|
// (Kind of like if the string were one-indexed)
|
||||||
const row = Row.from('foobar');
|
const row = Row.from('foobar');
|
||||||
const row2 = Row.from('bar');
|
const row2 = Row.from('bar');
|
||||||
assertEquals(row.split(3).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('For whom the bell tolls');
|
||||||
@ -567,7 +568,7 @@ const RowTest = {
|
|||||||
},
|
},
|
||||||
'.cxToRx, .rxToCx': () => {
|
'.cxToRx, .rxToCx': () => {
|
||||||
const row = Row.from('foo\tbar\tbaz');
|
const row = Row.from('foo\tbar\tbaz');
|
||||||
row.update(None);
|
row.update(None, FileType.default());
|
||||||
assertNotEquals(row.chars, row.rchars);
|
assertNotEquals(row.chars, row.rchars);
|
||||||
assertNotEquals(row.size, row.rsize);
|
assertNotEquals(row.size, row.rsize);
|
||||||
assertEquals(row.size, 11);
|
assertEquals(row.size, 11);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
|
import { FileType } from './filetype/mod.ts';
|
||||||
import { arrayInsert, maxAdd, minSub } from './fns.ts';
|
import { arrayInsert, maxAdd, minSub } 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, logDebug, logWarning } from './runtime/mod.ts';
|
||||||
import { Position, SearchDirection } from './types.ts';
|
import { Position, SearchDirection } from './types.ts';
|
||||||
|
|
||||||
export class Document {
|
export class Document {
|
||||||
|
/**
|
||||||
|
* Each line of the current document
|
||||||
|
*/
|
||||||
#rows: Row[];
|
#rows: Row[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -12,9 +16,19 @@ export class Document {
|
|||||||
*/
|
*/
|
||||||
public dirty: boolean;
|
public dirty: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The meta-data for the file type of the current document
|
||||||
|
*/
|
||||||
|
public type: FileType;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.#rows = [];
|
this.#rows = [];
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
this.type = FileType.default();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get fileType(): string {
|
||||||
|
return this.type.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get numRows(): number {
|
public get numRows(): number {
|
||||||
@ -40,6 +54,8 @@ export class Document {
|
|||||||
this.#rows = [];
|
this.#rows = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.type = FileType.from(filename);
|
||||||
|
|
||||||
const rawFile = await file.openFile(filename);
|
const rawFile = await file.openFile(filename);
|
||||||
rawFile.split(/\r?\n/)
|
rawFile.split(/\r?\n/)
|
||||||
.forEach((row) => this.insertRow(this.numRows, row));
|
.forEach((row) => this.insertRow(this.numRows, row));
|
||||||
@ -56,6 +72,7 @@ export class Document {
|
|||||||
const { file } = await getRuntime();
|
const { file } = await getRuntime();
|
||||||
|
|
||||||
await file.saveFile(filename, this.rowsToString());
|
await file.saveFile(filename, this.rowsToString());
|
||||||
|
this.type = FileType.from(filename);
|
||||||
|
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
@ -122,7 +139,7 @@ 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.#rows[at.y].update(None, this.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,9 +163,9 @@ export class Document {
|
|||||||
// Split the current row, and insert a new
|
// Split the current row, and insert a new
|
||||||
// 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);
|
const newRow = currentRow.split(at.x, this.type);
|
||||||
currentRow.update(None);
|
currentRow.update(None, this.type);
|
||||||
newRow.update(None);
|
newRow.update(None, this.type);
|
||||||
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
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
|
// At the end of a line, pressing delete will merge
|
||||||
// the next line into the current one
|
// the next line into the current one
|
||||||
const rowToAppend = this.#rows[at.y + 1].toString();
|
const rowToAppend = this.#rows[at.y + 1].toString();
|
||||||
row.append(rowToAppend);
|
row.append(rowToAppend, this.type);
|
||||||
this.deleteRow(at.y + 1);
|
this.deleteRow(at.y + 1);
|
||||||
} else {
|
} else {
|
||||||
row.delete(at.x);
|
row.delete(at.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.update(None);
|
row.update(None, this.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public row(i: number): Option<Row> {
|
public row(i: number): Option<Row> {
|
||||||
@ -207,14 +224,14 @@ export class Document {
|
|||||||
|
|
||||||
public insertRow(at: number = this.numRows, s: string = ''): void {
|
public insertRow(at: number = this.numRows, s: string = ''): void {
|
||||||
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
||||||
this.#rows[at].update(None);
|
this.#rows[at].update(None, this.type);
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public highlight(searchMatch: Option<string>): void {
|
public highlight(searchMatch: Option<string>): void {
|
||||||
this.#rows.forEach((row) => {
|
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 name = (this.filename !== '') ? this.filename : '[No Name]';
|
||||||
const modified = (this.document.dirty) ? '(modified)' : '';
|
const modified = (this.document.dirty) ? '(modified)' : '';
|
||||||
const status = `${truncate(name, 25)} - ${this.numRows} lines ${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);
|
let len = Math.min(status.length, this.screen.cols);
|
||||||
this.buffer.append(status, len);
|
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,
|
None,
|
||||||
Number,
|
Number,
|
||||||
Match,
|
Match,
|
||||||
|
String,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightToColor(type: HighlightType): string {
|
export function highlightToColor(type: HighlightType): string {
|
||||||
@ -14,6 +15,9 @@ export function highlightToColor(type: HighlightType): string {
|
|||||||
case HighlightType.Match:
|
case HighlightType.Match:
|
||||||
return Ansi.color256(21);
|
return Ansi.color256(21);
|
||||||
|
|
||||||
|
case HighlightType.String:
|
||||||
|
return Ansi.color256(201);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Ansi.ResetFormatting;
|
return Ansi.ResetFormatting;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
strChars,
|
strChars,
|
||||||
strlen,
|
strlen,
|
||||||
} from './fns.ts';
|
} from './fns.ts';
|
||||||
|
import { FileType } from './filetype/mod.ts';
|
||||||
import { highlightToColor, HighlightType } from './highlight.ts';
|
import { highlightToColor, HighlightType } from './highlight.ts';
|
||||||
import Option, { None, Some } from './option.ts';
|
import Option, { None, Some } from './option.ts';
|
||||||
import { SearchDirection } from './types.ts';
|
import { SearchDirection } from './types.ts';
|
||||||
@ -63,9 +64,9 @@ export class Row {
|
|||||||
return new Row(s);
|
return new Row(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
public append(s: string): void {
|
public append(s: string, syntax: FileType): void {
|
||||||
this.chars = this.chars.concat(strChars(s));
|
this.chars = this.chars.concat(strChars(s));
|
||||||
this.update(None);
|
this.update(None, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public insertChar(at: number, c: string): void {
|
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
|
* 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));
|
const newRow = new Row(this.chars.slice(at));
|
||||||
this.chars = this.chars.slice(0, at);
|
this.chars = this.chars.slice(0, at);
|
||||||
this.update(None);
|
this.update(None, syntax);
|
||||||
|
|
||||||
return newRow;
|
return newRow;
|
||||||
}
|
}
|
||||||
@ -213,17 +214,17 @@ export class Row {
|
|||||||
return this.chars.join('');
|
return this.chars.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(word: Option<string>): void {
|
public update(word: Option<string>, syntax: FileType): void {
|
||||||
const newString = this.chars.join('').replaceAll(
|
const newString = this.chars.join('').replaceAll(
|
||||||
'\t',
|
'\t',
|
||||||
' '.repeat(SCROLL_TAB_SIZE),
|
' '.repeat(SCROLL_TAB_SIZE),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.rchars = strChars(newString);
|
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 = [];
|
const highlighting = [];
|
||||||
let searchIndex = 0;
|
let searchIndex = 0;
|
||||||
const matches = [];
|
const matches = [];
|
||||||
@ -247,8 +248,10 @@ export class Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let prevIsSeparator = true;
|
let prevIsSeparator = true;
|
||||||
|
let inString: string | boolean = false;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (; i < this.rsize;) {
|
for (; i < this.rsize;) {
|
||||||
|
const ch = this.rchars[i];
|
||||||
const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None;
|
const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None;
|
||||||
|
|
||||||
// Highlight search matches
|
// 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
|
// Highlight numbers
|
||||||
const ch = this.rchars[i];
|
if (syntax.flags.numbers) {
|
||||||
const isNumeric = isAsciiDigit(ch) &&
|
const isNumeric = isAsciiDigit(ch) && (prevIsSeparator ||
|
||||||
(prevIsSeparator || prevHighlight === HighlightType.Number);
|
prevHighlight === HighlightType.Number);
|
||||||
const isDecimalNumeric = ch === '.' &&
|
const isDecimalNumeric = ch === '.' &&
|
||||||
prevHighlight === HighlightType.Number;
|
prevHighlight === HighlightType.Number;
|
||||||
const isHexNumeric = ch === 'x' && prevHighlight === HighlightType.Number;
|
const isHexNumeric = ch === 'x' &&
|
||||||
if (isNumeric || isDecimalNumeric || isHexNumeric) {
|
prevHighlight === HighlightType.Number;
|
||||||
highlighting.push(HighlightType.Number);
|
if (isNumeric || isDecimalNumeric || isHexNumeric) {
|
||||||
} else {
|
highlighting.push(HighlightType.Number);
|
||||||
highlighting.push(HighlightType.None);
|
} else {
|
||||||
|
highlighting.push(HighlightType.None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prevIsSeparator = isSeparator(ch);
|
prevIsSeparator = isSeparator(ch);
|
||||||
|
@ -10,12 +10,14 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"exclude": ["src/deno"]
|
"exclude": ["src/deno"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user