LSP working with Python!!

This commit is contained in:
Radon Rosborough 2020-06-23 14:51:43 -06:00
parent fc9cbbc7c6
commit 4425f31b88
7 changed files with 1473 additions and 11 deletions

View File

@ -1,8 +1,10 @@
import { ChildProcess, spawn } from "child_process";
import * as path from "path";
import * as WebSocket from "ws";
import * as pty from "node-pty";
import { IPty } from "node-pty";
import * as rpc from "vscode-jsonrpc";
import { v4 as getUUID } from "uuid";
import { PRIVILEGED } from "./config";
@ -15,6 +17,11 @@ export class Session {
code: string;
config: LangConfig;
term: { pty: IPty | null; live: boolean };
lsp: {
proc: ChildProcess;
reader: rpc.StreamMessageReader;
writer: rpc.StreamMessageWriter;
} | null;
ws: WebSocket;
homedir: string | null;
uid: number | null;
@ -28,6 +35,7 @@ export class Session {
this.ws = ws;
this.config = langs[lang];
this.term = { pty: null, live: false };
this.lsp = null;
this.code = "";
this.homedir = null;
this.uid = null;
@ -83,6 +91,13 @@ export class Session {
this.run();
}
break;
case "lspInput":
if (!this.lsp) {
this.log(`Got LSP input before language server was started`);
} else {
this.lsp.writer.write(msg.input);
}
break;
default:
this.log(`Got unknown message type: ${msg.event}`);
break;
@ -103,6 +118,7 @@ export class Session {
alwaysCreate,
compile,
run,
lsp,
hacks,
} = this.config;
if (this.term.pty) {
@ -210,6 +226,31 @@ export class Session {
}
}
});
if (lsp && this.lsp === null) {
const lspArgs = PRIVILEGED
? [
"/home/docker/src/system/out/riju-system-privileged",
"spawn",
`${this.uid}`,
`${this.uuid}`,
"bash",
"-c",
lsp,
]
: ["bash", "-c", lsp];
const proc = spawn(lspArgs[0], lspArgs.slice(1), {
env: getEnv(this.uuid),
});
this.lsp = {
proc,
reader: new rpc.StreamMessageReader(proc.stdout),
writer: new rpc.StreamMessageWriter(proc.stdin),
};
this.lsp.reader.listen((data) => {
this.ws.send(JSON.stringify({ event: "lspOutput", output: data }));
});
this.ws.send(JSON.stringify({ event: "lspStarted" }));
}
};
cleanup = async () => {
this.log(`Cleaning up session`);

View File

@ -9,6 +9,9 @@ export interface LangConfig {
alwaysCreate?: boolean;
compile?: string;
run: string;
lsp?: string;
lspInit?: any;
lspConfig?: any;
template: string;
hacks?: "ghci-config"[];
}
@ -720,6 +723,14 @@ main :-
repl: "python3 -u",
main: "main.py",
run: "python3 -u -i main.py",
lsp: "Microsoft.Python.LanguageServer",
lspInit: {
interpreter: {
properties: {
InterpreterPath: "/usr/bin/python3",
},
},
},
template: `print("Hello, world!")
`,
},

View File

