Skip to content

Commit 5098a46

Browse files
InteractiveUtils: refactor define_editor implementation
1 parent d22c88f commit 5098a46

File tree

3 files changed

+119
-153
lines changed

3 files changed

+119
-153
lines changed

stdlib/InteractiveUtils/src/InteractiveUtils.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ module InteractiveUtils
44

55
export apropos, edit, less, code_warntype, code_llvm, code_native, methodswith, varinfo,
66
versioninfo, subtypes, @which, @edit, @less, @functionloc, @code_warntype,
7-
@code_typed, @code_lowered, @code_llvm, @code_native, clipboard,
8-
define_editor
7+
@code_typed, @code_lowered, @code_llvm, @code_native, clipboard
98

109
import Base.Docs.apropos
1110

stdlib/InteractiveUtils/src/editless.jl

Lines changed: 110 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -2,85 +2,60 @@
22

33
# editing and paging files
44

5-
import Base.shell_split
6-
using Base: find_source_file
7-
8-
struct EditorEntry{P,Fn}
9-
pattern::P
10-
fn::Fn
11-
wait::Bool
12-
takesline::Bool
13-
end
14-
const editors = Vector{EditorEntry}(undef, 0)
15-
16-
struct EditorCommand{C,E}
17-
parsed_command::C
18-
entry::E
19-
end
20-
function parse_command(entry::EditorEntry, command, path, line)
21-
if entry.takesline
22-
cmd = entry.fn(command, path, line)
23-
if !isnothing(cmd)
24-
return EditorCommand(cmd, entry), true
25-
end
26-
end
5+
using Base: shell_split, shell_escape, find_source_file
276

