From eeb4d3af70c93a65cad9558368afadf2934af595 Mon Sep 17 00:00:00 2001 From: Radon Rosborough Date: Wed, 8 Jul 2020 18:42:31 -0600 Subject: [PATCH] Various hacks --- .gitignore | 1 + backend/src/api.ts | 56 +++++++------- backend/src/global.d.ts | 2 +- backend/src/langs.ts | 11 ++- backend/src/lsp-repl.ts | 116 +++++++++++++++++++++++++++++ backend/src/util.ts | 2 +- frontend/src/app.ts | 28 +++++-- package.json | 6 +- scripts/docker-install-phase2.bash | 1 + yarn.lock | 35 ++++++++- 10 files changed, 215 insertions(+), 43 deletions(-) create mode 100755 backend/src/lsp-repl.ts diff --git a/.gitignore b/.gitignore index 1a03776..9d1ea22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.log .log +.lsp-repl-history node_modules out diff --git a/backend/src/api.ts b/backend/src/api.ts index 130af6e..922998c 100644 --- a/backend/src/api.ts +++ b/backend/src/api.ts @@ -19,7 +19,7 @@ import { export class Session { uuid: string; - code: string; + code: string | null; config: LangConfig; term: { pty: IPty | null; live: boolean }; lsp: { @@ -41,7 +41,7 @@ export class Session { this.config = langs[lang]; this.term = { pty: null, live: false }; this.lsp = null; - this.code = ""; + this.code = null; this.homedir = null; this.uid = null; this.uidCleanup = null; @@ -121,11 +121,12 @@ export class Session { repl, main, suffix, - alwaysCreate, + createEmpty, compile, run, lspSetup, lsp, + template, hacks, } = this.config; if (this.term.pty) { @@ -141,42 +142,37 @@ export class Session { if (!run) { cmdline = `echo 'Support for ${this.config.name} is not yet implemented.'`; } else if (this.code) { - let code = this.code; - if (suffix) { - code += suffix; - } - if (main.includes("/")) { - await spawnPrivileged( - this.uid, - this.uuid, - ["mkdir", "-p", path.dirname(path.resolve(this.homedir, main))], - this.log - ); - } - await spawnPrivileged( - this.uid, - this.uuid, - ["sh", "-c", `cat > ${path.resolve(this.homedir, main)}`], - this.log, - { input: code } - ); cmdline = run; if (compile) { cmdline = `( ${compile} ) && ( ${run} )`; } } else if (repl) { - if (alwaysCreate) { - await spawnPrivileged( - this.uid, - this.uuid, - ["touch", `${path.resolve(this.homedir, main)}`], - this.log - ); - } cmdline = repl; } else { cmdline = `echo '${name} has no REPL, press Run to see it in action'`; } + let code = this.code; + if (this.code === null) { + code = createEmpty ? "" : template; + } + if (code && suffix) { + code += suffix; + } + if (main.includes("/")) { + await spawnPrivileged( + this.uid, + this.uuid, + ["mkdir", "-p", path.dirname(path.resolve(this.homedir, main))], + this.log + ); + } + await spawnPrivileged( + this.uid, + this.uuid, + ["sh", "-c", `cat > ${path.resolve(this.homedir, main)}`], + this.log, + { input: code as string } + ); if (hacks && hacks.includes("ghci-config") && run) { if (this.code) { const contents = ":load Main\nmain\n"; diff --git a/backend/src/global.d.ts b/backend/src/global.d.ts index 3aad786..6a63761 100644 --- a/backend/src/global.d.ts +++ b/backend/src/global.d.ts @@ -1 +1 @@ -declare module "heroku-ssl-redirect"; +declare module "historic-readline"; diff --git a/backend/src/langs.ts b/backend/src/langs.ts index 82d7982..6193c94 100644 --- a/backend/src/langs.ts +++ b/backend/src/langs.ts @@ -6,13 +6,15 @@ export interface LangConfig { main: string; prefix?: string; suffix?: string; - alwaysCreate?: boolean; + createEmpty?: boolean; compile?: string; run: string; lspSetup?: string; lsp?: string; + lspDisableDynamicRegistration?: boolean; lspInit?: any; lspConfig?: any; + lspLang?: string; template: string; hacks?: "ghci-config"[]; } @@ -340,6 +342,7 @@ output = "Hello, world!" monacoLang: "plaintext", repl: `SHELL=/usr/bin/elvish HOME="$PWD" elvish`, main: ".elvish/rc.elv", + createEmpty: true, run: `SHELL=/usr/bin/elvish HOME="$PWD" elvish`, template: `echo "Hello, world!" `, @@ -558,6 +561,7 @@ PLEASE GIVE UP monacoLang: "shell", repl: `SHELL=/usr/bin/ksh HOME="$PWD" ksh`, main: ".kshrc", + createEmpty: true, run: `SHELL=/usr/bin/ksh HOME="$PWD" ksh`, template: `echo "Hello, world!" `, @@ -865,6 +869,7 @@ binding_irb.run(IRB.conf) monacoLang: "shell", repl: `SHELL=/usr/bin/sh HOME="$PWD" posh -l`, main: ".profile", + createEmpty: true, run: `SHELL=/usr/bin/sh HOME="$PWD" posh -l`, template: `echo "Hello, world!" `, @@ -1025,6 +1030,7 @@ END monacoLang: "tcl", repl: "tclsh", main: ".tclshrc", + createEmpty: true, run: `HOME="$PWD" tclsh`, template: `puts {Hello, world!} `, @@ -1035,6 +1041,7 @@ END monacoLang: "shell", repl: `SHELL=/usr/bin/tcsh HOME="$PWD" tcsh`, main: ".tcshrc", + createEmpty: true, run: `SHELL=/usr/bin/tcsh HOME="$PWD" tcsh`, template: `echo "Hello, world!" `, @@ -1147,7 +1154,7 @@ message: monacoLang: "shell", repl: "SHELL=/usr/bin/zsh zsh", main: ".zshrc", - alwaysCreate: true, + createEmpty: true, run: `SHELL=/usr/bin/zsh ZDOTDIR="$PWD" zsh`, template: `echo "Hello, world!" `, diff --git a/backend/src/lsp-repl.ts b/backend/src/lsp-repl.ts new file mode 100755 index 0000000..fe2f4cc --- /dev/null +++ b/backend/src/lsp-repl.ts @@ -0,0 +1,116 @@ +import * as child_process from "child_process"; +import * as process from "process"; +import * as nodeReadline from "readline"; + +import * as appRoot from "app-root-path"; +import * as readline from "historic-readline"; +import { quote } from "shell-quote"; +import * as rpc from "vscode-jsonrpc"; + +import { langs } from "./langs"; + +const args = process.argv.slice(2); + +function printUsage() { + console.log(`usage: yarn lsp-repl (LANG | CMDLINE...)`); +} + +if (args.length === 0) { + printUsage(); + process.exit(1); +} + +if (["-h", "-help", "--help", "help"].includes(args[0])) { + printUsage(); + process.exit(0); +} + +let cmdline; +if ( + args.length === 1 && + langs[args[0]] && + typeof langs[args[0]].lsp === "string" +) { + cmdline = ["bash", "-c", langs[args[0]].lsp as string]; +} else { + cmdline = args; +} + +console.error(quote(cmdline)); +const proc = child_process.spawn(cmdline[0], cmdline.slice(1)); + +proc.stderr.on("data", (data) => process.stderr.write(data)); +proc.on("exit", (code, signal) => { + if (code) { + console.error(`Language server exited with code ${code}`); + process.exit(code); + } else { + console.error(`Language server exited due to signal ${signal}`); + process.exit(1); + } +}); +proc.on("error", (err) => { + console.error(`Failed to start language server: ${err}`); + process.exit(1); +}); + +const reader = new rpc.StreamMessageReader(proc.stdout); +const writer = new rpc.StreamMessageWriter(proc.stdin); + +reader.listen((data) => { + console.log("<<< " + JSON.stringify(data) + "\n"); +}); + +// https://stackoverflow.com/a/10608048/3538165 +function fixStdoutFor(cli: any) { + var oldStdout = process.stdout; + var newStdout = Object.create(oldStdout); + newStdout.write = function () { + cli.output.write("\x1b[2K\r"); + var result = oldStdout.write.apply( + this, + (Array.prototype.slice as any).call(arguments) + ); + cli._refreshLine(); + return result; + }; + (process as any).__defineGetter__("stdout", function () { + return newStdout; + }); +} + +readline.createInterface({ + input: process.stdin, + output: process.stdout, + path: appRoot.resolve(".lsp-repl-history"), + next: (cli: nodeReadline.Interface) => { + fixStdoutFor(cli); + cli.setPrompt(">>> "); + cli.on("line", (line: string) => { + if (line) { + let data; + try { + data = JSON.parse(line); + } catch (err) { + console.error(`Invalid JSON: ${err}`); + cli.prompt(); + return; + } + console.log(); + writer.write(data); + } + cli.prompt(); + }); + cli.on("SIGINT", () => { + console.error("^C"); + cli.write("", { ctrl: true, name: "u" }); + cli.prompt(); + }); + cli.on("close", () => { + console.error(); + process.exit(0); + }); + console.log(); + cli.prompt(); + }, +}); diff --git a/backend/src/util.ts b/backend/src/util.ts index 328f13a..eebe1d1 100644 --- a/backend/src/util.ts +++ b/backend/src/util.ts @@ -37,7 +37,7 @@ export async function call( delete options.input; delete options.check; const proc = spawn(args[0], args.slice(1), options); - if (input) { + if (typeof input === "string") { proc.stdin!.end(input); } let output = ""; diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 857e4bd..8337c3c 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -19,13 +19,16 @@ import { FitAddon } from "xterm-addon-fit"; import "xterm/css/xterm.css"; const DEBUG = window.location.hash === "#debug"; +const config: RijuConfig = (window as any).rijuConfig; interface RijuConfig { id: string; monacoLang: string; main: string; + lspDisableDynamicRegistration?: boolean; lspInit?: any; lspConfig?: any; + lspLang?: string; template: string; } @@ -84,19 +87,35 @@ class RijuMessageWriter extends AbstractMessageWriter { } write(msg: Message): void { - if ((msg as any).method === "initialize") { - (msg as any).params.processId = null; + switch ((msg as any).method) { + case "initialize": + (msg as any).params.processId = null; + if (config.lspDisableDynamicRegistration) { + this.disableDynamicRegistration(msg); + } + break; + case "textDocument/didOpen": + if (config.lspLang) { + (msg as any).params.textDocument.languageId = config.lspLang; + } } if (DEBUG) { console.log("SEND LSP:", msg); } this.socket.send(JSON.stringify({ event: "lspInput", input: msg })); } + + disableDynamicRegistration(msg: any) { + if (!msg || typeof msg !== "object") return; + for (const [key, val] of Object.entries(msg)) { + if (key === "dynamicRegistration" && val === true) + msg.dynamicRegistration = false; + this.disableDynamicRegistration(val); + } + } } async function main() { - const config: RijuConfig = (window as any).rijuConfig; - const term = new Terminal(); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); @@ -188,7 +207,6 @@ async function main() { middleware: { workspace: { configuration: (params, token, configuration) => { - (window as any).config = configuration; return Array( (configuration(params, token) as {}[]).length ).fill( diff --git a/package.json b/package.json index daf5eb5..02bd9dc 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/mkdirp": "^1.0.1", "@types/parse-passwd": "^1.0.0", "@types/rimraf": "^3.0.0", + "@types/shell-quote": "^1.7.0", "@types/tmp": "^0.2.0", "@types/uuid": "^8.0.0", "app-root-path": "^3.0.0", @@ -24,6 +25,7 @@ "express": "^4.17.1", "express-ws": "^4.0.0", "file-loader": "^6.0.0", + "historic-readline": "^1.0.8", "lodash": "^4.17.15", "monaco-editor": "^0.20.0", "monaco-editor-webpack-plugin": "^1.9.0", @@ -32,6 +34,7 @@ "node-pty": "^0.9.0", "npm-run-all": "^4.1.5", "parse-passwd": "^1.0.0", + "shell-quote": "^1.7.2", "style-loader": "^1.2.1", "ts-loader": "^7.0.5", "typescript": "^3.9.5", @@ -53,6 +56,7 @@ "system": "scripts/compile-system.bash", "system-dev": "watchexec --no-vcs-ignore -w system/src -n scripts/compile-system.bash", "build": "run-s backend frontend system", - "dev": "run-p backend-dev frontend-dev system-dev server-dev" + "dev": "run-p backend-dev frontend-dev system-dev server-dev", + "lsp": "node backend/out/lsp-repl.js" } } diff --git a/scripts/docker-install-phase2.bash b/scripts/docker-install-phase2.bash index 37d96fd..14d6574 100755 --- a/scripts/docker-install-phase2.bash +++ b/scripts/docker-install-phase2.bash @@ -27,6 +27,7 @@ make man-db moreutils nano +iputils-ping sudo tmux vim diff --git a/yarn.lock b/yarn.lock index 2f3509f..387f7bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -918,6 +918,11 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/shell-quote@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.7.0.tgz#0b6b930de00ad35128239b182c1fec8b8c40e876" + integrity sha512-0CJaSSayMigMKu/Egx1eVcFjgGYkP6T4dyPRs462BFrMB/OK2FAJFV/24+6R4cGIXw6ZQpZK8XEd2UCVKfkZRw== + "@types/tmp@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c" @@ -2362,6 +2367,16 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +fs-extra@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" + integrity sha1-1OQ0KpZnXLeEZjOmCZJJMytTmVI= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -2502,7 +2517,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -2582,6 +2597,13 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +historic-readline@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/historic-readline/-/historic-readline-1.0.8.tgz#6f36e770769275113a57ae1a6007001af771cff6" + integrity sha1-bzbncHaSdRE6V64aYAcAGvdxz/Y= + dependencies: + fs-extra "^0.24.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -2995,6 +3017,13 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= + optionalDependencies: + graceful-fs "^4.1.6" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -4133,7 +4162,7 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^2.5.4, rimraf@^2.6.3: +rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -4291,7 +4320,7 @@ shebang-regex@^1.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -shell-quote@^1.6.1: +shell-quote@^1.6.1, shell-quote@^1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==