Skip to content

Commit 993bb85

Browse files
make file paths clickable in URI-recognizing terminal
1 parent 8e53b1f commit 993bb85

File tree

2 files changed

+61
-25
lines changed

2 files changed

+61
-25
lines changed

stdlib/Profile/src/Allocs.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ end
321321
function flat(io::IO, data::Vector{Alloc}, cols::Int, fmt::ProfileFormat)
322322
fmt.combine || error(ArgumentError("combine=false"))
323323
lilist, n, m, totalbytes = parse_flat(fmt.combine ? StackFrame : UInt64, data, fmt.C)
324-
filenamemap = Dict{Symbol,Tuple{String,String}}()
324+
filenamemap = FileNameMap()
325325
if isempty(lilist)
326326
warning_empty()
327327
return true

stdlib/Profile/src/Profile.jl

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -505,9 +505,10 @@ end
505505

506506
# Take a file-system path and try to form a concise representation of it
507507
# based on the package ecosystem
508-
function short_path(spath::Symbol, filenamecache::Dict{Symbol, Tuple{String,String}})
508+
function short_path(spath::Symbol, filenamecache::Dict{Symbol, Tuple{String,String,String}})
509509
return get!(filenamecache, spath) do
510510
path = Base.fixup_stdlib_path(string(spath))
511+
possible_base_path = normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base", path))
511512
if isabspath(path)
512513
if ispath(path)
513514
# try to replace the file-system prefix with a short "@Module" one,
@@ -524,20 +525,21 @@ function short_path(spath::Symbol, filenamecache::Dict{Symbol, Tuple{String,Stri
524525
pkgid = Base.project_file_name_uuid(project_file, "")
525526
isempty(pkgid.name) && return path # bad Project file
526527
# return the joined the module name prefix and path suffix
527-
path = path[nextind(path, sizeof(root)):end]
528-
return string("@", pkgid.name), path
528+
_short_path = path[nextind(path, sizeof(root)):end]
529+
return path, string("@", pkgid.name), _short_path
529530
end
530531
end
531532
end
532533
end
533-
return "", path
534-
elseif isfile(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base", path))
534+
return path, "", path
535+
elseif isfile(possible_base_path)
535536
# do the same mechanic for Base (or Core/Compiler) files as above,
536537
# but they start from a relative path
537-
return "@Base", normpath(path)
538+
return possible_base_path, "@Base", normpath(path)
538539
else
539540
# for non-existent relative paths (such as "REPL[1]"), just consider simplifying them
540-
return "", normpath(path) # drop leading "./"
541+
path = normpath(path)
542+
return "", "", path # drop leading "./"
541543
end
542544
end
543545
end
@@ -758,6 +760,8 @@ function parse_flat(::Type{T}, data::Vector{UInt64}, lidict::Union{LineInfoDict,
758760
return (lilist, n, m, totalshots, nsleeping)
759761
end
760762

763+
const FileNameMap = Dict{Symbol,Tuple{String,String,String}}
764+
761765
function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfoFlatDict}, cols::Int, fmt::ProfileFormat,
762766
threads::Union{Int,AbstractVector{Int}}, tasks::Union{UInt,AbstractVector{UInt}}, is_subsection::Bool)
763767
lilist, n, m, totalshots, nsleeping = parse_flat(fmt.combine ? StackFrame : UInt64, data, lidict, fmt.C, threads, tasks)
@@ -768,7 +772,7 @@ function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfo
768772
m = m[keep]
769773
end
770774
util_perc = (1 - (nsleeping / totalshots)) * 100
771-
filenamemap = Dict{Symbol,Tuple{String,String}}()
775+
filenamemap = FileNameMap()
772776
if isempty(lilist)
773777
if is_subsection
774778
Base.print(io, "Total snapshots: ")
@@ -790,9 +794,34 @@ function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfo
790794
return false
791795
end
792796

797+
# make a terminal-clickable link to the file and linenum.
798+
# Similar to `define_default_editors` in `Base.Filesystem` but for creating URIs not commands
799+
function editor_link(path::String, linenum::Int)
800+
editor = get(ENV, "JULIA_EDITOR", "")
801+
802+
if editor == "code"
803+
return "vscode://file/$path:$linenum"
804+
elseif editor == "subl" || editor == "sublime_text"
805+
return "subl://$path:$linenum"
806+
elseif editor == "idea" || occursin("idea", editor)
807+
return "idea://open?file=$path&line=$linenum"
808+
elseif editor == "pycharm"
809+
return "pycharm://open?file=$path&line=$linenum"
810+
elseif editor == "atom"
811+
return "atom://core/open/file?filename=$path&line=$linenum"
812+
elseif editor == "emacsclient"
813+
return "emacs://open?file=$path&line=$linenum"
814+
elseif editor == "vim" || editor == "nvim"
815+
return "vim://open?file=$path&line=$linenum"
816+
else
817+
# TODO: convert the path to a generic URI (line numbers are not supported by generic URI)
818+
return path
819+
end
820+
end
821+
793822
function print_flat(io::IO, lilist::Vector{StackFrame},
794823
n::Vector{Int}, m::Vector{Int},
795-
cols::Int, filenamemap::Dict{Symbol,Tuple{String,String}},
824+
cols::Int, filenamemap::FileNameMap,
796825
fmt::ProfileFormat)
797826
if fmt.sortedby === :count
798827
p = sortperm(n)
@@ -804,7 +833,7 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
804833
lilist = lilist[p]
805834
n = n[p]
806835
m = m[p]
807-
pkgnames_filenames = Tuple{String,String}[short_path(li.file, filenamemap) for li in lilist]
836+
pkgnames_filenames = Tuple{String,String,String}[short_path(li.file, filenamemap) for li in lilist]
808837
funcnames = String[string(li.func) for li in lilist]
809838
wcounts = max(6, ndigits(maximum(n)))
810839
wself = max(9, ndigits(maximum(m)))
@@ -815,7 +844,7 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
815844
li = lilist[i]
816845
maxline = max(maxline, li.line)
817846
maxfunc = max(maxfunc, textwidth(funcnames[i]))
818-
maxfile = max(maxfile, sum(textwidth, pkgnames_filenames[i]) + 1)
847+
maxfile = max(maxfile, sum(textwidth, pkgnames_filenames[i][2:3]) + 1)
819848
end
820849
wline = max(5, ndigits(maxline))
821850
ntext = max(20, cols - wcounts - wself - wline - 3)
@@ -843,7 +872,7 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
843872
Base.print(io, "[any unknown stackframes]")
844873
end
845874
else
846-
pkgname, file = pkgnames_filenames[i]
875+
path, pkgname, file = pkgnames_filenames[i]
847876
isempty(file) && (file = "[unknown file]")
848877
pkgcolor = get!(() -> popfirst!(Base.STACKTRACE_MODULECOLORS), PACKAGE_FIXEDCOLORS, pkgname)
849878
Base.printstyled(io, pkgname, color=pkgcolor)
@@ -853,7 +882,12 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
853882
Base.print(io, "/")
854883
wpad -= 1
855884
end
856-
Base.print(io, rpad(file_trunc, wpad, " "), " ")
885+
if isempty(path)
886+
Base.print(io, rpad(file_trunc, wpad, " "))
887+
else
888+
link = editor_link(path, li.line)
889+
Base.print(io, rpad(styled"{link=$link:$file_trunc}", wpad, " "))
890+
end
857891
Base.print(io, lpad(li.line > 0 ? string(li.line) : "?", wline, " "), " ")
858892
fname = funcnames[i]
859893
if !li.from_c && li.linfo !== nothing
@@ -902,7 +936,7 @@ end
902936
# mimics Stacktraces
903937
const PACKAGE_FIXEDCOLORS = Dict{String, Any}("@Base" => :gray, "@Core" => :gray)
904938

905-
function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, maxes, filenamemap::Dict{Symbol,Tuple{String,String}}, showpointer::Bool)
939+
function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, maxes, filenamemap::FileNameMap, showpointer::Bool)
906940
nindent = min(cols>>1, level)
907941
ndigoverhead = ndigits(maxes.overhead)
908942
ndigcounts = ndigits(maxes.count)
@@ -937,7 +971,7 @@ function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, ma
937971
else
938972
fname = string(li.func)
939973
end
940-
pkgname, filename = short_path(li.file, filenamemap)
974+
path, pkgname, filename = short_path(li.file, filenamemap)
941975
if showpointer
942976
fname = string(
943977
"0x",
@@ -947,14 +981,16 @@ function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, ma
947981
end
948982
pkgcolor = get!(() -> popfirst!(Base.STACKTRACE_MODULECOLORS), PACKAGE_FIXEDCOLORS, pkgname)
949983
remaining_path = ltruncate(filename, widthfile - textwidth(pkgname) - 1)
950-
strs[i] = Base.annotatedstring(stroverhead, "", base, strcount, " ",
951-
styled"{$pkgcolor:$pkgname}",
952-
!isempty(pkgname) && !startswith(remaining_path, "/") ? "/" : "",
953-
remaining_path,
954-
":",
955-
li.line == -1 ? "?" : string(li.line),
956-
"; ",
957-
fname)
984+
linenum = li.line == -1 ? "?" : string(li.line)
985+
slash = (!isempty(pkgname) && !startswith(remaining_path, "/")) ? "/" : ""
986+
styled_path = styled"{$pkgcolor:$pkgname}$slash$remaining_path:$linenum"
987+
rich_file = if isempty(path)
988+
styled_path
989+
else
990+
link = editor_link(path, li.line)
991+
styled"{link=$link:$styled_path}"
992+
end
993+
strs[i] = Base.annotatedstring(stroverhead, "", base, strcount, " ", rich_file, "; $fname")
958994
end
959995
else
960996
strs[i] = string(stroverhead, "", base, strcount, " [unknown stackframe]")
@@ -1118,7 +1154,7 @@ end
11181154
# avoid stack overflows.
11191155
function print_tree(io::IO, bt::StackFrameTree{T}, cols::Int, fmt::ProfileFormat, is_subsection::Bool) where T
11201156
maxes = maxstats(bt)
1121-
filenamemap = Dict{Symbol,Tuple{String,String}}()
1157+
filenamemap = FileNameMap()
11221158
worklist = [(bt, 0, 0, AnnotatedString(""))]
11231159
if !is_subsection
11241160
Base.print(io, "Overhead ╎ [+additional indent] Count File:Line; Function\n")

0 commit comments

Comments
 (0)