Skip to content

Commit b07d6d3

Browse files
committed
full-analysis: auto-instantiate environments and cache detection results
Add automatic `Pkg.instantiate()` for environments that have not been instantiated yet (e.g., freshly cloned repositories or new project directories). This allows full analysis to work immediately upon opening files in such environments, whether they are package source files, test files, or scripts with their own Project.toml. Also add caching for environment detection results to avoid redundant `Pkg.instantiate()` calls and `Base.identify_package_env` lookups for the same environment. The behavior is controlled by `full_analysis.auto_instantiate` config option (default: `true`).
1 parent fcd647a commit b07d6d3

File tree

5 files changed

+131
-20
lines changed

5 files changed

+131
-20
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1616
- Commit: [`HEAD`](https://github.com/aviatesk/JETLS.jl/commit/HEAD)
1717
- Diff: [`eda08b5...HEAD`](https://github.com/aviatesk/JETLS.jl/compare/eda08b5...HEAD)
1818

19+
### Added
20+
21+
- JETLS now automatically runs `Pkg.instantiate()` for packages that have not
22+
been instantiated yet (e.g., freshly cloned repositories). This allows
23+
full analysis to work immediately upon opening such packages. Note that this
24+
will automatically create a `Manifest.toml` file when the package has not been
25+
instantiated yet. This behavior is controlled by the `full_analysis.auto_instantiate`
26+
configuration option (default: `true`). Set it to `false` to disable.
27+
1928
### Fixed
2029

2130
- Fixed error when receiving notifications after shutdown request. The server

docs/src/configuration.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This documentation uses TOML format to describe the configuration schema.
88
```toml
99
[full_analysis]
1010
debounce = 1.0 # number (seconds), default: 1.0
11+
auto_instantiate = true # boolean, default: true
1112

1213
formatter = "Runic" # String preset: "Runic" (default) or "JuliaFormatter"
1314

@@ -33,6 +34,7 @@ executable = "testrunner" # string, default: "testrunner" (or "testrunner.bat"
3334

3435
- [`[full_analysis]`](@ref config/full_analysis)
3536
- [`[full_analysis] debounce`](@ref config/full_analysis-debounce)
37+
- [`[full_analysis] auto_instantiate`](@ref config/full_analysis-auto_instantiate)
3638
- [`formatter`](@ref config/formatter)
3739
- [`[diagnostic]`](@ref config/diagnostic)
3840
- [`[diagnostic] enabled`](@ref config/diagnostic-enabled)
@@ -58,6 +60,22 @@ may delay diagnostic updates.
5860
debounce = 2.0 # Wait 2 seconds after save before analyzing
5961
```
6062

63+
#### [`[full_analysis] auto_instantiate`](@id config/full_analysis-auto_instantiate)
64+
65+
- **Type**: boolean
66+
- **Default**: `true`
67+
68+
When enabled, JETLS automatically runs `Pkg.instantiate()` for packages that have
69+
not been instantiated yet (e.g., freshly cloned repositories). This allows full
70+
analysis to work immediately upon opening such packages. Note that this will
71+
automatically create a `Manifest.toml` file when the package has not been
72+
instantiated yet.
73+
74+
```toml
75+
[full_analysis]
76+
auto_instantiate = false # Disable automatic instantiation
77+
```
78+
6179
### [`formatter`](@id config/formatter)
6280

6381
- **Type**: string or table

jetls-client/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@
119119
"default": 1,
120120
"minimum": 0,
121121
"markdownDescription": "Debounce time in seconds before triggering full analysis after a document change."
122+
},
123+
"auto_instantiate": {
124+
"type": "boolean",
125+
"default": true,
126+
"markdownDescription": "When enabled, JETLS automatically runs `Pkg.instantiate()` for packages that have not been instantiated yet (e.g., freshly cloned repositories). This allows full analysis to work immediately upon opening such packages. Note that this will automatically create a `Manifest.toml` file when the package has not been instantiated yet."
122127
}
123128
}
124129
},

