Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rename refactor #202

Merged
merged 19 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Atom.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__precompile__()

@doc read(joinpath(dirname(@__DIR__), "README.md"), String)
module Atom

using Juno, Lazy, JSON, MacroTools, Media, Base.StackTraces
Expand Down Expand Up @@ -51,6 +52,7 @@ include("outline.jl")
include("completions.jl")
include("goto.jl")
include("datatip.jl")
include("refactor.jl")
include("misc.jl")
include("formatter.jl")
include("frontend.jl")
Expand Down
15 changes: 5 additions & 10 deletions src/completions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,14 @@ completionurl(c::REPLCompletions.ModuleCompletion) = begin
mod, name = c.parent, c.mod
val = getfield′(mod, name)
if val isa Module # module info
parentmodule(val) == val || val ∈ (Main, Base, Core) ?
"atom://julia-client/?moduleinfo=true&mod=$(name)" :
"atom://julia-client/?moduleinfo=true&mod=$(mod).$(name)"
urimoduleinfo(parentmodule(val) == val || val ∈ (Base, Core) ? name : "$mod.$name")
else
"atom://julia-client/?docs=true&mod=$(mod)&word=$(name)"
uridocs(mod, name)
end
end
completionurl(c::REPLCompletions.MethodCompletion) =
"atom://julia-client/?docs=true&mod=$(c.method.module)&word=$(c.method.name)"
completionurl(c::REPLCompletions.PackageCompletion) =
"atom://julia-client/?moduleinfo=true&mod=$(c.package)"
completionurl(c::REPLCompletions.KeywordCompletion) =
"atom://julia-client/?docs=true&mod=Main&word=$(c.keyword)"
completionurl(c::REPLCompletions.MethodCompletion) = uridocs(c.method.module, c.method.name)
completionurl(c::REPLCompletions.PackageCompletion) = urimoduleinfo(c.package)
completionurl(c::REPLCompletions.KeywordCompletion) = uridocs("Main", c.keyword)

completionmodule(mod, c) = shortstr(mod)
completionmodule(mod, c::REPLCompletions.ModuleCompletion) = shortstr(c.parent)
Expand Down
2 changes: 1 addition & 1 deletion src/docs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function renderitem(x)

mod = getmodule(x.mod)
name = Symbol(x.name)
r[:typ], r[:icon], r[:nativetype] = if (name !== :ans || mod === Base) && name ∈ keys(Docs.keywords)
r[:typ], r[:icon], r[:nativetype] = if (name !== :ans || mod === Base) && iskeyword(name)
"keyword", "k", x.typ
else
val = getfield′(mod, name)
Expand Down
230 changes: 230 additions & 0 deletions src/refactor.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
handle("renamerefactor") do data
@destruct [
old,
full,
new,
path,
# local context
column || 1,
row || 1,
startRow || 0,
context || "",
# module context
mod || "Main",
] = data
renamerefactor(old, full, new, path, column, row, startRow, context, mod)
end

function renamerefactor(
old, full, new, path,
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)
head = first(split(full, '.'))
headval = getfield′(mod, head)

# catch field renaming
head ≠ old && !isa(headval, Module) && return Dict(
:warning => "Rename refactoring on a field isn't available: `$obj.$old`"
)

expr = CSTParser.parse(context)
items = toplevelitems(expr, context)
ind = findfirst(item -> item isa ToplevelBinding, items)
bind = ind === nothing ? nothing : items[ind].bind

# local rename refactor if `old` isn't a toplevel binding
if islocalrefactor(bind, old)
try
refactored = localrefactor(old, new, path, 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 = globalrefactor(old, new, mod, expr)

# make description
if kind === :success
val = getfield′(mod, full)
moddesc = if (headval isa Module && headval ≠ mod) ||
(applicable(parentmodule, val) && (headval = parentmodule(val)) ≠ mod)
moduledescription(old, headval)
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the logic here would look messy, so please feel free to ask me if you have anything unsure -- I probably miss some edge cases.


islocalrefactor(bind, name) = bind === nothing || name ≠ bind.name

# local refactor
# --------------

function localrefactor(old, new, path, column, row, startrow, context, expr)
bindings = local_bindings(expr, context)
line = row - startrow
scope = current_scope(old, bindings, byteoffset(context, line, column))
scope === nothing && return ""

current_context = scope.bindstr
oldsym = Symbol(old)
newsym = Symbol(new)
new_context = MacroTools.textwalk(current_context) do sym
sym === oldsym ? newsym : sym
end

replace(context, current_context => new_context)
end

function current_scope(name, bindings, byteoffset)
for binding in bindings
isa(binding, LocalScope) || continue

scope = binding
if byteoffset in scope.span &&
any(bind -> bind isa LocalBinding && name == bind.name, scope.children)
return scope
else
let scope = current_scope(name, scope.children, byteoffset)
scope !== nothing && return scope
end
end
end

return nothing
end

# global refactor
# ---------------

function globalrefactor(old, new, mod, expr)
entrypath, line = if mod == Main
MAIN_MODULE_LOCATION[]
else
moduledefinition(mod)
end
files = modulefiles(entrypath)

nonwritablefiles = filter(f -> Int(Base.uperm(f)) ≠ 6, files)
if !isempty(nonwritablefiles)
return :warning, nonwritabledescription(mod, nonwritablefiles)
end

with_logger(JunoProgressLogger()) do
refactorfiles(old, new, mod, files, expr)
end
end

function refactorfiles(old, new, mod, files, expr)
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)
refactoredfiles = Set{String}()

id = "global_rename_refactor_progress"
@info "Start global rename refactoring" progress=0 _id=id

for (i, file) ∈ enumerate(files)
@info "Refactoring: $file ($i / $total)" progress=i/total _id=id

MacroTools.sourcewalk(file) do ex
if ex === oldsym
push!(refactoredfiles, fullpath(file))
newsym
# handle dot (module) accessor
elseif @capture(ex, m_.$oldsym) && getfield′(mod, Symbol(m)) isa Module
push!(refactoredfiles, fullpath(file))
Expr(:., m, newsym)
# macro case
elseif ismacro && @capture(ex, macro $(Symbol(old))(args__) body_ end)
push!(refactoredfiles, 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(refactoredfiles)
:success, filedescription(mod, refactoredfiles)
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:
<details><summary>Context:</summary><pre><code>$(strip(context))</code></p></details>

If you want a global rename refactoring on `$mod.$old`, you need to run this command
from its definition. <button>[Go to `$mod.$old`]($gotouri)</button>
"""
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. <button>[Go to `$parentmod.$old`]($gotouri)</button>
"""
end

function nonwritabledescription(mod, files)
filelist = join(("<li>[$file]($(uriopen(file)))</li>" for file in files), '\n')
"""
Global rename refactor failed, since there are non-writable files detected in
`$mod` module.

<details><summary>
Non writable files (all in `$mod` module):
</summary><ul>$(filelist)</ul></details>
"""
end

function filedescription(mod, files)
filelist = join(("<li>[$file]($(uriopen(file)))</li>" for file in files), '\n')
"""
<details><summary>
Refactored files (all in `$mod` module):
</summary><ul>$(filelist)</ul></details>
"""
end

function errdescription(old, new, err)
"""
Rename refactoring `$old` ⟹ `$new` failed.

<details><summary>Error:</summary><pre><code>$(errmsg(err))</code></p></details>
"""
end
Loading