From 31a4dea53a9bf945426cbd7f998afdfc0da527ec Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Mon, 26 Nov 2018 14:05:06 +0800 Subject: [PATCH 01/12] Fix cannot import Node.js native modules, like 'util'. --- src/extension.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 09a6375..faad86b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -529,9 +529,11 @@ class NodeRepl extends EventEmitter { let path = Path.join(this.basePath, 'node_modules', name); if(Fs.existsSync(path) === false) - path = (doc.isUntitled) - ? Path.normalize(Path.join(this.basePath, name)) - : Path.join(Path.dirname(doc.fileName), name); + path = (name.indexOf('/') === -1) + ? name + : (doc.isUntitled) + ? Path.join(this.basePath, name) + : Path.join(Path.dirname(doc.fileName), name); return `require('${path.replace(/\\/g, '\\\\')}')`; }); From 31f96e29c0d5fd1a41414eaabd97579d403f5043 Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Tue, 4 Dec 2018 09:54:52 +0800 Subject: [PATCH 02/12] refactor all code --- src/Decorator.ts | 148 ++++++++++++ src/ReplClient.ts | 137 +++++++++++ src/ReplServer.ts | 92 +++++++ src/code.ts | 63 +++++ src/extension.ts | 600 +++++----------------------------------------- 5 files changed, 499 insertions(+), 541 deletions(-) create mode 100644 src/Decorator.ts create mode 100644 src/ReplClient.ts create mode 100644 src/ReplServer.ts create mode 100644 src/code.ts diff --git a/src/Decorator.ts b/src/Decorator.ts new file mode 100644 index 0000000..cda200c --- /dev/null +++ b/src/Decorator.ts @@ -0,0 +1,148 @@ +import { + DecorationOptions, + DecorationRangeBehavior, + MarkdownString, + OutputChannel, + Position, + Range, + TextEditor, + window, +} from "vscode"; + + +// create a decorator type that we use to decorate small numbers +const resultDecorationType = window.createTextEditorDecorationType({ + rangeBehavior: DecorationRangeBehavior.ClosedClosed, + light: {}, + dark: {}, +}); +const colorMap = { + Result: 'green', + Error: 'red', + Console: '#457abb', +} + +export default class Decorator { + private editor: TextEditor; + private lineToOutput: Map = new Map(); + private lineToDecorator: Map = new Map(); + private decorators: DecorationOptions[] = []; + + constructor(private outputChannel: OutputChannel) { } + + init(editor: TextEditor) { + this.editor = editor; + this.lineToOutput.clear(); + this.lineToDecorator.clear(); + this.decorators = []; + } + + async update(result: any) { + + result = (result.type === 'Expression') + ? await this.formatExpressionValue(result) + : this.formatTerminalOutput(result); + + let decorator: DecorationOptions; + + if ((decorator = this.lineToDecorator.get(result.line)) == null) { + let line = result.line, + length = this.editor.document.getText(new Range(new Position(line, 0), new Position(line, 37768))).length + 1, + pos = new Position(line, length); + + decorator = { + renderOptions: { before: { margin: '0 0 0 1em' } }, + range: new Range(pos, pos) + }; + this.lineToDecorator.set(result.line, decorator); + this.decorators.push(decorator); + } + + decorator.renderOptions.before.color = colorMap[result.type]; + decorator.renderOptions.before.contentText = ` ${result.text}`; + + decorator.hoverMessage = new MarkdownString(result.type); + decorator.hoverMessage.appendCodeblock( + result.type == 'Console' ? result.value.join('\n') : result.value || result.text, + 'javascript' + ); + + this.decorateAll(); + } + private async formatExpressionValue(data: any): Promise<{ line?: number, type: 'Result' | 'Error', text: string, value: any }> { + let result = data.value; + switch (typeof result) { + case 'undefined': + break; + + case 'object': + if (result.constructor && result.constructor.name === 'Promise' && result.then) { + try { + return this.formatExpressionValue(Object.assign(data, { value: await Promise.resolve(result) })); + } catch (ex) { + return { + line: data.line, + type: 'Error', + text: `${ex.name}: ${ex.message}`, + value: ex, + } + } + } + + return { + line: data.line, + type: 'Result', + text: (Array.isArray(result)) + ? JSON.stringify(result) + : JSON.stringify(result, null, '\t').replace(/\n/g, ' '), + value: result, + } + + default: + return { + line: data.line, + type: 'Result', + text: result.toString().replace(/\r?\n/g, ' '), + value: result, + } + } + } + private formatTerminalOutput(data: any): { line?: number, type: 'Console' | 'Error', text: string, value: any } { + let lineCount = data.line; + let out = data.text; + let match: RegExpExecArray; + + if ((match = /(\w+:\s.*)\n\s*at\s/gi.exec(out)) != null) { + this.outputChannel.appendLine(` ${match[1]}\n\tat line ${lineCount}`); + return { line: lineCount, type: 'Error', text: match[1], value: match[1] } + } + else if ((match = /`\{(\d+)\}`([\s\S]*)/gi.exec(out)) != null) { + let line = +match[1]; + let msg = match[2] || ''; + + let output = this.lineToOutput.get(line); + if (output == null) { + this.lineToOutput.set(+match[1], output = { line, type: 'Console', text: '', value: [] }); + } + + output.text += (output.text === '' ? '' : ', ') + msg.replace(/\r?\n/g, ' '); + output.value.push(msg); + + this.outputChannel.appendLine(` ${msg}`); + return output; + } + else { + this.outputChannel.appendLine(` ${out}`); + } + } + + decorateAll() { + this.decorate(this.decorators); + } + decorateExcept(line: number) { + this.decorate(this.decorators.filter(d => line != d.range.start.line)); + } + private decorate(decorations: DecorationOptions[]) { + this.editor.setDecorations(resultDecorationType, decorations); + } +} \ No newline at end of file diff --git a/src/ReplClient.ts b/src/ReplClient.ts new file mode 100644 index 0000000..bb58e0a --- /dev/null +++ b/src/ReplClient.ts @@ -0,0 +1,137 @@ +import { + Disposable, + OutputChannel, + TextEditor, + TextDocument, + window, + workspace, + Uri, +} from "vscode"; + +import { + rewriteImportToRequire, + rewriteModulePathInRequire, + rewriteConsoleToAppendLineNumber, + rewriteChainCallInOneLine +} from "./code"; + +import Decorator from "./Decorator"; +import REPLServer from "./ReplServer"; + + +export default class ReplClient { + private changeEventDisposable: Disposable; + private closeTextDocumentDisposable: Disposable; + private changeActiveDisposable: Disposable; + + private editor: TextEditor; + private decorator: Decorator; + private basePath: string; + private filePath: string; + + private repl: REPLServer; + + private editingTimer: NodeJS.Timer = null; + private afterEditTimer: NodeJS.Timer = null; + + constructor(private outputChannel: OutputChannel) { + this.decorator = new Decorator(outputChannel); + + this.changeActiveDisposable = window.onDidChangeActiveTextEditor(async (editor) => { + if (this.editor === editor) + this.interpret(); + }); + + this.closeTextDocumentDisposable = workspace.onDidCloseTextDocument(async (document) => { + if (this.editor && this.editor.document === document) + this.close(); + }); + + this.changeEventDisposable = workspace.onDidChangeTextDocument(async (event) => { + try { + if (!this.editor || this.editor.document !== event.document) + return; + + let change = event.contentChanges[0], + text = change.text; + + if (/\n/.test(text) === false && change.range.isSingleLine) { + let currentLine = this.editor.selection.active.line; + this.decorator.decorateExcept(currentLine); + } + + if (this.afterEditTimer) clearTimeout(this.afterEditTimer); + if (this.editingTimer) clearTimeout(this.editingTimer); + + if (text.lastIndexOf(';') >= 0 || text.lastIndexOf('\n') >= 0 || (text === '' && change.range.isSingleLine === false)) + this.editingTimer = setTimeout(async () => await this.interpret(), 300); + else + this.afterEditTimer = setTimeout(async () => await this.interpret(), 1500); + } + catch (err) { + this.outputChannel.appendLine(err); + } + }); + } + + init(editor: TextEditor, doc: TextDocument) { + this.outputChannel.appendLine(`Initializing REPL extension with Node ${process.version}`); + this.outputChannel.appendLine(` Warning; Be careful with CRUD operations since the code is running multiple times in REPL.`); + + this.editor = editor; + + if (workspace && Array.isArray(workspace.workspaceFolders)) { + + this.basePath = (doc.isUntitled) + ? workspace.workspaceFolders[0].uri.fsPath + : workspace.getWorkspaceFolder(Uri.file(doc.fileName)).uri.fsPath; + + this.filePath = doc.isUntitled ? '' : doc.fileName; + + this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] working at: ${this.basePath}`); + } + } + + async interpret() { + try { + this.decorator.init(this.editor); + + let code = this.editor.document.getText(); + // TODO: typescript REPL + // code = `require("${Path.join(this.basePath, "node_modules/ts-node").replace(/\\/g, '\\\\')}").register({});\n${code}`; + code = rewriteImportToRequire(code); + code = rewriteModulePathInRequire(code, this.basePath, this.filePath); + code = rewriteConsoleToAppendLineNumber(code); + code = rewriteChainCallInOneLine(code); + + this.repl = new REPLServer(this.outputChannel) + .on('output', async result => await this.decorator.update(result)); + + this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`); + this.repl.interpret(code); + } + catch (ex) { + this.outputChannel.appendLine(ex); + } + } + + close() { + if (this.outputChannel) + this.outputChannel.appendLine(`Disposing REPL server.`); + + this.editor = null; + this.repl = null; + } + + get isClosed() { + return this.repl == null; + } + + dispose() { + this.changeActiveDisposable.dispose(); + this.closeTextDocumentDisposable.dispose(); + this.changeEventDisposable.dispose(); + this.editor = null; + this.repl = null; + } +} \ No newline at end of file diff --git a/src/ReplServer.ts b/src/ReplServer.ts new file mode 100644 index 0000000..41e10fd --- /dev/null +++ b/src/ReplServer.ts @@ -0,0 +1,92 @@ +import { OutputChannel } from "vscode"; +import { EventEmitter } from "events"; +import { Readable, Writable } from "stream"; +import * as Repl from 'repl'; +import * as Util from 'util'; + + +export default class NodeRepl extends EventEmitter { + private replEval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: string) => void) => void; + + constructor(private outputChannel: OutputChannel) { + super(); + } + + public async interpret(code: string) { + try { + let inputStream = new Readable({ read: () => { } }), + lineCount = 0; + + let repl = Repl.start({ + prompt: '', + input: inputStream, + writer: (out) => { + if (out == null) return; + }, + output: new Writable({ + write: (chunk, encoding, cb) => { + let out = chunk.toString().trim(); + switch (out) { + case 'undefined': + case '...': + case '': + break; + + default: + this.emit('output', { line: lineCount, type: 'Terminal', text: out }) + break; + } + cb(); + } + }), + }) as Repl.REPLServer & { eval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => void }; + + if (this.replEval == null) + this.replEval = repl.eval; // keep a backup of original eval + + // nice place to read the result in sequence and inject it in the code + repl.eval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => { + + this.replEval(cmd, context, filename, (err, result: any) => { + let regex = /\/\*`(\d+)`\*\//gi, + match: RegExpExecArray + + if (!err) { + while ((match = regex.exec(cmd)) != null) + lineCount += +match[1]; + + this.emit('output', { line: lineCount, type: 'Expression', value: result }); + } + + cb(err, result); + }); + + lineCount++; + } + + const originLog = repl.context.console.log; + const appendLineLog = (lineNumber: number, text: any, ...args: any[]) => { + originLog(`\`{${lineNumber}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args); + } + Object.defineProperty(repl.context, '`console`', { + value: { + log: appendLineLog, + debug: appendLineLog, + error: appendLineLog, + } + }) + + for (let line of code.split(/\r?\n/)) { + // tell the REPL about the line of code to see if there is any result coming out of it + inputStream.push(`${line}\n`); + } + + inputStream.push(`.exit\n`); + inputStream.push(null); + + repl.on('exit', () => setTimeout(() => this.emit('exit'), 100)); + } catch (ex) { + this.outputChannel.appendLine(ex); + } + } +} \ No newline at end of file diff --git a/src/code.ts b/src/code.ts new file mode 100644 index 0000000..824acbe --- /dev/null +++ b/src/code.ts @@ -0,0 +1,63 @@ +import * as Path from 'path'; +import * as Fs from 'fs'; + + +const importStatement = /import\s*(?:(\*\s+as\s)?([\w-_]+),?)?\s*(?:\{([^\}]+)\})?\s+from\s+["']([^"']+)["']/gi; + +export function rewriteImportToRequire(code: string): string { + return code.replace(importStatement, (str, wildcard: string, module: string, modules: string, from: string) => { + let rewrite = ''; + + // 导入所有 + if (module) + rewrite += `${rewrite === '' ? '' : ', '}default: ${module} `; + // 解构导入 + if (modules) + rewrite += `${rewrite === '' ? '' : ', '}${modules + .split(',') + .map(r => r.replace(/\s*([\w-_]+)(?:\s+as\s+([\w-_]))?\s*/gi, (str, moduleName: string, moduleNewName: string) => { + return `${moduleNewName ? `${moduleNewName.trim()}: ` : ``}${moduleName.trim()}`; + })) + .join(', ')}`; + + return `const ${wildcard ? module : `{ ${rewrite} }`} = require('${from}')`; + }); +} + + +const requireStatement = /require\s*\(\s*(['"])([A-Z0-9_~\\\/\.]+)\s*\1\)/gi; + +export function rewriteModulePathInRequire(code: string, basePath: string, filePath: string): string { + return code.replace(requireStatement, (str, par, name) => { + let path = Path.join(basePath, 'node_modules', name); + + if (Fs.existsSync(path) === false) // 不是第三方模块 + path = (name.indexOf('/') === -1) // 是否标准模块 + ? name + : (filePath) // 文件路径是否存在 + ? Path.join(Path.dirname(filePath), name) + : Path.join(basePath, name); + + return `require('${path.replace(/\\/g, '\\\\')}')`; + }); +} + +const linBreak = /\r?\n/; +const consoleLogCall = /console\s*\.(log|debug|error)\(/g; + +export function rewriteConsoleToAppendLineNumber(code: string): string { + let num = 0, + out = []; + + for (let line of code.split(linBreak)) { + out.push(line.replace(consoleLogCall, `global['\`console\`'].$1(${num++}, `)); + } + + return out.join('\n'); +} + +const lineBreakInChainCall = /([\n\s]+)\./gi; + +export function rewriteChainCallInOneLine(code: string): string { + return code.replace(lineBreakInChainCall, (str, whitespace) => `/*\`${whitespace.split(linBreak).length - 1}\`*/.`); +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index faad86b..1b35600 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,568 +3,86 @@ // Import the module and reference it with the alias vscode in your code below import { commands, - Disposable, ExtensionContext, TextDocument, - Uri, - ViewColumn, + TextEditor, window, workspace, - WorkspaceEdit, - TextEditor, - Position, - Range, - DecorationOptions, - DecorationRangeBehavior, - MarkdownString + ViewColumn, + Disposable, } from 'vscode'; -import { EventEmitter } from 'events'; -import * as Repl from 'repl'; -import * as Path from 'path'; -import * as Fs from 'fs'; -import * as Util from 'util'; -import { Writable, Readable } from 'stream'; +import ReplClient from './ReplClient'; -let replExt: ReplExtension; -let outputWindow = window.createOutputChannel("NodeJs REPL"); -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed +let outputWindow = window.createOutputChannel("NodeJs REPL"); +let registeredCommands: Disposable[]; +let client = new ReplClient(outputWindow); +let editor: TextEditor; +let doc: TextDocument; + +/** + * this method is called when your extension is activated + * your extension is activated the very first time the command is executed + */ export function activate(context: ExtensionContext) { - context.subscriptions.push(commands.registerCommand('extension.nodejsRepl', async () => { - try { - if(!replExt || replExt.disposed) - replExt = new ReplExtension(); - - await replExt.close(); - await replExt.showEditor(); - - return; - } - catch(err) { - outputWindow.appendLine(err); - } - })); - - context.subscriptions.push(commands.registerCommand('extension.nodejsReplCurrent', async () => { - try { - if(!replExt || replExt.disposed) - replExt = new ReplExtension(); - - await replExt.close(); - await replExt.openDocument(true); - await replExt.showEditor(); - await replExt.interpret(); - - return; - } - catch(err) { - outputWindow.appendLine(err); - } - })); - - context.subscriptions.push(commands.registerCommand('extension.nodejsReplClose', async () => { - try { - if(replExt && !replExt.disposed) { - await replExt.close(); - - replExt.dispose() - } - } - catch(err) { - outputWindow.appendLine(err); - } - })); - - - (async () => { - try { - for(let document of workspace.textDocuments) { - if(document.fileName.indexOf('Untitled-') >= 0 && document.languageId == 'javascript') { - if(!replExt || replExt.disposed) - replExt = new ReplExtension(); + registeredCommands = [ + commands.registerCommand('extension.nodejsRepl', async () => { + try { + doc = await workspace.openTextDocument({ content: '', language: 'javascript' }); + editor = await window.showTextDocument(doc, ViewColumn.Active); - await replExt.showEditor(document); - await replExt.interpret(); - - break; - } + client.init(editor, doc); + await client.interpret(); } - } - catch(err) { - outputWindow.appendLine(err); - } - })() - - -} - -// this method is called when your extension is deactivated -export function deactivate() { - replExt.dispose(); - outputWindow.dispose(); -} - -class ReplExtension { - private changeEventDisposable: Disposable; - private changeActiveDisposable: Disposable; - - private repl: NodeRepl; - - private editor: TextEditor; - private document: TextDocument; - - private interpretTimer: NodeJS.Timer = null; - - // create a decorator type that we use to decorate small numbers - private resultDecorationType = window.createTextEditorDecorationType({ - rangeBehavior: DecorationRangeBehavior.ClosedClosed, - light: { - - }, - dark: { - - } - }); - - private resultDecorators: Map = new Map(); - - constructor() { - this.init(); - } - - public get disposed() { - return this.repl == null; - } - - public dispose() { - if(outputWindow) - outputWindow.appendLine(`Disposing REPL extension`) - - this.changeActiveDisposable.dispose(); - this.changeEventDisposable.dispose(); - - this.repl = null; - } - - public async init() { - outputWindow.appendLine(`Initializing REPL extension with Node ${process.version}`) - outputWindow.appendLine(` Warning; Be careful with CRUD operations since the code is running multiple times in REPL.`) - - this.changeActiveDisposable = window.onDidChangeActiveTextEditor(async (editor) => { - if(this.editor && this.editor.document === editor.document) { - this.interpret(); + catch (err) { + outputWindow.appendLine(err); } - }); - - workspace.onDidCloseTextDocument(async (document) => { - if(this.editor && this.editor.document == document) - this.dispose(); - }); - - this.changeEventDisposable = workspace.onDidChangeTextDocument(async (event) => { - try - { - if (!this.editor || this.editor.document !== event.document ) { + }), + commands.registerCommand('extension.nodejsReplCurrent', async () => { + try { + if (window.activeTextEditor && window.activeTextEditor.document.languageId === 'javascript') { + doc = window.activeTextEditor.document; + editor = await window.showTextDocument(doc, ViewColumn.Active); + + } else { + window.showErrorMessage('[Node.js REPL] Selected document is not Javascript, unable to start REPL here.'); return; } - let change = event.contentChanges[0], - text = change.text; - - if(/\n/.test(text) == false && change.range.isSingleLine == true) - this.editor.setDecorations(this.resultDecorationType, Array.from(this.resultDecorators.values()).filter(d => { - return this.editor.selection.active.line != d.range.start.line; - })); - - if(this.interpretTimer) - clearTimeout(this.interpretTimer); - - if(text.indexOf(';') >= 0 || text.indexOf('\n') >= 0 || (text == '' && change.range.isSingleLine == false)) { - await this.interpret(); - } - else { - this.interpretTimer = setTimeout(async () => { - await this.interpret(); - }, 2000); - } - } - catch(err) { + client.init(editor, doc); + await client.interpret(); + } catch (err) { outputWindow.appendLine(err); } - }); - } - - public async interpret() { - try { - await this.showEditor(); - - let code = this.editor.document.getText(); - - this.resultDecorators.clear(); - - new NodeRepl() - .on('exit', () => { - if(this.resultDecorators.size == 0) - this.editor.setDecorations(this.resultDecorationType, []); - }) - .on('output', (result) => { - let decorator: DecorationOptions, - color: string; - - switch(result.type) { - case 'result': color = 'green'; break; - case 'error': color = 'red'; break; - case 'console': color = '#457abb'; break; - } - - if((decorator = this.resultDecorators.get(result.line)) == null) - { - let length = this.getTextAtLine(result.line - 1).length, - startPos = new Position(result.line - 1, length + 1 ), - endPos = new Position(result.line - 1, length + 1); - - this.resultDecorators.set(result.line, decorator = { renderOptions: { before: { margin: '0 0 0 1em', contentText: '', color: color } }, range: new Range(startPos, endPos) }); - } - - decorator.renderOptions.before.color = color; - decorator.renderOptions.before.contentText = ` ${result.text}`; - - decorator.hoverMessage = new MarkdownString(result.type.slice(0, 1).toUpperCase() + result.type.slice(1)); - decorator.hoverMessage.appendCodeblock( - result.type == 'console' ? result.value.join('\n') : result.value || result.text, - "javascript" - ); - - this.editor.setDecorations(this.resultDecorationType, Array.from(this.resultDecorators.values())); - }) - .interpret(code); - } - catch(ex) { - outputWindow.appendLine(ex); - - return false; - } - - return true; - } - - public async close(): Promise { - this.document = null; - } - - public async show(): Promise { - await this.showEditor(); - } - - public async openDocument(currentWindow: boolean = false) { - if(this.document == null || this.document.isClosed == true) { - if(currentWindow && window.activeTextEditor) { - if(window.activeTextEditor.document.languageId == 'javascript') { - return this.document = window.activeTextEditor.document; - } - else { - window.showErrorMessage('Selected document is not Javascript, unable to start REPL here'); - - return null; + }), + commands.registerCommand('extension.nodejsReplClose', async () => { + try { + if (!client.isClosed) { + client.close(); + editor = null; + doc = null; } + } catch (err) { + outputWindow.appendLine(err); } + }), + ]; - this.document = await workspace.openTextDocument({ content: '', language: 'javascript' }); - } - - return this.document; - } - - public async showEditor(document: TextDocument = undefined) { - if(document) - this.document = document; - - this.editor = await window.showTextDocument(this.document || await this.openDocument(), ViewColumn.Active, ); - - return this.editor; - } - - private getTextAtLine(line: number) { - let startPos = new Position(line, 0), - endPos = new Position(line, 37768); - - return this.editor.document.getText(new Range(startPos, endPos)); - } + for (let cmd of registeredCommands) + context.subscriptions.push(cmd); } -class NodeRepl extends EventEmitter { - private replEval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: string) => void) => void; - private output: Map = new Map(); - - private basePath: string; - - constructor() { - super(); - - if(workspace && Array.isArray(workspace.workspaceFolders)) { - let doc = window.activeTextEditor.document; - this.basePath = (doc.isUntitled) - ? workspace.workspaceFolders[0].uri.fsPath - : workspace.getWorkspaceFolder(Uri.file(doc.fileName)).uri.fsPath; - - outputWindow.appendLine(`[${new Date().toLocaleTimeString()}] working at: ${this.basePath}`); - } - } - - public async interpret(code: string) { - try { - outputWindow.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`); - - let inputStream = new Readable({ - read: () => { } - }), - lineCount = 0, - outputCount = 0, - requireIdx = 0; - - this.output.clear(); // new interpretation, clear all outputs - - inputStream.push(""); - - let repl = Repl.start({ - prompt: '', - input: inputStream, - output: new Writable({ - write: (chunk, enc, cb) => { - let out = chunk.toString().trim(); - switch(out) { - case 'undefined': - case '...': - case '': - break; - - default: - let match: RegExpExecArray; - - if( (match = /(\w+:\s.*)\n\s*at\s/gi.exec(out)) != null) { - this.output.set(lineCount, {line: lineCount, type: 'error', text: match[1], value: match[1]}); - this.emit('output', {line: lineCount, type: 'error', text: match[1], value: match[1]}); - - outputWindow.appendLine(` ${match[1]}\n\tat line ${lineCount}`); - } - else if( (match = /`\{(\d+)\}`([\s\S]*)/gi.exec(out)) != null) { - let output = this.output.get( Number(match[1]) ); - if( output == null) - this.output.set(Number(match[1]), output = { line: Number(match[1]), type: 'console', text: '', value: [] }); - - output.text += (output.text == '' ? '' : ', ') + (match[2] || '').replace(/\r\n|\n/g, ' '); - output.value.push(match[2]); - - this.emit('output', output); - - outputWindow.appendLine(` ${match[2]}`); - } - else { - outputWindow.appendLine(` ${out}`); - } - - break; - } - cb(); - } - }), - writer: (out) => { - if(out == null) - return; - } - }) - - if(this.replEval == null) - this.replEval = (repl).eval; // keep a backup of original eval - - // nice place to read the result in sequence and inject it in the code - (repl).eval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => { - lineCount++; - - this.replEval(cmd, context, filename, (err, result: any) => { - let regex = /\/\*`(\d+)`\*\//gi, - match: RegExpExecArray - - if(!err) { - while((match = regex.exec(cmd)) != null) - lineCount += Number(match[1]); - - let currentLine = lineCount - - this.formatOutput(result) // we can't await this since callback of eval needs to be called synchronious - .then(output => { - if(output) { - this.output.set(currentLine, {line: currentLine, type: output.type, text: output.text, value: output.value}); - this.emit('output', { line: currentLine, type: output.type, text: `${output.text}`, value: output.value}); - - if(output.type == 'error') - outputWindow.appendLine(` ${err.name}: ${err.message}\n\tat line ${currentLine}`); - } - }) - } - - cb(err, result); - }) - } - - Object.defineProperty(repl.context, '_console', { - - value: function(line: number) { - let _log = function (text, ...args) { - repl.context.console.log(`\`{${line}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args); - } - - return Object.assign({}, repl.context.console, { - log: _log, - warn: _log, - error: _log - }) - } - - }); - - code = this.rewriteImport(code); - code = this.rewriteRequire(code); - code = this.rewriteConsole(code); - code = this.rewriteMethod(code); - - //inputStream.push(`require("${Path.join(this.basePath, "node_modules", "ts-node").replace(/\\/g, '\\\\')}").register({});\n`) - for(let line of code.split(/\r\n|\n/)) - { - inputStream.push(`${line}\n`); // tell the REPL about the line of code to see if there is any result coming out of it - } - - inputStream.push(`.exit\n`); - inputStream.push(null); - - repl.on('exit', () => { - setTimeout(() => this.emit('exit'), 100); - }) - } - catch(ex) - { - outputWindow.appendLine(ex); - } - } - - private async formatOutput(result: any): Promise<{type: 'result' | 'error', text: string, value: any }> { - switch(typeof(result)) - { - case 'undefined': - break; - - case 'object': - if(result.constructor && result.constructor.name == 'Promise' && result.then) { - try { - let ret = await Promise.resolve(result) - - return this.formatOutput(ret) - } - catch(ex) { - return { - type: 'error', - text: `${ex.name}: ${ex.message}`, - value: ex - } - } - } - - let text; - - if(Array.isArray(result)) { - text = JSON.stringify(result); - } - else { - text = JSON.stringify(result, null, "\t").replace(/\n/g, " "); - } - - return { - type: 'result', - text: text, - value: result - } - - default: - return { - type: 'result', - text: result.toString().replace(/(\r\n|\n)/g, ' '), - value: result - } - } - - } - - private rewriteImport(code: string): string { - let regex = /import\s*(?:(\*\s+as\s)?([\w-_]+),?)?\s*(?:\{([^\}]+)\})?\s+from\s+["']([^"']+)["']/gi, - match; - - return code.replace(regex, (str: string, wildcard: string, module: string, modules: string, from) => { - let rewrite = '', - path; - - if(module) - rewrite += `${rewrite == '' ? '' : ', '}default: ${module} `; - - if(modules) - rewrite += `${rewrite == '' ? '' : ', '}${modules - .split(',') - .map(r => r.replace(/\s*([\w-_]+)(?:\s+as\s+([\w-_]))?\s*/gi, (str, moduleName: string, moduleNewName: string) => { - return `${moduleNewName ? `${moduleNewName.trim()}: ` : ``}${moduleName.trim()}`; - })) - .join(', ')}`; - - return `const ${wildcard ? module : `{ ${rewrite} }`} = require('${from}')`; - }); - } - - private rewriteRequire(code: string): string { - let regex = /require\s*\(\s*(['"])([A-Z0-9_~\\\/\.]+)\s*\1\)/gi, - match: RegExpExecArray; - - return code.replace(regex, (str, par, name) => { - let doc = window.activeTextEditor.document; - let path = Path.join(this.basePath, 'node_modules', name); - - if(Fs.existsSync(path) === false) - path = (name.indexOf('/') === -1) - ? name - : (doc.isUntitled) - ? Path.join(this.basePath, name) - : Path.join(Path.dirname(doc.fileName), name); - - return `require('${path.replace(/\\/g, '\\\\')}')`; - }); - } - - private rewriteConsole(code: string): string { - let num = 0, - out = []; - - for(let line of code.split(/\r\n|\n/)) - { - out.push(line.replace(/console/g, `_console(${++num})`)); - } - - return out.join('\n'); - } - - private rewriteMethod(code: string): string { - let regex = /([\n\s]+)\./gi, - match; - - return code.replace(regex, (str: string, whitespace) => { - return `/*\`${whitespace.split(/\r\n|\n/).length - 1}\`*/.` - }); - } - - private isRecoverableError(error) { - if (error.name === 'SyntaxError') { - return /^(Unexpected end of input|Unexpected token)/.test(error.message); - } - - return false; - } +/** + * this method is called when your extension is deactivated + */ +export function deactivate() { + outputWindow.dispose(); + client.dispose(); + editor = null; + doc = null; + + for (let cmd of registeredCommands) + cmd.dispose(); } \ No newline at end of file From c3f52aa3bc4842d1a972f52b92a1b08796c5694c Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Tue, 4 Dec 2018 14:20:50 +0800 Subject: [PATCH 03/12] Use child process of User's Node.js as REPL. --- .editorconfig | 10 ++++ src/Decorator.ts | 62 ++++++++++++--------- src/ReplClient.ts | 27 +++++---- src/ReplServer.ts | 139 ++++++++++++++++++++-------------------------- src/code.ts | 2 + 5 files changed, 125 insertions(+), 115 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ca120b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true diff --git a/src/Decorator.ts b/src/Decorator.ts index cda200c..24ee512 100644 --- a/src/Decorator.ts +++ b/src/Decorator.ts @@ -9,6 +9,8 @@ import { window, } from "vscode"; +import * as Util from 'util'; + // create a decorator type that we use to decorate small numbers const resultDecorationType = window.createTextEditorDecorationType({ @@ -17,10 +19,14 @@ const resultDecorationType = window.createTextEditorDecorationType({ dark: {}, }); const colorMap = { - Result: 'green', - Error: 'red', - Console: '#457abb', + 'Value of Expression': 'green', + 'Console': '#457abb', + 'Error': 'red', } +const inspectOptions = { maxArrayLength: null, depth: null }; + +type Data = { line: number, type: 'Expression' | 'Terminal', value: any }; +type Result = { line: number, type: 'Value of Expression' | 'Console' | 'Error', text: string, value: any }; export default class Decorator { private editor: TextEditor; @@ -37,11 +43,13 @@ export default class Decorator { this.decorators = []; } - async update(result: any) { + async update(data: Data) { + + let result = (data.type === 'Expression') + ? await this.formatExpressionValue(data) + : this.formatTerminalOutput(data); - result = (result.type === 'Expression') - ? await this.formatExpressionValue(result) - : this.formatTerminalOutput(result); + if (!result) return; let decorator: DecorationOptions; @@ -63,60 +71,60 @@ export default class Decorator { decorator.hoverMessage = new MarkdownString(result.type); decorator.hoverMessage.appendCodeblock( - result.type == 'Console' ? result.value.join('\n') : result.value || result.text, + result.type === 'Console' ? result.value.join('\n') : result.value || result.text, 'javascript' ); this.decorateAll(); } - private async formatExpressionValue(data: any): Promise<{ line?: number, type: 'Result' | 'Error', text: string, value: any }> { + private async formatExpressionValue(data: Data): Promise { let result = data.value; switch (typeof result) { case 'undefined': - break; + return null; case 'object': if (result.constructor && result.constructor.name === 'Promise' && result.then) { try { - return this.formatExpressionValue(Object.assign(data, { value: await Promise.resolve(result) })); - } catch (ex) { + let value = await Promise.resolve(result); + return value ? this.formatExpressionValue(Object.assign(data, { value })) : null; + } catch (error) { return { line: data.line, type: 'Error', - text: `${ex.name}: ${ex.message}`, - value: ex, + text: `${error.name}: ${error.message}`, + value: error, } } } + let string = Util.inspect(result, inspectOptions); return { line: data.line, - type: 'Result', - text: (Array.isArray(result)) - ? JSON.stringify(result) - : JSON.stringify(result, null, '\t').replace(/\n/g, ' '), - value: result, + type: 'Value of Expression', + text: string, + value: string, } default: return { line: data.line, - type: 'Result', + type: 'Value of Expression', text: result.toString().replace(/\r?\n/g, ' '), value: result, } } } - private formatTerminalOutput(data: any): { line?: number, type: 'Console' | 'Error', text: string, value: any } { - let lineCount = data.line; - let out = data.text; + private formatTerminalOutput(data: Data): Result { + let out = data.value as string; let match: RegExpExecArray; - if ((match = /(\w+:\s.*)\n\s*at\s/gi.exec(out)) != null) { - this.outputChannel.appendLine(` ${match[1]}\n\tat line ${lineCount}`); - return { line: lineCount, type: 'Error', text: match[1], value: match[1] } + if ((match = /^(Error:\s.*)(?:\n\s*at\s)?/g.exec(out)) != null) { + this.outputChannel.appendLine(` ${match[1]}\n\tat line ${data.line}`); + + return { line: data.line, type: 'Error', text: match[1], value: match[1] }; } - else if ((match = /`\{(\d+)\}`([\s\S]*)/gi.exec(out)) != null) { + else if ((match = /^`\{(\d+)\}`([\s\S]*)$/g.exec(out)) != null) { let line = +match[1]; let msg = match[2] || ''; diff --git a/src/ReplClient.ts b/src/ReplClient.ts index bb58e0a..7d21939 100644 --- a/src/ReplClient.ts +++ b/src/ReplClient.ts @@ -16,7 +16,7 @@ import { } from "./code"; import Decorator from "./Decorator"; -import REPLServer from "./ReplServer"; +import { spawn, ChildProcess } from "child_process"; export default class ReplClient { @@ -29,7 +29,7 @@ export default class ReplClient { private basePath: string; private filePath: string; - private repl: REPLServer; + private repl: ChildProcess; private editingTimer: NodeJS.Timer = null; private afterEditTimer: NodeJS.Timer = null; @@ -59,12 +59,12 @@ export default class ReplClient { let currentLine = this.editor.selection.active.line; this.decorator.decorateExcept(currentLine); } - + if (this.afterEditTimer) clearTimeout(this.afterEditTimer); if (this.editingTimer) clearTimeout(this.editingTimer); if (text.lastIndexOf(';') >= 0 || text.lastIndexOf('\n') >= 0 || (text === '' && change.range.isSingleLine === false)) - this.editingTimer = setTimeout(async () => await this.interpret(), 300); + this.editingTimer = setTimeout(async () => await this.interpret(), 600); else this.afterEditTimer = setTimeout(async () => await this.interpret(), 1500); } @@ -75,7 +75,7 @@ export default class ReplClient { } init(editor: TextEditor, doc: TextDocument) { - this.outputChannel.appendLine(`Initializing REPL extension with Node ${process.version}`); + this.outputChannel.appendLine(`Initializing REPL extension.`); this.outputChannel.appendLine(` Warning; Be careful with CRUD operations since the code is running multiple times in REPL.`); this.editor = editor; @@ -96,7 +96,14 @@ export default class ReplClient { try { this.decorator.init(this.editor); + this.repl = spawn('node', [`${__dirname}/replServer.js`], { cwd: this.basePath, stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }) + .on('message', async result => await this.decorator.update(result)) + .on('error', err => this.outputChannel.appendLine(`[Repl Server] ${err.message}`)); + let code = this.editor.document.getText(); + + this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`); + // TODO: typescript REPL // code = `require("${Path.join(this.basePath, "node_modules/ts-node").replace(/\\/g, '\\\\')}").register({});\n${code}`; code = rewriteImportToRequire(code); @@ -104,11 +111,7 @@ export default class ReplClient { code = rewriteConsoleToAppendLineNumber(code); code = rewriteChainCallInOneLine(code); - this.repl = new REPLServer(this.outputChannel) - .on('output', async result => await this.decorator.update(result)); - - this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`); - this.repl.interpret(code); + this.repl.send({ code }); } catch (ex) { this.outputChannel.appendLine(ex); @@ -120,6 +123,8 @@ export default class ReplClient { this.outputChannel.appendLine(`Disposing REPL server.`); this.editor = null; + + this.repl.send({ operation: 'exit' }); this.repl = null; } @@ -132,6 +137,8 @@ export default class ReplClient { this.closeTextDocumentDisposable.dispose(); this.changeEventDisposable.dispose(); this.editor = null; + + this.repl.send({ operation: 'exit' }); this.repl = null; } } \ No newline at end of file diff --git a/src/ReplServer.ts b/src/ReplServer.ts index 41e10fd..e2ba468 100644 --- a/src/ReplServer.ts +++ b/src/ReplServer.ts @@ -1,92 +1,75 @@ -import { OutputChannel } from "vscode"; -import { EventEmitter } from "events"; -import { Readable, Writable } from "stream"; import * as Repl from 'repl'; import * as Util from 'util'; +import { Writable, Readable } from 'stream'; + + +type ReplServer = Repl.REPLServer & { inputStream: Readable, eval: ReplEval }; +type ReplEval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => void; + +let lineCount = 0; + +const server = Repl.start({ + prompt: '', + input: new Readable({ read: () => { } }), + output: new Writable({ + write: (chunk, encoding, callback) => { + let out = chunk.toString().trim(); + switch (out) { + case '...': break; + case '': break; + default: + process.send({ line: lineCount, type: 'Terminal', value: out }); + break; + } + callback(); + } + }), + ignoreUndefined: true, +}) as ReplServer; -export default class NodeRepl extends EventEmitter { - private replEval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: string) => void) => void; - - constructor(private outputChannel: OutputChannel) { - super(); - } - - public async interpret(code: string) { - try { - let inputStream = new Readable({ read: () => { } }), - lineCount = 0; - - let repl = Repl.start({ - prompt: '', - input: inputStream, - writer: (out) => { - if (out == null) return; - }, - output: new Writable({ - write: (chunk, encoding, cb) => { - let out = chunk.toString().trim(); - switch (out) { - case 'undefined': - case '...': - case '': - break; - - default: - this.emit('output', { line: lineCount, type: 'Terminal', text: out }) - break; - } - cb(); - } - }), - }) as Repl.REPLServer & { eval: (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => void }; - - if (this.replEval == null) - this.replEval = repl.eval; // keep a backup of original eval - - // nice place to read the result in sequence and inject it in the code - repl.eval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => { - - this.replEval(cmd, context, filename, (err, result: any) => { - let regex = /\/\*`(\d+)`\*\//gi, - match: RegExpExecArray - if (!err) { - while ((match = regex.exec(cmd)) != null) - lineCount += +match[1]; +const originEval = server.eval; // keep a backup of original eval +const lineNumber = /\/\*`(\d+)`\*\//gi; - this.emit('output', { line: lineCount, type: 'Expression', value: result }); - } +// nice place to read the result in sequence and inject it in the code +server.eval = (cmd, context, filename, callback) => { + originEval(cmd, context, filename, (err, result) => { + let match: RegExpExecArray; - cb(err, result); - }); + while ((match = lineNumber.exec(cmd)) != null) + lineCount += +match[1]; - lineCount++; - } + if (result) + process.send({ line: lineCount, type: 'Expression', value: result }); - const originLog = repl.context.console.log; - const appendLineLog = (lineNumber: number, text: any, ...args: any[]) => { - originLog(`\`{${lineNumber}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args); - } - Object.defineProperty(repl.context, '`console`', { - value: { - log: appendLineLog, - debug: appendLineLog, - error: appendLineLog, - } - }) + callback(err, result); + }); - for (let line of code.split(/\r?\n/)) { - // tell the REPL about the line of code to see if there is any result coming out of it - inputStream.push(`${line}\n`); - } + lineCount++; +} - inputStream.push(`.exit\n`); - inputStream.push(null); +const originLog = server.context.console.log; +const appendLineLog = (lineNumber: number, text: any, ...args: any[]) => { + originLog(`\`{${lineNumber}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args); +} +Object.defineProperty(server.context, '`console`', { + value: { + log: appendLineLog, + debug: appendLineLog, + error: appendLineLog, + } +}); - repl.on('exit', () => setTimeout(() => this.emit('exit'), 100)); - } catch (ex) { - this.outputChannel.appendLine(ex); +process.on('message', data => { + if (data.code) { + try { + for (let line of data.code.split('\n')) + server.inputStream.push(line + '\n'); + } catch (error) { + process.emit('error', error); } + } else if (data.operation === 'exit') { + process.exit(); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/code.ts b/src/code.ts index 824acbe..c56b891 100644 --- a/src/code.ts +++ b/src/code.ts @@ -42,6 +42,7 @@ export function rewriteModulePathInRequire(code: string, basePath: string, fileP }); } + const linBreak = /\r?\n/; const consoleLogCall = /console\s*\.(log|debug|error)\(/g; @@ -56,6 +57,7 @@ export function rewriteConsoleToAppendLineNumber(code: string): string { return out.join('\n'); } + const lineBreakInChainCall = /([\n\s]+)\./gi; export function rewriteChainCallInOneLine(code: string): string { From 4482f88d37004aa5078c12cb2ebd82c10204aaf3 Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Tue, 4 Dec 2018 14:47:07 +0800 Subject: [PATCH 04/12] Fix cannot show results after changed active TextEditor. --- src/Decorator.ts | 1 + src/ReplClient.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Decorator.ts b/src/Decorator.ts index 24ee512..905d81c 100644 --- a/src/Decorator.ts +++ b/src/Decorator.ts @@ -23,6 +23,7 @@ const colorMap = { 'Console': '#457abb', 'Error': 'red', } +// TODO: if Node's Version of VSCode >=9.9, use option `compact` const inspectOptions = { maxArrayLength: null, depth: null }; type Data = { line: number, type: 'Expression' | 'Terminal', value: any }; diff --git a/src/ReplClient.ts b/src/ReplClient.ts index 7d21939..2f7cd57 100644 --- a/src/ReplClient.ts +++ b/src/ReplClient.ts @@ -38,8 +38,10 @@ export default class ReplClient { this.decorator = new Decorator(outputChannel); this.changeActiveDisposable = window.onDidChangeActiveTextEditor(async (editor) => { - if (this.editor === editor) + if (this.editor && this.editor.document === editor.document) { + this.init(editor, editor.document); this.interpret(); + } }); this.closeTextDocumentDisposable = workspace.onDidCloseTextDocument(async (document) => { @@ -122,8 +124,6 @@ export default class ReplClient { if (this.outputChannel) this.outputChannel.appendLine(`Disposing REPL server.`); - this.editor = null; - this.repl.send({ operation: 'exit' }); this.repl = null; } From 8a8f7188f8ddedebd7faf4aa8a2dbf18243544fe Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Tue, 4 Dec 2018 15:12:44 +0800 Subject: [PATCH 05/12] Change file name to kebab-case from CamelCase. --- src/code.ts | 9 ++++----- src/{Decorator.ts => decorator.ts} | 0 src/extension.ts | 4 ++-- src/{ReplClient.ts => repl-client.ts} | 2 +- src/{ReplServer.ts => repl-server.ts} | 0 5 files changed, 7 insertions(+), 8 deletions(-) rename src/{Decorator.ts => decorator.ts} (100%) rename src/{ReplClient.ts => repl-client.ts} (99%) rename src/{ReplServer.ts => repl-server.ts} (100%) diff --git a/src/code.ts b/src/code.ts index c56b891..ae77872 100644 --- a/src/code.ts +++ b/src/code.ts @@ -8,10 +8,9 @@ export function rewriteImportToRequire(code: string): string { return code.replace(importStatement, (str, wildcard: string, module: string, modules: string, from: string) => { let rewrite = ''; - // 导入所有 if (module) rewrite += `${rewrite === '' ? '' : ', '}default: ${module} `; - // 解构导入 + if (modules) rewrite += `${rewrite === '' ? '' : ', '}${modules .split(',') @@ -31,10 +30,10 @@ export function rewriteModulePathInRequire(code: string, basePath: string, fileP return code.replace(requireStatement, (str, par, name) => { let path = Path.join(basePath, 'node_modules', name); - if (Fs.existsSync(path) === false) // 不是第三方模块 - path = (name.indexOf('/') === -1) // 是否标准模块 + if (Fs.existsSync(path) === false) + path = (name.indexOf('/') === -1) ? name - : (filePath) // 文件路径是否存在 + : (filePath) ? Path.join(Path.dirname(filePath), name) : Path.join(basePath, name); diff --git a/src/Decorator.ts b/src/decorator.ts similarity index 100% rename from src/Decorator.ts rename to src/decorator.ts diff --git a/src/extension.ts b/src/extension.ts index 1b35600..b3df0f3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,7 +12,7 @@ import { Disposable, } from 'vscode'; -import ReplClient from './ReplClient'; +import ReplClient from './repl-client'; let outputWindow = window.createOutputChannel("NodeJs REPL"); @@ -82,7 +82,7 @@ export function deactivate() { client.dispose(); editor = null; doc = null; - + for (let cmd of registeredCommands) cmd.dispose(); } \ No newline at end of file diff --git a/src/ReplClient.ts b/src/repl-client.ts similarity index 99% rename from src/ReplClient.ts rename to src/repl-client.ts index 2f7cd57..045ea62 100644 --- a/src/ReplClient.ts +++ b/src/repl-client.ts @@ -15,7 +15,7 @@ import { rewriteChainCallInOneLine } from "./code"; -import Decorator from "./Decorator"; +import Decorator from "./decorator"; import { spawn, ChildProcess } from "child_process"; diff --git a/src/ReplServer.ts b/src/repl-server.ts similarity index 100% rename from src/ReplServer.ts rename to src/repl-server.ts From e9cb02226c8e3ff2a1b8e3a1a54cf9dcc6498c86 Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Tue, 4 Dec 2018 18:32:01 +0800 Subject: [PATCH 06/12] Fix cannot start child process; Fix cannot output error message except `Error`; Change some local variables to global variable; Change `tsconfig.json` to remove comment in output; Change name of some variables. --- src/decorator.ts | 6 +++--- src/repl-client.ts | 7 ++++++- tsconfig.json | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/decorator.ts b/src/decorator.ts index 905d81c..a128b45 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -18,7 +18,7 @@ const resultDecorationType = window.createTextEditorDecorationType({ light: {}, dark: {}, }); -const colorMap = { +const colorOfType = { 'Value of Expression': 'green', 'Console': '#457abb', 'Error': 'red', @@ -67,7 +67,7 @@ export default class Decorator { this.decorators.push(decorator); } - decorator.renderOptions.before.color = colorMap[result.type]; + decorator.renderOptions.before.color = colorOfType[result.type]; decorator.renderOptions.before.contentText = ` ${result.text}`; decorator.hoverMessage = new MarkdownString(result.type); @@ -120,7 +120,7 @@ export default class Decorator { let out = data.value as string; let match: RegExpExecArray; - if ((match = /^(Error:\s.*)(?:\n\s*at\s)?/g.exec(out)) != null) { + if ((match = /(\w*Error:\s.*)(?:\n\s*at\s)?/g.exec(out)) != null) { this.outputChannel.appendLine(` ${match[1]}\n\tat line ${data.line}`); return { line: data.line, type: 'Error', text: match[1], value: match[1] }; diff --git a/src/repl-client.ts b/src/repl-client.ts index 045ea62..07136f5 100644 --- a/src/repl-client.ts +++ b/src/repl-client.ts @@ -18,6 +18,9 @@ import { import Decorator from "./decorator"; import { spawn, ChildProcess } from "child_process"; +const serverArguments = [`${__dirname}/repl-server.js`]; + +const stdioOptions = ['ignore', 'ignore', 'ignore', 'ipc']; export default class ReplClient { private changeEventDisposable: Disposable; @@ -96,9 +99,11 @@ export default class ReplClient { async interpret() { try { + if (!this.isClosed) this.close(); + this.decorator.init(this.editor); - this.repl = spawn('node', [`${__dirname}/replServer.js`], { cwd: this.basePath, stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }) + this.repl = spawn('node', serverArguments, { cwd: this.basePath, stdio: stdioOptions }) .on('message', async result => await this.decorator.update(result)) .on('error', err => this.outputChannel.appendLine(`[Repl Server] ${err.message}`)); diff --git a/tsconfig.json b/tsconfig.json index 8a1d847..446f4e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "es6" ], "sourceMap": true, - "rootDir": "src" + "rootDir": "src", + "removeComments": true }, "exclude": [ "node_modules", From d5778d963805b2da5046c557379f20697962901e Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Tue, 4 Dec 2018 23:40:25 +0800 Subject: [PATCH 07/12] Fix cannot import module use `import {a as b} from 'c'`; Show error stack in hover message of result; Rename some variable. --- src/code.ts | 21 ++++++++------------- src/decorator.ts | 35 ++++++++++++++++------------------- src/repl-server.ts | 2 +- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/code.ts b/src/code.ts index ae77872..40b9fb0 100644 --- a/src/code.ts +++ b/src/code.ts @@ -2,24 +2,19 @@ import * as Path from 'path'; import * as Fs from 'fs'; -const importStatement = /import\s*(?:(\*\s+as\s)?([\w-_]+),?)?\s*(?:\{([^\}]+)\})?\s+from\s+["']([^"']+)["']/gi; +const importStatement = /import\s+(?:(\*\s+as\s)?([\w-_]+),?)?\s*(?:\{([^\}]+)\})?\s+from\s+["']([^"']+)["']/gi; export function rewriteImportToRequire(code: string): string { return code.replace(importStatement, (str, wildcard: string, module: string, modules: string, from: string) => { let rewrite = ''; if (module) - rewrite += `${rewrite === '' ? '' : ', '}default: ${module} `; + rewrite = `default: ${module}`; if (modules) - rewrite += `${rewrite === '' ? '' : ', '}${modules - .split(',') - .map(r => r.replace(/\s*([\w-_]+)(?:\s+as\s+([\w-_]))?\s*/gi, (str, moduleName: string, moduleNewName: string) => { - return `${moduleNewName ? `${moduleNewName.trim()}: ` : ``}${moduleName.trim()}`; - })) - .join(', ')}`; - - return `const ${wildcard ? module : `{ ${rewrite} }`} = require('${from}')`; + rewrite += (rewrite && ', ') + modules.replace(/\sas\s/g, ': '); + + return `const ${wildcard ? module : `{ ${rewrite} }`} = require('${from}');`; }); } @@ -42,14 +37,14 @@ export function rewriteModulePathInRequire(code: string, basePath: string, fileP } -const linBreak = /\r?\n/; +const lineBreak = /\r?\n/; const consoleLogCall = /console\s*\.(log|debug|error)\(/g; export function rewriteConsoleToAppendLineNumber(code: string): string { let num = 0, out = []; - for (let line of code.split(linBreak)) { + for (let line of code.split(lineBreak)) { out.push(line.replace(consoleLogCall, `global['\`console\`'].$1(${num++}, `)); } @@ -60,5 +55,5 @@ export function rewriteConsoleToAppendLineNumber(code: string): string { const lineBreakInChainCall = /([\n\s]+)\./gi; export function rewriteChainCallInOneLine(code: string): string { - return code.replace(lineBreakInChainCall, (str, whitespace) => `/*\`${whitespace.split(linBreak).length - 1}\`*/.`); + return code.replace(lineBreakInChainCall, (str, whitespace) => `/*\`${whitespace.split(lineBreak).length - 1}\`*/.`); } \ No newline at end of file diff --git a/src/decorator.ts b/src/decorator.ts index a128b45..6403b79 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -23,11 +23,11 @@ const colorOfType = { 'Console': '#457abb', 'Error': 'red', } -// TODO: if Node's Version of VSCode >=9.9, use option `compact` -const inspectOptions = { maxArrayLength: null, depth: null }; +// TODO: if Node's Version of VSCode >=9.9, use default option +const inspectOptions: NodeJS.InspectOptions = { depth: 20 }; type Data = { line: number, type: 'Expression' | 'Terminal', value: any }; -type Result = { line: number, type: 'Value of Expression' | 'Console' | 'Error', text: string, value: any }; +type Result = { line: number, type: 'Value of Expression' | 'Console' | 'Error', text: string, value: string }; export default class Decorator { private editor: TextEditor; @@ -63,7 +63,7 @@ export default class Decorator { renderOptions: { before: { margin: '0 0 0 1em' } }, range: new Range(pos, pos) }; - this.lineToDecorator.set(result.line, decorator); + this.lineToDecorator.set(line, decorator); this.decorators.push(decorator); } @@ -71,10 +71,7 @@ export default class Decorator { decorator.renderOptions.before.contentText = ` ${result.text}`; decorator.hoverMessage = new MarkdownString(result.type); - decorator.hoverMessage.appendCodeblock( - result.type === 'Console' ? result.value.join('\n') : result.value || result.text, - 'javascript' - ); + decorator.hoverMessage.appendCodeblock(result.value, result.type === 'Error' ? 'text' : 'javascript'); this.decorateAll(); } @@ -87,14 +84,14 @@ export default class Decorator { case 'object': if (result.constructor && result.constructor.name === 'Promise' && result.then) { try { - let value = await Promise.resolve(result); - return value ? this.formatExpressionValue(Object.assign(data, { value })) : null; + data.value = await (>result); + return data.value ? this.formatExpressionValue(data) : null; } catch (error) { return { line: data.line, type: 'Error', text: `${error.name}: ${error.message}`, - value: error, + value: error.stack, } } } @@ -103,7 +100,7 @@ export default class Decorator { return { line: data.line, type: 'Value of Expression', - text: string, + text: string.replace(/\n/g, ' '), value: string, } @@ -120,22 +117,22 @@ export default class Decorator { let out = data.value as string; let match: RegExpExecArray; - if ((match = /(\w*Error:\s.*)(?:\n\s*at\s)?/g.exec(out)) != null) { - this.outputChannel.appendLine(` ${match[1]}\n\tat line ${data.line}`); + if ((match = /^(\w*Error(?: \[[^\]]+\])?:\s.*)(?:\n\s*at\s)?/.exec(out)) != null) { + this.outputChannel.appendLine(` ${out}`); - return { line: data.line, type: 'Error', text: match[1], value: match[1] }; + return { line: data.line, type: 'Error', text: match[1], value: out }; } - else if ((match = /^`\{(\d+)\}`([\s\S]*)$/g.exec(out)) != null) { + else if ((match = /^`\{(\d+)\}`([\s\S]*)$/.exec(out)) != null) { let line = +match[1]; let msg = match[2] || ''; let output = this.lineToOutput.get(line); if (output == null) { - this.lineToOutput.set(+match[1], output = { line, type: 'Console', text: '', value: [] }); + this.lineToOutput.set(line, output = { line, type: 'Console', text: '', value: '' }); } - output.text += (output.text === '' ? '' : ', ') + msg.replace(/\r?\n/g, ' '); - output.value.push(msg); + output.text += (output.text && ', ') + msg.replace(/\r?\n/g, ' '); + output.value += (output.value && '\n') + msg; this.outputChannel.appendLine(` ${msg}`); return output; diff --git a/src/repl-server.ts b/src/repl-server.ts index e2ba468..9df8d46 100644 --- a/src/repl-server.ts +++ b/src/repl-server.ts @@ -30,7 +30,7 @@ const server = Repl.start({ const originEval = server.eval; // keep a backup of original eval -const lineNumber = /\/\*`(\d+)`\*\//gi; +const lineNumber = /\/\*`(\d+)`\*\//g; // nice place to read the result in sequence and inject it in the code server.eval = (cmd, context, filename, callback) => { From 3aeea2f5c65b074c4bb7a97cdc4df42e5f968035 Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Wed, 5 Dec 2018 19:07:58 +0800 Subject: [PATCH 08/12] Fix results different with REPL outputs; Remove useless operations. --- src/code.ts | 5 +- src/decorator.ts | 128 +++++++++++++++------------------------------ src/repl-client.ts | 5 +- src/repl-server.ts | 63 +++++++++------------- 4 files changed, 71 insertions(+), 130 deletions(-) diff --git a/src/code.ts b/src/code.ts index 40b9fb0..6f3b0cd 100644 --- a/src/code.ts +++ b/src/code.ts @@ -38,14 +38,15 @@ export function rewriteModulePathInRequire(code: string, basePath: string, fileP const lineBreak = /\r?\n/; -const consoleLogCall = /console\s*\.(log|debug|error)\(/g; +const consoleLogCall = /console\s*\.(debug|error|info|log|warn)\(/g; export function rewriteConsoleToAppendLineNumber(code: string): string { let num = 0, out = []; for (let line of code.split(lineBreak)) { - out.push(line.replace(consoleLogCall, `global['\`console\`'].$1(${num++}, `)); + out.push(line.replace(consoleLogCall, (str, method) => `global['\`console\`'].${method}(${num}, `)); + num++; } return out.join('\n'); diff --git a/src/decorator.ts b/src/decorator.ts index 6403b79..5f2df06 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -9,8 +9,6 @@ import { window, } from "vscode"; -import * as Util from 'util'; - // create a decorator type that we use to decorate small numbers const resultDecorationType = window.createTextEditorDecorationType({ @@ -23,10 +21,7 @@ const colorOfType = { 'Console': '#457abb', 'Error': 'red', } -// TODO: if Node's Version of VSCode >=9.9, use default option -const inspectOptions: NodeJS.InspectOptions = { depth: 20 }; - -type Data = { line: number, type: 'Expression' | 'Terminal', value: any }; +type Data = { line: number, value: string }; type Result = { line: number, type: 'Value of Expression' | 'Console' | 'Error', text: string, value: string }; export default class Decorator { @@ -45,19 +40,48 @@ export default class Decorator { } async update(data: Data) { + if (!data) return; + + let result = this.format(data); + + this.outputChannel.appendLine(` ${result.value}`); + + if (result.type === 'Console') { + result = this.mergeConsoleOutput(result); + } + this.updateWith(result); + this.decorateAll(); + } + private format({ line, value }: Data): Result { + let match: RegExpExecArray; - let result = (data.type === 'Expression') - ? await this.formatExpressionValue(data) - : this.formatTerminalOutput(data); + if ((match = /((\w*Error(?:\s\[[^\]]+\])?:\s.*)(?:\n\s*at\s[\s\S]+)?)$/.exec(value)) != null) { + return { line, type: 'Error', text: match[1], value: match[2] }; + } + else if ((match = /^`\{(\d+)\}`([\s\S]*)$/.exec(value)) != null) { + let value = match[2] || ''; + return { line: +match[1], type: 'Console', text: value.replace(/\r?\n/g, ' '), value }; + } + else { + return { line, type: 'Value of Expression', text: value.replace(/\r?\n/g, ' '), value }; + } + } + private mergeConsoleOutput({ line, text, value }: Result) { + let output = this.lineToOutput.get(line); + if (output == null) { + this.lineToOutput.set(line, output = { line, type: 'Console', text: '', value: '' }); + } - if (!result) return; + output.text += (output.text && ', ') + text; + output.value += (output.value && '\n') + value; + return output; + } + private updateWith({ line, type, text, value }: Result) { let decorator: DecorationOptions; - if ((decorator = this.lineToDecorator.get(result.line)) == null) { - let line = result.line, - length = this.editor.document.getText(new Range(new Position(line, 0), new Position(line, 37768))).length + 1, - pos = new Position(line, length); + if ((decorator = this.lineToDecorator.get(line)) == null) { + let pos = new Position(line, Number.MAX_SAFE_INTEGER); decorator = { renderOptions: { before: { margin: '0 0 0 1em' } }, @@ -67,79 +91,11 @@ export default class Decorator { this.decorators.push(decorator); } - decorator.renderOptions.before.color = colorOfType[result.type]; - decorator.renderOptions.before.contentText = ` ${result.text}`; + decorator.renderOptions.before.color = colorOfType[type]; + decorator.renderOptions.before.contentText = ` ${text}`; - decorator.hoverMessage = new MarkdownString(result.type); - decorator.hoverMessage.appendCodeblock(result.value, result.type === 'Error' ? 'text' : 'javascript'); - - this.decorateAll(); - } - private async formatExpressionValue(data: Data): Promise { - let result = data.value; - switch (typeof result) { - case 'undefined': - return null; - - case 'object': - if (result.constructor && result.constructor.name === 'Promise' && result.then) { - try { - data.value = await (>result); - return data.value ? this.formatExpressionValue(data) : null; - } catch (error) { - return { - line: data.line, - type: 'Error', - text: `${error.name}: ${error.message}`, - value: error.stack, - } - } - } - - let string = Util.inspect(result, inspectOptions); - return { - line: data.line, - type: 'Value of Expression', - text: string.replace(/\n/g, ' '), - value: string, - } - - default: - return { - line: data.line, - type: 'Value of Expression', - text: result.toString().replace(/\r?\n/g, ' '), - value: result, - } - } - } - private formatTerminalOutput(data: Data): Result { - let out = data.value as string; - let match: RegExpExecArray; - - if ((match = /^(\w*Error(?: \[[^\]]+\])?:\s.*)(?:\n\s*at\s)?/.exec(out)) != null) { - this.outputChannel.appendLine(` ${out}`); - - return { line: data.line, type: 'Error', text: match[1], value: out }; - } - else if ((match = /^`\{(\d+)\}`([\s\S]*)$/.exec(out)) != null) { - let line = +match[1]; - let msg = match[2] || ''; - - let output = this.lineToOutput.get(line); - if (output == null) { - this.lineToOutput.set(line, output = { line, type: 'Console', text: '', value: '' }); - } - - output.text += (output.text && ', ') + msg.replace(/\r?\n/g, ' '); - output.value += (output.value && '\n') + msg; - - this.outputChannel.appendLine(` ${msg}`); - return output; - } - else { - this.outputChannel.appendLine(` ${out}`); - } + decorator.hoverMessage = new MarkdownString(type) + .appendCodeblock(value, type === 'Error' ? 'text' : 'javascript'); } decorateAll() { diff --git a/src/repl-client.ts b/src/repl-client.ts index 07136f5..3e9679e 100644 --- a/src/repl-client.ts +++ b/src/repl-client.ts @@ -138,12 +138,11 @@ export default class ReplClient { } dispose() { + if (!this.isClosed) this.close(); + this.changeActiveDisposable.dispose(); this.closeTextDocumentDisposable.dispose(); this.changeEventDisposable.dispose(); this.editor = null; - - this.repl.send({ operation: 'exit' }); - this.repl = null; } } \ No newline at end of file diff --git a/src/repl-server.ts b/src/repl-server.ts index 9df8d46..1174e03 100644 --- a/src/repl-server.ts +++ b/src/repl-server.ts @@ -3,8 +3,7 @@ import * as Util from 'util'; import { Writable, Readable } from 'stream'; -type ReplServer = Repl.REPLServer & { inputStream: Readable, eval: ReplEval }; -type ReplEval = (cmd: string, context: any, filename: string, cb: (err?: Error, result?: any) => void) => void; +type ReplServer = Repl.REPLServer & { inputStream: Readable }; let lineCount = 0; @@ -13,14 +12,11 @@ const server = Repl.start({ input: new Readable({ read: () => { } }), output: new Writable({ write: (chunk, encoding, callback) => { - let out = chunk.toString().trim(); - switch (out) { - case '...': break; - case '': break; - default: - process.send({ line: lineCount, type: 'Terminal', value: out }); - break; - } + let value = chunk.toString().trim(); + + if (value !== '' && /^\.{3,}$/.test(value) === false) + process.send({ line: lineCount, value }); + callback(); } }), @@ -28,48 +24,37 @@ const server = Repl.start({ }) as ReplServer; - -const originEval = server.eval; // keep a backup of original eval -const lineNumber = /\/\*`(\d+)`\*\//g; - -// nice place to read the result in sequence and inject it in the code -server.eval = (cmd, context, filename, callback) => { - originEval(cmd, context, filename, (err, result) => { - let match: RegExpExecArray; - - while ((match = lineNumber.exec(cmd)) != null) - lineCount += +match[1]; - - if (result) - process.send({ line: lineCount, type: 'Expression', value: result }); - - callback(err, result); - }); - - lineCount++; -} - const originLog = server.context.console.log; const appendLineLog = (lineNumber: number, text: any, ...args: any[]) => { originLog(`\`{${lineNumber}}\`${typeof text === 'string' ? text : Util.inspect(text)}`, ...args); } Object.defineProperty(server.context, '`console`', { value: { - log: appendLineLog, debug: appendLineLog, error: appendLineLog, + info: appendLineLog, + log: appendLineLog, + warn: appendLineLog, } }); +const lineNumber = /\/\*`(\d+)`\*\//g; + process.on('message', data => { - if (data.code) { - try { - for (let line of data.code.split('\n')) - server.inputStream.push(line + '\n'); - } catch (error) { - process.emit('error', error); + + if (typeof data.code === 'string') { + for (let codeLine of data.code.split('\n')) { + let match: RegExpExecArray; + + while ((match = lineNumber.exec(codeLine)) != null) + lineCount += +match[1]; + + server.inputStream.push(codeLine + '\n'); + + lineCount++; } - } else if (data.operation === 'exit') { + } + else if (data.operation === 'exit') { process.exit(); } }); \ No newline at end of file From 865d8b7a254ab7039f9af411704c90a5e08c274d Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Wed, 5 Dec 2018 22:00:37 +0800 Subject: [PATCH 09/12] Fix cannot show error stack in hover message of result. --- src/decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/decorator.ts b/src/decorator.ts index 5f2df06..fce9e35 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -56,7 +56,7 @@ export default class Decorator { let match: RegExpExecArray; if ((match = /((\w*Error(?:\s\[[^\]]+\])?:\s.*)(?:\n\s*at\s[\s\S]+)?)$/.exec(value)) != null) { - return { line, type: 'Error', text: match[1], value: match[2] }; + return { line, type: 'Error', text: match[2], value: match[1] }; } else if ((match = /^`\{(\d+)\}`([\s\S]*)$/.exec(value)) != null) { let value = match[2] || ''; From 055169c9f0484d41536e85f809ee35fcacfd96af Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Thu, 6 Dec 2018 18:15:57 +0800 Subject: [PATCH 10/12] Add mulitple language support; Add chinese translation. --- locale/lang.en-us.json | 14 ++++++++++++++ locale/lang.zh-cn.json | 14 ++++++++++++++ package.json | 6 +++--- package.nls.json | 5 +++++ package.nls.zh-cn.json | 5 +++++ src/extension.ts | 8 +++++--- src/i18n.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/repl-client.ts | 17 +++++++++++------ 8 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 locale/lang.en-us.json create mode 100644 locale/lang.zh-cn.json create mode 100644 package.nls.json create mode 100644 package.nls.zh-cn.json create mode 100644 src/i18n.ts diff --git a/locale/lang.en-us.json b/locale/lang.en-us.json new file mode 100644 index 0000000..8a5030f --- /dev/null +++ b/locale/lang.en-us.json @@ -0,0 +1,14 @@ +{ + "info": { + "initializing": "Initializing REPL extension...", + "cwd": "[%s] working at:%s", + "starting": "[%s] starting to interpret %d bytes of code:", + "disposing": "Disposing REPL server..." + }, + "warn": { + "CRUD": " Warning: Be careful with CRUD operations since the code is running multiple times in REPL." + }, + "error": { + "notJavascript": "[Node.js REPL] Selected document is not Javascript, unable to start REPL here." + } +} \ No newline at end of file diff --git a/locale/lang.zh-cn.json b/locale/lang.zh-cn.json new file mode 100644 index 0000000..5590064 --- /dev/null +++ b/locale/lang.zh-cn.json @@ -0,0 +1,14 @@ +{ + "info": { + "initializing": "初始化 REPL 服务端中……", + "cwd": "[%s] 工作目录为:%s", + "starting": "[%s] 开始解释长度为 %d 字节的代码:", + "disposing": "销毁 REPL 服务端中……" + }, + "warn": { + "CRUD": " 警告: 注意对文件、数据库等等的增删查改操作,因为代码会在 REPL 中执行多次。" + }, + "error": { + "notJavascript": "[Node.js REPL] 选中的文档不是 Javascript 文件,不能在此执行 REPL。" + } +} \ No newline at end of file diff --git a/package.json b/package.json index 33af666..1586a4b 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ "contributes": { "commands": [{ "command": "extension.nodejsRepl", - "title": "Node.js Interactive window (REPL)" + "title": "%command.extension.nodejsRepl%" }, { "command": "extension.nodejsReplCurrent", - "title": "Node.js Interactive window (REPL) at selected file" + "title": "%command.extension.nodejsReplCurrent%" }, { "command": "extension.nodejsReplClose", - "title": "Stop Node.js Interactive window (REPL)" + "title": "%command.extension.nodejsReplClose%" }] }, "scripts": { diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 0000000..fa9d9c9 --- /dev/null +++ b/package.nls.json @@ -0,0 +1,5 @@ +{ + "command.extension.nodejsRepl": "Node.js REPL: Start REPL at New Untitled File", + "command.extension.nodejsReplCurrent": "Node.js REPL: Start REPL at Current File", + "command.extension.nodejsReplClose": "Node.js REPL: Stop REPL" +} \ No newline at end of file diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json new file mode 100644 index 0000000..180a1a1 --- /dev/null +++ b/package.nls.zh-cn.json @@ -0,0 +1,5 @@ +{ + "command.extension.nodejsRepl": "Node.js REPL: 在新建无标题文件运行交互式解释器", + "command.extension.nodejsReplCurrent": "Node.js REPL: 在当前编辑器运行交互式解释器", + "command.extension.nodejsReplClose": "Node.js REPL: 停止交互式解释器" +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index b3df0f3..bdc1cfa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ // Import the module and reference it with the alias vscode in your code below import { commands, + env, ExtensionContext, TextDocument, TextEditor, @@ -13,8 +14,10 @@ import { } from 'vscode'; import ReplClient from './repl-client'; +import i18n from './i18n'; +const i18nTexts = i18n(env.language); let outputWindow = window.createOutputChannel("NodeJs REPL"); let registeredCommands: Disposable[]; let client = new ReplClient(outputWindow); @@ -47,7 +50,7 @@ export function activate(context: ExtensionContext) { editor = await window.showTextDocument(doc, ViewColumn.Active); } else { - window.showErrorMessage('[Node.js REPL] Selected document is not Javascript, unable to start REPL here.'); + window.showErrorMessage(i18nTexts.error.notJavascript); return; } @@ -70,8 +73,7 @@ export function activate(context: ExtensionContext) { }), ]; - for (let cmd of registeredCommands) - context.subscriptions.push(cmd); + context.subscriptions.push(...registeredCommands); } /** diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..70c8527 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,38 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; + +interface Resource { + info: { + initializing: string, + cwd: string, + starting: string, + disposing: string, + }, + warn: { + CRUD: string, + }, + error: { + notJavascript: string, + } +} + +let resource: Resource; + +export default function i18n(lang: string): Resource { + + if (resource) return resource; + + lang = lang.toLowerCase(); + let defaultLang = 'en-us'; + let langs = [lang, lang.split('-')[0], defaultLang]; + let localePath = path.join(__dirname, '../locale/lang.%s.json'); + + for (let language of langs) { + let langResourcePath = util.format(localePath, language); + if (fs.existsSync(langResourcePath)) { + return JSON.parse(fs.readFileSync(langResourcePath, 'utf8')); + } + } + throw new Error('Not found any language resource.'); +} \ No newline at end of file diff --git a/src/repl-client.ts b/src/repl-client.ts index 3e9679e..98d4210 100644 --- a/src/repl-client.ts +++ b/src/repl-client.ts @@ -1,5 +1,6 @@ import { Disposable, + env, OutputChannel, TextEditor, TextDocument, @@ -17,6 +18,10 @@ import { import Decorator from "./decorator"; import { spawn, ChildProcess } from "child_process"; +import { format } from 'util'; +import i18n from './i18n'; + +const i18nTexts = i18n(env.language); const serverArguments = [`${__dirname}/repl-server.js`]; @@ -80,8 +85,8 @@ export default class ReplClient { } init(editor: TextEditor, doc: TextDocument) { - this.outputChannel.appendLine(`Initializing REPL extension.`); - this.outputChannel.appendLine(` Warning; Be careful with CRUD operations since the code is running multiple times in REPL.`); + this.outputChannel.appendLine(i18nTexts.info.initializing); + this.outputChannel.appendLine(i18nTexts.warn.CRUD); this.editor = editor; @@ -93,7 +98,7 @@ export default class ReplClient { this.filePath = doc.isUntitled ? '' : doc.fileName; - this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] working at: ${this.basePath}`); + this.outputChannel.appendLine(format(i18nTexts.info.cwd, new Date().toLocaleTimeString(), this.basePath)); } } @@ -105,11 +110,11 @@ export default class ReplClient { this.repl = spawn('node', serverArguments, { cwd: this.basePath, stdio: stdioOptions }) .on('message', async result => await this.decorator.update(result)) - .on('error', err => this.outputChannel.appendLine(`[Repl Server] ${err.message}`)); + .on('error', err => this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}][REPL Server] ${err.message}`)); let code = this.editor.document.getText(); - this.outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] starting to interpret ${code.length} bytes of code`); + this.outputChannel.appendLine(format(i18nTexts.info.starting, new Date().toLocaleTimeString(), code.length)); // TODO: typescript REPL // code = `require("${Path.join(this.basePath, "node_modules/ts-node").replace(/\\/g, '\\\\')}").register({});\n${code}`; @@ -127,7 +132,7 @@ export default class ReplClient { close() { if (this.outputChannel) - this.outputChannel.appendLine(`Disposing REPL server.`); + this.outputChannel.appendLine(i18nTexts.info.disposing); this.repl.send({ operation: 'exit' }); this.repl = null; From bf9423e6e6141d0b7272f01b6b62c14ccbd9c87b Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Thu, 6 Dec 2018 21:47:29 +0800 Subject: [PATCH 11/12] Fix extension crash when active editor equal to undefined. --- src/repl-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repl-client.ts b/src/repl-client.ts index 98d4210..d5d0c66 100644 --- a/src/repl-client.ts +++ b/src/repl-client.ts @@ -46,7 +46,7 @@ export default class ReplClient { this.decorator = new Decorator(outputChannel); this.changeActiveDisposable = window.onDidChangeActiveTextEditor(async (editor) => { - if (this.editor && this.editor.document === editor.document) { + if (this.editor && editor && this.editor.document === editor.document) { this.init(editor, editor.document); this.interpret(); } From 4eaac4d51cb28576ad148f6ca330e7c39cfff341 Mon Sep 17 00:00:00 2001 From: plylrnsdy Date: Thu, 6 Dec 2018 22:16:10 +0800 Subject: [PATCH 12/12] Fix cannot stop extension. --- src/extension.ts | 2 +- src/repl-client.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index bdc1cfa..daf2c71 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -63,7 +63,7 @@ export function activate(context: ExtensionContext) { commands.registerCommand('extension.nodejsReplClose', async () => { try { if (!client.isClosed) { - client.close(); + client.dispose(); editor = null; doc = null; } diff --git a/src/repl-client.ts b/src/repl-client.ts index d5d0c66..af48a71 100644 --- a/src/repl-client.ts +++ b/src/repl-client.ts @@ -44,7 +44,9 @@ export default class ReplClient { constructor(private outputChannel: OutputChannel) { this.decorator = new Decorator(outputChannel); + } + private registerEventHandlers() { this.changeActiveDisposable = window.onDidChangeActiveTextEditor(async (editor) => { if (this.editor && editor && this.editor.document === editor.document) { this.init(editor, editor.document); @@ -88,6 +90,8 @@ export default class ReplClient { this.outputChannel.appendLine(i18nTexts.info.initializing); this.outputChannel.appendLine(i18nTexts.warn.CRUD); + if (this.isDisposed) this.registerEventHandlers(); + this.editor = editor; if (workspace && Array.isArray(workspace.workspaceFolders)) { @@ -137,7 +141,6 @@ export default class ReplClient { this.repl.send({ operation: 'exit' }); this.repl = null; } - get isClosed() { return this.repl == null; } @@ -150,4 +153,7 @@ export default class ReplClient { this.changeEventDisposable.dispose(); this.editor = null; } + get isDisposed() { + return this.editor == null; + } } \ No newline at end of file