diff --git a/.gitignore b/.gitignore index 432ac9ab6..5dc814def 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ rescript-tools.exe _opam/ _build/ + +*.tsbuildinfo \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 792849447..499e89aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ - Fix: JSON from `rescript-code-editor-analysis` was not always escaped properly, which prevented code actions from being available in certain situations https://github.com/rescript-lang/rescript-vscode/pull/1089 +#### :house: Internal + +- Find binary paths asynchronously. On `>=12.0.0-alpha.13` we do this by dynamically importing the `@rescript/{target}` package in the project root. https://github.com/rescript-lang/rescript-vscode/pull/1093 + ## 1.62.0 #### :nail_care: Polish diff --git a/package-lock.json b/package-lock.json index e45304ee5..61c020a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,10 @@ "license": "MIT", "devDependencies": { "@types/node": "^14.14.41", + "@types/semver": "^7.7.0", "@types/vscode": "1.68.0", "esbuild": "^0.20.1", - "semver": "^7.3.7", - "typescript": "^4.7.3" + "typescript": "^5.8.3" }, "engines": { "vscode": "^1.68.0" @@ -394,6 +394,13 @@ "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.68.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.68.0.tgz", @@ -438,51 +445,19 @@ "@esbuild/win32-x64": "0.20.1" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/typescript": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", - "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } }, "dependencies": { @@ -653,6 +628,12 @@ "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==", "dev": true }, + "@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true + }, "@types/vscode": { "version": "1.68.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.68.0.tgz", @@ -690,34 +671,10 @@ "@esbuild/win32-x64": "0.20.1" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "typescript": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", - "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true } } diff --git a/package.json b/package.json index 5f9ae3e91..2ca1b7915 100644 --- a/package.json +++ b/package.json @@ -255,9 +255,9 @@ }, "devDependencies": { "@types/node": "^14.14.41", + "@types/semver": "^7.7.0", "@types/vscode": "1.68.0", "esbuild": "^0.20.1", - "semver": "^7.3.7", - "typescript": "^4.7.3" + "typescript": "^5.8.3" } } diff --git a/server/package-lock.json b/server/package-lock.json index 5f0e16fb9..3c411217c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "chokidar": "^3.5.1", + "semver": "^7.7.2", "vscode-jsonrpc": "^8.0.1", "vscode-languageserver": "^8.0.1", "vscode-languageserver-protocol": "^3.17.1" @@ -175,6 +176,18 @@ "node": ">=8.10.0" } }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -324,6 +337,11 @@ "picomatch": "^2.2.1" } }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/server/package.json b/server/package.json index 19a30baa0..0df6affbb 100644 --- a/server/package.json +++ b/server/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "chokidar": "^3.5.1", + "semver": "^7.7.2", "vscode-jsonrpc": "^8.0.1", "vscode-languageserver": "^8.0.1", "vscode-languageserver-protocol": "^3.17.1" diff --git a/server/src/codeActions.ts b/server/src/codeActions.ts index d790f3616..77d82e3a2 100644 --- a/server/src/codeActions.ts +++ b/server/src/codeActions.ts @@ -141,14 +141,14 @@ let takeUntil = (array: string[], startsWith: string): string[] => { return res; }; -export let findCodeActionsInDiagnosticsMessage = ({ +export let findCodeActionsInDiagnosticsMessage = async ({ diagnostic, diagnosticMessage, file, range, addFoundActionsHere: codeActions, }: findCodeActionsConfig) => { - diagnosticMessage.forEach((line, index, array) => { + for (const [index, line] of diagnosticMessage.entries()) { // Because of how actions work, there can only be one per diagnostic. So, // halt whenever a code action has been found. let codeActionExtractors = [ @@ -166,8 +166,8 @@ export let findCodeActionsInDiagnosticsMessage = ({ let didFindAction = false; try { - didFindAction = extractCodeAction({ - array, + didFindAction = await extractCodeAction({ + array: diagnosticMessage, codeActions, diagnostic, file, @@ -183,7 +183,7 @@ export let findCodeActionsInDiagnosticsMessage = ({ break; } } - }); + } }; interface codeActionExtractorConfig { @@ -196,12 +196,12 @@ interface codeActionExtractorConfig { codeActions: filesCodeActions; } -type codeActionExtractor = (config: codeActionExtractorConfig) => boolean; +type codeActionExtractor = (config: codeActionExtractorConfig) => Promise; // This action extracts hints the compiler emits for misspelled identifiers, and // offers to replace the misspelled name with the correct name suggested by the // compiler. -let didYouMeanAction: codeActionExtractor = ({ +let didYouMeanAction: codeActionExtractor = async ({ codeActions, diagnostic, file, @@ -245,7 +245,7 @@ let didYouMeanAction: codeActionExtractor = ({ }; // This action offers to wrap patterns that aren't option in Some. -let wrapInSome: codeActionExtractor = ({ +let wrapInSome: codeActionExtractor = async ({ codeActions, diagnostic, file, @@ -425,7 +425,7 @@ let handleUndefinedRecordFieldsAction = ({ // being undefined. We then offers an action that inserts all of the record // fields, with an `assert false` dummy value. `assert false` is so applying the // code action actually compiles. -let addUndefinedRecordFieldsV10: codeActionExtractor = ({ +let addUndefinedRecordFieldsV10: codeActionExtractor = async ({ array, codeActions, diagnostic, @@ -459,7 +459,7 @@ let addUndefinedRecordFieldsV10: codeActionExtractor = ({ return false; }; -let addUndefinedRecordFieldsV11: codeActionExtractor = ({ +let addUndefinedRecordFieldsV11: codeActionExtractor = async ({ array, codeActions, diagnostic, @@ -508,7 +508,7 @@ let addUndefinedRecordFieldsV11: codeActionExtractor = ({ // This action detects suggestions of converting between mismatches in types // that the compiler tells us about. -let simpleConversion: codeActionExtractor = ({ +let simpleConversion: codeActionExtractor = async ({ line, codeActions, file, @@ -554,7 +554,7 @@ let simpleConversion: codeActionExtractor = ({ // This action will apply a curried function (essentially inserting a dot in the // correct place). -let applyUncurried: codeActionExtractor = ({ +let applyUncurried: codeActionExtractor = async ({ line, codeActions, file, @@ -608,7 +608,7 @@ let applyUncurried: codeActionExtractor = ({ // This action detects missing cases for exhaustive pattern matches, and offers // to insert dummy branches (using `failwith("TODO")`) for those branches. -let simpleAddMissingCases: codeActionExtractor = ({ +let simpleAddMissingCases: codeActionExtractor = async ({ line, codeActions, file, @@ -629,7 +629,7 @@ let simpleAddMissingCases: codeActionExtractor = ({ let filePath = fileURLToPath(file); - let newSwitchCode = utils.runAnalysisAfterSanityCheck(filePath, [ + let newSwitchCode = await utils.runAnalysisAfterSanityCheck(filePath, [ "codemod", filePath, range.start.line, @@ -665,7 +665,7 @@ let simpleAddMissingCases: codeActionExtractor = ({ // This detects concrete variables or values put in a position which expects an // optional of that same type, and offers to wrap the value/variable in // `Some()`. -let simpleTypeMismatches: codeActionExtractor = ({ +let simpleTypeMismatches: codeActionExtractor = async ({ line, codeActions, file, diff --git a/server/src/constants.ts b/server/src/constants.ts index a65bc133a..81d883b52 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -7,15 +7,8 @@ export let platformDir = // See https://microsoft.github.io/language-server-protocol/specification Abstract Message // version is fixed to 2.0 export let jsonrpcVersion = "2.0"; -export let platformPath = path.join("rescript", platformDir); -export let nodeModulesPlatformPath = path.join("node_modules", platformPath); -export let bscExeName = "bsc.exe"; -export let editorAnalysisName = "rescript-editor-analysis.exe"; -export let bscNativeReScriptPartialPath = path.join( - nodeModulesPlatformPath, - bscExeName -); +export let editorAnalysisName = "rescript-editor-analysis.exe"; export let builtinAnalysisDevPath = path.join( path.dirname(__dirname), "..", @@ -34,12 +27,6 @@ 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( - nodeModulesBinDir, - rescriptBinName -); - export let bsbLock = ".bsb.lock"; export let bsconfigPartialPath = "bsconfig.json"; export let rescriptJsonPartialPath = "rescript.json"; diff --git a/server/src/incrementalCompilation.ts b/server/src/incrementalCompilation.ts index 2cca64ae8..926b50342 100644 --- a/server/src/incrementalCompilation.ts +++ b/server/src/incrementalCompilation.ts @@ -6,6 +6,7 @@ import readline from "readline"; import { performance } from "perf_hooks"; import * as p from "vscode-languageserver-protocol"; import * as cp from "node:child_process"; +import semver from "semver"; import config, { send } from "./config"; import * as c from "./constants"; import * as chokidar from "chokidar"; @@ -573,8 +574,11 @@ async function figureOutBscArgs(entry: IncrementallyCompiledFileInfo) { }); callArgs.push("-color", "never"); - if (parseInt(project.rescriptVersion.split(".")[0] ?? "10") >= 11) { - // Only available in v11+ + // Only available in v11+ + if ( + semver.valid(project.rescriptVersion) && + semver.satisfies(project.rescriptVersion as string, ">=11", { includePrerelease: true }) + ) { callArgs.push("-ignore-parse-errors"); } @@ -619,7 +623,7 @@ async function compileContents( entry.project.bscBinaryLocation, callArgs, { cwd: entry.project.rootPath }, - (error, _stdout, stderr) => { + async (error, _stdout, stderr) => { if (!error?.killed) { if (debug()) console.log( @@ -644,7 +648,7 @@ async function compileContents( } // Reset compilation status as this compilation finished entry.compilation = null; - const { result, codeActions } = utils.parseCompilerLogOutput( + const { result, codeActions } = await utils.parseCompilerLogOutput( `${stderr}\n#Done()` ); diff --git a/server/src/server.ts b/server/src/server.ts index d7800c048..b1e3c02be 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -56,16 +56,14 @@ let codeActionsFromDiagnostics: codeActions.filesCodeActions = {}; // will be properly defined later depending on the mode (stdio/node-rpc) let send: (msg: p.Message) => void = (_) => {}; -let findRescriptBinary = (projectRootPath: p.DocumentUri | null) => - config.extensionConfiguration.binaryPath == null - ? lookup.findFilePathFromProjectRoot( - projectRootPath, - path.join(c.nodeModulesBinDir, c.rescriptBinName) - ) - : utils.findBinary( - config.extensionConfiguration.binaryPath, - c.rescriptBinName - ); +let findRescriptBinary = async (projectRootPath: p.DocumentUri | null): Promise => { + if (config.extensionConfiguration.binaryPath != null && + fs.existsSync(path.join(config.extensionConfiguration.binaryPath, "rescript"))) { + return path.join(config.extensionConfiguration.binaryPath, "rescript") + } + + return utils.findRescriptBinary(projectRootPath) +} let createInterfaceRequest = new v.RequestType< p.TextDocumentIdentifier, @@ -92,8 +90,9 @@ let getCurrentCompilerDiagnosticsForFile = ( return diagnostics ?? []; }; -let sendUpdatedDiagnostics = () => { - projectsFiles.forEach((projectFile, projectRootPath) => { + +let sendUpdatedDiagnostics = async () => { + for (const [projectRootPath, projectFile] of projectsFiles) { let { filesWithDiagnostics } = projectFile; let compilerLogPath = path.join(projectRootPath, c.compilerLogPartialPath); let content = fs.readFileSync(compilerLogPath, { encoding: "utf-8" }); @@ -102,7 +101,7 @@ let sendUpdatedDiagnostics = () => { result: filesAndErrors, codeActions, linesWithParseErrors, - } = utils.parseCompilerLogOutput(content); + } = await utils.parseCompilerLogOutput(content); if (linesWithParseErrors.length > 0) { let params: p.ShowMessageParams = { @@ -154,8 +153,9 @@ let sendUpdatedDiagnostics = () => { } }); } - }); + } }; + let deleteProjectDiagnostics = (projectRootPath: string) => { let root = projectsFiles.get(projectRootPath); if (root != null) { @@ -178,6 +178,7 @@ let deleteProjectDiagnostics = (projectRootPath: string) => { } } }; + let sendCompilationFinishedMessage = () => { let notification: p.NotificationMessage = { jsonrpc: c.jsonrpcVersion, @@ -189,20 +190,20 @@ let sendCompilationFinishedMessage = () => { let debug = false; -let syncProjectConfigCache = (rootPath: string) => { +let syncProjectConfigCache = async (rootPath: string) => { try { if (debug) console.log("syncing project config cache for " + rootPath); - utils.runAnalysisAfterSanityCheck(rootPath, ["cache-project", rootPath]); + await utils.runAnalysisAfterSanityCheck(rootPath, ["cache-project", rootPath]); if (debug) console.log("OK - synced project config cache for " + rootPath); } catch (e) { if (debug) console.error(e); } }; -let deleteProjectConfigCache = (rootPath: string) => { +let deleteProjectConfigCache = async (rootPath: string) => { try { if (debug) console.log("deleting project config cache for " + rootPath); - utils.runAnalysisAfterSanityCheck(rootPath, ["cache-delete", rootPath]); + await utils.runAnalysisAfterSanityCheck(rootPath, ["cache-delete", rootPath]); if (debug) console.log("OK - deleted project config cache for " + rootPath); } catch (e) { if (debug) console.error(e); @@ -215,17 +216,17 @@ let compilerLogsWatcher = chokidar stabilityThreshold: 1, }, }) - .on("all", (_e, changedPath) => { + .on("all", async (_e, changedPath) => { if (changedPath.includes("build.ninja")) { if (config.extensionConfiguration.cache?.projectConfig?.enable === true) { let projectRoot = utils.findProjectRootOfFile(changedPath); if (projectRoot != null) { - syncProjectConfigCache(projectRoot); + await syncProjectConfigCache(projectRoot); } } } else { try { - sendUpdatedDiagnostics(); + await sendUpdatedDiagnostics(); sendCompilationFinishedMessage(); if (config.extensionConfiguration.inlayHints?.enable === true) { sendInlayHintsRefresh(); @@ -247,7 +248,7 @@ type clientSentBuildAction = { title: string; projectRootPath: string; }; -let openedFile = (fileUri: string, fileContent: string) => { +let openedFile = async (fileUri: string, fileContent: string) => { let filePath = fileURLToPath(fileUri); stupidFileContentCache.set(filePath, fileContent); @@ -268,10 +269,10 @@ let openedFile = (fileUri: string, fileContent: string) => { filesDiagnostics: {}, namespaceName: namespaceName.kind === "success" ? namespaceName.result : null, - rescriptVersion: utils.findReScriptVersionForProjectRoot(projectRootPath), + rescriptVersion: await utils.findReScriptVersionForProjectRoot(projectRootPath), bsbWatcherByEditor: null, - bscBinaryLocation: utils.findBscExeBinary(projectRootPath), - editorAnalysisLocation: utils.findEditorAnalysisBinary(projectRootPath), + bscBinaryLocation: await utils.findBscExeBinary(projectRootPath), + editorAnalysisLocation: await utils.findEditorAnalysisBinary(projectRootPath), hasPromptedToStartBuild: /(\/|\\)node_modules(\/|\\)/.test( projectRootPath ) @@ -286,7 +287,7 @@ let openedFile = (fileUri: string, fileContent: string) => { compilerLogsWatcher.add( path.join(projectRootPath, c.buildNinjaPartialPath) ); - syncProjectConfigCache(projectRootPath); + await syncProjectConfigCache(projectRootPath); } } let root = projectsFiles.get(projectRootPath)!; @@ -302,7 +303,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 (findRescriptBinary(projectRootPath) != null) { + if (await findRescriptBinary(projectRootPath) != null) { let payload: clientSentBuildAction = { title: c.startBuildAction, projectRootPath: projectRootPath, @@ -347,7 +348,7 @@ let openedFile = (fileUri: string, fileContent: string) => { } }; -let closedFile = (fileUri: string) => { +let closedFile = async (fileUri: string) => { let filePath = fileURLToPath(fileUri); if (config.extensionConfiguration.incrementalTypechecking?.enable) { @@ -369,7 +370,7 @@ let closedFile = (fileUri: string) => { compilerLogsWatcher.unwatch( path.join(projectRootPath, c.buildNinjaPartialPath) ); - deleteProjectConfigCache(projectRootPath); + await deleteProjectConfigCache(projectRootPath); deleteProjectDiagnostics(projectRootPath); if (root.bsbWatcherByEditor !== null) { root.bsbWatcherByEditor.kill(); @@ -420,13 +421,13 @@ export default function listen(useStdio = false) { } } -function hover(msg: p.RequestMessage) { +async function hover(msg: p.RequestMessage) { let params = msg.params as p.HoverParams; let filePath = fileURLToPath(params.textDocument.uri); let code = getOpenedFileContent(params.textDocument.uri); let tmpname = utils.createFileInTempDir(); fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, [ "hover", @@ -442,11 +443,11 @@ function hover(msg: p.RequestMessage) { return response; } -function inlayHint(msg: p.RequestMessage) { +async function inlayHint(msg: p.RequestMessage) { const params = msg.params as p.InlayHintParams; const filePath = fileURLToPath(params.textDocument.uri); - const response = utils.runAnalysisCommand( + const response = await utils.runAnalysisCommand( filePath, [ "inlayHint", @@ -469,11 +470,11 @@ function sendInlayHintsRefresh() { send(request); } -function codeLens(msg: p.RequestMessage) { +async function codeLens(msg: p.RequestMessage) { const params = msg.params as p.CodeLensParams; const filePath = fileURLToPath(params.textDocument.uri); - const response = utils.runAnalysisCommand( + const response = await utils.runAnalysisCommand( filePath, ["codeLens", filePath], msg @@ -490,13 +491,13 @@ function sendCodeLensRefresh() { send(request); } -function signatureHelp(msg: p.RequestMessage) { +async function signatureHelp(msg: p.RequestMessage) { let params = msg.params as p.SignatureHelpParams; let filePath = fileURLToPath(params.textDocument.uri); let code = getOpenedFileContent(params.textDocument.uri); let tmpname = utils.createFileInTempDir(); fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, [ "signatureHelp", @@ -514,11 +515,11 @@ function signatureHelp(msg: p.RequestMessage) { return response; } -function definition(msg: p.RequestMessage) { +async function definition(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition let params = msg.params as p.DefinitionParams; let filePath = fileURLToPath(params.textDocument.uri); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, ["definition", filePath, params.position.line, params.position.character], msg @@ -526,11 +527,11 @@ function definition(msg: p.RequestMessage) { return response; } -function typeDefinition(msg: p.RequestMessage) { +async function typeDefinition(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specification/specification-current/#textDocument_typeDefinition let params = msg.params as p.TypeDefinitionParams; let filePath = fileURLToPath(params.textDocument.uri); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, [ "typeDefinition", @@ -543,11 +544,11 @@ function typeDefinition(msg: p.RequestMessage) { return response; } -function references(msg: p.RequestMessage) { +async function references(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references let params = msg.params as p.ReferenceParams; let filePath = fileURLToPath(params.textDocument.uri); - let result: typeof p.ReferencesRequest.type = utils.getReferencesForPosition( + let result: typeof p.ReferencesRequest.type = await utils.getReferencesForPosition( filePath, params.position ); @@ -560,11 +561,11 @@ function references(msg: p.RequestMessage) { return response; } -function prepareRename(msg: p.RequestMessage): p.ResponseMessage { +async function prepareRename(msg: p.RequestMessage): Promise { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_prepareRename let params = msg.params as p.PrepareRenameParams; let filePath = fileURLToPath(params.textDocument.uri); - let locations: null | p.Location[] = utils.getReferencesForPosition( + let locations: null | p.Location[] = await utils.getReferencesForPosition( filePath, params.position ); @@ -595,12 +596,12 @@ function prepareRename(msg: p.RequestMessage): p.ResponseMessage { }; } -function rename(msg: p.RequestMessage) { +async function rename(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename let params = msg.params as p.RenameParams; let filePath = fileURLToPath(params.textDocument.uri); let documentChanges: (p.RenameFile | p.TextDocumentEdit)[] | null = - utils.runAnalysisAfterSanityCheck(filePath, [ + await utils.runAnalysisAfterSanityCheck(filePath, [ "rename", filePath, params.position.line, @@ -619,7 +620,7 @@ function rename(msg: p.RequestMessage) { return response; } -function documentSymbol(msg: p.RequestMessage) { +async function documentSymbol(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol let params = msg.params as p.DocumentSymbolParams; let filePath = fileURLToPath(params.textDocument.uri); @@ -627,7 +628,7 @@ function documentSymbol(msg: p.RequestMessage) { let code = getOpenedFileContent(params.textDocument.uri); let tmpname = utils.createFileInTempDir(extension); fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, ["documentSymbol", tmpname], msg, @@ -655,7 +656,7 @@ function askForAllCurrentConfiguration() { send(req); } -function semanticTokens(msg: p.RequestMessage) { +async function semanticTokens(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens let params = msg.params as p.SemanticTokensParams; let filePath = fileURLToPath(params.textDocument.uri); @@ -663,7 +664,7 @@ function semanticTokens(msg: p.RequestMessage) { let code = getOpenedFileContent(params.textDocument.uri); let tmpname = utils.createFileInTempDir(extension); fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, ["semanticTokens", tmpname], msg, @@ -673,14 +674,14 @@ function semanticTokens(msg: p.RequestMessage) { return response; } -function completion(msg: p.RequestMessage) { +async function completion(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion let params = msg.params as p.ReferenceParams; let filePath = fileURLToPath(params.textDocument.uri); let code = getOpenedFileContent(params.textDocument.uri); let tmpname = utils.createFileInTempDir(); fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, [ "completion", @@ -695,7 +696,7 @@ function completion(msg: p.RequestMessage) { return response; } -function completionResolve(msg: p.RequestMessage) { +async function completionResolve(msg: p.RequestMessage) { const item = msg.params as p.CompletionItem; let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion, @@ -705,7 +706,7 @@ function completionResolve(msg: p.RequestMessage) { if (item.documentation == null && item.data != null) { const data = item.data as { filePath: string; modulePath: string }; - let result = utils.runAnalysisAfterSanityCheck( + let result = await utils.runAnalysisAfterSanityCheck( data.filePath, ["completionResolve", data.filePath, data.modulePath], true @@ -716,7 +717,7 @@ function completionResolve(msg: p.RequestMessage) { return response; } -function codeAction(msg: p.RequestMessage): p.ResponseMessage { +async function codeAction(msg: p.RequestMessage): Promise { let params = msg.params as p.CodeActionParams; let filePath = fileURLToPath(params.textDocument.uri); let code = getOpenedFileContent(params.textDocument.uri); @@ -738,7 +739,7 @@ function codeAction(msg: p.RequestMessage): p.ResponseMessage { ); fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, [ "codeAction", @@ -833,7 +834,7 @@ function format(msg: p.RequestMessage): Array { } } -let updateDiagnosticSyntax = (fileUri: string, fileContent: string) => { +let updateDiagnosticSyntax = async (fileUri: string, fileContent: string) => { if (config.extensionConfiguration.incrementalTypechecking?.enable) { // The incremental typechecking already sends syntax diagnostics. return; @@ -851,7 +852,7 @@ let updateDiagnosticSyntax = (fileUri: string, fileContent: string) => { let compilerDiagnosticsForFile = getCurrentCompilerDiagnosticsForFile(fileUri); let syntaxDiagnosticsForFile: p.Diagnostic[] = - utils.runAnalysisAfterSanityCheck(filePath, ["diagnosticSyntax", tmpname]); + await utils.runAnalysisAfterSanityCheck(filePath, ["diagnosticSyntax", tmpname]); let notification: p.NotificationMessage = { jsonrpc: c.jsonrpcVersion, @@ -867,7 +868,7 @@ let updateDiagnosticSyntax = (fileUri: string, fileContent: string) => { send(notification); }; -function createInterface(msg: p.RequestMessage): p.Message { +async function createInterface(msg: p.RequestMessage): Promise { let params = msg.params as p.TextDocumentIdentifier; let extension = path.extname(params.uri); let filePath = fileURLToPath(params.uri); @@ -949,7 +950,7 @@ function createInterface(msg: p.RequestMessage): p.Message { return response; } - let response = utils.runAnalysisCommand( + let response = await utils.runAnalysisCommand( filePath, ["createInterface", filePath, cmiPath], msg @@ -1036,7 +1037,7 @@ function openCompiledFile(msg: p.RequestMessage): p.Message { return response; } -function onMessage(msg: p.Message) { +async function onMessage(msg: p.Message) { if (p.Message.isNotification(msg)) { // notification message, aka the client ends it and doesn't want a reply if (!initialized && msg.method !== "exit") { @@ -1052,8 +1053,8 @@ function onMessage(msg: p.Message) { } } else if (msg.method === DidOpenTextDocumentNotification.method) { let params = msg.params as p.DidOpenTextDocumentParams; - openedFile(params.textDocument.uri, params.textDocument.text); - updateDiagnosticSyntax(params.textDocument.uri, params.textDocument.text); + await openedFile(params.textDocument.uri, params.textDocument.text); + await updateDiagnosticSyntax(params.textDocument.uri, params.textDocument.text); } else if (msg.method === DidChangeTextDocumentNotification.method) { let params = msg.params as p.DidChangeTextDocumentParams; let extName = path.extname(params.textDocument.uri); @@ -1067,7 +1068,7 @@ function onMessage(msg: p.Message) { params.textDocument.uri, changes[changes.length - 1].text ); - updateDiagnosticSyntax( + await updateDiagnosticSyntax( params.textDocument.uri, changes[changes.length - 1].text ); @@ -1075,7 +1076,7 @@ function onMessage(msg: p.Message) { } } else if (msg.method === DidCloseTextDocumentNotification.method) { let params = msg.params as p.DidCloseTextDocumentParams; - closedFile(params.textDocument.uri); + await closedFile(params.textDocument.uri); } else if (msg.method === DidChangeConfigurationNotification.type.method) { // Can't seem to get this notification to trigger, but if it does this will be here and ensure we're synced up at the server. askForAllCurrentConfiguration(); @@ -1221,51 +1222,51 @@ function onMessage(msg: p.Message) { send(response); } } else if (msg.method === p.HoverRequest.method) { - send(hover(msg)); + send(await hover(msg)); } else if (msg.method === p.DefinitionRequest.method) { - send(definition(msg)); + send(await definition(msg)); } else if (msg.method === p.TypeDefinitionRequest.method) { - send(typeDefinition(msg)); + send(await typeDefinition(msg)); } else if (msg.method === p.ReferencesRequest.method) { - send(references(msg)); + send(await references(msg)); } else if (msg.method === p.PrepareRenameRequest.method) { - send(prepareRename(msg)); + send(await prepareRename(msg)); } else if (msg.method === p.RenameRequest.method) { - send(rename(msg)); + send(await rename(msg)); } else if (msg.method === p.DocumentSymbolRequest.method) { - send(documentSymbol(msg)); + send(await documentSymbol(msg)); } else if (msg.method === p.CompletionRequest.method) { - send(completion(msg)); + send(await completion(msg)); } else if (msg.method === p.CompletionResolveRequest.method) { - send(completionResolve(msg)); + send(await completionResolve(msg)); } else if (msg.method === p.SemanticTokensRequest.method) { - send(semanticTokens(msg)); + send(await semanticTokens(msg)); } else if (msg.method === p.CodeActionRequest.method) { - send(codeAction(msg)); + send(await codeAction(msg)); } else if (msg.method === p.DocumentFormattingRequest.method) { let responses = format(msg); responses.forEach((response) => send(response)); } else if (msg.method === createInterfaceRequest.method) { - send(createInterface(msg)); + send(await createInterface(msg)); } else if (msg.method === openCompiledFileRequest.method) { send(openCompiledFile(msg)); } else if (msg.method === p.InlayHintRequest.method) { let params = msg.params as InlayHintParams; let extName = path.extname(params.textDocument.uri); if (extName === c.resExt) { - send(inlayHint(msg)); + send(await inlayHint(msg)); } } else if (msg.method === p.CodeLensRequest.method) { let params = msg.params as CodeLensParams; let extName = path.extname(params.textDocument.uri); if (extName === c.resExt) { - send(codeLens(msg)); + send(await codeLens(msg)); } } else if (msg.method === p.SignatureHelpRequest.method) { let params = msg.params as SignatureHelpParams; let extName = path.extname(params.textDocument.uri); if (extName === c.resExt) { - send(signatureHelp(msg)); + send(await signatureHelp(msg)); } } else { let response: p.ResponseMessage = { @@ -1308,7 +1309,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 rescriptBinaryPath = findRescriptBinary(projectRootPath); + let rescriptBinaryPath = await findRescriptBinary(projectRootPath); if (rescriptBinaryPath != null) { let bsbProcess = utils.runBuildWatcherUsingValidBuildPath( rescriptBinaryPath, @@ -1327,7 +1328,7 @@ onErrorReported((msg) => { let params: p.ShowMessageParams = { type: p.MessageType.Warning, message: `ReScript tooling: Internal error. Something broke. Here's the error message that you can report if you want: - + ${msg} (this message will only be reported once every 15 minutes)`, diff --git a/server/src/utils.ts b/server/src/utils.ts index dbb63a526..fd704a5ba 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -7,7 +7,9 @@ import { ResponseMessage, } from "vscode-languageserver-protocol"; import fs from "fs"; +import fsAsync from "fs/promises"; import * as os from "os"; +import semver from "semver"; import * as codeActions from "./codeActions"; import * as c from "./constants"; @@ -74,21 +76,72 @@ export let findProjectRootOfFile = ( } }; -// Check if binaryName exists inside binaryDirPath and return the joined path. -export let findBinary = ( - binaryDirPath: p.DocumentUri | null, - binaryName: string -): p.DocumentUri | null => { - if (binaryDirPath == null) { +// If ReScript < 12.0.0-alpha.13, then we want `{project_root}/node_modules/rescript/{c.platformDir}/{binary}`. +// Otherwise, we want to dynamically import `{project_root}/node_modules/rescript` and from `binPaths` get the relevant binary. +// We won't know which version is in the project root until we read and parse `{project_root}/node_modules/rescript/package.json` +let findBinary = async ( + projectRootPath: p.DocumentUri | null, + binary: "bsc.exe" | "rescript-editor-analysis.exe" | "rescript" +) => { + if (config.extensionConfiguration.platformPath != null) { + return path.join(config.extensionConfiguration.platformPath, binary); + } + + const rescriptDir = lookup.findFilePathFromProjectRoot( + projectRootPath, + path.join("node_modules", "rescript") + ); + if (rescriptDir == null) { return null; } - let binaryPath: p.DocumentUri = path.join(binaryDirPath, binaryName); - if (fs.existsSync(binaryPath)) { - return binaryPath; + + let rescriptVersion = null; + let rescriptJSWrapperPath = null + try { + const rescriptPackageJSONPath = path.join(rescriptDir, "package.json"); + const rescriptPackageJSON = JSON.parse(await fsAsync.readFile(rescriptPackageJSONPath, "utf-8")); + rescriptVersion = rescriptPackageJSON.version + rescriptJSWrapperPath = rescriptPackageJSON.bin.rescript + } catch (error) { + return null + } + + let binaryPath: string | null = null + if (binary == "rescript") { + // Can't use the native bsb/rescript since we might need the watcher -w + // flag, which is only in the JS wrapper + binaryPath = path.join(rescriptDir, rescriptJSWrapperPath) + } else if (semver.gte(rescriptVersion, "12.0.0-alpha.13")) { + // TODO: export `binPaths` from `rescript` package so that we don't need to + // copy the logic for figuring out `target`. + const target = `${process.platform}-${process.arch}`; + const targetPackagePath = path.join(rescriptDir, "..", `@rescript/${target}/bin.js`) + const { binPaths } = await import(targetPackagePath); + + if (binary == "bsc.exe") { + binaryPath = binPaths.bsc_exe + } else if (binary == "rescript-editor-analysis.exe") { + binaryPath = binPaths.rescript_editor_analysis_exe + } } else { - return null; + binaryPath = path.join(rescriptDir, c.platformDir, binary) } -}; + + if (binaryPath != null && fs.existsSync(binaryPath)) { + return binaryPath + } else { + return null + } +} + +export let findRescriptBinary = (projectRootPath: p.DocumentUri | null) => + findBinary(projectRootPath, "rescript"); + +export let findBscExeBinary = (projectRootPath: p.DocumentUri | null) => + findBinary(projectRootPath, "bsc.exe"); + +export let findEditorAnalysisBinary = (projectRootPath: p.DocumentUri | null) => + findBinary(projectRootPath, "rescript-editor-analysis.exe"); type execResult = | { @@ -140,31 +193,12 @@ export let formatCode = ( } }; -export let findReScriptVersion = ( - filePath: p.DocumentUri -): string | undefined => { - let projectRoot = findProjectRootOfFile(filePath); - if (projectRoot == null) { - return undefined; - } - - const bscExe = findBinary(findPlatformPath(projectRoot), c.bscExeName); - - if (bscExe == null) { - return undefined; - } - - try { - let version = childProcess.execSync(`${bscExe} -v`); - return version.toString().replace(/rescript/gi, "").trim(); - } catch (e) { - console.error("rescrip binary failed", e); +export async function findReScriptVersionForProjectRoot(projectRootPath: string | null): Promise { + if (projectRootPath == null) { return undefined; } -}; -export function findReScriptVersionForProjectRoot(projectRootPath:string) : string | undefined { - const bscExe = findBinary(findPlatformPath(projectRootPath), c.bscExeName); + const bscExe = await findBscExeBinary(projectRootPath) if (bscExe == null) { return undefined; @@ -186,7 +220,7 @@ if (fs.existsSync(c.builtinAnalysisDevPath)) { builtinBinaryPath = c.builtinAnalysisProdPath; } -export let runAnalysisAfterSanityCheck = ( +export let runAnalysisAfterSanityCheck = async ( filePath: p.DocumentUri, args: Array, projectRequired = false @@ -197,7 +231,7 @@ export let runAnalysisAfterSanityCheck = ( } let rescriptVersion = projectsFiles.get(projectRootPath ?? "")?.rescriptVersion ?? - findReScriptVersion(filePath); + await findReScriptVersionForProjectRoot(projectRootPath) let binaryPath = builtinBinaryPath; @@ -209,15 +243,8 @@ export let runAnalysisAfterSanityCheck = ( * with the extension itself. */ let shouldUseBuiltinAnalysis = - rescriptVersion?.startsWith("9.") || - rescriptVersion?.startsWith("10.") || - rescriptVersion?.startsWith("11.") || - [ - "12.0.0-alpha.1", - "12.0.0-alpha.2", - "12.0.0-alpha.3", - "12.0.0-alpha.4", - ].includes(rescriptVersion ?? ""); + semver.valid(rescriptVersion) && + semver.lt(rescriptVersion as string, "12.0.0-alpha.5"); if (!shouldUseBuiltinAnalysis && project != null) { binaryPath = project.editorAnalysisLocation; @@ -263,13 +290,13 @@ export let runAnalysisAfterSanityCheck = ( } }; -export let runAnalysisCommand = ( +export let runAnalysisCommand = async ( filePath: p.DocumentUri, args: Array, msg: RequestMessage, projectRequired = true ) => { - let result = runAnalysisAfterSanityCheck(filePath, args, projectRequired); + let result = await runAnalysisAfterSanityCheck(filePath, args, projectRequired); let response: ResponseMessage = { jsonrpc: c.jsonrpcVersion, id: msg.id, @@ -278,11 +305,11 @@ export let runAnalysisCommand = ( return response; }; -export let getReferencesForPosition = ( +export let getReferencesForPosition = async ( filePath: p.DocumentUri, position: p.Position ) => - runAnalysisAfterSanityCheck(filePath, [ + await runAnalysisAfterSanityCheck(filePath, [ "references", filePath, position.line, @@ -508,9 +535,9 @@ type parsedCompilerLogResult = { codeActions: codeActions.filesCodeActions; linesWithParseErrors: string[]; }; -export let parseCompilerLogOutput = ( +export let parseCompilerLogOutput = async ( content: string -): parsedCompilerLogResult => { +): Promise => { type parsedDiagnostic = { code: number | undefined; severity: t.DiagnosticSeverity; @@ -655,7 +682,7 @@ export let parseCompilerLogOutput = ( let result: filesDiagnostics = {}; let foundCodeActions: codeActions.filesCodeActions = {}; - parsedDiagnostics.forEach((parsedDiagnostic) => { + for (const parsedDiagnostic of parsedDiagnostics) { let [fileAndRangeLine, ...diagnosticMessage] = parsedDiagnostic.content; let { file, range } = parseFileAndRange(fileAndRangeLine); @@ -674,7 +701,7 @@ export let parseCompilerLogOutput = ( }; // Check for potential code actions - codeActions.findCodeActionsInDiagnosticsMessage({ + await codeActions.findCodeActionsInDiagnosticsMessage({ addFoundActionsHere: foundCodeActions, diagnostic, diagnosticMessage, @@ -683,7 +710,7 @@ export let parseCompilerLogOutput = ( }); result[file].push(diagnostic); - }); + } return { done, @@ -723,46 +750,3 @@ export let rangeContainsRange = ( } return true; }; - -let findPlatformPath = (projectRootPath: p.DocumentUri | null) => { - if (config.extensionConfiguration.platformPath != null) { - return config.extensionConfiguration.platformPath; - } - - let rescriptDir = lookup.findFilePathFromProjectRoot( - projectRootPath, - path.join("node_modules", "rescript") - ); - if (rescriptDir == null) { - return null; - } - - let platformPath = path.join(rescriptDir, c.platformDir); - - // Binaries have been split into optional platform-specific dependencies - // since v12.0.0-alpha.13 - if (!fs.existsSync(platformPath)) { - platformPath = path.join( - rescriptDir, - "..", - `@rescript/${process.platform}-${process.arch}/bin` - ) - } - - // Workaround for darwinarm64 which has no folder yet in ReScript <= 9.1.4 - if ( - process.platform == "darwin" && - process.arch == "arm64" && - !fs.existsSync(platformPath) - ) { - platformPath = path.join(rescriptDir, process.platform); - } - - return platformPath; -}; - -export let findBscExeBinary = (projectRootPath: p.DocumentUri | null) => - findBinary(findPlatformPath(projectRootPath), c.bscExeName); - -export let findEditorAnalysisBinary = (projectRootPath: p.DocumentUri | null) => - findBinary(findPlatformPath(projectRootPath), c.editorAnalysisName);