Skip to content

Commit 458d0f8

Browse files
committed
implement rename refactor
1 parent 34b458c commit 458d0f8

2 files changed

Lines changed: 249 additions & 0 deletions

File tree

src/Atom.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ include("completions.jl")
5757
include("goto.jl")
5858
include("datatip.jl")
5959
include("formatter.jl")
60+
include("refactor.jl")
6061
include("frontend.jl")
6162
include("debugger/debugger.jl")
6263
include("profiler/profiler.jl")

src/refactor.jl

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
handle("renamerefactor") do data
2+
@destruct [
3+
old,
4+
full,
5+
new,
6+
# local context
7+
column || 1,
8+
row || 1,
9+
startRow || 0,
10+
context || "",
11+
# module context
12+
mod || "Main",
13+
] = data
14+
renamerefactor(old, full, new, column, row, startRow, context, mod)
15+
end
16+
17+
# NOTE: invalid identifiers will be caught by frontend
18+
function renamerefactor(
19+
old, full, new,
20+
column = 1, row = 1, startrow = 0, context = "",
21+
mod = "Main",
22+
)
23+
# catch keyword renaming
24+
iskeyword(old) && return Dict(:warning => "Keywords can't be renamed: `$old`")
25+
26+
mod = getmodule(mod)
27+
hstr = first(split(full, '.'))
28+
head = getfield′(mod, hstr)
29+
30+
# catch field renaming
31+
hstr old && !isa(head, Module) && return Dict(
32+
:warning => "Rename refactoring on a field isn't available: `$hstr.$old`"
33+
)
34+
35+
expr = CSTParser.parse(context)
36+
bind = let
37+
if expr !== nothing
38+
items = toplevelitems(expr, context)
39+
ind = findfirst(item -> item isa ToplevelBinding, items)
40+
ind === nothing ? nothing : items[ind].bind
41+
else
42+
nothing
43+
end
44+
end
45+
46+
# local rename refactor if `old` isn't a toplevel binding
47+
if islocalrefactor(bind, old)
48+
try
49+
refactored = localrenamerefactor(old, new, column, row, startrow, context, expr)
50+
return isempty(refactored) ?
51+
# NOTE: global refactoring not on definition, e.g.: on a call site, will be caught here
52+
Dict(:info => contextdescription(old, mod, context)) :
53+
Dict(
54+
:text => refactored,
55+
:success => "_Local_ rename refactoring `$old` ⟹ `$new` succeeded"
56+
)
57+
catch err
58+
return Dict(:error => errdescription(old, new, err))
59+
end
60+
end
61+
62+
# global rename refactor if the local rename refactor didn't happen
63+
try
64+
kind, desc = globalrenamerefactor(old, new, mod, expr)
65+
66+
# make description
67+
if kind === :success
68+
val = getfield′(mod, full)
69+
moddesc = if (head isa Module && head mod) ||
70+
(applicable(parentmodule, val) && (head = parentmodule(val)) mod)
71+
moduledescription(old, head)
72+
else
73+
""
74+
end
75+
76+
desc = join(("_Global_ rename refactoring `$mod.$old` ⟹ `$mod.$new` succeeded.", moddesc, desc), "\n\n")
77+
end
78+
79+
return Dict(kind => desc)
80+
catch err
81+
return Dict(:error => errdescription(old, new, err))
82+
end
83+
end
84+
85+
islocalrefactor(bind, name) = bind === nothing || name bind.name
86+
87+
# local refactor
88+
# --------------
89+
90+
function localrenamerefactor(old, new, column, row, startrow, context, expr)
91+
bindings = localbindings(expr, context)
92+
line = row - startrow
93+
scope = currentscope(old, bindings, byteoffset(context, line, column))
94+
scope === nothing && return ""
95+
96+
currentcontext = scope.bindstr
97+
oldsym = Symbol(old)
98+
newsym = Symbol(new)
99+
newcontext = MacroTools.textwalk(currentcontext) do sym
100+
sym === oldsym ? newsym : sym
101+
end
102+
103+
replace(context, currentcontext => newcontext)
104+
end
105+
localrenamerefactor(old, new, column, row, startrow, context, expr::Nothing) = ""
106+
107+
function currentscope(name, bindings, byteoffset)
108+
for binding in bindings
109+
isa(binding, LocalScope) || continue
110+
111+
# first looks for innermost scope
112+
childscope = currentscope(name, binding.children, byteoffset)
113+
childscope !== nothing && return childscope
114+
115+
if byteoffset in binding.span &&
116+
any(bind -> bind isa LocalBinding && name == bind.name, binding.children)
117+
return binding
118+
end
119+
end
120+
121+
return nothing
122+
end
123+
124+
# global refactor
125+
# ---------------
126+
127+
function globalrenamerefactor(old, new, mod, expr)
128+
entrypath, _ = if mod == Main
129+
MAIN_MODULE_LOCATION[]
130+
else
131+
moduledefinition(mod)
132+
end
133+
134+
files = modulefiles(entrypath)
135+
136+
# catch refactorings on an unsaved / non-existing file
137+
isempty(files) && return :warning, unsaveddescription()
138+
139+
# catch refactorings on files without write permission
140+
nonwritables = nonwritablefiles(files)
141+
if !isempty(nonwritables)
142+
return :warning, nonwritablesdescription(mod, nonwritables)
143+
end
144+
145+
with_logger(JunoProgressLogger()) do
146+
_globalrenamerefactor(old, new, mod, expr, files)
147+
end
148+
end
149+
150+
function _globalrenamerefactor(old, new, mod, expr, files)
151+
ismacro = CSTParser.defines_macro(expr)
152+
oldsym = ismacro ? Symbol("@" * old) : Symbol(old)
153+
newsym = ismacro ? Symbol("@" * new) : Symbol(new)
154+
155+
total = length(files)
156+
# TODO: enable line location information (the upstream needs to be enhanced)
157+
modifiedfiles = Set{String}()
158+
159+
id = "global_rename_refactor_progress"
160+
@info "Start global rename refactoring" progress=0 _id=id
161+
162+
for (i, file) enumerate(files)
163+
@logmsg -1 "Refactoring: $file ($i / $total)" progress=i/total _id=id
164+
165+
MacroTools.sourcewalk(file) do ex
166+
if ex === oldsym
167+
push!(modifiedfiles, fullpath(file))
168+
newsym
169+
# handle dot accessor
170+
elseif @capture(ex, m_.$oldsym) && getfield′(mod, Symbol(m)) isa Module
171+
push!(modifiedfiles, fullpath(file))
172+
Expr(:., m, newsym)
173+
# macro case
174+
elseif ismacro && @capture(ex, macro $(Symbol(old))(args__) body_ end)
175+
push!(modifiedfiles, fullpath(file))
176+
Expr(:macro, :($(Symbol(new))($(args...))), :($body))
177+
else
178+
ex
179+
end
180+
end
181+
end
182+
183+
@info "Finish global rename refactoring" progress=1 _id=id
184+
185+
return if !isempty(modifiedfiles)
186+
:success, filesdescription(mod, modifiedfiles)
187+
else
188+
:warning, "No rename refactoring occured on `$old` in `$mod` module."
189+
end
190+
end
191+
192+
# descriptions
193+
# ------------
194+
195+
function contextdescription(old, mod, context)
196+
gotouri = urigoto(mod, old)
197+
"""
198+
`$old` isn't found in local bindings in the current context:
199+
<details><summary>Context:</summary><pre><code>$(strip(context))</code></p></details>
200+
201+
If you want a global rename refactoring on `$mod.$old`, you need to run this command
202+
from its definition. <button>[Go to `$mod.$old`]($gotouri)</button>
203+
"""
204+
end
205+
206+
function moduledescription(old, parentmod)
207+
gotouri = urigoto(parentmod, old)
208+
"""
209+
**NOTE**: `$old` is defined in `$parentmod` -- you may need the same rename refactorings
210+
in that module as well. <button>[Go to `$parentmod.$old`]($gotouri)</button>
211+
"""
212+
end
213+
214+
function unsaveddescription()
215+
"""
216+
Global rename refactor failed, since the given file isn't saved on the disk yet.
217+
Please run this command again after you save the file.
218+
"""
219+
end
220+
221+
function nonwritablesdescription(mod, files)
222+
filelist = join(("<li>[$file]($(uriopen(file)))</li>" for file in files), '\n')
223+
"""
224+
Global rename refactor failed, since there are non-writable files detected in
225+
`$mod` module. Please make sure the files have an write access.
226+
227+
<details><summary>
228+
Non writable files (all in `$mod` module):
229+
</summary><ul>$(filelist)</ul></details>
230+
"""
231+
end
232+
233+
function filesdescription(mod, files)
234+
filelist = join(("<li>[$file]($(uriopen(file)))</li>" for file in files), '\n')
235+
"""
236+
<details><summary>
237+
Refactored files (all in `$mod` module):
238+
</summary><ul>$(filelist)</ul></details>
239+
"""
240+
end
241+
242+
function errdescription(old, new, err)
243+
"""
244+
Rename refactoring `$old` ⟹ `$new` failed.
245+
246+
<details><summary>Error:</summary><pre><code>$(errmsg(err))</code></p></details>
247+
"""
248+
end

0 commit comments

Comments
 (0)