lean4-htt/src/Lean/Server/FileWorker/SetupFile.lean
Marc Huisinga 2ede81fe10
fix: search path related bugs (#7873)
This PR fixes a number of bugs related to the handling of the source
search path in the language server, where deleting files could cause
several features to stop functioning and both untitled files and files
that don't exist on disc could have conflicting module names.

In detail, it makes the following adjustments:
- The URI <-> module name conversion was adjusted to produce no name
collisions.
- File URIs in the search path yield a module name relative to the
search path, as before.
- File URIs not in the search path, non-file URIs and non-`.lean` files
yield a `«external:<full uri>»` module name.
- To avoid the issue of the URI -> module name conversion failing when a
file is deleted from disc, we now cache the result of this conversion in
the watchdog and the file worker when the file is first opened.
- All of the URI <-> module name conversions now consistently go through
`Server.documentUriFromModule?` and `moduleFromDocumentUri` to ensure
that we don't have minor deviations for this conversion all over the
place.
- The threading of the source search path through the file worker (from
`lake setup-file`) is removed. It turns out that `lake serve` already
sets the correct source search path in the environment, so we can just
always use the search path from the environment.
- Since we can now answer more requests that need the .ileans in
untitled files, a lot of the tests that test 'Go to definition' needed
to be adjusted so that they use the information from the watchdog, not
the file worker. As we load references asynchronously, this PR adds an
internal `$/lean/waitForILeans` request that tests can use to wait for
all .ilean files to be loaded and for the ilean references from the file
worker for the current document version to be finalized.
- As part of this PR, we noticed that the .ileans aren't available in
the NixOS setup, so @Kha adjusted the Nix CI to fix this.

### Breaking changes
- `Server.documentUriFromModule` has been renamed to
`Server.documentUriFromModule?` and doesn't take a `SearchPath` argument
anymore, as the `SearchPath` is now computed from the `LEAN_SRC_PATH`
environment variable. It has also been moved from `Lean.Server.GoTo` to
`Lean.Server.Utils`.
- `Server.moduleFromDocumentUri` does not take a `SearchPath` argument
anymore and won't return an `Option` anymore. It has also been moved
from `Lean.Server.GoTo` to `Lean.Server.Utils`.
- The `System.SearchPath.searchModuleNameOfUri` function has been
removed. It is recommended to use `Server.moduleFromDocumentUri`
instead.
- The `initSrcSearchPath` function has been renamed to
`getSrcSearchPath` and has been moved from `Lean.Util.Paths` to
`Lean.Util.Path`. It also doesn't need to take a `pkgSearchPath`
argument anymore.

---------

Co-authored-by: Sebastian Ullrich <sebasti@nullri.ch>
2025-04-09 15:37:49 +00:00

128 lines
4.6 KiB
Text

/-
Copyright (c) 2023 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Ullrich, Marc Huisinga
-/
prelude
import Init.System.IO
import Lean.Server.Utils
import Lean.Util.FileSetupInfo
import Lean.Util.LakePath
import Lean.LoadDynlib
import Lean.Server.ServerTask
namespace Lean.Server.FileWorker
open IO
structure LakeSetupFileOutput where
spawnArgs : Process.SpawnArgs
exitCode : UInt32
stdout : String
stderr : String
partial def runLakeSetupFile
(m : DocumentMeta)
(lakePath filePath : System.FilePath)
(imports : Array Import)
(handleStderr : String → IO Unit)
: IO LakeSetupFileOutput := do
let mut args := #["setup-file", filePath.toString] ++ imports.map (toString ·.module)
if m.dependencyBuildMode matches .never then
args := args.push "--no-build" |>.push "--no-cache"
let spawnArgs : Process.SpawnArgs := {
stdin := Process.Stdio.null
stdout := Process.Stdio.piped
stderr := Process.Stdio.piped
cmd := lakePath.toString
args
}
let lakeProc ← Process.spawn spawnArgs
let rec processStderr (acc : String) : IO String := do
let line ← lakeProc.stderr.getLine
if line == "" then
return acc
else
handleStderr line
processStderr (acc ++ line)
let stderr ← ServerTask.IO.asTask (processStderr "")
let stdout := String.trim (← lakeProc.stdout.readToEnd)
let stderr ← IO.ofExcept stderr.get
let exitCode ← lakeProc.wait
return ⟨spawnArgs, exitCode, stdout, stderr⟩
/-- Categorizes possible outcomes of running `lake setup-file`. -/
inductive FileSetupResultKind where
/-- File configuration loaded and dependencies updated successfully. -/
| success
/-- No Lake project found, no setup was done. -/
| noLakefile
/-- Imports must be rebuilt but `--no-build` was specified. -/
| importsOutOfDate
/-- Other error during Lake invocation. -/
| error (msg : String)
/-- Result of running `lake setup-file`. -/
structure FileSetupResult where
/-- Kind of outcome. -/
kind : FileSetupResultKind
/-- Additional options from successful setup, or else empty. -/
fileOptions : Options
/-- Lean plugins from successful setup, or else empty. -/
plugins : Array System.FilePath
def FileSetupResult.ofSuccess (fileOptions : Options)
(plugins : Array System.FilePath) : IO FileSetupResult := do return {
kind := FileSetupResultKind.success
fileOptions, plugins
}
def FileSetupResult.ofNoLakefile : IO FileSetupResult := do return {
kind := FileSetupResultKind.noLakefile
fileOptions := Options.empty
plugins := #[]
}
def FileSetupResult.ofImportsOutOfDate : IO FileSetupResult := do return {
kind := FileSetupResultKind.importsOutOfDate
fileOptions := Options.empty
plugins := #[]
}
def FileSetupResult.ofError (msg : String) : IO FileSetupResult := do return {
kind := FileSetupResultKind.error msg
fileOptions := Options.empty
plugins := #[]
}
/-- Uses `lake setup-file` to compile dependencies on the fly and add them to `LEAN_PATH`.
Compilation progress is reported to `handleStderr`. Returns the search path for
source files and the options for the file. -/
partial def setupFile (m : DocumentMeta) (imports : Array Import) (handleStderr : String → IO Unit) : IO FileSetupResult := do
let some filePath := System.Uri.fileUriToPath? m.uri
| return ← FileSetupResult.ofNoLakefile -- untitled files have no lakefile
let lakePath ← determineLakePath
if !(← System.FilePath.pathExists lakePath) then
return ← FileSetupResult.ofNoLakefile
let result ← runLakeSetupFile m lakePath filePath imports handleStderr
let cmdStr := " ".intercalate (toString result.spawnArgs.cmd :: result.spawnArgs.args.toList)
match result.exitCode with
| 0 =>
let Except.ok (info : FileSetupInfo) := Json.parse result.stdout >>= fromJson?
| return ← FileSetupResult.ofError s!"Invalid output from `{cmdStr}`:\n{result.stdout}\nstderr:\n{result.stderr}"
initSearchPath (← getBuildDir) info.paths.oleanPath
info.paths.loadDynlibPaths.forM loadDynlib
let pluginPaths ← info.paths.pluginPaths.mapM realPathNormalized
FileSetupResult.ofSuccess info.setupOptions.toOptions pluginPaths
| 2 => -- exit code for lake reporting that there is no lakefile
FileSetupResult.ofNoLakefile
| 3 => -- exit code for `--no-build`
FileSetupResult.ofImportsOutOfDate
| _ =>
FileSetupResult.ofError s!"`{cmdStr}` failed:\n{result.stdout}\nstderr:\n{result.stderr}"