Skip to content

Commit a6bf74a

Browse files
Merge pull request #34107 from JuliaLang/sk/define_editor
InteractiveUtils: make editors pluggable via `define_editor`
2 parents 57cb6e0 + 5098a46 commit a6bf74a

File tree

3 files changed

+188
-70
lines changed

3 files changed

+188
-70
lines changed

stdlib/InteractiveUtils/docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ InteractiveUtils.subtypes
99
InteractiveUtils.edit(::AbstractString, ::Integer)
1010
InteractiveUtils.edit(::Any)
1111
InteractiveUtils.@edit
12+
InteractiveUtils.define_editor
1213
InteractiveUtils.less(::AbstractString)
1314
InteractiveUtils.less(::Any)
1415
InteractiveUtils.@less

stdlib/InteractiveUtils/src/editless.jl

Lines changed: 179 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,179 @@
22

33
# editing and paging files
44

5-
import Base.shell_split
6-
using Base: find_source_file
5+
using Base: shell_split, shell_escape, find_source_file
6+
7+
"""
8+
EDITOR_CALLBACKS :: Vector{Function}
9+
10+
A vector of editor callback functions, which take as arguments `cmd`, `path` and
11+
`line` and which is then expected to either open an editor and return `true` to
12+
indicate that it has handled the request, or return `false` to decline the
13+
editing request.
14+
"""
15+
const EDITOR_CALLBACKS = Function[]
16+
17+
"""
18+
define_editor(fn, pattern; wait=false)
19+
20+
Define a new editor matching `pattern` that can be used to open a file (possibly
21+
at a given line number) using `fn`.
22+
23+
The `fn` argument is a function that determines how to open a file with the
24+
given editor. It should take three arguments, as follows:
25+
26+
* `cmd` - a base command object for the editor
27+
* `path` - the path to the source file to open
28+
* `line` - the line number to open the editor at
29+
30+
Editors which cannot open to a specific line with a command may ignore the
31+
`line` argument. The `fn` callback must return either an appropriate `Cmd`
32+
object to open a file or `nothing` to indicate that they cannot edit this file.
33+
Use `nothing` to indicate that this editor is not appropriate for the current
34+
environment and another editor should be attempted. It is possible to add more
35+
general editing hooks that need not spawn external commands by pushing a
36+
callback directly to the vector `EDITOR_CALLBACKS`.
37+
38+
The `pattern` argument is a string, regular expression, or an array of strings
39+
and regular expressions. For the `fn` to be called, one of the patterns must
40+
match the value of `EDITOR`, `VISUAL` or `JULIA_EDITOR`. For strings, the string
41+
must equal the [`basename`](@ref) of the first word of the editor command, with
42+
its extension, if any, removed. E.g. "vi" doesn't match "vim -g" but matches
43+
"/usr/bin/vi -m"; it also matches `vi.exe`. If `pattern` is a regex it is
44+
matched against all of the editor command as a shell-escaped string. An array
45+
pattern matches if any of its items match. If multiple editors match, the one
46+
added most recently is used.
47+
48+
By default julia does not wait for the editor to close, running it in the
49+
background. However, if the editor is terminal based, you will probably want to
50+
set `wait=true` and julia will wait for the editor to close before resuming.
51+
52+
If one of the editor environment variables is set, but no editor entry matches it,
53+
the default editor entry is invoked:
54+
55+
(cmd, path, line) -> `\$cmd \$path`
56+
57+
Note that many editors are already defined. All of the following commands should
58+
already work:
59+
60+
- emacs
61+
- vim
62+
- nvim
63+
- nano
64+
- textmate
65+
- mate
66+
- kate
67+
- subl
68+
- atom
69+
- notepad++
70+
- Visual Studio Code
71+
- open
72+
73+
# Example:
74+
75+
The following defines the usage of terminal-based `emacs`:
76+
77+
define_editor(
78+
r"\\bemacs\\b.*\\s(-nw|--no-window-system)\\b", wait=true) do cmd, path, line
79+
`\$cmd +\$line \$path`
80+
end
81+
82+
!!! compat "Julia 1.4"
83+
`define_editor` was introduced in Julia 1.4.
84+
"""
85+
function define_editor(fn::Function, pattern; wait::Bool=false)
86+
callback = function (cmd::Cmd, path::AbstractString, line::Integer)
87+
editor_matches(pattern, cmd) || return false
88+
editor = fn(cmd, path, line)
89+
if editor isa Cmd
90+
if wait
91+
run(editor) # blocks while editor runs
92+
else
93+
run(pipeline(editor, stderr=stderr), wait=false)
94+
end
95+
return true
96+
elseif editor isa Nothing
97+
return false
98+
end
99+
@warn "invalid editor value returned" pattern=pattern editor=editor
100+
return false
101+
end
102+
pushfirst!(EDITOR_CALLBACKS, callback)
103+
end
104+
105+
editor_matches(p::Regex, cmd::Cmd) = occursin(p, shell_escape(cmd))
106+
editor_matches(p::String, cmd::Cmd) = p == splitext(basename(first(cmd)))[1]
107+
editor_matches(ps::AbstractArray, cmd::Cmd) = any(editor_matches(p, cmd) for p in ps)
108+
109+
function define_default_editors()
110+
# fallback: just call the editor with the path as argument
111+
define_editor(r".*") do cmd, path, line
112+
`$cmd $path`
113+
end
114+
define_editor([
115+
"vim", "vi", "nvim", "mvim", "nano",
116+
r"\bemacs\b.*\s(-nw|--no-window-system)\b",
117+
r"\bemacsclient\b.\s*-(-?nw|t|-?tty)\b"], wait=true) do cmd, path, line
118+
`$cmd +$line $path`
119+
end
120+
define_editor([r"\bemacs", "gedit", r"\bgvim"]) do cmd, path, line
121+
`$cmd +$line $path`
122+
end
123+
define_editor(["textmate", "mate", "kate"]) do cmd, path, line
124+
`$cmd $path -l $line`
125+
end
126+
define_editor([r"\bsubl", r"\batom"]) do cmd, path, line
127+
`$cmd $path:$line`
128+
end
129+
define_editor("code") do cmd, path, line
130+
`$cmd -g $path:$line`
131+
end
132+
define_editor(r"\bnotepad++") do cmd, path, line
133+
`$cmd $path -n$line`
134+
end
135+
if Sys.iswindows()
136+
define_editor(r"\bCODE\.EXE\b"i) do cmd, path, line
137+
`$cmd -g $path:$line`
138+
end
139+
callback = function (cmd::Cmd, path::AbstractString, line::Integer)
140+
cmd == `open` || return false
141+
# don't emit this ccall on other platforms
142+
@static if Sys.iswindows()
143+
result = ccall((:ShellExecuteW, "shell32"), stdcall,
144+
Int, (Ptr{Cvoid}, Cwstring, Cwstring,
145+
Ptr{Cvoid}, Ptr{Cvoid}, Cint),
146+
C_NULL, "open", path, C_NULL, C_NULL, 10)
147+
systemerror(:edit, result 32)
148+
end
149+
return true
150+
end
151+
pushfirst!(EDITOR_CALLBACKS, callback)
152+
elseif Sys.isapple()
153+
define_editor("open") do cmd, path, line
154+
`open -t $path`
155+
end
156+
end
157+
end
7158

