Skip to content

Commit d22c88f

Browse files
haberdashPIStefanKarpinski
authored andcommitted
InteractiveUtils: make editors pluggable via define_editor
This function makes it possible to properly define how to open a file and pass a specific line number for a given editor using `@edit` and friends.
1 parent a0b67dc commit d22c88f

File tree

3 files changed

+196
-44
lines changed

3 files changed

+196
-44
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/InteractiveUtils.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ 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
7+
@code_typed, @code_lowered, @code_llvm, @code_native, clipboard,
8+
define_editor
89

910
import Base.Docs.apropos
1011

stdlib/InteractiveUtils/src/editless.jl

Lines changed: 193 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,167 @@
55
import Base.shell_split
66
using Base: find_source_file
77

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
27+
28+
cmd = entry.fn(command, path)
29+
if !isnothing(cmd)
30+
return EditorCommand(cmd, entry), false
31+
end
32+
33+
nothing, false
34+
end
35+
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()
44+
45+
"""
46+
define_editor(fn, pattern;wait=false)
47+
48+
Define a new editor matching `pattern` that can be used to open a file
49+
(possibly at a given line number) using `fn`.
50+
51+
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:
53+
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.
59+
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.
65+
66+
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.
74+
75+
By default julia does not wait for the editor to close, running it in the
76+
background. However, if the editor is terminal based, you will probably want to
77+
set `wait=true` and julia will wait for the editor to close before resuming.
78+
79+
If no editor entry can be found, then a file is opened by running
80+
`\$command \$path`.
81+
82+
Note that many editors are already defined. All of the following commands
83+
should already work:
84+
85+
- emacs
86+
- vim
87+
- nvim
88+
- nano
89+
- textmate
90+
- mate
91+
- kate
92+
- subl
93+
- atom
94+
- notepad++
95+
- Visual Studio Code
96+
- open
97+
98+
# Example:
99+
The following defines the usage of terminal-based `emacs`:
100+
101+
define_editor(r"\bemacs\b.*(-nw|--no-window-system)",
102+
wait=true) do cmd, path, line
103+
`\$cmd +\$line \$path`
104+
end
105+
"""
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")
115+
end
116+
end
117+
118+
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`
125+
end
126+
define_editor(r"\bemacs\b.*(-nw|--no-window-system)",
127+
wait=true) do cmd, path, line
128+
`$cmd +$line $path`
129+
end
130+
define_editor(r"\bemacsclient\b.*(-nw|-t|-tty)",
131+
wait=true) do cmd, path, line
132+
`$cmd +$line $path`
133+
end
134+
define_editor(["textmate","mate","kate"]) do cmd, path, line
135+
`$cmd $path -l $line`
136+
end
137+
define_editor([r"\bsubl",r"\batom"]) do cmd, path, line
138+
`$cmd $path:$line`
139+
end
140+
define_editor("code") do cmd, path, line
141+
`$cmd -g $path:$line`
142+
end
143+
define_editor(r"\bnotepad++") do cmd, path, line
144+
`$cmd $path -n$line`
145+
end
146+
if Sys.iswindows()
147+
define_editor(r"\bCODE\.EXE\b"i) do cmd, path, line
148+
`$cmd -g $path:$line`
149+
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
160+
end
161+
end
162+
elseif Sys.isapple()
163+
define_editor("open") do cmd, path
164+
`open -t $path`
165+
end
166+
end
167+
end
168+
8169
"""
9170
editor()
10171
@@ -21,65 +182,54 @@ function editor()
21182
default_editor = "emacs"
22183
end
23184
# 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))))
185+
args = shell_split(get(ENV, "JULIA_EDITOR",
186+
get(ENV,"VISUAL", get(ENV,"EDITOR", default_editor))))
25187
isempty(args) && error("editor is empty")
26188
return args
27189
end
28190

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+
29200
"""
30201
edit(path::AbstractString, line::Integer=0)
31202
32203
Edit a file or directory optionally providing a line number to edit the file at.
33204
Return to the `julia` prompt when you quit the editor. The editor can be changed
34205
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.
35208
"""
36209
function edit(path::AbstractString, line::Integer=0)
210+
!isempty(editors) || define_default_editors()
37211
command = editor()
38-
name = basename(first(command))
39212
if endswith(path, ".jl")
40213
f = find_source_file(path)
41214
f !== nothing && (path = f)
42215
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
70-
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)
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
226+
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.")
81231
end
82-
line != 0 && line_unsupported && println("Unknown editor: no line number information passed.\nThe method is defined at line $line.")
232+
run(parsed)
83233

84234
nothing
85235
end
@@ -95,8 +245,8 @@ method to edit. For modules, open the main source file. The module needs to be l
95245
!!! compat "Julia 1.1"
96246
`edit` on modules requires at least Julia 1.1.
97247
98-
The editor can be changed by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR` as an environment
99-
variable.
248+
To ensure that the file can be opened at the given line, you may need to call
249+
`define_editor` first.
100250
"""
101251
edit(f) = edit(functionloc(f)...)
102252
edit(f, @nospecialize t) = edit(functionloc(f,t)...)

0 commit comments

Comments
 (0)