vis-config

lua scripts to configure vis editor

git clone https://9o.is/git/vis-config.git

util.lua

(8586B)


      1 --- Module containing simple utility functions for vis-lspc/
      2 -- @module util
      3 -- @author Florian Fischer
      4 -- @author git-bruh <prathamIN@proton.me>
      5 -- @license GPL-3
      6 -- @copyright 2024 Florian Fischer
      7 -- @copyright 2024 git-bruh <prathamIN@proton.me>
      8 local util = {}
      9 
     10 local source_str = debug.getinfo(1, 'S').source:sub(2)
     11 local source_path = source_str:match('(.*/)')
     12 
     13 local lspc
     14 
     15 function util.init(lspc_)
     16   lspc = lspc_
     17   return util
     18 end
     19 
     20 --- Execute a command and capture its output.
     21 -- @param cmd the command to execute
     22 -- @return the output of the command written to stdout
     23 function util.capture_cmd(cmd)
     24   local p = assert(io.popen(cmd, 'r'))
     25   local s = assert(p:read('*a'))
     26   local success, _, status = p:close()
     27   if not success then
     28     local err = cmd .. ' failed with exit code: ' .. status
     29     lspc:err(err)
     30   end
     31   return s
     32 end
     33 
     34 local vis_supports_pipe_buf = pcall(vis.pipe, vis, 'foo', 'true', false)
     35 
     36 --- Wrapper for the two vis:pipe variants.
     37 -- If vis does not support vis:pipe(input, cmd), prefix the command
     38 -- with a printf call piping the result to the original command.
     39 -- @param input The input to pipe to the command
     40 -- @param cmd The external command to pipe the input to
     41 function util.vis_pipe(input, cmd, fullscreen)
     42   if vis_supports_pipe_buf then
     43     return vis:pipe(input, cmd, fullscreen or false)
     44   end
     45 
     46   local escaped_input = input:gsub('\'', '\'"\'"\'')
     47   cmd = 'printf %s \'' .. escaped_input .. '\' | ' .. cmd
     48   return vis:pipe(vis.win.file, {start = 0, finish = 0}, cmd)
     49 end
     50 
     51 --- Split a path into its components
     52 -- @param path the path to split into components
     53 -- @return a table containing the path components
     54 function util.split_path_into_components(path)
     55   local components = {}
     56 
     57   if #path == 1 then
     58     return nil
     59   end
     60 
     61   -- Skip the initial '/'
     62   local start_idx = 2
     63 
     64   while true do
     65     local slash = path:find('/', start_idx + 1)
     66 
     67     if slash == nil then
     68       table.insert(components, path:sub(start_idx, #path))
     69       return components
     70     else
     71       table.insert(components, path:sub(start_idx, slash - 1))
     72       start_idx = slash + 1
     73     end
     74   end
     75 end
     76 
     77 --- Get a path relative to the current working directory
     78 -- @param cwd_components Table of the path components of the CWD
     79 -- @param absolute_path_or_components absolute path or table of its path components
     80 -- @return the relative path
     81 function util.get_relative_path(cwd_components, absolute_path_or_components)
     82   local absolute_components
     83   if type(absolute_path_or_components) == 'string' then
     84     absolute_components = util.split_path_into_components(absolute_path_or_components)
     85   else
     86     absolute_components = absolute_path_or_components
     87   end
     88 
     89   for idx = 1, #cwd_components do
     90     local cwd = cwd_components[idx]
     91     local absolute = absolute_components[idx]
     92 
     93     if cwd ~= absolute then
     94       local dir = ''
     95 
     96       -- Atleast the first component must match for us to convert
     97       -- it to a relative path
     98       if idx ~= 1 then
     99         for _ = idx, #cwd_components do
    100           dir = dir .. '..' .. '/'
    101         end
    102 
    103         -- Skip trailing '/'
    104         dir = dir:sub(1, #dir - 1)
    105       end
    106 
    107       for i = idx, #absolute_components do
    108         dir = dir .. '/' .. absolute_components[i]
    109       end
    110 
    111       return dir
    112     end
    113   end
    114 
    115   -- cwd shorter than absolute path
    116   local dir = ''
    117 
    118   for i = #cwd_components + 1, #absolute_components do
    119     dir = dir .. '/' .. absolute_components[i]
    120   end
    121 
    122   -- Skip leading '/'
    123   return dir:sub(2)
    124 end
    125 
    126 --- Strip the last component from a pathname
    127 -- @param the pathname
    128 -- @return the pathname up to the last '/'
    129 function util.dirname(name)
    130   if name == '.' or name == '..' or name == '/' then
    131     return name
    132   end
    133 
    134   -- strip a trailing path separator
    135   if name:sub(#name, #name) == '/' then
    136     name = name:sub(1, #name - 1)
    137   end
    138 
    139   local dirname = name:match('(.*)[/]')
    140   -- There was no path separator in name.
    141   if not dirname then
    142     return '.'
    143   end
    144 
    145   -- The name started with the root dir.
    146   if dirname == '' then
    147     return '/'
    148   end
    149 
    150   return dirname
    151 end
    152 
    153 --- Create an iterator yielding the nth line of a file
    154 --
    155 -- @param path The path to the file
    156 function util.file_line_iterator_to_n(path)
    157   local file = assert(io.open(path, 'r'))
    158   local lines = file:lines()
    159   local last_line = nil
    160   local last_n = 1
    161 
    162   return function(n)
    163     if n == -1 then
    164       file:close()
    165       return nil
    166     end
    167 
    168     if n < last_n then
    169       -- We might have multiple references on the same line, so we can
    170       -- get called again with the previous line number
    171       if (n + 1) == last_n then
    172         return last_line
    173       end
    174 
    175       return nil
    176     end
    177 
    178     for line in lines do
    179       if n == last_n then
    180         last_n = last_n + 1
    181         last_line = line
    182 
    183         return line
    184       end
    185 
    186       last_n = last_n + 1
    187     end
    188 
    189     -- Iterator exhausted
    190     return nil
    191   end
    192 end
    193 
    194 --- Find file based on globs in the parent file system tree
    195 -- @param globs a new line separated string of file globs
    196 -- @param start the starting path
    197 function util.find_upwards(globs, start)
    198   local status, out = util.vis_pipe(globs, '\'' .. source_path:gsub('\'', '\'\\\'\'') ..
    199                                         '/tools/find-upwards\' "' .. start .. '"')
    200 
    201   if status ~= 0 or out == nil then
    202     return nil
    203   end
    204 
    205   -- Skip trailing newline
    206   return out:sub(1, #out - 1)
    207 end
    208 
    209 -- get the vis_selection from current primary selection
    210 local function get_selection(win)
    211   return {line = win.selection.line, col = win.selection.col}
    212 end
    213 
    214 --- Calculate the 0-based byte offsets from multiple sorted selections
    215 -- @param file the file to calculate the positions in
    216 -- @param sorted_selections a table of sorted vis_selections
    217 -- @return a table of positions
    218 function util.vis_sorted_selections_to_pos(file, sorted_selections)
    219   local positions = {}
    220   if file.pos_by_linecol then
    221     for _, sel in ipairs(sorted_selections) do
    222       table.insert(positions, file:pos_by_linecol(sel.line, sel.col))
    223     end
    224     return positions
    225   end
    226 
    227   local line_count = 0
    228   local pos = 0
    229   local sel_i = 1
    230   local sel = sorted_selections[sel_i]
    231   for line in file:lines_iterator() do
    232     line_count = line_count + 1
    233     while line_count == sel.line do
    234       table.insert(positions, pos + (sel.col - 1))
    235       sel_i = sel_i + 1
    236       -- no more selections to convert
    237       if sel_i > #sorted_selections then
    238         break
    239       end
    240       sel = sorted_selections[sel_i]
    241     end
    242 
    243     pos = pos + #line + 1
    244   end
    245   return positions
    246 end
    247 
    248 local function vis_pos_before(p1, p2)
    249   return p1.line < p2.line or (p1.line == p2.line and p1.col < p2.col)
    250 end
    251 
    252 --- Calculate the 0-based byte offsets from multiple selections
    253 -- @param file the file to calculate the positions in
    254 -- @param selections a table of selections
    255 -- @return a table of positions
    256 function util.vis_selections_to_pos(file, selections)
    257   table.sort(selections, vis_pos_before)
    258   return util.vis_sorted_selections_to_pos(file, selections)
    259 end
    260 
    261 --- Get the line and column from a 0-based byte offset
    262 -- ATTENTION: the fallback version of this function modifies the primary
    263 -- selection so it is not safe to call it for example during WIN_HIGHLIGHT events
    264 -- @param pos the 0-based byte offset into the file
    265 -- @return the 1-based line number
    266 -- @return the 1-based column
    267 function util.vis_pos_to_sel(win, pos)
    268   if win.file.linecol_by_pos then
    269     local lineno, col = win.file:linecol_by_pos(pos)
    270     return {line = lineno, col = col}
    271   end
    272 
    273   local old_selection = get_selection(win)
    274   -- move primary selection
    275   win.selection.pos = pos
    276   local sel = get_selection(win)
    277   -- restore old primary selection
    278   win.selection:to(old_selection.line, old_selection.col)
    279   return sel
    280 end
    281 
    282 --- Count the visual characters in a line
    283 -- This is useful to detect wrapped lines including tabs which may add a
    284 -- unspecified amount of white space to a line depending on their position and
    285 -- the used tabwidth.
    286 -- @param win the window containing the line
    287 -- @param line the line to count
    288 -- @param nchars the number of characters in the line
    289 -- @return the number of visual characters
    290 util.visual_chars_in_line = function(win, line, nchars)
    291   -- fast string iteration inspired by:
    292   -- https://stackoverflow.com/a/49222705
    293   local l = {string.byte(line, 1, nchars)}
    294   local line_len = 0
    295   for i = 1, nchars do
    296     local c = l[i] -- Note: produces char codes instead of chars.
    297     if c == 9 then -- '\t'
    298       local chars_to_tab_stop = win.options.tabwidth - (line_len % win.options.tabwidth)
    299       line_len = line_len + chars_to_tab_stop
    300     else
    301       line_len = line_len + 1
    302     end
    303   end
    304   return line_len
    305 end
    306 
    307 return util