From 2f2e9c892ab9a021eb7085365882a7f4facbf01d Mon Sep 17 00:00:00 2001
From: aviatesk
Date: Tue, 29 Oct 2019 23:37:45 +0900
Subject: [PATCH] implement rename refactor
---
src/Atom.jl | 1 +
src/refactor.jl | 248 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 249 insertions(+)
create mode 100644 src/refactor.jl
diff --git a/src/Atom.jl b/src/Atom.jl
index a7a4e78a..6d671bf3 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/refactor.jl b/src/refactor.jl
new file mode 100644
index 00000000..6c675290
--- /dev/null
+++ b/src/refactor.jl
@@ -0,0 +1,248 @@
+handle("renamerefactor") do data
+ @destruct [
+ old,
+ full,
+ new,
+ # local context
+ column || 1,
+ row || 1,
+ startRow || 0,
+ context || "",
+ # module context
+ mod || "Main",
+ ] = data
+ renamerefactor(old, full, new, column, row, startRow, context, mod)
+end
+
+# NOTE: invalid identifiers will be caught by frontend
+function renamerefactor(
+ old, full, new,
+ column = 1, row = 1, startrow = 0, context = "",
+ mod = "Main",
+)
+ # catch keyword renaming
+ iskeyword(old) && return Dict(:warning => "Keywords can't be renamed: `$old`")
+
+ mod = getmodule(mod)
+ hstr = first(split(full, '.'))
+ head = getfield′(mod, hstr)
+
+ # catch field renaming
+ hstr ≠ old && !isa(head, Module) && return Dict(
+ :warning => "Rename refactoring on a field isn't available: `$hstr.$old`"
+ )
+
+ expr = CSTParser.parse(context)
+ bind = let
+ if expr !== nothing
+ items = toplevelitems(expr, context)
+ ind = findfirst(item -> item isa ToplevelBinding, items)
+ ind === nothing ? nothing : items[ind].bind
+ else
+ nothing
+ end
+ end
+
+ # local rename refactor if `old` isn't a toplevel binding
+ if islocalrefactor(bind, old)
+ try
+ refactored = localrenamerefactor(old, new, 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(old, mod, context)) :
+ Dict(
+ :text => refactored,
+ :success => "_Local_ rename refactoring `$old` ⟹ `$new` succeeded"
+ )
+ catch err
+ return Dict(:error => errdescription(old, new, err))
+ end
+ end
+
+ # global rename refactor if the local rename refactor didn't happen
+ try
+ kind, desc = globalrenamerefactor(old, new, mod, expr)
+
+ # make description
+ if kind === :success
+ val = getfield′(mod, full)
+ moddesc = if (head isa Module && head ≠ mod) ||
+ (applicable(parentmodule, val) && (head = parentmodule(val)) ≠ mod)
+ moduledescription(old, head)
+ else
+ ""
+ end
+
+ desc = join(("_Global_ rename refactoring `$mod.$old` ⟹ `$mod.$new` succeeded.", moddesc, desc), "\n\n")
+ end
+
+ return Dict(kind => desc)
+ catch err
+ return Dict(:error => errdescription(old, new, err))
+ end
+end
+
+islocalrefactor(bind, name) = bind === nothing || name ≠ bind.name
+
+# local refactor
+# --------------
+
+function localrenamerefactor(old, new, column, row, startrow, context, expr)
+ bindings = localbindings(expr, context)
+ line = row - startrow
+ scope = currentscope(old, bindings, byteoffset(context, line, column))
+ scope === nothing && return ""
+
+ currentcontext = scope.bindstr
+ oldsym = Symbol(old)
+ newsym = Symbol(new)
+ newcontext = MacroTools.textwalk(currentcontext) do sym
+ sym === oldsym ? newsym : sym
+ end
+
+ replace(context, currentcontext => newcontext)
+end
+localrenamerefactor(old, new, 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(old, new, mod, expr)
+ entrypath, _ = if mod == Main
+ MAIN_MODULE_LOCATION[]
+ else
+ moduledefinition(mod)
+ end
+
+ files = modulefiles(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(old, new, mod, expr, files)
+ end
+end
+
+function _globalrenamerefactor(old, new, mod, expr, files)
+ ismacro = CSTParser.defines_macro(expr)
+ oldsym = ismacro ? Symbol("@" * old) : Symbol(old)
+ newsym = ismacro ? Symbol("@" * new) : Symbol(new)
+
+ 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
+
+ MacroTools.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(old))(args__) body_ end)
+ push!(modifiedfiles, fullpath(file))
+ Expr(:macro, :($(Symbol(new))($(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 `$old` 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))
+
+ If you want a global rename refactoring on `$mod.$old`, you need to run this command
+ from its definition.
+ """
+end
+
+function moduledescription(old, parentmod)
+ gotouri = urigoto(parentmod, old)
+ """
+ **NOTE**: `$old` is defined in `$parentmod` -- you may need the same rename refactorings
+ in that module as well.
+ """
+end
+
+function unsaveddescription()
+ """
+ Global rename refactor failed, since the given file isn't saved on the disk yet.
+ Please run this command again after you save the file.
+ """
+end
+
+function nonwritablesdescription(mod, files)
+ filelist = join(("
[$file]($(uriopen(file)))
" for file in files), '\n')
+ """
+ Global rename refactor failed, since there are non-writable files detected in
+ `$mod` module. Please make sure the files have an write access.
+
+
+ Non writable files (all in `$mod` module):
+