28-
cmd = entry.fn(command, path)
29-
if !isnothing(cmd)
30-
return EditorCommand(cmd, entry), false
31-
end
32-
33-
nothing, false
34-
end
7+
"""
8+
EDITOR_CALLBACKS :: Vector{Function}
359
36-
function Base.run(x::EditorCommand{Cmd})
37-
if !x.entry.wait
38-
run(pipeline(x.parsed_command, stderr=stderr), wait=false)
39-
else
40-
run(x.parsed_command)
41-
end
42-
end
43-
Base.run(x::EditorCommand{Function}) = x.parsed_command()
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[]
4416

4517
"""
46-
define_editor(fn, pattern;wait=false)
18+
define_editor(fn, pattern; wait=false)
4719
48-
Define a new editor matching `pattern` that can be used to open a file
49-
(possibly at a given line number) using `fn`.
20+
Define a new editor matching `pattern` that can be used to open a file (possibly
21+
at a given line number) using `fn`.
5022
5123
The `fn` argument is a function that determines how to open a file with the
52-
given editor. It should take 2 or 3 arguments, as follows:
24+
given editor. It should take three arguments, as follows:
5325
54-
* `command` - an array of strings representing the editor command. It can be
55-
safely interpolated into a command created using backtick notation.
56-
* `path` - the path to the source file to open
57-
* `line` - the optional line number to open to; if specified the returned
58-
command must open the file at the given line.
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
5929
60-
`fn` must return either an appropriate `Cmd` object to open a file, a
61-
zero-argument function that will open the file directly, or `nothing`.
62-
Returning a `Cmd` is preferred over returning a function. Use `nothing` to
63-
indicate that this editor is not appropriate for the current environment and
64-
another editor should be attempted.
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`.
6537
6638
The `pattern` argument is a string, regular expression, or an array of strings
67-
and regular expressions. For the `fn` to be called one of the patterns must
68-
match the value of `EDITOR`, `VISUAL` or `JULIA_EDITOR`. For strings, only
69-
whole words can match (i.e. "vi" doesn't match "vim -g" but will match
70-
"/usr/bin/vi -m").
71-
72-
If multiple defined editors match, the one most recently defined will be
73-
used.
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.
7447
7548
By default julia does not wait for the editor to close, running it in the
7649
background. However, if the editor is terminal based, you will probably want to
7750
set `wait=true` and julia will wait for the editor to close before resuming.
7851
79-
If no editor entry can be found, then a file is opened by running
80-
`\$command \$path`.
52+
If one of the editor environment variables is set, but no editor entry matches it,
53+
the default editor entry is invoked:
8154
82-
Note that many editors are already defined. All of the following commands
83-
should already work:
55+
(cmd, path, line) -> `\$cmd \$path`
56+
57+
Note that many editors are already defined. All of the following commands should
58+
already work:
8459
8560
- emacs
8661
- vim
@@ -96,45 +71,59 @@ should already work:
9671
- open
9772
9873
# Example:
74+
9975
The following defines the usage of terminal-based `emacs`:
10076
101-
define_editor(r"\bemacs\b.*(-nw|--no-window-system)",
102-
wait=true) do cmd, path, line
77+
define_editor(
78+
r"\\bemacs\\b.*\\s(-nw|--no-window-system)\\b", wait=true) do cmd, path, line
10379
`\$cmd +\$line \$path`
10480
end
81+
82+
!!! compat "Julia 1.4"
83+
`define_editor` was introduced in Julia 1.4.
10584
"""
106-
function define_editor(fn, pattern; wait=false, priority=0)
107-
nargs = map(x -> x.nargs - 1, methods(fn).ms)
108-
has3args = 3 nargs
109-
has2args = 2 nargs
110-
entry = EditorEntry(pattern, fn, wait, has3args)
111-
pushfirst!(editors, entry)
112-
113-
if !(has3args || has2args)
114-
error("Editor function must take 2 or 3 arguments")
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
115101
end
102+
pushfirst!(EDITOR_CALLBACKS, callback)
116103
end
117104

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+
118109
function define_default_editors()
119-
define_editor(["vim","vi","nvim","mvim","nano"],
120-
wait=true) do cmd, path, line
121-
`$cmd +$line $path`
122-
end
123-
define_editor([r"\bemacs","gedit",r"\bgvim"]) do cmd, path, line
124-
`$cmd +$line $path`
110+
# fallback: just call the editor with the path as argument
111+
define_editor(r".*") do cmd, path, line
112+
`$cmd $path`
125113
end
126-
define_editor(r"\bemacs\b.*(-nw|--no-window-system)",
127-
wait=true) do cmd, path, line
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
128118
`$cmd +$line $path`
129119
end
130-
define_editor(r"\bemacsclient\b.*(-nw|-t|-tty)",
131-
wait=true) do cmd, path, line
120+
define_editor([r"\bemacs", "gedit", r"\bgvim"]) do cmd, path, line
132121
`$cmd +$line $path`
133122
end
134-
define_editor(["textmate","mate","kate"]) do cmd, path, line
123+
define_editor(["textmate", "mate", "kate"]) do cmd, path, line
135124
`$cmd $path -l $line`
136125
end
137-
define_editor([r"\bsubl",r"\batom"]) do cmd, path, line
126+
define_editor([r"\bsubl", r"\batom"]) do cmd, path, line
138127
`$cmd $path:$line`
139128
end
140129
define_editor("code") do cmd, path, line
@@ -147,20 +136,21 @@ function define_default_editors()
147136
define_editor(r"\bCODE\.EXE\b"i) do cmd, path, line
148137
`$cmd -g $path:$line`
149138
end
150-
define_editor("open") do cmd, path, line
151-
function()
152-
# don't emit this ccall on other platforms
153-
@static if Sys.iswindows()
154-
result = ccall((:ShellExecuteW, "shell32"), stdcall,
155-
Int, (Ptr{Cvoid}, Cwstring, Cwstring,
156-
Ptr{Cvoid}, Ptr{Cvoid}, Cint),
157-
C_NULL, "open", path, C_NULL, C_NULL, 10)
158-
systemerror(:edit, result 32)
159-
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)
160148
end
149+
return true
161150
end
151+
pushfirst!(EDITOR_CALLBACKS, callback)
162152
elseif Sys.isapple()
163-
define_editor("open") do cmd, path
153+
define_editor("open") do cmd, path, line
164154
`open -t $path`
165155
end
166156
end
@@ -169,69 +159,46 @@ end
169159
"""
170160
editor()
171161
172-
Determine the editor to use when running functions like `edit`. Return an `Array` compatible
173-
for use within backticks. You can change the editor by setting `JULIA_EDITOR`, `VISUAL` or
174-
`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.
175165
"""
176166
function editor()
177-
if Sys.iswindows() || Sys.isapple()
178-
default_editor = "open"
179-
elseif isfile("/etc/alternatives/editor")
180-
default_editor = realpath("/etc/alternatives/editor")
181-
else
182-
default_editor = "emacs"
183-
end
184167
# Note: the editor path can include spaces (if escaped) and flags.
185-
args = shell_split(get(ENV, "JULIA_EDITOR",
186-
get(ENV,"VISUAL", get(ENV,"EDITOR", default_editor))))
187-
isempty(args) && error("editor is empty")
188-
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])
189178
end
190179

191-
editormatches(pattern::String, command) =
192-
occursin(Regex("\\b"*pattern*"\\b"), command)
193-
editormatches(pattern::Regex, command) =
194-
occursin(pattern, command)
195-
editormatches(pattern::AbstractArray, command) =
196-
any(x -> editormatches(x, command), pattern)
197-
findeditors(command) =
198-
filter(e -> editormatches(e.pattern,join(command," ")), editors)
199-
200180
"""
201181
edit(path::AbstractString, line::Integer=0)
202182
203183
Edit a file or directory optionally providing a line number to edit the file at.
204184
Return to the `julia` prompt when you quit the editor. The editor can be changed
205185
by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR` as an environment variable.
206-
To ensure that the file can be opened at the given line, you may need to
207-
call `define_editor` first.
186+
187+
See also: (`define_editor`)[@ref]
208188
"""
209189
function edit(path::AbstractString, line::Integer=0)
210-
!isempty(editors) || define_default_editors()
211-
command = editor()
190+
isempty(EDITOR_CALLBACKS) && define_default_editors()
191+
path isa String || (path = convert(String, path))
212192
if endswith(path, ".jl")
213-
f = find_source_file(path)
214-
f !== nothing && (path = f)
215-
end
216-
217-
parsed = nothing
218-
line_supported = false
219-
for entry in findeditors(command)
220-
parsed, line_supported = parse_command(entry, command, path, line)
221-
isnothing(parsed) || break
222-
end
223-
if isnothing(parsed)
224-
parsed = `$command $path`
225-
line_supported = false
193+
p = find_source_file(path)
194+
p !== nothing && (path = p)
226195
end
227-
228-
if line != 0 && !line_supported
229-
@info("Unknown editor: no line number information passed.\n"*
230-
"The method is defined at line $line.")
196+
cmd = editor()
197+
for callback in EDITOR_CALLBACKS
198+
callback(cmd, path, line) && return
231199
end
232-
run(parsed)
233-
234-
nothing
200+
# shouldn't happen unless someone has removed fallback entry
201+
error("no editor found")
235202
end
236203

237204
"""

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)