/// The project the server checks against: the packages `bjo` fetched for it, /// which of them declared a .NET shared framework, and its NuGet packages. /// /// `bjo` writes these under the project's `.bjo` and passes them to the /// compiler as `--roots`, `--frameworks` and `--nuget`. A compiler process /// serves one roots file, since every cache keyed by a path would answer from /// a stale registry if the roots changed under it. So the server installs one /// project's, that of the first file it is asked about, before anything is /// resolved, and says so on a file it cannot serve as the command line would. module BjolangLsp.Project open System open System.IO /// The nearest directory at or above `dir` that holds a `manifest.bjodat`. let rec private containing (dir: string) : string option = if isNull dir then None elif File.Exists(Path.Combine(dir, "manifest.bjodat")) then Some dir else containing (Path.GetDirectoryName dir) /// The project a file is in, if any. let projectOf (path: string) : string option = containing (Path.GetDirectoryName(Path.GetFullPath path)) let private rootsFile (project: string) = Path.Combine(project, ".bjo", "roots") let private writtenAt (file: string) = if File.Exists file then Some(File.GetLastWriteTimeUtc file) else None type private State = /// Nothing asked about yet. | Undecided /// The project whose inputs are installed, and when its roots file was /// written then. `None` for a file in no project, or in a project whose /// packages were not fetched: the standard library only. | Serving of project: string option * roots: DateTime option /// The inputs did not install, and why. | Failed of project: string * message: string let mutable private state = Undecided /// Installs the inputs of the project `path` is in, the first time it is /// called, and answers what the file's diagnostics should say about it: why /// its packages are not seen as the command line sees them, or nothing. let prepare (path: string) : string option = let project = projectOf path if state = Undecided then state <- match project with | Some dir when File.Exists(rootsFile dir) -> let frameworks = Path.Combine(dir, ".bjo", "frameworks") let nuget = Path.Combine(dir, ".bjo", "nuget", "root") let installed = Bjolang.ProjectInputs.install (Some(rootsFile dir)) (if File.Exists frameworks then Some frameworks else None) [] (if Directory.Exists nuget then Some nuget else None) match installed with | Ok() -> Rpc.log $"checking against the packages of %s{dir}" Serving(Some dir, writtenAt (rootsFile dir)) | Error message -> Failed(dir, message) | Some _ | None -> Serving(None, None) match state, project with | Failed(dir, message), Some p when p = dir -> Some $"The packages of this project did not load, so only the standard library is seen:\n%s{message}" | Serving(Some dir, roots), Some p when p = dir -> if writtenAt (rootsFile dir) <> roots then Some "The project's packages changed after the language server started. Restart it to see them." else None | Serving(None, _), Some p when not (File.Exists(rootsFile p)) -> Some $"This project's packages are not fetched, so only the standard library is seen. Run `bjo fetch` in %s{p}, then restart the language server." | Serving(None, _), Some p -> Some $"The language server started on a file outside this project, and sees only the standard library. Restart it from a file in %s{p}." | (Serving(Some dir, _) | Failed(dir, _)), Some p -> Some $"The language server checks against the packages of %s{dir}. This file is in %s{p}: open it in a server of its own to see its packages." | _ -> None