Skip to content
Merged
6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
DocumentFormat = "ffa9a821-9c82-50df-894e-fbcef3ed31cd"
CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f"
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
StaticLint = "b3cc710f-9c33-5bdb-a03d-a94903873e97"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JSONRPC = "b9b8584e-8fd3-41f9-ad0c-7255d428e418"
CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
SymbolServer = "cf896787-08d5-524d-9de7-132aaa0cb996"
URIParser = "30578b45-9adc-5946-b283-645ec420af67"
Expand All @@ -27,7 +27,7 @@ Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
JSON = "0.20, 0.21"
julia = "1"
CSTParser = "3.1"
DocumentFormat = "3.2.2"
JuliaFormatter = "0.15.4"
StaticLint = "8.0"
Tokenize = "0.5.10"
JSONRPC = "1.1"
Expand Down
2 changes: 1 addition & 1 deletion src/LanguageServer.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module LanguageServer
import URIParser
using JSON, REPL, CSTParser, DocumentFormat, SymbolServer, StaticLint
using JSON, REPL, CSTParser, JuliaFormatter, SymbolServer, StaticLint
using CSTParser: EXPR, Tokenize.Tokens, Tokenize.Tokens.kind, headof, parentof, valof
using StaticLint: refof, scopeof, bindingof
using UUIDs
Expand Down
5 changes: 4 additions & 1 deletion src/document.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function set_doc(x::EXPR, doc)
x.meta.error = doc
end

function get_path(doc)
return doc._path
end

function get_text(doc::Document)
return doc._content
Expand Down Expand Up @@ -65,7 +68,7 @@ end
"""
get_offset(doc, line, char)

Returns the byte offset position corresponding to a line/character position.
Returns the 0 based byte offset position corresponding to a line/character position.
This takes 0 based line/char inputs. Corresponding functions are available for
Position and Range arguments, the latter returning a UnitRange{Int}.
"""
Expand Down
3 changes: 1 addition & 2 deletions src/languageserverinstance.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ mutable struct LanguageServerInstance
roots_env_map::Dict{Document,StaticLint.ExternalEnv}
symbol_store_ready::Bool

