diff --git a/backend/src/api.ts b/backend/src/api.ts index 1101aeb..1576a38 100644 --- a/backend/src/api.ts +++ b/backend/src/api.ts @@ -187,6 +187,7 @@ export class Session { this.ws.send(JSON.stringify(msg)); } catch (err) { this.log(`Failed to send websocket message: ${err}`); + console.log(err); await this.teardown(); } }; diff --git a/backend/src/langs.ts b/backend/src/langs.ts index 4a3ff41..9aef3aa 100644 --- a/backend/src/langs.ts +++ b/backend/src/langs.ts @@ -39,7 +39,7 @@ export interface LangConfig { init?: any; config?: any; lang?: string; - code?: string; + code?: string; // FIXME after?: string; item?: string; // FIXME }; @@ -1404,6 +1404,8 @@ main = do }, }, }, + code: "import func", + item: "functools", }, template: `print("Hello, world!") `, diff --git a/backend/src/lsp-repl.ts b/backend/src/lsp-repl.ts index ceb7e9f..faeb9e4 100755 --- a/backend/src/lsp-repl.ts +++ b/backend/src/lsp-repl.ts @@ -26,11 +26,7 @@ if (["-h", "-help", "--help", "help"].includes(args[0])) { } let cmdline; -if ( - args.length === 1 && - langs[args[0]] && - typeof langs[args[0]].lsp === "string" -) { +if (args.length === 1 && langs[args[0]] && langs[args[0]].lsp) { cmdline = ["bash", "-c", langs[args[0]].lsp!.start]; } else { cmdline = args; diff --git a/backend/src/test-runner.ts b/backend/src/test-runner.ts index fe2b6d5..94e7671 100644 --- a/backend/src/test-runner.ts +++ b/backend/src/test-runner.ts @@ -8,6 +8,13 @@ import { LangConfig, langs } from "./langs"; const TIMEOUT_MS = 3000; +function findPosition(str: string, idx: number) { + const lines = str.substring(0, idx).split("\n"); + const line = lines.length - 1; + const character = lines[lines.length - 1].length; + return { line, character }; +} + class Test { lang: string; type: string; @@ -24,6 +31,8 @@ class Test { send = (msg: any) => { this.ws.onMessage(JSON.stringify(msg)); + this.messages.push(msg); + this.handledMessages += 1; }; constructor(lang: string, type: string) { @@ -112,7 +121,7 @@ class Test { } }; - wait = async (handler: (msg: any) => boolean) => { + wait = async (handler: (msg: any) => T) => { return await new Promise((resolve, reject) => { this.handleUpdate = () => { if (this.timedOut) { @@ -203,7 +212,320 @@ class Test { throw new Error("formatted code did not match"); } }; - testLsp = async () => {}; + testLsp = async () => { + const code = this.config.lsp!.code!; // FIXME + const after = this.config.lsp!.after; + const item = this.config.lsp!.item!; // FIXME + const root = await this.wait((msg: any) => { + if (msg.event === "lspStarted") { + return msg.root; + } + }); + const idx = after + ? this.config.template.indexOf(after) + : this.config.template.length; + const pos = findPosition(this.config.template, idx); + const newCode = + this.config.template.slice(0, idx) + + code + + this.config.template.slice(idx); + const newIdx = idx + code.length; + const newPos = findPosition(newCode, newIdx); + this.send({ + event: "lspInput", + input: { + jsonrpc: "2.0", + id: "0d75333a-47d8-4da8-8030-c81d7bd9eed7", + method: "initialize", + params: { + processId: null, + clientInfo: { name: "vscode" }, + rootPath: root, + rootUri: `file://${root}`, + capabilities: { + workspace: { + applyEdit: true, + workspaceEdit: { + documentChanges: true, + resourceOperations: ["create", "rename", "delete"], + failureHandling: "textOnlyTransactional", + }, + didChangeConfiguration: { dynamicRegistration: true }, + didChangeWatchedFiles: { dynamicRegistration: true }, + symbol: { + dynamicRegistration: true, + symbolKind: { + valueSet: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ], + }, + }, + executeCommand: { dynamicRegistration: true }, + configuration: true, + workspaceFolders: true, + }, + textDocument: { + publishDiagnostics: { + relatedInformation: true, + versionSupport: false, + tagSupport: { valueSet: [1, 2] }, + }, + synchronization: { + dynamicRegistration: true, + willSave: true, + willSaveWaitUntil: true, + didSave: true, + }, + completion: { + dynamicRegistration: true, + contextSupport: true, + completionItem: { + snippetSupport: true, + commitCharactersSupport: true, + documentationFormat: ["markdown", "plaintext"], + deprecatedSupport: true, + preselectSupport: true, + tagSupport: { valueSet: [1] }, + }, + completionItemKind: { + valueSet: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + ], + }, + }, + hover: { + dynamicRegistration: true, + contentFormat: ["markdown", "plaintext"], + }, + signatureHelp: { + dynamicRegistration: true, + signatureInformation: { + documentationFormat: ["markdown", "plaintext"], + parameterInformation: { labelOffsetSupport: true }, + }, + contextSupport: true, + }, + definition: { dynamicRegistration: true, linkSupport: true }, + references: { dynamicRegistration: true }, + documentHighlight: { dynamicRegistration: true }, + documentSymbol: { + dynamicRegistration: true, + symbolKind: { + valueSet: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ], + }, + hierarchicalDocumentSymbolSupport: true, + }, + codeAction: { + dynamicRegistration: true, + isPreferredSupport: true, + codeActionLiteralSupport: { + codeActionKind: { + valueSet: [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ], + }, + }, + }, + codeLens: { dynamicRegistration: true }, + formatting: { dynamicRegistration: true }, + rangeFormatting: { dynamicRegistration: true }, + onTypeFormatting: { dynamicRegistration: true }, + rename: { dynamicRegistration: true, prepareSupport: true }, + documentLink: { dynamicRegistration: true, tooltipSupport: true }, + typeDefinition: { dynamicRegistration: true, linkSupport: true }, + implementation: { dynamicRegistration: true, linkSupport: true }, + colorProvider: { dynamicRegistration: true }, + foldingRange: { + dynamicRegistration: true, + rangeLimit: 5000, + lineFoldingOnly: true, + }, + declaration: { dynamicRegistration: true, linkSupport: true }, + }, + }, + initializationOptions: this.config.lsp!.init || {}, + trace: "off", + workspaceFolders: [ + { + uri: `file://${root}`, + name: `file://${root}`, + }, + ], + }, + }, + }); + await this.wait((msg: any) => { + return ( + msg.event === "lspOutput" && + msg.output.id === "0d75333a-47d8-4da8-8030-c81d7bd9eed7" + ); + }); + this.send({ + event: "lspInput", + input: { jsonrpc: "2.0", method: "initialized", params: {} }, + }); + this.send({ + event: "lspInput", + input: { + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: `file://${root}/${this.config.main}`, + languageId: "python", // FIXME + version: 1, + text: `${this.config.template}`, + }, + }, + }, + }); + this.send({ + event: "lspInput", + input: { + jsonrpc: "2.0", + method: "textDocument/didChange", + params: { + textDocument: { + uri: `file://${root}/${this.config.main}`, + version: 3, + }, + contentChanges: [ + { + range: { + start: pos, + end: pos, + }, + rangeLength: 0, + text: code, + }, + ], + }, + }, + }); + this.send({ + event: "lspInput", + input: { + jsonrpc: "2.0", + id: "ecdb8a55-f755-4553-ae8e-91d6ebbc2045", + method: "textDocument/completion", + params: { + textDocument: { + uri: `file://${root}/${this.config.main}`, + }, + position: newPos, + context: { triggerKind: 1 }, + }, + }, + }); + const items: any = await this.wait((msg: any) => { + if (msg.event === "lspOutput") { + if (msg.output.method === "workspace/configuration") { + this.send({ + event: "lspInput", + input: { + jsonrpc: "2.0", + id: msg.output.id, + result: Array(msg.output.params.items.length).fill( + this.config.lsp!.config !== undefined + ? this.config.lsp!.config + : {} + ), + }, + }); + } else if (msg.output.id === "ecdb8a55-f755-4553-ae8e-91d6ebbc2045") { + return msg.output.result.items; + } + } + }); + if ( + !(items && items.filter(({ label }: any) => label === item).length > 0) + ) { + throw new Error("completion item did not appear"); + } + }; } function lint(lang: string) { diff --git a/frontend/src/app.ts b/frontend/src/app.ts index fca4d4a..fa24564 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -25,11 +25,13 @@ interface RijuConfig { id: string; monacoLang?: string; main: string; - format?: string; - lspDisableDynamicRegistration?: boolean; - lspInit?: any; - lspConfig?: any; - lspLang?: string; + format?: any; + lsp?: { + disableDynamicRegistration?: boolean; + init?: any; + config?: any; + lang?: string; + }; template: string; } @@ -91,13 +93,13 @@ class RijuMessageWriter extends AbstractMessageWriter { switch ((msg as any).method) { case "initialize": (msg as any).params.processId = null; - if (config.lspDisableDynamicRegistration) { + if (config.lsp!.disableDynamicRegistration) { this.disableDynamicRegistration(msg); } break; case "textDocument/didOpen": - if (config.lspLang) { - (msg as any).params.textDocument.languageId = config.lspLang; + if (config.lsp!.lang) { + (msg as any).params.textDocument.languageId = config.lsp!.lang; } } if (DEBUG) { @@ -201,7 +203,7 @@ async function main() { console.error("Unexpected message from server:", message); return; } - const services = MonacoServices.create(editor, { + const services = MonacoServices.create(editor as any, { rootUri: `file://${message.root}`, }); servicesDisposable = Services.install(services); @@ -230,12 +232,12 @@ async function main() { return Array( (configuration(params, token) as {}[]).length ).fill( - config.lspConfig !== undefined ? config.lspConfig : {} + config.lsp!.config !== undefined ? config.lsp!.config : {} ); }, }, }, - initializationOptions: config.lspInit || {}, + initializationOptions: config.lsp!.init || {}, }, connectionProvider: { get: (errorHandler: any, closeHandler: any) =>