• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / 1513

22 Apr 2026 10:04PM UTC coverage: 77.843% (-0.04%) from 77.882%
1513

push

buildkite

web-flow
enable `less`/`edit` for documented variables (#53539)

We don't store the locations of definitions of variables, but the doc
system does (when they are documented). This PR uses that info to make
`less` and `edit` work on variables.

Fix #53534.

18 of 24 new or added lines in 3 files covered. (75.0%)

70 existing lines in 5 files now uncovered.

65422 of 84043 relevant lines covered (77.84%)

24394421.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.4
/stdlib/InteractiveUtils/src/editless.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
# editing and paging files
4

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`, `line`
11
and `column` 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 four 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
* `column` - the column number to open the editor at
30

31
Editors which cannot open to a specific line with a command or a specific column
32
may ignore the `line` and/or `column` argument. The `fn` callback must return
33
either an appropriate `Cmd` object to open a file or `nothing` to indicate that
34
they cannot edit this file. Use `nothing` to indicate that this editor is not
35
appropriate for the current environment and another editor should be attempted.
36
It is possible to add more general editing hooks that need not spawn
37
external commands by pushing a callback directly to the vector `EDITOR_CALLBACKS`.
38

39
The `pattern` argument is a string, regular expression, or an array of strings
40
and regular expressions. For the `fn` to be called, one of the patterns must
41
match the value of `EDITOR`, `VISUAL` or `JULIA_EDITOR`. For strings, the string
42
must equal the [`basename`](@ref) of the first word of the editor command, with
43
its extension, if any, removed. E.g. "vi" doesn't match "vim -g" but matches
44
"/usr/bin/vi -m"; it also matches `vi.exe`. If `pattern` is a regex it is
45
matched against all of the editor command as a shell-escaped string. An array
46
pattern matches if any of its items match. If multiple editors match, the one
47
added most recently is used.
48

49
By default julia does not wait for the editor to close, running it in the
50
background. However, if the editor is terminal based, you will probably want to
51
set `wait=true` and julia will wait for the editor to close before resuming.
52

53
If one of the editor environment variables is set, but no editor entry matches it,
54
the default editor entry is invoked:
55

56
    (cmd, path, line, column) -> `\$cmd \$path`
57

58
Note that many editors are already defined. All of the following commands should
59
already work:
60

61
- emacs
62
- emacsclient
63
- vim
64
- nvim
65
- nano
66
- micro
67
- kak
68
- helix
69
- textmate
70
- mate
71
- kate
72
- subl
73
- atom
74
- notepad++
75
- Visual Studio Code
76
- open
77
- pycharm
78
- bbedit
79

80
# Examples
81

82
The following defines the usage of terminal-based `emacs`:
83

84
    define_editor(
85
        r"\\bemacs\\b.*\\s(-nw|--no-window-system)\\b", wait=true) do cmd, path, line
86
        `\$cmd +\$line \$path`
87
    end
88

89
!!! compat "Julia 1.4"
90
    `define_editor` was introduced in Julia 1.4.
91
"""
92
function define_editor(fn::Function, pattern; wait::Bool=false)
×
93
    callback = function (cmd::Cmd, path::AbstractString, line::Integer, column::Integer)
×
94
        editor_matches(pattern, cmd) || return false
×
95
        editor = if !applicable(fn, cmd, path, line, column)
×
96
            # Be backwards compatible with editors that did not define the newly added column argument
97
            fn(cmd, path, line)
×
98
        else
99
            fn(cmd, path, line, column)
×
100
        end
101
        if editor isa Cmd
×
102
            if wait
×
103
                run(editor) # blocks while editor runs
×
104
            else
105
                run(pipeline(editor, stderr=stderr), wait=false)
×
106
            end
107
            return true
×
108
        elseif editor isa Nothing
×
109
            return false
×
110
        end
111
        @warn "invalid editor value returned" pattern=pattern editor=editor
×
112
        return false
×
113
    end
114
    pushfirst!(EDITOR_CALLBACKS, callback)
×
115
end
116

117
editor_matches(p::Regex, cmd::Cmd) = occursin(p, shell_escape(cmd))
×
118
editor_matches(p::String, cmd::Cmd) = p == splitext(basename(first(cmd)))[1]
×
119
editor_matches(ps::AbstractArray, cmd::Cmd) = any(editor_matches(p, cmd) for p in ps)
×
120

121
function define_default_editors()
×
122
    # fallback: just call the editor with the path as argument
123
    define_editor(r".*") do cmd, path, line, column
×
124
        `$cmd $path`
×
125
    end
126
    # vim family
127
    for (editors, wait) in [
×
128
        [["vim", "vi", "nvim", "mvim"], true],
129
        [[r"\bgvim"], false],
×
130
    ]
131
        define_editor(editors; wait) do cmd, path, line, column
×
132
            cmd = line == 0 ? `$cmd $path` :
×
133
                column == 0 ? `$cmd +$line $path` :
×
134
                `$cmd "+normal $(line)G$(column)|" $path`
×
135
        end
136
    end
×
137
    define_editor("nano"; wait=true) do cmd, path, line, column
×
138
        cmd = `$cmd +$line,$column $path`
×
139
    end
140
    # emacs (must check that emacs not running in -t/-nw
141
    # before regex match for general emacs)
142
    for (editors, wait) in [
×
143
        [[r"\bemacs"], false],
×
144
        [[r"\bemacs\b.*\s(-nw|--no-window-system)\b",
×
145
          r"\bemacsclient\b.\s*-(-?nw|t|-?tty)\b"], true],
×
146
    ]
147
        define_editor(editors; wait) do cmd, path, line, column
×
148
            `$cmd +$line:$column $path`
×
149
        end
150
    end
×
151
    # other editors
152
    define_editor("gedit") do cmd, path, line, column
×
153
        `$cmd +$line:$column $path`
×
154
    end
155
    define_editor(["micro", "kak"]; wait=true) do cmd, path, line, column
×
156
        `$cmd +$line $path`
×
157
    end
158
    define_editor(["hx", "helix"]; wait=true) do cmd, path, line, column
×
159
        `$cmd $path:$line:$column`
×
160
    end
161
    define_editor(["textmate", "mate", "kate"]) do cmd, path, line, column
×
162
        `$cmd $path -l $line`
×
163
    end
164
    define_editor([r"\bsubl", r"\batom", "pycharm", "bbedit"]) do cmd, path, line, column
×
165
        `$cmd $path:$line`
×
166
    end
167
    define_editor(["code", "code-insiders"]) do cmd, path, line, column
×
168
        `$cmd -g $path:$line:$column`
×
169
    end
170
    define_editor(r"\bnotepad++") do cmd, path, line, column
×
171
        `$cmd $path -n$line`
×
172
    end
173
    if Sys.iswindows()
×
174
        define_editor(r"\bCODE\.EXE\b"i) do cmd, path, line, column
×
175
            `$cmd -g $path:$line:$column`
×
176
        end
177
        callback = function (cmd::Cmd, path::AbstractString, line::Integer)
×
178
            cmd == `open` || return false
×
179
            # don't emit this ccall on other platforms
180
            @static if Sys.iswindows()
×
181
                result = ccall((:ShellExecuteW, "shell32"), stdcall,
×
182
                               Int, (Ptr{Cvoid}, Cwstring, Cwstring,
183
                                     Ptr{Cvoid}, Ptr{Cvoid}, Cint),
184
                               C_NULL, "open", path, C_NULL, C_NULL, 10)
185
                systemerror(:edit, result ≤ 32)
×
186
            end
187
            return true
×
188
        end
189
        pushfirst!(EDITOR_CALLBACKS, callback)
×
190
    elseif Sys.isapple()
×
191
        define_editor("open") do cmd, path, line, column
×
192
            `open -t $path`
×
193
        end
194
    end
195
end
196
define_default_editors()
197

198
"""
199
    editor()
200

201
Determine the editor to use when running functions like `edit`. Returns a `Cmd`
202
object. Change editor by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR`
203
environment variables.
204
"""
205
function editor()
27✔
206
    # Note: the editor path can include spaces (if escaped) and flags.
207
    for var in ["JULIA_EDITOR", "VISUAL", "EDITOR"]
27✔
208
        str = get(ENV, var, nothing)
33✔
209
        str === nothing && continue
33✔
210
        isempty(str) && error("invalid editor \$$var: $(repr(str))")
24✔
211
        return Cmd(shell_split(str))
21✔
212
    end
9✔
213
    editor_file = "/etc/alternatives/editor"
3✔
214
    editor = (Sys.iswindows() || Sys.isapple()) ? "open" :
3✔
215
        isfile(editor_file) ? realpath(editor_file) : "emacs"
216
    return Cmd([editor])
3✔
217
end
218

219
"""
220
    edit(path::AbstractString, line::Integer=0, column::Integer=0)
221

222
Edit a file or directory optionally providing a line number to edit the file at.
223
Return to the `julia` prompt when you quit the editor. The editor can be changed
224
by setting `JULIA_EDITOR`, `VISUAL` or `EDITOR` as an environment variable.
225

226
!!! compat "Julia 1.9"
227
    The `column` argument requires at least Julia 1.9.
228

229
See also [`InteractiveUtils.define_editor`](@ref).
230
"""
231
function edit(path::AbstractString, line::Integer=0, column::Integer=0)
×
232
    path isa String || (path = convert(String, path))
×
233
    if endswith(path, ".jl")
×
234
        p = find_source_file(path)
×
235
        p !== nothing && (path = p)
×
236
    end
237
    cmd = editor()
×
238
    for callback in EDITOR_CALLBACKS
×
239
        if !applicable(callback, cmd, path, line, column)
×
240
            callback(cmd, path, line) && return
×
241
        else
242
            callback(cmd, path, line, column) && return
×
243
        end
244
    end
×
245
    # shouldn't happen unless someone has removed fallback entry
246
    error("no editor found")
×
247
end
248

249
"""
250
    edit(function, [types])
251
    edit(module)
252

253
Edit the definition of a function, optionally specifying a tuple of types to indicate which
254
method to edit. For modules, open the main source file. The module needs to be loaded with
255
`using` or `import` first.
256

257
!!! compat "Julia 1.1"
258
    `edit` on modules requires at least Julia 1.1.
259

260
To ensure that the file can be opened at the given line, you may need to call
261
`InteractiveUtils.define_editor` first.
262
"""
263
function edit(@nospecialize f)
×
264
    ms = methods(f).ms
×
265
    length(ms) == 1 && edit(functionloc(ms[1])...)
×
266
    length(ms) > 1 && return ms
×
267
    length(ms) == 0 && functionloc(f) # throws
×
268
    nothing
×
269
end
270
edit(m::Method) = edit(functionloc(m)...)
×
271
edit(@nospecialize(f), idx::Integer) = edit(methods(f).ms[idx])
×
272
edit(f, t) = (@nospecialize; edit(functionloc(f, t)...))
×
273
edit(@nospecialize argtypes::Union{Tuple, Type{<:Tuple}}) = edit(functionloc(argtypes)...)
×
274
edit(file::Nothing, line::Integer) = error("could not find source file for function")
×
275
function edit(m::Module)
3✔
276
    path = pathof(m)
3✔
277
    path === nothing && error("could not find source file for module: $m")
3✔
278
    edit(path)
×
279
end
NEW
280
edit(m::Module, n::Symbol) = edit(varloc(m, n)...)
×
281

282
# terminal pager
283

284
if Sys.iswindows()
285
    function less(file::AbstractString, line::Integer)
×
286
        pager = shell_split(get(ENV, "PAGER", "more"))
×
287
        if pager[1] == "more"
×
288
            g = ""
×
289
            line -= 1
×
290
        else
291
            g = "g"
×
292
        end
293
        run(Cmd(`$pager +$(line)$(g) \"$file\"`, windows_verbatim = true))
×
294
        nothing
×
295
    end
296
else
297
    function less(file::AbstractString, line::Integer)
×
298
        pager = shell_split(get(ENV, "PAGER", "less"))
×
299
        run(`$pager +$(line)g $file`)
×
300
        nothing
×
301
    end
302
end
303

304
"""
305
    less(file::AbstractString, [line::Integer])
306

307
Show a file using the default pager, optionally providing a starting line number. Returns to
308
the `julia` prompt when you quit the pager.
309
"""
310
less(file::AbstractString) = less(file, 1)
×
311

312
"""
313
    less(function, [types])
314

315
Show the definition of a function using the default pager, optionally specifying a tuple of
316
types to indicate which method to see.
317
"""
NEW
318
less(f)                    = less(functionloc(f)...)
×
NEW
319
less(f, @nospecialize t)   = less(functionloc(f,t)...)
×
NEW
320
less(file, line::Integer)  = error("could not find source file for function")
×
NEW
321
less(m::Module, n::Symbol) = less(varloc(m, n)...)
×
322

323
# find where a (documented) global is defined using the doc system
324
function varloc(mod::Module, name::Symbol)
3✔
325
    m = Base.Docs.meta(mod)
3✔
326
    if m !== nothing
3✔
327
        bnd = Base.Docs.Binding(mod, name)
6✔
328
        if haskey(m, bnd)
3✔
329
            docstr = m[bnd]
3✔
330
            if docstr isa Base.Docs.MultiDoc
3✔
331
                length(docstr.docs) != 1 && @goto err
3✔
332
                docstr = only(docstr.docs).second
6✔
333
            end
334
            if docstr isa Base.Docs.DocStr
3✔
335
                mm = docstr.data
3✔
336
                if haskey(mm, :path) && haskey(mm, :linenumber)
6✔
337
                    return (mm[:path], mm[:linenumber])
3✔
338
                end
339
            end
340
        end
341
    end
342
    @label err
NEW
343
    error("could not determine location of variable $mod.$name")
×
344
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc