Skip to content

Commit 510d5f9

Browse files
committed
add LS integration
1 parent 579d3e8 commit 510d5f9

8 files changed

Lines changed: 396 additions & 1 deletion

File tree

src/JETLS.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Analyzer.LSAnalyzer(args...; kwargs...) = LSAnalyzer(ScriptAnalysisEntry(filepat
4141
include("analysis/resolver.jl")
4242

4343
include("testrunner-types.jl")
44+
include("report-trim-types.jl")
4445
include("types.jl")
4546

4647
include("utils/general.jl")
@@ -69,6 +70,7 @@ include("diagnostics.jl")
6970
include("code-lens.jl")
7071
include("code-action.jl")
7172
include("testrunner.jl")
73+
include("report-trim.jl")
7274
include("response.jl")
7375
include("lifecycle.jl")
7476

src/code-lens.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function handle_CodeLensRequest(server::Server, msg::CodeLensRequest)
3333
end
3434
code_lenses = CodeLens[]
3535
testrunner_code_lenses!(code_lenses, uri, fi)
36+
report_trim_code_lens!(code_lenses, uri, fi)
3637
return send(server,
3738
CodeLensResponse(;
3839
id = msg.id,

src/document-synchronization.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ function handle_DidChangeTextDocumentNotification(server::Server, msg::DidChange
126126
text = last(contentChanges).text
127127
fi = cache_file_info!(server.state, uri, textDocument.version, text)
128128
update_testsetinfos!(server, fi)
129+
update_entrypoint!(server, fi)
129130
end
130131

131132
function handle_DidSaveTextDocumentNotification(server::Server, msg::DidSaveTextDocumentNotification)

src/execute-command.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ const COMMAND_TESTRUNNER_RUN_TESTCASE = "JETLS.TestRunner.run@test"
66
const COMMAND_TESTRUNNER_CLEAR_RESULT = "JETLS.TestRunner.clearResult"
77
const COMMAND_TESTRUNNER_OPEN_LOGS = "JETLS.TestRunner.openLogs"
88

9+
const COMMAND_REPORT_TRIM_RUN = "JETLS.ReportTrim.run"
10+
const COMMAND_REPORT_TRIM_CLEAR_RESULT = "JETLS.ReportTrim.clearResult"
11+
912
const SUPPORTED_COMMANDS = [
1013
COMMAND_TESTRUNNER_RUN_TESTSET,
1114
COMMAND_TESTRUNNER_RUN_TESTCASE,
1215
COMMAND_TESTRUNNER_OPEN_LOGS,
1316
COMMAND_TESTRUNNER_CLEAR_RESULT,
17+
COMMAND_REPORT_TRIM_RUN,
18+
COMMAND_REPORT_TRIM_CLEAR_RESULT,
1419
]
1520

1621
function execute_command_options()
@@ -42,6 +47,10 @@ function handle_ExecuteCommandRequest(server::Server, msg::ExecuteCommandRequest
4247
return execute_testrunner_open_logs_command(server, msg)
4348
elseif command == COMMAND_TESTRUNNER_CLEAR_RESULT
4449
return execute_testrunner_clear_result_command(server, msg)
50+
elseif command == COMMAND_REPORT_TRIM_RUN
51+
return execute_report_trim_run_command(server, msg)
52+
elseif command == COMMAND_REPORT_TRIM_CLEAR_RESULT
53+
return execute_report_trim_clear_result_command(server, msg)
4554
end
4655
return send(server,
4756
invalid_execute_command_response(msg, "Unknown execution command: $command"))
@@ -140,3 +149,30 @@ function execute_testrunner_clear_result_command(server::Server, msg::ExecuteCom
140149
id = msg.id,
141150
result = null))
142151
end
152+
153+
function execute_report_trim_run_command(server::Server, msg::ExecuteCommandRequest)
154+
uri = convert(URI, @tryparsearg server msg[1]::String)
155+
error_msg = report_trim_run_from_uri(server, uri)
156+
if error_msg !== nothing
157+
show_error_message(server, error_msg)
158+
return send(server,
159+
ExecuteCommandResponse(;
160+
id = msg.id,
161+
result = nothing,
162+
error = request_failed_error(error_msg)))
163+
end
164+
return send(server,
165+
ExecuteCommandResponse(;
166+
id = msg.id,
167+
result = null))
168+
end
169+
170+
171+
function execute_report_trim_clear_result_command(server::Server, msg::ExecuteCommandRequest)
172+
uri = convert(URI, @tryparsearg server msg[1]::String)
173+
try_clear_report_trim_result!(server, uri)
174+
return send(server,
175+
ExecuteCommandResponse(;
176+
id = msg.id,
177+
result = null))
178+
end

src/report-trim-types.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
struct ReportTrimResult
2+
filepath::String
3+
success::Bool
4+
diagnostics::Vector{Diagnostic}
5+
end

src/report-trim.jl

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
const REPORT_TRIM_DIAGNOSTIC_SOURCE = "JETLS - TrimAnalyzer"
2+
3+
const REPORT_TRIM_RUN_TITLE = "▶ Run TrimAnalyzer"
4+
const REPORT_TRIM_RERUN_TITLE = "▶ Rerun TrimAnalyzer"
5+
const REPORT_TRIM_CLEAR_RESULT_TITLE = "✓ Clear result"
6+
7+
function update_entrypoint!(server::Server, fi::FileInfo)
8+
st0_top = build_tree!(JL.SyntaxTree, fi)
9+
entrypoint = find_main_entrypoint(st0_top)
10+
prev_entrypointinfo = fi.entrypointinfo
11+
any_deleted = false
12+
13+
if isnothing(entrypoint)
14+
if !isnothing(prev_entrypointinfo)
15+
if isdefined(prev_entrypointinfo, :result)
16+
key = prev_entrypointinfo.result.key
17+
any_deleted |= clear_extra_diagnostics!(server, key)
18+
end
19+
fi.entrypointinfo = nothing
20+
end
21+
else
22+
if !isnothing(prev_entrypointinfo)
23+
if isdefined(prev_entrypointinfo, :result)
24+
# Preserve the result if the entrypoint hasn't changed
25+
fi.entrypointinfo = EntrypointInfo(entrypoint, prev_entrypointinfo.result)
26+
else
27+
fi.entrypointinfo = EntrypointInfo(entrypoint)
28+
end
29+
else
30+
fi.entrypointinfo = EntrypointInfo(entrypoint)
31+
end
32+
end
33+
34+
if any_deleted
35+
notify_diagnostics!(server)
36+
end
37+
38+
return fi.entrypointinfo
39+
end
40+
41+
# TODO support `function @main(args::Vector{String}) ... end`
42+
function find_main_entrypoint(st0_top::SyntaxTree0)
43+
for st0 in JS.children(st0_top)
44+
if JS.kind(st0) === JS.K"function" && JS.numchildren(st0) >= 2
45+
st01 = st0[1]
46+
if JS.kind(st01) === JS.K"call" && JS.numchildren(st01) >= 1
47+
st011 = st01[1]
48+
if JS.kind(st011) === JS.K"macrocall" && JS.numchildren(st011) >= 1
49+
st0111 = st011[1]
50+
if JS.kind(st0111) === JS.K"MacroName" && hasproperty(st0111, :name_val)
51+
if st0111.name_val == "@main"
52+
return st0
53+
end
54+
end
55+
end
56+
end
57+
end
58+
end
59+
return nothing
60+
end
61+
62+
function report_trim_code_lens!(code_lenses, uri::URI, fi::FileInfo)
63+
entrypointinfo = fi.entrypointinfo
64+
if isnothing(entrypointinfo)
65+
return
66+
end
67+
68+
range = get_source_range(entrypointinfo.st0)
69+
run_arguments = Any[uri]
70+
71+
if isdefined(entrypointinfo, :result)
72+
result = entrypointinfo.result.result
73+
summary = result.success ? "✓ No dispatch errors" : "$(length(result.diagnostics)) dispatch error(s)"
74+
75+
command = Command(;
76+
title = "$REPORT_TRIM_RERUN_TITLE $summary",
77+
command = COMMAND_REPORT_TRIM_RUN,
78+
arguments = run_arguments)
79+
push!(code_lenses, CodeLens(; range, command))
80+
81+
command = Command(;
82+
title = REPORT_TRIM_CLEAR_RESULT_TITLE,
83+
command = COMMAND_REPORT_TRIM_CLEAR_RESULT,
84+
arguments = run_arguments)
85+
push!(code_lenses, CodeLens(; range, command))
86+
else
87+
command = Command(;
88+
title = REPORT_TRIM_RUN_TITLE,
89+
command = COMMAND_REPORT_TRIM_RUN,
90+
arguments = run_arguments)
91+
push!(code_lenses, CodeLens(; range, command))
92+
end
93+
94+
return code_lenses
95+
end
96+
97+
function report_trim_cmd(filepath::String, env_path::Union{Nothing,String})
98+
report_trim_exe = Sys.which("report-trim")
99+
if isnothing(env_path)
100+
return `$report_trim_exe --json $filepath`
101+
else
102+
return `$report_trim_exe --project=$env_path --json $filepath`
103+
end
104+
end
105+
106+
function report_trim_result_to_diagnostics(result::ReportTrimResult)
107+
uri2diagnostics = URI2Diagnostics()
108+
uri = filename2uri(result.filepath)
109+
isnothing(uri) && return uri2diagnostics
110+
uri2diagnostics[uri] = result.diagnostics
111+
return uri2diagnostics
112+
end
113+
114+
function report_trim_run(server::Server, uri::URI, fi::FileInfo, filepath::String;
115+
token::Union{Nothing,ProgressToken}=nothing)
116+
if isnothing(Sys.which("report-trim"))
117+
show_error_message(server, """
118+
`report-trim` executable is not found on the `PATH`.
119+
Please install TrimAnalyzer app to use this feature.
120+
""")
121+
return token !== nothing && end_report_trim_progress(server, token, "TrimAnalyzer not installed")
122+
end
123+
124+
if token !== nothing
125+
send(server, ProgressNotification(;
126+
params = ProgressParams(;
127+
token,
128+
value = WorkDoneProgressBegin(;
129+
title = "Running TrimAnalyzer",
130+
cancellable = false))))
131+
end
132+
133+
local result::String
134+
try
135+
result = _report_trim_run(server, uri, fi, filepath)
136+
catch err
137+
result = sprint(Base.showerror, err, catch_backtrace())
138+
@error "Error from TrimAnalyzer executor" err
139+
show_error_message(server, """
140+
An unexpected error occurred while running TrimAnalyzer:
141+
See the server log for details.
142+
""")
143+
finally
144+
@assert @isdefined(result) "`result` should be defined at this point"
145+
if token !== nothing
146+
end_report_trim_progress(server, token, result)
147+
end
148+
end
149+
end
150+
151+
function _report_trim_run(server::Server, uri::URI, fi::FileInfo, filepath::String)
152+
env_path = find_uri_env_path(server.state, uri)
153+
cmd = report_trim_cmd(filepath, env_path)
154+
155+
proc = open(cmd; read=true, write=false)
156+
output = read(proc, String)
157+
wait(proc)
158+
159+
result = try
160+
JSONRPC.JSON3.read(output, ReportTrimResult)
161+
catch err
162+
@error "Error parsing TrimAnalyzer output" err output
163+
show_error_message(server, """
164+
Failed to parse TrimAnalyzer output.
165+
See the server log for details.
166+
""")
167+
return "Analysis failed"
168+
end
169+
170+
if !isnothing(fi.entrypointinfo)
171+
key = ReportTrimDiagnosticsKey(fi)
172+
report_trim_info = ReportTrimInfo(result, key)
173+
fi.entrypointinfo = EntrypointInfo(fi.entrypointinfo.st0, report_trim_info)
174+
175+
uri2diagnostics = report_trim_result_to_diagnostics(result)
176+
if !isempty(result.diagnostics)
177+
server.state.extra_diagnostics[key] = uri2diagnostics
178+
elseif haskey(server.state.extra_diagnostics, key)
179+
delete!(server.state.extra_diagnostics, key)
180+
end
181+
notify_diagnostics!(server)
182+
183+
if supports(server, :workspace, :codeLens, :refreshSupport)
184+
request_codelens_refresh!(server)
185+
end
186+
end
187+
188+
show_report_trim_result_in_message(server, result, fi, uri)
189+
190+
summary = result.success ? "✓ No dispatch errors found" : "✗ Found $(length(result.diagnostics)) dispatch error(s)"
191+
return summary
192+
end
193+
194+
function show_report_trim_result_in_message(server::Server, result::ReportTrimResult, fi::FileInfo, uri::URI)
195+
summary = result.success ? "✓ No dispatch errors found" : "✗ Found $(length(result.diagnostics)) dispatch error(s)"
196+
message = "TrimAnalyzer: $summary"
197+
198+
msg_type = if !result.success
199+
MessageType.Error
200+
else
201+
MessageType.Info
202+
end
203+
204+
actions = MessageActionItem[
205+
MessageActionItem(; title = REPORT_TRIM_RERUN_TITLE),
206+
MessageActionItem(; title = REPORT_TRIM_CLEAR_RESULT_TITLE)
207+
]
208+
209+
id = String(gensym(:ShowMessageRequest))
210+
server.state.currently_requested[id] = ReportTrimMessageRequestCaller(uri, fi)
211+
212+
send(server, ShowMessageRequest(;
213+
id,
214+
params = ShowMessageRequestParams(;
215+
type = msg_type,
216+
message,
217+
actions)))
218+
end
219+
220+
function end_report_trim_progress(server::Server, token::ProgressToken, message::String)
221+
send(server, ProgressNotification(;
222+
params = ProgressParams(;
223+
token,
224+
value = WorkDoneProgressEnd(; message))))
225+
end
226+
227+
struct ReportTrimMessageRequestCaller <: RequestCaller
228+
uri::URI
229+
fi::FileInfo
230+
end
231+
232+
struct ReportTrimProgressCaller <: RequestCaller
233+
uri::URI
234+
fi::FileInfo
235+
filepath::String
236+
token::ProgressToken
237+
end
238+
239+
function report_trim_run_from_uri(server::Server, uri::URI)
240+
fi = get_file_info(server.state, uri)
241+
if fi === nothing
242+
return "File is no longer available in the editor"
243+
end
244+
245+
if isnothing(fi.entrypointinfo)
246+
return "No @main function found in this file"
247+
end
248+
249+
sfi = get_saved_file_info(server.state, uri)
250+
if sfi === nothing
251+
return "The file appears not to exist on disk. Save the file first to run TrimAnalyzer."
252+
elseif JS.sourcetext(fi.parsed_stream) JS.sourcetext(sfi.parsed_stream)
253+
return "The editor state differs from the saved file. Save the file first to run TrimAnalyzer."
254+
end
255+
256+
filepath = uri2filepath(uri)
257+
if isnothing(filepath)
258+
return "Cannot determine file path for the URI"
259+
end
260+
261+
if supports(server, :window, :workDoneProgress)
262+
id = String(gensym(:WorkDoneProgressCreateRequest_report_trim))
263+
token = String(gensym(:ReportTrimProgress))
264+
server.state.currently_requested[id] = ReportTrimProgressCaller(uri, fi, filepath, token)
265+
params = WorkDoneProgressCreateParams(; token)
266+
send(server, WorkDoneProgressCreateRequest(; id, params))
267+
else
268+
@async report_trim_run(server, uri, fi, filepath)
269+
end
270+
271+
return nothing
272+
end
273+
274+
275+
function try_clear_report_trim_result!(server::Server, uri::URI)
276+
fi = get_file_info(server.state, uri)
277+
if fi === nothing || isnothing(fi.entrypointinfo)
278+
return nothing
279+
end
280+
281+
if isdefined(fi.entrypointinfo, :result)
282+
fi.entrypointinfo = EntrypointInfo(fi.entrypointinfo.st0)
283+
284+
if clear_extra_diagnostics!(server, ReportTrimDiagnosticsKey(fi))
285+
notify_diagnostics!(server)
286+
end
287+
288+
if supports(server, :workspace, :codeLens, :refreshSupport)
289+
request_codelens_refresh!(server)
290+
end
291+
end
292+
293+
return nothing
294+
end

0 commit comments

Comments
 (0)