-- help to inspect results, e.g.: -- ':lua _G.dump(vim.fn.getwininfo())' -- use ':messages' to see the dump function _G.dump(...) local objects = vim.tbl_map(vim.inspect, { ... }) print(unpack(objects)) end local M = {} -- invisible unicode char as icon|git separator -- this way we can split our string by space -- this causes "invalid escape sequence" error -- local nbsp = "\u{00a0}" M.nbsp = " " M._if = function(bool, a, b) if bool then return a else return b end end M.strsplit = function(inputstr, sep) local t={} for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end return t end function M.round(num, limit) if not num then return nil end if not limit then limit = 0.5 end local fraction = num - math.floor(num) if fraction > limit then return math.ceil(num) end return math.floor(num) end function M.nvim_has_option(option) return vim.fn.exists('&' .. option) == 1 end function M._echo_multiline(msg) for _, s in ipairs(vim.fn.split(msg, "\n")) do vim.cmd("echom '" .. s:gsub("'", "''").."'") end end function M.info(msg) vim.cmd('echohl Directory') M._echo_multiline("[Fzf-lua] " .. msg) vim.cmd('echohl None') end function M.warn(msg) vim.cmd('echohl WarningMsg') M._echo_multiline("[Fzf-lua] " .. msg) vim.cmd('echohl None') end function M.err(msg) vim.cmd('echohl ErrorMsg') M._echo_multiline("[Fzf-lua] " .. msg) vim.cmd('echohl None') end function M.shell_error() return vim.v.shell_error ~= 0 end function M.rg_escape(str) if not str then return str end -- [(~'"\/$?'`*&&||;[]<>)] -- escape "\~$?*|[()" return str:gsub('[\\~$?*|{\\[()-]', function(x) return '\\' .. x end) end function M.sk_escape(str) if not str then return str end return str:gsub('["`]', function(x) return '\\' .. x end) end -- TODO: why does `file --dereference --mime` return -- wrong result for some lua files ('charset=binary')? M.file_is_binary = function(filepath) filepath = vim.fn.expand(filepath) if vim.fn.executable("file") ~= 1 or not vim.loop.fs_stat(filepath) then return false end local out = M.io_system("file --dereference --mime " .. vim.fn.shellescape(filepath)) return out:match("charset=binary") ~= nil end M.perl_file_is_binary = function(filepath) filepath = vim.fn.expand(filepath) if vim.fn.executable("perl") ~= 1 or not vim.loop.fs_stat(filepath) then return false end -- can also use '-T' to test for text files -- `perldoc -f -x` to learn more about '-B|-T' M.io_system("perl -E 'exit((-B $ARGV[0])?0:1);' " .. vim.fn.shellescape(filepath)) return not M.shell_error() end M.read_file = function(filepath) local fd = vim.loop.fs_open(filepath, "r", 438) if fd == nil then return '' end local stat = assert(vim.loop.fs_fstat(fd)) if stat.type ~= 'file' then return '' end local data = assert(vim.loop.fs_read(fd, stat.size, 0)) assert(vim.loop.fs_close(fd)) return data end M.read_file_async = function(filepath, callback) vim.loop.fs_open(filepath, "r", 438, function(err_open, fd) if err_open then M.warn("We tried to open this file but couldn't. We failed with following error message: " .. err_open) return end vim.loop.fs_fstat(fd, function(err_fstat, stat) assert(not err_fstat, err_fstat) if stat.type ~= 'file' then return callback('') end vim.loop.fs_read(fd, stat.size, 0, function(err_read, data) assert(not err_read, err_read) vim.loop.fs_close(fd, function(err_close) assert(not err_close, err_close) return callback(data) end) end) end) end) end function M.tbl_deep_clone(t) if not t then return end local clone = {} for k, v in pairs(t) do if type(v) == "table" then clone[k] = M.tbl_deep_clone(v) else clone[k] = v end end return clone end function M.tbl_length(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count end function M.tbl_isempty(T) if not T or not next(T) then return true end return false end function M.tbl_concat(...) local result = {} local n = 0 for _, t in ipairs({...}) do for i, v in ipairs(t) do result[n + i] = v end n = n + #t end return result end function M.tbl_pack(...) return {n=select('#',...); ...} end function M.tbl_unpack(t, i, j) return unpack(t, i or 1, j or t.n or #t) end M.ansi_codes = {} M.ansi_colors = { -- the "\x1b" esc sequence causes issues -- with older Lua versions -- clear = "\x1b[0m", clear = "", bold = "", black = "", red = "", green = "", yellow = "", blue = "", magenta = "", cyan = "", grey = "", dark_grey = "", white = "", } M.add_ansi_code = function(name, escseq) M.ansi_codes[name] = function(string) if string == nil or #string == 0 then return '' end return escseq .. string .. M.ansi_colors.clear end end for color, escseq in pairs(M.ansi_colors) do M.add_ansi_code(color, escseq) end function M.strip_ansi_coloring(str) if not str then return str end -- remove escape sequences of the following formats: -- 1. ^[[34m -- 2. ^[[0;34m return str:gsub("%[[%d;]+m", "") end function M.get_visual_selection() -- this will exit visual mode -- use 'gv' to reselect the text local _, csrow, cscol, cerow, cecol local mode = vim.fn.mode() if mode == 'v' or mode == 'V' or mode == '' then -- if we are in visual mode use the live position _, csrow, cscol, _ = unpack(vim.fn.getpos(".")) _, cerow, cecol, _ = unpack(vim.fn.getpos("v")) if mode == 'V' then -- visual line doesn't provide columns cscol, cecol = 0, 999 end -- exit visual mode vim.api.nvim_feedkeys( vim.api.nvim_replace_termcodes("", true, false, true), 'n', true) else -- otherwise, use the last known visual position _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) end -- swap vars if needed if cerow < csrow then csrow, cerow = cerow, csrow end if cecol < cscol then cscol, cecol = cecol, cscol end local lines = vim.fn.getline(csrow, cerow) -- local n = cerow-csrow+1 local n = M.tbl_length(lines) if n <= 0 then return '' end lines[n] = string.sub(lines[n], 1, cecol) lines[1] = string.sub(lines[1], cscol) return table.concat(lines, "\n") end function M.send_ctrl_c() vim.api.nvim_feedkeys( vim.api.nvim_replace_termcodes("", true, false, true), 'n', true) end function M.feed_keys_termcodes(key) vim.api.nvim_feedkeys( vim.api.nvim_replace_termcodes(key, true, false, true), 'n', true) end function M.delayed_cb(cb, fn) -- HACK: slight delay to prevent missing results -- otherwise the input stream closes too fast -- sleep was causing all sorts of issues -- vim.cmd("sleep! 10m") if fn == nil then fn = function() end end vim.defer_fn(function() cb(nil, fn) end, 20) end function M.is_term_bufname(bufname) if bufname and bufname:match("term://") then return true end return false end function M.is_term_buffer(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr or 0) return M.is_term_bufname(bufname) end function M.winid_from_tab_buf(tabnr, bufnr) for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tabnr)) do if bufnr == vim.api.nvim_win_get_buf(w) then return w end end return nil end function M.zz() -- skip for terminal buffers if M.is_term_buffer() then return end local lnum1 = vim.api.nvim_win_get_cursor(0)[1] local lcount = vim.api.nvim_buf_line_count(0) local zb = 'keepj norm! %dzb' if lnum1 == lcount then vim.fn.execute(zb:format(lnum1)) return end vim.cmd('norm! zvzz') lnum1 = vim.api.nvim_win_get_cursor(0)[1] vim.cmd('norm! L') local lnum2 = vim.api.nvim_win_get_cursor(0)[1] if lnum2 + vim.fn.getwinvar(0, '&scrolloff') >= lcount then vim.fn.execute(zb:format(lnum2)) end if lnum1 ~= lnum2 then vim.cmd('keepj norm! ``') end end function M.win_execute(winid, func) vim.validate({ winid = { winid, function(w) return w and vim.api.nvim_win_is_valid(w) end, 'a valid window' }, func = {func, 'function'} }) local cur_winid = vim.api.nvim_get_current_win() local noa_set_win = 'noa call nvim_set_current_win(%d)' if cur_winid ~= winid then vim.cmd(noa_set_win:format(winid)) end local ret = func() if cur_winid ~= winid then vim.cmd(noa_set_win:format(cur_winid)) end return ret end function M.ft_detect(ext) local ft = '' if not ext then return ft end local tmp_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_option(tmp_buf, 'bufhidden', 'wipe') pcall(vim.api.nvim_buf_call, tmp_buf, function() local filename = (vim.fn.tempname() .. '.' .. ext) vim.cmd("file " .. filename) vim.cmd("doautocmd BufEnter") vim.cmd("filetype detect") ft = vim.api.nvim_buf_get_option(tmp_buf, 'filetype') end) if vim.api.nvim_buf_is_valid(tmp_buf) then vim.api.nvim_buf_delete(tmp_buf, {force=true}) end return ft end -- speed up exteral commands (issue #126) local _use_lua_io = false function M.set_lua_io(b) _use_lua_io = b if _use_lua_io then M.warn("using experimental feature 'lua_io'") end end function M.io_systemlist(cmd, use_lua_io) if not use_lua_io then use_lua_io = _use_lua_io end if use_lua_io then local rc = 0 local stdout = '' local handle = io.popen(cmd .. " 2>&1; echo $?", "r") if handle then stdout = {} for h in handle:lines() do stdout[#stdout + 1] = h end -- last line contains the exit status rc = tonumber(stdout[#stdout]) stdout[#stdout] = nil end handle:close() return stdout, rc else return vim.fn.systemlist(cmd), vim.v.shell_error end end function M.io_system(cmd, use_lua_io) if not use_lua_io then use_lua_io = _use_lua_io end if use_lua_io then local stdout, rc = M.io_systemlist(cmd, true) if type(stdout) == 'table' then stdout = table.concat(stdout, "\n") end return stdout, rc else return vim.fn.system(cmd), vim.v.shell_error end end function M.fzf_bind_to_neovim(key) local conv_map = { ['alt'] = 'A', ['ctrl'] = 'C', ['shift'] = 'S', } key = key:lower() for k, v in pairs(conv_map) do key = key:gsub(k, v) end return ("<%s>"):format(key) end return M