Add text insertion
This commit is contained in:
parent
35f949c4b5
commit
95c979066a
@ -17,6 +17,10 @@ const BunFileIO: IFileIO = {
|
|||||||
appendFileSync: function (path: string, contents: string) {
|
appendFileSync: function (path: string, contents: string) {
|
||||||
return appendFileSync(path, contents);
|
return appendFileSync(path, contents);
|
||||||
},
|
},
|
||||||
|
saveFile: async function (path: string, contents: string): Promise<void> {
|
||||||
|
await Bun.write(path, contents);
|
||||||
|
return;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BunFileIO;
|
export default BunFileIO;
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
import { getTestRunner } from './runtime.ts';
|
|
||||||
import { Ansi, KeyCommand, readKey } from './ansi.ts';
|
import { Ansi, KeyCommand, readKey } from './ansi.ts';
|
||||||
import { Document, Row } from './document.ts';
|
|
||||||
import Buffer from './buffer.ts';
|
import Buffer from './buffer.ts';
|
||||||
import {
|
import Document from './document.ts';
|
||||||
chars,
|
import Editor from './editor.ts';
|
||||||
ctrlKey,
|
import Row from './row.ts';
|
||||||
isAscii,
|
import { getTestRunner } from './runtime.ts';
|
||||||
isControl,
|
|
||||||
noop,
|
|
||||||
ord,
|
|
||||||
strlen,
|
|
||||||
truncate,
|
|
||||||
} from './utils.ts';
|
|
||||||
import { Editor } from './editor.ts';
|
|
||||||
import { defaultTerminalSize } from './termios.ts';
|
import { defaultTerminalSize } from './termios.ts';
|
||||||
|
import * as Util from './utils.ts';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
assertEquals,
|
assertEquals,
|
||||||
@ -25,7 +17,7 @@ const {
|
|||||||
testSuite,
|
testSuite,
|
||||||
} = await getTestRunner();
|
} = await getTestRunner();
|
||||||
|
|
||||||
const testObj = {
|
testSuite({
|
||||||
'ANSI::ANSI utils': {
|
'ANSI::ANSI utils': {
|
||||||
'Ansi.moveCursor': () => {
|
'Ansi.moveCursor': () => {
|
||||||
assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
||||||
@ -112,76 +104,74 @@ const testObj = {
|
|||||||
assertInstanceOf(doc.row(0), Row);
|
assertInstanceOf(doc.row(0), Row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'Document::Row': {
|
|
||||||
'new Row': () => {
|
|
||||||
const row = new Row();
|
|
||||||
assertEquals(row.toString(), '');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Editor: {
|
Editor: {
|
||||||
'new Editor': () => {
|
'new Editor': () => {
|
||||||
const e = new Editor(defaultTerminalSize);
|
const e = new Editor(defaultTerminalSize);
|
||||||
assertInstanceOf(e, Editor);
|
assertInstanceOf(e, Editor);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Row: {
|
||||||
|
'new Row': () => {
|
||||||
|
const row = new Row();
|
||||||
|
assertEquals(row.toString(), '');
|
||||||
|
},
|
||||||
|
},
|
||||||
'Util::Misc fns': {
|
'Util::Misc fns': {
|
||||||
'noop fn': () => {
|
'noop fn': () => {
|
||||||
assertExists(noop);
|
assertExists(Util.noop);
|
||||||
assertEquals(noop(), undefined);
|
assertEquals(Util.noop(), undefined);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'Util::String fns': {
|
'Util::String fns': {
|
||||||
'ord()': () => {
|
'ord()': () => {
|
||||||
// Invalid output
|
// Invalid output
|
||||||
assertEquals(ord(''), 256);
|
assertEquals(Util.ord(''), 256);
|
||||||
|
|
||||||
// Valid output
|
// Valid output
|
||||||
assertEquals(ord('a'), 97);
|
assertEquals(Util.ord('a'), 97);
|
||||||
},
|
},
|
||||||
'chars() properly splits strings into unicode characters': () => {
|
'chars() properly splits strings into unicode characters': () => {
|
||||||
assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
|
assertEquals(Util.chars('😺😸😹'), ['😺', '😸', '😹']);
|
||||||
},
|
},
|
||||||
'ctrl_key()': () => {
|
'ctrl_key()': () => {
|
||||||
const ctrl_a = ctrlKey('a');
|
const ctrl_a = Util.ctrlKey('a');
|
||||||
assertTrue(isControl(ctrl_a));
|
assertTrue(Util.isControl(ctrl_a));
|
||||||
assertEquals(ctrl_a, String.fromCodePoint(0x01));
|
assertEquals(ctrl_a, String.fromCodePoint(0x01));
|
||||||
|
|
||||||
const invalid = ctrlKey('😺');
|
const invalid = Util.ctrlKey('😺');
|
||||||
assertFalse(isControl(invalid));
|
assertFalse(Util.isControl(invalid));
|
||||||
assertEquals(invalid, '😺');
|
assertEquals(invalid, '😺');
|
||||||
},
|
},
|
||||||
'is_ascii()': () => {
|
'is_ascii()': () => {
|
||||||
assertTrue(isAscii('asjyverkjhsdf1928374'));
|
assertTrue(Util.isAscii('asjyverkjhsdf1928374'));
|
||||||
assertFalse(isAscii('😺acalskjsdf'));
|
assertFalse(Util.isAscii('😺acalskjsdf'));
|
||||||
assertFalse(isAscii('ab😺ac'));
|
assertFalse(Util.isAscii('ab😺ac'));
|
||||||
},
|
},
|
||||||
'is_control()': () => {
|
'is_control()': () => {
|
||||||
assertFalse(isControl('abc'));
|
assertFalse(Util.isControl('abc'));
|
||||||
assertTrue(isControl(String.fromCodePoint(0x01)));
|
assertTrue(Util.isControl(String.fromCodePoint(0x01)));
|
||||||
assertFalse(isControl('😺'));
|
assertFalse(Util.isControl('😺'));
|
||||||
},
|
},
|
||||||
'strlen()': () => {
|
'strlen()': () => {
|
||||||
// Ascii length
|
// Ascii length
|
||||||
assertEquals(strlen('abc'), 'abc'.length);
|
assertEquals(Util.strlen('abc'), 'abc'.length);
|
||||||
|
|
||||||
// Get number of visible unicode characters
|
// Get number of visible unicode characters
|
||||||
assertEquals(strlen('😺😸😹'), 3);
|
assertEquals(Util.strlen('😺😸😹'), 3);
|
||||||
assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
|
assertNotEquals('😺😸😹'.length, Util.strlen('😺😸😹'));
|
||||||
|
|
||||||
// Skin tone modifier + base character
|
// Skin tone modifier + base character
|
||||||
assertEquals(strlen('🤰🏼'), 2);
|
assertEquals(Util.strlen('🤰🏼'), 2);
|
||||||
assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
|
assertNotEquals('🤰🏼'.length, Util.strlen('🤰🏼'));
|
||||||
|
|
||||||
// This has 4 sub-characters, and 3 zero-width-joiners
|
// This has 4 sub-characters, and 3 zero-width-joiners
|
||||||
assertEquals(strlen('👨👩👧👦'), 7);
|
assertEquals(Util.strlen('👨👩👧👦'), 7);
|
||||||
assertNotEquals('👨👩👧👦'.length, strlen('👨👩👧👦'));
|
assertNotEquals('👨👩👧👦'.length, Util.strlen('👨👩👧👦'));
|
||||||
},
|
},
|
||||||
'truncate()': () => {
|
'truncate()': () => {
|
||||||
assertEquals(truncate('😺😸😹', 1), '😺');
|
assertEquals(Util.truncate('😺😸😹', 1), '😺');
|
||||||
assertEquals(truncate('😺😸😹', 5), '😺😸😹');
|
assertEquals(Util.truncate('😺😸😹', 5), '😺😸😹');
|
||||||
assertEquals(truncate('👨👩👧👦', 5), '👨👩👧');
|
assertEquals(Util.truncate('👨👩👧👦', 5), '👨👩👧');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
testSuite(testObj);
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* ANSI/VT terminal escape code handling
|
* ANSI/VT terminal escape code handling
|
||||||
*/
|
*/
|
||||||
|
import { ctrlKey } from './utils.ts';
|
||||||
|
|
||||||
export const ANSI_PREFIX = '\x1b[';
|
export const ANSI_PREFIX = '\x1b[';
|
||||||
|
|
||||||
@ -15,8 +16,11 @@ export enum KeyCommand {
|
|||||||
Delete = ANSI_PREFIX + '3~',
|
Delete = ANSI_PREFIX + '3~',
|
||||||
PageUp = ANSI_PREFIX + '5~',
|
PageUp = ANSI_PREFIX + '5~',
|
||||||
PageDown = ANSI_PREFIX + '6~',
|
PageDown = ANSI_PREFIX + '6~',
|
||||||
|
Enter = '\r',
|
||||||
|
|
||||||
// These keys have several possible escape sequences
|
// These keys have several possible escape sequences
|
||||||
|
Escape = 'EscapeKey',
|
||||||
|
Backspace = 'BackspaceKey',
|
||||||
Home = 'LineHome',
|
Home = 'LineHome',
|
||||||
End = 'LineEnd',
|
End = 'LineEnd',
|
||||||
}
|
}
|
||||||
@ -67,6 +71,14 @@ export function readKey(parsed: string): string {
|
|||||||
case '\x1b[F':
|
case '\x1b[F':
|
||||||
return KeyCommand.End;
|
return KeyCommand.End;
|
||||||
|
|
||||||
|
case ctrlKey('l'):
|
||||||
|
case '\x1b':
|
||||||
|
return KeyCommand.Escape;
|
||||||
|
|
||||||
|
case ctrlKey('H'):
|
||||||
|
case String.fromCodePoint(127):
|
||||||
|
return KeyCommand.Backspace;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,17 @@
|
|||||||
import { chars } from './utils.ts';
|
import { chars } from './utils.ts';
|
||||||
|
import Row from './row.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime } from './runtime.ts';
|
||||||
import { TAB_SIZE } from './mod.ts';
|
import { Position, TAB_SIZE } from './mod.ts';
|
||||||
|
|
||||||
export class Row {
|
|
||||||
chars: string[] = [];
|
|
||||||
render: string[] = [];
|
|
||||||
|
|
||||||
constructor(s: string = '') {
|
|
||||||
this.chars = chars(s);
|
|
||||||
this.render = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get size(): number {
|
|
||||||
return this.chars.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get rsize(): number {
|
|
||||||
return this.render.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public rstring(offset: number = 0): string {
|
|
||||||
return this.render.slice(offset).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
public cxToRx(cx: number): number {
|
|
||||||
let rx = 0;
|
|
||||||
let j = 0;
|
|
||||||
for (; j < cx; j++) {
|
|
||||||
if (this.chars[j] === '\t') {
|
|
||||||
rx += (TAB_SIZE - 1) - (rx % TAB_SIZE);
|
|
||||||
}
|
|
||||||
rx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rx;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return this.chars.join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Document {
|
export class Document {
|
||||||
|
#filename: string | null;
|
||||||
#rows: Row[];
|
#rows: Row[];
|
||||||
|
dirty: boolean;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.#rows = [];
|
this.#rows = [];
|
||||||
|
this.#filename = null;
|
||||||
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get numRows(): number {
|
get numRows(): number {
|
||||||
@ -72,9 +38,35 @@ export class Document {
|
|||||||
rawFile.split(/\r?\n/)
|
rawFile.split(/\r?\n/)
|
||||||
.forEach((row) => this.appendRow(row));
|
.forEach((row) => this.appendRow(row));
|
||||||
|
|
||||||
|
this.#filename = filename;
|
||||||
|
this.dirty = false;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async save() {
|
||||||
|
if (this.#filename === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { file } = await getRuntime();
|
||||||
|
|
||||||
|
await file.saveFile(this.#filename, this.rowsToString());
|
||||||
|
|
||||||
|
this.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public insert(at: Position, c: string): void {
|
||||||
|
if (at.y === this.numRows) {
|
||||||
|
this.appendRow(c);
|
||||||
|
} else {
|
||||||
|
this.#rows[at.y].insertChar(at.x, c);
|
||||||
|
this.updateRow(this.#rows[at.y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
public row(i: number): Row | null {
|
public row(i: number): Row | null {
|
||||||
return this.#rows[i] ?? null;
|
return this.#rows[i] ?? null;
|
||||||
}
|
}
|
||||||
@ -83,11 +75,17 @@ export class Document {
|
|||||||
const at = this.numRows;
|
const at = this.numRows;
|
||||||
this.#rows[at] = new Row(s);
|
this.#rows[at] = new Row(s);
|
||||||
this.updateRow(this.#rows[at]);
|
this.updateRow(this.#rows[at]);
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateRow(r: Row): void {
|
private updateRow(r: Row): void {
|
||||||
r.render = chars(r.toString().replace('\t', ' '.repeat(TAB_SIZE)));
|
r.render = chars(r.toString().replace('\t', ' '.repeat(TAB_SIZE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private rowsToString(): string {
|
||||||
|
return this.#rows.map((r) => r.toString()).join('\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Document;
|
export default Document;
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import Ansi, { KeyCommand } from './ansi.ts';
|
import Ansi, { KeyCommand } from './ansi.ts';
|
||||||
import Buffer from './buffer.ts';
|
import Buffer from './buffer.ts';
|
||||||
import Document, { Row } from './document.ts';
|
import Document from './document.ts';
|
||||||
import { IPoint, ITerminalSize, logToFile, VERSION } from './mod.ts';
|
import { ITerminalSize, logToFile, Position, VERSION } from './mod.ts';
|
||||||
|
import Row from './row.ts';
|
||||||
import { ctrlKey, maxAdd, posSub, truncate } from './utils.ts';
|
import { ctrlKey, maxAdd, posSub, truncate } from './utils.ts';
|
||||||
|
|
||||||
export class Editor {
|
class Editor {
|
||||||
/**
|
/**
|
||||||
* The document being edited
|
* The document being edited
|
||||||
* @private
|
* @private
|
||||||
@ -24,11 +25,11 @@ export class Editor {
|
|||||||
* The current location of the mouse cursor
|
* The current location of the mouse cursor
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
#cursor: IPoint;
|
#cursor: Position;
|
||||||
/**
|
/**
|
||||||
* The current scrolling offset
|
* The current scrolling offset
|
||||||
*/
|
*/
|
||||||
#offset: IPoint;
|
#offset: Position;
|
||||||
/**
|
/**
|
||||||
* The scrolling offset for the rendered row
|
* The scrolling offset for the rendered row
|
||||||
* @private
|
* @private
|
||||||
@ -52,19 +53,22 @@ export class Editor {
|
|||||||
|
|
||||||
constructor(terminalSize: ITerminalSize) {
|
constructor(terminalSize: ITerminalSize) {
|
||||||
this.#buffer = new Buffer();
|
this.#buffer = new Buffer();
|
||||||
|
|
||||||
|
// Subtract two rows from the terminal size
|
||||||
|
// for displaying the status bar
|
||||||
|
// and message bar
|
||||||
this.#screen = terminalSize;
|
this.#screen = terminalSize;
|
||||||
this.#screen.rows -= 2;
|
this.#screen.rows -= 2;
|
||||||
this.#cursor = {
|
|
||||||
x: 0,
|
this.#cursor = Position.default();
|
||||||
y: 0,
|
this.#offset = Position.default();
|
||||||
};
|
|
||||||
this.#offset = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
this.#document = Document.empty();
|
this.#document = Document.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get numRows(): number {
|
||||||
|
return this.#document.numRows;
|
||||||
|
}
|
||||||
|
|
||||||
private get currentRow(): Row | null {
|
private get currentRow(): Row | null {
|
||||||
return this.#document.row(this.#cursor.y);
|
return this.#document.row(this.#cursor.y);
|
||||||
}
|
}
|
||||||
@ -84,12 +88,21 @@ export class Editor {
|
|||||||
* Determine what to do based on input
|
* Determine what to do based on input
|
||||||
* @param input - the decoded chunk of stdin
|
* @param input - the decoded chunk of stdin
|
||||||
*/
|
*/
|
||||||
public processKeyPress(input: string): boolean {
|
public async processKeyPress(input: string): Promise<boolean> {
|
||||||
switch (input) {
|
switch (input) {
|
||||||
|
case KeyCommand.Enter:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
|
||||||
case ctrlKey('q'):
|
case ctrlKey('q'):
|
||||||
this.clearScreen().then(() => {});
|
await this.clearScreen();
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
case ctrlKey('s'):
|
||||||
|
await this.#document.save();
|
||||||
|
this.setStatusMessage(`${this.#filename} was saved to disk.`);
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyCommand.Home:
|
case KeyCommand.Home:
|
||||||
this.#cursor.x = 0;
|
this.#cursor.x = 0;
|
||||||
break;
|
break;
|
||||||
@ -100,6 +113,11 @@ export class Editor {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyCommand.Backspace:
|
||||||
|
case KeyCommand.Delete:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyCommand.PageUp:
|
case KeyCommand.PageUp:
|
||||||
case KeyCommand.PageDown:
|
case KeyCommand.PageDown:
|
||||||
{
|
{
|
||||||
@ -109,7 +127,7 @@ export class Editor {
|
|||||||
this.#cursor.y = maxAdd(
|
this.#cursor.y = maxAdd(
|
||||||
this.#offset.y,
|
this.#offset.y,
|
||||||
this.#screen.rows - 1,
|
this.#screen.rows - 1,
|
||||||
this.#document.numRows,
|
this.numRows,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +148,13 @@ export class Editor {
|
|||||||
case KeyCommand.ArrowLeft:
|
case KeyCommand.ArrowLeft:
|
||||||
this.moveCursor(input);
|
this.moveCursor(input);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyCommand.Escape:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.#document.insert(this.#cursor, input);
|
||||||
|
this.#cursor.x++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -166,7 +191,7 @@ export class Editor {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KeyCommand.ArrowDown:
|
case KeyCommand.ArrowDown:
|
||||||
if (this.#cursor.y < this.#document.numRows) {
|
if (this.#cursor.y < this.numRows) {
|
||||||
this.#cursor.y++;
|
this.#cursor.y++;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -239,11 +264,11 @@ export class Editor {
|
|||||||
private drawRows(): void {
|
private drawRows(): void {
|
||||||
for (let y = 0; y < this.#screen.rows; y++) {
|
for (let y = 0; y < this.#screen.rows; y++) {
|
||||||
this.#buffer.append(Ansi.ClearLine);
|
this.#buffer.append(Ansi.ClearLine);
|
||||||
const filerow = y + this.#offset.y;
|
const fileRow = y + this.#offset.y;
|
||||||
if (filerow >= this.#document.numRows) {
|
if (fileRow >= this.numRows) {
|
||||||
this.drawPlaceholderRow(filerow);
|
this.drawPlaceholderRow(fileRow);
|
||||||
} else {
|
} else {
|
||||||
this.drawFileRow(filerow);
|
this.drawFileRow(fileRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#buffer.appendLine();
|
this.#buffer.appendLine();
|
||||||
@ -288,14 +313,15 @@ export class Editor {
|
|||||||
private drawStatusBar(): void {
|
private drawStatusBar(): void {
|
||||||
this.#buffer.append(Ansi.InvertColor);
|
this.#buffer.append(Ansi.InvertColor);
|
||||||
const name = (this.#filename !== '') ? this.#filename : '[No Name]';
|
const name = (this.#filename !== '') ? this.#filename : '[No Name]';
|
||||||
const status = `${truncate(name, 20)} - ${this.#document.numRows} lines`;
|
const modified = (this.#document.dirty) ? '(modified)' : '';
|
||||||
const rstatus = `${this.#cursor.y + 1}/${this.#document.numRows}`;
|
const status = `${truncate(name, 20)} - ${this.numRows} lines ${modified}`;
|
||||||
|
const rStatus = `${this.#cursor.y + 1}/${this.numRows}`;
|
||||||
let len = Math.min(status.length, this.#screen.cols);
|
let len = Math.min(status.length, this.#screen.cols);
|
||||||
this.#buffer.append(status, len);
|
this.#buffer.append(status, len);
|
||||||
|
|
||||||
while (len < this.#screen.cols) {
|
while (len < this.#screen.cols) {
|
||||||
if (this.#screen.cols - len === rstatus.length) {
|
if (this.#screen.cols - len === rStatus.length) {
|
||||||
this.#buffer.append(rstatus);
|
this.#buffer.append(rStatus);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
this.#buffer.append(' ');
|
this.#buffer.append(' ');
|
||||||
@ -313,3 +339,5 @@ export class Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Editor;
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { readKey } from './ansi.ts';
|
import { readKey } from './ansi.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime } from './runtime.ts';
|
||||||
import { getTermios } from './termios.ts';
|
import { getTermios } from './termios.ts';
|
||||||
import { Editor } from './editor.ts';
|
import Editor from './editor.ts';
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const runTime = await getRuntime();
|
const { term, file, onExit, onEvent } = await getRuntime();
|
||||||
const { term, file, onExit, onEvent } = runTime;
|
|
||||||
|
|
||||||
// Setup raw mode, and tear down on error or normal exit
|
// Setup raw mode, and tear down on error or normal exit
|
||||||
const t = await getTermios();
|
const t = await getTermios();
|
||||||
@ -26,8 +25,9 @@ export async function main() {
|
|||||||
|
|
||||||
// Create the editor itself
|
// Create the editor itself
|
||||||
const editor = new Editor(terminalSize);
|
const editor = new Editor(terminalSize);
|
||||||
editor.setStatusMessage('HELP: Ctrl-Q = quit');
|
editor.setStatusMessage('HELP: Ctrl-S = save | Ctrl-Q = quit');
|
||||||
|
|
||||||
|
// Process cli arguments
|
||||||
if (term.argv.length > 0) {
|
if (term.argv.length > 0) {
|
||||||
const filename = term.argv[0];
|
const filename = term.argv[0];
|
||||||
if (filename.trim() !== '') {
|
if (filename.trim() !== '') {
|
||||||
@ -38,11 +38,10 @@ export async function main() {
|
|||||||
// Clear the screen
|
// Clear the screen
|
||||||
await editor.refreshScreen();
|
await editor.refreshScreen();
|
||||||
|
|
||||||
// The main event loop
|
|
||||||
for await (const chunk of term.inputLoop()) {
|
for await (const chunk of term.inputLoop()) {
|
||||||
// Process input
|
// Process input
|
||||||
const char = readKey(decoder.decode(chunk));
|
const char = readKey(decoder.decode(chunk));
|
||||||
const shouldLoop = editor.processKeyPress(char);
|
const shouldLoop = await editor.processKeyPress(char);
|
||||||
if (!shouldLoop) {
|
if (!shouldLoop) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
export * from './editor.ts';
|
export * from './editor.ts';
|
||||||
export * from './runtime.ts';
|
export * from './runtime.ts';
|
||||||
export * from './termios.ts';
|
export * from './termios.ts';
|
||||||
|
export * from './types.ts';
|
||||||
export * from './utils.ts';
|
export * from './utils.ts';
|
||||||
|
|
||||||
export type * from './types.ts';
|
export type * from './types.ts';
|
||||||
|
|
||||||
export const VERSION = '0.0.1';
|
export const VERSION = '0.0.1';
|
||||||
|
65
src/common/row.ts
Normal file
65
src/common/row.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { TAB_SIZE } from './mod.ts';
|
||||||
|
import * as Util from './utils.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row of text in the current document
|
||||||
|
*/
|
||||||
|
export class Row {
|
||||||
|
/**
|
||||||
|
* The actual characters in the current row
|
||||||
|
*/
|
||||||
|
chars: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The characters rendered for the current row
|
||||||
|
* (like replacing tabs with spaces)
|
||||||
|
*/
|
||||||
|
render: string[] = [];
|
||||||
|
|
||||||
|
constructor(s: string = '') {
|
||||||
|
this.chars = Util.chars(s);
|
||||||
|
this.render = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get size(): number {
|
||||||
|
return this.chars.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get rsize(): number {
|
||||||
|
return this.render.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rstring(offset: number = 0): string {
|
||||||
|
return this.render.slice(offset).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public insertChar(at: number, c: string): void {
|
||||||
|
const newSlice = Util.chars(c);
|
||||||
|
if (at >= this.size) {
|
||||||
|
this.chars = this.chars.concat(newSlice);
|
||||||
|
} else {
|
||||||
|
const front = this.chars.slice(0, at + 1);
|
||||||
|
const back = this.chars.slice(at + 1);
|
||||||
|
this.chars = front.concat(newSlice, back);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cxToRx(cx: number): number {
|
||||||
|
let rx = 0;
|
||||||
|
let j = 0;
|
||||||
|
for (; j < cx; j++) {
|
||||||
|
if (this.chars[j] === '\t') {
|
||||||
|
rx += (TAB_SIZE - 1) - (rx % TAB_SIZE);
|
||||||
|
}
|
||||||
|
rx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return this.chars.join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Row;
|
@ -84,6 +84,7 @@ export interface IFileIO {
|
|||||||
openFileSync(path: string): string;
|
openFileSync(path: string): string;
|
||||||
appendFile(path: string, contents: string): Promise<void>;
|
appendFile(path: string, contents: string): Promise<void>;
|
||||||
appendFileSync(path: string, contents: string): void;
|
appendFileSync(path: string, contents: string): void;
|
||||||
|
saveFile(path: string, contents: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,9 +2,22 @@
|
|||||||
// General types
|
// General types
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IPoint {
|
export class Position {
|
||||||
x: number;
|
public x: number;
|
||||||
y: number;
|
public y: number;
|
||||||
|
|
||||||
|
private constructor(x: number, y: number) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static at(x: number, y: number): Position {
|
||||||
|
return new Position(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static default(): Position {
|
||||||
|
return new Position(0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -39,7 +39,7 @@ const DenoFFI: IFFI = {
|
|||||||
tcsetattr,
|
tcsetattr,
|
||||||
cfmakeraw,
|
cfmakeraw,
|
||||||
getPointer: Deno.UnsafePointer.of,
|
getPointer: Deno.UnsafePointer.of,
|
||||||
close: () => {},
|
close: cStdLib.close,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DenoFFI;
|
export default DenoFFI;
|
||||||
|
@ -27,6 +27,9 @@ const DenoFileIO: IFileIO = {
|
|||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
Deno.writeFileSync(path, encoder.encode(contents));
|
Deno.writeFileSync(path, encoder.encode(contents));
|
||||||
},
|
},
|
||||||
|
saveFile: async function (path: string, contents: string): Promise<void> {
|
||||||
|
return await Deno.writeTextFile(path, contents);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DenoFileIO;
|
export default DenoFileIO;
|
||||||
|
Loading…
Reference in New Issue
Block a user