8159
"""
9160
editor()
10161
11-
Determine the editor to use when running functions like `edit`. Return an `Array` compatible
12-
for use within backticks. You can change the editor by setting `JULIA_EDITOR`, `VISUAL` or
13-
`EDITOR` as an environment variable.
162+
Determine the editor to use when running functions like `edit`. Returns a `Cmd`
163+
object. Change editor by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR`
164+
environment variables.
14165
"""
15166
function editor()
16-
if Sys.iswindows() || Sys.isapple()
17-
default_editor = "open"
18-
elseif isfile("/etc/alternatives/editor")
19-
default_editor = realpath("/etc/alternatives/editor")
20-
else
21-
default_editor = "emacs"
22-
end
23167
# Note: the editor path can include spaces (if escaped) and flags.
24-
args = shell_split(get(ENV,"JULIA_EDITOR", get(ENV,"VISUAL", get(ENV,"EDITOR", default_editor))))
25-
isempty(args) && error("editor is empty")
26-
return args
168+
for var in ["JULIA_EDITOR", "VISUAL", "EDITOR"]
169+
str = get(ENV, var, nothing)
170+
str === nothing && continue
171+
isempty(str) && error("invalid editor \$$var: $(repr(str))")
172+
return Cmd(shell_split(str))
173+
end
174+
editor_file = "/etc/alternatives/editor"
175+
editor = (Sys.iswindows() || Sys.isapple()) ? "open" :
176+
isfile(editor_file) ? realpath(editor_file) : "emacs"
177+
return Cmd([editor])
27178
end
28179

