aboutsummaryrefslogtreecommitdiff
path: root/.config/vis/plugins/vis-lspc/util.lua
diff options
context:
space:
mode:
authordacctal <dacctal@symlinx.net>2026-04-27 05:30:46 +0000
committerdacctal <dacctal@symlinx.net>2026-04-27 05:30:46 +0000
commitd33d907f53c50d323eca75c4bfc02ab5b989b30a (patch)
tree6888a2074338a723f0fde99b03b09fbab91a265f /.config/vis/plugins/vis-lspc/util.lua
parent89505535c652cd6f31a15df73293c6e90eaa852f (diff)
added bare pluginsHEADmaster
Diffstat (limited to '.config/vis/plugins/vis-lspc/util.lua')
-rw-r--r--.config/vis/plugins/vis-lspc/util.lua370
1 files changed, 370 insertions, 0 deletions
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 <prathamIN@proton.me>
+-- @license GPL-3
+-- @copyright 2024-2025 Florian Fischer
+-- @copyright 2024 git-bruh <prathamIN@proton.me>
+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