From 4637b8a6c6d65e80aa71b574b35e0539cc2ffb63 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Thu, 31 Jan 2019 18:24:17 +0300 Subject: [PATCH] WIP: support for ocamlmerlin-lsp --- package.json | 5 ++ src/client/index.ts | 134 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/package.json b/package.json index 2c14199..0582fb2 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,11 @@ "default": "ocamlmerlin", "description": "The path to the `ocamlmerlin` binary." }, + "reason.path.ocamlmerlin-lsp": { + "type": "string", + "default": null, + "description": "The path to the `ocamlmerlin-lsp` binary." + }, "reason.path.ocpindent": { "type": "string", "default": "ocp-indent", diff --git a/src/client/index.ts b/src/client/index.ts index 2a44e90..e42e3d1 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,10 +1,17 @@ +import * as fs from "fs"; import flatMap = require("lodash.flatmap"); import * as path from "path"; +import { promisify } from "util"; import * as vscode from "vscode"; import * as client from "vscode-languageclient"; import * as command from "./command"; import * as request from "./request"; +const exists = promisify(fs.exists); +const readFile = promisify(fs.readFile); + +const isWin = process.platform === "win32"; + class ClientWindow implements vscode.Disposable { public readonly merlin: vscode.StatusBarItem; constructor() { @@ -29,6 +36,133 @@ class ErrorHandler { } export async function launch(context: vscode.ExtensionContext): Promise { + if (await isEsyProject()) { + return launchMerlinLsp(context, true); + } else { + return launchOCamlLanguageServer(context); + } +} + +async function isEsyProject() { + const reasonConfig = vscode.workspace.getConfiguration("reason"); + const forceEsy = reasonConfig.get("forceEsy", false); + if (forceEsy) { + return true; + } + + // TODO: we need to use workspace.workspaceFolders here and run LSP server per + // root. For now we'll just run LSP per workspace. + const root = vscode.workspace.rootPath; + if (root == null) { + return false; + } + + const esyJson = path.join(root, "esy.json"); + const packageJson = path.join(root, "package.json"); + if (await exists(esyJson)) { + return true; + } else if (await exists(packageJson)) { + // package.json could be unrelated to esy, check if it has "esy" config + // then. + try { + const data = await readFile(packageJson, "utf8"); + const json = JSON.parse(data); + return json.esy != null; + } catch (_e) { + return false; + } + } + + return false; +} + +function getMerlinLspOptions(useEsy: boolean) { + const reasonConfig = vscode.workspace.getConfiguration("reason"); + let ocamlmerlinLsp = reasonConfig.get("path.ocamlmerlin-lsp", null); + if (ocamlmerlinLsp == null) { + ocamlmerlinLsp = isWin ? "ocamlmerlin-lsp.exe" : "ocamlmerlin-lsp"; + } + + let run; + if (useEsy) { + run = { + args: ["exec-command", "--include-current-env", ocamlmerlinLsp], + command: process.platform === "win32" ? "esy.cmd" : "esy", + }; + } else { + run = { + args: [], + command: ocamlmerlinLsp, + }; + } + + const serverOptions: client.ServerOptions = { + debug: { + ...run, + options: { + env: { + ...process.env, + MERLIN_LOG: "-", + OCAMLRUNPARAM: "b", + }, + }, + }, + run: { + ...run, + options: { + env: { + ...process.env, + MERLIN_LOG: "-", + OCAMLRUNPARAM: "b", + }, + }, + }, + }; + return serverOptions; +} + +export async function launchMerlinLsp(context: vscode.ExtensionContext, useEsy: boolean): Promise { + const serverOptions = getMerlinLspOptions(useEsy); + const reasonConfig = vscode.workspace.getConfiguration("reason"); + + const languages = reasonConfig.get("server.languages", ["ocaml", "reason"]); + const documentSelector = flatMap(languages, (language: string) => [ + { language, scheme: "file" }, + { language, scheme: "untitled" }, + ]); + const clientOptions: client.LanguageClientOptions = { + diagnosticCollectionName: "ocamlmerlin-lsp", + documentSelector, + errorHandler: new ErrorHandler(), + initializationOptions: reasonConfig, + outputChannelName: "Merlin Language Server", + stdioEncoding: "utf8", + synchronize: { + configurationSection: "reason", + fileEvents: [ + vscode.workspace.createFileSystemWatcher("**/.merlin"), + vscode.workspace.createFileSystemWatcher("**/*.ml"), + vscode.workspace.createFileSystemWatcher("**/*.re"), + vscode.workspace.createFileSystemWatcher("**/command-exec"), + vscode.workspace.createFileSystemWatcher("**/command-exec.bat"), + vscode.workspace.createFileSystemWatcher("**/_build"), + vscode.workspace.createFileSystemWatcher("**/_build/*"), + ], + }, + }; + const languageClient = new client.LanguageClient("Reason", serverOptions, clientOptions); + const window = new ClientWindow(); + const session = languageClient.start(); + context.subscriptions.push(window); + context.subscriptions.push(session); + await languageClient.onReady(); + command.registerAll(context, languageClient); + request.registerAll(context, languageClient); + window.merlin.text = "$(hubot) [merlin]"; + window.merlin.tooltip = "merlin server online"; +} + +export async function launchOCamlLanguageServer(context: vscode.ExtensionContext): Promise { const reasonConfig = vscode.workspace.getConfiguration("reason"); const module = context.asAbsolutePath(path.join("node_modules", "ocaml-language-server", "bin", "server")); const options = { execArgv: ["--nolazy", "--inspect=6009"] };