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/.github/workflows/jlpkgbutler-ci-master-workflow.yml b/.github/workflows/jlpkgbutler-ci-master-workflow.yml index 09cff08a..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'] + 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 9114217f..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'] + 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/Project.toml b/Project.toml index e2e74850..a596b9a6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,41 +1,44 @@ name = "LanguageServer" uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" -version = "4.3.2-DEV" +version = "4.5.1" [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" -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" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" 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" 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" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" -TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +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" +PrecompileTools = "1" StaticLint = "8.0" -Tokenize = "0.5.10" -JSONRPC = "1.1" SymbolServer = "7.1" -TestItemDetection = "0.1.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/README.md b/README.md index 4c2edf10..576c840d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ 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/) +- [Kate](../../wiki/Kate) +- [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) @@ -41,3 +44,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 diff --git a/src/LanguageServer.jl b/src/LanguageServer.jl index 259fc01f..d784cfdb 100644 --- a/src/LanguageServer.jl +++ b/src/LanguageServer.jl @@ -1,41 +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 - -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") - -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 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/languageserverinstance.jl b/src/languageserverinstance.jl index 435e1874..051f334a 100644 --- a/src/languageserverinstance.jl +++ b/src/languageserverinstance.jl @@ -44,6 +44,9 @@ mutable struct LanguageServerInstance lint_disableddirs::Vector{String} completion_mode::Symbol complete_func_parens::Bool + inlay_hints::Bool + inlay_hints_variable_types::Bool + inlay_hints_parameter_names::Symbol combined_msg_queue::Channel{Any} @@ -63,18 +66,19 @@ mutable struct LanguageServerInstance clientInfo::Union{InfoParams,Missing} initialization_options::Union{Missing,Dict} + editor_pid::Union{Nothing,Int} shutdown_requested::Bool 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}(), 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(), @@ -85,6 +89,9 @@ mutable struct LanguageServerInstance LINT_DIABLED_DIRS, :qualify, # options: :import or :qualify, anything else turns this off false, + true, + true, + :literals, Channel{Any}(Inf), err_handler, :created, @@ -96,6 +103,7 @@ mutable struct LanguageServerInstance missing, missing, missing, + nothing, false, JuliaWorkspace() ) @@ -186,7 +194,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( @@ -194,13 +202,11 @@ 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 + download=server.symserver_use_download ) server.number_of_outstanding_symserver_requests -= 1 @@ -276,61 +282,67 @@ 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) + poll_editor_pid(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)) + put!(server.combined_msg_queue, (type=:clientmsg, msg=msg)) end catch err bt = catch_backtrace() 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) - 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." 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)) + put!(server.combined_msg_queue, (type=:symservmsg, msg=msg)) end catch err bt = catch_backtrace() 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) - 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." end + yield() - @debug "Symbol Server started at $(round(Int, time()))" + @debug "async tasks started at $(round(Int, time()))" msg_dispatcher = JSONRPC.MsgDispatcher() @@ -371,28 +383,37 @@ 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 # 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 @@ -404,11 +425,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 @@ -424,11 +448,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/precompile.jl b/src/precompile.jl new file mode 100644 index 00000000..32de9f78 --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,16 @@ +@setup_workload begin + iob = IOBuffer() + println(iob) + @compile_workload begin + # Suppress errors + if get(ENV, "JULIA_DEBUG", "") in ("all", "LanguageServer") + precompile_logger = Logging.ConsoleLogger() + else + precompile_logger = Logging.NullLogger() + end + Logging.with_logger(precompile_logger) do + runserver(iob) + end + end +end +precompile(runserver, ()) diff --git a/src/protocol/features.jl b/src/protocol/features.jl index 57a1e8a8..b6290a8b 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/actions.jl b/src/requests/actions.jl index 63f47b25..c6aa2a7e 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 @@ -350,14 +350,19 @@ 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 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 @@ -374,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 @@ -384,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[ @@ -474,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") diff --git a/src/requests/completions.jl b/src/requests/completions.jl index 68abe5c2..5353dd40 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) diff --git a/src/requests/features.jl b/src/requests/features.jl index a57b24c4..9d232b64 100644 --- a/src/requests/features.jl +++ b/src/requests/features.jl @@ -183,51 +183,31 @@ 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)) + 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. + 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 +216,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) @@ -263,7 +252,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 @@ -334,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 @@ -350,10 +356,32 @@ function collect_document_symbols(x::EXPR, server::LanguageServerInstance, doc, push!(symbols, ds) symbols = ds.children end + elseif x.head == :macrocall + # 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" || 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 + 3, # kind (namespace) + 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 - collect_document_symbols(a, server, doc, pos, symbols) + collect_document_symbols(a, server, doc, pos, ctx, symbols) pos += a.fullspan end end @@ -389,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 @@ -402,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 @@ -477,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 @@ -504,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) @@ -538,3 +576,102 @@ function get_selection_range_of_expr(x::EXPR) l2, c2 = get_position_from_offset(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} + if !server.inlay_hints + return nothing + end + + doc = getdocument(server, params.textDocument.uri) + + start, stop = get_offset(doc, params.range.start), get_offset(doc, params.range.stop) + + 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)) && + !( + parentof(parentof(x)) isa EXPR && + CSTParser.defines_function(parentof(parentof(x))) + ) && + parentof(x).args[1] != x # function calls + 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)) && + parentof(x).args[1] == x && + StaticLint.hasbinding(x) # assignment + 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 + for a in x + 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 +end diff --git a/src/requests/hover.jl b/src/requests/hover.jl index 1596db49..d5172c41 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 @@ -70,12 +70,13 @@ function get_tooltip(b::StaticLint.Binding, documentation::String, server; show_ end end elseif b.val isa SymbolServer.SymStore - documentation = get_hover(b.val, documentation, server) + documentation = get_hover(b.val, documentation, server, expr, env) end 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,22 +137,34 @@ 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 + if expr !== nothing && env !== nothing + 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 + + method_count = 0 + totalio = IOBuffer() + itr() do m + method_count += 1 + io = IOBuffer() print(io, m.name, "(") nsig = length(m.sig) @@ -165,24 +178,32 @@ function get_hover(f::SymbolServer.FunctionStore, documentation::String, server) print(io, ")") sig = String(take!(io)) - text = replace(string(m.file, ':', m.line), "\\" => "\\\\") + 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 - - documentation = string(documentation, "- `$(sig)` in `$(m.mod)` at [$(text)]($(link))", '\n') + 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 - -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) @@ -267,7 +288,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/src/requests/init.jl b/src/requests/init.jl index a8b21f77..eb5580b1 100644 --- a/src/requests/init.jl +++ b/src/requests/init.jl @@ -31,6 +31,7 @@ function ServerCapabilities(client::ClientCapabilities) ExecuteCommandOptions(missing, collect(keys(LSActions))), true, true, + true, WorkspaceOptions(WorkspaceFoldersOptions(true, true)), missing ) @@ -160,8 +161,9 @@ 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 + 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 @@ -175,7 +177,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 diff --git a/src/requests/signatures.jl b/src/requests/signatures.jl index 197935d6..46fb696b 100644 --- a/src/requests/signatures.jl +++ b/src/requests/signatures.jl @@ -1,9 +1,38 @@ 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 + + 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 +47,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 diff --git a/src/requests/textdocument.jl b/src/requests/textdocument.jl index 4be191a0..f4e1125e 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 @@ -85,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 @@ -106,8 +112,16 @@ 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 > 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 @@ -120,6 +134,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 @@ -198,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) @@ -237,7 +253,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 @@ -385,7 +401,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) @@ -409,7 +425,9 @@ function find_testitems!(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) @@ -420,25 +438,31 @@ function find_testitems!(doc, server::LanguageServerInstance, jr_endpoint) cst = getcst(doc) testitems = [] + testsetups = [] + testerrors = [] 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]) + 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]) 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 diff --git a/src/requests/workspace.jl b/src/requests/workspace.jl index b97f6a73..aa42a358 100644 --- a/src/requests/workspace.jl +++ b/src/requests/workspace.jl @@ -109,6 +109,9 @@ function request_julia_config(server::LanguageServerInstance, conn) ConfigurationItem(missing, "julia.lint.disabledDirs"), ConfigurationItem(missing, "julia.completionmode"), ConfigurationItem(missing, "julia.completeFunctionParens") + 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) @@ -118,7 +121,10 @@ function request_julia_config(server::LanguageServerInstance, conn) new_lint_disableddirs = something(response[13], LINT_DIABLED_DIRS) new_completion_mode = Symbol(something(response[14], :import)) new_complete_func_parens = something(response[15], false) - + inlayHints = something(response[16], true) + inlayHintsVariableTypes = something(response[17], true) + inlayHintsParameterNames = Symbol(something(response[18], :literals)) + rerun_lint = begin any(getproperty(server.lint_options, opt) != getproperty(new_SL_opts, opt) for opt in fieldnames(StaticLint.LintOptions)) || server.runlinter != new_runlinter || @@ -132,6 +138,9 @@ function request_julia_config(server::LanguageServerInstance, conn) server.lint_disableddirs = new_lint_disableddirs server.completion_mode = new_completion_mode server.complete_func_parens = new_complete_func_parens + server.inlay_hints = inlayHints + server.inlay_hints_variable_types = inlayHintsVariableTypes + server.inlay_hints_parameter_names = inlayHintsParameterNames if rerun_lint relintserver(server) 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/staticlint.jl b/src/staticlint.jl index 69140f9d..b0ff36db 100644 --- a/src/staticlint.jl +++ b/src/staticlint.jl @@ -71,12 +71,12 @@ 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) # 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 diff --git a/src/utilities.jl b/src/utilities.jl index 3ef9e115..73487f04 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( + -33100, + "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, + -33101, "version mismatch in $(msg) request for $(uri): JLS $(get_version(doc)), client: $(params.version)", data ) @@ -195,6 +200,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) @@ -440,7 +446,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 +454,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 +485,68 @@ end return Base.UUID(u) end end -end \ No newline at end of file +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 + +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 diff --git a/test/requests/test_completions.jl b/test/requests/test_completions.jl index dcb2bd83..640255a2 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 @@ -42,15 +42,12 @@ 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 .""") @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) @@ -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 @@ -175,7 +175,7 @@ end @testitem "completion details" begin include("../test_shared_server.jl") - + settestdoc(""" struct Bar end struct Foo diff --git a/test/requests/test_features.jl b/test/requests/test_features.jl index 5fa9561b..791ca435 100644 --- a/test/requests/test_features.jl +++ b/test/requests/test_features.jl @@ -93,7 +93,7 @@ end @testitem "doc symbols" begin include("../test_shared_server.jl") - + doc = settestdoc(""" a = 1 b = 2 @@ -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/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 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 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