diff --git a/package.json b/package.json index 950827877..8f7972c98 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,11 @@ "type": "boolean", "default": false, "description": "Automatically start ReScript's code analysis." + }, + "rescript.settings.binaryPath": { + "type": ["string", "null"], + "default": null, + "description": "Path to the directory where ReScript binaries are. You can use it if you haven't or don't want to use the installed ReScript from node_modules in your project." } } }, diff --git a/server/src/constants.ts b/server/src/constants.ts index 223a00f7b..f6e34e327 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -32,13 +32,20 @@ export let analysisProdPath = path.join( "rescript-editor-analysis.exe" ); +export let rescriptBinName = "rescript"; + +export let bsbBinName = "bsb"; + +export let bscBinName = "bsc"; + +export let nodeModulesBinDir = path.join("node_modules", ".bin"); + // can't use the native bsb/rescript since we might need the watcher -w flag, which is only in the JS wrapper export let rescriptNodePartialPath = path.join( - "node_modules", - ".bin", - "rescript" + nodeModulesBinDir, + rescriptBinName ); -export let bsbNodePartialPath = path.join("node_modules", ".bin", "bsb"); +export let bsbNodePartialPath = path.join(nodeModulesBinDir, bsbBinName); export let bsbLock = ".bsb.lock"; export let bsconfigPartialPath = "bsconfig.json"; diff --git a/server/src/server.ts b/server/src/server.ts index 855a7d84a..5f6d9ecf3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,6 +4,7 @@ import * as v from "vscode-languageserver"; import * as rpc from "vscode-jsonrpc/node"; import * as path from "path"; import fs from "fs"; +import os from "os"; // TODO: check DidChangeWatchedFilesNotification. import { DidOpenTextDocumentNotification, @@ -24,9 +25,11 @@ import { filesDiagnostics } from "./utils"; interface extensionConfiguration { askToStartBuild: boolean; + binaryPath: string | null; } let extensionConfiguration: extensionConfiguration = { askToStartBuild: true, + binaryPath: null, }; let pullConfigurationPeriodically: NodeJS.Timeout | null = null; @@ -62,6 +65,21 @@ let codeActionsFromDiagnostics: codeActions.filesCodeActions = {}; // will be properly defined later depending on the mode (stdio/node-rpc) let send: (msg: p.Message) => void = (_) => {}; +let findBuildBinary = (projectRootPath: p.DocumentUri) => + extensionConfiguration.binaryPath === null + ? utils.findBuildBinaryFromProjectRoot(projectRootPath) + : utils.findBuildBinaryFromConfig(extensionConfiguration.binaryPath); + +let findBscBinary = (filePath: p.DocumentUri) => + extensionConfiguration.binaryPath === null + ? utils.findBscBinaryFromProjectRoot(filePath) + : utils.findBscBinaryFromConfig(extensionConfiguration.binaryPath); + +let getConjecturalDirOfBuildBinary = (projectRootPath: p.DocumentUri) => + extensionConfiguration.binaryPath === null + ? path.join(projectRootPath, c.nodeModulesBinDir) + : extensionConfiguration.binaryPath; + interface CreateInterfaceRequestParams { uri: string; } @@ -232,7 +250,7 @@ let openedFile = (fileUri: string, fileContent: string) => { // TODO: sometime stale .bsb.lock dangling. bsb -w knows .bsb.lock is // stale. Use that logic // TODO: close watcher when lang-server shuts down - if (utils.findNodeBuildOfProjectRoot(projectRootPath) != null) { + if (findBuildBinary(projectRootPath) != null) { let payload: clientSentBuildAction = { title: c.startBuildAction, projectRootPath: projectRootPath, @@ -254,7 +272,17 @@ let openedFile = (fileUri: string, fileContent: string) => { // handle in the isResponseMessage check in the message handling way // below } else { - // we should send something to say that we can't find bsb.exe. But right now we'll silently not do anything + let request: p.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "window/showMessage", + params: { + type: p.MessageType.Error, + message: `Can't find ReScript binary in the directory ${getConjecturalDirOfBuildBinary( + projectRootPath + )}`, + }, + }; + send(request); } } @@ -593,7 +621,8 @@ function format(msg: p.RequestMessage): Array { } else { // code will always be defined here, even though technically it can be undefined let code = getOpenedFileContent(params.textDocument.uri); - let formattedResult = utils.formatCode(filePath, code); + let bscBinaryPath = findBscBinary(filePath); + let formattedResult = utils.formatCode(bscBinaryPath, filePath, code); if (formattedResult.kind === "success") { let max = code.length; let result: p.TextEdit[] = [ @@ -933,6 +962,17 @@ function onMessage(msg: p.Message) { if (initialConfiguration != null) { extensionConfiguration = initialConfiguration; + if ( + extensionConfiguration.binaryPath !== null && + extensionConfiguration.binaryPath[0] === "~" + ) { + // What should happen if the path contains the home directory symbol? + // This situation is handled below, but maybe it isn't the best option. + extensionConfiguration.binaryPath = path.join( + os.homedir(), + extensionConfiguration.binaryPath.slice(1) + ); + } } send(response); @@ -1041,7 +1081,7 @@ function onMessage(msg: p.Message) { // TODO: close watcher when lang-server shuts down. However, by Node's // default, these subprocesses are automatically killed when this // language-server process exits - let found = utils.findNodeBuildOfProjectRoot(projectRootPath); + let found = findBuildBinary(projectRootPath); if (found != null) { let bsbProcess = utils.runBuildWatcherUsingValidBuildPath( found.buildPath, diff --git a/server/src/utils.ts b/server/src/utils.ts index 0e814e470..344c37879 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -48,7 +48,7 @@ export let findProjectRootOfFile = ( // Also, if someone's ever formatting a regular project setup's dependency // (which is weird but whatever), they'll at least find an upward bs-platform // from the dependent. -export let findBscNativeOfFile = ( +export let findBscBinaryFromProjectRoot = ( source: p.DocumentUri ): null | p.DocumentUri => { let dir = path.dirname(source); @@ -66,25 +66,50 @@ export let findBscNativeOfFile = ( // reached the top return null; } else { - return findBscNativeOfFile(dir); + return findBscBinaryFromProjectRoot(dir); } }; +export let findBscBinaryFromConfig = ( + pathToBinaryDirFromConfig: p.DocumentUri +): null | p.DocumentUri => { + let bscPath = path.join(pathToBinaryDirFromConfig, c.bscBinName); + if (fs.existsSync(bscPath)) { + return bscPath; + } + return null; +}; + +// The function is to check that the build binary file exists +// and also determine what exactly the user is using ReScript or BuckleScript. // TODO: this doesn't handle file:/// scheme -export let findNodeBuildOfProjectRoot = ( - projectRootPath: p.DocumentUri -): null | { buildPath: p.DocumentUri; isReScript: boolean } => { - let rescriptNodePath = path.join(projectRootPath, c.rescriptNodePartialPath); - let bsbNodePath = path.join(projectRootPath, c.bsbNodePartialPath); - - if (fs.existsSync(rescriptNodePath)) { - return { buildPath: rescriptNodePath, isReScript: true }; - } else if (fs.existsSync(bsbNodePath)) { - return { buildPath: bsbNodePath, isReScript: false }; +let findBuildBinaryBase = ({ + rescriptPath, + bsbPath, +}: { + rescriptPath: p.DocumentUri; + bsbPath: p.DocumentUri; +}): null | { buildPath: p.DocumentUri; isReScript: boolean } => { + if (fs.existsSync(rescriptPath)) { + return { buildPath: rescriptPath, isReScript: true }; + } else if (fs.existsSync(bsbPath)) { + return { buildPath: bsbPath, isReScript: false }; } return null; }; +export let findBuildBinaryFromConfig = (pathToBinaryDirFromConfig: p.DocumentUri) => + findBuildBinaryBase({ + rescriptPath: path.join(pathToBinaryDirFromConfig, c.rescriptBinName), + bsbPath: path.join(pathToBinaryDirFromConfig, c.bsbBinName), + }); + +export let findBuildBinaryFromProjectRoot = (projectRootPath: p.DocumentUri) => + findBuildBinaryBase({ + rescriptPath: path.join(projectRootPath, c.rescriptNodePartialPath), + bsbPath: path.join(projectRootPath, c.bsbNodePartialPath), + }); + type execResult = | { kind: "success"; @@ -94,21 +119,22 @@ type execResult = kind: "error"; error: string; }; -export let formatCode = (filePath: string, code: string): execResult => { + +export let formatCode = ( + bscPath: p.DocumentUri | null, + filePath: string, + code: string +): execResult => { let extension = path.extname(filePath); let formatTempFileFullPath = createFileInTempDir(extension); fs.writeFileSync(formatTempFileFullPath, code, { encoding: "utf-8", }); try { - // See comment on findBscNativeDirOfFile for why we need - // to recursively search for bsc.exe upward - let bscNativePath = findBscNativeOfFile(filePath); - - // Default to using the project formatter. If not, use the one we ship with - // the analysis binary in the extension itself. - if (bscNativePath != null) { - let result = childProcess.execFileSync(bscNativePath, [ + // It will try to use the user formatting binary. + // If not, use the one we ship with the analysis binary in the extension itself. + if (bscPath != null) { + let result = childProcess.execFileSync(bscPath, [ "-color", "never", "-format", @@ -589,7 +615,7 @@ export let parseCompilerLogOutput = ( code: undefined, severity: t.DiagnosticSeverity.Error, tag: undefined, - content: [lines[i], lines[i+1]], + content: [lines[i], lines[i + 1]], }); i++; } else if (/^ +([0-9]+| +|\.) (│|┆)/.test(line)) { @@ -603,9 +629,9 @@ export let parseCompilerLogOutput = ( // 10 ┆ } else if (line.startsWith(" ")) { // part of the actual diagnostics message - parsedDiagnostics[parsedDiagnostics.length - 1].content.push( - line.slice(2) - ); + parsedDiagnostics[parsedDiagnostics.length - 1].content.push( + line.slice(2) + ); } else if (line.trim() != "") { // We'll assume that everything else is also part of the diagnostics too. // Most of these should have been indented 2 spaces; sadly, some of them