format_options::DocumentFormat.FormatOptions
runlinter::Bool
lint_options::StaticLint.LintOptions
lint_missingrefs::Symbol
Expand Down Expand Up @@ -76,7 +75,6 @@ mutable struct LanguageServerInstance
StaticLint.ExternalEnv(deepcopy(SymbolServer.stdlibs), SymbolServer.collect_extended_methods(SymbolServer.stdlibs), collect(keys(SymbolServer.stdlibs))),
Dict(),
false,
DocumentFormat.FormatOptions(),
true,
StaticLint.LintOptions(),
:all,
Expand Down Expand Up @@ -319,6 +317,7 @@ function Base.run(server::LanguageServerInstance)
msg_dispatcher[textDocument_signatureHelp_request_type] = request_wrapper(textDocument_signatureHelp_request, server)
msg_dispatcher[textDocument_definition_request_type] = request_wrapper(textDocument_definition_request, server)
msg_dispatcher[textDocument_formatting_request_type] = request_wrapper(textDocument_formatting_request, server)
msg_dispatcher[textDocument_range_formatting_request_type] = request_wrapper(textDocument_range_formatting_request, server)
msg_dispatcher[textDocument_references_request_type] = request_wrapper(textDocument_references_request, server)
msg_dispatcher[textDocument_rename_request_type] = request_wrapper(textDocument_rename_request, server)
msg_dispatcher[textDocument_prepareRename_request_type] = request_wrapper(textDocument_prepareRename_request, server)
Expand Down
1 change: 1 addition & 0 deletions src/protocol/messagedefs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const textDocument_completion_request_type = JSONRPC.RequestType("textDocument/c
const textDocument_signatureHelp_request_type = JSONRPC.RequestType("textDocument/signatureHelp", TextDocumentPositionParams, Union{SignatureHelp, Nothing})
const textDocument_definition_request_type = JSONRPC.RequestType("textDocument/definition", TextDocumentPositionParams, Union{Location, Vector{Location}, Vector{LocationLink}, Nothing})
const textDocument_formatting_request_type = JSONRPC.RequestType("textDocument/formatting", DocumentFormattingParams, Union{Vector{TextEdit}, Nothing})
const textDocument_range_formatting_request_type = JSONRPC.RequestType("textDocument/rangeFormatting", DocumentRangeFormattingParams, Union{Vector{TextEdit}, Nothing})
const textDocument_references_request_type = JSONRPC.RequestType("textDocument/references", ReferenceParams, Union{Vector{Location}, Nothing})
const textDocument_rename_request_type = JSONRPC.RequestType("textDocument/rename", RenameParams, Union{WorkspaceEdit, Nothing})
const textDocument_prepareRename_request_type = JSONRPC.RequestType("textDocument/prepareRename", PrepareRenameParams, Range)
Expand Down
103 changes: 102 additions & 1 deletion src/requests/features.jl
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,116 @@ function get_file_loc(x::EXPR, offset=0, c=nothing)
end
end

function search_file(filename, dir, topdir)
parent_dir = dirname(dir)
return if (!startswith(dir, topdir) || parent_dir == dir || isempty(dir))
nothing
else
path = joinpath(dir, filename)
isfile(path) ? path : search_file(filename, parent_dir, topdir)
end
end

function get_juliaformatter_config(doc, server)
path = get_path(doc)

# search through workspace for a `.JuliaFormatter.toml`
workspace_dirs = sort(filter(f -> startswith(path, f), collect(server.workspaceFolders)), by = length, rev = true)
config_path = length(workspace_dirs) > 0 ?
search_file(JuliaFormatter.CONFIG_FILE_NAME, path, workspace_dirs[1]) :
nothing

config_path === nothing && return nothing

@debug "Found JuliaFormatter config at $(config_path)"
return JuliaFormatter.parse_config(config_path)
end

function textDocument_formatting_request(params::DocumentFormattingParams, server::LanguageServerInstance, conn)
doc = getdocument(server, URI2(params.textDocument.uri))
newcontent = DocumentFormat.format(get_text(doc), server.format_options)

config = get_juliaformatter_config(doc, server)

newcontent = if config === nothing
JuliaFormatter.format_text(get_text(doc); indent=params.options.tabSize)
else
JuliaFormatter.format_text(get_text(doc); JuliaFormatter.kwargs(config)...)
end

end_l, end_c = get_position_at(doc, sizeof(get_text(doc))) # AUDIT: OK
lsedits = TextEdit[TextEdit(Range(0, 0, end_l, end_c), newcontent)]

return lsedits
end

function textDocument_range_formatting_request(params::DocumentRangeFormattingParams, server::LanguageServerInstance, conn)
doc = getdocument(server, URI2(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))
if expr.parent !== nothing
expr = expr.parent
else
return nothing
end
end

_, offset = get_file_loc(expr)
l1, c1 = get_position_at(doc, offset)
c1 = 0
start_offset = get_offset2(doc, l1, c1)
l2, c2 = get_position_at(doc, offset + expr.span)
end_offset = get_offset(doc, l2, c2)

text = get_text(doc)[start_offset:end_offset]

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

config = get_juliaformatter_config(doc, server)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this request get hit every key stroke? If so it's not ideal to search the disk each time. I can't remember whether we're able to have a file watch from within the languageserver which may be why this has been done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just add those files to https://github.com/julia-vscode/julia-vscode/blob/master/src/extension.ts#L232, and then we would get normal LSP messages about their content etc. That seems in general better?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this request get hit every key stroke?

No, only when explicitly requested.

We could just add those files to https://github.com/julia-vscode/julia-vscode/blob/master/src/extension.ts#L232, and then we would get normal LSP messages about their content etc. That seems in general better?

IIUC we'd only be notified by FS events or when the user opens/changes the .JuliaFormatter.toml. The client won't just dump all matching files into the server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client won't just dump all matching files into the server.

Not sure what you mean by that. We could just handle .JuliaFormatter.toml files the same way we handle all .jl files in the workspace, right? Then they would be in-memory always?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that so? I thought we'd load most files directly from disk?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we load initially, but then we don't need to reload because we get notifications whenever they are updated. If this is not hit with every keystroke that probably doesn't matter that much, though.

Having said that, the LSP will have to move to a model at some point where language servers just don't touch the file system at all and instead can get all content from the client, just to make things work properly with virtual file systems that are supported in the client. At the moment there is not support for that in the protocol itself, but we would probably be better prepared for that if we made these files "normal" files. On the other hand, lets not make that a roadblock for this PR :) The MinimalStyle is, though...


newcontent = try
if config === nothing
JuliaFormatter.format_text(text; indent=params.options.tabSize)
else
JuliaFormatter.format_text(text; JuliaFormatter.kwargs(config)...)
end
catch err
@debug "Formatter errored:" exception=(err, catch_backtrace())
return nothing
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))
end

