From 21d26ede6c6a42cff01888f1583145dcf870b37f Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 19 Jul 2024 15:31:27 -0400 Subject: [PATCH] A lot of tweaks --- demo/colors.ts | 1 + src/common/all_test.ts | 47 +++- src/common/ansi.ts | 6 +- src/common/buffer.ts | 6 +- src/common/document.ts | 9 - src/common/editor.ts | 88 +++---- src/common/filetype/base.ts | 87 +++++++ src/common/filetype/css.ts | 393 ++++++++++++++++++++++++++++++ src/common/filetype/filetype.ts | 259 +------------------- src/common/filetype/javascript.ts | 149 +++++++++++ src/common/filetype/mod.ts | 1 + src/common/filetype/shell.ts | 37 +++ src/common/main.ts | 4 +- src/common/row.ts | 154 ++++++++---- 14 files changed, 856 insertions(+), 385 deletions(-) create mode 100644 src/common/filetype/base.ts create mode 100644 src/common/filetype/css.ts create mode 100644 src/common/filetype/javascript.ts create mode 100644 src/common/filetype/shell.ts diff --git a/demo/colors.ts b/demo/colors.ts index 42bc0ab..55249c1 100644 --- a/demo/colors.ts +++ b/demo/colors.ts @@ -63,6 +63,7 @@ function print16colorTable(): void { console.log(colorTable); } + function print256colorTable(): void { let colorTable = ''; // deno-fmt-ignore diff --git a/src/common/all_test.ts b/src/common/all_test.ts index b7fc6e7..3097e62 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -2,7 +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 { FileLang, FileType } from './filetype/mod.ts'; import { highlightToColor, HighlightType } from './highlight.ts'; import Option, { None, Some } from './option.ts'; import Position from './position.ts'; @@ -202,9 +202,19 @@ const readKeyTest = () => { const highlightToColorTest = { 'highlightToColor()': () => { - assertTrue(highlightToColor(HighlightType.Number).length > 0); - assertTrue(highlightToColor(HighlightType.Match).length > 0); - assertTrue(highlightToColor(HighlightType.None).length > 0); + [ + HighlightType.Number, + HighlightType.Match, + HighlightType.String, + HighlightType.SingleLineComment, + HighlightType.MultiLineComment, + HighlightType.Keyword1, + HighlightType.Keyword2, + HighlightType.Operator, + HighlightType.None, + ].forEach((type) => { + assertTrue(highlightToColor(type).length > 0); + }); }, }; @@ -243,12 +253,12 @@ const ANSITest = () => { const BufferTest = { 'new Buffer': () => { - const b = new Buffer(); + const b = Buffer.default(); assertInstanceOf(b, Buffer); assertEquals(b.strlen(), 0); }, '.appendLine': () => { - const b = new Buffer(); + const b = Buffer.default(); // Carriage return and line feed b.appendLine(); @@ -261,7 +271,7 @@ const BufferTest = { assertEquals(b.strlen(), 5); }, '.append': () => { - const b = new Buffer(); + const b = Buffer.default(); b.append('foobar'); assertEquals(b.strlen(), 6); @@ -271,7 +281,7 @@ const BufferTest = { assertEquals(b.strlen(), 3); }, '.flush': async () => { - const b = new Buffer(); + const b = Buffer.default(); b.appendLine('foobarbaz' + Ansi.ClearLine); assertEquals(b.strlen(), 14); @@ -297,6 +307,7 @@ const DocumentTest = { assertEquals(oldDoc.numRows, 1); const doc = await oldDoc.open(THIS_FILE); + assertEquals(FileLang.TypeScript, doc.fileType); assertFalse(doc.dirty); assertFalse(doc.isEmpty()); assertTrue(doc.numRows > 1); @@ -347,6 +358,16 @@ const DocumentTest = { const pos3 = query3.unwrap(); assertEquivalent(pos3, Position.at(5, 328)); }, + '.find - empty result': () => { + const doc = Document.default(); + doc.insertNewline(Position.default()); + + const query = doc.find('foo', Position.default(), SearchDirection.Forward); + assertNone(query); + + const query2 = doc.find('bar', Position.at(0, 5), SearchDirection.Forward); + assertNone(query2); + }, '.insert': () => { const doc = Document.default(); assertFalse(doc.dirty); @@ -420,22 +441,22 @@ const DocumentTest = { const EditorTest = { 'new Editor': () => { - const e = new Editor(defaultTerminalSize); + const e = Editor.create(defaultTerminalSize); assertInstanceOf(e, Editor); }, '.open': async () => { - const e = new Editor(defaultTerminalSize); + const e = Editor.create(defaultTerminalSize); await e.open(THIS_FILE); assertInstanceOf(e, Editor); }, '.processKeyPress - letters': async () => { - const e = new Editor(defaultTerminalSize); + const e = Editor.create(defaultTerminalSize); const res = await e.processKeyPress('a'); assertTrue(res); }, '.processKeyPress - ctrl-q': async () => { // Dirty file (Need to clear confirmation messages) - const e = new Editor(defaultTerminalSize); + const e = Editor.create(defaultTerminalSize); await e.processKeyPress('d'); assertTrue(await e.processKeyPress(Fn.ctrlKey('q'))); assertTrue(await e.processKeyPress(Fn.ctrlKey('q'))); @@ -443,7 +464,7 @@ const EditorTest = { assertFalse(await e.processKeyPress(Fn.ctrlKey('q'))); // Clean file - const e2 = new Editor(defaultTerminalSize); + const e2 = Editor.create(defaultTerminalSize); const res = await e2.processKeyPress(Fn.ctrlKey('q')); assertFalse(res); }, diff --git a/src/common/ansi.ts b/src/common/ansi.ts index af0d838..ef91615 100644 --- a/src/common/ansi.ts +++ b/src/common/ansi.ts @@ -42,6 +42,7 @@ export enum AnsiColor { FgMagenta, FgCyan, FgWhite, + ForegroundColor, FgDefault, // Background Colors @@ -53,6 +54,7 @@ export enum AnsiColor { BgMagenta, BgCyan, BgWhite, + BackgroundColor, BgDefault, // Bright Foreground Colors @@ -77,8 +79,8 @@ export enum AnsiColor { } export enum Ground { - Fore = AnsiColor.FgDefault, - Back = AnsiColor.BgDefault, + Fore = AnsiColor.ForegroundColor, + Back = AnsiColor.BackgroundColor, } // ---------------------------------------------------------------------------- diff --git a/src/common/buffer.ts b/src/common/buffer.ts index f2eeb85..3201df0 100644 --- a/src/common/buffer.ts +++ b/src/common/buffer.ts @@ -4,7 +4,11 @@ import { getRuntime } from './runtime/mod.ts'; class Buffer { #b = ''; - constructor() { + private constructor() { + } + + public static default(): Buffer { + return new Buffer(); } public append(s: string, maxLen?: number): void { diff --git a/src/common/document.ts b/src/common/document.ts index 4599855..b4836a4 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -102,15 +102,6 @@ export class Document { const position = Position.from(at); for (let y = at.y; y >= 0 && y < this.numRows; y += direction) { - if (this.row(position.y).isNone()) { - logWarning('Invalid Search location', { - position, - document: this, - }); - - return None; - } - const maybeMatch = this.#rows[y].find(q, position.x, direction); if (maybeMatch.isSome()) { position.x = this.#rows[y].rxToCx(maybeMatch.unwrap()); diff --git a/src/common/editor.ts b/src/common/editor.ts index 169cc19..7f5bb8c 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -16,62 +16,45 @@ import Option, { None, Some } from './option.ts'; import { getRuntime, logDebug, logWarning } from './runtime/mod.ts'; import { ITerminalSize, Position, SearchDirection } from './types.ts'; +/** + * The main Editor interface + */ export default class Editor { /** - * The document being edited + * @param screen - The size of the screen in rows/columns + * @param document - The document being edited + * @param buffer - The output buffer for the terminal + * @param cursor - The current location of the mouse cursor + * @param offset - The current scrolling offset + * @param renderX - The scrolling offset for the rendered row + * @param filename - The name of the currently open file + * @param statusMessage - A message to display at the bottom of the screen + * @param statusTimeout - Timeout for status messages + * @param quitTimes - The number of times required to quit a dirty document + * @param highlightedWord - The current search term, if there is one + * @private */ - protected document: Document; - /** - * The output buffer for the terminal - */ - protected buffer: Buffer; - /** - * The size of the screen in rows/columns - */ - protected screen: ITerminalSize; - /** - * The current location of the mouse cursor - */ - protected cursor: Position; - /** - * The current scrolling offset - */ - protected offset: Position; - /** - * The scrolling offset for the rendered row - */ - protected renderX: number = 0; - /** - * The name of the currently open file - */ - protected filename: string = ''; - /** - * A message to display at the bottom of the screen - */ - protected statusMessage: string = ''; - /** - * Timeout for status messages - */ - protected statusTimeout: number = 0; - /** - * The number of times required to quit a dirty document - */ - protected quitTimes: number = SCROLL_QUIT_TIMES; - - protected highlightedWord: Option = None; - - constructor(terminalSize: ITerminalSize) { - this.buffer = new Buffer(); - + private constructor( + protected screen: ITerminalSize, + protected document: Document = Document.default(), + protected buffer: Buffer = Buffer.default(), + protected cursor: Position = Position.default(), + protected offset: Position = Position.default(), + protected renderX: number = 0, + protected filename: string = '', + protected statusMessage: string = '', + protected statusTimeout: number = 0, + protected quitTimes: number = SCROLL_QUIT_TIMES, + protected highlightedWord: Option = None, + ) { // Subtract two rows from the terminal size // for displaying the status bar // and message bar - this.screen = terminalSize; this.screen.rows -= 2; + } - this.cursor = Position.default(); - this.offset = Position.default(); - this.document = Document.default(); + public static create(terminalSize: ITerminalSize) { + return new Editor(terminalSize); } protected get numRows(): number { @@ -82,10 +65,6 @@ export default class Editor { return this.document.row(at); } - protected get currentRow(): Option { - return this.row(this.cursor.y); - } - public async open(filename: string): Promise { await this.document.open(filename); this.filename = filename; @@ -400,9 +379,12 @@ export default class Editor { this.cursor = Position.at(x, y); } + /** + * Calculate the window of a file to display + */ protected scroll(): void { this.renderX = (this.row(this.cursor.y).isSome()) - ? this.document.row(this.cursor.y).unwrap().cxToRx(this.cursor.x) + ? this.row(this.cursor.y).unwrap().cxToRx(this.cursor.x) : 0; const { y } = this.cursor; diff --git a/src/common/filetype/base.ts b/src/common/filetype/base.ts new file mode 100644 index 0000000..7574c19 --- /dev/null +++ b/src/common/filetype/base.ts @@ -0,0 +1,87 @@ +import Option, { None } from '../option.ts'; + +// ---------------------------------------------------------------------------- +// File-related types +// ---------------------------------------------------------------------------- + +export enum FileLang { + TypeScript = 'TypeScript', + JavaScript = 'JavaScript', + PHP = 'PHP', + Go = 'Golang', + Rust = 'Rust', + CSS = 'CSS', + Shell = 'Shell', + Plain = 'Plain Text', +} + +export interface HighlightingOptions { + numbers: boolean; + octalNumbers: boolean; + hexNumbers: boolean; + binNumbers: boolean; + jsBigInt: boolean; + strings: boolean; +} + +interface IFileType { + readonly name: FileLang; + readonly singleLineComment: Option; + readonly multiLineCommentStart: Option; + readonly multiLineCommentEnd: Option; + readonly keywords1: string[]; + readonly keywords2: string[]; + readonly operators: string[]; + readonly hlOptions: HighlightingOptions; + get flags(): HighlightingOptions; + get primaryKeywords(): string[]; + get secondaryKeywords(): string[]; + hasMultilineComments(): boolean; +} + +/** + * The base class for File Types + */ +export abstract class AbstractFileType implements IFileType { + public readonly name: FileLang = FileLang.Plain; + public readonly singleLineComment = None; + public readonly multiLineCommentStart: Option = None; + public readonly multiLineCommentEnd: Option = None; + public readonly keywords1: string[] = []; + public readonly keywords2: string[] = []; + public readonly operators: string[] = []; + public readonly hlOptions: HighlightingOptions = { + numbers: false, + octalNumbers: false, + hexNumbers: false, + binNumbers: false, + jsBigInt: false, + strings: false, + }; + + get flags(): HighlightingOptions { + return this.hlOptions; + } + + get primaryKeywords(): string[] { + return this.keywords1; + } + + get secondaryKeywords(): string[] { + return this.keywords2; + } + + public hasMultilineComments(): boolean { + return this.multiLineCommentStart.isSome() && + this.multiLineCommentEnd.isSome(); + } +} + +export const defaultHighlightOptions: HighlightingOptions = { + numbers: true, + octalNumbers: false, + hexNumbers: false, + binNumbers: false, + jsBigInt: false, + strings: true, +}; diff --git a/src/common/filetype/css.ts b/src/common/filetype/css.ts new file mode 100644 index 0000000..09da21c --- /dev/null +++ b/src/common/filetype/css.ts @@ -0,0 +1,393 @@ +import Option, { None, Some } from '../option.ts'; +import { + AbstractFileType, + defaultHighlightOptions, + FileLang, + HighlightingOptions, +} from './base.ts'; + +export class CSSFile extends AbstractFileType { + public readonly name: FileLang = FileLang.CSS; + public readonly singleLineComment = None; + public readonly multiLineCommentStart: Option = Some('/*'); + public readonly multiLineCommentEnd: Option = Some('*/'); + public readonly keywords1 = [ + ':active', + ':any-link', + ':autofill', + ':checked', + ':default', + ':disabled', + ':empty', + ':enabled', + ':first-child', + ':first-of-type', + ':focus-visible', + ':focus-within', + ':focus', + ':fullscreen', + ':hover', + ':in-range', + ':indeterminate', + ':invalid', + ':last-child', + ':last-of-type', + ':link', + ':modal', + ':nth-child', + ':nth-last-child', + ':nth-last-of-type', + ':nth-of-type', + ':only-child', + ':only-of-type', + ':optional', + ':out-of-range', + ':paused', + ':picture-in-picture', + ':placeholder-shown', + ':playing', + ':read-only', + ':read-write', + ':required', + ':root', + ':scope', + ':target', + ':user-valid', + ':valid', + ':visited', + '::after', + '::backdrop', + '::before', + '::cue', + '::file-selector-button', + '::first-letter', + '::first-line', + '::grammar-error', + '::marker', + '::placeholder', + '::selection', + '::spelling-error', + '@charset', + '@color-profile', + '@container', + '@counter-style', + '@font-face', + '@font-feature-values', + '@font-palette-values', + '@import', + '@keyframes', + '@layer', + '@media', + '@namespace', + '@page', + '@position-try', + '@property', + '@scope', + '@starting-style', + '@supports', + '@view-transition', + ]; + public readonly keywords2 = [ + 'animation-range-end', + 'animation-range-start', + 'accent-color', + 'animation-timeline', + 'animation', + 'animation-timing-function', + 'animation-composition', + 'animation-delay', + 'animation-direction', + 'appearance', + 'align-content', + 'animation-duration', + 'align-items', + 'animation-fill-mode', + 'align-self', + 'animation-iteration-count', + 'aspect-ratio', + 'align-tracks', + 'animation-name', + 'all', + 'animation-play-state', + 'animation-name', + 'anchor-name', + 'border-block-start-color', + 'border-inline-style', + 'backdrop-filter', + 'border-block-start-style', + 'border-inline-width', + 'backface-visibility', + 'border-block-start-width', + 'border-left', + 'background', + 'border-block-style', + 'border-left-color', + 'background-attachment', + 'border-block-width', + 'border-left-style', + 'background-blend-mode', + 'border-bottom', + 'border-left-width', + 'background-clip', + 'border-bottom-color', + 'border-radius', + 'background-color', + 'border-bottom-left-radius', + 'border-right', + 'background-image', + 'border-bottom-right-radius', + 'border-right-color', + 'background-origin', + 'border-bottom-style', + 'border-right-style', + 'background-position', + 'border-bottom-width', + 'border-right-width', + 'background-position-x', + 'border-collapse', + 'border-spacing', + 'background-position-y', + 'border-color', + 'border-start-end-radius', + 'background-repeat', + 'border-end-end-radius', + 'border-start-start-radius', + 'background-size', + 'border-end-start-radius', + 'border-style', + 'border-image', + 'border-top', + 'border-image-outset', + 'border-top-color', + 'border-image-repeat', + 'border-top-left-radius', + 'border-image-slice', + 'border-top-right-radius', + 'border-image-source', + 'border-top-style', + 'border-image-width', + 'border-top-width', + 'border-inline', + 'border-width', + 'block-size', + 'border-inline-color', + 'bottom', + 'border-inline-end', + 'border', + 'border-inline-end-color', + 'box-decoration-break', + 'border-block', + 'border-inline-end-style', + 'box-shadow', + 'border-block-color', + 'border-inline-end-width', + 'box-sizing', + 'border-block-end', + 'border-inline-start', + 'break-after', + 'border-block-end-color', + 'border-inline-start-color', + 'break-before', + 'border-block-end-style', + 'border-inline-start-style', + 'break-inside', + 'border-block-end-width', + 'border-inline-start-width', + 'border-block-start', + 'column-rule', + 'content-visibility', + 'caption-side', + 'column-rule-color', + 'column-rule-style', + 'caret-color', + 'column-rule-width', + 'column-span', + 'counter-increment', + 'column-width', + 'counter-reset', + 'columns', + 'counter-set', + 'contain', + 'contain-intrinsic-block-size', + 'clear', + 'contain-intrinsic-height', + 'clip', + 'contain-intrinsic-inline-size', + 'clip-path', + 'color', + 'contain-intrinsic-width', + 'cursor', + 'color-scheme', + 'container', + 'column-count', + 'container-name', + 'column-fill', + 'container-type', + 'column-gap', + 'content', + 'direction', + 'display', + 'empty-cells', + 'font-synthesis-position', + 'field-sizing', + 'font', + 'font-synthesis-small-caps', + 'filter', + 'font-synthesis-style', + 'font-synthesis-weight', + 'font-family', + 'font-variant', + 'font-variant-alternates', + 'font-variant-caps', + 'font-feature-settings', + 'font-variant-east-asian', + 'font-variant-emoji', + 'font-variant-ligatures', + 'font-variant-numeric', + 'font-variant-position', + 'flex', + 'font-kerning', + 'font-variation-settings', + 'flex-basis', + 'font-language-override', + 'flex-direction', + 'font-optical-sizing', + 'flex-flow', + 'font-palette', + 'font-weight', + 'flex-grow', + 'flex-shrink', + 'font-size', + 'forced-color-adjust', + 'flex-wrap', + 'font-size-adjust', + 'font-stretch', + 'float', + 'font-style', + 'font-synthesis', + 'grid-auto-columns', + 'grid-row-end', + 'gap', + 'grid-auto-flow', + 'grid-row-start', + 'grid-auto-rows', + 'grid-template', + 'grid-column', + 'grid-template-areas', + 'grid-column-end', + 'grid-template-columns', + 'grid', + 'grid-column-start', + 'grid-template-rows', + 'grid-area', + 'grid-row', + 'hanging-punctuation', + 'hyphenate-character', + 'hyphenate-limit-chars', + 'height', + 'hyphens', + 'initial', + 'inset-inline', + 'initial-letter', + 'inset-inline-end', + 'image-orientation', + 'image-rendering', + 'inline-size', + 'image-resolution', + 'inset', + 'isolation', + 'inset-area', + 'inset-block', + 'inherit', + 'inset-block-end', + 'inset-block-start', + 'justify-content', + 'justify-self', + 'justify-items', + 'justify-tracks', + 'letter-spacing', + 'list-style', + 'list-style-image', + 'line-break', + 'list-style-position', + 'line-clamp', + 'list-style-type', + 'line-height', + 'left', + 'line-height-step', + 'mask-border-outset', + 'margin', + 'mask-border-repeat', + 'margin-block', + 'mask-border-slice', + 'margin-block-end', + 'mask-border-source', + 'margin-block-start', + 'mask-border-width', + 'max-height', + 'margin-bottom', + 'mask-clip', + 'max-inline-size', + 'margin-inline', + 'mask-composite', + 'margin-inline-end', + 'mask-image', + 'max-width', + 'margin-inline-start', + 'mask-mode', + 'margin-left', + 'mask-origin', + 'margin-right', + 'mask-position', + 'min-block-size', + 'margin-top', + 'mask-repeat', + 'min-height', + 'margin-trim', + 'mask-size', + 'min-inline-size', + 'mask-type', + 'min-width', + 'masonry-auto-flow', + 'mask', + 'math-depth', + 'mix-blend-mode', + 'mask-border', + 'math-shift', + 'mask-border-mode', + 'math-style', + 'object-fit', + 'order', + 'overflow-inline', + 'object-position', + 'overflow-wrap', + 'offset', + 'orphans', + 'overflow-x', + 'offset-anchor', + 'overflow-y', + 'offset-distance', + 'outline', + 'overlay', + 'offset-path', + 'outline-color', + 'offset-position', + 'outline-offset', + 'offset-rotate', + 'outline-style', + 'overscroll-behavior', + 'outline-width', + 'overscroll-behavior-block', + 'overscroll-behavior-inline', + 'opacity', + 'overflow-anchor', + 'overscroll-behavior-x', + 'overflow-block', + 'overscroll-behavior-y', + 'overflow-clip-margin', + ]; + public readonly operators = ['::', ':', ',', '+', '>', '~', '-']; + public readonly hlOptions: HighlightingOptions = { + ...defaultHighlightOptions, + }; +} diff --git a/src/common/filetype/filetype.ts b/src/common/filetype/filetype.ts index 761d19c..9c8b057 100644 --- a/src/common/filetype/filetype.ts +++ b/src/common/filetype/filetype.ts @@ -1,259 +1,8 @@ import { node_path as path } from '../runtime/mod.ts'; -import Option, { None, Some } from '../option.ts'; - -// ---------------------------------------------------------------------------- -// File-related types -// ---------------------------------------------------------------------------- - -export enum FileLang { - TypeScript = 'TypeScript', - JavaScript = 'JavaScript', - CSS = 'CSS', - Shell = 'Shell', - Plain = 'Plain Text', -} - -export interface HighlightingOptions { - numbers: boolean; - strings: boolean; -} - -interface IFileType { - readonly name: FileLang; - readonly singleLineComment: Option; - readonly multiLineCommentStart: Option; - readonly multiLineCommentEnd: Option; - readonly keywords1: string[]; - readonly keywords2: string[]; - readonly operators: string[]; - readonly hlOptions: HighlightingOptions; - get flags(): HighlightingOptions; - get primaryKeywords(): string[]; - get secondaryKeywords(): string[]; - hasMultilineComments(): boolean; -} - -/** - * The base class for File Types - */ -export abstract class AbstractFileType implements IFileType { - public readonly name: FileLang = FileLang.Plain; - public readonly singleLineComment = None; - public readonly multiLineCommentStart: Option = None; - public readonly multiLineCommentEnd: Option = None; - public readonly keywords1: string[] = []; - public readonly keywords2: string[] = []; - public readonly operators: string[] = []; - public readonly hlOptions: HighlightingOptions = { - numbers: false, - strings: false, - }; - - get flags(): HighlightingOptions { - return this.hlOptions; - } - - get primaryKeywords(): string[] { - return this.keywords1; - } - - get secondaryKeywords(): string[] { - return this.keywords2; - } - - public hasMultilineComments(): boolean { - return this.multiLineCommentStart.isSome() && - this.multiLineCommentEnd.isSome(); - } -} - -// ---------------------------------------------------------------------------- -// FileType implementations -// ---------------------------------------------------------------------------- -const defaultHighlightOptions: HighlightingOptions = { - numbers: true, - strings: true, -}; - -class JavaScriptFile extends AbstractFileType { - public readonly name: FileLang = FileLang.JavaScript; - public readonly singleLineComment = Some('//'); - public readonly multiLineCommentStart: Option = Some('/*'); - public readonly multiLineCommentEnd: Option = Some('*/'); - public readonly keywords1 = [ - '=>', - 'await', - 'break', - 'case', - 'catch', - 'class', - 'const', - 'continue', - 'debugger', - 'default', - 'delete', - 'do', - 'else', - 'export', - 'extends', - 'false', - 'finally', - 'for', - 'function', - 'if', - 'import', - 'in', - 'instanceof', - 'let', - 'new', - 'null', - 'return', - 'static', - 'super', - 'switch', - 'this', - 'throw', - 'true', - 'try', - 'typeof', - 'var', - 'void', - 'while', - 'with', - 'yield', - ]; - public readonly keywords2 = [ - 'arguments', - 'as', - 'async', - 'BigInt', - 'Boolean', - 'eval', - 'from', - 'get', - 'JSON', - 'Math', - 'Number', - 'Object', - 'of', - 'set', - 'String', - 'Symbol', - 'undefined', - ]; - public readonly operators = [ - '>>>=', - '**=', - '<<=', - '>>=', - '&&=', - '||=', - '??=', - '===', - '!==', - '>>>', - '+=', - '-=', - '*=', - '/=', - '%=', - '&=', - '^=', - '|=', - '==', - '!=', - '>=', - '<=', - '++', - '--', - '**', - '<<', - '>>', - '&&', - '||', - '??', - '?.', - '?', - ':', - '=', - '>', - '<', - '%', - '-', - '+', - '&', - '|', - '^', - '~', - '!', - ]; - public readonly hlOptions: HighlightingOptions = { - ...defaultHighlightOptions, - }; -} - -class TypeScriptFile extends JavaScriptFile { - public readonly name: FileLang = FileLang.TypeScript; - public readonly multiLineCommentStart: Option = Some('/*'); - public readonly multiLineCommentEnd: Option = Some('*/'); - public readonly keywords2 = [ - ...super.secondaryKeywords, - // Typescript-specific - 'any', - 'bigint', - 'boolean', - 'enum', - 'interface', - 'keyof', - 'number', - 'private', - 'protected', - 'public', - 'string', - 'type', - 'unknown', - ]; -} - -class ShellFile extends AbstractFileType { - public readonly name: FileLang = FileLang.Shell; - public readonly singleLineComment = Some('#'); - public readonly keywords1 = [ - 'case', - 'do', - 'done', - 'elif', - 'else', - 'esac', - 'fi', - 'for', - 'function', - 'if', - 'in', - 'select', - 'then', - 'time', - 'until', - 'while', - 'declare', - ]; - public readonly keywords2 = ['set']; - public readonly operators = ['[[', ']]']; - public readonly hlOptions: HighlightingOptions = { - ...defaultHighlightOptions, - numbers: false, - }; -} - -class CSSFile extends AbstractFileType { - public readonly name: FileLang = FileLang.CSS; - public readonly singleLineComment = None; - public readonly multiLineCommentStart: Option = Some('/*'); - public readonly multiLineCommentEnd: Option = Some('*/'); - public readonly hlOptions: HighlightingOptions = { - ...defaultHighlightOptions, - }; -} +import { AbstractFileType } from './base.ts'; +import { CSSFile } from './css.ts'; +import { JavaScriptFile, TypeScriptFile } from './javascript.ts'; +import { ShellFile } from './shell.ts'; // ---------------------------------------------------------------------------- // External interface diff --git a/src/common/filetype/javascript.ts b/src/common/filetype/javascript.ts new file mode 100644 index 0000000..e388ce8 --- /dev/null +++ b/src/common/filetype/javascript.ts @@ -0,0 +1,149 @@ +import Option, { Some } from '../option.ts'; +import { + AbstractFileType, + defaultHighlightOptions, + FileLang, + HighlightingOptions, +} from './base.ts'; + +export class JavaScriptFile extends AbstractFileType { + public readonly name: FileLang = FileLang.JavaScript; + public readonly singleLineComment = Some('//'); + public readonly multiLineCommentStart: Option = Some('/*'); + public readonly multiLineCommentEnd: Option = Some('*/'); + public readonly keywords1 = [ + '=>', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'let', + 'new', + 'null', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', + ]; + public readonly keywords2 = [ + 'arguments', + 'as', + 'async', + 'BigInt', + 'Boolean', + 'eval', + 'from', + 'get', + 'JSON', + 'Math', + 'Number', + 'Object', + 'of', + 'set', + 'String', + 'Symbol', + 'undefined', + ]; + public readonly operators = [ + '>>>=', + '**=', + '<<=', + '>>=', + '&&=', + '||=', + '??=', + '===', + '!==', + '>>>', + '+=', + '-=', + '*=', + '/=', + '%=', + '&=', + '^=', + '|=', + '==', + '!=', + '>=', + '<=', + '++', + '--', + '**', + '<<', + '>>', + '&&', + '||', + '??', + '?.', + '?', + ':', + '=', + '>', + '<', + '%', + '-', + '+', + '&', + '|', + '^', + '~', + '!', + ]; + public readonly hlOptions: HighlightingOptions = { + ...defaultHighlightOptions, + octalNumbers: true, + hexNumbers: true, + binNumbers: true, + jsBigInt: true, + }; +} + +export class TypeScriptFile extends JavaScriptFile { + public readonly name: FileLang = FileLang.TypeScript; + public readonly keywords2 = [ + ...super.secondaryKeywords, + // Typescript-specific + 'any', + 'bigint', + 'boolean', + 'enum', + 'interface', + 'keyof', + 'number', + 'private', + 'protected', + 'public', + 'string', + 'type', + 'unknown', + ]; +} diff --git a/src/common/filetype/mod.ts b/src/common/filetype/mod.ts index 4dbdee3..eee6487 100644 --- a/src/common/filetype/mod.ts +++ b/src/common/filetype/mod.ts @@ -1 +1,2 @@ +export * from './base.ts'; export * from './filetype.ts'; diff --git a/src/common/filetype/shell.ts b/src/common/filetype/shell.ts new file mode 100644 index 0000000..5ffc0de --- /dev/null +++ b/src/common/filetype/shell.ts @@ -0,0 +1,37 @@ +import { Some } from '../option.ts'; +import { + AbstractFileType, + defaultHighlightOptions, + FileLang, + HighlightingOptions, +} from './base.ts'; + +export class ShellFile extends AbstractFileType { + public readonly name: FileLang = FileLang.Shell; + public readonly singleLineComment = Some('#'); + public readonly keywords1 = [ + 'case', + 'do', + 'done', + 'elif', + 'else', + 'esac', + 'fi', + 'for', + 'function', + 'if', + 'in', + 'select', + 'then', + 'time', + 'until', + 'while', + 'declare', + ]; + public readonly keywords2 = ['set']; + public readonly operators = ['[[', ']]']; + public readonly hlOptions: HighlightingOptions = { + ...defaultHighlightOptions, + numbers: false, + }; +} diff --git a/src/common/main.ts b/src/common/main.ts index 41ba834..a3f3acd 100644 --- a/src/common/main.ts +++ b/src/common/main.ts @@ -23,10 +23,8 @@ export async function main() { logError(JSON.stringify(error, null, 2)); }); - const terminalSize = await term.getTerminalSize(); - // Create the editor itself - const editor = new Editor(terminalSize); + const editor = Editor.create(await term.getTerminalSize()); // Process cli arguments if (term.argv.length > 0) { diff --git a/src/common/row.ts b/src/common/row.ts index 4a97290..5407be8 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -46,22 +46,37 @@ export class Row { this.rchars = []; } + /** + * Get the number of 'characters' in this row + */ public get size(): number { return this.chars.length; } + /** + * Get the number of 'characters' in the 'render' array + */ public get rsize(): number { return this.rchars.length; } + /** + * Get the 'render' string + */ public rstring(offset: number = 0): string { return this.rchars.slice(offset).join(''); } + /** + * Create a new empty Row + */ public static default(): Row { return new Row(); } + /** + * Create a new Row + */ public static from(s: string | string[] | Row): Row { if (s instanceof Row) { return s; @@ -70,11 +85,17 @@ export class Row { return new Row(s); } + /** + * Add a character to the end of the current row + */ public append(s: string, syntax: FileType): void { this.chars = this.chars.concat(strChars(s)); this.update(None, syntax); } + /** + * Add a character to the current row at the specified location + */ public insertChar(at: number, c: string): void { const newSlice = strChars(c); if (at >= this.size) { @@ -141,6 +162,10 @@ export class Row { return Some(this.cxToRx(this.byteIndexToCharIndex(byteIndex))); } + /** + * Search the current Row for the given string, returning the index in + * the 'render' version + */ public rIndexOf(s: string, offset: number = 0): Option { const rstring = this.rchars.join(''); const byteIndex = rstring.indexOf(s, this.charIndexToByteIndex(offset)); @@ -223,10 +248,17 @@ export class Row { ); } + /** + * Output the contents of the row + */ public toString(): string { return this.chars.join(''); } + /** + * Setup up the row by converting tabs to spaces for rendering, + * then setup syntax highlighting + */ public update( word: Option, syntax: FileType, @@ -241,17 +273,24 @@ export class Row { return this.highlight(word, syntax, startWithComment); } + /** + * Calculate the syntax types of the current Row + */ public highlight( word: Option, syntax: FileType, startWithComment: boolean, ): boolean { + // When the highlighting is already up-to-date if (this.isHighlighted && word.isNone()) { return false; } this.hl = []; let i = 0; + + // Handle the case where we are in a multi-line + // comment from a previous row let inMlComment = startWithComment; if (inMlComment && syntax.hasMultilineComments()) { const maybeEnd = this.rIndexOf(syntax.multiLineCommentEnd.unwrap(), i); @@ -266,8 +305,7 @@ export class Row { } for (; i < this.rsize;) { - const ch = this.rchars[i]; - const maybeMultiline = this.highlightMultilineComment(i, syntax, ch); + const maybeMultiline = this.highlightMultilineComment(i, syntax); if (maybeMultiline.isSome()) { inMlComment = true; i = maybeMultiline.unwrap(); @@ -276,11 +314,14 @@ export class Row { inMlComment = false; - const maybeNext = this.highlightComment(i, syntax, ch) + // Go through the syntax highlighting types in order: + // If there is a match, we end the chain of syntax types + // and 'consume' the number of characters that matched + const maybeNext = this.highlightComment(i, syntax) .orElse(() => this.highlightPrimaryKeywords(i, syntax)) .orElse(() => this.highlightSecondaryKeywords(i, syntax)) - .orElse(() => this.highlightString(i, syntax, ch)) - .orElse(() => this.highlightNumber(i, syntax, ch)) + .orElse(() => this.highlightString(i, syntax)) + .orElse(() => this.highlightNumber(i, syntax)) .orElse(() => this.highlightOperators(i, syntax)); if (maybeNext.isSome()) { @@ -346,7 +387,6 @@ export class Row { protected highlightComment( i: number, syntax: FileType, - _ch: string, ): Option { // Highlight single-line comments if (syntax.singleLineComment.isSome()) { @@ -365,7 +405,7 @@ export class Row { return None; } - protected highlightStr( + private highlightStr( i: number, substring: string, hl_type: HighlightType, @@ -390,7 +430,7 @@ export class Row { return Some(i); } - protected highlightKeywords( + private highlightKeywords( i: number, keywords: string[], hl_type: HighlightType, @@ -473,9 +513,9 @@ export class Row { protected highlightString( i: number, syntax: FileType, - ch: string, ): Option { // Highlight strings + const ch = this.rchars[i]; if (syntax.flags.strings && ch === '"' || ch === "'") { while (true) { this.hl.push(HighlightType.String); @@ -500,12 +540,13 @@ export class Row { protected highlightMultilineComment( i: number, syntax: FileType, - ch: string, ): Option { if (!syntax.hasMultilineComments()) { return None; } + const ch = this.rchars[i]; + const startChars = syntax.multiLineCommentStart.unwrap(); const endChars = syntax.multiLineCommentEnd.unwrap(); if (ch === startChars[0] && this.rchars[i + 1] == startChars[1]) { @@ -526,50 +567,65 @@ export class Row { protected highlightNumber( i: number, syntax: FileType, - ch: string, ): Option { - // Highlight numbers - if (syntax.flags.numbers && isAsciiDigit(ch)) { - if (i > 0 && !isSeparator(this.rchars[i - 1])) { - return None; - } - - while (true) { - this.hl.push(HighlightType.Number); - i += 1; - 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; - } - } - - return Some(i); + // Exit early + const ch = this.rchars[i]; + if (!(syntax.flags.numbers && isAsciiDigit(ch))) { + return None; } - return None; + // Configure which characters are valid + // for numbers in the current FileType + let validChars = ['.']; + if (syntax.flags.binNumbers) { + validChars = validChars.concat(['b', 'B']); + } + if (syntax.flags.octalNumbers) { + validChars = validChars.concat(['o', 'O']); + } + if (syntax.flags.hexNumbers) { + // deno-fmt-ignore + validChars = validChars.concat([ + 'a','A', + 'b','B', + 'c','C', + 'd','D', + 'e','E', + 'f','F', + 'x','X', + ]); + } + if (syntax.flags.jsBigInt) { + validChars.push('n'); + } + + // Number literals are not attached to other syntax + if (i > 0 && !isSeparator(this.rchars[i - 1])) { + return None; + } + + // Match until the end of the number literal + while (true) { + this.hl.push(HighlightType.Number); + i += 1; + if (i >= this.rsize) { + break; + } + + const nextChar = this.rchars[i]; + if ( + !(validChars.includes(nextChar) || isAsciiDigit(nextChar)) + ) { + break; + } + } + + return Some(i); } + /** + * Return a terminal-formatted version of the current row + */ public render(offset: number, len: number): string { const end = Math.min(len, this.rsize); const start = Math.min(offset, len);