diff --git a/Project.toml b/Project.toml
index 99090cfa..66ac187d 100644
--- a/Project.toml
+++ b/Project.toml
@@ -28,6 +28,7 @@ Profile = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
+SourceWalk = "2987fe4f-acca-4bd1-87ee-98b3d562f1a1"
Traceur = "37b6cedf-1f77-55f8-9503-c64b63398394"
TreeViews = "a2a6695c-b41b-5b7d-aed9-dbfdeacea5d7"
WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29"
@@ -47,7 +48,7 @@ JuliaInterpreter = "^0.7.10"
Juno = "^0.7, ^0.8"
LNR = "^0.2.0"
Lazy = "^0.13.2, ^0.14, ^0.15"
-MacroTools = "^0.5"
+MacroTools = "^0.5.1"
Media = "^0.5"
OrderedCollections = "^1.1"
Requires = "^0.5, 1.0"
diff --git a/src/Atom.jl b/src/Atom.jl
index 7b4f15cb..5ef85c22 100644
--- a/src/Atom.jl
+++ b/src/Atom.jl
@@ -57,6 +57,7 @@ include("completions.jl")
include("goto.jl")
include("datatip.jl")
include("formatter.jl")
+include("refactor.jl")
include("frontend.jl")
include("debugger/debugger.jl")
include("profiler/profiler.jl")
diff --git a/src/debugger/stepper.jl b/src/debugger/stepper.jl
index 3c596eb1..c3e423e7 100644
--- a/src/debugger/stepper.jl
+++ b/src/debugger/stepper.jl
@@ -3,7 +3,6 @@ using JuliaInterpreter: pc_expr, extract_args, debug_command, root, caller,
import JuliaInterpreter
import ..Atom: fullpath, handle, @msg, Inline, display_error, hideprompt, getmodule
import Juno: Row
-using MacroTools
mutable struct DebuggerState
frame::Union{Nothing, Frame}
diff --git a/src/refactor.jl b/src/refactor.jl
new file mode 100644
index 00000000..692c0ddc
--- /dev/null
+++ b/src/refactor.jl
@@ -0,0 +1,247 @@
+using SourceWalk: textwalk, sourcewalk
+
+handle("renamerefactor") do data
+ @destruct [
+ oldWord,
+ fullWord,
+ newWord,
+ # local context
+ column || 1,
+ row || 1,
+ startRow || 0,
+ context || "",
+ # module context
+ mod || "Main",
+ ] = data
+ renamerefactor(oldWord, fullWord, newWord, column, row, startRow, context, mod)
+end
+
+# NOTE: invalid identifiers will be caught by frontend
+function renamerefactor(
+ oldword, fullword, newword,
+ column = 1, row = 1, startrow = 0, context = "",
+ mod = "Main",
+)
+ # catch keyword renaming
+ iskeyword(oldword) && return Dict(:warning => "Keywords can't be renamed: `$oldword`")
+
+ mod = getmodule(mod)
+ hstr = first(split(fullword, '.'))
+ head = getfield′(mod, hstr)
+
+ # catch field renaming
+ hstr ≠ oldword && !isa(head, Module) && return Dict(
+ :warning => "Rename refactoring on a field isn't available: `$hstr.$oldword`"
+ )
+
+ expr = CSTParser.parse(context)
+
+ bind = let
+ items = toplevelitems(context, expr)
+ ind = findfirst(item -> item isa ToplevelBinding, items)
+ ind === nothing ? nothing : items[ind].bind
+ end
+
+ # local rename refactor if `old` isn't a toplevel binding
+ if islocalrefactor(bind, oldword)
+ try
+ refactored = localrenamerefactor(oldword, newword, column, row, startrow, context, expr)
+ return isempty(refactored) ?
+ # NOTE: global refactoring not on definition, e.g.: on a call site, will be caught here
+ Dict(:info => contextdescription(oldword, mod, context)) :
+ Dict(
+ :text => refactored,
+ :success => "_Local_ rename refactoring `$oldword` ⟹ `$newword` succeeded"
+ )
+ catch err
+ return Dict(:error => errdescription(oldword, newword, err))
+ end
+ end
+
+ # global rename refactor if the local rename refactor didn't happen
+ try
+ kind, desc = globalrenamerefactor(oldword, newword, mod, expr)
+
+ # make description
+ if kind === :success
+ val = getfield′(mod, fullword)
+ moddesc = if (head isa Module && head ≠ mod) ||
+ (applicable(parentmodule, val) && (head = parentmodule(val)) ≠ mod)
+ moduledescription(oldword, head)
+ else
+ ""
+ end
+
+ desc = join(("_Global_ rename refactoring `$mod.$oldword` ⟹ `$mod.$newword` succeeded.", moddesc, desc), "\n\n")
+ end
+
+ return Dict(kind => desc)
+ catch err
+ return Dict(:error => errdescription(oldword, newword, err))
+ end
+end
+
+islocalrefactor(bind, name) = bind === nothing || name ≠ bind.name
+
+# local refactor
+# --------------
+
+function localrenamerefactor(oldword, newword, column, row, startrow, context, expr)
+ bindings = localbindings(expr, context)
+ line = row - startrow
+ scope = currentscope(oldword, bindings, byteoffset(context, line, column))
+ scope === nothing && return ""
+
+ currentcontext = scope.bindstr
+ oldsym = Symbol(oldword)
+ newsym = Symbol(newword)
+ newcontext = textwalk(currentcontext) do sym
+ sym === oldsym ? newsym : sym
+ end
+
+ replace(context, currentcontext => newcontext)
+end
+localrenamerefactor(oldword, newword, column, row, startrow, context, expr::Nothing) = ""
+
+function currentscope(name, bindings, byteoffset)
+ for binding in bindings
+ isa(binding, LocalScope) || continue
+
+ # first looks for innermost scope
+ childscope = currentscope(name, binding.children, byteoffset)
+ childscope !== nothing && return childscope
+
+ if byteoffset in binding.span &&
+ any(bind -> bind isa LocalBinding && name == bind.name, binding.children)
+ return binding
+ end
+ end
+
+ return nothing
+end
+
+# global refactor
+# ---------------
+
+function globalrenamerefactor(oldword, newword, mod, expr)
+ entrypath, _ = if mod == Main
+ MAIN_MODULE_LOCATION[]
+ else
+ moduledefinition(mod)
+ end
+
+ files = modulefiles(string(mod), entrypath)
+
+ # catch refactorings on an unsaved / non-existing file
+ isempty(files) && return :warning, unsaveddescription()
+
+ # catch refactorings on files without write permission
+ nonwritables = nonwritablefiles(files)
+ if !isempty(nonwritables)
+ return :warning, nonwritablesdescription(mod, nonwritables)
+ end
+
+ with_logger(JunoProgressLogger()) do
+ _globalrenamerefactor(oldword, newword, mod, expr, files)
+ end
+end
+
+function _globalrenamerefactor(oldword, newword, mod, expr, files)
+ ismacro = CSTParser.defines_macro(expr)
+ oldsym = ismacro ? Symbol("@" * oldword) : Symbol(oldword)
+ newsym = ismacro ? Symbol("@" * newword) : Symbol(newword)
+
+ total = length(files)
+ # TODO: enable line location information (the upstream needs to be enhanced)
+ modifiedfiles = Set{String}()
+
+ id = "global_rename_refactor_progress"
+ @info "Start global rename refactoring" progress=0 _id=id
+
+ for (i, file) ∈ enumerate(files)
+ @logmsg -1 "Refactoring: $file ($i / $total)" progress=i/total _id=id
+
+ sourcewalk(file) do ex
+ if ex === oldsym
+ push!(modifiedfiles, fullpath(file))
+ newsym
+ # handle dot accessor
+ elseif @capture(ex, m_.$oldsym) && getfield′(mod, Symbol(m)) isa Module
+ push!(modifiedfiles, fullpath(file))
+ Expr(:., m, newsym)
+ # macro case
+ elseif ismacro && @capture(ex, macro $(Symbol(oldword))(args__) body_ end)
+ push!(modifiedfiles, fullpath(file))
+ Expr(:macro, :($(Symbol(newword))($(args...))), :($body))
+ else
+ ex
+ end
+ end
+ end
+
+ @info "Finish global rename refactoring" progress=1 _id=id
+
+ return if !isempty(modifiedfiles)
+ :success, filesdescription(mod, modifiedfiles)
+ else
+ :warning, "No rename refactoring occured on `$oldword` in `$mod` module."
+ end
+end
+
+# descriptions
+# ------------
+
+function contextdescription(old, mod, context)
+ gotouri = urigoto(mod, old)
+ """
+ `$old` isn't found in local bindings in the current context:
+ Context:
$(strip(context))
$(errmsg(err))