From 7481d8f9a634b99c0ef28f5a04ac3c1570754d88 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Tue, 8 Jul 2025 04:55:17 +0900 Subject: [PATCH 1/4] re-support untitled editors Those method errors are detected by new analysis pass --- LSP/src/URIs2/uri_helpers.jl | 3 ++- src/analysis/full-analysis.jl | 12 ++++++------ src/completions.jl | 8 ++++---- src/definition.jl | 1 + src/diagnostics.jl | 2 +- src/hover.jl | 6 +++--- src/signature-help.jl | 2 +- src/utils/binding.jl | 1 + src/utils/lsp.jl | 22 +++++++++++----------- src/utils/pkg.jl | 4 ++-- 10 files changed, 32 insertions(+), 29 deletions(-) diff --git a/LSP/src/URIs2/uri_helpers.jl b/LSP/src/URIs2/uri_helpers.jl index 162b9fa7c..b26d3665b 100644 --- a/LSP/src/URIs2/uri_helpers.jl +++ b/LSP/src/URIs2/uri_helpers.jl @@ -3,8 +3,9 @@ function uri2filename(uri::URI) return uri2filepath(uri)::String elseif uri.scheme == "untitled" return uri.path + else + error(lazy"Unsupported uri: $uri") end - return nothing end function uri2filepath(uri::URI) diff --git a/src/analysis/full-analysis.jl b/src/analysis/full-analysis.jl index ab9b309e7..fe2ccedfb 100644 --- a/src/analysis/full-analysis.jl +++ b/src/analysis/full-analysis.jl @@ -46,9 +46,9 @@ function begin_full_analysis_progress(server::Server, info::FullAnalysisInfo) if token === nothing return nothing end - filepath = uri2filepath(entryuri(info.entry)) + filename = uri2filename(entryuri(info.entry)) pre = info.reanalyze ? "Reanalyzing" : "Analyzing" - title = "$(pre) $(basename(filepath)) [$(entrykind(info.entry))]" + title = "$(pre) $(basename(filename)) [$(entrykind(info.entry))]" send(server, ProgressNotification(; params = ProgressParams(; token, @@ -77,7 +77,7 @@ function analyze_parsed_if_exist(server::Server, info::FullAnalysisInfo, args... fi = get_saved_file_info(server.state, uri) if !isnothing(fi) filename = uri2filename(uri) - @assert !isnothing(filename) "Unsupported URI: $uri" + @assert !isnothing(filename) lazy"Unsupported URI: $uri" parsed = build_tree!(JS.SyntaxNode, fi; filename) begin_full_analysis_progress(server, info) try @@ -87,7 +87,7 @@ function analyze_parsed_if_exist(server::Server, info::FullAnalysisInfo, args... end else filepath = uri2filepath(uri) - @assert filepath !== nothing "Unsupported URI: $uri" + @assert filepath !== nothing lazy"Unsupported URI: $uri" begin_full_analysis_progress(server, info) try return JET.analyze_and_report_file!(LSInterpreter(server, info), filepath, args...; jetconfigs...) @@ -117,7 +117,7 @@ function new_analysis_unit(entry::AnalysisEntry, result) successfully_analyzed_file_infos = copy(analyzed_file_infos) is_full_analysis_successful(result) || empty!(successfully_analyzed_file_infos) analysis_result = FullAnalysisResult( - #=staled=#false, result.res.actual2virtual, update_analyzer_world(result.analyzer), + #=staled=#false, result.res.actual2virtual::JET.Actual2Virtual, update_analyzer_world(result.analyzer), uri2diagnostics, analyzed_file_infos, successfully_analyzed_file_infos) return AnalysisUnit(entry, analysis_result) end @@ -219,7 +219,7 @@ function initiate_analysis_unit!(server::Server, uri::URI; token::Union{Nothing, elseif pkgname === nothing @goto analyze_script else # this file is likely one within a package - filepath = uri2filepath(uri) + filepath = uri2filepath(uri)::String # uri.scheme === "file" filekind, filedir = find_package_directory(filepath, env_path) if filekind === :script @goto analyze_script diff --git a/src/completions.jl b/src/completions.jl index 14f22e969..c400f176a 100644 --- a/src/completions.jl +++ b/src/completions.jl @@ -118,7 +118,7 @@ function to_completion(binding::JL.BindingInfo, JL.showprov(io, st; include_location=false) println(io) println(io, "```") - filepath = uri2filepath(uri) + filepath = uri2filename(uri) line, character = JS.source_location(st) showtext = "`@ " * simple_loc_text(filepath; line) * "`" println(io, create_source_location_link(filepath, showtext; line, character)) @@ -185,18 +185,18 @@ function global_completions!(items::Dict{String, CompletionItem}, state::ServerS # Case: `@│` if prev_kind === JS.K"@" - edit_start_pos = offset_to_xy(fi, JS.token_first_byte(fi.parsed_stream, prev_token_idx)) + edit_start_pos = offset_to_xy(fi, JS.token_first_byte(fi.parsed_stream, prev_token_idx::Int)) is_macro_invoke = true # Case: `@macr│` elseif prev_kind === JS.K"MacroName" - edit_start_pos = offset_to_xy(fi, JS.token_first_byte(fi.parsed_stream, prev_token_idx-1)) + edit_start_pos = offset_to_xy(fi, JS.token_first_byte(fi.parsed_stream, prev_token_idx::Int-1)) is_macro_invoke = true # Case `│` (empty program) elseif isnothing(prev_token_idx) edit_start_pos = Position(; line=0, character=0) is_macro_invoke = false elseif JS.is_identifier(prev_kind) - edit_start_pos = offset_to_xy(fi, JS.token_first_byte(fi.parsed_stream, prev_token_idx)) + edit_start_pos = offset_to_xy(fi, JS.token_first_byte(fi.parsed_stream, prev_token_idx::Int)) is_macro_invoke = false else # When completion is triggered within unknown scope (e.g., comment), diff --git a/src/definition.jl b/src/definition.jl index 8eb41c029..8fffd4892 100644 --- a/src/definition.jl +++ b/src/definition.jl @@ -35,6 +35,7 @@ For now, it just returns the first line of the method """ function LSP.Location(m::Method) file, line = functionloc(m) + file = file::String file = to_full_path(file) return Location(; uri = filename2uri(file), diff --git a/src/diagnostics.jl b/src/diagnostics.jl index 84cf2bc63..4a106bfb4 100644 --- a/src/diagnostics.jl +++ b/src/diagnostics.jl @@ -285,7 +285,7 @@ function handle_DocumentDiagnosticRequest(server::Server, msg::DocumentDiagnosti end parsed_stream = file_info.parsed_stream filename = uri2filename(uri) - @assert !isnothing(filename) "Unsupported URI: $uri" + @assert !isnothing(filename) lazy"Unsupported URI: $uri" if isempty(parsed_stream.diagnostics) diagnostics = lowering_diagnostics(file_info, filename) else diff --git a/src/hover.jl b/src/hover.jl index 1a04f6901..5cf072843 100644 --- a/src/hover.jl +++ b/src/hover.jl @@ -38,15 +38,15 @@ function handle_HoverRequest(server::Server, msg::HoverRequest) target_binding, definitions = target_binding_definitions io = IOBuffer() n = length(definitions) - filepath = uri2filepath(uri) + filename = uri2filename(uri) for (i, definition) in enumerate(definitions) println(io, "```julia") JL.showprov(io, definition; include_location=false) println(io) println(io, "```") line, character = JS.source_location(definition) - showtext = "`@ " * simple_loc_text(filepath; line) * "`" - println(io, create_source_location_link(filepath, showtext; line, character)) + showtext = "`@ " * simple_loc_text(filename; line) * "`" + println(io, create_source_location_link(filename, showtext; line, character)) if i ≠ n println(io, "\n---\n") # separator else diff --git a/src/signature-help.jl b/src/signature-help.jl index 45883c622..407d81b7e 100644 --- a/src/signature-help.jl +++ b/src/signature-help.jl @@ -384,7 +384,7 @@ function cursor_call(ps::JS.ParseStream, st0::JL.SyntaxTree, b::Int) bas = byte_ancestors(st0, pnb) # If the previous nontrivia byte is part of a call or macrocall, and it is # missing a closing paren, use that. - i = findfirst(st -> is_relevant_call(st) && !noparen_macrocall(st), bas) + i = findfirst(st::JL.SyntaxTree -> is_relevant_call(st) && !noparen_macrocall(st), bas) if !isnothing(i) basᵢ = bas[i] if JS.is_error(JS.children(basᵢ)[end]) diff --git a/src/utils/binding.jl b/src/utils/binding.jl index ea1664b9f..c85990007 100644 --- a/src/utils/binding.jl +++ b/src/utils/binding.jl @@ -27,6 +27,7 @@ let lowering_module = Module() ctx1, st1 = JL.expand_forms_1(lowering_module, remove_macrocalls(st0)); ctx2, st2 = JL.expand_forms_2(ctx1, st1); ctx3, st3 = JL.resolve_scopes(ctx2, st2); + @assert !isnothing(st3) return ctx3, st3 end end diff --git a/src/utils/lsp.jl b/src/utils/lsp.jl index ecd2c08da..3e59133d0 100644 --- a/src/utils/lsp.jl +++ b/src/utils/lsp.jl @@ -14,7 +14,7 @@ const DEFAULT_DOCUMENT_SELECTOR = DocumentFilter[ ] """ - create_source_location_link(filepath::AbstractString, [showtext::AbstractString]; + create_source_location_link(filename::AbstractString, [showtext::AbstractString]; line=nothing, character=nothing) Create a markdown-style link to a source location that can be displayed in LSP clients. @@ -24,9 +24,9 @@ not explicitly stated in the LSP specification, is supported by most LSP clients navigation to specific file locations. # Arguments -- `filepath::AbstractString`: The file path to link to +- `filename::AbstractString`: The file path to link to - `showtext::AbstractString`: Optional display text for the link. If not provided, - defaults to the filepath with optional line number + defaults to the filename with optional line number - `line::Union{Integer,Nothing}=nothing`: Optional 1-based line number - `character::Union{Integer,Nothing}=nothing`: Optional character position (requires `line` to be specified) @@ -45,10 +45,10 @@ create_source_location_link("/path/to/file.jl", line=42, character=10) # Returns: "[/path/to/file.jl:42](file:///path/to/file.jl#L42C10)" ``` """ -function create_source_location_link(filepath::AbstractString, showtext::AbstractString; +function create_source_location_link(filename::AbstractString, showtext::AbstractString; line::Union{Integer,Nothing}=nothing, character::Union{Integer,Nothing}=nothing) - linktext = string(filepath2uri(filepath)) + linktext = string(filename2uri(filename)) if line !== nothing linktext *= "#L$line" if character !== nothing @@ -58,15 +58,15 @@ function create_source_location_link(filepath::AbstractString, showtext::Abstrac return "[$showtext]($linktext)" end -function create_source_location_link(filepath::AbstractString; +function create_source_location_link(filename::AbstractString; line::Union{Integer,Nothing}=nothing, character::Union{Integer,Nothing}=nothing) - create_source_location_link(filepath, full_loc_text(filepath; line); line, character) + create_source_location_link(filename, full_loc_text(filename; line); line, character) end -function full_loc_text(filepath::AbstractString; +function full_loc_text(filename::AbstractString; line::Union{Integer,Nothing}=nothing) - loctext = filepath + loctext = filename Base.stacktrace_contract_userdir() && (loctext = Base.contractuser(loctext)) if line !== nothing loctext *= string(":", line) @@ -74,8 +74,8 @@ function full_loc_text(filepath::AbstractString; return loctext end -function simple_loc_text(filepath::AbstractString; line::Union{Integer,Nothing}=nothing) - loctext = basename(filepath) +function simple_loc_text(filename::AbstractString; line::Union{Integer,Nothing}=nothing) + loctext = basename(filename) if line !== nothing loctext *= string(":", line) end diff --git a/src/utils/pkg.jl b/src/utils/pkg.jl index b7665d374..53a9155e1 100644 --- a/src/utils/pkg.jl +++ b/src/utils/pkg.jl @@ -17,7 +17,7 @@ function find_analysis_env_path(state::ServerState, uri::URI) # try to analyze untitled editors using the root environment return isdefined(state, :root_env_path) ? state.root_env_path : nothing end - error("Unsupported URI: $uri") + error(lazy"Unsupported URI: $uri") end function find_uri_env_path(state::ServerState, uri::URI) @@ -28,7 +28,7 @@ function find_uri_env_path(state::ServerState, uri::URI) # try to analyze untitled editors using the root environment return isdefined(state, :root_env_path) ? state.root_env_path : nothing end - error("Unsupported URI: $uri") + error(lazy"Unsupported URI: $uri") end function find_pkg_name(env_path::AbstractString) From 579e094892b10e536025d7aa0d7700cf2613372c Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Tue, 8 Jul 2025 05:32:53 +0900 Subject: [PATCH 2/4] yet more solidify --- src/completions.jl | 5 ++- src/hover.jl | 5 ++- src/signature-help.jl | 4 +-- src/utils/lsp.jl | 74 ++++++++++++++++++++++++------------------ test/utils/test_lsp.jl | 15 +++++++-- 5 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/completions.jl b/src/completions.jl index c400f176a..73bcfb5d0 100644 --- a/src/completions.jl +++ b/src/completions.jl @@ -118,10 +118,9 @@ function to_completion(binding::JL.BindingInfo, JL.showprov(io, st; include_location=false) println(io) println(io, "```") - filepath = uri2filename(uri) line, character = JS.source_location(st) - showtext = "`@ " * simple_loc_text(filepath; line) * "`" - println(io, create_source_location_link(filepath, showtext; line, character)) + showtext = "`@ " * simple_loc_text(uri; line) * "`" + println(io, create_source_location_link(uri, showtext; line, character)) value = String(take!(io)) documentation = MarkupContent(; kind = MarkupKind.Markdown, diff --git a/src/hover.jl b/src/hover.jl index 5cf072843..9bc2d67a6 100644 --- a/src/hover.jl +++ b/src/hover.jl @@ -38,15 +38,14 @@ function handle_HoverRequest(server::Server, msg::HoverRequest) target_binding, definitions = target_binding_definitions io = IOBuffer() n = length(definitions) - filename = uri2filename(uri) for (i, definition) in enumerate(definitions) println(io, "```julia") JL.showprov(io, definition; include_location=false) println(io) println(io, "```") line, character = JS.source_location(definition) - showtext = "`@ " * simple_loc_text(filename; line) * "`" - println(io, create_source_location_link(filename, showtext; line, character)) + showtext = "`@ " * simple_loc_text(uri; line) * "`" + println(io, create_source_location_link(uri, showtext; line, character)) if i ≠ n println(io, "\n---\n") # separator else diff --git a/src/signature-help.jl b/src/signature-help.jl index 407d81b7e..2de5707c8 100644 --- a/src/signature-help.jl +++ b/src/signature-help.jl @@ -242,10 +242,10 @@ function make_siginfo(m::Method, ca::CallArgs, active_arg::Union{Int, Symbol}; documentation = let mdl = postprocessor(string(Base.parentmodule(m))) file, line = Base.updated_methodloc(m) - filepath = to_full_path(file) + filename = to_full_path(file) MarkupContent(; kind = MarkupKind.Markdown, - value = "@ `$(mdl)` " * create_source_location_link(filepath; line)) + value = "@ `$(mdl)` " * create_source_location_link(filename2uri(filename); line)) end # We could show the full docs, but there isn't a way to resolve items lazily diff --git a/src/utils/lsp.jl b/src/utils/lsp.jl index 3e59133d0..81e1d1c2b 100644 --- a/src/utils/lsp.jl +++ b/src/utils/lsp.jl @@ -14,59 +14,71 @@ const DEFAULT_DOCUMENT_SELECTOR = DocumentFilter[ ] """ - create_source_location_link(filename::AbstractString, [showtext::AbstractString]; - line=nothing, character=nothing) + create_source_location_link(uri::URI, showtext::Union{Nothing,AbstractString}=nothing; + line::Union{Integer,Nothing}=nothing, + character::Union{Integer,Nothing}=nothing) Create a markdown-style link to a source location that can be displayed in LSP clients. -This function generates links in the format `"[show text](file://path#L#C)"` which, while -not explicitly stated in the LSP specification, is supported by most LSP clients for -navigation to specific file locations. +This function generates clickable links in the format `[display text](uri#LC)` +that LSP clients can use to navigate to specific file locations. While not explicitly part of +the LSP specification, this markdown link format is widely supported by LSP clients including +VS Code, Neovim, and others. # Arguments -- `filename::AbstractString`: The file path to link to -- `showtext::AbstractString`: Optional display text for the link. If not provided, - defaults to the filename with optional line number -- `line::Union{Integer,Nothing}=nothing`: Optional 1-based line number -- `character::Union{Integer,Nothing}=nothing`: Optional character position (requires `line` to be specified) +- `uri::URI`: The file URI to link to +- `showtext::Union{Nothing,AbstractString}`: Optional display text for the link. + If unspecified, automatically generated using `full_loc_text` from the URI's filename. +- `line::Union{Integer,Nothing}=nothing`: Optional 1-based line number to link to +- `character::Union{Integer,Nothing}=nothing`: Optional 1-based character position within the line. + Note: `character` is only used when `line` is also specified. # Returns -A markdown-formatted string containing the clickable link. +A markdown-formatted string containing the clickable link that can be rendered in hover +documentation, completion items, or other LSP responses supporting markdown content. + +[remote file](http://example.com/file.jl#L5) # Examples ```julia -create_source_location_link("/path/to/file.jl") -# Returns: "[/path/to/file.jl](file:///path/to/file.jl)" - -create_source_location_link("/path/to/file.jl", line=42) +# Basic file link +uri = URI("file:///path/to/file.jl") +create_source_location_link(uri, "file.jl") +# Returns: "[file.jl](file:///path/to/file.jl)" + +# Link with line number +create_source_location_link(uri, "file.jl:42"; line=42) +# Returns: "[file.jl:42](file:///path/to/file.jl#L42)" + +# Link with line and character position +create_source_location_link(uri, "file.jl:42:10"; line=42, character=10) +# Returns: "[file.jl:42:10](file:///path/to/file.jl#L42C10)" + +# Using URI with automatic display text +uri = URI("file:///path/to/file.jl") +create_source_location_link(uri; line=42) # Returns: "[/path/to/file.jl:42](file:///path/to/file.jl#L42)" - -create_source_location_link("/path/to/file.jl", line=42, character=10) -# Returns: "[/path/to/file.jl:42](file:///path/to/file.jl#L42C10)" ``` """ -function create_source_location_link(filename::AbstractString, showtext::AbstractString; +function create_source_location_link(uri::URI, + showtext::Union{Nothing,AbstractString}; line::Union{Integer,Nothing}=nothing, character::Union{Integer,Nothing}=nothing) - linktext = string(filename2uri(filename)) + linktext = string(uri) if line !== nothing linktext *= "#L$line" if character !== nothing linktext *= "C$character" end end + if isnothing(showtext) + showtext = full_loc_text(uri; line) + end return "[$showtext]($linktext)" end -function create_source_location_link(filename::AbstractString; - line::Union{Integer,Nothing}=nothing, - character::Union{Integer,Nothing}=nothing) - create_source_location_link(filename, full_loc_text(filename; line); line, character) -end - -function full_loc_text(filename::AbstractString; - line::Union{Integer,Nothing}=nothing) - loctext = filename +function full_loc_text(uri::URI; line::Union{Integer,Nothing}=nothing) + loctext = uri2filename(uri) Base.stacktrace_contract_userdir() && (loctext = Base.contractuser(loctext)) if line !== nothing loctext *= string(":", line) @@ -74,8 +86,8 @@ function full_loc_text(filename::AbstractString; return loctext end -function simple_loc_text(filename::AbstractString; line::Union{Integer,Nothing}=nothing) - loctext = basename(filename) +function simple_loc_text(uri::URI; line::Union{Integer,Nothing}=nothing) + loctext = basename(uri2filename(uri)) if line !== nothing loctext *= string(":", line) end diff --git a/test/utils/test_lsp.jl b/test/utils/test_lsp.jl index ca3bd425f..3f904d859 100644 --- a/test/utils/test_lsp.jl +++ b/test/utils/test_lsp.jl @@ -3,11 +3,20 @@ module test_lsp using Test using JETLS: JETLS using JETLS.LSP +using JETLS.LSP.URIs2 @testset "create_source_location_link" begin - @test JETLS.create_source_location_link("/path/to/file.jl") == "[/path/to/file.jl](file:///path/to/file.jl)" - @test JETLS.create_source_location_link("/path/to/file.jl", line=42) == "[/path/to/file.jl:42](file:///path/to/file.jl#L42)" - @test JETLS.create_source_location_link("/path/to/file.jl", line=42, character=10) == "[/path/to/file.jl:42](file:///path/to/file.jl#L42C10)" + uri = URI("file:///path/to/file.jl") + @test JETLS.create_source_location_link(uri) == "[/path/to/file.jl](file:///path/to/file.jl)" + @test JETLS.create_source_location_link(uri; line=42) == "[/path/to/file.jl:42](file:///path/to/file.jl#L42)" + @test JETLS.create_source_location_link(uri; line=42, character=10) == "[/path/to/file.jl:42](file:///path/to/file.jl#L42C10)" + @test JETLS.create_source_location_link(uri, "file.jl") == "[file.jl](file:///path/to/file.jl)" + @test JETLS.create_source_location_link(uri, "file.jl:42"; line=42) == "[file.jl:42](file:///path/to/file.jl#L42)" + @test JETLS.create_source_location_link(uri, "file.jl:42:10"; line=42, character=10) == "[file.jl:42:10](file:///path/to/file.jl#L42C10)" + @test JETLS.create_source_location_link(uri; character=10) == "[/path/to/file.jl](file:///path/to/file.jl)" + + http_uri = URI("http://example.com/file.jl") + @test JETLS.create_source_location_link(http_uri, "remote file"; line=5) == "[remote file](http://example.com/file.jl#L5)" end @testset "Position comparison" begin From 4690156be75c089ee79ee3ec6292c87d7e248b4a Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Tue, 8 Jul 2025 06:20:27 +0900 Subject: [PATCH 3/4] Update lsp.jl --- src/utils/lsp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/lsp.jl b/src/utils/lsp.jl index 81e1d1c2b..97790e67a 100644 --- a/src/utils/lsp.jl +++ b/src/utils/lsp.jl @@ -61,7 +61,7 @@ create_source_location_link(uri; line=42) ``` """ function create_source_location_link(uri::URI, - showtext::Union{Nothing,AbstractString}; + showtext::Union{Nothing,AbstractString} = nothing; line::Union{Integer,Nothing}=nothing, character::Union{Integer,Nothing}=nothing) linktext = string(uri) From 3dfb0ee6f5ac7f3468f6ddc15b257c4fd02e1bfb Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Tue, 8 Jul 2025 06:21:12 +0900 Subject: [PATCH 4/4] Update lsp.jl --- src/utils/lsp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/lsp.jl b/src/utils/lsp.jl index 97790e67a..9b4e2aed3 100644 --- a/src/utils/lsp.jl +++ b/src/utils/lsp.jl @@ -61,7 +61,7 @@ create_source_location_link(uri; line=42) ``` """ function create_source_location_link(uri::URI, - showtext::Union{Nothing,AbstractString} = nothing; + showtext::Union{Nothing,AbstractString}=nothing; line::Union{Integer,Nothing}=nothing, character::Union{Integer,Nothing}=nothing) linktext = string(uri)