1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
11 local P
, S
, R
, C
, V
, Cp
, Cmt
, Carg
, Cs
=
12 lpeg
.P
, lpeg
.S
, lpeg
.R
, lpeg
.C
, lpeg
.V
, lpeg
.Cp
, lpeg
.Cmt
, lpeg
.Carg
, lpeg
.Cs
16 local function at_pos(_
, position
, start
, pos
, ...) return (pos
>= start
and pos
< position
), ... end
17 local function if_pos(start
, mod, finish
, pos
) if pos
>= start
and pos
< finish
then return mod end end
18 local function replace() return "/" end
19 local function mkpath(mod, sep
) return Cs(mod * (("/" + C(sep
) / replace
) * mod)^
0) end
20 local function anywhere(patt
) return P
{Cmt(Cp() * Carg(1) * patt
, at_pos
) + 1 * V(1)} end
21 local function spot(patt
) return (Cp() * patt
* Cp() * Carg(1) / if_pos
) end
22 local function join(from
, mod)
24 return from
.. "/" .. mod
31 local snake_kebab
= (R
"az" + R
"AZ" + R
"09" + S
"_-")^
1
32 local snake
= (R
"az" + R
"AZ" + R
"09" + S
"_")^
1
33 local c_name
= (1 - S
'">')^
1
34 local py_pkg
= mkpath(snake
, P
".")
35 local py_renamer
= sp^
1 * "as" * sp^
1 * snake
36 local py_mod
= py_pkg
* py_renamer^
-1
37 local py_import
= spot(sp^
0 * "import" * sp^
1 * py_mod
) * (spot(sp^
0 * "," * sp^
0 * py_mod
))^
0
38 local lua_require
= anywhere("require" * sp^
0 * P
"("^
-1 * sp^
0 * S
'"\'' * mkpath(snake_kebab
, "."))
43 ansi_c
= P
{sp^
0 * "#" * sp^
0 * "include" * sp^
0 * S
'<"' * C(c_name
)},
44 cpp
= P
{sp^
0 * "#" * sp^
0 * "include" * sp^
0 * S
'<"' * C(c_name
)},
45 tcl
= anywhere("package" * sp^
1 * "require" * (sp^
1 * "-exact")^
-1 * sp^
1 * mkpath(snake
, "::")),
46 perl
= anywhere((P
"use" + "require") * sp^
1 * mkpath(snake
, "::")),
48 moonscript
= lua_require
,
49 python
= ((sp^
0 * "from" * sp^
1 * py_pkg
)^
-1 * py_import
) / join
57 scheme
= "main", -- Racket-specific
58 tcl
= true, -- arbitrary way to flag that a package dir contains a namesake file inside
61 local suffixesadd
= {}
63 local external_match
= {}
68 local function regex_escape(text
)
69 return text
:gsub("[][)(}{|+?*.]", "\\%0")
70 :gsub("%^", "\\^"):gsub("^/\\%^", "/^")
71 :gsub("%$", "\\$"):gsub("\\%$/$", "$/")
74 local function lua_escape(text
)
75 return text
:gsub("[][^$)(%.*+?-]", "%%%0")
78 local function extract_location(text
, syntax
, column
, complete
, strip_count
)
80 if external_match
[syntax
] and not complete
then
81 fname
= external_match
[syntax
](vis
.win
, vis
.win
.selection
.pos
)
85 local cmdpatt
= P
":" * C(R
"09"^
1 + "/" * (1 - P
"/")^
1 * "/") + P
"#" * S
"Ll" * C(R
"09"^
1)
87 local fnamepatt
= P(1 - S
":#")^
1
88 patt
= C(fnamepatt
) * cmdpatt^
-1
90 local fnamepatt
= P(1 - S
':# \t"\'`*][><}{)(')^
1
91 patt
= P
{Cmt(Cp() * Carg(1) * C(fnamepatt
) * cmdpatt^
-1, at_pos
) + 1 * V(1)}
92 if syntax
and M
.includeexpr
[syntax
] then
93 patt
= M
.includeexpr
[syntax
] + patt
96 fname
, cmd
= patt
:match(text
, 1, column
)
97 if cmd
and cmd
:find
"^/" then
98 cmd
= regex_escape(cmd
)
101 return fname
and fname
:gsub("[^/]-/", "", strip_count
or 0), cmd
104 local function try_suffixes(fpath
, syntax
)
105 for _
, ext
in ipairs(suffixesadd
[syntax
]) do
106 if lfs
.attributes(fpath
.. ext
, "mode") == "file" then
112 local function try(fpath
, fname
, syntax
)
113 local found
= try_suffixes(fpath
, syntax
)
115 if lfs
.attributes(fpath
, "mode") == "directory" and pkg_init
[syntax
] then
116 local initpath
= fpath
.. "/" .. (pkg_init
[syntax
] == true and fname
or pkg_init
[syntax
])
117 found
= try_suffixes(initpath
, syntax
)
123 local function goto_location(text
, win
, split
)
124 local syntax
= win
.syntax
125 local fname
, cmd
= extract_location(text
or win
.file
.lines
[win
.selection
.line
],
126 syntax
, win
.selection
.col
, not not text
, vis
.count
)
128 if not fname
then return end
130 if extensions
[syntax
] then
131 for _
, ext
in ipairs(extensions
[syntax
]) do
132 if fname
:match(ext
) then
138 local same
= fname
== win
.file
.path
or (fname
:sub(1, 1) ~= "/" and fname
== win
.file
.name
)
139 if not same
or split
and same
then
142 if fname
:sub(1, 1) == "/" then
143 mode
, errmsg
= lfs
.attributes(fname
, "mode")
144 if mode
== "file" then
148 for _
, dir
in ipairs(M
.path
[win
]) do
149 if dir
:sub(1, 1) == "." and win
.file
.path
then
150 dir
= win
.file
.path
:match("(.+/)[^/]+$") .. dir
152 local fpath
= #dir
> 0 and (dir
.. "/" .. fname
) or fname
153 mode
, errmsg
= lfs
.attributes(fpath
, "mode")
154 if mode
== "file" then
156 elseif syntax
and not has_extension
then
157 realname
= try(fpath
, fname
, syntax
)
158 if not realname
and fpath
:find
"/" then
159 -- If the regular magic yielded nothing, assume that the last part
160 -- of fpath is not a dir or file, but an identifier, and ignore it.
161 local modpath
= fpath
:match("(.+)/[^/]+$")
162 realname
= try(modpath
, fname
, syntax
)
165 if realname
then break end
169 if realname
:find("%s") then realname
= string.format("%q", realname
) end
170 vis
:command((split
and "open " or "e ") .. realname
)
172 vis
:info(errmsg
or fname
.. ": File not found")
176 if cmd
and win
~= vis
.win
then
177 if tonumber(cmd
) then
178 vis
.win
.selection
:to(cmd
, 1)
180 vis
.win
.selection
:to(1, 1)
182 vis
.win
.selection
.pos
= vis
.win
.selection
.range
.start
183 vis
.mode
= vis
.modes
.NORMAL
189 vis
.events
.subscribe(vis
.events
.WIN_STATUS
, function(win
)
190 if win
== vis
.win
and win
.syntax
~= last_syntax
then
192 suffixesadd
[win
.syntax
] = {}
193 extensions
[win
.syntax
] = vis
.ftdetect
.filetypes
[win
.syntax
].ext
194 for _
, patt
in ipairs(extensions
[win
.syntax
]) do
195 local ext
= patt
:gsub("^%%(.+)%$$", "%1")
196 table.insert(suffixesadd
[win
.syntax
], ext
)
199 last_syntax
= win
.syntax
203 -- Inverted and unrolled config table.
204 -- When a file from one of the search dirs of a workspace is opened,
205 -- (but is outside the workspace) this dir is added to the file's search path.
206 -- This allows one to keep exploring with gf without having to explicitly
207 -- include the dir in a workspace.
210 local function preprocess(tbl
)
211 for _
, map
in pairs(tbl
) do
212 for syntax
, path
in pairs(map
) do
213 lookup
[syntax
] = lookup
[syntax
] or {}
214 for _
, dir
in ipairs(path
) do
215 lookup
[syntax
][dir
] = true
221 vis
.events
.subscribe(vis
.events
.INIT
, function()
223 preprocess(M
.workspaces
)
228 __newindex
= function(t
, k
, v
)
229 for _
, dir
in ipairs(t
) do
230 if v
== dir
then return end
236 vis
.events
.subscribe(vis
.events
.WIN_OPEN
, function(win
)
238 M
.path
[win
] = setmetatable({"."}, ignoredups
)
240 if M
.workspaces
and win
.file
.path
and win
.syntax
then
241 local dirname
= win
.file
.path
:match("(.+/)[^/]+$")
242 local sorted_workspaces
= {}
243 for wsdir
in pairs(M
.workspaces
) do
244 table.insert(sorted_workspaces
, wsdir
)
246 table.sort(sorted_workspaces
, function(x
, y
) return x
> y
end)
247 for _
, wsdir
in ipairs(sorted_workspaces
) do
248 local map
= M
.workspaces
[wsdir
]
249 if dirname
:match("^" .. lua_escape(wsdir
)) then
250 for _
, syntax
in ipairs({win
.syntax
, 1}) do
252 for _
, dir
in ipairs(map
[syntax
]) do
253 table.insert(M
.path
[win
], dir
)
259 for _
, syntax
in ipairs({win
.syntax
, 1}) do
260 if lookup
[syntax
] then
261 for dir
in pairs(lookup
[syntax
]) do
262 if dirname
:match("^" .. dir
) then
263 table.insert(M
.path
[win
], dir
)
270 if #M
.path
[win
] <= 1 then
271 table.insert(M
.path
[win
], "/usr/include")
273 table.insert(M
.path
[win
], "")
276 vis
.events
.subscribe(vis
.events
.WIN_CLOSE
, function(win
)
280 local function goto_file(file
, win
, split
)
282 if win
.file
.name
then
284 path
= win
.file
.path
,
285 line
= win
.selection
.line
,
286 col
= win
.selection
.col
,
289 if goto_location(file
, win
, split
) and not split
then
290 table.insert(locations
, old
)
294 local function h(msg
)
295 return string.format("|@%s| %s", progname
, msg
)
298 vis
:map(vis
.modes
.NORMAL
, "gf", function()
299 goto_file(nil, vis
.win
)
300 end, h
"Edit the file whose name is under the cursor")
302 vis
:map(vis
.modes
.NORMAL
, "<C-w><C-f>", function()
303 goto_file(nil, vis
.win
, true)
304 end, h
"Edit the file whose name is under the cursor, in a new window")
306 vis
:map(vis
.modes
.VISUAL
, "gf", function()
308 local text
= win
.file
:content(win
.selection
.range
)
310 end, h
"Edit the file whose name is selected")
312 vis
:map(vis
.modes
.VISUAL
, "<C-w><C-f>", function()
314 local text
= win
.file
:content(win
.selection
.range
)
315 goto_file(text
, win
, true)
316 end, h
"Edit the file whose name is selected, in a new window")
318 vis
:command_register("find", function(argv
, _
, win
)
319 if #argv
~= 1 then return end
320 return goto_file(argv
[1], win
)
321 end, h
"Same as :e, but search in 'path'")
323 vis
:command_register("sfind", function(argv
, _
, win
)
324 if #argv
~= 1 then return end
325 return goto_file(argv
[1], win
, true)
326 end, h
"Same as :split, but search in 'path'")
328 vis
:map(vis
.modes
.NORMAL
, "<C-w>f", "<C-w><C-f>")
329 vis
:map(vis
.modes
.VISUAL
, "<C-w>f", "<C-w><C-f>")
331 vis
:map(vis
.modes
.NORMAL
, "<C-o>", function()
332 if #locations
< 1 then
335 local last
= locations
[#locations
]
336 if last
.path
~= vis
.win
.file
.path
then
337 vis
:command("e" .. last
.path
)
338 if last
.path
~= vis
.win
.file
.path
then
342 vis
.win
.selection
:to(last
.line
, last
.col
)
343 table.remove(locations
)
344 end, h
"Go to older cursor position in jump list")
346 -- XXX: other plugins can check for this and register their own handlers.
347 -- Useful for multi-line includeexpr or mechanisms that require evaluating external code.
348 function vis_goto_file(syntax
, handler
)
349 if syntax
and handler
then
350 external_match
[syntax
] = handler