lsedits = TextEdit[TextEdit(Range(l1, c1, l2, c2), newcontent)]

return lsedits
end

function find_references(textDocument::TextDocumentIdentifier, position::Position, server)
locations = Location[]
doc = getdocument(server, URI2(textDocument.uri))
Expand Down
2 changes: 1 addition & 1 deletion src/requests/init.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function ServerCapabilities(client::ClientCapabilities)
missing,
false,
true,
false,
true,
missing,
RenameOptions(missing, prepareSupport),
false,
Expand Down
25 changes: 6 additions & 19 deletions src/requests/workspace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,6 @@ function request_julia_config(server::LanguageServerInstance, conn)
(ismissing(server.clientCapabilities.workspace) || server.clientCapabilities.workspace.configuration !== true) && return

response = JSONRPC.send(conn, workspace_configuration_request_type, ConfigurationParams([
ConfigurationItem(missing, "julia.format.indent"), # FormatOptions
ConfigurationItem(missing, "julia.format.indents"),
ConfigurationItem(missing, "julia.format.ops"),
ConfigurationItem(missing, "julia.format.tuples"),
ConfigurationItem(missing, "julia.format.curly"),
ConfigurationItem(missing, "julia.format.calls"),
ConfigurationItem(missing, "julia.format.iterOps"),
ConfigurationItem(missing, "julia.format.comments"),
ConfigurationItem(missing, "julia.format.docs"),
ConfigurationItem(missing, "julia.format.lineends"),
ConfigurationItem(missing, "julia.format.keywords"),
ConfigurationItem(missing, "julia.format.kwarg"),
ConfigurationItem(missing, "julia.lint.call"), # LintOptions
ConfigurationItem(missing, "julia.lint.iter"),
ConfigurationItem(missing, "julia.lint.nothingcomp"),
Expand All @@ -108,15 +96,14 @@ function request_julia_config(server::LanguageServerInstance, conn)
ConfigurationItem(missing, "julia.lint.missingrefs"),
ConfigurationItem(missing, "julia.lint.disabledDirs"),
ConfigurationItem(missing, "julia.completionmode")
]))
]))

server.format_options = DocumentFormat.FormatOptions(response[1:12]...)
new_runlinter = something(response[23], true)
new_SL_opts = StaticLint.LintOptions(response[13:22]...)
new_runlinter = something(response[11], true)
new_SL_opts = StaticLint.LintOptions(response[1:10]...)

new_lint_missingrefs = Symbol(something(response[24], :all))
new_lint_disableddirs = something(response[25], LINT_DIABLED_DIRS)
new_completion_mode = Symbol(something(response[26], :import))
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))

rerun_lint = begin
any(getproperty(server.lint_options, opt) != getproperty(new_SL_opts, opt) for opt in fieldnames(StaticLint.LintOptions)) ||
Expand Down
26 changes: 25 additions & 1 deletion src/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,31 @@ 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
Expand Down Expand Up @@ -464,4 +489,3 @@ function is_in_target_dir_of_package(pkgpath, target)
return false
end
end