@ -1,4 +1,17 @@
import * as monaco from "monaco-editor";
import {
createConnection,
MonacoLanguageClient,
MonacoServices,
} from "monaco-languageclient";
import { Disposable } from "vscode";
import { createMessageConnection } from "vscode-jsonrpc";
import {
AbstractMessageReader,
DataCallback,
} from "vscode-jsonrpc/lib/messageReader";
import { AbstractMessageWriter } from "vscode-jsonrpc/lib/messageWriter";
import { Message } from "vscode-jsonrpc/lib/messages";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
@ -7,9 +20,67 @@ import "xterm/css/xterm.css";
interface RijuConfig {
id: string;
monacoLang: string;
lspInit?: any;
lspConfig?: any;
template: string;
}
class RijuMessageReader extends AbstractMessageReader {
state: "initial" | "listening" | "closed" = "initial";
callback: DataCallback | null = null;
messageQueue: any[] = [];
socket: WebSocket;
constructor(socket: WebSocket) {
super();
this.socket = socket;
this.socket.addEventListener("message", (event: MessageEvent) => {
this.readMessage(event.data);
});
}
listen(callback: DataCallback): void {
if (this.state === "initial") {
this.state = "listening";
this.callback = callback;
while (this.messageQueue.length > 0) {
this.readMessage(this.messageQueue.pop()!);
}
}
}
readMessage(rawMessage: string): void {
if (this.state === "initial") {
this.messageQueue.splice(0, 0, rawMessage);
} else if (this.state === "listening") {
let message: any;
try {
message = JSON.parse(rawMessage);
} catch (err) {
return;
}
switch (message?.event) {
case "lspOutput":
this.callback!(message?.output);
break;
}
}
}
}
class RijuMessageWriter extends AbstractMessageWriter {
socket: WebSocket;
constructor(socket: WebSocket) {
super();
this.socket = socket;
}
write(msg: Message): void {
this.socket.send(JSON.stringify({ event: "lspInput", input: msg }));
}
}
async function main() {
const config: RijuConfig = (window as any).rijuConfig;
@ -29,6 +100,7 @@ async function main() {
let retryDelayMs = initialRetryDelayMs;
function tryConnect() {
let clientDisposable: Disposable | null = null;
console.log("Connecting to server...");
socket = new WebSocket(
(document.location.protocol === "http:" ? "ws://" : "wss://") +
@ -60,6 +132,36 @@ async function main() {
}
term.write(message.output);
return;
case "lspStarted":
const connection = createMessageConnection(
new RijuMessageReader(socket!),
new RijuMessageWriter(socket!)
);
const client = new MonacoLanguageClient({
name: "Riju",
clientOptions: {
documentSelector: [{ pattern: "**" }],
middleware: {
workspace: {
configuration: () => {
return [config.lspConfig || {}];
},
},
},
initializationOptions: config.lspInit || {},
},
connectionProvider: {
get: (errorHandler, closeHandler) =>
Promise.resolve(
createConnection(connection, errorHandler, closeHandler)
),
},
});
clientDisposable = client.start();
return;
case "lspOutput":
// Should be handled by RijuMessageReader
return;
default:
console.error("Unexpected message from server:", message);
return;
@ -71,6 +173,9 @@ async function main() {
} else {
console.error("Connection died");
}
if (clientDisposable) {
clientDisposable.dispose();
}
scheduleConnect();
});
}
@ -102,6 +207,8 @@ async function main() {
document.getElementById("runButton")!.addEventListener("click", () => {
socket?.send(JSON.stringify({ event: "runCode", code: editor.getValue() }));
});
MonacoServices.install(editor);
}
main().catch(console.error);

View File

@ -4,6 +4,8 @@
"license": "MIT",
"private": true,
"dependencies": {
"@babel/core": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@types/app-root-path": "^1.2.4",
"@types/async-lock": "^1.1.2",
"@types/express": "^4.17.6",
@ -16,6 +18,7 @@
"@types/uuid": "^8.0.0",
"app-root-path": "^3.0.0",
"async-lock": "^1.2.4",
"babel-loader": "^8.1.0",
"css-loader": "^3.5.3",
"ejs": "^3.1.3",
"express": "^4.17.1",
@ -23,6 +26,8 @@
"file-loader": "^6.0.0",
"lodash": "^4.17.15",
"monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.0",
"monaco-languageclient": "^0.13.0",
"node-cleanup": "^2.1.2",
"node-pty": "^0.9.0",
"npm-run-all": "^4.1.5",
@ -31,6 +36,8 @@
"ts-loader": "^7.0.5",
"typescript": "^3.9.5",
"uuid": "^8.1.0",
"vscode": "^1.1.37",
"vscode-jsonrpc": "^5.0.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"xterm": "^4.6.0",

View File

@ -60,6 +60,15 @@ tar -xf powershell-*.tar.gz -C /opt/powershell
ln -s /opt/powershell/pwsh /usr/bin/pwsh
rm powershell-*.tar.gz
# Python
cd /tmp
xml="$(curl -sSL "https://pvsc.blob.core.windows.net/python-language-server-stable?restype=container&comp=list&prefix=Python-Language-Server-linux-x64")"
nupkg="$(echo "$xml" | grep -Eo 'https://[^<]+\.nupkg' | tail -n1)"
wget -nv "${nupkg}"
unzip -d /opt/mspyls Python-Language-Server-linux-x64.*.nupkg
chmod +x /opt/mspyls/Microsoft.Python.LanguageServer
ln -s /opt/mspyls/Microsoft.Python.LanguageServer /usr/bin/Microsoft.Python.LanguageServer
# SNOBOL
wget -nv ftp://ftp.snobol4.org/snobol/snobol4-2.0.tar.gz
tar -xf snobol4-*.tar.gz

View File

@ -1,11 +1,13 @@
const path = require("path");
const webpack = require("webpack");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
function isProduction(argv) {
return !argv.development;
}
module.exports = (_, argv) => ({
devtool: isProduction(argv) ? undefined : "source-map",
entry: "./frontend/src/app.ts",
mode: isProduction(argv) ? "production" : "development",
module: {
@ -26,8 +28,21 @@ module.exports = (_, argv) => ({
test: /\.ttf$/,
use: ["file-loader"],
},
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
include: /vscode-jsonrpc/,
},
],
},
node: {
net: "mock",
},
output: {
path: path.resolve(__dirname, "frontend/out"),
publicPath: "/js/",
@ -36,4 +51,10 @@ module.exports = (_, argv) => ({
performance: {
hints: false,
},
plugins: [new MonacoWebpackPlugin()],
resolve: {
alias: {
vscode: require.resolve("monaco-languageclient/lib/vscode-compatibility"),
},
},
});

1286
yarn.lock

File diff suppressed because it is too large Load Diff