src/analysis/full-analysis.jl

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,17 @@ function request_analysis!(
5959
notify::Bool = true, # used by tests
6060
)
6161
manager = server.state.analysis_manager
62-
analysis_info = get_analysis_info(server.state.analysis_manager, uri)
63-
prev_analysis_result = nothing
64-
if isnothing(analysis_info)
65-
entry = lookup_analysis_entry(server.state, uri)
66-
elseif analysis_info isa OutOfScope
67-
entry = analysis_info
68-
else
69-
analysis_result = analysis_info::AnalysisResult # cached analysis result
70-
entry = analysis_result.entry
71-
prev_analysis_result = analysis_result
72-
end
73-
74-
if entry isa OutOfScope
75-
local outofscope = entry
62+
prev_analysis_result = get_analysis_info(server.state.analysis_manager, uri)
63+
local outofscope::OutOfScope
64+
if isnothing(prev_analysis_result)
65+
entry = lookup_analysis_entry(server, uri)
66+
if entry isa OutOfScope
67+
outofscope = entry
68+
@goto out_of_scope
69+
end
70+
elseif prev_analysis_result isa OutOfScope
71+
outofscope = prev_analysis_result
72+
@label out_of_scope
7673
store!(manager.cache) do cache
7774
if get(cache, uri, nothing) === outofscope
7875
cache, nothing
@@ -83,6 +80,9 @@ function request_analysis!(
8380
end
8481
end
8582
return nothing
83+
else
84+
prev_analysis_result::AnalysisResult
85+
entry = prev_analysis_result.entry
8686
end
8787
entry = entry::AnalysisEntry
8888

@@ -324,40 +324,114 @@ function new_analysis_result(request::AnalysisRequest, result)
324324
return AnalysisResult(entry, uri2diagnostics, analyzer, analyzed_file_infos, actual2virtual)
325325
end
326326

327-
function lookup_analysis_entry(state::ServerState, uri::URI)
327+
function ensure_instantiated(server::Server, env_path::String, context::String)
328+
if get_config(server.state.config_manager, :full_analysis, :auto_instantiate)
329+
try
330+
JETLS_DEV_MODE && @info "Instantiating package environment" env_path context
331+
Pkg.instantiate()
332+
catch e
333+
@error """Failed to instantiate package environment;
334+
Unable to instantiate the environment of the target package for analysis,
335+
so this package will be analyzed as a script instead.
336+
This may cause various features such as diagnostics to not function properly.
337+
It is recommended to fix the problem by referring to the following error""" env_path
338+
Base.showerror(stderr, e, catch_backtrace())
339+
show_warning_message(server, """
340+
Failed to instantiate package environment at $env_path.
341+
The package will be analyzed as a script, which may result in incomplete diagnostics.
342+
See the language server log for details.
343+
It is recommended to fix your environment setup and restart the language server.""")
344+
end
345+
end
346+
end
347+
348+
function ensure_instantiated_if_needed(server::Server, env_path::String, context::String)
349+
instantiated_envs = server.state.analysis_manager.instantiated_envs
350+
activate_do(env_path) do
351+
# Check if already processed (success or failure)
352+
if haskey(load(instantiated_envs), env_path)
353+
return
354+
end
355+
ensure_instantiated(server, env_path, context)
356+
# Mark as processed
357+
store!(instantiated_envs) do cache
358+
if haskey(cache, env_path)
359+
cache, nothing
360+
else
361+
new_cache = copy(cache)
362+
new_cache[env_path] = nothing
363+
new_cache, nothing
364+
end
365+
end
366+
end
367+
end
368+
369+
function lookup_analysis_entry(server::Server, uri::URI)
370+
state = server.state
328371
maybe_env_path = find_analysis_env_path(state, uri)
329372
if maybe_env_path isa OutOfScope
330-
return maybe_env_path
373+
outofscope = maybe_env_path
374+
return outofscope
331375
end
332376

333377
env_path = maybe_env_path
334378
if isnothing(env_path)
335379
return ScriptAnalysisEntry(uri)
336380
elseif uri.scheme == "untitled"
381+
ensure_instantiated_if_needed(server, env_path, "untitled")
337382
return ScriptInEnvAnalysisEntry(env_path, uri)
338383
end
339384

340385
pkgname = find_pkg_name(env_path)
386+
filepath = uri2filepath(uri)::String # uri.scheme === "file"
341387
if isnothing(pkgname)
388+
ensure_instantiated_if_needed(server, env_path, "script: $filepath")
342389
return ScriptInEnvAnalysisEntry(env_path, uri)
343390
end
344-
filepath = uri2filepath(uri)::String # uri.scheme === "file"
345391
filekind, filedir = find_package_directory(filepath, env_path)
346392
if filekind === :src
393+
instantiated_envs = server.state.analysis_manager.instantiated_envs
347394
return @something activate_do(env_path) do
395+
# Check cache inside lock to avoid race conditions
396+
cached = get(load(instantiated_envs), env_path, missing)
397+
if cached === nothing
398+
# Previously failed to detect package environment
399+
return ScriptInEnvAnalysisEntry(env_path, uri)
400+
elseif cached !== missing
401+
pkgid, pkgfileuri = cached
402+
return PackageSourceAnalysisEntry(env_path, pkgfileuri, pkgid)
403+
end
404+
# Cache miss - perform environment detection
405+
ensure_instantiated(server, env_path, "src: $filepath")
348406
pkgenv = @lock Base.require_lock @something Base.identify_package_env(pkgname) begin
349-
@warn "Failed to identify package environment" pkgname
407+
@warn "Failed to identify package environment" env_path pkgname filepath
408+
store!(instantiated_envs) do cache
409+
new_cache = copy(cache)
410+
new_cache[env_path] = nothing
411+
new_cache, nothing
412+
end
350413
return nothing
351414
end
352415
pkgid, env = pkgenv
353416
pkgfile = @something Base.locate_package(pkgid, env) begin
354417
@warn "Expected a package to have a source file" pkgname
418+
store!(instantiated_envs) do cache
419+
new_cache = copy(cache)
420+
new_cache[env_path] = nothing
421+
new_cache, nothing
422+
end
355423
return nothing
356424
end
357425
pkgfileuri = filepath2uri(pkgfile)
426+
store!(instantiated_envs) do cache
427+
new_cache = copy(cache)
428+
new_cache[env_path] = (pkgid, pkgfileuri)
429+
new_cache, nothing
430+
end
358431
PackageSourceAnalysisEntry(env_path, pkgfileuri, pkgid)
359432
end ScriptInEnvAnalysisEntry(env_path, uri)
360433
elseif filekind === :test
434+
ensure_instantiated_if_needed(server, env_path, "test: $filepath")
361435
runtestsfile = joinpath(filedir, "runtests.jl")
362436
runtestsuri = filepath2uri(runtestsfile)
363437
return PackageTestAnalysisEntry(env_path, runtestsuri)
@@ -366,6 +440,7 @@ function lookup_analysis_entry(state::ServerState, uri::URI)
366440
else
367441
@assert filekind === :script
368442
end
443+
ensure_instantiated_if_needed(server, env_path, "script: $filepath")
369444
return ScriptInEnvAnalysisEntry(env_path, uri)
370445
end
371446

src/types.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ const PendingAnalyses = CASContainer{Dict{AnalysisEntry,Union{Nothing,AnalysisRe
186186
const CurrentGenerations = CASContainer{Dict{AnalysisEntry,Int}}
187187
const AnalyzedGenerations = CASContainer{Dict{AnalysisEntry,Int}}
188188
const DebouncedRequests = LWContainer{Dict{AnalysisEntry,Timer}, LWStats}
189+
const InstantiatedEnvs = LWContainer{Dict{String,Union{Nothing,Tuple{Base.PkgId,URI}}}}
189190

190191
struct AnalysisManager
191192
cache::AnalysisCache
@@ -195,6 +196,7 @@ struct AnalysisManager
195196
current_generations::CurrentGenerations
196197
analyzed_generations::AnalyzedGenerations
197198
debounced::DebouncedRequests
199+
instantiated_envs::InstantiatedEnvs
198200
function AnalysisManager(n_workers::Int)
199201
return new(
200202
AnalysisCache(Dict{URI,AnalysisInfo}()),
@@ -203,7 +205,8 @@ struct AnalysisManager
203205
Vector{Task}(undef, n_workers),
204206
CurrentGenerations(Dict{AnalysisEntry,Int}()),
205207
AnalyzedGenerations(Dict{AnalysisEntry,Int}()),
206-
DebouncedRequests(Dict{AnalysisEntry,Timer}())
208+
DebouncedRequests(Dict{AnalysisEntry,Timer}()),
209+
InstantiatedEnvs(Dict{String,Union{Nothing,Tuple{Base.PkgId,URI}}}())
207210
)
208211
end
209212
end
@@ -316,8 +319,9 @@ _unwrap_maybe(::Type{T}) where {T} = T
316319

317320
@option struct FullAnalysisConfig <: ConfigSection
318321
debounce::Maybe{Float64}
322+
auto_instantiate::Maybe{Bool}
319323
end
320-
default_config(::Type{FullAnalysisConfig}) = FullAnalysisConfig(1.0)
324+
default_config(::Type{FullAnalysisConfig}) = FullAnalysisConfig(1.0, true)
321325

322326
@option struct TestRunnerConfig <: ConfigSection
323327
executable::Maybe{String}

0 commit comments

Comments
 (0)