A lot of tweaks
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-19 15:31:27 -04:00
parent 5b40d16999
commit 21d26ede6c
14 changed files with 856 additions and 385 deletions

View File

@ -63,6 +63,7 @@ function print16colorTable(): void {
console.log(colorTable);
}
function print256colorTable(): void {
let colorTable = '';
// deno-fmt-ignore

View File

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

View File

@ -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,
}
// ----------------------------------------------------------------------------

View File

@ -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 {

View File

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

View File

@ -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<string> = 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<string> = 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<Row> {
return this.row(this.cursor.y);
}
public async open(filename: string): Promise<Editor> {
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;

View File

@ -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<string>;
readonly multiLineCommentStart: Option<string>;
readonly multiLineCommentEnd: Option<string>;
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<string> = None;
public readonly multiLineCommentEnd: Option<string> = 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,
};

393
src/common/filetype/css.ts Normal file
View File

@ -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<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = 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,
};
}

View File

@ -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<string>;
readonly multiLineCommentStart: Option<string>;
readonly multiLineCommentEnd: Option<string>;
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<string> = None;
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,
};
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<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = 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<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = 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<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = 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

View File

@ -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<string> = Some('/*');
public readonly multiLineCommentEnd: Option<string> = 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',
];
}

View File

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

View File

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

View File

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

View File

@ -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<number> {
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<string>,
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<string>,
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<number> {
// 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<number> {
// 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<number> {
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<number> {
// 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);