Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate committed Aug 29, 2024
1 parent e9a5255 commit 485f76d
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 222 deletions.
2 changes: 1 addition & 1 deletion .changeset/spicy-spies-leave.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@nx.js/repl": patch
---

Add abstract base `REPL` class and add basic vitest tests
Allow customizing `REPL` class (for example for use with Node.js)
58 changes: 27 additions & 31 deletions packages/repl/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,40 @@ import { cursor } from 'sisteransi';
import util from 'node:util';
import { Writable } from 'node:stream';
import { emitKeypressEvents } from 'node:readline';
import { REPL } from './src/repl';

class NodeREPL extends REPL {
inspect(v: unknown): string {
return util.inspect(v, { colors: true });
}
escape(): void {
process.stdout.write(cursor.show);
process.exit();
}
}
import { REPL } from './src/index';

const encoder = new TextEncoder();
const writable = Writable.toWeb(process.stdout);
const repl = new NodeREPL(writable.getWriter());
const repl = new REPL(writable.getWriter(), {
inspect(v: unknown): string {
return util.inspect(v, { colors: true });
},
});
repl.renderPrompt();

emitKeypressEvents(process.stdin);
process.stdin.on('keypress', (str, key) => {
if (key.ctrl && key.name === 'c') {
repl.escape();
} else if (key.name === 'return') {
repl.submit();
} else if (key.name === 'backspace') {
repl.backspace();
} else if (key.name === 'left') {
repl.arrowLeft();
} else if (key.name === 'right') {
repl.arrowRight();
} else if (key.name === 'up') {
repl.arrowUp();
} else if (key.name === 'down') {
repl.arrowDown();
} else if (key.name === 'escape') {
repl.escape();
} else {
repl.write(encoder.encode(str));
}
if (key.ctrl && key.name === 'c') {
process.stdout.write(cursor.show);
process.exit();
} else if (key.name === 'return') {
repl.submit();
} else if (key.name === 'backspace') {
repl.backspace();
} else if (key.name === 'left') {
repl.arrowLeft();
} else if (key.name === 'right') {
repl.arrowRight();
} else if (key.name === 'up') {
repl.arrowUp();
} else if (key.name === 'down') {
repl.arrowDown();
} else if (key.name === 'escape') {
process.stdout.write(cursor.show);
process.exit();
} else {
repl.write(encoder.encode(str));
}
});
process.stdin.setRawMode(true);
process.stdin.resume();
Expand Down
153 changes: 147 additions & 6 deletions packages/repl/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,151 @@
import { REPL as BaseREPL } from './repl';
import { erase } from 'sisteransi';
import { bold, cyan, bgYellow } from 'kleur/colors';

export class REPL extends BaseREPL {
inspect(v: unknown): string {
return Switch.inspect(v);
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const cursorChar = (v: string) => bold(bgYellow(v || ' '));

const AsyncFunction = async function () {}.constructor;

export interface REPLOptions {
inspect?: (v: unknown) => string;
history?: string[];
prompt?: string;
}

export class REPL {
buffer = '';
historyIndex = 0;
cursorPosition = 0;
history: string[];
prompt: string;
writer: WritableStreamDefaultWriter<Uint8Array>;
inspect: (v: unknown) => string;

constructor(
writer: WritableStreamDefaultWriter<Uint8Array>,
opts?: REPLOptions,
) {
this.writer = writer;
this.history = [];
this.prompt = opts?.prompt ?? bold(cyan('> '));
this.inspect = opts?.inspect ?? Switch.inspect;
}

async renderPrompt(extra = '') {
let b = this.buffer;
if (this.cursorPosition >= 0) {
const bufferL = this.buffer.slice(0, this.cursorPosition);
const bufferP = this.buffer[this.cursorPosition];
const bufferR = this.buffer.slice(this.cursorPosition + 1);
b = `${bufferL}${cursorChar(bufferP)}${bufferR}`;
}
await this.print(`\r${erase.line}${this.prompt}${b}${extra}`);
}

async print(str: string) {
await this.writer.write(
encoder.encode(str.replace(/(?:\r\n|\n)/g, '\r\n')),
);
}

async write(data: Uint8Array) {
this.buffer = `${this.buffer.slice(0, this.cursorPosition)}${decoder.decode(
data,
)}${this.buffer.slice(this.cursorPosition)}`;
this.cursorPosition += data.length;
await this.renderPrompt();
}
escape(): void {
Switch.exit();

async submit() {
// Remove cursor
this.cursorPosition = -1;
await this.renderPrompt('\n');
try {
this.history.push(this.buffer);
this.historyIndex = this.history.length;
const trimmed = this.buffer.trim();
let result: any = undefined;
if (trimmed.length > 0) {
if (/\bawait\b/.test(trimmed)) {
const r = await AsyncFunction(
`try {
var v = (${trimmed});
if (v && typeof v.catch === 'function') v.catch();
return { v }
} catch (err) {
return { err }
}`,
)();
if (r.err) throw r.err;
result = r.v;
} else {
try {
result = (0, eval)(`(${trimmed})`);
} catch (err: unknown) {
if (err instanceof SyntaxError) {
result = (0, eval)(trimmed);
} else {
throw err;
}
}
}
}
if (typeof result?.catch === 'function') result.catch();
this.buffer = '';
this.cursorPosition = 0;
Object.defineProperty(globalThis, '_', {
value: result,
enumerable: false,
configurable: true,
});
await this.print(`${this.inspect(result)}\n\n`);
} catch (err: unknown) {
this.buffer = '';
this.cursorPosition = 0;
await this.print(`Uncaught ${this.inspect(err)}\n`);
}
await this.renderPrompt();
}

async backspace() {
if (this.buffer.length) {
this.buffer = `${this.buffer.slice(
0,
this.cursorPosition - 1,
)}${this.buffer.slice(this.cursorPosition)}`;
this.cursorPosition--;
await this.renderPrompt();
}
}

async arrowUp() {
if (this.historyIndex > 0) {
this.buffer = this.history[--this.historyIndex];
this.cursorPosition = this.buffer.length;
await this.renderPrompt();
}
}

async arrowDown() {
if (this.historyIndex < this.history.length) {
this.buffer = this.history[++this.historyIndex] ?? '';
this.cursorPosition = this.buffer.length;
await this.renderPrompt();
}
}

async arrowLeft() {
if (this.cursorPosition > 0) {
this.cursorPosition--;
await this.renderPrompt();
}
}

async arrowRight() {
if (this.cursorPosition < this.buffer.length) {
this.cursorPosition++;
await this.renderPrompt();
}
}
}
148 changes: 0 additions & 148 deletions packages/repl/src/repl.ts

This file was deleted.

Loading

0 comments on commit 485f76d

Please sign in to comment.