diff --git a/server/darwin/rescript-editor-support.exe b/server/darwin/rescript-editor-support.exe new file mode 100755 index 000000000..da90bc08a Binary files /dev/null and b/server/darwin/rescript-editor-support.exe differ diff --git a/server/linux/rescript-editor-support.exe b/server/linux/rescript-editor-support.exe new file mode 100755 index 000000000..b07bd6139 Binary files /dev/null and b/server/linux/rescript-editor-support.exe differ diff --git a/server/src/RescriptEditorSupport.ts b/server/src/RescriptEditorSupport.ts new file mode 100644 index 000000000..44e550e84 --- /dev/null +++ b/server/src/RescriptEditorSupport.ts @@ -0,0 +1,90 @@ +import { fileURLToPath } from "url"; +import { RequestMessage } from "vscode-languageserver"; +import * as utils from "./utils"; +import * as path from "path"; +import { exec } from "child_process"; +import * as tmp from "tmp"; +import fs from "fs"; + +let binaryPath = path.join( + path.dirname(__dirname), + process.platform, + "rescript-editor-support.exe" +); + +export let binaryExists = fs.existsSync(binaryPath); + +let findExecutable = (uri: string) => { + let filePath = fileURLToPath(uri); + let projectRootPath = utils.findProjectRootOfFile(filePath); + if (projectRootPath == null || !binaryExists) { + return null; + } else { + return { binaryPath, filePath, cwd: projectRootPath }; + } +}; + +export function runDumpCommand( + msg: RequestMessage, + onResult: ( + result: { hover?: string; definition?: { uri?: string; range: any } } | null + ) => void +) { + let executable = findExecutable(msg.params.textDocument.uri); + if (executable == null) { + onResult(null); + } else { + let command = + executable.binaryPath + + " dump " + + executable.filePath + + ":" + + msg.params.position.line + + ":" + + msg.params.position.character; + exec(command, { cwd: executable.cwd }, function (_error, stdout, _stderr) { + let result = JSON.parse(stdout); + if (result && result[0]) { + onResult(result[0]); + } else { + onResult(null); + } + }); + } +} + +export function runCompletionCommand( + msg: RequestMessage, + code: string, + onResult: (result: [{ label: string }] | null) => void +) { + let executable = findExecutable(msg.params.textDocument.uri); + if (executable == null) { + onResult(null); + } else { + let tmpobj = tmp.fileSync(); + let tmpname = tmpobj.name; + fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); + + let command = + executable.binaryPath + + " complete " + + executable.filePath + + ":" + + msg.params.position.line + + ":" + + msg.params.position.character + + " " + + tmpname; + + exec(command, { cwd: executable.cwd }, function (_error, stdout, _stderr) { + tmpobj.removeCallback(); + let result = JSON.parse(stdout); + if (result && result[0]) { + onResult(result); + } else { + onResult(null); + } + }); + } +} diff --git a/server/src/server.ts b/server/src/server.ts index fcb873a75..527c64129 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -20,6 +20,11 @@ import * as chokidar from "chokidar"; import { assert } from "console"; import { fileURLToPath } from "url"; import { ChildProcess } from "child_process"; +import { + binaryExists, + runDumpCommand, + runCompletionCommand, +} from "./RescriptEditorSupport"; // https://microsoft.github.io/language-server-protocol/specification#initialize // According to the spec, there could be requests before the 'initialize' request. Link in comment tells how to handle them. @@ -268,8 +273,11 @@ process.on("message", (msg: m.Message) => { // TODO: incremental sync? textDocumentSync: v.TextDocumentSyncKind.Full, documentFormattingProvider: true, - hoverProvider: true, - definitionProvider: true, + hoverProvider: binaryExists, + definitionProvider: binaryExists, + completionProvider: binaryExists + ? { triggerCharacters: ["."] } + : undefined, }, }; let response: m.ResponseMessage = { @@ -313,32 +321,68 @@ process.on("message", (msg: m.Message) => { process.send!(response); } } else if (msg.method === p.HoverRequest.method) { - let dummyHoverResponse: m.ResponseMessage = { + let emptyHoverResponse: m.ResponseMessage = { jsonrpc: c.jsonrpcVersion, id: msg.id, // type result = Hover | null // type Hover = {contents: MarkedString | MarkedString[] | MarkupContent, range?: Range} - result: { contents: "Time to go for a 20k run!" }, + result: null, }; - - process.send!(dummyHoverResponse); + runDumpCommand(msg, (result) => { + if (result && result.hover) { + let hoverResponse: m.ResponseMessage = { + ...emptyHoverResponse, + result: { + contents: result.hover, + }, + }; + process.send!(hoverResponse); + } else { + process.send!(emptyHoverResponse); + } + }); } else if (msg.method === p.DefinitionRequest.method) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition - let dummyDefinitionResponse: m.ResponseMessage = { + let emptyDefinitionResponse: m.ResponseMessage = { jsonrpc: c.jsonrpcVersion, id: msg.id, // result should be: Location | Array | Array | null - result: { - uri: msg.params.textDocument.uri, - range: { - start: { line: 2, character: 4 }, - end: { line: 2, character: 12 }, - }, - }, + result: null, // error: code and message set in case an exception happens during the definition request. }; - process.send!(dummyDefinitionResponse); + runDumpCommand(msg, (result) => { + if (result && result.definition) { + let definitionResponse: m.ResponseMessage = { + ...emptyDefinitionResponse, + result: { + uri: result.definition.uri || msg.params.textDocument.uri, + range: result.definition.range, + }, + }; + process.send!(definitionResponse); + } else { + process.send!(emptyDefinitionResponse); + } + }); + } else if (msg.method === p.CompletionRequest.method) { + let emptyCompletionResponse: m.ResponseMessage = { + jsonrpc: c.jsonrpcVersion, + id: msg.id, + result: null, + }; + let code = getOpenedFileContent(msg.params.textDocument.uri); + runCompletionCommand(msg, code, (result) => { + if (result) { + let definitionResponse: m.ResponseMessage = { + ...emptyCompletionResponse, + result: result, + }; + process.send!(definitionResponse); + } else { + process.send!(emptyCompletionResponse); + } + }); } else if (msg.method === p.DocumentFormattingRequest.method) { // technically, a formatting failure should reply with the error. Sadly // the LSP alert box for these error replies sucks (e.g. doesn't actually diff --git a/server/win32/rescript-editor-support.exe b/server/win32/rescript-editor-support.exe new file mode 100755 index 000000000..3dd8a8cd5 Binary files /dev/null and b/server/win32/rescript-editor-support.exe differ