From 3c26b914591cd43612a8d2340de5978691e476ec Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Fri, 6 May 2022 17:26:46 +0200 Subject: [PATCH 01/55] Initial InlayHint provider implementation --- src/languageserverinstance.jl | 1 + src/protocol/features.jl | 43 ++++++++++++++++++++++++++++ src/protocol/initialize.jl | 1 + src/protocol/messagedefs.jl | 1 + src/requests/features.jl | 54 +++++++++++++++++++++++++++++++++++ src/requests/init.jl | 1 + src/requests/signatures.jl | 37 ++++++++++++++++-------- 7 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index 32ab1616..6ebf00a6 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -364,6 +364,7 @@ function Base.run(server::LanguageServerInstance) msg_dispatcher[julia_getDocFromWord_request_type] = request_wrapper(julia_getDocFromWord_request, server) msg_dispatcher[textDocument_selectionRange_request_type] = request_wrapper(textDocument_selectionRange_request, server) msg_dispatcher[textDocument_documentLink_request_type] = request_wrapper(textDocument_documentLink_request, server) + msg_dispatcher[textDocument_inlayHint_request_type] = request_wrapper(textDocument_inlayHint_request, server) # The exit notification message should not be wrapped in request_wrapper (which checks # if the server have been requested to be shut down). Instead, this message needs to be diff --git a/src/protocol/features.jl b/src/protocol/features.jl index e8e0d777..c7d429ef 100644 --- a/src/protocol/features.jl +++ b/src/protocol/features.jl @@ -296,6 +296,49 @@ end arguments::Union{Vector{Any},Missing} end +############################################################################## +# inlay hints +@dict_readable struct InlayHintOptions <: Outbound + workDoneToken::Union{Int,String,Missing} # ProgressToken + resolveProvider::Bool +end + +@dict_readable struct InlayHintRegistrationOptions <: Outbound + workDoneToken::Union{Int,String,Missing} # ProgressToken + resolveProvider::Bool # InlayHintOptions + id::Union{Missing, String} # StaticRegistrationOptions + documentSelector::Union{Nothing, DocumentSelector} # TextDocumentRegistrationOptions +end + +@dict_readable struct InlayHintParams <: Outbound + textDocument::TextDocumentIdentifier + range::Range + workDoneToken::Union{Int,String,Missing} # ProgressToken +end + +@dict_readable struct InlayHintLabelPart <: Outbound + value::String + tooltip::Union{Missing, String, MarkupContent} + location::Union{Missing, Location} + command::Union{Missing, Command} +end + +const InlayHintKind = Int +const InlayHintKinds = ( + Type = 1, + Parameter = 2 +) + +@dict_readable struct InlayHint <: Outbound + position::Position + label::Union{String, Vector{InlayHintLabelPart}} + kind::Union{Missing, InlayHintKind} + textEdits::Union{Missing, Vector{TextEdit}} + tooltip::Union{Missing, String, MarkupContent} + paddingLeft::Union{Missing, Bool} + paddingRight::Union{Missing, Bool} + data::Union{Missing, Any} +end ############################################################################## diff --git a/src/protocol/initialize.jl b/src/protocol/initialize.jl index fa4cc395..62d204df 100644 --- a/src/protocol/initialize.jl +++ b/src/protocol/initialize.jl @@ -183,6 +183,7 @@ struct ServerCapabilities <: Outbound foldingRangeProvider::Union{Bool,FoldingRangeOptions,FoldingRangeRegistrationOptions,Missing} executeCommandProvider::Union{ExecuteCommandOptions,Missing} selectionRangeProvider::Union{Bool,SelectionRangeOptions,SelectionRangeRegistrationOptions,Missing} + inlayHintProvider::Union{Bool,InlayHintOptions,InlayHintRegistrationOptions} workspaceSymbolProvider::Union{Bool,Missing} workspace::Union{WorkspaceOptions,Missing} experimental::Union{Any,Missing} diff --git a/src/protocol/messagedefs.jl b/src/protocol/messagedefs.jl index 5af22e93..4b7a22d7 100644 --- a/src/protocol/messagedefs.jl +++ b/src/protocol/messagedefs.jl @@ -19,6 +19,7 @@ const textDocument_willSaveWaitUntil_request_type = JSONRPC.RequestType("textDoc const textDocument_publishDiagnostics_notification_type = JSONRPC.NotificationType("textDocument/publishDiagnostics", PublishDiagnosticsParams) const textDocument_selectionRange_request_type = JSONRPC.RequestType("textDocument/selectionRange", SelectionRangeParams, Union{Vector{SelectionRange}, Nothing}) const textDocument_documentLink_request_type = JSONRPC.RequestType("textDocument/documentLink", DocumentLinkParams, Union{Vector{DocumentLink}, Nothing}) +const textDocument_inlayHint_request_type = JSONRPC.RequestType("textDocument/inlayHint", InlayHintParams, Union{Vector{InlayHint}, Nothing}) const workspace_executeCommand_request_type = JSONRPC.RequestType("workspace/executeCommand", ExecuteCommandParams, Any) const workspace_symbol_request_type = JSONRPC.RequestType("workspace/symbol", WorkspaceSymbolParams, Union{Vector{SymbolInformation}, Nothing}) diff --git a/src/requests/features.jl b/src/requests/features.jl index 04583045..2784f0cc 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -539,3 +539,57 @@ function get_selection_range_of_expr(x::EXPR) l2, c2 = get_position_at(doc, offset + x.span) SelectionRange(Range(l1, c1, l2, c2), get_selection_range_of_expr(x.parent)) end + +function textDocument_inlayHint_request(params::InlayHintParams, server::LanguageServerInstance, conn)::Union{Vector{InlayHint}, Nothing} + doc = getdocument(server, params.textDocument.uri) + + # FIXME: this should take params.range into account! + + return collect_inlay_hints(getcst(doc), server, doc) +end + +function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, pos=0, hints=InlayHint[]) + if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) + if parentof(x).args[1] != x + sigs = collect_signatures(x, doc, server) + if !isempty(sigs) + args = length(parentof(x).args) - 1 + if args > 0 + filter!(s -> length(s.parameters) == args, sigs) + if !isempty(sigs) + pars = first(sigs).parameters + thisarg = 0 + for a in parentof(x).args + if x == a + break + end + thisarg += 1 + end + if thisarg <= args && thisarg <= length(pars) + push!( + hints, + InlayHint( + Position(get_position_at(doc, pos)...), + string(pars[thisarg].label, ':'), + InlayHintKinds.Parameter, + missing, + pars[thisarg].documentation, + false, + true, + missing + ) + ) + end + end + end + end + end + end + if length(x) > 0 + for a in x + collect_inlay_hints(a, server, doc, pos, hints) + pos += a.fullspan + end + end + return hints +end diff --git a/src/requests/init.jl b/src/requests/init.jl index aa6ada50..29f927fa 100644 --- a/src/requests/init.jl +++ b/src/requests/init.jl @@ -30,6 +30,7 @@ function ServerCapabilities(client::ClientCapabilities) ExecuteCommandOptions(missing, collect(keys(LSActions))), true, true, + true, WorkspaceOptions(WorkspaceFoldersOptions(true, true)), missing ) diff --git a/src/requests/signatures.jl b/src/requests/signatures.jl index 197935d6..67682e1f 100644 --- a/src/requests/signatures.jl +++ b/src/requests/signatures.jl @@ -1,9 +1,31 @@ function textDocument_signatureHelp_request(params::TextDocumentPositionParams, server::LanguageServerInstance, conn) doc = getdocument(server, params.textDocument.uri) - sigs = SignatureInformation[] + offset = get_offset(doc, params.position) x = get_expr(getcst(doc), offset) - arg = 0 + + sigs = collect_signatures(x, doc, server) + + if (isempty(sigs) || (headof(x) === :RPAREN)) + return SignatureHelp(SignatureInformation[], 0, 0) + end + + arg = fcall_arg_number(x) + + return SignatureHelp(filter(s -> length(s.parameters) > arg, sigs), 0, arg) +end + +function fcall_arg_number(x) + if headof(x) === :LPAREN + 0 + else + sum(headof(a) === :COMMA for a in parentof(x).trivia) + end +end + +function collect_signatures(x, doc, server) + sigs = SignatureInformation[] + if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) if CSTParser.isidentifier(parentof(x).args[1]) call_name = parentof(x).args[1] @@ -18,22 +40,13 @@ function textDocument_signatureHelp_request(params::TextDocumentPositionParams, get_signatures(f_binding, tls, sigs, getenv(doc, server)) end end - if (isempty(sigs) || (headof(x) === :RPAREN)) - return SignatureHelp(SignatureInformation[], 0, 0) - end - if headof(x) === :LPAREN - arg = 0 - else - arg = sum(headof(a) === :COMMA for a in parentof(x).trivia) - end - return SignatureHelp(filter(s -> length(s.parameters) > arg, sigs), 0, arg) + return sigs end function get_signatures(b, tls::StaticLint.Scope, sigs::Vector{SignatureInformation}, env) end function get_signatures(b::StaticLint.Binding, tls::StaticLint.Scope, sigs::Vector{SignatureInformation}, env) - if b.val isa StaticLint.Binding get_signatures(b.val, tls, sigs, env) end From a963d55212a650b81ed0e079df84786d7938c2b6 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 9 May 2022 14:55:07 +0200 Subject: [PATCH 02/55] Fixes --- src/requests/features.jl | 25 +++++++++++++++++-------- src/utilities.jl | 1 + 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index 2784f0cc..0246109b 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -188,7 +188,7 @@ function textDocument_range_formatting_request(params::DocumentRangeFormattingPa doc = getdocument(server, params.textDocument.uri) cst = getcst(doc) - expr = get_inner_expr(cst, get_offset(doc, params.range.start):get_offset(doc, params.range.stop)) + expr = get_inner_expr(cst, get_offset(doc, params.range)) if expr === nothing return nothing @@ -540,16 +540,18 @@ function get_selection_range_of_expr(x::EXPR) SelectionRange(Range(l1, c1, l2, c2), get_selection_range_of_expr(x.parent)) end -function textDocument_inlayHint_request(params::InlayHintParams, server::LanguageServerInstance, conn)::Union{Vector{InlayHint}, Nothing} +function textDocument_inlayHint_request(params::InlayHintParams, server::LanguageServerInstance, conn)::Union{Vector{InlayHint},Nothing} doc = getdocument(server, params.textDocument.uri) - # FIXME: this should take params.range into account! + start, stop = get_offset(doc, params.range.start), get_offset(doc, params.range.stop) - return collect_inlay_hints(getcst(doc), server, doc) + return collect_inlay_hints(getcst(doc), server, doc, start, stop) end -function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, pos=0, hints=InlayHint[]) - if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) +function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start, stop, pos=0, hints=InlayHint[]) + if x isa EXPR && parentof(x) isa EXPR && + CSTParser.iscall(parentof(x)) && + !(parentof(parentof(x)) isa EXPR && CSTParser.defines_function(parentof(parentof(x)))) if parentof(x).args[1] != x sigs = collect_signatures(x, doc, server) if !isempty(sigs) @@ -566,11 +568,15 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, pos=0 thisarg += 1 end if thisarg <= args && thisarg <= length(pars) + label = pars[thisarg].label + if label == "#unused#" + label = "_" + end push!( hints, InlayHint( Position(get_position_at(doc, pos)...), - string(pars[thisarg].label, ':'), + string(label, ':'), InlayHintKinds.Parameter, missing, pars[thisarg].documentation, @@ -587,8 +593,11 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, pos=0 end if length(x) > 0 for a in x - collect_inlay_hints(a, server, doc, pos, hints) + if pos < stop && pos + a.fullspan > start + collect_inlay_hints(a, server, doc, start, stop, pos, hints) + end pos += a.fullspan + pos > stop && break end end return hints diff --git a/src/utilities.jl b/src/utilities.jl index 1beabef5..36f5a6c8 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -190,6 +190,7 @@ function get_expr(x, offset::UnitRange{Int}, pos=0, ignorewhitespace=false) end end +get_inner_expr(doc::Document, rng::Range) = get_inner_expr(getcst(doc), get_offset(doc, rng)) # full (not only trivia) expr containing rng, modulo whitespace function get_inner_expr(x, rng::UnitRange{Int}, pos=0, pos_span = 0) if all(pos .> rng) From 8e3e093b7b4be2937b26034f356428143a9b7e34 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 9 May 2022 15:09:25 +0200 Subject: [PATCH 03/55] add assignment hints --- src/requests/features.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/requests/features.jl b/src/requests/features.jl index 0246109b..42c278e7 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -590,6 +590,25 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start end end end + elseif x isa EXPR && parentof(x) isa EXPR && CSTParser.isassignment(parentof(x)) && parentof(x).args[1] == x + if StaticLint.hasbinding(x) + typ = _completion_type(StaticLint.bindingof(x)) + if typ !== missing + push!( + hints, + InlayHint( + Position(get_position_at(doc, pos + x.span)...), + string("::", typ), + InlayHintKinds.Type, + missing, + missing, + false, + true, + missing + ) + ) + end + end end if length(x) > 0 for a in x From c559fabc80bd7ad5540c032f4c35e56c7d8125cc Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 9 May 2022 15:26:26 +0200 Subject: [PATCH 04/55] add parameterHint setting --- src/languageserverinstance.jl | 18 +++++++------ src/requests/features.jl | 49 +++++++++++++++++++++-------------- src/requests/workspace.jl | 5 +++- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index 6ebf00a6..c860b520 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -43,6 +43,7 @@ mutable struct LanguageServerInstance lint_missingrefs::Symbol lint_disableddirs::Vector{String} completion_mode::Symbol + parameter_hint_mode::Symbol # :none, :literals, :all combined_msg_queue::Channel{Any} @@ -63,14 +64,14 @@ mutable struct LanguageServerInstance shutdown_requested::Bool - function LanguageServerInstance(pipe_in, pipe_out, env_path="", depot_path="", err_handler=nothing, symserver_store_path=nothing, download=true, symbolcache_upstream = nothing) + function LanguageServerInstance(pipe_in, pipe_out, env_path="", depot_path="", err_handler=nothing, symserver_store_path=nothing, download=true, symbolcache_upstream=nothing) new( JSONRPC.JSONRPCEndpoint(pipe_in, pipe_out, err_handler), Set{String}(), Dict{URI,Document}(), env_path, depot_path, - SymbolServer.SymbolServerInstance(depot_path, symserver_store_path; symbolcache_upstream = symbolcache_upstream), + SymbolServer.SymbolServerInstance(depot_path, symserver_store_path; symbolcache_upstream=symbolcache_upstream), Channel(Inf), StaticLint.ExternalEnv(deepcopy(SymbolServer.stdlibs), SymbolServer.collect_extended_methods(SymbolServer.stdlibs), collect(keys(SymbolServer.stdlibs))), Dict(), @@ -80,6 +81,7 @@ mutable struct LanguageServerInstance :all, LINT_DIABLED_DIRS, :qualify, # options: :import or :qualify, anything else turns this off + :literals, Channel{Any}(Inf), err_handler, :created, @@ -179,7 +181,7 @@ function trigger_symbolstore_reload(server::LanguageServerInstance) ssi_ret, payload = SymbolServer.getstore( server.symbol_server, server.env_path, - function (msg, percentage = missing) + function (msg, percentage=missing) if server.clientcapability_window_workdoneprogress && server.current_symserver_progress_token !== nothing msg = ismissing(percentage) ? msg : string(msg, " ($percentage%)") JSONRPC.send( @@ -193,7 +195,7 @@ function trigger_symbolstore_reload(server::LanguageServerInstance) end end, server.err_handler, - download = server.symserver_use_download + download=server.symserver_use_download ) server.number_of_outstanding_symserver_requests -= 1 @@ -281,7 +283,7 @@ function Base.run(server::LanguageServerInstance) @debug "LS: Starting client listener task." while true msg = JSONRPC.get_next_message(server.jr_endpoint) - put!(server.combined_msg_queue, (type = :clientmsg, msg = msg)) + put!(server.combined_msg_queue, (type=:clientmsg, msg=msg)) end catch err bt = catch_backtrace() @@ -294,7 +296,7 @@ function Base.run(server::LanguageServerInstance) end finally if isopen(server.combined_msg_queue) - put!(server.combined_msg_queue, (type = :close,)) + put!(server.combined_msg_queue, (type=:close,)) close(server.combined_msg_queue) end @debug "LS: Client listener task done." @@ -304,7 +306,7 @@ function Base.run(server::LanguageServerInstance) @debug "LS: Starting symbol server listener task." while true msg = take!(server.symbol_results_channel) - put!(server.combined_msg_queue, (type = :symservmsg, msg = msg)) + put!(server.combined_msg_queue, (type=:symservmsg, msg=msg)) end catch err bt = catch_backtrace() @@ -317,7 +319,7 @@ function Base.run(server::LanguageServerInstance) end finally if isopen(server.combined_msg_queue) - put!(server.combined_msg_queue, (type = :close,)) + put!(server.combined_msg_queue, (type=:close,)) close(server.combined_msg_queue) end @debug "LS: Symbol server listener task done." diff --git a/src/requests/features.jl b/src/requests/features.jl index 42c278e7..4e8ad062 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -541,6 +541,10 @@ function get_selection_range_of_expr(x::EXPR) end function textDocument_inlayHint_request(params::InlayHintParams, server::LanguageServerInstance, conn)::Union{Vector{InlayHint},Nothing} + if server.parameter_hint_mode === :none + return nothing + end + doc = getdocument(server, params.textDocument.uri) start, stop = get_offset(doc, params.range.start), get_offset(doc, params.range.stop) @@ -549,10 +553,16 @@ function textDocument_inlayHint_request(params::InlayHintParams, server::Languag end function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start, stop, pos=0, hints=InlayHint[]) + literals_only = server.parameter_hint_mode === :literals if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) && - !(parentof(parentof(x)) isa EXPR && CSTParser.defines_function(parentof(parentof(x)))) - if parentof(x).args[1] != x + !( + parentof(parentof(x)) isa EXPR && + CSTParser.defines_function(parentof(parentof(x))) + ) && + parentof(x).args[1] != x # function calls + + if !literals_only || CSTParser.isliteral(x) sigs = collect_signatures(x, doc, server) if !isempty(sigs) args = length(parentof(x).args) - 1 @@ -590,24 +600,25 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start end end end - elseif x isa EXPR && parentof(x) isa EXPR && CSTParser.isassignment(parentof(x)) && parentof(x).args[1] == x - if StaticLint.hasbinding(x) - typ = _completion_type(StaticLint.bindingof(x)) - if typ !== missing - push!( - hints, - InlayHint( - Position(get_position_at(doc, pos + x.span)...), - string("::", typ), - InlayHintKinds.Type, - missing, - missing, - false, - true, - missing - ) + elseif x isa EXPR && parentof(x) isa EXPR && + CSTParser.isassignment(parentof(x)) && + parentof(x).args[1] == x && + StaticLint.hasbinding(x) # assignment + typ = _completion_type(StaticLint.bindingof(x)) + if typ !== missing + push!( + hints, + InlayHint( + Position(get_position_at(doc, pos + x.span)...), + string("::", typ), + InlayHintKinds.Type, + missing, + missing, + false, + true, + missing ) - end + ) end end if length(x) > 0 diff --git a/src/requests/workspace.jl b/src/requests/workspace.jl index c0e105c0..44279bb8 100644 --- a/src/requests/workspace.jl +++ b/src/requests/workspace.jl @@ -99,7 +99,8 @@ function request_julia_config(server::LanguageServerInstance, conn) ConfigurationItem(missing, "julia.lint.run"), ConfigurationItem(missing, "julia.lint.missingrefs"), ConfigurationItem(missing, "julia.lint.disabledDirs"), - ConfigurationItem(missing, "julia.completionmode") + ConfigurationItem(missing, "julia.completionmode"), + ConfigurationItem(missing, "julia.parameterHints"), ])) new_runlinter = something(response[11], true) @@ -108,6 +109,7 @@ function request_julia_config(server::LanguageServerInstance, conn) new_lint_missingrefs = Symbol(something(response[12], :all)) new_lint_disableddirs = something(response[13], LINT_DIABLED_DIRS) new_completion_mode = Symbol(something(response[14], :import)) + parameterHintMode = Symbol(something(response[14], :literals)) rerun_lint = begin any(getproperty(server.lint_options, opt) != getproperty(new_SL_opts, opt) for opt in fieldnames(StaticLint.LintOptions)) || @@ -121,6 +123,7 @@ function request_julia_config(server::LanguageServerInstance, conn) server.lint_missingrefs = new_lint_missingrefs server.lint_disableddirs = new_lint_disableddirs server.completion_mode = new_completion_mode + server.parameter_hint_mode = parameterHintMode if rerun_lint relintserver(server) From ca7837ab5810508ae6ecec9d7ed20b667e6f2fbb Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 9 May 2022 15:35:34 +0200 Subject: [PATCH 05/55] Fix setting --- src/languageserverinstance.jl | 2 +- src/requests/features.jl | 4 ++-- src/requests/workspace.jl | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index c860b520..408b1021 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -43,7 +43,7 @@ mutable struct LanguageServerInstance lint_missingrefs::Symbol lint_disableddirs::Vector{String} completion_mode::Symbol - parameter_hint_mode::Symbol # :none, :literals, :all + inlay_hint_mode::Symbol # :none, :literals, :all combined_msg_queue::Channel{Any} diff --git a/src/requests/features.jl b/src/requests/features.jl index 4e8ad062..32c43e70 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -541,7 +541,7 @@ function get_selection_range_of_expr(x::EXPR) end function textDocument_inlayHint_request(params::InlayHintParams, server::LanguageServerInstance, conn)::Union{Vector{InlayHint},Nothing} - if server.parameter_hint_mode === :none + if server.inlay_hint_mode === :none return nothing end @@ -553,7 +553,7 @@ function textDocument_inlayHint_request(params::InlayHintParams, server::Languag end function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start, stop, pos=0, hints=InlayHint[]) - literals_only = server.parameter_hint_mode === :literals + literals_only = server.inlay_hint_mode === :literals if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) && !( diff --git a/src/requests/workspace.jl b/src/requests/workspace.jl index 44279bb8..80caf476 100644 --- a/src/requests/workspace.jl +++ b/src/requests/workspace.jl @@ -100,7 +100,7 @@ function request_julia_config(server::LanguageServerInstance, conn) ConfigurationItem(missing, "julia.lint.missingrefs"), ConfigurationItem(missing, "julia.lint.disabledDirs"), ConfigurationItem(missing, "julia.completionmode"), - ConfigurationItem(missing, "julia.parameterHints"), + ConfigurationItem(missing, "julia.inlayHints"), ])) new_runlinter = something(response[11], true) @@ -109,7 +109,7 @@ function request_julia_config(server::LanguageServerInstance, conn) new_lint_missingrefs = Symbol(something(response[12], :all)) new_lint_disableddirs = something(response[13], LINT_DIABLED_DIRS) new_completion_mode = Symbol(something(response[14], :import)) - parameterHintMode = Symbol(something(response[14], :literals)) + inlayHints = Symbol(something(response[15], :literals)) rerun_lint = begin any(getproperty(server.lint_options, opt) != getproperty(new_SL_opts, opt) for opt in fieldnames(StaticLint.LintOptions)) || @@ -123,7 +123,7 @@ function request_julia_config(server::LanguageServerInstance, conn) server.lint_missingrefs = new_lint_missingrefs server.lint_disableddirs = new_lint_disableddirs server.completion_mode = new_completion_mode - server.parameter_hint_mode = parameterHintMode + server.inlay_hint_mode = inlayHints if rerun_lint relintserver(server) From c2cb902b88477b9db4bb2649b2abcb6efcb3cf5b Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 9 May 2022 16:17:12 +0200 Subject: [PATCH 06/55] tests --- test/requests/features.jl | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/requests/features.jl b/test/requests/features.jl index 91dae501..b3b3fd2b 100644 --- a/test/requests/features.jl +++ b/test/requests/features.jl @@ -100,3 +100,63 @@ end """) @test all(item.name in ("a", "b", "func", "::Bar", "::Type{Foo}") for item in LanguageServer.textDocument_documentSymbol_request(LanguageServer.DocumentSymbolParams(LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), missing, missing), server, server.jr_endpoint)) end + +@testset "inlay hints" begin + doc = settestdoc(""" + a = 1 + b = 2.0 + f(xx) = xx + f(xxx, yyy) = xx + yy + f(2) + f(2, 3) + f(2, f(3)) + + f(2, 3) # this request is outside of the requested range + """) + function hints_with_mode(mode) + old_mode = server.inlay_hint_mode + server.inlay_hint_mode = mode + hints = LanguageServer.textDocument_inlayHint_request( + LanguageServer.InlayHintParams( + LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), + LanguageServer.Range(LanguageServer.Position(0, 0), LanguageServer.Position(7, 0)), + missing + ), + server, + server.jr_endpoint + ) + server.inlay_hint_mode = old_mode + return hints + end + @test hints_with_mode(:none) === nothing + @test map(x -> x.label, hints_with_mode(:literals)) == [ + string("::", Int), + "::Float64", + "xx:", + "xxx:", + "yyy:", + "xxx:", + "xx:" + ] + @test map(x -> x.label, hints_with_mode(:all)) == [ + string("::", Int), + "::Float64", + "xx:", + "xxx:", + "yyy:", + "xxx:", + "yyy:", # not a literal + "xx:" + ] + map(x -> x.position, hints_with_mode(:all)) == [ + LanguageServer.Position(0, 1), + LanguageServer.Position(1, 1), + LanguageServer.Position(2, 4), + LanguageServer.Position(4, 2), + LanguageServer.Position(5, 2), + LanguageServer.Position(5, 5), + LanguageServer.Position(6, 2), + LanguageServer.Position(6, 5), + LanguageServer.Position(6, 7), + ] +end From 10b9021e1b084f8e51c5963ea91c3a7dfe6b64eb Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 9 May 2022 18:00:49 +0200 Subject: [PATCH 07/55] better padding --- src/requests/features.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index 32c43e70..df38dcfa 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -614,8 +614,8 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start InlayHintKinds.Type, missing, missing, - false, - true, + missing, + missing, missing ) ) From 1e701d1e741741d58cb6598d9ac44bece294a846 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Fri, 17 Jun 2022 15:46:16 +0200 Subject: [PATCH 08/55] Use loose_refs in for_each_ref --- src/requests/features.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index fc07fa27..343fcd66 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -262,7 +262,7 @@ end function for_each_ref(f, identifier::EXPR) if identifier isa EXPR && StaticLint.hasref(identifier) && refof(identifier) isa StaticLint.Binding - for r in refof(identifier).refs + for r in StaticLint.loose_refs(refof(identifier)) if r isa EXPR doc1, o = get_file_loc(r) if doc1 isa Document From 3387508bd208c78791caece6ddd683415f8ea595 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 15 Aug 2022 13:34:26 +0200 Subject: [PATCH 09/55] Parse documenter example and setup blocks --- src/requests/textdocument.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index b61bfe6a..34b40918 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -236,7 +236,7 @@ function parse_jmd(str) cleaned = IOBuffer() in_julia_block = false for line in eachline(IOBuffer(str), keep=true) - if startswith(line, r"\s*```julia") || startswith(line, r"\s*```{julia") + if startswith(line, r"^```({?julia|@example|@setup)") in_julia_block = true print_substitute_line(cleaned, line) continue From 1988ca28bbe5da4615a7504cb893d0f992475b92 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 13 Oct 2022 16:21:41 +0200 Subject: [PATCH 10/55] Print content directly to stderr instead of using logging --- src/requests/textdocument.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 4be191a0..a3c4cccb 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -46,7 +46,13 @@ function textDocument_didSave_notification(params::DidSaveTextDocumentParams, se doc = getdocument(server, uri) if params.text isa String if get_text(doc) != params.text - @error "Mismatch between server and client text" get_text(doc) params.text + println(stderr, "Mismatch between server and client text") + println(stderr, "========== BEGIN SERVER SIDE TEXT ==========") + println(stderr, get_text(doc)) + println(stderr, "========== END SERVER SIDE TEXT ==========") + println(stderr, "========== BEGIN CLIENT SIDE TEXT ==========") + println(stderr, params.text) + println(stderr, "========== END CLIENT SIDE TEXT ==========") JSONRPC.send(conn, window_showMessage_notification_type, ShowMessageParams(MessageTypes.Error, "Julia Extension: Please contact us! Your extension just crashed with a bug that we have been trying to replicate for a long time. You could help the development team a lot by contacting us at https://github.com/julia-vscode/julia-vscode so that we can work together to fix this issue.")) throw(LSSyncMismatch("Mismatch between server and client text for $(get_uri(doc)). _open_in_editor is $(doc._open_in_editor). _workspace_file is $(doc._workspace_file). _version is $(get_version(doc)).")) end From e5a786da530b3d41a81fca4539931284da502f39 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Sat, 15 Oct 2022 14:18:42 +0200 Subject: [PATCH 11/55] Add some diagnostic code for signature help --- src/requests/signatures.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/requests/signatures.jl b/src/requests/signatures.jl index 197935d6..c48ad0de 100644 --- a/src/requests/signatures.jl +++ b/src/requests/signatures.jl @@ -1,6 +1,13 @@ function textDocument_signatureHelp_request(params::TextDocumentPositionParams, server::LanguageServerInstance, conn) doc = getdocument(server, params.textDocument.uri) sigs = SignatureInformation[] + # TODO The following call is just here for diagnostics + # We currently have crashes in the call to get_offset in crash reporting + # but they are fairly rare. So the idea here is to see whether we also get_expr + # crashes in index_at or not. If we still see crashes in get_offset after this here + # is merged, then the bug is simply in get_offset and we should migrate this function + # over to use index_at. If not, then there might still be a problem in the sync protocol. + index_at(get_text_document(doc), params.position) offset = get_offset(doc, params.position) x = get_expr(getcst(doc), offset) arg = 0 From a2f0a24ac66e4d01fe4edbdef98bd1d3fcc59b85 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Sun, 16 Oct 2022 17:16:23 +0200 Subject: [PATCH 12/55] Use unreserved error codes --- src/utilities.jl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index 3ef9e115..aa53b175 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -1,12 +1,17 @@ # VSCode specific # --------------- -nodocument_error(uri, data=nothing) = - return JSONRPC.JSONRPCError(-32099, "document $(uri) requested but not present in the JLS", data) +function nodocument_error(uri, data=nothing) + return JSONRPC.JSONRPCError( + 1000, + "document $(uri) requested but not present in the JLS", + data + ) +end function mismatched_version_error(uri, doc, params, msg, data=nothing) return JSONRPC.JSONRPCError( - -32099, + 1001, "version mismatch in $(msg) request for $(uri): JLS $(get_version(doc)), client: $(params.version)", data ) @@ -440,7 +445,7 @@ end _A <= _c <= _Z ? _c-_A+ UInt32(10) : _a <= _c <= _z ? _c-_a+a : UInt32(base) end - + @inline function uuid_kernel(s, i, u) _c = UInt32(@inbounds codeunit(s, i)) d = __convert_digit(_c, UInt32(16)) @@ -448,7 +453,7 @@ end u <<= 4 return u | d end - + function Base.tryparse(::Type{UUID}, s::AbstractString) u = UInt128(0) ncodeunits(s) != 36 && return nothing @@ -479,4 +484,4 @@ end return Base.UUID(u) end end -end \ No newline at end of file +end From 6c847071dd48244ccadb413a56ee78a3cd438437 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 17 Oct 2022 14:50:51 +0200 Subject: [PATCH 13/55] fix: use !isuppercase instead of islowercase in completion logic we're not really interested in whether all chars are lowercase (which is only true for letters); instead we want to determine whether the user manually upper-cased any chars and treat that as a signal that they care about the casing. --- src/requests/completions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/completions.jl b/src/requests/completions.jl index ee264ca9..6ca1f5e0 100644 --- a/src/requests/completions.jl +++ b/src/requests/completions.jl @@ -30,7 +30,7 @@ using REPL Returns true if `s` starts with `prefix` or has a sufficiently high fuzzy score. """ function is_completion_match(s::AbstractString, prefix::AbstractString, cutoff=3) - starter = if all(islowercase, prefix) + starter = if !any(isuppercase, prefix) startswith(lowercase(s), prefix) else startswith(s, prefix) From 4971c78b04b99639a8c17430d9e74bfae22e4676 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 20 Oct 2022 12:36:32 +0200 Subject: [PATCH 14/55] Fix a MethodError crash when filepath not found. --- src/requests/actions.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/requests/actions.jl b/src/requests/actions.jl index 63f47b25..46c5f32b 100644 --- a/src/requests/actions.jl +++ b/src/requests/actions.jl @@ -355,9 +355,12 @@ function get_spdx_header(doc::Document) end function in_same_workspace_folder(server::LanguageServerInstance, file1::URI, file2::URI) + file1_str = uri2filepath(file1) + file2_str = uri2filepath(file2) + (file1_str === nothing || file2_str === nothing) && return false for ws in server.workspaceFolders - if startswith(uri2filepath(file1), ws) && - startswith(uri2filepath(file2), ws) + if startswith(file1_str, ws) && + startswith(file2_str, ws) return true end end From 3e65e6acf29b14f75c12143f8cf3d9df7f4ea9bc Mon Sep 17 00:00:00 2001 From: Ben Peachey Higdon Date: Fri, 21 Oct 2022 20:57:45 -0400 Subject: [PATCH 15/55] fix completion test test would pass even if incorrect since `all` of empty array is true --- test/requests/test_completions.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/requests/test_completions.jl b/test/requests/test_completions.jl index 6a0f5258..52d57db8 100644 --- a/test/requests/test_completions.jl +++ b/test/requests/test_completions.jl @@ -42,7 +42,7 @@ end @test any(item.label == "rand" for item in completion_test(0, 14).items) settestdoc("import ") - @test all(item.label in ("Main", "Base", "Core") for item in completion_test(0, 7).items) + @test (r = all(item.label in ("Main", "Base", "Core") for item in completion_test(0, 7).items)) && !isempty(r) settestdoc("""module M end import .""") @@ -85,7 +85,7 @@ end x = Expr() x. """) - @test all(item.label in ("head", "args") for item in completion_test(1, 2).items) + @test (r = all(item.label in ("head", "args") for item in completion_test(1, 2).items)) && (!isempty(r)) settestdoc(""" struct T @@ -95,7 +95,7 @@ end x = T() x. """) - @test all(item.label in ("f1", "f2") for item in completion_test(1, 2).items) + @test (r = all(item.label in ("f1", "f2") for item in completion_test(5, 2).items)) && !isempty(r) end @testitem "token completions" begin From 030eef876f8e1e2156f90c970ce181f2c64d93ff Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Tue, 25 Oct 2022 17:58:37 +0200 Subject: [PATCH 16/55] Use different error codes --- src/utilities.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index aa53b175..e4582979 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -3,7 +3,7 @@ function nodocument_error(uri, data=nothing) return JSONRPC.JSONRPCError( - 1000, + -33100, "document $(uri) requested but not present in the JLS", data ) @@ -11,7 +11,7 @@ end function mismatched_version_error(uri, doc, params, msg, data=nothing) return JSONRPC.JSONRPCError( - 1001, + -33101, "version mismatch in $(msg) request for $(uri): JLS $(get_version(doc)), client: $(params.version)", data ) From 9ace567690cf90534d466cf79aee1ae84d2b66e2 Mon Sep 17 00:00:00 2001 From: Martin Roa Villescas Date: Wed, 16 Nov 2022 13:35:08 +0100 Subject: [PATCH 17/55] Fix range formatting (#1190) This patch reworks range formatting to fix numerous issues. Previously the text range to format was deduced through looking at the AST which often resulted in formatting more code than initially wanted. In the new approach we insert a comment-marker before and after the requested range, format the full document, and then extract the formatted code in between the markers. Fixes #1180, fixes #1187, fixes #1188, fixes #1184. --- src/requests/features.jl | 88 ++++++++++++++------------------ src/utilities.jl | 25 --------- test/requests/test_features.jl | 66 ++++++++++++++++++++++++ test/test_shared_init_request.jl | 2 +- test/test_shared_server.jl | 2 + 5 files changed, 107 insertions(+), 76 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index a57b24c4..0313f604 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -183,51 +183,30 @@ function format_text(text::AbstractString, params, config) end end +# Strings broken up and joined with * to make this file formattable +const FORMAT_MARK_BEGIN = "---- BEGIN LANGUAGESERVER" * " RANGE FORMATTING ----" +const FORMAT_MARK_END = "---- END LANGUAGESERVER" * " RANGE FORMATTING ----" + function textDocument_range_formatting_request(params::DocumentRangeFormattingParams, server::LanguageServerInstance, conn) doc = getdocument(server, params.textDocument.uri) - cst = getcst(doc) - - expr = get_inner_expr(cst, get_offset(doc, params.range.start):get_offset(doc, params.range.stop)) - - if expr === nothing - return nothing - end - - while !(expr.head in (:for, :if, :function, :module, :file, :call) || CSTParser.isassignment(expr)) - if expr.parent !== nothing - expr = expr.parent - else - return nothing - end - end - - _, offset = get_file_loc(expr) - l1, c1 = get_position_from_offset(doc, offset) - c1 = 0 - start_offset = index_at(doc, Position(l1, c1)) - l2, c2 = get_position_from_offset(doc, offset + expr.span) - - fulltext = get_text(doc) - text = fulltext[start_offset:prevind(fulltext, start_offset+expr.span)] - - longest_prefix = nothing - for line in eachline(IOBuffer(text)) - (isempty(line) || occursin(r"^\s*$", line)) && continue - idx = 0 - for c in line - if c == ' ' || c == '\t' - idx += 1 - else - break - end - end - line = line[1:idx] - longest_prefix = CSTParser.longest_common_prefix(something(longest_prefix, line), line) - end - - newcontent = try + oldcontent = get_text(doc) + startline = params.range.start.line + 1 + stopline = params.range.stop.line + 1 + + # Insert start and stop line comments as markers in the original text + original_lines = collect(eachline(IOBuffer(oldcontent); keep=true)) + original_block = join(@view(original_lines[startline:stopline])) + # If the stopline do not have a trailing newline we need to add that before our stop + # comment marker. This is removed after formatting. + stopline_has_newline = original_lines[stopline] != chomp(original_lines[stopline]) + insert!(original_lines, stopline + 1, (stopline_has_newline ? "# " : "\n# ") * FORMAT_MARK_END * "\n") + insert!(original_lines, startline, "# " * FORMAT_MARK_BEGIN * "\n") + text_marked = join(original_lines) + + # Format the full marked text + text_formatted = try config = get_juliaformatter_config(doc, server) - format_text(text, params, config) + format_text(text_marked, params, config) catch err return JSONRPC.JSONRPCError( -33000, @@ -236,17 +215,26 @@ function textDocument_range_formatting_request(params::DocumentRangeFormattingPa ) end - if longest_prefix !== nothing && !isempty(longest_prefix) - io = IOBuffer() - for line in eachline(IOBuffer(newcontent), keep=true) - print(io, longest_prefix, line) - end - newcontent = String(take!(io)) + # Find the markers in the formatted text and extract the lines in between + formatted_lines = collect(eachline(IOBuffer(text_formatted); keep=true)) + start_idx = findfirst(x -> occursin(FORMAT_MARK_BEGIN, x), formatted_lines) + start_idx === nothing && return TextEdit[] + stop_idx = findfirst(x -> occursin(FORMAT_MARK_END, x), formatted_lines) + stop_idx === nothing && return TextEdit[] + formatted_block = join(@view(formatted_lines[(start_idx+1):(stop_idx-1)])) + + # Remove the extra inserted newline if there was none from the start + if !stopline_has_newline + formatted_block = chomp(formatted_block) end - lsedits = TextEdit[TextEdit(Range(l1, c1, l2, c2), newcontent)] + # Don't suggest an edit in case the formatted text is identical to original text + if formatted_block == original_block + return TextEdit[] + end - return lsedits + # End position is exclusive, replace until start of next line + return TextEdit[TextEdit(Range(params.range.start.line, 0, params.range.stop.line + 1, 0), formatted_block)] end function find_references(textDocument::TextDocumentIdentifier, position::Position, server) diff --git a/src/utilities.jl b/src/utilities.jl index e4582979..67b9ac6e 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -200,31 +200,6 @@ function get_expr(x, offset::UnitRange{Int}, pos=0, ignorewhitespace=false) end end -# full (not only trivia) expr containing rng, modulo whitespace -function get_inner_expr(x, rng::UnitRange{Int}, pos=0, pos_span = 0) - if all(pos .> rng) - return nothing - end - if length(x) > 0 && headof(x) !== :NONSTDIDENTIFIER - pos_span′ = pos_span - for a in x - if a in x.args && all(pos_span′ .< rng .<= (pos + a.fullspan)) - return get_inner_expr(a, rng, pos, pos_span′) - end - pos += a.fullspan - pos_span′ = pos - (a.fullspan - a.span) - end - elseif pos == 0 - return x - elseif all(pos_span .< rng .<= (pos + x.fullspan)) - return x - end - pos -= x.fullspan - if all(pos_span .< rng .<= (pos + x.fullspan)) - return x - end -end - function get_expr1(x, offset, pos=0) if length(x) == 0 || headof(x) === :NONSTDIDENTIFIER if pos <= offset <= pos + x.span diff --git a/test/requests/test_features.jl b/test/requests/test_features.jl index 5fa9561b..ebdbd68c 100644 --- a/test/requests/test_features.jl +++ b/test/requests/test_features.jl @@ -103,3 +103,69 @@ end """) @test all(item.name in ("a", "b", "func", "::Bar", "::Type{Foo}") for item in LanguageServer.textDocument_documentSymbol_request(LanguageServer.DocumentSymbolParams(LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), missing, missing), server, server.jr_endpoint)) end + +@testitem "range formatting" begin + include("../test_shared_server.jl") + + doc = settestdoc(""" + map([A,B,C]) do x + if x<0 && iseven(x) + return 0 + elseif x==0 + return 1 + else + return x + end + end + """) + @test range_formatting_test(0, 0, 8, 0)[1].newText == """ + map([A, B, C]) do x + if x < 0 && iseven(x) + return 0 + elseif x == 0 + return 1 + else + return x + end + end + """ + + doc = settestdoc(""" + map([A,B,C]) do x + if x<0 && iseven(x) + return 0 + elseif x==0 + return 1 + else + return x + end + end + """) + @test range_formatting_test(2, 0, 2, 0)[1].newText == " return 0\n" + + doc = settestdoc(""" + function add(a,b) a+b end + function sub(a,b) a-b end + function mul(a,b) a*b end + """) + @test range_formatting_test(1, 0, 1, 0)[1].newText == """ + function sub(a, b) + a - b + end + """ + + doc = settestdoc(""" + function sub(a, b) + a - b + end + """) + @test range_formatting_test(0, 0, 2, 0) == LanguageServer.TextEdit[] + + # \r\n line endings + doc = settestdoc("function foo(a, b)\r\na - b\r\n end\r\n") + @test range_formatting_test(0, 0, 2, 0)[1].newText == "function foo(a, b)\r\n a - b\r\nend\r\n" + + # no trailing newline + doc = settestdoc("function foo(a, b)\na - b\n end") + @test range_formatting_test(0, 0, 2, 0)[1].newText == "function foo(a, b)\n a - b\nend" +end diff --git a/test/test_shared_init_request.jl b/test/test_shared_init_request.jl index ceed7363..358df0d9 100644 --- a/test/test_shared_init_request.jl +++ b/test/test_shared_init_request.jl @@ -1,4 +1,4 @@ -import JSONRPC +import JSONRPC, LanguageServer init_request = LanguageServer.InitializeParams( 9902, diff --git a/test/test_shared_server.jl b/test/test_shared_server.jl index 989d45c1..11aeb472 100644 --- a/test/test_shared_server.jl +++ b/test/test_shared_server.jl @@ -1,5 +1,6 @@ import Pkg using LanguageServer.URIs2 +using LanguageServer: LanguageServerInstance include("test_shared_init_request.jl") @@ -19,6 +20,7 @@ def_test(line, char) = LanguageServer.textDocument_definition_request(LanguageSe ref_test(line, char) = LanguageServer.textDocument_references_request(LanguageServer.ReferenceParams(LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), LanguageServer.Position(line, char), missing, missing, LanguageServer.ReferenceContext(true)), server, server.jr_endpoint) rename_test(line, char) = LanguageServer.textDocument_rename_request(LanguageServer.RenameParams(LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), LanguageServer.Position(line, char), missing, "newname"), server, server.jr_endpoint) hover_test(line, char) = LanguageServer.textDocument_hover_request(LanguageServer.TextDocumentPositionParams(LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), LanguageServer.Position(line, char)), server, server.jr_endpoint) +range_formatting_test(line0, char0, line1, char1) = LanguageServer.textDocument_range_formatting_request(LanguageServer.DocumentRangeFormattingParams(LanguageServer.TextDocumentIdentifier(uri"untitled:testdoc"), LanguageServer.Range(LanguageServer.Position(line0, char0), LanguageServer.Position(line1, char1)), LanguageServer.FormattingOptions(4, true, missing, missing, missing)), server, server.jr_endpoint) # TODO Replace this with a proper mock endpoint JSONRPC.send(::Nothing, ::Any, ::Any) = nothing From 12ee36558111931f2ecaf0cfd5f12dfbd80649c7 Mon Sep 17 00:00:00 2001 From: Ben Peachey Higdon Date: Fri, 28 Oct 2022 19:44:42 -0400 Subject: [PATCH 18/55] fix hover on arguments of qualified call --- src/requests/hover.jl | 7 ++++++- test/requests/test_hover.jl | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/requests/hover.jl b/src/requests/hover.jl index 1596db49..ac73626a 100644 --- a/src/requests/hover.jl +++ b/src/requests/hover.jl @@ -267,7 +267,12 @@ function get_fcall_position(x::EXPR, documentation, visited=Set{EXPR}()) documentation = string("Datatype field `$(dts.fieldnames[arg_i])`", "\n", documentation) end else - documentation = string("Argument $arg_i of $(minargs) in call to `", CSTParser.str_value(fname), "`\n", documentation) + callname = if CSTParser.is_getfield(fname) + CSTParser.str_value(fname.args[1]) * "." * CSTParser.str_value(CSTParser.get_rhs_of_getfield(fname)) + else + CSTParser.str_value(fname) + end + documentation = string("Argument $arg_i of $(minargs) in call to `", callname, "`\n", documentation) end return documentation else diff --git a/test/requests/test_hover.jl b/test/requests/test_hover.jl index e273db41..3c4d425f 100644 --- a/test/requests/test_hover.jl +++ b/test/requests/test_hover.jl @@ -51,7 +51,7 @@ end @testitem "hover docs" begin include("../test_shared_server.jl") - + settestdoc(""" "I have a docstring" Base.@kwdef struct SomeStruct @@ -60,3 +60,15 @@ end """) @test startswith(hover_test(1, 20).contents.value, "I have a docstring") end + +@testitem "hover argument qualified function" begin + include("../test_shared_server.jl") + + settestdoc(""" + module M + f(a,b,c,d,e) = 1 + end + M.f(1,2,3,4,5) + """) + @test hover_test(3, 5).contents.value == "Argument 1 of 5 in call to `M.f`\n" +end From 0cbc887b5663907a30d3c2d23d21712fd467457b Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Mon, 19 Dec 2022 06:13:34 +0100 Subject: [PATCH 19/55] Fix "Expand Function" code action removing comments and whitespace --- src/requests/actions.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/requests/actions.jl b/src/requests/actions.jl index 46c5f32b..294d0fcd 100644 --- a/src/requests/actions.jl +++ b/src/requests/actions.jl @@ -163,7 +163,7 @@ function expand_inline_func(x, server, conn) if headof(body) == :block && length(body) == 1 file, offset = get_file_loc(func) tde = TextDocumentEdit(VersionedTextDocumentIdentifier(get_uri(file), get_version(file)), TextEdit[ - TextEdit(Range(file, offset .+ (0:func.fullspan)), string("function ", get_text(file)[offset .+ (1:sig.span)], "\n ", get_text(file)[offset + sig.fullspan + op.fullspan .+ (1:body.span)], "\nend\n")) + TextEdit(Range(file, offset .+ (0:func.span)), string("function ", get_text(file)[offset .+ (1:sig.span)], "\n ", get_text(file)[offset + sig.fullspan + op.fullspan .+ (1:body.span)], "\nend")) ]) JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde]))) elseif (headof(body) === :begin || CSTParser.isbracketed(body)) && @@ -175,8 +175,8 @@ function expand_inline_func(x, server, conn) newtext = string(newtext, "\n ", get_text(file)[blockoffset .+ (1:body.args[1].args[i].span)]) blockoffset += body.args[1].args[i].fullspan end - newtext = string(newtext, "\nend\n") - tde = TextDocumentEdit(VersionedTextDocumentIdentifier(get_uri(file), get_version(file)), TextEdit[TextEdit(Range(file, offset .+ (0:func.fullspan)), newtext)]) + newtext = string(newtext, "\nend") + tde = TextDocumentEdit(VersionedTextDocumentIdentifier(get_uri(file), get_version(file)), TextEdit[TextEdit(Range(file, offset .+ (0:func.span)), newtext)]) JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde]))) end end From 82fa29827f886c77c7bf53a823c60392344248b5 Mon Sep 17 00:00:00 2001 From: Bill Burdick Date: Wed, 11 Jan 2023 11:38:27 +0530 Subject: [PATCH 20/55] add check for nothing in init params to support emacs --- src/requests/init.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/init.jl b/src/requests/init.jl index a8b21f77..c842723c 100644 --- a/src/requests/init.jl +++ b/src/requests/init.jl @@ -175,7 +175,7 @@ function initialize_request(params::InitializeParams, server::LanguageServerInst server.clientcapability_workspace_didChangeConfiguration = true end - if !ismissing(params.initializationOptions) + if !ismissing(params.initializationOptions) && params.initializationOptions !== nothing server.initialization_options = params.initializationOptions end From a538bc0947d8d31dd039bdc34b704fd9e7c68e6a Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 13 Jan 2023 21:17:11 -0800 Subject: [PATCH 21/55] Use TestItemDetection 0.2 --- Project.toml | 2 +- src/requests/textdocument.jl | 5 +++-- src/staticlint.jl | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index e2e74850..a7d9dd17 100644 --- a/Project.toml +++ b/Project.toml @@ -35,7 +35,7 @@ StaticLint = "8.0" Tokenize = "0.5.10" JSONRPC = "1.1" SymbolServer = "7.1" -TestItemDetection = "0.1.1" +TestItemDetection = "0.2.0" [targets] test = ["Test", "Sockets", "LibGit2", "Serialization", "SHA", "TestItemRunner"] diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 40e0ed6f..213c727f 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -391,7 +391,7 @@ function find_project_for_file(jw::JuliaWorkspace, file::URI) return project end -function find_testitems!(doc, server::LanguageServerInstance, jr_endpoint) +function find_tests!(doc, server::LanguageServerInstance, jr_endpoint) if !ismissing(server.initialization_options) && get(server.initialization_options, "julialangTestItemIdentification", false) # Find which workspace folder the doc is in. parent_workspaceFolders = sort(filter(f -> startswith(doc._path, f), collect(server.workspaceFolders)), by=length, rev=true) @@ -429,9 +429,10 @@ function find_testitems!(doc, server::LanguageServerInstance, jr_endpoint) for i in cst.args file_testitems = [] + file_testsetups = [] file_errors = [] - TestItemDetection.find_test_items_detail!(i, file_testitems, file_errors) + TestItemDetection.find_test_detail!(i, file_testitems, file_testsetups, file_errors) append!(testitems, [TestItemDetail(i.name, i.name, Range(doc, i.range), get_text(doc)[i.code_range], Range(doc, i.code_range), i.option_default_imports, string.(i.option_tags), nothing) for i in file_testitems]) append!(testitems, [TestItemDetail("Test error", "Test error", Range(doc, i.range), nothing, nothing, nothing, nothing, i.error) for i in file_errors]) diff --git a/src/staticlint.jl b/src/staticlint.jl index 69140f9d..347a38b9 100644 --- a/src/staticlint.jl +++ b/src/staticlint.jl @@ -78,5 +78,5 @@ function lint!(doc, server) # TODO Ideally we would not want to acces jr_endpoint here publish_diagnostics(doc, server, server.jr_endpoint) - find_testitems!(doc, server, server.jr_endpoint) + find_tests!(doc, server, server.jr_endpoint) end From 23a2bc68f40c98847cde0957ddbe113cd781201e Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 13 Jan 2023 21:30:12 -0800 Subject: [PATCH 22/55] Pass new testsetups to client --- src/extensions/extensions.jl | 17 +++++++++++++++-- src/extensions/messagedefs.jl | 2 +- src/requests/textdocument.jl | 13 +++++++++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/extensions/extensions.jl b/src/extensions/extensions.jl index 17dddcef..640bfde8 100644 --- a/src/extensions/extensions.jl +++ b/src/extensions/extensions.jl @@ -14,16 +14,29 @@ end code_range::Union{Nothing,Range} option_default_imports::Union{Nothing,Bool} option_tags::Union{Nothing,Vector{String}} - error::Union{Nothing,String} end -struct PublishTestItemsParams <: Outbound +@dict_readable struct TestSetupDetail <: Outbound + name::String + range::Range + code::Union{Nothing,String} + code_range::Union{Nothing,Range} +end + +@dict_readable struct TestErrorDetail <: Outbound + range::Range + error::String +end + +struct PublishTestsParams <: Outbound uri::DocumentUri version::Union{Int,Missing} project_path::String package_path::String package_name::String testitemdetails::Vector{TestItemDetail} + testsetupdetails::Vector{TestSetupDetail} + testerrordetails::Vector{TestErrorDetail} end include("messagedefs.jl") diff --git a/src/extensions/messagedefs.jl b/src/extensions/messagedefs.jl index 3f8e0940..e27c515a 100644 --- a/src/extensions/messagedefs.jl +++ b/src/extensions/messagedefs.jl @@ -2,4 +2,4 @@ const julia_getModuleAt_request_type = JSONRPC.RequestType("julia/getModuleAt", const julia_getCurrentBlockRange_request_type = JSONRPC.RequestType("julia/getCurrentBlockRange", VersionedTextDocumentPositionParams, Tuple{Position, Position, Position}) const julia_getDocAt_request_type = JSONRPC.RequestType("julia/getDocAt", VersionedTextDocumentPositionParams, String) const julia_getDocFromWord_request_type = JSONRPC.RequestType("julia/getDocFromWord", NamedTuple{(:word,),Tuple{String}}, String) -const textDocument_publishTestitems_notification_type = JSONRPC.NotificationType("julia/publishTestitems", PublishTestItemsParams) +const textDocument_publishTests_notification_type = JSONRPC.NotificationType("julia/publishTests", PublishTestsParams) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 213c727f..349c3876 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -426,6 +426,8 @@ function find_tests!(doc, server::LanguageServerInstance, jr_endpoint) cst = getcst(doc) testitems = [] + testsetups = [] + testerrors = [] for i in cst.args file_testitems = [] @@ -435,17 +437,20 @@ function find_tests!(doc, server::LanguageServerInstance, jr_endpoint) TestItemDetection.find_test_detail!(i, file_testitems, file_testsetups, file_errors) append!(testitems, [TestItemDetail(i.name, i.name, Range(doc, i.range), get_text(doc)[i.code_range], Range(doc, i.code_range), i.option_default_imports, string.(i.option_tags), nothing) for i in file_testitems]) - append!(testitems, [TestItemDetail("Test error", "Test error", Range(doc, i.range), nothing, nothing, nothing, nothing, i.error) for i in file_errors]) + append!(testsetups, [TestSetupDetail(i.name, Range(doc, i.range), get_text(doc)[i.code_range], Range(doc, i.code_range), ) for i in file_testsetups]) + append!(testerrors, [TestErrorDetail(Range(doc, i.range), i.error) for i in file_errors]) end - params = PublishTestItemsParams( + params = PublishTestsParams( get_uri(doc), get_version(doc), project_path, package_path, package_name, - testitems + testitems, + testsetups, + testerrors ) - JSONRPC.send(jr_endpoint, textDocument_publishTestitems_notification_type, params) + JSONRPC.send(jr_endpoint, textDocument_publishTests_notification_type, params) end end From f8177b4bf23abd8fefe17621374ed633f82c2caf Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 13 Jan 2023 22:58:57 -0800 Subject: [PATCH 23/55] Fix a typo --- src/requests/textdocument.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 349c3876..a3c4d214 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -436,9 +436,9 @@ function find_tests!(doc, server::LanguageServerInstance, jr_endpoint) TestItemDetection.find_test_detail!(i, file_testitems, file_testsetups, file_errors) - append!(testitems, [TestItemDetail(i.name, i.name, Range(doc, i.range), get_text(doc)[i.code_range], Range(doc, i.code_range), i.option_default_imports, string.(i.option_tags), nothing) for i in file_testitems]) + append!(testitems, [TestItemDetail(i.name, i.name, Range(doc, i.range), get_text(doc)[i.code_range], Range(doc, i.code_range), i.option_default_imports, string.(i.option_tags)) for i in file_testitems]) append!(testsetups, [TestSetupDetail(i.name, Range(doc, i.range), get_text(doc)[i.code_range], Range(doc, i.code_range), ) for i in file_testsetups]) - append!(testerrors, [TestErrorDetail(Range(doc, i.range), i.error) for i in file_errors]) + append!(testerrors, [TestErrorDetail(Range(doc, i.range), i.error) for i in file_errors]) end params = PublishTestsParams( From 4d40ca67d2a50485a04f42823b76302407249e42 Mon Sep 17 00:00:00 2001 From: Ben Peachey Higdon Date: Tue, 31 Jan 2023 02:58:00 -0500 Subject: [PATCH 24/55] Mark errors to end of file (#1169) Fixes https://github.com/julia-vscode/julia-vscode/issues/2393, https://github.com/julia-vscode/StaticLint.jl/issues/353 --- src/requests/textdocument.jl | 2 ++ test/requests/test_textdocument.jl | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index a3c4d214..aa2114f5 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -126,6 +126,8 @@ end function mark_errors(doc, out=Diagnostic[]) line_offsets = get_line_offsets(get_text_document(doc)) + # Extend line_offsets by one to consider up to EOF + line_offsets = vcat(line_offsets, length(get_text(doc)) + 1) errs = StaticLint.collect_hints(getcst(doc), getenv(doc), doc.server.lint_missingrefs) n = length(errs) n == 0 && return out diff --git a/test/requests/test_textdocument.jl b/test/requests/test_textdocument.jl index 652dab40..9c178fd4 100644 --- a/test/requests/test_textdocument.jl +++ b/test/requests/test_textdocument.jl @@ -1,6 +1,6 @@ @testitem "TextDocument" begin include("../test_shared_server.jl") - + empty!(server._documents) LanguageServer.textDocument_didOpen_notification(LanguageServer.DidOpenTextDocumentParams(LanguageServer.TextDocumentItem(uri"untitled:none", "julia", 0, "")), server, server.jr_endpoint) @@ -17,3 +17,13 @@ LanguageServer.textDocument_didClose_notification(LanguageServer.DidCloseTextDocumentParams(LanguageServer.TextDocumentIdentifier(uri"untitled:none")), server, server.jr_endpoint) @test !LanguageServer.hasdocument(server, uri"untitled:none") end + +@testitem "mark errors to end of file" begin + include("../test_shared_server.jl") + + # Missing a closing ')' + doc = settestdoc("println(\"Hello, world!\"") + diagnostics = LanguageServer.mark_errors(doc) + @test length(diagnostics) == 1 + @test diagnostics[1].range == LanguageServer.Range(0, 23, 0, 23) +end From 3628f1081db5f51b102167f781a36a084931db34 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 4 Feb 2023 09:01:12 -0500 Subject: [PATCH 25/55] add dev note to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4c2edf10..8c78a01c 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,7 @@ julia --project=/path/to/LanguageServer.jl/environment \ If `env_path` is not specified, the language server will run on the parent project of `pwd` or on the default `.julia/environments/v#.#` if there is no parent project. + +## Development of the VSCode extension + +See https://github.com/julia-vscode/julia-vscode/wiki for information on how to test this package with the VSCode extension From 431365642125d83f58cfea47d15562db8e9b1fec Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Mon, 20 Mar 2023 17:30:00 -0700 Subject: [PATCH 26/55] Implement a test detection fix --- src/requests/textdocument.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index aa2114f5..aa408bdb 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -417,7 +417,9 @@ function find_tests!(doc, server::LanguageServerInstance, jr_endpoint) end project_path = "" - if haskey(server.workspace._projects, project_uri) + if project_uri == package_uri + project_path = uri2filepath(project_uri) + elseif haskey(server.workspace._projects, project_uri) relevant_project = server.workspace._projects[project_uri] if haskey(relevant_project.deved_packages, package_uri) From 5b73a96b2034196dff1afbfcce1d7b9ed7ef1c22 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Thu, 13 Apr 2023 12:29:39 -0400 Subject: [PATCH 27/55] warn if static analysis is slow for a given file --- src/requests/textdocument.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index aa408bdb..8e49ebb5 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -112,8 +112,14 @@ function parse_all(doc::Document, server::LanguageServerInstance) if get_language_id(doc) in ("markdown", "juliamarkdown") doc.cst, ps = parse_jmd(get_text(doc)) elseif get_language_id(doc) == "julia" - ps = CSTParser.ParseState(get_text(doc)) - doc.cst, ps = CSTParser.parse(ps, true) + t = @elapsed begin + ps = CSTParser.ParseState(get_text(doc)) + doc.cst, ps = CSTParser.parse(ps, true) + end + if t > 10 + # warn to help debugging in the wild + @warn "CSTParser took a long time ($(round(Int, t)) seconds) to parse $(repr(getpath(doc)))" + end end sizeof(get_text(doc)) == getcst(doc).fullspan || @error "CST does not match input string length." if headof(doc.cst) === :file From 8a3ae73b510aa66e302f57d5d9545a550fcb4e7a Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 20 Apr 2023 09:36:19 +0200 Subject: [PATCH 28/55] Set version to v4.4.0 --- Project.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index a7d9dd17..22f3c09b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,13 @@ name = "LanguageServer" uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" -version = "4.3.2-DEV" +version = "4.4.0" [deps] Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624" +TestItemDetection = "76b0de8b-5c4b-48ef-a724-914b33ca988d" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" StaticLint = "b3cc710f-9c33-5bdb-a03d-a94903873e97" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" @@ -15,15 +16,14 @@ JSONRPC = "b9b8584e-8fd3-41f9-ad0c-7255d428e418" CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" SymbolServer = "cf896787-08d5-524d-9de7-132aaa0cb996" -TestItemDetection = "76b0de8b-5c4b-48ef-a724-914b33ca988d" [extras] Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" -TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [compat] JSON = "0.20, 0.21" From 73de4169f8011eff150c56726cf6a62d370be194 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 20 Apr 2023 09:36:19 +0200 Subject: [PATCH 29/55] Set version to v4.4.1-DEV --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 22f3c09b..20e4debc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LanguageServer" uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" -version = "4.4.0" +version = "4.4.1-DEV" [deps] Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" From 49b3fac9fcfeb69fb705adc7f60a0fa6312c12a8 Mon Sep 17 00:00:00 2001 From: Mark Kittisopikul Date: Tue, 25 Apr 2023 23:22:03 -0400 Subject: [PATCH 30/55] Add PrecompileTools, precompile runserver --- Project.toml | 45 ++++++++++++++++++++++--------------------- src/LanguageServer.jl | 10 ++++++++++ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/Project.toml b/Project.toml index 20e4debc..6b56b686 100644 --- a/Project.toml +++ b/Project.toml @@ -3,39 +3,40 @@ uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" version = "4.4.1-DEV" [deps] -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624" -TestItemDetection = "76b0de8b-5c4b-48ef-a724-914b33ca988d" -REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -StaticLint = "b3cc710f-9c33-5bdb-a03d-a94903873e97" +CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" JSONRPC = "b9b8584e-8fd3-41f9-ad0c-7255d428e418" -CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +StaticLint = "b3cc710f-9c33-5bdb-a03d-a94903873e97" SymbolServer = "cf896787-08d5-524d-9de7-132aaa0cb996" - -[extras] -Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" -LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" -TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +TestItemDetection = "76b0de8b-5c4b-48ef-a724-914b33ca988d" +Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624" +URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] +CSTParser = "3.3" JSON = "0.20, 0.21" -julia = "1" +JSONRPC = "1.1" JuliaFormatter = "0.20.0, 0.21, 0.22, 0.23, 1" -CSTParser = "3.3" -URIs = "1.3" StaticLint = "8.0" -Tokenize = "0.5.10" -JSONRPC = "1.1" SymbolServer = "7.1" TestItemDetection = "0.2.0" +Tokenize = "0.5.10" +URIs = "1.3" +julia = "1" + +[extras] +LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] test = ["Test", "Sockets", "LibGit2", "Serialization", "SHA", "TestItemRunner"] diff --git a/src/LanguageServer.jl b/src/LanguageServer.jl index 259fc01f..58f85c4d 100644 --- a/src/LanguageServer.jl +++ b/src/LanguageServer.jl @@ -7,6 +7,7 @@ using Base.Docs, Markdown import JSONRPC using JSONRPC: Outbound, @dict_readable import TestItemDetection +using PrecompileTools export LanguageServerInstance, runserver @@ -38,4 +39,13 @@ include("requests/signatures.jl") include("requests/highlight.jl") include("utilities.jl") +@setup_workload begin + iob = IOBuffer() + println(iob) + @compile_workload begin + runserver(iob) + end +end +precompile(runserver, ()) + end From 0000e81924c308537d2c34e933eeb1081a8dcbe6 Mon Sep 17 00:00:00 2001 From: Julia Package Butler <> Date: Thu, 11 May 2023 01:35:26 +0000 Subject: [PATCH 31/55] Fix issues identified by Julia Package Butler --- .github/workflows/jlpkgbutler-ci-master-workflow.yml | 2 +- .github/workflows/jlpkgbutler-ci-pr-workflow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jlpkgbutler-ci-master-workflow.yml b/.github/workflows/jlpkgbutler-ci-master-workflow.yml index 09cff08a..8d881dc8 100644 --- a/.github/workflows/jlpkgbutler-ci-master-workflow.yml +++ b/.github/workflows/jlpkgbutler-ci-master-workflow.yml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8'] + julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9'] julia-arch: [x64, x86] os: [ubuntu-latest, windows-latest, macOS-latest] exclude: diff --git a/.github/workflows/jlpkgbutler-ci-pr-workflow.yml b/.github/workflows/jlpkgbutler-ci-pr-workflow.yml index 9114217f..8793bf55 100644 --- a/.github/workflows/jlpkgbutler-ci-pr-workflow.yml +++ b/.github/workflows/jlpkgbutler-ci-pr-workflow.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8'] + julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9'] julia-arch: [x64, x86] os: [ubuntu-latest, windows-latest, macOS-latest] exclude: From 94c84b9d830137334d882dd5e0c13f824fdfa85d Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Wed, 26 Apr 2023 16:18:07 +0000 Subject: [PATCH 32/55] CompatHelper: add new compat entry for PrecompileTools at version 1, (keep existing compat) --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 6b56b686..7ecb9cd8 100644 --- a/Project.toml +++ b/Project.toml @@ -23,6 +23,7 @@ CSTParser = "3.3" JSON = "0.20, 0.21" JSONRPC = "1.1" JuliaFormatter = "0.20.0, 0.21, 0.22, 0.23, 1" +PrecompileTools = "1" StaticLint = "8.0" SymbolServer = "7.1" TestItemDetection = "0.2.0" From 3ffb6f82972f9915de853e04b6045797e3777396 Mon Sep 17 00:00:00 2001 From: Mark Kittisopikul Date: Thu, 11 May 2023 15:20:39 -0400 Subject: [PATCH 33/55] Supress precompile errors, which are normal --- Project.toml | 1 + src/LanguageServer.jl | 11 ++--------- src/languageserverinstance.jl | 4 +--- src/precompile.jl | 17 +++++++++++++++++ 4 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 src/precompile.jl diff --git a/Project.toml b/Project.toml index 6b56b686..f0b6d924 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSONRPC = "b9b8584e-8fd3-41f9-ad0c-7255d428e418" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" diff --git a/src/LanguageServer.jl b/src/LanguageServer.jl index 58f85c4d..b382bf84 100644 --- a/src/LanguageServer.jl +++ b/src/LanguageServer.jl @@ -7,6 +7,7 @@ using Base.Docs, Markdown import JSONRPC using JSONRPC: Outbound, @dict_readable import TestItemDetection +import Logging using PrecompileTools export LanguageServerInstance, runserver @@ -38,14 +39,6 @@ include("requests/init.jl") include("requests/signatures.jl") include("requests/highlight.jl") include("utilities.jl") - -@setup_workload begin - iob = IOBuffer() - println(iob) - @compile_workload begin - runserver(iob) - end -end -precompile(runserver, ()) +include("precompile.jl") end diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index 751e9e38..cbdc6b7c 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -293,9 +293,7 @@ function Base.run(server::LanguageServerInstance) if server.err_handler !== nothing server.err_handler(err, bt) else - io = IOBuffer() - Base.display_error(io, err, bt) - print(stderr, String(take!(io))) + @warn "LS: An error occurred in the client listener task. This may be normal." exception=(err, bt) end finally if isopen(server.combined_msg_queue) diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 00000000..0ff98dcf --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,17 @@ +@setup_workload begin + iob = IOBuffer() + println(iob) + @compile_workload begin + # Suppress errors + if get(ENV, "JULIA_DEBUG", "") == "LanguageServer" + precompile_logger = Logging.ConsoleLogger() + else + precompile_logger = Logging.NullLogger() + end + Logging.with_logger(precompile_logger) do + runserver(iob) + end + end +end +precompile(runserver, ()) + From 08170e401856b3031de9f85baf9de4cb8546fe4a Mon Sep 17 00:00:00 2001 From: xgdgsc Date: Thu, 20 Jul 2023 20:18:54 +0800 Subject: [PATCH 34/55] fix stopline in formatting (#1228) --- src/requests/features.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/requests/features.jl b/src/requests/features.jl index 0313f604..30a0a3f1 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -195,6 +195,7 @@ function textDocument_range_formatting_request(params::DocumentRangeFormattingPa # Insert start and stop line comments as markers in the original text original_lines = collect(eachline(IOBuffer(oldcontent); keep=true)) + stopline = min(stopline, length(original_lines)) original_block = join(@view(original_lines[startline:stopline])) # If the stopline do not have a trailing newline we need to add that before our stop # comment marker. This is removed after formatting. From 5abaa83afb4e335b2b466047b933a995f16c5183 Mon Sep 17 00:00:00 2001 From: ShalokShalom Date: Sat, 2 Sep 2023 21:24:48 +0000 Subject: [PATCH 35/55] Update README.md Add instructions for Helix Add information about other clients --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4c2edf10..9efac3f6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ make use of the Julia Language Server for various code editing features: - [Emacs](../../wiki/Emacs) - [Sublime Text](https://github.com/tomv564/LSP) - [Kakoune](../../wiki/Kakoune) +- [Helix](https://uncomfyhalomacro.pl/blog/14/) +- [Others](https://microsoft.github.io/language-server-protocol/implementors/tools/) ## Installation and Usage **Documentation**: [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://www.julia-vscode.org/LanguageServer.jl/dev) From 15c0fef9872c24c0c4268eb3df7106e59aff14af Mon Sep 17 00:00:00 2001 From: Till Brychcy Date: Fri, 8 Sep 2023 16:11:19 +0200 Subject: [PATCH 36/55] show @testitem in Outline view --- src/requests/features.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/requests/features.jl b/src/requests/features.jl index 30a0a3f1..0ca69522 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -339,6 +339,28 @@ function collect_document_symbols(x::EXPR, server::LanguageServerInstance, doc, push!(symbols, ds) symbols = ds.children end + elseif x.head == :macrocall + # detect @testitem "testname" ... + child_nodes = filter(i -> !(isa(i, EXPR) && i.head == :NOTHING && i.args === nothing), x.args) + if length(child_nodes) > 1 + macroname = CSTParser.valof(child_nodes[1]) + if macroname == "@testitem" + if (child_nodes[2] isa EXPR && child_nodes[2].head == :STRING) + testname = CSTParser.valof(child_nodes[2]) + ds = DocumentSymbol( + "$(macroname) \"$(testname)\"", # name + missing, # detail + 6, # kind (6==method) + false, # deprecated + Range(doc, (pos .+ (0:x.span))), # range + Range(doc, (pos .+ (0:x.span))), # selection range + DocumentSymbol[] # children + ) + push!(symbols, ds) + symbols = ds.children + end + end + end end if length(x) > 0 for a in x From 86d0201fd6bbb646263ff8fdff8773044f5b084c Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 11 Sep 2023 15:08:51 +0200 Subject: [PATCH 37/55] fix: show all methods in function hovers previously, we were not looking at references when determining the list of methods defined for any given function, which means that `sin` only reported one method in the hover (because that's the only method that is defined directly in Base, the others are in Base.Math or LinearAlgebra). --- src/requests/hover.jl | 96 +++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/src/requests/hover.jl b/src/requests/hover.jl index ac73626a..09e74655 100644 --- a/src/requests/hover.jl +++ b/src/requests/hover.jl @@ -3,7 +3,7 @@ function textDocument_hover_request(params::TextDocumentPositionParams, server:: env = getenv(doc, server) x = get_expr1(getcst(doc), get_offset(doc, params.position)) x isa EXPR && CSTParser.isoperator(x) && resolve_op_ref(x, env) - documentation = get_hover(x, "", server) + documentation = get_hover(x, "", server, x, env) documentation = get_closer_hover(x, documentation) documentation = get_fcall_position(x, documentation) documentation = sanitize_docstring(documentation) @@ -11,15 +11,15 @@ function textDocument_hover_request(params::TextDocumentPositionParams, server:: return isempty(documentation) ? nothing : Hover(MarkupContent(documentation), missing) end -get_hover(x, documentation::String, server) = documentation +get_hover(x, documentation::String, server, expr, env) = documentation -function get_hover(x::EXPR, documentation::String, server) +function get_hover(x::EXPR, documentation::String, server, expr, env) if (CSTParser.isidentifier(x) || CSTParser.isoperator(x)) && StaticLint.hasref(x) r = refof(x) documentation = if r isa StaticLint.Binding - get_hover(r, documentation, server) + get_hover(r, documentation, server, expr, env) elseif r isa SymbolServer.SymStore - get_hover(r, documentation, server) + get_hover(r, documentation, server, expr, env) else documentation end @@ -27,12 +27,12 @@ function get_hover(x::EXPR, documentation::String, server) return documentation end -function get_tooltip(b::StaticLint.Binding, documentation::String, server; show_definition = false) +function get_tooltip(b::StaticLint.Binding, documentation::String, server, expr = nothing, env = nothing; show_definition = false) if b.val isa StaticLint.Binding - documentation = get_hover(b.val, documentation, server) + documentation = get_hover(b.val, documentation, server, expr, env) elseif b.val isa EXPR if CSTParser.defines_function(b.val) || CSTParser.defines_datatype(b.val) - documentation = get_func_hover(b, documentation, server) + documentation = get_func_hover(b, documentation, server, expr, env) for r in b.refs method = StaticLint.get_method(r) if method isa EXPR @@ -43,7 +43,7 @@ function get_tooltip(b::StaticLint.Binding, documentation::String, server; show_ documentation = string(ensure_ends_with(documentation), "```julia\n", to_codeobject(method), "\n```\n") end elseif method isa SymbolServer.SymStore - documentation = get_hover(method, documentation, server) + documentation = get_hover(method, documentation, server, expr, env) end end else @@ -75,7 +75,8 @@ function get_tooltip(b::StaticLint.Binding, documentation::String, server; show_ return documentation end -get_hover(b::StaticLint.Binding, documentation::String, server) = get_tooltip(b, documentation, server; show_definition = true) +get_hover(b::StaticLint.Binding, documentation::String, server, expr, env) = + get_tooltip(b, documentation, server, expr, env; show_definition = true) get_typed_definition(b) = _completion_type(b) get_typed_definition(b::StaticLint.Binding) = @@ -136,53 +137,66 @@ end prettify_expr(ex) = string(ex) # print(io, x::SymStore) methods are defined in SymbolServer -function get_hover(b::SymbolServer.SymStore, documentation::String, server) +function get_hover(b::SymbolServer.SymStore, documentation::String, server, expr, env) if !isempty(b.doc) documentation = string(documentation, b.doc, "\n") end documentation = string(documentation, "```julia\n", b, "\n```") end -function get_hover(f::SymbolServer.FunctionStore, documentation::String, server) +function get_hover(f::SymbolServer.FunctionStore, documentation::String, server, expr, env) if !isempty(f.doc) - documentation = string(documentation, f.doc, "\n") + documentation = string(documentation, f.doc, "\n\n") end - documentation = string(documentation, "`$(f.name)` is a `Function`.\n") - nm = length(f.methods) - documentation = string(documentation, "**$(nm)** method", nm == 1 ? "" : "s", " for function ", '`', f.name, '`', '\n') - for m in f.methods - io = IOBuffer() - print(io, m.name, "(") - nsig = length(m.sig) - for (i, sig) = enumerate(m.sig) - if sig[1] ≠ Symbol("#unused#") - print(io, sig[1]) - end - print(io, "::", sig[2]) - i ≠ nsig && print(io, ", ") - end - print(io, ")") - sig = String(take!(io)) + if expr !== nothing && env !== nothing + method_count = 0 + tls = StaticLint.retrieve_toplevel_scope(expr) - text = replace(string(m.file, ':', m.line), "\\" => "\\\\") - link = text + totalio = IOBuffer() + StaticLint.iterate_over_ss_methods(f, tls, env, function (m) + method_count += 1 - if server.clientInfo !== missing && isabspath(m.file) - clientname = lowercase(server.clientInfo.name) - if occursin("code", clientname) || occursin("sublime", clientname) - link = string(filepath2uri(m.file), "#", m.line) + io = IOBuffer() + print(io, m.name, "(") + nsig = length(m.sig) + for (i, sig) = enumerate(m.sig) + if sig[1] ≠ Symbol("#unused#") + print(io, sig[1]) + end + print(io, "::", sig[2]) + i ≠ nsig && print(io, ", ") end - end - - documentation = string(documentation, "- `$(sig)` in `$(m.mod)` at [$(text)]($(link))", '\n') + print(io, ")") + sig = String(take!(io)) + + path = replace(m.file, "\\" => "\\\\") + text = string(path, ':', m.line) + link = text + + if server.clientInfo !== missing && isabspath(m.file) + clientname = lowercase(server.clientInfo.name) + if occursin("code", clientname) || occursin("sublime", clientname) + link = string(filepath2uri(m.file), "#", m.line) + text = string(basename(path), ':', m.line) + end + end + println(totalio, "$(method_count). `$(sig)` in `$(m.mod)` at [$(text)]($(link))\n") + return false + end) + + documentation = string( + documentation, + "`$(f.name)` is a function with **$(method_count)** method$(method_count == 1 ? "" : "s")\n", + String(take!(totalio)) + ) end + return documentation end - -get_func_hover(x, documentation, server) = documentation -get_func_hover(x::SymbolServer.SymStore, documentation, server) = get_hover(x, documentation, server) +get_func_hover(x, documentation, server, expr, env) = documentation +get_func_hover(x::SymbolServer.SymStore, documentation, server, expr, env) = get_hover(x, documentation, server, expr, env) function get_preceding_docs(expr::EXPR, documentation) if expr_has_preceding_docs(expr) From 42a647f4f72ba9819f6282ab656c50561c83c2ae Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Wed, 13 Sep 2023 16:24:17 +0200 Subject: [PATCH 38/55] feat: add timer outputs and fix precompile types --- src/languageserverinstance.jl | 43 +++++++++++++++++++++---------- src/precompile.jl | 3 +-- src/runserver.jl | 2 +- src/utilities.jl | 48 +++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index cbdc6b7c..7fc13140 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -66,7 +66,7 @@ mutable struct LanguageServerInstance workspace::JuliaWorkspace - function LanguageServerInstance(pipe_in, pipe_out, env_path="", depot_path="", err_handler=nothing, symserver_store_path=nothing, download=true, symbolcache_upstream = nothing) + function LanguageServerInstance(@nospecialize(pipe_in), @nospecialize(pipe_out), env_path="", depot_path="", err_handler=nothing, symserver_store_path=nothing, download=true, symbolcache_upstream = nothing) new( JSONRPC.JSONRPCEndpoint(pipe_in, pipe_out, err_handler), Set{String}(), @@ -192,10 +192,8 @@ function trigger_symbolstore_reload(server::LanguageServerInstance) progress_notification_type, ProgressParams(server.current_symserver_progress_token, WorkDoneProgressReport(missing, msg, missing)) ) - @info msg - else - @info msg end + @info msg end, server.err_handler, download = server.symserver_use_download @@ -274,16 +272,21 @@ end Run the language `server`. """ -function Base.run(server::LanguageServerInstance) +function Base.run(server::LanguageServerInstance; timings = []) + did_show_timer = Ref(false) + add_timer_message!(did_show_timer, timings, "LS startup started") + server.status = :started run(server.jr_endpoint) @debug "Connected at $(round(Int, time()))" + add_timer_message!(did_show_timer, timings, "connection established") trigger_symbolstore_reload(server) @async try @debug "LS: Starting client listener task." + add_timer_message!(did_show_timer, timings, "(async) listening to client events") while true msg = JSONRPC.get_next_message(server.jr_endpoint) put!(server.combined_msg_queue, (type = :clientmsg, msg = msg)) @@ -302,9 +305,11 @@ function Base.run(server::LanguageServerInstance) end @debug "LS: Client listener task done." end + yield() @async try @debug "LS: Starting symbol server listener task." + add_timer_message!(did_show_timer, timings, "(async) listening to symbol server events") while true msg = take!(server.symbol_results_channel) put!(server.combined_msg_queue, (type = :symservmsg, msg = msg)) @@ -314,9 +319,7 @@ function Base.run(server::LanguageServerInstance) if server.err_handler !== nothing server.err_handler(err, bt) else - io = IOBuffer() - Base.display_error(io, err, bt) - print(stderr, String(take!(io))) + @error "LS: Queue op failed" ex=(err, bt) end finally if isopen(server.combined_msg_queue) @@ -325,8 +328,9 @@ function Base.run(server::LanguageServerInstance) end @debug "LS: Symbol server listener task done." end + yield() - @debug "Symbol Server started at $(round(Int, time()))" + @debug "async tasks started at $(round(Int, time()))" msg_dispatcher = JSONRPC.MsgDispatcher() @@ -373,22 +377,30 @@ function Base.run(server::LanguageServerInstance) # handled directly. msg_dispatcher[exit_notification_type] = (conn, params) -> exit_notification(params, server, conn) - @debug "starting main loop" @debug "Starting event listener loop at $(round(Int, time()))" + add_timer_message!(did_show_timer, timings, "starting combined listener") + while true message = take!(server.combined_msg_queue) + if message.type == :close - @info "Shutting down server instance." + @debug "Shutting down server instance." return elseif message.type == :clientmsg msg = message.msg + + add_timer_message!(did_show_timer, timings, msg) + JSONRPC.dispatch_msg(server.jr_endpoint, msg_dispatcher, msg) elseif message.type == :symservmsg - @info "Received new data from Julia Symbol Server." + @debug "Received new data from Julia Symbol Server." server.global_env.symbols = message.msg + add_timer_message!(did_show_timer, timings, "symbols received") server.global_env.extended_methods = SymbolServer.collect_extended_methods(server.global_env.symbols) + add_timer_message!(did_show_timer, timings, "extended methods computed") server.global_env.project_deps = collect(keys(server.global_env.symbols)) + add_timer_message!(did_show_timer, timings, "project deps computed") # redo roots_env_map for (root, _) in server.roots_env_map @@ -400,11 +412,14 @@ function Base.run(server::LanguageServerInstance) server.roots_env_map[root] = newenv end end + add_timer_message!(did_show_timer, timings, "env map computed") + + @debug "Linting started at $(round(Int, time()))" - @debug "starting re-lint of everything" relintserver(server) - @debug "re-lint done" + @debug "Linting finished at $(round(Int, time()))" + add_timer_message!(did_show_timer, timings, "initial lint done") end end end diff --git a/src/precompile.jl b/src/precompile.jl index 0ff98dcf..32de9f78 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -3,7 +3,7 @@ println(iob) @compile_workload begin # Suppress errors - if get(ENV, "JULIA_DEBUG", "") == "LanguageServer" + if get(ENV, "JULIA_DEBUG", "") in ("all", "LanguageServer") precompile_logger = Logging.ConsoleLogger() else precompile_logger = Logging.NullLogger() @@ -14,4 +14,3 @@ end end precompile(runserver, ()) - diff --git a/src/runserver.jl b/src/runserver.jl index da5a82cc..55dc40cb 100644 --- a/src/runserver.jl +++ b/src/runserver.jl @@ -34,7 +34,7 @@ julia --project=/path/to/LanguageServer.jl \\ -e "using LanguageServer; runserver()" ``` """ -function runserver(pipe_in=stdin, pipe_out=stdout, env_path=choose_env(), +function runserver(@nospecialize(pipe_in)=stdin, pipe_out=stdout, env_path=choose_env(), depot_path="", err_handler=nothing, symserver_store_path=nothing) server = LanguageServerInstance(pipe_in, pipe_out, env_path, depot_path, err_handler, symserver_store_path) diff --git a/src/utilities.jl b/src/utilities.jl index 67b9ac6e..ef0732b1 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -460,3 +460,51 @@ end end end end + +# some timer utilities +add_timer_message!(did_show_timer, timings, msg::Dict) = add_timer_message!(did_show_timer, timings, string("LSP/", get(msg, "method", ""))) +function add_timer_message!(did_show_timer, timings, msg::String) + if did_show_timer[] + return + end + + push!(timings, (msg, time())) + + if should_show_timer_message(timings) + send_startup_time_message(timings) + did_show_timer[] = true + end +end + +function should_show_timer_message(timings) + required_messages = [ + "LSP/initialize", + "LSP/initialized", + "initial lint done" + ] + + return all(in(first.(timings)), required_messages) +end + +function send_startup_time_message(timings) + length(timings) > 1 || return + + io = IOBuffer() + println(io, "============== Startup timings ==============") + starttime = prevtime = first(timings)[2] + for (msg, thistime) in timings + println( + io, + lpad(string(round(thistime - starttime; sigdigits = 5)), 10), + " - ", msg, " (", + round(thistime - prevtime; sigdigits = 5), + "s since last event)" + ) + prevtime = thistime + end + println(io, "=============================================") + + empty!(timings) + + println(stderr, String(take!(io))) +end From 3a4c44441178b35d256298b41c208a033e96bcb3 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 25 Sep 2023 11:55:49 +0200 Subject: [PATCH 39/55] feat: improve outline item types --- src/requests/features.jl | 45 ++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index c19b5036..ce636a2f 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -323,14 +323,31 @@ function textDocument_documentSymbol_request(params::DocumentSymbolParams, serve return collect_document_symbols(getcst(doc), server, doc) end -function collect_document_symbols(x::EXPR, server::LanguageServerInstance, doc, pos=0, symbols=DocumentSymbol[]) +struct BindingContext + is_function_def::Bool + is_datatype_def::Bool + is_datatype_def_body::Bool +end +BindingContext() = BindingContext(false, false, false) + +function collect_document_symbols(x::EXPR, server::LanguageServerInstance, doc, pos=0, ctx=BindingContext(), symbols=DocumentSymbol[]) + is_datatype_def_body = ctx.is_datatype_def_body + if ctx.is_datatype_def && !is_datatype_def_body + is_datatype_def_body = x.head === :block && length(x.parent.args) >= 3 && x.parent.args[3] == x + end + ctx = BindingContext( + ctx.is_function_def || CSTParser.defines_function(x), + ctx.is_datatype_def || CSTParser.defines_datatype(x), + is_datatype_def_body, + ) + if bindingof(x) !== nothing b = bindingof(x) if b.val isa EXPR && is_valid_binding_name(b.name) ds = DocumentSymbol( get_name_of_binding(b.name), # name missing, # detail - _binding_kind(b), # kind + _binding_kind(b, ctx), # kind false, # deprecated Range(doc, (pos .+ (0:x.span))), # range Range(doc, (pos .+ (0:x.span))), # selection range @@ -340,17 +357,17 @@ function collect_document_symbols(x::EXPR, server::LanguageServerInstance, doc, symbols = ds.children end elseif x.head == :macrocall - # detect @testitem "testname" ... + # detect @testitem/testset "testname" ... child_nodes = filter(i -> !(isa(i, EXPR) && i.head == :NOTHING && i.args === nothing), x.args) if length(child_nodes) > 1 macroname = CSTParser.valof(child_nodes[1]) - if macroname == "@testitem" + if macroname == "@testitem" || macroname == "@testset" if (child_nodes[2] isa EXPR && child_nodes[2].head == :STRING) testname = CSTParser.valof(child_nodes[2]) ds = DocumentSymbol( "$(macroname) \"$(testname)\"", # name missing, # detail - 6, # kind (6==method) + 3, # kind (namespace) false, # deprecated Range(doc, (pos .+ (0:x.span))), # range Range(doc, (pos .+ (0:x.span))), # selection range @@ -364,7 +381,7 @@ function collect_document_symbols(x::EXPR, server::LanguageServerInstance, doc, end if length(x) > 0 for a in x - collect_document_symbols(a, server, doc, pos, symbols) + collect_document_symbols(a, server, doc, pos, ctx, symbols) pos += a.fullspan end end @@ -400,10 +417,16 @@ function collect_toplevel_bindings_w_loc(x::EXPR, pos=0, bindings=Tuple{UnitRang return bindings end -function _binding_kind(b) +function _binding_kind(b, ctx::BindingContext) if b isa StaticLint.Binding if b.type === nothing - return 13 + if ctx.is_datatype_def_body && !ctx.is_function_def + return 8 + elseif ctx.is_datatype_def + return 26 + else + return 13 + end elseif b.type == StaticLint.CoreTypes.Module return 2 elseif b.type == StaticLint.CoreTypes.Function @@ -413,7 +436,11 @@ function _binding_kind(b) elseif b.type == StaticLint.CoreTypes.Int || b.type == StaticLint.CoreTypes.Float64 return 16 elseif b.type == StaticLint.CoreTypes.DataType - return 23 + if ctx.is_datatype_def && !ctx.is_datatype_def_body + return 23 + else + return 26 + end else return 13 end From d3fe98619224c8e94f4557479fef39101b401eb9 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Wed, 11 Oct 2023 15:20:33 +0200 Subject: [PATCH 40/55] fix(docs): fix get_hover calls for docs --- src/requests/features.jl | 4 +- src/requests/hover.jl | 79 ++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index ce636a2f..d058dc04 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -515,7 +515,7 @@ function julia_getDocAt_request(params::VersionedTextDocumentPositionParams, ser x = get_expr1(getcst(doc), get_offset(doc, params.position)) x isa EXPR && CSTParser.isoperator(x) && resolve_op_ref(x, env) - documentation = get_hover(x, "", server) + documentation = get_hover(x, "", server, x, env) return documentation end @@ -542,7 +542,7 @@ function julia_getDocFromWord_request(params::NamedTuple{(:word,),Tuple{String}} # this would ideally use the Damerau-Levenshtein distance or even something fancier: score = _score(needle, sym) if score < 2 - val = get_hover(val, "", server) + val = get_hover(val, "", server, nothing, getenv(server)) if !isempty(val) nfound += 1 push!(matches, score => val) diff --git a/src/requests/hover.jl b/src/requests/hover.jl index 6b0b2f5e..d5172c41 100644 --- a/src/requests/hover.jl +++ b/src/requests/hover.jl @@ -150,48 +150,55 @@ function get_hover(f::SymbolServer.FunctionStore, documentation::String, server, end if expr !== nothing && env !== nothing - method_count = 0 tls = StaticLint.retrieve_toplevel_scope(expr) + itr = func -> StaticLint.iterate_over_ss_methods(f, tls, env, func) + else + itr = func -> begin + for m in f.methods + func(m) + end + end + end - totalio = IOBuffer() - StaticLint.iterate_over_ss_methods(f, tls, env, function (m) - method_count += 1 - - io = IOBuffer() - print(io, m.name, "(") - nsig = length(m.sig) - for (i, sig) = enumerate(m.sig) - if sig[1] ≠ Symbol("#unused#") - print(io, sig[1]) - end - print(io, "::", sig[2]) - i ≠ nsig && print(io, ", ") + method_count = 0 + totalio = IOBuffer() + itr() do m + method_count += 1 + + io = IOBuffer() + print(io, m.name, "(") + nsig = length(m.sig) + for (i, sig) = enumerate(m.sig) + if sig[1] ≠ Symbol("#unused#") + print(io, sig[1]) end - print(io, ")") - sig = String(take!(io)) - - path = replace(m.file, "\\" => "\\\\") - text = string(path, ':', m.line) - link = text - - if server.clientInfo !== missing && isabspath(m.file) - clientname = lowercase(server.clientInfo.name) - if occursin("code", clientname) || occursin("sublime", clientname) - link = string(filepath2uri(m.file), "#", m.line) - text = string(basename(path), ':', m.line) - end + print(io, "::", sig[2]) + i ≠ nsig && print(io, ", ") + end + print(io, ")") + sig = String(take!(io)) + + path = replace(m.file, "\\" => "\\\\") + text = string(path, ':', m.line) + link = text + + if server.clientInfo !== missing && isabspath(m.file) + clientname = lowercase(server.clientInfo.name) + if occursin("code", clientname) || occursin("sublime", clientname) + link = string(filepath2uri(m.file), "#", m.line) + text = string(basename(path), ':', m.line) end - println(totalio, "$(method_count). `$(sig)` in `$(m.mod)` at [$(text)]($(link))\n") - return false - end) - - documentation = string( - documentation, - "`$(f.name)` is a function with **$(method_count)** method$(method_count == 1 ? "" : "s")\n", - String(take!(totalio)) - ) + end + println(totalio, "$(method_count). `$(sig)` in `$(m.mod)` at [$(text)]($(link))\n") + return false end + documentation = string( + documentation, + "`$(f.name)` is a function with **$(method_count)** method$(method_count == 1 ? "" : "s")\n", + String(take!(totalio)) + ) + return documentation end From 64742802be13b5ffa6037879773be62826fbf186 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Thu, 12 Oct 2023 14:50:45 +0200 Subject: [PATCH 41/55] fix: stop parsing and linting toml files --- src/languageserverinstance.jl | 10 +++++++--- src/requests/textdocument.jl | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index 7fc13140..4485d370 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -435,11 +435,15 @@ function relintserver(server) # only do a pass on documents once root = getroot(doc) if !(root in roots) - push!(roots, root) - semantic_pass(root) + if get_language_id(root) in ("julia", "markdown", "juliamarkdown") + push!(roots, root) + semantic_pass(root) + end end end for doc in documents - lint!(doc, server) + if get_language_id(doc) in ("julia", "markdown", "juliamarkdown") + lint!(doc, server) + end end end diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 8e49ebb5..4d019e87 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -91,7 +91,7 @@ function textDocument_didChange_notification(params::DidChangeTextDocumentParams if get_language_id(doc) in ("markdown", "juliamarkdown") parse_all(doc, server) - else + else get_language_id(doc) == "julia" cst0, cst1 = getcst(doc), CSTParser.parse(get_text(doc), true) r1, r2, r3 = CSTParser.minimal_reparse(s0, get_text(doc), cst0, cst1, inds = true) for i in setdiff(1:length(cst0.args), r1 , r3) # clean meta from deleted expr @@ -116,10 +116,12 @@ function parse_all(doc::Document, server::LanguageServerInstance) ps = CSTParser.ParseState(get_text(doc)) doc.cst, ps = CSTParser.parse(ps, true) end - if t > 10 + if t > 1 # warn to help debugging in the wild @warn "CSTParser took a long time ($(round(Int, t)) seconds) to parse $(repr(getpath(doc)))" end + else + return end sizeof(get_text(doc)) == getcst(doc).fullspan || @error "CST does not match input string length." if headof(doc.cst) === :file From 50a795c1ba3b4f093fb4db3b5232ed921551efd0 Mon Sep 17 00:00:00 2001 From: Frederik Banning <65158285+fbanning@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:43:17 +0200 Subject: [PATCH 42/55] Add Kate editor to list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 01f1c09a..576c840d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ make use of the Julia Language Server for various code editing features: - [Sublime Text](https://github.com/tomv564/LSP) - [Kakoune](../../wiki/Kakoune) - [Helix](https://uncomfyhalomacro.pl/blog/14/) +- [Kate](../../wiki/Kate) - [Others](https://microsoft.github.io/language-server-protocol/implementors/tools/) ## Installation and Usage From f426752143440e2e54666a4eb24f676d1677fbef Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 23 Oct 2023 10:48:40 +0200 Subject: [PATCH 43/55] fix: more eagerly publish diagnostics --- src/requests/textdocument.jl | 2 +- src/staticlint.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 4d019e87..f4e1125e 100644 --- a/src/requests/textdocument.jl +++ b/src/requests/textdocument.jl @@ -214,7 +214,7 @@ end function publish_diagnostics(doc::Document, server, conn) - diagnostics = if server.runlinter && server.symbol_store_ready && (is_workspace_file(doc) || isunsavedfile(doc)) + diagnostics = if server.runlinter && (is_workspace_file(doc) || isunsavedfile(doc)) pkgpath = getpath(doc) if any(is_in_target_dir_of_package.(Ref(pkgpath), server.lint_disableddirs)) filter!(!is_diag_dependent_on_env, doc.diagnostics) diff --git a/src/staticlint.jl b/src/staticlint.jl index 347a38b9..b0ff36db 100644 --- a/src/staticlint.jl +++ b/src/staticlint.jl @@ -71,7 +71,7 @@ function setserver(file::Document, server::LanguageServerInstance) return file end -function lint!(doc, server) +function lint!(doc::Document, server) StaticLint.check_all(getcst(doc), server.lint_options, getenv(doc, server)) empty!(doc.diagnostics) mark_errors(doc, doc.diagnostics) From 3552997360d861cb712c74c52307e59b37dd047f Mon Sep 17 00:00:00 2001 From: Julia Package Butler <> Date: Thu, 28 Dec 2023 18:44:11 +0000 Subject: [PATCH 44/55] Fix issues identified by Julia Package Butler --- .github/workflows/jlpkgbutler-ci-master-workflow.yml | 2 +- .github/workflows/jlpkgbutler-ci-pr-workflow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jlpkgbutler-ci-master-workflow.yml b/.github/workflows/jlpkgbutler-ci-master-workflow.yml index 8d881dc8..dd8c64d6 100644 --- a/.github/workflows/jlpkgbutler-ci-master-workflow.yml +++ b/.github/workflows/jlpkgbutler-ci-master-workflow.yml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9'] + julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10'] julia-arch: [x64, x86] os: [ubuntu-latest, windows-latest, macOS-latest] exclude: diff --git a/.github/workflows/jlpkgbutler-ci-pr-workflow.yml b/.github/workflows/jlpkgbutler-ci-pr-workflow.yml index 8793bf55..08f833c5 100644 --- a/.github/workflows/jlpkgbutler-ci-pr-workflow.yml +++ b/.github/workflows/jlpkgbutler-ci-pr-workflow.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9'] + julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10'] julia-arch: [x64, x86] os: [ubuntu-latest, windows-latest, macOS-latest] exclude: From 199a176ea85221b2499c6977b4ba05d64f2b3be3 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 8 Jan 2024 17:53:00 +0100 Subject: [PATCH 45/55] v4.5.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5610c044..10ea64c0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LanguageServer" uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" -version = "4.4.1-DEV" +version = "4.5.0" [deps] CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" From a1a72d5de0e1cbb3c55bb4a0f09f6a34684e303f Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Tue, 9 Jan 2024 11:11:11 +0100 Subject: [PATCH 46/55] chore: make completion test more robust --- test/requests/test_completions.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/requests/test_completions.jl b/test/requests/test_completions.jl index 52d57db8..3802bf4b 100644 --- a/test/requests/test_completions.jl +++ b/test/requests/test_completions.jl @@ -1,5 +1,5 @@ @testitem "latex completions" begin - include("../test_shared_server.jl") + include("../test_shared_server.jl") settestdoc(""" \\therefor @@ -48,9 +48,6 @@ end import .""") @test_broken completion_test(1, 8).items[1].label == "M" - settestdoc("import Base.") - @test any(item.label == "Meta" for item in completion_test(0, 12).items) - settestdoc("import Base.M") @test any(item.label == "Meta" for item in completion_test(0, 13).items) @@ -62,7 +59,10 @@ end include("../test_shared_server.jl") settestdoc("Base.") - @test any(item.label == "Base" for item in completion_test(0, 5).items) + @test length(completion_test(0, 5).items) > 10 + + settestdoc("Base.B") + @test any(item.label == "Base" for item in completion_test(0, 6).items) settestdoc("Base.r") @test any(item.label == "rand" for item in completion_test(0, 6).items) @@ -175,7 +175,7 @@ end @testitem "completion details" begin include("../test_shared_server.jl") - + settestdoc(""" struct Bar end struct Foo From 59cfee7cc1c02aac144ac30fc6612151603cc3f7 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Fri, 19 Jan 2024 14:17:50 +0100 Subject: [PATCH 47/55] fix merge issues --- src/requests/features.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index 4e4c447d..10756245 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -622,7 +622,7 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start push!( hints, InlayHint( - Position(get_position_at(doc, pos)...), + Position(get_position_from_offset(doc, pos)...), string(label, ':'), InlayHintKinds.Parameter, missing, @@ -646,7 +646,7 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start push!( hints, InlayHint( - Position(get_position_at(doc, pos + x.span)...), + Position(get_position_from_offset(doc, pos + x.span)...), string("::", typ), InlayHintKinds.Type, missing, From 7fb279144c56257b1260c7a7ee34a344d34f70d4 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Fri, 19 Jan 2024 16:21:28 +0100 Subject: [PATCH 48/55] better setting names --- src/languageserverinstance.jl | 6 +++++- src/requests/features.jl | 38 +++++++++++++++++++---------------- src/requests/workspace.jl | 12 ++++++++--- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index b5d42f8a..bf18e17e 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -43,7 +43,9 @@ mutable struct LanguageServerInstance lint_missingrefs::Symbol lint_disableddirs::Vector{String} completion_mode::Symbol - inlay_hint_mode::Symbol # :none, :literals, :all + inlay_hints::Bool + inlay_hints_variable_types::Bool + inlay_hints_parameter_names::Symbol combined_msg_queue::Channel{Any} @@ -84,6 +86,8 @@ mutable struct LanguageServerInstance :all, LINT_DIABLED_DIRS, :qualify, # options: :import or :qualify, anything else turns this off + true, + true, :literals, Channel{Any}(Inf), err_handler, diff --git a/src/requests/features.jl b/src/requests/features.jl index 10756245..804cdfb9 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -578,7 +578,7 @@ function get_selection_range_of_expr(x::EXPR) end function textDocument_inlayHint_request(params::InlayHintParams, server::LanguageServerInstance, conn)::Union{Vector{InlayHint},Nothing} - if server.inlay_hint_mode === :none + if !server.inlay_hints return nothing end @@ -590,7 +590,6 @@ function textDocument_inlayHint_request(params::InlayHintParams, server::Languag end function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start, stop, pos=0, hints=InlayHint[]) - literals_only = server.inlay_hint_mode === :literals if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) && !( @@ -599,7 +598,10 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start ) && parentof(x).args[1] != x # function calls - if !literals_only || CSTParser.isliteral(x) + if server.inlay_hints_parameter_names === :all || ( + server.inlay_hints_parameter_names === :literals && + CSTParser.isliteral(x) + ) sigs = collect_signatures(x, doc, server) if !isempty(sigs) args = length(parentof(x).args) - 1 @@ -641,21 +643,23 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start CSTParser.isassignment(parentof(x)) && parentof(x).args[1] == x && StaticLint.hasbinding(x) # assignment - typ = _completion_type(StaticLint.bindingof(x)) - if typ !== missing - push!( - hints, - InlayHint( - Position(get_position_from_offset(doc, pos + x.span)...), - string("::", typ), - InlayHintKinds.Type, - missing, - missing, - missing, - missing, - missing + if server.inlay_hints_variable_types + typ = _completion_type(StaticLint.bindingof(x)) + if typ !== missing + push!( + hints, + InlayHint( + Position(get_position_from_offset(doc, pos + x.span)...), + string("::", typ), + InlayHintKinds.Type, + missing, + missing, + missing, + missing, + missing + ) ) - ) + end end end if length(x) > 0 diff --git a/src/requests/workspace.jl b/src/requests/workspace.jl index d7e452c0..2caadcdf 100644 --- a/src/requests/workspace.jl +++ b/src/requests/workspace.jl @@ -108,7 +108,9 @@ function request_julia_config(server::LanguageServerInstance, conn) ConfigurationItem(missing, "julia.lint.missingrefs"), ConfigurationItem(missing, "julia.lint.disabledDirs"), ConfigurationItem(missing, "julia.completionmode"), - ConfigurationItem(missing, "julia.inlayHints"), + ConfigurationItem(missing, "julia.inlayHints.static.enabled"), + ConfigurationItem(missing, "julia.inlayHints.static.variableTypes.enabled"), + ConfigurationItem(missing, "julia.inlayHints.static.parameterNames.enabled"), ])) new_runlinter = something(response[11], true) @@ -117,7 +119,9 @@ function request_julia_config(server::LanguageServerInstance, conn) new_lint_missingrefs = Symbol(something(response[12], :all)) new_lint_disableddirs = something(response[13], LINT_DIABLED_DIRS) new_completion_mode = Symbol(something(response[14], :import)) - inlayHints = Symbol(something(response[15], :literals)) + inlayHints = something(response[15], true) + inlayHintsVariableTypes = something(response[16], true) + inlayHintsParameterNames = Symbol(something(response[17], :literals)) rerun_lint = begin any(getproperty(server.lint_options, opt) != getproperty(new_SL_opts, opt) for opt in fieldnames(StaticLint.LintOptions)) || @@ -131,7 +135,9 @@ function request_julia_config(server::LanguageServerInstance, conn) server.lint_missingrefs = new_lint_missingrefs server.lint_disableddirs = new_lint_disableddirs server.completion_mode = new_completion_mode - server.inlay_hint_mode = inlayHints + server.inlay_hints = inlayHints + server.inlay_hints_variable_types = inlayHintsVariableTypes + server.inlay_hints_parameter_names = inlayHintsParameterNames if rerun_lint relintserver(server) From 2fcbf4967ebffdff9d674e2f8d8f9da6bd755737 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 29 Jan 2024 11:22:59 +0100 Subject: [PATCH 49/55] feat: monitor editor pid for exit --- src/languageserverinstance.jl | 4 ++++ src/requests/init.jl | 1 + src/utilities.jl | 16 ++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/languageserverinstance.jl b/src/languageserverinstance.jl index bf18e17e..e207fb59 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -65,6 +65,7 @@ mutable struct LanguageServerInstance clientInfo::Union{InfoParams,Missing} initialization_options::Union{Missing,Dict} + editor_pid::Union{Nothing,Int} shutdown_requested::Bool workspace::JuliaWorkspace @@ -100,6 +101,7 @@ mutable struct LanguageServerInstance missing, missing, missing, + nothing, false, JuliaWorkspace() ) @@ -290,6 +292,8 @@ function Base.run(server::LanguageServerInstance; timings = []) trigger_symbolstore_reload(server) + poll_editor_pid(server) + @async try @debug "LS: Starting client listener task." add_timer_message!(did_show_timer, timings, "(async) listening to client events") diff --git a/src/requests/init.jl b/src/requests/init.jl index 4d17e369..d7b1d5b9 100644 --- a/src/requests/init.jl +++ b/src/requests/init.jl @@ -161,6 +161,7 @@ function initialize_request(params::InitializeParams, server::LanguageServerInst server.clientCapabilities = params.capabilities server.clientInfo = params.clientInfo + server.editor_pid = params.processId if !ismissing(params.capabilities.window) && params.capabilities.window.workDoneProgress server.clientcapability_window_workdoneprogress = true diff --git a/src/utilities.jl b/src/utilities.jl index b1189350..73487f04 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -534,3 +534,19 @@ function send_startup_time_message(timings) println(stderr, String(take!(io))) end + +function poll_editor_pid(server::LanguageServerInstance) + if server.editor_pid === nothing + return + end + @info "Monitoring editor process with id $(server.editor_pid)" + return @async while !server.shutdown_requested + sleep(10) + + # kill -0 $editor_pid + r = ccall(:uv_kill, Cint, (Cint, Cint), server.editor_pid, 0) + if r != 0 + exit(1) + end + end +end From 71a9093edc19026530b96a6e7dfeba2ce387d760 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 29 Jan 2024 11:26:57 +0100 Subject: [PATCH 50/55] chore: refactor inlay hint logic a bit --- src/requests/features.jl | 84 ++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/src/requests/features.jl b/src/requests/features.jl index 804cdfb9..9d232b64 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -589,6 +589,46 @@ function textDocument_inlayHint_request(params::InlayHintParams, server::Languag return collect_inlay_hints(getcst(doc), server, doc, start, stop) end +function get_inlay_parameter_hints(x::EXPR, server::LanguageServerInstance, doc, pos=0) + if server.inlay_hints_parameter_names === :all || ( + server.inlay_hints_parameter_names === :literals && + CSTParser.isliteral(x) + ) + sigs = collect_signatures(x, doc, server) + + nargs = length(parentof(x).args) - 1 + nargs == 0 && return nothing + + filter!(s -> length(s.parameters) == nargs, sigs) + isempty(sigs) && return nothing + + pars = first(sigs).parameters + thisarg = 0 + for a in parentof(x).args + if x == a + break + end + thisarg += 1 + end + if thisarg <= nargs && thisarg <= length(pars) + label = pars[thisarg].label + label == "#unused#" && return nothing + + return InlayHint( + Position(get_position_from_offset(doc, pos)...), + string(label, ':'), + InlayHintKinds.Parameter, + missing, + pars[thisarg].documentation, + false, + true, + missing + ) + end + end + return nothing +end + function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start, stop, pos=0, hints=InlayHint[]) if x isa EXPR && parentof(x) isa EXPR && CSTParser.iscall(parentof(x)) && @@ -597,47 +637,9 @@ function collect_inlay_hints(x::EXPR, server::LanguageServerInstance, doc, start CSTParser.defines_function(parentof(parentof(x))) ) && parentof(x).args[1] != x # function calls - - if server.inlay_hints_parameter_names === :all || ( - server.inlay_hints_parameter_names === :literals && - CSTParser.isliteral(x) - ) - sigs = collect_signatures(x, doc, server) - if !isempty(sigs) - args = length(parentof(x).args) - 1 - if args > 0 - filter!(s -> length(s.parameters) == args, sigs) - if !isempty(sigs) - pars = first(sigs).parameters - thisarg = 0 - for a in parentof(x).args - if x == a - break - end - thisarg += 1 - end - if thisarg <= args && thisarg <= length(pars) - label = pars[thisarg].label - if label == "#unused#" - label = "_" - end - push!( - hints, - InlayHint( - Position(get_position_from_offset(doc, pos)...), - string(label, ':'), - InlayHintKinds.Parameter, - missing, - pars[thisarg].documentation, - false, - true, - missing - ) - ) - end - end - end - end + maybe_hint = get_inlay_parameter_hints(x, server, doc, pos) + if maybe_hint !== nothing + push!(hints, maybe_hint) end elseif x isa EXPR && parentof(x) isa EXPR && CSTParser.isassignment(parentof(x)) && From ccbb6eab44d53aa8c96ff2432395cce3af6dc893 Mon Sep 17 00:00:00 2001 From: Sukera Date: Mon, 19 Feb 2024 13:53:45 +0100 Subject: [PATCH 51/55] Add better support for SPDX headers This commit does these things: * Add rudimentary detection of EUPL-* licenses * Add logging statements throughout the action to make it debuggable should some license not be properly detected * Fix a bug with SPDX header detection in existing files To expand on the last point - previously, the header detection didn't have multiline matching enabled on its regex, which meant that any file having any lines other than the SPDX header wasn't detected as actually having a header. That's a bit bad, since presumably there would be actual source code following that header. --- src/requests/actions.jl | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/requests/actions.jl b/src/requests/actions.jl index 294d0fcd..0a67f928 100644 --- a/src/requests/actions.jl +++ b/src/requests/actions.jl @@ -350,7 +350,9 @@ function double_to_triple_equal(x, _, conn) end function get_spdx_header(doc::Document) - m = match(r"(*ANYCRLF)^# SPDX-License-Identifier:\h+((?:[\w\.-]+)(?:\h+[\w\.-]+)*)\h*$", get_text(doc)) + # note the multiline flag - without that, we'd try to match the end of the _document_ + # instead of the end of the line. + m = match(r"(*ANYCRLF)^# SPDX-License-Identifier:\h+((?:[\w\.-]+)(?:\h+[\w\.-]+)*)\h*$"m, get_text(doc)) return m === nothing ? m : String(m[1]) end @@ -377,7 +379,11 @@ function identify_short_identifier(server::LanguageServerInstance, file::Documen end if length(candidate_identifiers) == 1 return first(candidate_identifiers) + else + numerous = iszero(length(candidate_identifiers)) ? "no" : "multiple" + @warn "Found $numerous candidates for the SPDX header from open files, falling back to LICENSE" Candidates=candidate_identifiers end + # Fallback to looking for a license file in the same workspace folder candidate_files = String[] for dir in server.workspaceFolders @@ -387,21 +393,36 @@ function identify_short_identifier(server::LanguageServerInstance, file::Documen end end end - length(candidate_files) == 1 || return nothing - license = read(first(candidate_files), String) + + num_candidates = length(candidate_files) + if num_candidates != 1 + iszero(num_candidates) && @warn "No candidate for licenses found, can't add identifier!" + num_candidates > 1 && @warn "More than one candidate for licenses found, choose licensing manually!" + return nothing + end # This is just a heuristic, but should be OK since this is not something automated, and # the programmer will see directly if the wrong license is added. - # TODO: Add more licenses... - if contains(license, r"MIT\s+(\"?Expat\"?\s+)?License") + license_text = read(first(candidate_files), String) + + # Some known different spellings of some licences + if contains(license_text, r"^\s*MIT\s+(\"?Expat\"?\s+)?Licen[sc]e") return "MIT" + elseif contains(license_text, r"^\s*EUROPEAN\s+UNION\s+PUBLIC\s+LICEN[CS]E\s+v\."i) + # the first version should be the EUPL version + version = match(r"\d\.\d", license_text).match + return "EUPL-$version" end + + @warn "A license was found, but could not be identified! Consider adding its licence identifier once to a file manually so that LanguageServer.jl can find it automatically next time." Location=first(candidate_files) return nothing end function add_license_header(x, server::LanguageServerInstance, conn) file, _ = get_file_loc(x) + # does the current file already have a header? get_spdx_header(file) === nothing || return # TODO: Would be nice to check this already before offering the action + # no, so try to find one short_identifier = identify_short_identifier(server, file) short_identifier === nothing && return tde = TextDocumentEdit(VersionedTextDocumentIdentifier(get_uri(file), get_version(file)), TextEdit[ From bb453ae7491bee65f0ed974254c082fd83ec8669 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Thu, 22 Feb 2024 12:13:07 +0100 Subject: [PATCH 52/55] v4.5.1 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 10ea64c0..a596b9a6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LanguageServer" uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" -version = "4.5.0" +version = "4.5.1" [deps] CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" From 11180f4cdcc19a03c90ebbca946a09835977f484 Mon Sep 17 00:00:00 2001 From: Sukera Date: Thu, 22 Feb 2024 17:52:19 +0100 Subject: [PATCH 53/55] Fix whitespace The repo currently, for some reason, used mixed line endings in src/LanguageServer.jl. This unifies the repo to always use LF (LineFeed) line endings on the remote server. --- .gitattributes | 1 + src/LanguageServer.jl | 88 +++++++++++++++++++++---------------------- 2 files changed, 45 insertions(+), 44 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/src/LanguageServer.jl b/src/LanguageServer.jl index b382bf84..d784cfdb 100644 --- a/src/LanguageServer.jl +++ b/src/LanguageServer.jl @@ -1,44 +1,44 @@ -module LanguageServer -using JSON, REPL, CSTParser, JuliaFormatter, SymbolServer, StaticLint -using CSTParser: EXPR, Tokenize.Tokens, Tokenize.Tokens.kind, headof, parentof, valof, to_codeobject -using StaticLint: refof, scopeof, bindingof -using UUIDs -using Base.Docs, Markdown -import JSONRPC -using JSONRPC: Outbound, @dict_readable -import TestItemDetection -import Logging -using PrecompileTools - -export LanguageServerInstance, runserver - -include("URIs2/URIs2.jl") -using .URIs2 - -JSON.lower(uri::URI) = string(uri) - -include("exception_types.jl") -include("protocol/protocol.jl") -include("extensions/extensions.jl") -include("textdocument.jl") -include("document.jl") -include("juliaworkspace.jl") -include("languageserverinstance.jl") -include("multienv.jl") -include("runserver.jl") -include("staticlint.jl") - -include("requests/misc.jl") -include("requests/textdocument.jl") -include("requests/features.jl") -include("requests/hover.jl") -include("requests/completions.jl") -include("requests/workspace.jl") -include("requests/actions.jl") -include("requests/init.jl") -include("requests/signatures.jl") -include("requests/highlight.jl") -include("utilities.jl") -include("precompile.jl") - -end +module LanguageServer +using JSON, REPL, CSTParser, JuliaFormatter, SymbolServer, StaticLint +using CSTParser: EXPR, Tokenize.Tokens, Tokenize.Tokens.kind, headof, parentof, valof, to_codeobject +using StaticLint: refof, scopeof, bindingof +using UUIDs +using Base.Docs, Markdown +import JSONRPC +using JSONRPC: Outbound, @dict_readable +import TestItemDetection +import Logging +using PrecompileTools + +export LanguageServerInstance, runserver + +include("URIs2/URIs2.jl") +using .URIs2 + +JSON.lower(uri::URI) = string(uri) + +include("exception_types.jl") +include("protocol/protocol.jl") +include("extensions/extensions.jl") +include("textdocument.jl") +include("document.jl") +include("juliaworkspace.jl") +include("languageserverinstance.jl") +include("multienv.jl") +include("runserver.jl") +include("staticlint.jl") + +include("requests/misc.jl") +include("requests/textdocument.jl") +include("requests/features.jl") +include("requests/hover.jl") +include("requests/completions.jl") +include("requests/workspace.jl") +include("requests/actions.jl") +include("requests/init.jl") +include("requests/signatures.jl") +include("requests/highlight.jl") +include("utilities.jl") +include("precompile.jl") + +end From a8f9e9b5908566b1e8b7abc38a902123f45847ce Mon Sep 17 00:00:00 2001 From: Chetan Vardhan <51269425+VarLad@users.noreply.github.com> Date: Sun, 10 Mar 2024 02:11:05 +0530 Subject: [PATCH 54/55] Add check for `params.capabilities.window.workDoneProgress` --- src/requests/init.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/init.jl b/src/requests/init.jl index d7b1d5b9..eb5580b1 100644 --- a/src/requests/init.jl +++ b/src/requests/init.jl @@ -163,7 +163,7 @@ function initialize_request(params::InitializeParams, server::LanguageServerInst server.clientInfo = params.clientInfo server.editor_pid = params.processId - if !ismissing(params.capabilities.window) && params.capabilities.window.workDoneProgress + if !ismissing(params.capabilities.window) && !ismissing(params.capabilities.window.workDoneProgress) && params.capabilities.window.workDoneProgress server.clientcapability_window_workdoneprogress = true else server.clientcapability_window_workdoneprogress = false From 93452f5847d2201eae86f45e3f242131954ebafd Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Mon, 18 Mar 2024 01:43:11 +0100 Subject: [PATCH 55/55] Organize imports: put module self-binding first This patch changes the organize import action to put any self-bindings first in the sorted import list. Before this patch sorting results in ```julia using Example: AbstractThing, Example ``` and after the patch ```julia using Example: Example, AbstractThing ``` --- src/requests/actions.jl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/requests/actions.jl b/src/requests/actions.jl index 0a67f928..c6aa2a7e 100644 --- a/src/requests/actions.jl +++ b/src/requests/actions.jl @@ -498,19 +498,28 @@ function organize_import_block(x, _, conn) # BlueStyle (modules, types, ..., functions) since usually CamelCase is used for # modules, types, etc, but possibly this can be improved by using information # available from SymbolServer + function sort_with_self_first(set, self) + self′ = pop!(set, self, nothing) + x = sort!(collect(set)) + if self′ !== nothing + @assert self == self′ + pushfirst!(x, self) + end + return x + end import_lines = String[] for m in import_mods push!(import_lines, "import " * m) end for (m, s) in import_syms - push!(import_lines, "import " * m * ": " * join(sort!(collect(s)), ", ")) + push!(import_lines, "import " * m * ": " * join(sort_with_self_first(s, m), ", ")) end using_lines = String[] for m in using_mods push!(using_lines, "using " * m) end for (m, s) in using_syms - push!(using_lines, "using " * m * ": " * join(sort!(collect(s)), ", ")) + push!(using_lines, "using " * m * ": " * join(sort_with_self_first(s, m), ", ")) end io = IOBuffer() join(io, sort!(import_lines), "\n")