vis-config
lua scripts to configure vis editor
git clone https://9o.is/git/vis-config.git
ctags.lua
(7578B)
1 local positions = {}
2 local tags = { 'tags' }
3 local ctags = { actions = {} }
4
5 local function abs_path(prefix, path)
6 if string.find(path, '^/') ~= nil then
7 return path, path
8 end
9
10 if string.find(path, '^./') ~= nil then
11 path = path:sub(3)
12 end
13
14 return prefix .. path, path
15 end
16
17 local function is_directory(path)
18 local dir = io.open(path .. '/', 'r')
19 if dir then
20 dir:close()
21 return true
22 else
23 return false
24 end
25 end
26
27 local function find_tags(path)
28 for i = #path, 1, -1 do
29 if path:sub(i, i) == '/' then
30 local prefix = path:sub(1, i)
31 for j = 1, #tags do
32 local tagfile = tags[j]
33 local filename
34 if tagfile:sub(1, 1) == '/' then
35 filename = tagfile
36 else
37 filename = prefix .. tagfile
38 end
39 if not is_directory(filename) then
40 local file = io.open(filename, 'r')
41
42 if file ~= nil then
43 return file, prefix
44 end
45 end
46 end
47 end
48 end
49 end
50
51 local function bsearch(file, word)
52 local buffer_size = 8096
53 local format = '\n(.-)\t(.-)\t(.-);"\t'
54
55 local from = 0
56 local to = file:seek('end')
57 local startpos = nil
58
59 while from <= to do
60 local mid = from + math.floor((to - from) / 2)
61 file:seek('set', mid)
62
63 local content = file:read(buffer_size, '*line')
64 if content ~= nil then
65 local key, _, _ = string.match(content, format)
66 if key == nil then
67 break
68 end
69
70 if key == word then
71 startpos = mid
72 end
73
74 if key >= word then
75 to = mid - 1
76 else
77 from = mid + 1
78 end
79 else
80 to = mid - 1
81 end
82 end
83
84 if startpos ~= nil then
85 file:seek('set', startpos)
86
87 local result = {}
88 while true do
89 local content = file:read(buffer_size, '*line')
90 if content == nil then
91 break
92 end
93
94 for key, filename, excmd in string.gmatch(content, format) do
95 if key == word then
96 result[#result + 1] = { name = filename, excmd = excmd }
97 else
98 return result
99 end
100 end
101 end
102
103 return result
104 end
105 end
106
107 local function get_query()
108 local line = vis.win.selection.line
109 local pos = vis.win.selection.col
110 local str = vis.win.file.lines[line]
111
112 local from, to = 0, 0
113 while pos > to do
114 from, to = str:find('[%a_]+[%a%d_]*', to + 1)
115 if from == nil or from > pos then
116 return nil
117 end
118 end
119
120 return string.sub(str, from, to)
121 end
122
123 local function get_matches(word, path)
124 local file, prefix = find_tags(path)
125
126 if file ~= nil then
127 local results = bsearch(file, word)
128 file:close()
129
130 if results ~= nil then
131 local matches = {}
132 for i = 1, #results do
133 local result = results[i]
134 local abspath, name = abs_path(prefix, result.name)
135 local desc = string.format('%s%s', name, tonumber(result.excmd) and ':' .. result.excmd or '')
136
137 matches[#matches + 1] = { desc = desc, path = abspath, excmd = result.excmd }
138 end
139
140 return matches
141 end
142 end
143 end
144
145 local function get_match(word, path)
146 local matches = get_matches(word, path)
147 if matches ~= nil then
148 for i = 1, #matches do
149 if matches[i].path == path then
150 return matches[i]
151 end
152 end
153
154 return matches[1]
155 end
156 end
157
158 local function escape(text)
159 return text:gsub('[][)(}{|+?*.]', '\\%0')
160 :gsub('%^', '\\^')
161 :gsub('^/\\%^', '/^')
162 :gsub('%$', '\\$')
163 :gsub('\\%$/$', '$/')
164 :gsub('\\\\%$%$/$', '\\$$')
165 end
166
167 --[[
168 - Can't test vis:command() as it will still return true if the edit command fails.
169 - Can't test File.modified as the edit command can succeed if the current file is
170 modified but open in another window and this behavior is useful.
171 - Instead just check the path again after trying the edit command.
172 ]]
173 local function goto_pos(pos, force)
174 if pos.path ~= vis.win.file.path then
175 vis:command(string.format(force and 'e! "%s"' or 'e "%s"', pos.path))
176 if pos.path ~= vis.win.file.path then
177 return false
178 end
179 end
180 if tonumber(pos.excmd) then
181 vis.win.selection:to(pos.excmd, pos.col)
182 else
183 vis.win.selection:to(1, 1)
184 vis:command(escape(pos.excmd))
185 vis.win.selection.pos = vis.win.selection.range.start
186 vis.mode = vis.modes.NORMAL
187 end
188 return true
189 end
190
191 local function goto_tag(path, excmd, force)
192 local old = {
193 path = vis.win.file.path,
194 excmd = vis.win.selection.line,
195 col = vis.win.selection.col,
196 }
197
198 local last_search = vis.registers['/']
199 if goto_pos({ path = path, excmd = excmd, col = 1 }, force) then
200 positions[#positions + 1] = old
201 vis.registers['/'] = last_search
202 end
203 end
204
205 local function pop_pos(force)
206 if #positions < 1 then
207 return
208 end
209 if goto_pos(positions[#positions], force) then
210 table.remove(positions, #positions)
211 end
212 end
213
214 local function win_path()
215 if vis.win.file.path == nil then
216 return os.getenv('PWD') .. '/'
217 end
218 return vis.win.file.path
219 end
220
221 local function tag_cmd(tag, force)
222 local match = get_match(tag, win_path())
223 if match == nil then
224 vis:info(string.format('Tag not found: %s', tag))
225 else
226 goto_tag(match.path, match.excmd, force)
227 end
228 end
229
230 local function gen_vis_menu(matches)
231 local width = 0
232 for _, match in ipairs(matches) do
233 width = math.max(width, match.desc:len())
234 end
235 -- limit max width of desc field (filename) in menu
236 width = math.min(width, 40)
237 local fmt = '%' .. #tostring(#matches) .. 'd %-' .. width .. 's %s'
238
239 local lines = {}
240 for i, match in ipairs(matches) do
241 local desc = match.desc
242 if desc:len() > width then
243 desc = '...' .. desc:sub(desc:len() - width + 4)
244 end
245
246 -- work around bug displaying tabs in vis-menu and
247 -- provide a clearer context
248 local excmd = match.excmd:gsub('%s+', ' ')
249 excmd = excmd:gsub('^/^', '')
250 excmd = excmd:gsub('$/$', '')
251 table.insert(lines, fmt:format(i, desc, excmd))
252 end
253
254 -- limit vis-menu height to ~1/4 the window height
255 -- +1 gives an empty line at bottom to signify
256 -- that there are no more lines to scroll through
257 local nlines = math.min(math.floor(vis.win.height / 4), #lines)
258 if nlines == #lines then
259 nlines = nlines + 1
260 end
261 return 'vis-menu -l ' .. nlines .. " -p 'Choose tag:' << 'EOF'\n" .. table.concat(lines, '\n') .. '\n' .. 'EOF'
262 end
263
264 local function tselect_cmd(tag, force)
265 local matches = get_matches(tag, win_path())
266 if matches == nil then
267 vis:info(string.format('Tag not found: %s', tag))
268 else
269 local status, output = vis:pipe(vis.win.file, { start = 0, finish = 0 }, gen_vis_menu(matches))
270
271 if status ~= 0 then
272 vis:info('Command failed')
273 return
274 end
275
276 local choice = tonumber(string.match(output, '%d+'))
277 if choice == nil or choice < 1 or choice > #matches then
278 vis:info('Invalid choice')
279 return
280 end
281 goto_tag(matches[choice].path, matches[choice].excmd, force)
282 end
283 end
284
285 vis:command_register('tag', function(argv, force, win, selection, range)
286 if #argv == 1 then
287 tag_cmd(argv[1], force)
288 end
289 end)
290
291 vis:command_register('tselect', function(argv, force, win, selection, range)
292 if #argv == 1 then
293 tselect_cmd(argv[1], force)
294 end
295 end)
296
297 vis:command_register('pop', function(argv, force, win, selection, range)
298 pop_pos(force)
299 end)
300
301 vis:option_register('tags', 'string', function(value)
302 tags = {}
303 for str in value:gmatch('([^%s]+)') do
304 table.insert(tags, str)
305 end
306 end, 'Paths to search for tags (separated by spaces)')
307
308 ctags.actions.tag = function(keys)
309 local query = get_query()
310 local force = false
311 if query ~= nil then
312 tag_cmd(query, force)
313 end
314 return 0
315 end
316
317 ctags.actions.tselect = function(keys)
318 local query = get_query()
319 local force = false
320 if query ~= nil then
321 tselect_cmd(query, force)
322 end
323 return 0
324 end
325
326 ctags.actions.pop = function(keys)
327 pop_pos()
328 return 0
329 end
330
331 vis:map(vis.modes.NORMAL, '<C-]>', ctags.actions.tag)
332
333 vis:map(vis.modes.NORMAL, 'g<C-]>', ctags.actions.tselect)
334
335 vis:map(vis.modes.NORMAL, '<C-t>', ctags.actions.pop)
336
337 return ctags