From d33d907f53c50d323eca75c4bfc02ab5b989b30a Mon Sep 17 00:00:00 2001 From: dacctal Date: Mon, 27 Apr 2026 05:30:46 +0000 Subject: added bare plugins --- .config/vis/plugins/vis-lspc/util.lua | 370 ++++++++++++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 .config/vis/plugins/vis-lspc/util.lua (limited to '.config/vis/plugins/vis-lspc/util.lua') diff --git a/.config/vis/plugins/vis-lspc/util.lua b/.config/vis/plugins/vis-lspc/util.lua new file mode 100644 index 0000000..265c272 --- /dev/null +++ b/.config/vis/plugins/vis-lspc/util.lua @@ -0,0 +1,370 @@ +--- Module containing simple utility functions for vis-lspc/ +-- @module util +-- @author Florian Fischer +-- @author git-bruh +-- @license GPL-3 +-- @copyright 2024-2025 Florian Fischer +-- @copyright 2024 git-bruh +local util = {} + +local source_str = debug.getinfo(1, 'S').source:sub(2) +local source_path = source_str:match('(.*/)') + +local lspc + +function util.init(lspc_) + lspc = lspc_ + return util +end + +--- Execute a command and capture its output. +-- @param cmd the command to execute +-- @return the output of the command written to stdout +function util.capture_cmd(cmd) + local p = assert(io.popen(cmd, 'r')) + local s = assert(p:read('*a')) + local success, _, status = p:close() + if not success then + local err = cmd .. ' failed with exit code: ' .. status + lspc:err(err) + end + return s +end + +local vis_supports_pipe_buf = pcall(vis.pipe, vis, 'foo', 'true', false) + +--- Wrapper for the two vis:pipe variants. +-- If vis does not support vis:pipe(input, cmd), prefix the command +-- with a printf call piping the result to the original command. +-- @param input The input to pipe to the command +-- @param cmd The external command to pipe the input to +function util.vis_pipe(input, cmd, fullscreen) + if vis_supports_pipe_buf then + return vis:pipe(input, cmd, fullscreen or false) + end + + local escaped_input = input:gsub('\'', '\'"\'"\'') + cmd = 'printf %s \'' .. escaped_input .. '\' | ' .. cmd + return vis:pipe(vis.win.file, {start = 0, finish = 0}, cmd) +end + +--- Split a path into its components +-- @param path the path to split into components +-- @return a table containing the path components +function util.split_path_into_components(path) + local components = {} + + if #path == 1 then + return nil + end + + -- Skip the initial '/' + local start_idx = 2 + + while true do + local slash = path:find('/', start_idx + 1) + + if slash == nil then + table.insert(components, path:sub(start_idx, #path)) + return components + else + table.insert(components, path:sub(start_idx, slash - 1)) + start_idx = slash + 1 + end + end +end + +--- Get a path relative to the current working directory +-- @param cwd_components Table of the path components of the CWD +-- @param absolute_path_or_components absolute path or table of its path components +-- @return the relative path +function util.get_relative_path(cwd_components, absolute_path_or_components) + local absolute_components + if type(absolute_path_or_components) == 'string' then + absolute_components = util.split_path_into_components(absolute_path_or_components) + else + absolute_components = absolute_path_or_components + end + + for idx = 1, #cwd_components do + local cwd = cwd_components[idx] + local absolute = absolute_components[idx] + + if cwd ~= absolute then + local dir = '' + + -- Atleast the first component must match for us to convert + -- it to a relative path + if idx ~= 1 then + for _ = idx, #cwd_components do + dir = dir .. '..' .. '/' + end + + -- Skip trailing '/' + dir = dir:sub(1, #dir - 1) + end + + for i = idx, #absolute_components do + dir = dir .. '/' .. absolute_components[i] + end + + return dir + end + end + + -- cwd shorter than absolute path + local dir = '' + + for i = #cwd_components + 1, #absolute_components do + dir = dir .. '/' .. absolute_components[i] + end + + -- Skip leading '/' + return dir:sub(2) +end + +--- Strip the last component from a pathname +-- @param the pathname +-- @return the pathname up to the last '/' +function util.dirname(name) + if name == '.' or name == '..' or name == '/' then + return name + end + + -- strip a trailing path separator + if name:sub(#name, #name) == '/' then + name = name:sub(1, #name - 1) + end + + local dirname = name:match('(.*)[/]') + -- There was no path separator in name. + if not dirname then + return '.' + end + + -- The name started with the root dir. + if dirname == '' then + return '/' + end + + return dirname +end + +--- Create an iterator yielding the nth line of a file +-- +-- @param path The path to the file +function util.file_line_iterator_to_n(path) + local file = assert(io.open(path, 'r')) + local lines = file:lines() + local last_line = nil + local last_n = 1 + + return function(n) + if n == -1 then + file:close() + return nil + end + + if n < last_n then + -- We might have multiple references on the same line, so we can + -- get called again with the previous line number + if (n + 1) == last_n then + return last_line + end + + return nil + end + + for line in lines do + if n == last_n then + last_n = last_n + 1 + last_line = line + + return line + end + + last_n = last_n + 1 + end + + -- Iterator exhausted + return nil + end +end + +--- Find file based on globs in the parent file system tree +-- @param globs a new line separated string of file globs +-- @param start the starting path +function util.find_upwards(globs, start) + local status, out = util.vis_pipe(globs, '\'' .. source_path:gsub('\'', '\'\\\'\'') .. + '/tools/find-upwards\' "' .. start .. '"') + + if status ~= 0 or out == nil then + return nil + end + + -- Skip trailing newline + return out:sub(1, #out - 1) +end + +-- get the vis_selection from current primary selection +local function get_selection(win) + return {line = win.selection.line, col = win.selection.col} +end + +--- Calculate the 0-based byte offsets from multiple sorted selections +-- @param file the file to calculate the positions in +-- @param sorted_selections a table of sorted vis_selections +-- @return a table of positions +function util.vis_sorted_selections_to_pos(file, sorted_selections) + local positions = {} + if file.pos_by_linecol then + for _, sel in ipairs(sorted_selections) do + table.insert(positions, file:pos_by_linecol(sel.line, sel.col)) + end + return positions + end + + local line_count = 0 + local pos = 0 + local sel_i = 1 + local sel = sorted_selections[sel_i] + for line in file:lines_iterator() do + line_count = line_count + 1 + while line_count == sel.line do + table.insert(positions, pos + (sel.col - 1)) + sel_i = sel_i + 1 + -- no more selections to convert + if sel_i > #sorted_selections then + break + end + sel = sorted_selections[sel_i] + end + + pos = pos + #line + 1 + end + + -- Some language servers (pylsp) send ranges including the first character after the last line. + -- e.g. [{"line":1, "col":1}, {"line":#lines+1, "col":1}] + -- But this selection can not be handled by iterating all lines. + -- Special case ranges ensing after the last line. + -- Additionaly the start and end can overlap. + -- Gopls sends the range [{"line":1, "col":1}, {"line":1, "col":1}] when editing an empty file. + while sel and sel.line == line_count + 1 and sel.col == 1 do + table.insert(positions, pos) + sel_i = sel_i + 1 + sel = sorted_selections[sel_i] + end + return positions +end + +local function vis_pos_before(p1, p2) + return p1.line < p2.line or (p1.line == p2.line and p1.col < p2.col) +end + +--- Calculate the 0-based byte offsets from multiple selections +-- @param file the file to calculate the positions in +-- @param selections a table of selections +-- @return a table of positions +function util.vis_selections_to_pos(file, selections) + table.sort(selections, vis_pos_before) + return util.vis_sorted_selections_to_pos(file, selections) +end + +--- Get the line and column from a 0-based byte offset +-- ATTENTION: the fallback version of this function modifies the primary +-- selection so it is not safe to call it for example during WIN_HIGHLIGHT events +-- @param pos the 0-based byte offset into the file +-- @return the 1-based line number +-- @return the 1-based column +function util.vis_pos_to_sel(win, pos) + if win.file.linecol_by_pos then + local lineno, col = win.file:linecol_by_pos(pos) + return {line = lineno, col = col} + end + + local old_selection = get_selection(win) + -- move primary selection + win.selection.pos = pos + local sel = get_selection(win) + -- restore old primary selection + win.selection:to(old_selection.line, old_selection.col) + return sel +end + +--- Count the visual characters in a line +-- This is useful to detect wrapped lines including tabs which may add a +-- unspecified amount of white space to a line depending on their position and +-- the used tabwidth. +-- @param win the window containing the line +-- @param line the line to count +-- @param nchars the number of characters in the line +-- @return the number of visual characters +util.visual_chars_in_line = function(win, line, nchars) + -- fast string iteration inspired by: + -- https://stackoverflow.com/a/49222705 + local l = {string.byte(line, 1, nchars)} + local line_len = 0 + for i = 1, nchars do + local c = l[i] -- Note: produces char codes instead of chars. + if c == 9 then -- '\t' + local chars_to_tab_stop = win.options.tabwidth - (line_len % win.options.tabwidth) + line_len = line_len + chars_to_tab_stop + else + line_len = line_len + 1 + end + end + return line_len +end + +--- Utility table function +util.table = {} + +--- Recursively copy a table and all its members. +-- @param tbl the table to copy +-- @return a deep copy of the table +function util.table.deep_copy(tbl) + local cpy = {} + for k, v in pairs(tbl) do + if type(v) ~= 'table' then + cpy[k] = v + else + cpy[k] = util.table.deep_copy(v) + end + end + + for i, v in ipairs(tbl) do + if type(v) ~= 'table' then + cpy[i] = v + else + cpy[i] = util.table.deep_copy(v) + end + end + return cpy +end + +--- Recursively merge two tables. +-- This method modifies the weak table, if this is not intended make sure you +-- use a deep copy of the weak table. +-- Named members of the strong table override members of the weak one. +-- Numbered members get inserted into the weak table. +-- @param weak table which's members are potentially overridden +-- @param strong table which's members override ones from the weak table +-- @return the merged table +function util.table.merge(weak, strong) + for k, v in pairs(strong) do + if type(v) ~= 'table' then + if tonumber(k) then -- append numeric members + table.insert(weak, v) + else -- potentially override the weak member + weak[k] = v + end + + else + weak[k] = util.table.merge(weak[k] or {}, v) + end + end + + return weak +end + +return util -- cgit v1.2.3