Skip to content

Commit c3f52aa

Browse files
committed
Use child process of User's Node.js as REPL.
1 parent 31f96e2 commit c3f52aa

File tree

5 files changed

+125
-115
lines changed

5 files changed

+125
-115
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# http://editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
charset = utf-8
7+
indent_style = space
8+
indent_size = 4
9+
end_of_line = lf
10+
trim_trailing_whitespace = true

src/Decorator.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
window,
1010
} from "vscode";
1111

12+
import * as Util from 'util';
13+
1214

1315
// create a decorator type that we use to decorate small numbers
1416
const resultDecorationType = window.createTextEditorDecorationType({
@@ -17,10 +19,14 @@ const resultDecorationType = window.createTextEditorDecorationType({
1719
dark: {},
1820
});
1921
const colorMap = {
20-
Result: 'green',
21-
Error: 'red',
22-
Console: '#457abb',
22+
'Value of Expression': 'green',
23+
'Console': '#457abb',
24+
'Error': 'red',
2325
}
26+
const inspectOptions = { maxArrayLength: null, depth: null };
27+
28+
type Data = { line: number, type: 'Expression' | 'Terminal', value: any };
29+
type Result = { line: number, type: 'Value of Expression' | 'Console' | 'Error', text: string, value: any };
2430

2531
export default class Decorator {
2632
private editor: TextEditor;
@@ -37,11 +43,13 @@ export default class Decorator {
3743
this.decorators = [];
3844
}
3945

40-
async update(result: any) {
46+
async update(data: Data) {
47+
48+
let result = (data.type === 'Expression')
49+
? await this.formatExpressionValue(data)
50+
: this.formatTerminalOutput(data);
4151

42-
result = (result.type === 'Expression')
43-
? await this.formatExpressionValue(result)
44-
: this.formatTerminalOutput(result);
52+
if (!result) return;
4553

4654
let decorator: DecorationOptions;
4755

@@ -63,60 +71,60 @@ export default class Decorator {
6371

6472
decorator.hoverMessage = new MarkdownString(result.type);
6573
decorator.hoverMessage.appendCodeblock(
66-
result.type == 'Console' ? result.value.join('\n') : result.value || result.text,
74+
result.type === 'Console' ? result.value.join('\n') : result.value || result.text,
6775
'javascript'
6876
);
6977

7078
this.decorateAll();
7179
}
72-
private async formatExpressionValue(data: any): Promise<{ line?: number, type: 'Result' | 'Error', text: string, value: any }> {
80+
private async formatExpressionValue(data: Data): Promise<Result> {
7381
let result = data.value;
7482
switch (typeof result) {
7583
case 'undefined':
76-
break;
84+
return null;
7785

7886
case 'object':
7987
if (result.constructor && result.constructor.name === 'Promise' && result.then) {
8088
try {
81-
return this.formatExpressionValue(Object.assign(data, { value: await Promise.resolve(result) }));
82-
} catch (ex) {
89+
let value = await Promise.resolve(result);
90+
return value ? this.formatExpressionValue(Object.assign(data, { value })) : null;
91+
} catch (error) {
8392
return {
8493
line: data.line,
8594
type: 'Error',
86-
text: `${ex.name}: ${ex.message}`,
87-
value: ex,
95+
text: `${error.name}: ${error.message}`,
96+
value: error,
8897
}
8998
}
9099
}
91100

101+
let string = Util.inspect(result, inspectOptions);
92102
return {
93103
line: data.line,
94-
type: 'Result',
95-
text: (Array.isArray(result))
96-
? JSON.stringify(result)
97-
: JSON.stringify(result, null, '\t').replace(/\n/g, ' '),
98-
value: result,
104+
type: 'Value of Expression',
105+
text: string,
106+
value: string,
99107
}
100108

101109
default:
102110
return {
103111
line: data.line,
104-
type: 'Result',
112+
type: 'Value of Expression',
105113
text: result.toString().replace(/\r?\n/g, ' '),
106114
value: result,
107115
}
108116
}
109117
}
110-
private formatTerminalOutput(data: any): { line?: number, type: 'Console' | 'Error', text: string, value: any } {
111-
let lineCount = data.line;
112-
let out = data.text;
118+
private formatTerminalOutput(data: Data): Result {
119+
let out = data.value as string;
113120
let match: RegExpExecArray;
114121

115-
if ((match = /(\w+:\s.*)\n\s*at\s/gi.exec(out)) != null) {
116-
this.outputChannel.appendLine(` ${match[1]}\n\tat line ${lineCount}`);
117-
return { line: lineCount, type: 'Error', text: match[1], value: match[1] }
122+
if ((match = /^(Error:\s.*)(?:\n\s*at\s)?/g.exec(out)) != null) {
123+
this.outputChannel.appendLine(` ${match[1]}\n\tat line ${data.line}`);
124+
125+
return { line: data.line, type: 'Error', text: match[1], value: match[1] };
118126
}
119-
else if ((match = /`\{(\d+)\}`([\s\S]*)/gi.exec(out)) != null) {
127+
else if ((match = /^`\{(\d+)\}`([\s\S]*)$/g.exec(out)) != null) {
120128
let line = +match[1];
121129
let msg = match[2] || '';
122130

src/ReplClient.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "./code";
1717

1818
import Decorator from "./Decorator";
19-
import REPLServer from "./ReplServer";
19+
import { spawn, ChildProcess } from "child_process";
2020

2121

2222
export default class ReplClient {
@@ -29,7 +29,7 @@ export default class ReplClient {
2929
private basePath: string;
3030
private filePath: string;
3131

32-
private repl: REPLServer;
32+
private repl: ChildProcess;
3333

3434
private editingTimer: NodeJS.Timer = null;
3535
private afterEditTimer: NodeJS.Timer = null;
@@ -59,12 +59,12 @@ export default class ReplClient {
5959
let currentLine = this.editor.selection.active.line;
6060
this.decorator.decorateExcept(currentLine);
6161
}
62-
62+
6363
if (this.afterEditTimer) clearTimeout(this.afterEditTimer);
6464
if (this.editingTimer) clearTimeout(this.editingTimer);
6565

6666
if (text.lastIndexOf(';') >= 0 || text.lastIndexOf('\n') >= 0 || (text === '' && change.range.isSingleLine === false))
67-
this.editingTimer = setTimeout(async () => await this.interpret(), 300);
67+
this.editingTimer = setTimeout(async () => await this.interpret(), 600);
6868
else
6969
this.afterEditTimer = setTimeout(async () => await this.interpret(), 1500);
7070
}
@@ -75,7 +75,7 @@ export default class ReplClient {
7575
}
7676

7777
init(editor: TextEditor, doc: TextDocument) {
78-
this.outputChannel.appendLine(`Initializing REPL extension with Node ${process.version}`);
78+
this.outputChannel.appendLine(`Initializing REPL extension.`);
7979
this.outputChannel.appendLine(` Warning; Be careful with CRUD operations since the code is running multiple times in REPL.`);
8080

8181
this.editor = editor;
@@ -96,19 +96,22 @@ export default class ReplClient {
9696
try {
9797
this.decorator.init(this.editor);
9898

99+
this.repl = spawn('node', [`${__dirname}/replServer.js`], { cwd: this.basePath, stdio: ['ignore', 'ignore', 'ignore', 'ipc'] })
100+
.on('message', async result => await this.decorator.update(result))
101+
.on('error', err => this.outputChannel.appendLine(`[Repl Server] ${err.message}`));
102+
99103
let code = this.editor.document.getText();
104+
105+
this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`);
106+
100107
// TODO: typescript REPL
101108
// code = `require("${Path.join(this.basePath, "node_modules/ts-node").replace(/\\/g, '\\\\')}").register({});\n${code}`;
102109
code = rewriteImportToRequire(code);
103110
code = rewriteModulePathInRequire(code, this.basePath, this.filePath);
104111
code = rewriteConsoleToAppendLineNumber(code);
105112
code = rewriteChainCallInOneLine(code);
106113

107-
this.repl = new REPLServer(this.outputChannel)
108-
.on('output', async result => await this.decorator.update(result));
109-
110-
this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`);
111-
this.repl.interpret(code);
114+
this.repl.send({ code });
112115
}
113116
catch (ex) {
114117
this.outputChannel.appendLine(ex);
@@ -120,6 +123,8 @@ export default class ReplClient {
120123
this.outputChannel.appendLine(`Disposing REPL server.`);
121124

122125
this.editor = null;
126+
127+
this.repl.send({ operation: 'exit' });
123128
this.repl = null;
124129
}
125130

@@ -132,6 +137,8 @@ export default class ReplClient {
132137
this.closeTextDocumentDisposable.dispose();
133138
this.changeEventDisposable.dispose();
134139
this.editor = null;
140+
141+
this.repl.send({ operation: 'exit' });
135142
this.repl = null;
136143
}
137144
}

src/ReplServer.ts

Lines changed: 61 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,75 @@
1-
import { OutputChannel } from "vscode";
2-
import { EventEmitter } from "events";
3-
import { Readable, Writable } from "stream";
41
import * as Repl from 'repl';
52
import * as Util from 'util';
3+
import { Writable, Readable } from 'stream';
4+
5+
6+
type ReplServer = Repl.REPLServer & { inputStream: Readable, eval: ReplEval };
7+
type ReplEval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => void;
8+
9+
let lineCount = 0;
10+
11+
const server = Repl.start({
12+
prompt: '',
13+
input: new Readable({ read: () => { } }),
14+
output: new Writable({
15+
write: (chunk, encoding, callback) => {
16+
let out = chunk.toString().trim();
17+
switch (out) {
18+
case '...': break;
19+
case '': break;
20+
default:
21+
process.send({ line: lineCount, type: 'Terminal', value: out });
22+
break;
23+
}
24+
callback();
25+
}
26+
}),
27+
ignoreUndefined: true,
628

29+
}) as ReplServer;
730

8-
export default class NodeRepl extends EventEmitter {
9-
private replEval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: string) => void) => void;
10-
11-
constructor(private outputChannel: OutputChannel) {
12-
super();
13-
}
14-
15-
public async interpret(code: string) {
16-
try {
17-
let inputStream = new Readable({ read: () => { } }),
18-
lineCount = 0;
19-
20-
let repl = Repl.start({
21-
prompt: '',
22-
input: inputStream,
23-
writer: (out) => {
24-
if (out == null) return;
25-
},
26-
output: new Writable({
27-
write: (chunk, encoding, cb) => {
28-
let out = chunk.toString().trim();
29-
switch (out) {
30-
case 'undefined':
31-
case '...':
32-
case '':
33-
break;
34-
35-
default:
36-
this.emit('output', { line: lineCount, type: 'Terminal', text: out })
37-
break;
38-
}
39-
cb();
40-
}
41-
}),
42-
}) as Repl.REPLServer & { eval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => void };
43-
44-
if (this.replEval == null)
45-
this.replEval = repl.eval; // keep a backup of original eval
46-
47-
// nice place to read the result in sequence and inject it in the code
48-
repl.eval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => {
49-
50-
this.replEval(cmd, context, filename, (err, result: any) => {
51-
let regex = /\/\*`(\d+)`\*\//gi,
52-
match: RegExpExecArray
5331

54-
if (!err) {
55-
while ((match = regex.exec(cmd)) != null)
56-
lineCount += +match[1];
32+
const originEval = server.eval; // keep a backup of original eval
33+
const lineNumber = /\/\*`(\d+)`\*\//gi;
5734

58-
this.emit('output', { line: lineCount, type: 'Expression', value: result });
59-
}
35+
// nice place to read the result in sequence and inject it in the code
36+
server.eval = (cmd, context, filename, callback) => {
37+
originEval(cmd, context, filename, (err, result) => {
38+
let match: RegExpExecArray;
6039

61-
cb(err, result);
62-
});
40+
while ((match = lineNumber.exec(cmd)) != null)
41+
lineCount += +match[1];
6342

64-
lineCount++;
65-
}
43+
if (result)
44+
process.send({ line: lineCount, type: 'Expression', value: result });
6645

67-
const originLog = repl.context.console.log;
68-
const appendLineLog = (lineNumber: number, text: any, ...args: any[]) => {
69-
originLog(`\`{${lineNumber}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args);
70-
}
71-
Object.defineProperty(repl.context, '`console`', {
72-
value: {
73-
log: appendLineLog,
74-
debug: appendLineLog,
75-
error: appendLineLog,
76-
}
77-
})
46+
callback(err, result);
47+
});
7848

79-
for (let line of code.split(/\r?\n/)) {
80-
// tell the REPL about the line of code to see if there is any result coming out of it
81-
inputStream.push(`${line}\n`);
82-
}
49+
lineCount++;
50+
}
8351

84-
inputStream.push(`.exit\n`);
85-
inputStream.push(null);
52+
const originLog = server.context.console.log;
53+
const appendLineLog = (lineNumber: number, text: any, ...args: any[]) => {
54+
originLog(`\`{${lineNumber}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args);
55+
}
56+
Object.defineProperty(server.context, '`console`', {
57+
value: {
58+
log: appendLineLog,
59+
debug: appendLineLog,
60+
error: appendLineLog,
61+
}
62+
});
8663

87-
repl.on('exit', () => setTimeout(() => this.emit('exit'), 100));
88-
} catch (ex) {
89-
this.outputChannel.appendLine(ex);
64+
process.on('message', data => {
65+
if (data.code) {
66+
try {
67+
for (let line of data.code.split('\n'))
68+
server.inputStream.push(line + '\n');
69+
} catch (error) {
70+
process.emit('error', error);
9071
}
72+
} else if (data.operation === 'exit') {
73+
process.exit();
9174
}
92-
}
75+
});

src/code.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function rewriteModulePathInRequire(code: string, basePath: string, fileP
4242
});
4343
}
4444

45+
4546
const linBreak = /\r?\n/;
4647
const consoleLogCall = /console\s*\.(log|debug|error)\(/g;
4748

@@ -56,6 +57,7 @@ export function rewriteConsoleToAppendLineNumber(code: string): string {
5657
return out.join('\n');
5758
}
5859

60+
5961
const lineBreakInChainCall = /([\n\s]+)\./gi;
6062

6163
export function rewriteChainCallInOneLine(code: string): string {

0 commit comments

Comments
 (0)