diff --git a/CHANGELOG.md b/CHANGELOG.md index 0374167fb..c34b1898a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ ## 1.52.0 +#### :rocket: New Feature + +- Experimental support for caching the project config to reduce latency. https://github.com/rescript-lang/rescript-vscode/pull/1000 + #### :bug: Bug Fix - Fix highlighting of other languages being affected by rescript-vscode. https://github.com/rescript-lang/rescript-vscode/pull/973 diff --git a/analysis/bin/main.ml b/analysis/bin/main.ml index c8af54330..259b1a300 100644 --- a/analysis/bin/main.ml +++ b/analysis/bin/main.ml @@ -110,6 +110,23 @@ let main () = path line col in match args with + | [_; "cache-project"; rootPath] -> ( + Cfg.readProjectConfigCache := false; + let uri = Uri.fromPath rootPath in + match Packages.getPackage ~uri with + | Some package -> Cache.cacheProject package + | None -> print_endline "\"ERR\"") + | [_; "cache-delete"; rootPath] -> ( + Cfg.readProjectConfigCache := false; + let uri = Uri.fromPath rootPath in + match Packages.findRoot ~uri (Hashtbl.create 0) with + | Some (`Bs rootPath) -> ( + match BuildSystem.getLibBs rootPath with + | None -> print_endline "\"ERR\"" + | Some libBs -> + Cache.deleteCache (Cache.targetFileFromLibBs libBs); + print_endline "\"OK\"") + | _ -> print_endline "\"ERR: Did not find root \"") | [_; "completion"; path; line; col; currentFile] -> printHeaderInfo path line col; Commands.completion ~debug ~path diff --git a/analysis/src/Cache.ml b/analysis/src/Cache.ml new file mode 100644 index 000000000..5e7b3203f --- /dev/null +++ b/analysis/src/Cache.ml @@ -0,0 +1,41 @@ +open SharedTypes + +type cached = { + projectFiles: FileSet.t; + dependenciesFiles: FileSet.t; + pathsForModule: (file, paths) Hashtbl.t; +} + +let writeCache filename (data : cached) = + let oc = open_out_bin filename in + Marshal.to_channel oc data []; + close_out oc + +let readCache filename = + if !Cfg.readProjectConfigCache && Sys.file_exists filename then + try + let ic = open_in_bin filename in + let data : cached = Marshal.from_channel ic in + close_in ic; + Some data + with _ -> None + else None + +let deleteCache filename = try Sys.remove filename with _ -> () + +let targetFileFromLibBs libBs = Filename.concat libBs ".project-files-cache" + +let cacheProject (package : package) = + let cached = + { + projectFiles = package.projectFiles; + dependenciesFiles = package.dependenciesFiles; + pathsForModule = package.pathsForModule; + } + in + match BuildSystem.getLibBs package.rootPath with + | None -> print_endline "\"ERR\"" + | Some libBs -> + let targetFile = targetFileFromLibBs libBs in + writeCache targetFile cached; + print_endline "\"OK\"" diff --git a/analysis/src/Cfg.ml b/analysis/src/Cfg.ml index cf06b28d2..bd4166d5a 100644 --- a/analysis/src/Cfg.ml +++ b/analysis/src/Cfg.ml @@ -9,3 +9,11 @@ let inIncrementalTypecheckingMode = | "true" -> true | _ -> false with _ -> false) + +let readProjectConfigCache = + ref + (try + match Sys.getenv "RESCRIPT_PROJECT_CONFIG_CACHE" with + | "true" -> true + | _ -> false + with _ -> false) diff --git a/analysis/src/Packages.ml b/analysis/src/Packages.ml index c3fd7d691..caeeda424 100644 --- a/analysis/src/Packages.ml +++ b/analysis/src/Packages.ml @@ -44,165 +44,167 @@ let newBsPackage ~rootPath = in match Json.parse raw with | Some config -> ( - match FindFiles.findDependencyFiles rootPath config with - | None -> None - | Some (dependencyDirectories, dependenciesFilesAndPaths) -> ( - match libBs with + let namespace = FindFiles.getNamespace config in + let rescriptVersion = getReScriptVersion () in + let suffix = + match config |> Json.get "suffix" with + | Some (String suffix) -> suffix + | _ -> ".js" + in + let uncurried = + let ns = config |> Json.get "uncurried" in + match (rescriptVersion, ns) with + | (major, _), None when major >= 11 -> Some true + | _, ns -> Option.bind ns Json.bool + in + let genericJsxModule = + let jsxConfig = config |> Json.get "jsx" in + match jsxConfig with + | Some jsxConfig -> ( + match jsxConfig |> Json.get "module" with + | Some (String m) when String.lowercase_ascii m <> "react" -> Some m + | _ -> None) | None -> None - | Some libBs -> - Some - (let namespace = FindFiles.getNamespace config in - let rescriptVersion = getReScriptVersion () in - let suffix = - match config |> Json.get "suffix" with - | Some (String suffix) -> suffix - | _ -> ".js" - in - let uncurried = - let ns = config |> Json.get "uncurried" in - match (rescriptVersion, ns) with - | (major, _), None when major >= 11 -> Some true - | _, ns -> Option.bind ns Json.bool - in - let genericJsxModule = - let jsxConfig = config |> Json.get "jsx" in - match jsxConfig with - | Some jsxConfig -> ( - match jsxConfig |> Json.get "module" with - | Some (String m) when String.lowercase_ascii m <> "react" -> - Some m - | _ -> None) - | None -> None - in - let uncurried = uncurried = Some true in - let sourceDirectories = - FindFiles.getSourceDirectories ~includeDev:true ~baseDir:rootPath - config - in - let projectFilesAndPaths = - FindFiles.findProjectFiles - ~public:(FindFiles.getPublic config) - ~namespace ~path:rootPath ~sourceDirectories ~libBs - in - projectFilesAndPaths - |> List.iter (fun (_name, paths) -> Log.log (showPaths paths)); - let pathsForModule = - makePathsForModule ~projectFilesAndPaths - ~dependenciesFilesAndPaths - in - let opens_from_namespace = - match namespace with - | None -> [] - | Some namespace -> - let cmt = Filename.concat libBs namespace ^ ".cmt" in - Log.log - ("############ Namespaced as " ^ namespace ^ " at " ^ cmt); - Hashtbl.add pathsForModule namespace (Namespace {cmt}); - let path = [FindFiles.nameSpaceToName namespace] in - [path] - in - Log.log - ("Dependency dirs: " - ^ String.concat " " - (dependencyDirectories |> List.map Utils.dumpPath)); - let opens_from_bsc_flags = - let bind f x = Option.bind x f in - match Json.get "bsc-flags" config |> bind Json.array with - | Some l -> - List.fold_left - (fun opens item -> - match item |> Json.string with - | None -> opens - | Some s -> ( - let parts = String.split_on_char ' ' s in - match parts with - | "-open" :: name :: _ -> - let path = name |> String.split_on_char '.' in - path :: opens - | _ -> opens)) - [] l - | None -> [] - in - let opens = - [ - (if uncurried then "PervasivesU" else "Pervasives"); - "JsxModules"; - ] - :: opens_from_namespace - |> List.rev_append opens_from_bsc_flags - |> List.map (fun path -> path @ ["place holder"]) - in - Log.log - ("Opens from ReScript config file: " - ^ (opens |> List.map pathToString |> String.concat " ")); - { - genericJsxModule; - suffix; - rescriptVersion; - rootPath; - projectFiles = - projectFilesAndPaths |> List.map fst |> FileSet.of_list; - dependenciesFiles = - dependenciesFilesAndPaths |> List.map fst |> FileSet.of_list; - pathsForModule; - opens; - namespace; - builtInCompletionModules = - (if - opens_from_bsc_flags - |> List.find_opt (fun opn -> - match opn with - | ["RescriptCore"] -> true - | _ -> false) - |> Option.is_some - then - { - arrayModulePath = ["Array"]; - optionModulePath = ["Option"]; - stringModulePath = ["String"]; - intModulePath = ["Int"]; - floatModulePath = ["Float"]; - promiseModulePath = ["Promise"]; - listModulePath = ["List"]; - resultModulePath = ["Result"]; - exnModulePath = ["Exn"]; - regexpModulePath = ["RegExp"]; - } - else if - opens_from_bsc_flags - |> List.find_opt (fun opn -> - match opn with - | ["Belt"] -> true - | _ -> false) - |> Option.is_some - then - { - arrayModulePath = ["Array"]; - optionModulePath = ["Option"]; - stringModulePath = ["Js"; "String2"]; - intModulePath = ["Int"]; - floatModulePath = ["Float"]; - promiseModulePath = ["Js"; "Promise"]; - listModulePath = ["List"]; - resultModulePath = ["Result"]; - exnModulePath = ["Js"; "Exn"]; - regexpModulePath = ["Js"; "Re"]; - } - else - { - arrayModulePath = ["Js"; "Array2"]; - optionModulePath = ["Belt"; "Option"]; - stringModulePath = ["Js"; "String2"]; - intModulePath = ["Belt"; "Int"]; - floatModulePath = ["Belt"; "Float"]; - promiseModulePath = ["Js"; "Promise"]; - listModulePath = ["Belt"; "List"]; - resultModulePath = ["Belt"; "Result"]; - exnModulePath = ["Js"; "Exn"]; - regexpModulePath = ["Js"; "Re"]; - }); - uncurried; - }))) + in + let uncurried = uncurried = Some true in + match libBs with + | None -> None + | Some libBs -> + let cached = Cache.readCache (Cache.targetFileFromLibBs libBs) in + let projectFiles, dependenciesFiles, pathsForModule = + match cached with + | Some cached -> + ( cached.projectFiles, + cached.dependenciesFiles, + cached.pathsForModule ) + | None -> + let dependenciesFilesAndPaths = + match FindFiles.findDependencyFiles rootPath config with + | None -> [] + | Some (_dependencyDirectories, dependenciesFilesAndPaths) -> + dependenciesFilesAndPaths + in + let sourceDirectories = + FindFiles.getSourceDirectories ~includeDev:true ~baseDir:rootPath + config + in + let projectFilesAndPaths = + FindFiles.findProjectFiles + ~public:(FindFiles.getPublic config) + ~namespace ~path:rootPath ~sourceDirectories ~libBs + in + let pathsForModule = + makePathsForModule ~projectFilesAndPaths + ~dependenciesFilesAndPaths + in + let projectFiles = + projectFilesAndPaths |> List.map fst |> FileSet.of_list + in + let dependenciesFiles = + dependenciesFilesAndPaths |> List.map fst |> FileSet.of_list + in + (projectFiles, dependenciesFiles, pathsForModule) + in + Some + (let opens_from_namespace = + match namespace with + | None -> [] + | Some namespace -> + let cmt = Filename.concat libBs namespace ^ ".cmt" in + Hashtbl.replace pathsForModule namespace (Namespace {cmt}); + let path = [FindFiles.nameSpaceToName namespace] in + [path] + in + let opens_from_bsc_flags = + let bind f x = Option.bind x f in + match Json.get "bsc-flags" config |> bind Json.array with + | Some l -> + List.fold_left + (fun opens item -> + match item |> Json.string with + | None -> opens + | Some s -> ( + let parts = String.split_on_char ' ' s in + match parts with + | "-open" :: name :: _ -> + let path = name |> String.split_on_char '.' in + path :: opens + | _ -> opens)) + [] l + | None -> [] + in + let opens = + [(if uncurried then "PervasivesU" else "Pervasives"); "JsxModules"] + :: opens_from_namespace + |> List.rev_append opens_from_bsc_flags + |> List.map (fun path -> path @ ["place holder"]) + in + { + genericJsxModule; + suffix; + rescriptVersion; + rootPath; + projectFiles; + dependenciesFiles; + pathsForModule; + opens; + namespace; + builtInCompletionModules = + (if + opens_from_bsc_flags + |> List.find_opt (fun opn -> + match opn with + | ["RescriptCore"] -> true + | _ -> false) + |> Option.is_some + then + { + arrayModulePath = ["Array"]; + optionModulePath = ["Option"]; + stringModulePath = ["String"]; + intModulePath = ["Int"]; + floatModulePath = ["Float"]; + promiseModulePath = ["Promise"]; + listModulePath = ["List"]; + resultModulePath = ["Result"]; + exnModulePath = ["Exn"]; + regexpModulePath = ["RegExp"]; + } + else if + opens_from_bsc_flags + |> List.find_opt (fun opn -> + match opn with + | ["Belt"] -> true + | _ -> false) + |> Option.is_some + then + { + arrayModulePath = ["Array"]; + optionModulePath = ["Option"]; + stringModulePath = ["Js"; "String2"]; + intModulePath = ["Int"]; + floatModulePath = ["Float"]; + promiseModulePath = ["Js"; "Promise"]; + listModulePath = ["List"]; + resultModulePath = ["Result"]; + exnModulePath = ["Js"; "Exn"]; + regexpModulePath = ["Js"; "Re"]; + } + else + { + arrayModulePath = ["Js"; "Array2"]; + optionModulePath = ["Belt"; "Option"]; + stringModulePath = ["Js"; "String2"]; + intModulePath = ["Belt"; "Int"]; + floatModulePath = ["Belt"; "Float"]; + promiseModulePath = ["Js"; "Promise"]; + listModulePath = ["Belt"; "List"]; + resultModulePath = ["Belt"; "Result"]; + exnModulePath = ["Js"; "Exn"]; + regexpModulePath = ["Js"; "Re"]; + }); + uncurried; + })) | None -> None in @@ -229,7 +231,7 @@ let findRoot ~uri packagesByRoot = let parent = Filename.dirname path in if parent = path then (* reached root *) None else loop parent in - loop (Filename.dirname path) + loop (if Sys.is_directory path then path else Filename.dirname path) let getPackage ~uri = let open SharedTypes in diff --git a/client/src/extension.ts b/client/src/extension.ts index 08bcfd1fa..350d88718 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -341,7 +341,8 @@ export function activate(context: ExtensionContext) { affectsConfiguration("rescript.settings.inlayHints") || affectsConfiguration("rescript.settings.codeLens") || affectsConfiguration("rescript.settings.signatureHelp") || - affectsConfiguration("rescript.settings.incrementalTypechecking") + affectsConfiguration("rescript.settings.incrementalTypechecking") || + affectsConfiguration("rescript.settings.cache") ) { commands.executeCommand("rescript-vscode.restart_language_server"); } else { diff --git a/package.json b/package.json index 69829b9cd..7bb695c2c 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,11 @@ "default": false, "description": "(debug) Enable debug logging (ends up in the extension output)." }, + "rescript.settings.cache.projectConfig.enabled": { + "type": "boolean", + "default": false, + "description": "(beta/experimental) Enable project config caching. Can speed up latency dramatically." + }, "rescript.settings.binaryPath": { "type": [ "string", diff --git a/server/src/config.ts b/server/src/config.ts index 9949d6bde..567f05585 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -21,6 +21,11 @@ export interface extensionConfiguration { acrossFiles?: boolean; debugLogging?: boolean; }; + cache?: { + projectConfig?: { + enabled?: boolean; + }; + }; } // All values here are temporary, and will be overridden as the server is @@ -43,7 +48,12 @@ let config: { extensionConfiguration: extensionConfiguration } = { incrementalTypechecking: { enabled: false, acrossFiles: false, - debugLogging: true, + debugLogging: false, + }, + cache: { + projectConfig: { + enabled: false, + }, }, }, }; diff --git a/server/src/constants.ts b/server/src/constants.ts index fd6e29472..a08525523 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -44,6 +44,7 @@ export let bsconfigPartialPath = "bsconfig.json"; export let rescriptJsonPartialPath = "rescript.json"; export let compilerDirPartialPath = path.join("lib", "bs"); export let compilerLogPartialPath = path.join("lib", "bs", ".compiler.log"); +export let buildNinjaPartialPath = path.join("lib", "bs", "build.ninja"); export let resExt = ".res"; export let resiExt = ".resi"; export let cmiExt = ".cmi"; diff --git a/server/src/server.ts b/server/src/server.ts index cfef4f572..2203ce851 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -205,20 +205,53 @@ let sendCompilationFinishedMessage = () => { send(notification); }; +let debug = false; + +let syncProjectConfigCache = (rootPath: string) => { + try { + if (debug) console.log("syncing project config cache for " + rootPath); + 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) => { + try { + if (debug) console.log("deleting project config cache for " + rootPath); + utils.runAnalysisAfterSanityCheck(rootPath, ["cache-delete", rootPath]); + if (debug) console.log("OK - deleted project config cache for " + rootPath); + } catch (e) { + if (debug) console.error(e); + } +}; + let compilerLogsWatcher = chokidar .watch([], { awaitWriteFinish: { stabilityThreshold: 1, }, }) - .on("all", (_e, _changedPath) => { - sendUpdatedDiagnostics(); - sendCompilationFinishedMessage(); - if (config.extensionConfiguration.inlayHints?.enable === true) { - sendInlayHintsRefresh(); - } - if (config.extensionConfiguration.codeLens === true) { - sendCodeLensRefresh(); + .on("all", (_e, changedPath) => { + if (changedPath.includes("build.ninja")) { + if ( + config.extensionConfiguration.cache?.projectConfig?.enabled === true + ) { + let projectRoot = utils.findProjectRootOfFile(changedPath); + if (projectRoot != null) { + syncProjectConfigCache(projectRoot); + } + } + } else { + sendUpdatedDiagnostics(); + sendCompilationFinishedMessage(); + if (config.extensionConfiguration.inlayHints?.enable === true) { + sendInlayHintsRefresh(); + } + if (config.extensionConfiguration.codeLens === true) { + sendCodeLensRefresh(); + } } }); let stopWatchingCompilerLog = () => { @@ -257,6 +290,14 @@ let openedFile = (fileUri: string, fileContent: string) => { compilerLogsWatcher.add( path.join(projectRootPath, c.compilerLogPartialPath) ); + if ( + config.extensionConfiguration.cache?.projectConfig?.enabled === true + ) { + compilerLogsWatcher.add( + path.join(projectRootPath, c.buildNinjaPartialPath) + ); + syncProjectConfigCache(projectRootPath); + } } let root = projectsFiles.get(projectRootPath)!; root.openFiles.add(filePath); @@ -335,6 +376,10 @@ let closedFile = (fileUri: string) => { compilerLogsWatcher.unwatch( path.join(projectRootPath, c.compilerLogPartialPath) ); + compilerLogsWatcher.unwatch( + path.join(projectRootPath, c.buildNinjaPartialPath) + ); + deleteProjectConfigCache(projectRootPath); deleteProjectDiagnostics(projectRootPath); if (root.bsbWatcherByEditor !== null) { root.bsbWatcherByEditor.kill(); diff --git a/server/src/utils.ts b/server/src/utils.ts index 8f952ffff..0fbe42a9c 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -190,6 +190,10 @@ export let runAnalysisAfterSanityCheck = ( config.extensionConfiguration.incrementalTypechecking?.enabled === true ? "true" : undefined, + RESCRIPT_PROJECT_CONFIG_CACHE: + config.extensionConfiguration.cache?.projectConfig?.enabled === true + ? "true" + : undefined, }, }; let stdout = "";