Squash commits
[vis-goto-file.git] / init.lua
blob4922e57d89c92a7d00f2dc525489d68aebe8e684
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 require("vis")
5 require("lfs")
6 require("lpeg")
8 local vis = vis
9 local lfs = lfs
10 local lpeg = lpeg
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
14 local progname = ...
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)
23 if from and mod then
24 return from .. "/" .. mod
25 else
26 return mod or from
27 end
28 end
30 local sp = S" \t"
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, "."))
40 local M = {
41 path = {},
42 includeexpr = {
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, "::")),
47 lua = lua_require,
48 moonscript = lua_require,
49 python = ((sp^0 * "from" * sp^1 * py_pkg)^-1 * py_import) / join
53 local pkg_init = {
54 lua = "init",
55 fennel = "init",
56 python = "__init__",
57 scheme = "main", -- Racket-specific
58 tcl = true, -- arbitrary way to flag that a package dir contains a namesake file inside
61 local suffixesadd = {}
62 local extensions = {}
63 local external_match = {}
65 local locations = {}
66 local last_syntax
68 local function regex_escape(text)
69 return text:gsub("[][)(}{|+?*.]", "\\%0")
70 :gsub("%^", "\\^"):gsub("^/\\%^", "/^")
71 :gsub("%$", "\\$"):gsub("\\%$/$", "$/")
72 end
74 local function lua_escape(text)
75 return text:gsub("[][^$)(%.*+?-]", "%%%0")
76 end
78 local function extract_location(text, syntax, column, complete, strip_count)
79 local fname, cmd
80 if external_match[syntax] and not complete then
81 fname = external_match[syntax](vis.win, vis.win.selection.pos)
82 end
83 if not fname then
84 local patt
85 local cmdpatt = P":" * C(R"09"^1 + "/" * (1 - P"/")^1 * "/") + P"#" * S"Ll" * C(R"09"^1)
86 if complete then
87 local fnamepatt = P(1 - S":#")^1
88 patt = C(fnamepatt) * cmdpatt^-1
89 else
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
94 end
95 end
96 fname, cmd = patt:match(text, 1, column)
97 if cmd and cmd:find"^/" then
98 cmd = regex_escape(cmd)
99 end
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
107 return fpath .. ext
112 local function try(fpath, fname, syntax)
113 local found = try_suffixes(fpath, syntax)
114 if not found then
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)
120 return found
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)
127 vis.count = nil
128 if not fname then return end
129 local has_extension
130 if extensions[syntax] then
131 for _, ext in ipairs(extensions[syntax]) do
132 if fname:match(ext) then
133 has_extension = true
134 break
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
140 local realname
141 local mode, errmsg
142 if fname:sub(1, 1) == "/" then
143 mode, errmsg = lfs.attributes(fname, "mode")
144 if mode == "file" then
145 realname = fname
147 else
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
155 realname = fpath
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
168 if realname then
169 if realname:find("%s") then realname = string.format("%q", realname) end
170 vis:command((split and "open " or "e ") .. realname)
171 else
172 vis:info(errmsg or fname .. ": File not found")
173 return false
176 if cmd and win ~= vis.win then
177 if tonumber(cmd) then
178 vis.win.selection:to(cmd, 1)
179 else
180 vis.win.selection:to(1, 1)
181 vis:command(cmd)
182 vis.win.selection.pos = vis.win.selection.range.start
183 vis.mode = vis.modes.NORMAL
186 return true
189 vis.events.subscribe(vis.events.WIN_STATUS, function(win)
190 if win == vis.win and win.syntax ~= last_syntax then
191 if win.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
201 end)
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.
208 local lookup = {}
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()
222 if M.workspaces then
223 preprocess(M.workspaces)
225 end)
227 local ignoredups = {
228 __newindex = function(t, k, v)
229 for _, dir in ipairs(t) do
230 if v == dir then return end
232 rawset(t, k, v)
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
251 if map[syntax] then
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], "")
274 end)
276 vis.events.subscribe(vis.events.WIN_CLOSE, function(win)
277 M.path[win] = nil
278 end)
280 local function goto_file(file, win, split)
281 local old
282 if win.file.name then
283 old = {
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()
307 local win = vis.win
308 local text = win.file:content(win.selection.range)
309 goto_file(text, win)
310 end, h"Edit the file whose name is selected")
312 vis:map(vis.modes.VISUAL, "<C-w><C-f>", function()
313 local win = vis.win
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
333 return
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
339 return false
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
354 return M