29180
"""
@@ -32,56 +183,22 @@ end
32183
Edit a file or directory optionally providing a line number to edit the file at.
33184
Return to the `julia` prompt when you quit the editor. The editor can be changed
34185
by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR` as an environment variable.
186+
187+
See also: (`define_editor`)[@ref]
35188
"""
36189
function edit(path::AbstractString, line::Integer=0)
37-
command = editor()
38-
name = basename(first(command))
190+
isempty(EDITOR_CALLBACKS) && define_default_editors()
191+
path isa String || (path = convert(String, path))
39192
if endswith(path, ".jl")
40-
f = find_source_file(path)
41-
f !== nothing && (path = f)
193+
p = find_source_file(path)
194+
p !== nothing && (path = p)
42195
end
43-
background = true
44-
line_unsupported = false
45-
if startswith(name, "vim.") || name == "vi" || name == "vim" || name == "nvim" ||
46-
name == "mvim" || name == "nano" ||
47-
name == "emacs" && any(c -> c in ["-nw", "--no-window-system" ], command) ||
48-
name == "emacsclient" && any(c -> c in ["-nw", "-t", "-tty"], command)
49-
cmd = line != 0 ? `$command +$line $path` : `$command $path`
50-
background = false
51-
elseif startswith(name, "emacs") || name == "gedit" || startswith(name, "gvim")
52-
cmd = line != 0 ? `$command +$line $path` : `$command $path`
53-
elseif name == "textmate" || name == "mate" || name == "kate"
54-
cmd = line != 0 ? `$command $path -l $line` : `$command $path`
55-
elseif name == "rmate"
56-
cmd = line != 0 ? `$command $path -l $line -f` : `$command $path -f`
57-
elseif startswith(name, "subl") || startswith(name, "atom")
58-
cmd = line != 0 ? `$command $path:$line` : `$command $path`
59-
elseif name == "code" || (Sys.iswindows() && (uppercase(name) == "CODE.EXE" || uppercase(name) == "CODE.CMD"))
60-
cmd = line != 0 ? `$command -g $path:$line` : `$command -g $path`
61-
elseif startswith(name, "notepad++")
62-
cmd = line != 0 ? `$command $path -n$line` : `$command $path`
63-
elseif Sys.isapple() && name == "open"
64-
cmd = `open -t $path`
65-
line_unsupported = true
66-
else
67-
cmd = `$command $path`
68-
background = false
69-
line_unsupported = true
196+
cmd = editor()
197+
for callback in EDITOR_CALLBACKS
198+
callback(cmd, path, line) && return
70199
end
71-
72-
if Sys.iswindows() && name == "open"
73-
@static Sys.iswindows() && # don't emit this ccall on other platforms
74-
systemerror(:edit, ccall((:ShellExecuteW, "shell32"), stdcall, Int,
75-
(Ptr{Cvoid}, Cwstring, Cwstring, Ptr{Cvoid}, Ptr{Cvoid}, Cint),
76-
C_NULL, "open", path, C_NULL, C_NULL, 10) 32)
77-
elseif background
78-
run(pipeline(cmd, stderr=stderr), wait=false)
79-
else
80-
run(cmd)
81-
end
82-
line != 0 && line_unsupported && println("Unknown editor: no line number information passed.\nThe method is defined at line $line.")
83-
84-
nothing
200+
# shouldn't happen unless someone has removed fallback entry
201+
error("no editor found")
85202
end
86203

87204
"""
@@ -95,8 +212,8 @@ method to edit. For modules, open the main source file. The module needs to be l
95212
!!! compat "Julia 1.1"
96213
`edit` on modules requires at least Julia 1.1.
97214
98-
The editor can be changed by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR` as an environment
99-
variable.
215+
To ensure that the file can be opened at the given line, you may need to call
216+
`define_editor` first.
100217
"""
101218
edit(f) = edit(functionloc(f)...)
102219
edit(f, @nospecialize t) = edit(functionloc(f,t)...)

stdlib/InteractiveUtils/test/runtests.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ using InteractiveUtils: editor
346346
# Issue #13032
347347
withenv("JULIA_EDITOR" => nothing, "VISUAL" => nothing, "EDITOR" => nothing) do
348348
# Make sure editor doesn't error when no ENV editor is set.
349-
@test isa(editor(), Array)
349+
@test isa(editor(), Cmd)
350350

351351
# Invalid editor
352352
ENV["JULIA_EDITOR"] = ""
@@ -357,29 +357,29 @@ withenv("JULIA_EDITOR" => nothing, "VISUAL" => nothing, "EDITOR" => nothing) do
357357

358358
# Editor on the path.
359359
ENV["JULIA_EDITOR"] = "vim"
360-
@test editor() == ["vim"]
360+
@test editor() == `vim`
361361

362362
# Absolute path to editor.
363363
ENV["JULIA_EDITOR"] = "/usr/bin/vim"
364-
@test editor() == ["/usr/bin/vim"]
364+
@test editor() == `/usr/bin/vim`
365365

366366
# Editor on the path using arguments.
367367
ENV["JULIA_EDITOR"] = "subl -w"
368-
@test editor() == ["subl", "-w"]
368+
@test editor() == `subl -w`
369369

370370
# Absolute path to editor with spaces.
371371
ENV["JULIA_EDITOR"] = "/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl"
372-
@test editor() == ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"]
372+
@test editor() == `'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl'`
373373

374374
# Paths with spaces and arguments (#13032).
375375
ENV["JULIA_EDITOR"] = "/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl -w"
376-
@test editor() == ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "-w"]
376+
@test editor() == `'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -w`
377377

378378
ENV["JULIA_EDITOR"] = "'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -w"
379-
@test editor() == ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "-w"]
379+
@test editor() == `'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -w`
380380

381381
ENV["JULIA_EDITOR"] = "\"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl\" -w"
382-
@test editor() == ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "-w"]
382+
@test editor() == `'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -w`
383383
end
384384

385385
# clipboard functionality

0 commit comments

Comments
 (0)