diff --git a/src/Atom.jl b/src/Atom.jl
index 6cf4c10b..cca3c671 100644
--- a/src/Atom.jl
+++ b/src/Atom.jl
@@ -1,5 +1,6 @@
__precompile__()
+@doc read(joinpath(dirname(@__DIR__), "README.md"), String)
module Atom
using Juno, Lazy, JSON, MacroTools, Media, Base.StackTraces
@@ -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")
diff --git a/src/completions.jl b/src/completions.jl
index 80dbb01d..6682ec21 100644
--- a/src/completions.jl
+++ b/src/completions.jl
@@ -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)
diff --git a/src/docs.jl b/src/docs.jl
index d4909835..473a42f3 100644
--- a/src/docs.jl
+++ b/src/docs.jl
@@ -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)
diff --git a/src/refactor.jl b/src/refactor.jl
new file mode 100644
index 00000000..07dccdd0
--- /dev/null
+++ b/src/refactor.jl
@@ -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
+
+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:
+ Context:
$(strip(context))
$(errmsg(err))