From 4be7be09a7e5edb1819c290256eedaa2e29010e6 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 10 Jul 2024 16:12:39 -0400 Subject: [PATCH] Fix some issues with line splitting/merging --- .gitignore | 7 +++- README.md | 3 ++ justfile | 19 ++++++---- package.json | 10 ++++++ src/common/all_test.ts | 19 ++++------ src/common/config.ts | 4 +-- src/common/document.ts | 35 +++++++----------- src/common/editor.ts | 82 ++++++++++++++++++++---------------------- src/common/runtime.ts | 40 +++++++++++++-------- 9 files changed, 117 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index 90897de..e91414a 100644 --- a/.gitignore +++ b/.gitignore @@ -335,8 +335,13 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,vim,node,deno,macos,windows,linux +# Other editors +.nova/ +.zed/ + ## Misc generated files -scroll.err +scroll*.log +docs deno.lock cov_profile/ coverage/ diff --git a/README.md b/README.md index aa6bafa..c7b4e8f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ To simplify running, I'm using [Just](https://github.com/casey/just). - Deno: `just deno-run [filename]` - TSX: `just tsx-run [filename` +Deno is generally used for dev tools, but each runtime should be functionally +equivalent running the text editor. + ## Development Notes - Implementation is based on [Kilo](https://viewsourcecode.org/snaptoken/kilo/) diff --git a/justfile b/justfile index 92e2c46..e6194d8 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ default: coverage: bun-test deno-coverage # Typescript checking -check: deno-check bun-check +check: deno-check bun-check tsx-check docs: deno doc --html --unstable-ffi --name="Scroll" ./src/scroll.ts ./src/common/*.ts ./src/deno/mod.ts ./src/bun/mod.ts @@ -27,8 +27,8 @@ clean: rm -rf .deno-cover rm -rf coverage rm -rf docs - rm -f scroll.log - rm -f scroll.err + rm -f scroll*.log + rm -f test.file rm -f tsconfig.tsbuildinfo ########################################################################################## @@ -53,8 +53,8 @@ bun-run file="": # Lint code and check types deno-check: - deno lint - deno check --unstable-ffi --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts + deno task deno-lint + deno task deno-check # Test with deno deno-test: @@ -66,15 +66,20 @@ deno-coverage: # Run with deno deno-run file="": - deno run --allow-all --allow-ffi --deny-hrtime --unstable-ffi ./src/scroll.ts {{file}} + deno task deno-run {{file}} + #deno run --allow-all --allow-ffi --deny-hrtime --unstable-ffi ./src/scroll.ts {{file}} ########################################################################################## # tsx(Node JS)-specific commands ########################################################################################## +# Check code with actual Typescript compiler +tsx-check: + npm run tsx-check + # Test with tsx (NodeJS) tsx-test: - npx tsx --test './src/common/all_test.ts' + npm run tsx-test # Run with tsx (NodeJS) tsx-run file="": diff --git a/package.json b/package.json index ad5b2ad..91e1445 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,16 @@ "bun-types": "^1.0.11" }, "scripts": { + "bun-check": "bunx tsc", + "bun-coverage": "bun test --coverage", + "bun-test": "bun test", + "deno-lint": "deno lint", + "deno-check": "deno check --unstable-ffi --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts ./src/tsx/*.ts", + "deno-coverage": "./coverage.sh", + "deno-run": "deno run --allow-all --allow-ffi --deny-hrtime --unstable-ffi ./src/scroll.ts", + "deno-test": "deno test --allow-all --unstable-ffi", + "tsx-check": "npx tsc", + "tsx-test": "npx tsx --test './src/common/all_test.ts'" }, "type": "module" } diff --git a/src/common/all_test.ts b/src/common/all_test.ts index 2c276a7..7841d84 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -381,25 +381,18 @@ const DocumentTest = { doc.delete(Position.at(3, 0)); assertEquals(doc.row(0).unwrap().toString(), 'fooar'); - // Merge previous row + // Merge next row const doc2 = Document.default(); doc2.insertNewline(Position.default()); doc2.insert(Position.at(0, 1), 'foobar'); - doc2.delete(Position.at(0, 1)); + doc2.delete(Position.at(0, 0)); assertEquals(doc2.row(0).unwrap().toString(), 'foobar'); - // Merge next row - const doc3 = Document.default(); - doc3.insertNewline(Position.default()); - doc3.insert(Position.at(0, 1), 'foobar'); - doc3.delete(Position.at(0, 0)); - assertEquals(doc3.row(0).unwrap().toString(), 'foobar'); - // Invalid delete location - const doc4 = Document.default(); - doc4.insert(Position.default(), 'foobar'); - doc4.delete(Position.at(0, 1)); - assertEquals(doc4.row(0).unwrap().toString(), 'foobar'); + const doc3 = Document.default(); + doc3.insert(Position.default(), 'foobar'); + doc3.delete(Position.at(0, 3)); + assertEquals(doc3.row(0).unwrap().toString(), 'foobar'); }, }; diff --git a/src/common/config.ts b/src/common/config.ts index 8018d55..ea6143e 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -4,8 +4,8 @@ export const SCROLL_VERSION = '0.0.1'; export const SCROLL_QUIT_TIMES = 3; export const SCROLL_TAB_SIZE = 4; -export const SCROLL_LOG_FILE = './scroll.log'; -export const SCROLL_ERR_FILE = './scroll.err'; +export const SCROLL_LOG_FILE_PREFIX = './scroll'; +export const SCROLL_LOG_FILE_SUFFIX = '.log'; export const defaultTerminalSize: ITerminalSize = { rows: 24, diff --git a/src/common/document.ts b/src/common/document.ts index e2b57c6..97ba79a 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -1,7 +1,7 @@ import Row from './row.ts'; import { arrayInsert, maxAdd, minSub } from './fns.ts'; import Option, { None, Some } from './option.ts'; -import { getRuntime, log, LogLevel } from './runtime.ts'; +import { getRuntime, logDebug } from './runtime.ts'; import { Position, SearchDirection } from './types.ts'; export class Document { @@ -153,6 +153,8 @@ export class Document { return; } + this.dirty = true; + const maybeRow = this.row(at.y); if (maybeRow.isNone()) { return; @@ -161,38 +163,29 @@ export class Document { const row = maybeRow.unwrap(); const mergeNextRow = at.x === row.size && this.row(at.y + 1).isSome(); - const mergeIntoPrevRow = at.x === 0 && this.row(at.y - 1).isSome() && - this.row(at.y).isSome(); - log({ + logDebug('Document.delete', { method: 'Document.delete', at, mergeNextRow, - mergeIntoPrevRow, - }, LogLevel.Debug); + }); // If we are at the end of a line, and press delete, // add the contents of the next row, and delete - // the merged row object + // the merged row object (This also works for pressing + // backspace at the beginning of a line: the cursor is + // moved to the end of the previous line) if (mergeNextRow) { // At the end of a line, pressing delete will merge - // the next line into the current on - const rowToAppend = this.#rows.at(at.y + 1)!.toString(); + // the next line into the current one + const rowToAppend = this.#rows[at.y + 1].toString(); row.append(rowToAppend); this.deleteRow(at.y + 1); - } else if (mergeIntoPrevRow) { - // At the beginning of a line, merge the current line - // into the previous Row - const rowToAppend = row.toString(); - this.#rows[at.y - 1].append(rowToAppend); - this.deleteRow(at.y); } else { row.delete(at.x); } row.update(None); - - this.dirty = true; } public row(i: number): Option { @@ -200,7 +193,7 @@ export class Document { return None; } - return Option.from(this.#rows[i]); + return Option.from(this.#rows.at(i)); } public insertRow(at: number = this.numRows, s: string = ''): void { @@ -219,17 +212,15 @@ export class Document { /** * Delete the specified row * @param at - the index of the row to delete - * @private */ - private deleteRow(at: number): void { + protected deleteRow(at: number): void { this.#rows.splice(at, 1); } /** * Convert the array of row objects into one string - * @private */ - private rowsToString(): string { + protected rowsToString(): string { return this.#rows.map((r) => r.toString()).join('\n'); } } diff --git a/src/common/editor.ts b/src/common/editor.ts index 9539652..db40ae3 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -13,7 +13,7 @@ import { truncate, } from './fns.ts'; import Option, { None, Some } from './option.ts'; -import { getRuntime, log, LogLevel } from './runtime.ts'; +import { getRuntime, logDebug, logWarning } from './runtime.ts'; import { ITerminalSize, Position, SearchDirection } from './types.ts'; export default class Editor { @@ -72,12 +72,16 @@ export default class Editor { this.document = Document.default(); } - private get numRows(): number { + protected get numRows(): number { return this.document.numRows; } - private get currentRow(): Option { - return this.document.row(this.cursor.y); + protected row(at: number): Option { + return this.document.row(at); + } + + protected get currentRow(): Option { + return this.row(this.cursor.y); } public async open(filename: string): Promise { @@ -150,11 +154,9 @@ export default class Editor { break; case KeyCommand.Backspace: - { - if (this.cursor.x > 0 || this.cursor.y > 0) { - this.moveCursor(KeyCommand.ArrowLeft); - this.document.delete(this.cursor); - } + if (this.cursor.x > 0 || this.cursor.y > 0) { + this.moveCursor(KeyCommand.ArrowLeft); + this.document.delete(this.cursor); } break; @@ -317,20 +319,19 @@ export default class Editor { * Filter out any additional unwanted keyboard input * * @param input - * @private */ - private shouldFilter(input: string): boolean { + protected shouldFilter(input: string): boolean { const isEscapeSequence = input[0] === KeyCommand.Escape; const isCtrl = isControl(input); const shouldFilter = isEscapeSequence || isCtrl; const whitelist = ['\t']; if (shouldFilter && !whitelist.includes(input)) { - log({ - 'msg': `Ignoring input: ${input}`, + logDebug('Ignoring input:', { + input, isEscapeSequence, isCtrl, - }, LogLevel.Debug); + }); return true; } @@ -338,22 +339,20 @@ export default class Editor { return false; } - private moveCursor(char: string): void { + protected moveCursor(char: string): void { const screenHeight = this.screen.rows; let { x, y } = this.cursor; const height = this.numRows; - let width = (this.document.row(y).isSome()) - ? this.currentRow.unwrap().size - : 0; + let width = (this.row(y).isSome()) ? this.row(y).unwrap().size : 0; - log({ - method: 'Editor.moveCursor - start', + logDebug('Editor.moveCursor - start', { + char, cursor: this.cursor, renderX: this.renderX, screen: this.screen, height, width, - }, LogLevel.Debug); + }); switch (char) { case KeyCommand.ArrowUp: @@ -371,12 +370,12 @@ export default class Editor { x -= 1; } else if (y > 0) { y -= 1; - x = (this.currentRow.isSome()) ? this.currentRow.unwrap().rsize : 0; + x = (this.row(y).isSome()) ? this.row(y).unwrap().size : 0; } break; case KeyCommand.ArrowRight: if ( - this.currentRow.isSome() && x < width + this.row(y).isSome() && x < width ) { x += 1; } else if (y < height) { @@ -398,7 +397,7 @@ export default class Editor { break; } - width = (this.currentRow.isSome()) ? this.currentRow.unwrap().size : 0; + width = (this.row(y).isSome()) ? this.row(y).unwrap().size : 0; if (x > width) { x = width; @@ -406,27 +405,25 @@ export default class Editor { this.cursor = Position.at(x, y); - log({ - method: 'Editor.moveCursor - end', + logDebug('Editor.moveCursor - end', { cursor: this.cursor, renderX: this.renderX, screen: this.screen, height, width, - }, LogLevel.Debug); + }); } - private scroll(): void { - this.renderX = (this.currentRow.isSome()) - ? this.currentRow.unwrap().cxToRx(this.cursor.x) + protected scroll(): void { + this.renderX = (this.row(this.cursor.y).isSome()) + ? this.document.row(this.cursor.y).unwrap().cxToRx(this.cursor.x) : 0; - log({ - method: 'Editor.scroll - start', + logDebug('Editor.scroll - start', { cursor: this.cursor, renderX: this.renderX, offset: this.offset, - }, LogLevel.Debug); + }); const { y } = this.cursor; const offset = this.offset; @@ -445,12 +442,11 @@ export default class Editor { offset.x = this.renderX - width + 1; } - log({ - method: 'Editor.scroll - end', + logDebug('Editor.scroll - end', { cursor: this.cursor, renderX: this.renderX, offset: this.offset, - }, LogLevel.Debug); + }); } // -------------------------------------------------------------------------- @@ -484,14 +480,14 @@ export default class Editor { await this.buffer.flush(); } - private async clearScreen(): Promise { + protected async clearScreen(): Promise { this.buffer.append(Ansi.ClearScreen); this.buffer.append(Ansi.ResetCursor); await this.buffer.flush(); } - private drawRows(): void { + protected drawRows(): void { for (let y = 0; y < this.screen.rows; y++) { this.buffer.append(Ansi.ClearLine); const fileRow = y + this.offset.y; @@ -505,10 +501,10 @@ export default class Editor { } } - private drawFileRow(y: number): void { + protected drawFileRow(y: number): void { const maybeRow = this.document.row(y); if (maybeRow.isNone()) { - log(`Trying to draw non-existent row '${y}'`, LogLevel.Warning); + logWarning(`Trying to draw non-existent row '${y}'`); return this.drawPlaceholderRow(y); } @@ -522,7 +518,7 @@ export default class Editor { this.buffer.append(row.render(this.offset.x, len)); } - private drawPlaceholderRow(y: number): void { + protected drawPlaceholderRow(y: number): void { if (y === Math.trunc(this.screen.rows / 2) && this.document.isEmpty()) { const message = `Scroll editor -- version ${SCROLL_VERSION}`; const messageLen = (message.length > this.screen.cols) @@ -542,7 +538,7 @@ export default class Editor { } } - private drawStatusBar(): void { + protected drawStatusBar(): void { this.buffer.append(Ansi.InvertColor); const name = (this.filename !== '') ? this.filename : '[No Name]'; const modified = (this.document.dirty) ? '(modified)' : ''; @@ -563,7 +559,7 @@ export default class Editor { this.buffer.appendLine(Ansi.ResetFormatting); } - private drawMessageBar(): void { + protected drawMessageBar(): void { this.buffer.append(Ansi.ClearLine); const msgLen = this.statusMessage.length; if (msgLen > 0 && (Date.now() - this.statusTimeout < 5000)) { diff --git a/src/common/runtime.ts b/src/common/runtime.ts index 24b46e6..8f05b95 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -7,8 +7,8 @@ import { IRuntime, ITerminalSize, ITestBase } from './types.ts'; import { noop } from './fns.ts'; import { defaultTerminalSize, - SCROLL_ERR_FILE, - SCROLL_LOG_FILE, + SCROLL_LOG_FILE_PREFIX, + SCROLL_LOG_FILE_SUFFIX, } from './config.ts'; export type { IFileIO, IRuntime, ITerminal } from './types.ts'; @@ -79,24 +79,36 @@ async function _getTerminalSizeFromAnsi(): Promise { }; } -export function log(s: unknown, level: LogLevel = LogLevel.Notice): void { +/** + * Basic logging - + */ +export function log( + s: unknown, + level: LogLevel = LogLevel.Notice, + data?: any, +): void { getRuntime().then(({ file }) => { - const raw = JSON.stringify(s, null, 2); - const output = `${level}: ${raw}\n`; + const rawS = JSON.stringify(s, null, 2); + const rawData = JSON.stringify(data, null, 2); + const output = (typeof data !== 'undefined') + ? `${rawS}\n${rawData}\n\n` + : `${rawS}\n`; - const outputFile = (level === LogLevel.Error) - ? SCROLL_ERR_FILE - : SCROLL_LOG_FILE; + const outputFile = + `${SCROLL_LOG_FILE_PREFIX}-${level.toLowerCase()}${SCROLL_LOG_FILE_SUFFIX}`; file.appendFile(outputFile, output).then(noop); }); } -/** - * Append information to the scroll.err logfile - */ -export function logError(s: unknown): void { - log(s, LogLevel.Error); -} +export const logDebug = (s: unknown, data?: any) => + log(s, LogLevel.Debug, data); +export const logInfo = (s: unknown, data?: any) => log(s, LogLevel.Info, data); +export const logNotice = (s: unknown, data?: any) => + log(s, LogLevel.Notice, data); +export const logWarning = (s: unknown, data?: any) => + log(s, LogLevel.Warning, data); +export const logError = (s: unknown, data?: any) => + log(s, LogLevel.Warning, data); /** * Kill program, displaying an error message