go.nvim/lua/go/utils.lua

931 lines
22 KiB
Lua

local util = {}
local fn = vim.fn
local os_name = vim.loop.os_uname().sysname
local is_windows = os_name == 'Windows' or os_name == 'Windows_NT'
local is_git_shell = is_windows and (vim.fn.exists('$SHELL') and vim.fn.expand('$SHELL'):find('bash.exe') ~= nil)
local HASNVIM0_9 = vim.fn.has('nvim-0.9') == 1
util.get_node_text = vim.treesitter.get_node_text
if not HASNVIM0_9 or util.get_node_text == nil then
util.get_node_text = vim.treesitter.query.get_node_text
end
local nvim_exec = vim.api.nvim_exec2
if nvim_exec == nil then
nvim_exec = vim.api.nvim_exec
end
-- Check whether current buffer contains main function
function util.has_main()
local output = nvim_exec('grep func\\ main\\(\\) %', true)
local matchCount = vim.split(output, '\n')
return #matchCount > 3
end
function util.sep()
if is_windows then
return '\\'
end
return '/'
end
function util.sep2()
if is_windows then
return ';'
end
return ':'
end
function util.is_windows()
return is_windows
end
function util.ext()
if is_windows then
return '.exe'
end
return ''
end
local function get_path_sep()
if is_windows then
return ';'
end
return ':'
end
local function strip_path_sep(path)
local l = path[#path]
util.log(l, util.sep(), path:sub(1, #path - 1))
if l == util.sep() then
return path:sub(1, #path - 1)
end
return path
end
function util.root_dirs()
local dirs = {}
local root = fn.systemlist({ _GO_NVIM_CFG.go, 'env', 'GOROOT' })
table.insert(dirs, root[1])
local paths = fn.systemlist({ _GO_NVIM_CFG.go, 'env', 'GOPATH' })
local sp = get_path_sep()
paths = vim.split(paths[1], sp)
for _, p in pairs(paths) do
p = fn.substitute(p, '\\\\', '/', 'g')
table.insert(dirs, p)
end
return dirs
end
function util.go_packages(dirs, arglead)
util.log(debug.traceback())
local pkgs = {}
for _, dir in pairs(dirs) do
util.log(dir)
local scr_root = fn.expand(dir .. util.sep() .. 'src' .. util.sep())
util.log(scr_root, arglead)
local roots = fn.globpath(scr_root, arglead .. '*', 0, 1)
if roots == { '' } then
roots = {}
end
util.log(roots)
for _, pkg in pairs(roots) do
util.log(pkg)
if fn.isdirectory(pkg) then
pkg = pkg .. util.sep()
table.insert(pkgs, pkg)
elseif not pkg:match([[%.a$]]) then
-- without this the result can have duplicates in form of
-- 'encoding/json' and '/encoding/json/'
pkg = strip_path_sep(pkg)
-- remove the scr root and keep the package in tact
pkg = fn.substitute(pkg, scr_root, '', '')
table.insert(pkgs, pkg)
end
end
end
util.log(pkgs)
return pkgs
end
-- function! s:interface_list(pkg) abort
-- let [contents, err] = go#util#Exec(['go', 'doc', a:pkg])
-- if err
-- return []
-- endif
--
-- let contents = split(contents, "\n")
-- call filter(contents, 'v:val =~# ''^type\s\+\h\w*\s\+interface''')
-- return map(contents, 'a:pkg . "." . matchstr(v:val, ''^type\s\+\zs\h\w*\ze\s\+interface'')')
-- endfunction
function util.interface_list(pkg)
local p = fn.systemlist({ _GO_NVIM_CFG.go, 'doc', pkg })
util.log(p)
local ifaces = {}
if p then
local contents = p -- vim.split(p[1], "\n")
for _, content in pairs(contents) do
util.log(content)
if content:find('interface') then
local iface_name = fn.matchstr(content, [[^type\s\+\zs\h\w*\ze\s\+interface]])
if iface_name ~= '' then
table.insert(ifaces, pkg .. iface_name)
end
end
end
end
util.log(ifaces)
return ifaces
end
-- https://alpha2phi.medium.com/neovim-101-regular-expression-f15a6d782add
function util.get_fname_num(line)
line = util.trim(line)
local reg = [[\(.\+\.go\)\:\(\d\+\):]]
local f = fn.matchlist(line, reg)
if f[1] and f[1] ~= '' then
return f[2], tonumber(f[3])
end
end
function util.smartrun()
local cmd
if util.has_main() then
cmd = string.format('lcd %:p:h | :set makeprg=%s\\ run\\ . | :make | :lcd -', _GO_NVIM_CFG.go)
vim.cmd(cmd)
else
cmd = string.format('setl makeprg=%s\\ run\\ . | :make', _GO_NVIM_CFG.go)
vim.cmd(cmd)
end
end
function util.smartbuild()
local cmd
if util.has_main() then
cmd = string.format('lcd %:p:h | :set makeprg=%s\\ build\\ . | :make | :lcd -', _GO_NVIM_CFG.go)
vim.cmd(cmd)
else
cmd = string.format('setl makeprg=%s\\ build\\ . | :make', _GO_NVIM_CFG.go)
vim.cmd(cmd)
end
end
util.check_same = function(tbl1, tbl2)
if #tbl1 ~= #tbl2 then
return false
end
for k, v in ipairs(tbl1) do
if v ~= tbl2[k] then
return false
end
end
return true
end
util.map = function(modes, key, result, options)
options =
util.merge({ noremap = true, silent = false, expr = false, nowait = false }, options or {})
local buffer = options.buffer
options.buffer = nil
if type(modes) ~= 'table' then
modes = { modes }
end
for i = 1, #modes do
if buffer then
vim.api.nvim_buf_set_keymap(0, modes[i], key, result, options)
else
vim.api.nvim_set_keymap(modes[i], key, result, options)
end
end
end
util.copy_array = function(from, to)
for i = 1, #from do
to[i] = from[i]
end
end
util.deepcopy = function(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[util.deepcopy(orig_key)] = util.deepcopy(orig_value)
end
setmetatable(copy, util.deepcopy(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
util.handle_job_data = function(data)
if not data then
return nil
end
-- Because the nvim.stdout's data will have an extra empty line at end on some OS (e.g. maxOS), we should remove it.
for _ = 1, 3, 1 do
if data[#data] == '' then
table.remove(data, #data)
end
end
if #data < 1 then
return nil
end
-- remove ansi escape code
for i, v in ipairs(data) do
data[i] = util.remove_ansi_escape(data[i])
end
return data
end
local cache_dir = fn.stdpath('cache')
util.log = function(...)
if not _GO_NVIM_CFG or not _GO_NVIM_CFG.verbose then
return
end
local arg = { ... }
local log_default = string.format('%s%sgonvim.log', cache_dir, util.sep())
local log_path = _GO_NVIM_CFG.log_path or log_default
local str = ''
local info = debug.getinfo(2, 'Sl')
str = str .. info.short_src .. ':' .. info.currentline
local _, ms = vim.loop.gettimeofday()
str = string.format('[%s %d] %s', os.date(), ms, str)
for i, v in ipairs(arg) do
if type(v) == 'table' then
str = str .. ' |' .. tostring(i) .. ': ' .. vim.inspect(v or 'nil') .. '\n'
else
str = str .. ' |' .. tostring(i) .. ': ' .. tostring(v or 'nil')
end
end
if #str > 2 then
if log_path ~= nil and #log_path > 3 then
local f, err = io.open(log_path, 'a+')
if err then
vim.notify('failed to open log' .. log_path .. err, vim.log.levels.ERROR)
return
end
if not f then
error('open file ' .. log_path, f)
end
io.output(f)
io.write(str .. '\n')
io.close(f)
else
vim.notify(str .. '\n', vim.log.levels.DEBUG)
end
end
end
util.trace = function(...)
end
local rhs_options = {}
function rhs_options:new()
local instance = {
cmd = '',
options = { noremap = false, silent = false, expr = false, nowait = false },
}
setmetatable(instance, self)
self.__index = self
return instance
end
function rhs_options:map_cmd(cmd_string)
self.cmd = cmd_string
return self
end
function rhs_options:map_cr(cmd_string)
self.cmd = (':%s<CR>'):format(cmd_string)
return self
end
function rhs_options:map_args(cmd_string)
self.cmd = (':%s<Space>'):format(cmd_string)
return self
end
function rhs_options:map_cu(cmd_string)
self.cmd = (':<C-u>%s<CR>'):format(cmd_string)
return self
end
function rhs_options:with_silent()
self.options.silent = true
return self
end
function rhs_options:with_noremap()
self.options.noremap = true
return self
end
function rhs_options:with_expr()
self.options.expr = true
return self
end
function rhs_options:with_nowait()
self.options.nowait = true
return self
end
function util.map_cr(cmd_string)
local ro = rhs_options:new()
return ro:map_cr(cmd_string)
end
function util.map_cmd(cmd_string)
local ro = rhs_options:new()
return ro:map_cmd(cmd_string)
end
function util.map_cu(cmd_string)
local ro = rhs_options:new()
return ro:map_cu(cmd_string)
end
function util.map_args(cmd_string)
local ro = rhs_options:new()
return ro:map_args(cmd_string)
end
function util.nvim_load_mapping(mapping)
for key, value in pairs(mapping) do
local mode, keymap = key:match('([^|]*)|?(.*)')
if type(value) == 'table' then
local rhs = value.cmd
local options = value.options
vim.api.nvim_set_keymap(mode, keymap, rhs, options)
end
end
end
function util.load_plugin(name, modulename)
assert(name ~= nil, 'plugin should not empty')
modulename = modulename or name
local has, plugin = pcall(require, modulename)
if has then
return plugin
end
local pkg = packer_plugins
local has_packer = pcall(require, 'packer')
local has_lazy = pcall(require, 'lazy')
if has_packer or has_lazy then
-- packer installed
if has_packer then
local loader = require('packer').loader
if not pkg[name] or not pkg[name].loaded then
util.log('packer loader ' .. name)
vim.cmd('packadd ' .. name) -- load with default
if pkg[name] ~= nil then
loader(name)
end
end
else
require('lazy').load({ plugins = name })
end
else
util.log('packadd ' .. name)
local paths = vim.o.runtimepath
if paths:find(name) then
vim.cmd('packadd ' .. name)
end
end
has, plugin = pcall(require, modulename)
if not has then
util.info('plugin ' .. name .. ' module ' .. modulename .. ' not loaded ')
return nil
end
return plugin
end
-- deprecated
-- function util.check_capabilities(feature, client_id)
-- local clients = vim.lsp.buf_get_clients(client_id or 0)
--
-- local supported_client = false
-- for _, client in pairs(clients) do
-- -- util.log(client.resolved_capabilities)
-- util.log(client.server_capabilities)
-- supported_client = client.resolved_capabilities[feature]
-- supported_client = client.resolved_capabilities[feature]
-- if supported_client then
-- break
-- end
-- end
--
-- if supported_client then
-- return true
-- else
-- if #clients == 0 then
-- util.log("LSP: no client attached")
-- else
-- util.log("LSP: server does not support " .. feature)
-- end
-- return false
-- end
-- end
function util.relative_to_cwd(name)
local rel = fn.isdirectory(name) == 0 and fn.fnamemodify(name, ':h:.')
or fn.fnamemodify(name, ':.')
if rel == '.' then
return '.'
else
return '.' .. util.sep() .. rel
end
end
function util.chdir(dir)
if fn.exists('*chdir') then
return fn.chdir(dir)
end
local oldir = fn.getcwd()
local cd = 'cd'
if fn.exists('*haslocaldir') and fn.haslocaldir() then
cd = 'lcd'
vim.cmd(cd .. ' ' .. fn.fnameescape(dir))
return oldir
end
end
function util.all_pkgs()
return '.' .. util.sep() .. '...'
end
-- log and messages
function util.warn(msg)
vim.schedule(function()
vim.notify('WARN: ' .. msg, vim.log.levels.WARN)
end)
end
function util.error(msg)
vim.schedule(function()
vim.notify('ERR: ' .. msg, vim.log.levels.ERROR)
end)
end
function util.info(msg)
vim.schedule(function()
vim.notify('INFO: ' .. msg, vim.log.levels.INFO)
end)
end
function util.debug(msg)
vim.schedule(function()
vim.notify('DEBUG: ' .. msg, vim.log.levels.DEBUG)
end)
end
function util.rel_path(folder)
-- maybe expand('%:p:h:t')
local mod = '%:p'
if folder then
mod = '%:p:h'
end
local fpath = fn.expand(mod)
local workfolders = vim.lsp.buf.list_workspace_folders()
if fn.empty(workfolders) == 0 then
fpath = '.' .. fpath:sub(#workfolders[1] + 1)
else
fpath = fn.fnamemodify(fn.expand(mod), ':p:.')
end
util.log(fpath:sub(#fpath), fpath, util.sep())
if fpath:sub(#fpath) == util.sep() then
fpath = fpath:sub(1, #fpath - 1)
util.log(fpath)
end
return fpath
end
function util.trim(s)
if s then
s = util.ltrim(s)
return util.rtrim(s)
end
end
function util.rtrim(s)
local n = #s
while n > 0 and s:find('^%s', n) do
n = n - 1
end
return s:sub(1, n)
end
function util.ltrim(s)
return (s:gsub('^%s*', ''))
end
function util.work_path()
local fpath = fn.expand('%:p:h')
local workfolders = vim.lsp.buf.list_workspace_folders()
if #workfolders == 1 then
return workfolders[1]
end
for _, value in pairs(workfolders) do
local mod = value .. util.sep() .. 'go.mod'
if util.file_exists(mod) then
return value
end
end
return workfolders[1] or fpath
end
function util.empty(t)
if t == nil then
return true
end
if type(t) ~= 'table' then
return false
end
return next(t) == nil
end
local open = io.open
function util.read_file(path)
local file = open(path, 'rb') -- r read mode and b binary mode
if not file then
return nil
end
local content = file:read('*a') -- *a or *all reads the whole file
file:close()
return content
end
function util.restart(cmd_args)
local old_lsp_clients = vim.lsp.get_active_clients()
local configs = require('lspconfig.configs')
for _, client in pairs(old_lsp_clients) do
if client.name == 'gopls' then
vim.lsp.stop_client(client.id)
end
end
if configs['gopls'] ~= nil then
vim.defer_fn(function()
configs['gopls'].launch()
end, 500)
end
end
util.deletedir = function(dir)
local lfs = require('lfs')
for file in lfs.dir(dir) do
local file_path = dir .. '/' .. file
if file ~= '.' and file ~= '..' then
if lfs.attributes(file_path, 'mode') == 'file' then
os.remove(file_path)
print('remove file', file_path)
elseif lfs.attributes(file_path, 'mode') == 'directory' then
print('dir', file_path)
util.deletedir(file_path)
end
end
end
lfs.rmdir(dir)
util.log('remove dir', dir)
end
function util.file_exists(name)
local f = io.open(name, 'r')
if f ~= nil then
io.close(f)
return true
end
return false
end
-- get all lines from a file, returns an empty
-- list/table if the file does not exist
function util.lines_from(file)
if not util.file_exists(file) then
return {}
end
local lines = {}
for line in io.lines(file) do
lines[#lines + 1] = line
end
return lines
end
function util.list_directory()
return fn.map(fn.glob(fn.fnameescape('./') .. '/{,.}*/', 1, 1), 'fnamemodify(v:val, ":h:t")')
end
function util.get_active_buf()
local lb = fn.getbufinfo({ buflisted = 1 })
util.log(lb)
local result = {}
for _, item in ipairs(lb) do
if fn.empty(item.name) == 0 and item.hidden == 0 then
util.log('buf loaded', item.name)
table.insert(result, { name = fn.shellescape(item.name), bufnr = item.bufnr })
end
end
return result
end
-- for l:item in l:blist
-- "skip unnamed buffers; also skip hidden buffers?
-- if empty(l:item.name) || l:item.hidden
-- continue
-- endif
-- call add(l:result, shellescape(l:item.name))
-- return l:result
function util.set_nulls()
if _GO_NVIM_CFG.null_ls_document_formatting_disable then
local query = {}
if type(_GO_NVIM_CFG.null_ls_document_formatting_disable) ~= 'boolean' then
query = _GO_NVIM_CFG.null_ls_document_formatting_disable
end
local ok, nulls = pcall(require, 'null-ls')
if ok then
nulls.disable(query)
end
end
end
-- run in current source code path
function util.exec_in_path(cmd, bufnr, ...)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local path
if type(bufnr) == 'string' then
path = bufnr
else
path = fn.fnamemodify(fn.bufname(bufnr), ':p:h')
end
local dir = util.chdir(path)
local result
if type(cmd) == 'function' then
result = cmd(bufnr, ...)
else
result = fn.systemlist(cmd, ...)
end
util.log(result)
util.chdir(dir)
return result
end
function util.line_ending()
if vim.o.fileformat == 'dos' then
return '\r\n'
elseif vim.o.fileformat == 'mac' then
return '\r'
end
return '\n'
end
function util.offset(line, col)
util.log(line, col)
if vim.o.encoding ~= 'utf-8' then
print('only utf-8 encoding is supported current encoding: ', vim.o.encoding)
end
return fn.line2byte(line) + col - 2
end
-- parse //+build integration unit
-- //go:build ci
function util.get_build_tags(buf)
local tags = {}
buf = buf or '%'
local pattern = [[^//\s*[+|(go:)]*build\s\+\(.\+\)]]
local cnt = vim.fn.getbufinfo(buf)[1]['linecount']
cnt = math.min(cnt, 10)
for i = 1, cnt do
local line = vim.fn.trim(vim.fn.getbufline(buf, i)[1])
if string.find(line, 'package') then
break
end
local t = vim.fn.substitute(line, pattern, [[\1]], '')
if t ~= line then -- tag found
t = vim.fn.substitute(t, [[ \+]], ',', 'g')
table.insert(tags, t)
end
end
if #tags > 0 then
return tags
end
end
-- a uuid
function util.uuid()
math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 9)))
local random = math.random
local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
return string.gsub(template, '[xy]', function(c)
local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
return string.format('%x', v)
end)
end
local lorem =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
function util.lorem()
return lorem
end
function util.random_words(len)
local str = util.lorem()
local words = fn.split(str, ' ')
str = ''
for i = 1, len do
str = str .. ' ' .. words[math.random(#words)]
end
return str
end
function util.random_line()
local lines = vim.split(lorem, ', ')
return lines[math.random(#lines)] .. ','
end
function util.run_command(cmd, ...)
local result = fn.systemlist(cmd, ...)
return result
end
function util.quickfix(cmd)
if _GO_NVIM_CFG.trouble == true then
local ok, trouble = pcall(require, 'trouble')
if ok then
if cmd:find('copen') then
trouble.open('quickfix')
else
trouble.open('loclist')
end
else
vim.notify('trouble not found')
end
else
vim.cmd(cmd)
end
end
util.debounce = function(func, duration)
local timer = vim.loop.new_timer()
local function inner(args)
timer:stop()
timer:start(
duration,
0,
vim.schedule_wrap(function()
func(args)
end)
)
end
local group = vim.api.nvim_create_augroup('gonvim__CleanupLuvTimers', {})
vim.api.nvim_create_autocmd('VimLeavePre', {
group = group,
pattern = '*',
callback = function()
if timer then
if timer:has_ref() then
timer:stop()
if not timer:is_closing() then
timer:close()
end
end
timer = nil
end
end,
})
return timer, inner
end
local namepath = {}
util.extract_filepath = function(msg, pkg_path)
msg = msg or ''
-- util.log(msg)
--[[ or [[ findAllSubStr_test.go:234: Error inserting caseResult1: operation error DynamoDB: PutItem, exceeded maximum number of attempts]]
-- or 'path/path2/filename.go:50:11: Error invaild
-- or /home/ray/go/src/github/sample/app/driver.go:342 +0x19e5
local ma = fn.matchlist(msg, [[\v\s*(\w+.+\.go):(\d+):]])
ma = ma or fn.matchlist(msg, [[\v\s*(\w+.+\.go):(\d+)]])
local filename, lnum
if ma[2] then
util.log(ma)
filename = ma[2]
lnum = ma[3]
else
return
end
util.log('fname : ' .. (filename or 'nil') .. ':' .. (lnum or '-1'))
if namepath[filename] then
-- if name is same, no need to update path
return (namepath[filename] ~= filename), namepath[filename], lnum
end
if vim.fn.filereadable(filename) == 1 then
util.log('filename', filename)
-- no need to extract path, already quickfix format
namepath[filename] = filename
return false, filename, lnum
end
if pkg_path then
local pn = pkg_path:gsub('%.%.%.', '')
local fname = pn .. util.sep() .. filename
if vim.fn.filereadable(fname) == 1 then
namepath[filename] = fname
util.log('fname with pkg_name', fname)
return true, fname, lnum
end
end
local fname = fn.fnamemodify(fn.expand('%:h'), ':~:.') .. util.sep() .. ma[2]
util.log(fname, namepath[fname])
if vim.fn.filereadable(fname) == 1 then
namepath[filename] = fname
return true, fname, lnum
end
if namepath[filename] ~= nil then
util.log(namepath[filename])
return namepath[filename], lnum
end
if vim.fn.executable('find') == 0 then
return false, fname, lnum
end
-- note: slow operations
local cmd = 'find ./ -type f -name ' .. filename
local path = vim.fn.systemlist(cmd)
if vim.v.shell_error ~= 0 then
util.warn('find failed ' .. cmd .. vim.inspect(path))
end
for _, value in pairs(path) do
local st, _ = value:find(filename)
if st then
-- find cmd returns `./path/path2/filename.go`, the leading './' is not needed for quickfix
local p = value:sub(1, st - 1)
util.log(value, st, p)
namepath[filename] = p
return true, p, lnum
end
end
-- nothing... we will not check this file again
namepath[filename] = filename
end
util.remove_ansi_escape = function(str)
local ansi_escape_pattern = '\27%[%d+;%d*;%d*m'
-- Replace all occurrences of the pattern with an empty string
str = str:gsub(ansi_escape_pattern, '')
str = str:gsub('\27%[[%d;]*%a', '')
return str
end
-- Keeps track of tools that are already installed.
-- The keys are the names of tools and the values are booleans
-- indicating whether the tools is available or not.
util.installed_tools = {}
-- Check if host has goenv in path.
util.goenv_mode = function()
if is_windows then
-- always return false for Windows because goenv doesn't seem to be supported there.
return false
end
local cmd = "command -v goenv > /dev/null 2>&1"
local status = os.execute(cmd)
return status == 0
end
return util