Fix some issues with line splitting/merging
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-10 16:12:39 -04:00
parent 4436a8a783
commit 4be7be09a7
9 changed files with 117 additions and 102 deletions

7
.gitignore vendored
View File

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

View File

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

View File

@ -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="":

View File

@ -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"
}

View File

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

View File

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

View File

@ -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<Row> {
@ -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');
}
}

View File

@ -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<Row> {
return this.document.row(this.cursor.y);
protected row(at: number): Option<Row> {
return this.document.row(at);
}
protected get currentRow(): Option<Row> {
return this.row(this.cursor.y);
}
public async open(filename: string): Promise<Editor> {
@ -150,12 +154,10 @@ 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);
}
}
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<void> {
protected async clearScreen(): Promise<void> {
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)) {

View File

@ -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<ITerminalSize> {
};
}
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