You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

674 lines
18 KiB
Lua

3 years ago
-- help to inspect results, e.g.:
-- ':lua _G.dump(vim.fn.getwininfo())'
-- use ':messages' to see the dump
3 years ago
function _G.dump(...)
local objects = vim.tbl_map(vim.inspect, { ... })
print(unpack(objects))
end
local M = {}
function M.__FILE__() return debug.getinfo(2, 'S').source end
function M.__LINE__() return debug.getinfo(2, 'l').currentline end
function M.__FNC__() return debug.getinfo(2, 'n').name end
function M.__FNCREF__() return debug.getinfo(2, 'f').func end
-- sets an invisible unicode character as icon seaprator
-- the below was reached after many iterations, a short summary of everything
-- that was tried and why it failed:
--
-- nbsp, U+00a0: the original separator, fails with files that contain nbsp
-- nbsp + zero-width space (U+200b): works only with `sk` (`fzf` shows <200b>)
-- word joiner (U+2060): display works fine, messes up fuzzy search highlights
-- line separator (U+2028), paragraph separator (U+2029): created extra space
-- EN space (U+2002): seems to work well
--
-- For more unicode SPACE options see:
-- http://unicode-search.net/unicode-namesearch.pl?term=SPACE&.submit=Search
-- DO NOT USE '\u{}' escape, it will fail with
-- "invalid escape sequence" if Lua < 5.3
-- '\x' escape sequence requires Lua 5.2
-- M.nbsp = "\xc2\xa0" -- "\u{00a0}"
M.nbsp = "\xe2\x80\x82" -- "\u{2002}"
-- Lua 5.1 compatibility, not sure if required since we're running LuaJIT
-- but it's harmless anyways since if the '\x' escape worked it will do nothing
-- https://stackoverflow.com/questions/29966782/how-to-embed-hex-values-in-a-lua-string-literal-i-e-x-equivalent
if _VERSION and type(_VERSION) == 'string' then
local ver= tonumber(_VERSION:match("%d+.%d+"))
if ver< 5.2 then
M.nbsp = M.nbsp:gsub("\\x(%x%x)",
function (x) return string.char(tonumber(x,16))
end)
end
end
3 years ago
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
local string_byte = string.byte
M.find_last_char = function(str, c)
for i=#str,1,-1 do
if string_byte(str, i) == c then
return i
end
end
end
M.find_next_char = function(str, c, start_idx)
for i=start_idx or 1,#str do
if string_byte(str, i) == c then
return i
end
end
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
3 years ago
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):gsub([[\\]], [[\\\\]]):gsub([[\%$]], [[\\\$]])
end
function M.lua_escape(str)
if not str then return str end
return str:gsub('[%%]', function(x)
return '%' .. x
end)
end
function M.lua_regex_escape(str)
-- escape all lua special chars
-- ( ) % . + - * [ ? ^ $
if not str then return nil end
return str:gsub('[%(%)%.%+%-%*%[%?%^%$%%]', function(x)
return '%' .. x
end)
end
function M.pcall_expand(filepath)
-- expand using pcall, this is a workaround to trying to
-- expand certain special chars, more info in issue #285
-- expanding the below fails with:
-- "special[1][98f3a7e3-0d6e-f432-8a18-e1144b53633f][-1].xml"
-- "Vim:E944: Reverse range in character class"
-- this seems to fail with only a single hypen:
-- :lua print(vim.fn.expand("~/file[2-1].ext"))
-- but not when escaping the hypen:
-- :lua print(vim.fn.expand("~/file[2\\-1].ext"))
local ok, expanded = pcall(vim.fn.expand,
filepath:gsub("%-", "\\-"))
if ok and expanded and #expanded>0 then
return expanded
else
return filepath
end
end
-- TODO: why does `file --dereference --mime` return
-- wrong result for some lua files ('charset=binary')?
M.file_is_binary = function(filepath)
filepath = M.pcall_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", filepath})
return out:match("charset=binary") ~= nil
end
M.perl_file_is_binary = function(filepath)
filepath = M.pcall_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);', filepath})
return not M.shell_error()
end
3 years ago
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
-- we must schedule this or we get
-- E5560: nvim_exec must not be called in a lua loop callback
vim.schedule(function()
M.warn(("Unable to open file '%s', error: %s"):format(filepath, err_open))
end)
3 years ago
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
-- deepcopy can fail with: "Cannot deepcopy object of type userdata" (#353)
-- this can happen when copying items/on_choice params of vim.ui.select
-- run in a pcall and fallback to our poor man's clone
function M.deepcopy(t)
local ok, res = pcall(vim.deepcopy, t)
if ok then
return res
else
return M.tbl_deep_clone(t)
end
end
3 years ago
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
3 years ago
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 = "",
italic = "",
underline = "",
black = "",
red = "",
green = "",
yellow = "",
blue = "",
magenta = "",
cyan = "",
white = "",
grey = "",
dark_grey = "",
3 years ago
}
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
3 years ago
for color, escseq in pairs(M.ansi_colors) do
M.add_ansi_code(color, escseq)
3 years ago
end
local function hex2rgb(hexcol)
local r,g,b = hexcol:match('#(..)(..)(..)')
if not r or not g or not b then return end
r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
return r, g, b
end
local function synIDattr(hl, w)
return vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(hl)), w)
end
function M.ansi_from_hl(hl, s, colormap)
if vim.fn.hlexists(hl) == 1 then
-- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#rgb-colors
-- Set foreground color as RGB: 'ESC[38;2;{r};{g};{b}m'
-- Set background color as RGB: 'ESC[48;2;{r};{g};{b}m'
local what = {
['fg'] = { rgb = true, code = 38 },
['bg'] = { rgb = true, code = 48 },
['bold'] = { code = 1 },
['italic'] = { code = 3 },
['underline'] = { code = 4 },
['inverse'] = { code = 7 },
['reverse'] = { code = 7 },
['strikethrough'] = { code = 9 },
}
for w, p in pairs(what) do
local escseq = nil
if p.rgb then
local hexcol = synIDattr(hl, w)
if hexcol and not hexcol:match("^#") and colormap then
-- try to acquire the color from the map
-- some schemes don't capitalize first letter?
local col = colormap[hexcol:sub(1,1):upper()..hexcol:sub(2)]
if col then
-- format as 6 digit hex for hex2rgb()
hexcol = ("#%06x"):format(col)
end
end
local r, g, b = hex2rgb(hexcol)
if r and g and b then
escseq = ('[%d;2;%d;%d;%dm'):format(p.code, r, g, b)
-- elseif #hexcol>0 then
-- print("unresolved", hl, w, hexcol, colormap[synIDattr(hl, w)])
end
else
local value = synIDattr(hl, w)
if value and tonumber(value)==1 then
escseq = ('[%dm'):format(p.code)
end
end
if escseq then
s = ("%s%s%s"):format(escseq, s, M.ansi_colors.clear)
end
end
end
return s
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
3 years ago
function M.get_visual_selection()
-- this will exit visual mode
3 years ago
-- 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("<Esc>",
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
3 years ago
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.fzf_exit()
vim.cmd([[lua require('fzf-lua.win').win_leave()]])
end
function M.fzf_winobj()
-- use 'loadstring' to prevent circular require
return loadstring("return require'fzf-lua'.win.__SELF()")()
end
function M.send_ctrl_c()
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<C-c>", 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.is_term_bufname(bufname)
3 years ago
if bufname and bufname:match("term://") then return true end
return false
end
function M.is_term_buffer(bufnr)
bufnr = tonumber(bufnr) or 0
-- convert bufnr=0 to current buf so we can call 'bufwinid'
bufnr = bufnr==0 and vim.api.nvim_get_current_buf() or bufnr
local winid = vim.fn.bufwinid(bufnr)
if tonumber(winid)>0 and vim.api.nvim_win_is_valid(winid) then
return vim.fn.getwininfo(winid)[1].terminal == 1
end
local bufname = vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_get_name(bufnr)
return M.is_term_bufname(bufname)
end
function M.buffer_is_dirty(bufnr, warn)
bufnr = tonumber(bufnr) or vim.api.nvim_get_current_buf()
local info = bufnr and vim.fn.getbufinfo(bufnr)[1]
if info and info.changed ~= 0 then
if warn then
M.warn(('buffer %d has unsaved changes "%s"'):format(bufnr, info.name))
end
return true
end
return false
end
-- returns:
-- 1 for qf list
-- 2 for loc list
function M.win_is_qf(winid, wininfo)
wininfo = wininfo or
(vim.api.nvim_win_is_valid(winid) and vim.fn.getwininfo(winid)[1])
if wininfo and wininfo.quickfix == 1 then
return wininfo.loclist == 1 and 2 or 1
end
return false
end
function M.buf_is_qf(bufnr, bufinfo)
bufinfo = bufinfo or
(vim.api.nvim_buf_is_valid(bufnr) and vim.fn.getbufinfo(bufnr)[1])
if bufinfo and bufinfo.variables and
bufinfo.variables.current_syntax == 'qf' and
not vim.tbl_isempty(bufinfo.windows) then
return M.win_is_qf(bufinfo.windows[1])
end
return false
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.nvim_buf_get_name(bufnr, bufinfo)
if not vim.api.nvim_buf_is_valid(bufnr) then return end
if bufinfo and bufinfo.name and #bufinfo.name>0 then
return bufinfo.name
end
local bufname = vim.api.nvim_buf_get_name(bufnr)
if not bufname or #bufname==0 then
local is_qf = M.buf_is_qf(bufnr, bufinfo)
if is_qf then
bufname = is_qf==1 and "[Quickfix List]" or "[Location List]"
else
bufname = "[No Name]"
end
end
assert(#bufname>0)
return bufname
end
function M.zz()
3 years ago
-- 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.nvim_win_call(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
-- only supported with string cmds (no tables)
if use_lua_io and cmd == 'string' 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
handle:close()
end
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
-- wrapper around |input()| to allow cancellation with `<C-c>`
-- without "E5108: Error executing lua Keyboard interrupt"
function M.input(prompt, text)
local ok, res = pcall(vim.fn.input, prompt, text or '')
return ok and res or nil
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
function M.neovim_bind_to_fzf(key)
local conv_map = {
['a'] = 'alt',
['c'] = 'ctrl',
['s'] = 'shift',
}
key = key:lower():gsub("[<>]", "")
for k, v in pairs(conv_map) do
key = key:gsub(k..'%-', v..'-')
end
return key
end
function M.git_version()
local out = M.io_system({"git", "--version"})
return tonumber(out:match("(%d+.%d+)."))
end
function M.find_version()
local out, rc = M.io_systemlist({"find", "--version"})
return rc==0 and tonumber(out[1]:match("(%d+.%d+)")) or nil
end
3 years ago
return M