-
Notifications
You must be signed in to change notification settings - Fork 92
Expand file tree
/
Copy pathactions.jl
More file actions
288 lines (259 loc) · 13.3 KB
/
actions.jl
File metadata and controls
288 lines (259 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
struct ServerAction
command::Command
when::Function
handler::Function
end
function textDocument_codeAction_request(params::CodeActionParams, server::LanguageServerInstance, conn)
commands = Command[]
doc = getdocument(server, URI2(params.textDocument.uri))
offset = get_offset(doc, params.range.start) # Should usef get_offset2?
x = get_expr(getcst(doc), offset)
arguments = Any[params.textDocument.uri, offset] # use the same arguments for all commands
if x isa EXPR
for (_, sa) in LSActions
if sa.when(x, params)
push!(commands, Command(sa.command.title, sa.command.command, arguments))
end
end
end
return commands
end
function workspace_executeCommand_request(params::ExecuteCommandParams, server::LanguageServerInstance, conn)
uri = params.arguments[1]
offset = params.arguments[2]
doc = getdocument(server, URI2(uri))
x = get_expr(getcst(doc), offset)
if haskey(LSActions, params.command)
LSActions[params.command].handler(x, server, conn)
end
end
function find_using_statement(x::EXPR)
for ref in refof(x).refs
if StaticLint.is_in_fexpr(ref, x -> headof(x) === :using || headof(x) === :import)
return parentof(ref)
end
end
return nothing
end
function explicitly_import_used_variables(x::EXPR, server, conn)
!(refof(x) isa StaticLint.Binding && refof(x).val isa SymbolServer.ModuleStore) && return
using_stmt = find_using_statement(x)
using_stmt isa Nothing && return
tdes = Dict{String,TextDocumentEdit}()
vars = Set{String}() # names that need to be imported
# Find uses of `x` and mark edits
for ref in refof(x).refs
if parentof(ref) isa EXPR && CSTParser.is_getfield_w_quotenode(parentof(ref)) && parentof(ref).args[1] == ref
childname = parentof(ref).args[2].args[1]
StaticLint.hasref(childname) && refof(childname) isa StaticLint.Binding && continue # check this isn't the name of something being explictly overwritten
!haskey(refof(x).val.vals, Symbol(valof(childname))) && continue # skip, perhaps mark as missing ref ?
file, offset = get_file_loc(ref)
if !haskey(tdes, file._uri)
tdes[file._uri] = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[])
end
push!(tdes[file._uri].edits, TextEdit(Range(file, offset .+ (0:parentof(ref).span)), valof(childname)))
push!(vars, valof(childname))
end
end
isempty(tdes) && return
# Add `using x: vars...` statement
if parentof(using_stmt) isa EXPR && (headof(parentof(using_stmt)) === :block || headof(parentof(using_stmt)) === :file)
# this should cover all cases
i1 = 0
for i = 1:length(parentof(using_stmt).args)
if using_stmt === parentof(using_stmt).args[i]
i1 = i
break
end
end
i1 == 0 && return WorkspaceEdit(missing, missing)
file, offset = get_file_loc(using_stmt)
insertpos = get_next_line_offset(using_stmt)
insertpos == -1 && return
if !haskey(tdes, file._uri)
tdes[file._uri] = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[])
end
push!(tdes[file._uri].edits, TextEdit(Range(file, insertpos .+ (0:0)), string("using ", valof(x), ": ", join(vars, ", "), "\n")))
else
return
end
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, collect(values(tdes)))))
end
is_single_line_func(x) = CSTParser.defines_function(x) && headof(x) !== :function
function expand_inline_func(x, server, conn)
func = _get_parent_fexpr(x, is_single_line_func)
length(func) < 3 && return
sig = func.args[1]
op = func.head
body = func.args[2]
if headof(body) == :block && length(body) == 1
file, offset = get_file_loc(func)
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), 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"))
])
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
elseif (headof(body) === :begin || CSTParser.isbracketed(body)) &&
headof(body.args[1]) === :block && length(body.args[1]) > 0
file, offset = get_file_loc(func)
newtext = string("function ", get_text(file)[offset .+ (1:sig.span)])
blockoffset = offset + sig.fullspan + op.fullspan + body.trivia[1].fullspan
for i = 1:length(body.args[1].args)
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(file._uri, file._version), TextEdit[TextEdit(Range(file, offset .+ (0:func.fullspan)), newtext)])
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
end
end
function is_in_fexpr(x::EXPR, f)
if f(x)
return true
elseif parentof(x) isa EXPR
return is_in_fexpr(parentof(x), f)
else
return false
end
end
function _get_parent_fexpr(x::EXPR, f)
if f(x)
return x
elseif parentof(x) isa EXPR
return _get_parent_fexpr(parentof(x), f)
end
end
function get_next_line_offset(x)
file, offset = get_file_loc(x)
# get next line after using_stmt
insertpos = -1
line_offsets = get_line_offsets(file)
for i = 1:length(line_offsets) - 1
if line_offsets[i] < offset + x.span <= line_offsets[i + 1]
insertpos = line_offsets[i + 1]
end
end
return insertpos
end
function reexport_package(x::EXPR, server, conn)
(refof(x) isa SymbolServer.ModuleStore || refof(x).type === StaticLint.CoreTypes.Module || (refof(x).val isa StaticLint.Binding && refof(x).val.type === StaticLint.CoreTypes.Module)) || (refof(x).val isa SymbolServer.ModuleStore) || return
mod = if refof(x) isa SymbolServer.ModuleStore
refof(x)
elseif refof(x).val isa SymbolServer.ModuleStore
refof(x).val
else
return
end
using_stmt = parentof(x)
file, _ = get_file_loc(x)
insertpos = get_next_line_offset(using_stmt)
insertpos == -1 && return
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[
TextEdit(Range(file, insertpos .+ (0:0)), string("export ", join(sort([string(n) for (n, v) in mod.vals if StaticLint.isexportedby(n, mod)]), ", "), "\n"))
])
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
end
# TODO move to StaticL int
# to be called where typof(x) === CSTParser.ModuleH/BareModule
function find_exported_names(x::EXPR)
exported_vars = EXPR[]
for i in 1:length(x.args[3].args)
expr = x.args[3].args[i]
if headof(expr) === :export
for j = 2:length(expr)
if CSTParser.isidentifier(expr.args[j]) && StaticLint.hasref(expr.args[j])
push!(exported_vars, expr.args[j])
end
end
end
end
return exported_vars
end
function reexport_module(x::EXPR, server, conn)
using_stmt = parentof(x)
mod_expr = refof(x).val isa StaticLint.Binding ? refof(x).val.val : refof(x).val
(mod_expr.args isa Nothing || length(mod_expr.args) < 3 || headof(mod_expr.args[3]) !== :block || mod_expr.args[3].args isa Nothing) && return # module expr without block
# find export EXPR
exported_names = find_exported_names(mod_expr)
isempty(exported_names) && return
file, _ = get_file_loc(x)
insertpos = get_next_line_offset(using_stmt)
insertpos == -1 && return
names = filter!(s -> !isempty(s), collect(CSTParser.str_value.(exported_names)))
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[
TextEdit(Range(file, insertpos .+ (0:0)), string("export ", join(sort(names), ", "), "\n"))
])
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
end
function wrap_block(x, server, type, conn) end
function wrap_block(x::EXPR, server, type, conn)
file, offset = get_file_loc(x) # rese
if type == :if
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[
TextEdit(Range(file, offset .+ (0:0)), "if CONDITION\n"),
TextEdit(Range(file, offset + x.span .+ (0:0)), "\nend")
])
end
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
end
function is_fixable_missing_ref(x::EXPR, cac::CodeActionContext)
if !isempty(cac.diagnostics) && any(startswith(d.message, "Missing reference") for d::Diagnostic in cac.diagnostics) && CSTParser.isidentifier(x)
xname = StaticLint.valofid(x)
tls = StaticLint.retrieve_toplevel_scope(x)
if tls isa StaticLint.Scope && tls.modules !== nothing
for m in values(tls.modules)
if (m isa SymbolServer.ModuleStore && haskey(m, Symbol(xname))) || (m isa StaticLint.Scope && StaticLint.scopehasbinding(m, xname))
return true
end
end
end
end
return false
end
function applymissingreffix(x, server, conn)
xname = StaticLint.valofid(x)
file, offset = get_file_loc(x)
tls = StaticLint.retrieve_toplevel_scope(x)
if tls.modules !== nothing
for (n, m) in tls.modules
if (m isa SymbolServer.ModuleStore && haskey(m, Symbol(xname))) || (m isa StaticLint.Scope && StaticLint.scopehasbinding(m, xname))
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[
TextEdit(Range(file, offset .+ (0:0)), string(n, "."))
])
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
end
end
end
end
function remove_farg_name(x, server, conn)
x1 = StaticLint.get_parent_fexpr(x, x -> StaticLint.haserror(x) && StaticLint.errorof(x) == StaticLint.UnusedFunctionArgument)
file, offset = get_file_loc(x1)
if CSTParser.isdeclaration(x1)
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[
TextEdit(Range(file, offset .+ (0:x1.args[1].fullspan)), "")
])
else
tde = TextDocumentEdit(VersionedTextDocumentIdentifier(file._uri, file._version), TextEdit[
TextEdit(Range(file, offset .+ (0:x1.fullspan)), "::Any")
])
end
JSONRPC.send(conn, workspace_applyEdit_request_type, ApplyWorkspaceEditParams(missing, WorkspaceEdit(missing, TextDocumentEdit[tde])))
end
# Adding a CodeAction requires defining:
# * a Command (title and description);
# * a function (.when) called on the currently selected expression and parameters of the CodeAction call;
# * a function (.handler) called on three arguments (current expression, server and the jr connection) to implement the command.
const LSActions = Dict(
"ExplicitPackageVarImport" => ServerAction(Command("Explicitly import used package variables.", "ExplicitPackageVarImport", missing),
(x, params) -> refof(x) isa StaticLint.Binding && refof(x).val isa SymbolServer.ModuleStore,
explicitly_import_used_variables),
"ExpandFunction" => ServerAction(Command("Expand function definition.", "ExpandFunction", missing),
(x, params) -> is_in_fexpr(x, is_single_line_func),
expand_inline_func),
"FixMissingRef" => ServerAction(Command("Fix missing reference", "FixMissingRef", missing),
(x, params) -> is_fixable_missing_ref(x, params.context),
applymissingreffix),
"ReexportModule" => ServerAction(Command("Re-export package variables.", "ReexportModule", missing),
(x, params) -> StaticLint.is_in_fexpr(x, x -> headof(x) === :using || headof(x) === :import) && (refof(x) isa StaticLint.Binding && (refof(x).type === StaticLint.CoreTypes.Module || (refof(x).val isa StaticLint.Binding && refof(x).val.type === StaticLint.CoreTypes.Module) || refof(x).val isa SymbolServer.ModuleStore) || refof(x) isa SymbolServer.ModuleStore),
reexport_package),
"DeleteUnusedFunctionArgumentName" => ServerAction(Command("Delete name of unused function argument.", "DeleteUnusedFunctionArgumentName", missing), (x, params) -> StaticLint.is_in_fexpr(x, x -> StaticLint.haserror(x) && StaticLint.errorof(x) == StaticLint.UnusedFunctionArgument), remove_farg_name)
)