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