feat(mvp): MVP (#3)

This commit is contained in:
linrongbin16
2023-12-28 16:07:44 +08:00
committed by GitHub
parent 2692957e04
commit 510976fd2c
35 changed files with 3477 additions and 21 deletions

View File

@@ -26,6 +26,17 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install commons.nvim
if: ${{ github.ref != 'refs/heads/main' }}
shell: bash
run: |
git clone --depth=1 https://github.com/linrongbin16/commons.nvim.git ~/.commons.nvim
rm -rf ./lua/gentags/commons
mkdir -p ./lua/gentags/commons
cp -rf ~/.commons.nvim/lua/commons/*.lua ./lua/gentags/commons
cp ~/.commons.nvim/version.txt ./lua/gentags/commons/version.txt
cd ./lua/gentags/commons
find . -type f -name '*.lua' -exec sed -i 's/require("commons/require("gentags.commons/g' {} \;
- name: Luacheck
uses: lunarmodules/luacheck@v1
with:
@@ -36,14 +47,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
args: --config-path .stylua.toml ./lua ./test
- name: Add json.lua
if: ${{ github.ref != 'refs/heads/main' }}
shell: bash
run: |
echo "pwd"
echo $PWD
git clone --depth=1 https://github.com/actboy168/json.lua.git ~/.json.lua
cp ~/.json.lua/json.lua ./lua/gentags/actboy168_json.lua
- name: Auto Commit
if: ${{ github.ref != 'refs/heads/main' }}
uses: stefanzweifel/git-auto-commit-action@v4
@@ -51,6 +54,8 @@ jobs:
commit_message: "chore(pr): auto-commit"
unit_test:
name: Unit Test
needs:
- luacheck
strategy:
matrix:
nvim_version: [stable, nightly, v0.7.0]
@@ -99,7 +104,6 @@ jobs:
name: Release
if: ${{ github.ref == 'refs/heads/main' }}
needs:
- luacheck
- unit_test
runs-on: ubuntu-latest
steps:

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ luac.out
# macOS
.DS_Store
# tags
tags

View File

@@ -4,5 +4,5 @@ modules = {
}
exclude = {
"lua/actboy168_json.lua",
"lua/gentags/commons/*.lua",
}

1
.styluaignore Normal file
View File

@@ -0,0 +1 @@
lua/gentags/commons/*.lua

View File

@@ -1,8 +1,11 @@
<!-- markdownlint-disable MD001 MD013 MD034 MD033 MD051 -->
# gentags.nvim
<p align="center">
<a href="https://github.com/neovim/neovim/releases/v0.7.0"><img alt="Neovim" src="https://img.shields.io/badge/Neovim-v0.7+-57A143?logo=neovim&logoColor=57A143" /></a>
<a href="https://luarocks.org/modules/linrongbin16/gentags.nvim"><img alt="luarocks" src="https://custom-icon-badges.demolab.com/luarocks/v/linrongbin16/gentags.nvim?label=Luarocks&labelColor=063B70&logo=feed-tag&logoColor=fff&color=008B8B" /></a>
<a href="https://github.com/linrongbin16/commons.nvim"><img alt="commons.nvim" src="https://custom-icon-badges.demolab.com/badge/Powered_by-commons.nvim-teal?logo=heart&logoColor=fff&labelColor=deeppink" /></a>
<a href="https://luarocks.org/modules/linrongbin16/gentags.nvim"><img alt="luarocks" src="https://custom-icon-badges.demolab.com/luarocks/v/linrongbin16/gentags.nvim?label=LuaRocks&labelColor=063B70&logo=tag&logoColor=fff&color=blue" /></a>
<a href="https://github.com/linrongbin16/gentags.nvim/actions/workflows/ci.yml"><img alt="ci.yml" src="https://img.shields.io/github/actions/workflow/status/linrongbin16/gentags.nvim/ci.yml?label=GitHub%20CI&labelColor=181717&logo=github&logoColor=fff" /></a>
<a href="https://app.codecov.io/github/linrongbin16/gentags.nvim"><img alt="codecov" src="https://img.shields.io/codecov/c/github/linrongbin16/gentags.nvim?logo=codecov&logoColor=F01F7A&label=Codecov" /></a>
</p>
@@ -11,25 +14,97 @@
Tags generator/management for old school vimers in Neovim.
</i></p>
> [!WARNING]
>
> Don't use this plugin now, it's not finished yet!
## Table of Contents
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [Configuration](#configuration)
- [Alternatives](#alternatives)
- [Development](#development)
- [Contribute](#contribute)
## Features
- [x] Support both workspace/single file.
- [ ] Incremental update on file save.
- [ ] Disk cache management and garbage collection.
- [x] Async run & terminate immediately on nvim leave.
- [ ] Real-time status for Neovim components such as statusline.
## Install
Requirements:
- Neovim &ge; 0.7.0.
For now the required (or supported) backends are:
- [universal-ctags](https://github.com/universal-ctags/ctags).
PRs are welcome to add other backends.
<details>
<summary><b>With <a href="https://github.com/folke/lazy.nvim">lazy.nvim</a>.</b></summary>
<summary><b>With <a href="https://github.com/folke/lazy.nvim">lazy.nvim</a></b></summary>
```lua
require('lazy').setup({
{ "linrongbin16/gentags.nvim", opts = {} }
require("lazy").setup({
{
"linrongbin16/gentags.nvim",
config = function()
require('gentags').setup()
end,
},
})
```
</details>
## Credits
<details>
<summary><b>With <a href="https://github.com/lewis6991/pckr.nvim">pckr.nvim</a></b></summary>
```lua
require("pckr").add({
{
"linrongbin16/gentags.nvim",
config = function()
require("gentags").setup()
end,
},
})
```
</details>
## Usage
Gentags will automatically run below jobs in backend when you work in the nvim editor:
- Load a tags for the whole worksapce or the single file on first open a file.
- Generate tags for the whole worksapce or the single file on first open a file.
- Update tags after you save writtens on a file.
- Terminate all background child processes when you leave nvim.
By default all tags are generated in `stdpath('cache') . '/gentags.nvim'` directory.
- For UNIX/Linux: `~/.cache/nvim/gentags.nvim`.
- For Windows: `$env:USERPROFILE\AppData\Local\Temp\nvim\gentags.nvim`.
## Configuration
To configure default options, please use:
```lua
require('gentags').setup(opts)
```
The `otps` is an optional lua table that overwrites default options.
For complete options and defaults, please see [configs.lua](https://github.com/linrongbin16/gentags.nvim/tree/main/lua/gentags/configs.lua).
## Alternatives
- [gentags.lua](https://github.com/JMarkin/gentags.lua)
- [vim-gutentags](https://github.com/ludovicchabant/vim-gutentags)
## Development

View File

@@ -7,4 +7,4 @@ coverage:
default:
threshold: 90%
ignore:
- "lua/actboy168_json.lua"
- "lua/gentags/commons/*.lua"

View File

@@ -1,5 +1,67 @@
local logging = require("gentags.commons.logging")
local LogLevels = require("gentags.commons.logging").LogLevels
local configs = require("gentags.configs")
local M = {}
M.setup = function() end
--- @param opts gentags.Options?
M.setup = function(opts)
local cfg = configs.setup(opts)
-- print(vim.inspect(cfg))
logging.setup({
name = "gentags",
level = cfg.debug.enable and LogLevels.DEBUG or LogLevels.INFO,
console_log = cfg.debug.console_log,
file_log = cfg.debug.file_log,
file_log_name = "gentags.log",
})
local logger = logging.get("gentags") --[[@as commons.logging.Logger]]
-- cache dir
logger:ensure(
vim.fn.filereadable(cfg.cache_dir) <= 0,
"%s (cache_dir) already exist but not a directory!",
vim.inspect(cfg.cache_dir)
)
vim.fn.mkdir(cfg.cache_dir, "p")
-- init tags (first generate) when open/create file
vim.api.nvim_create_autocmd({
"BufReadPre",
"BufNewFile",
"FileReadPre",
}, {
callback = function(event)
logging
.get("gentags")
:debug("|setup| enter buffer:%s", vim.inspect(event))
require("gentags.dispatcher").load()
require("gentags.dispatcher").init()
end,
})
-- update tags when write/modify file
vim.api.nvim_create_autocmd({
"BufWritePost",
"FileWritePost",
"FileAppendPost",
}, {
callback = function(event)
logging
.get("gentags")
:debug("|setup| write buffer:%s", vim.inspect(event))
require("gentags.dispatcher").update()
end,
})
-- terminate before leaving vim
vim.api.nvim_create_autocmd({ "VimLeavePre" }, {
callback = function(event)
logging.get("gentags"):debug("|setup| leave vim:%s", vim.inspect(event))
require("gentags.dispatcher").terminate()
end,
})
end
return M

View File

View File

@@ -0,0 +1,374 @@
local uv = vim.loop
--- @class SystemOpts
--- @field stdin? string|string[]|true
--- @field stdout? fun(err:string?, data: string?)|false
--- @field stderr? fun(err:string?, data: string?)|false
--- @field cwd? string
--- @field env? table<string,string|number>
--- @field clear_env? boolean
--- @field text? boolean
--- @field timeout? integer Timeout in ms
--- @field detach? boolean
--- @class vim.SystemCompleted
--- @field code integer
--- @field signal integer
--- @field stdout? string
--- @field stderr? string
--- @class vim.SystemState
--- @field handle? uv.uv_process_t
--- @field timer? uv.uv_timer_t
--- @field pid? integer
--- @field timeout? integer
--- @field done? boolean|'timeout'
--- @field stdin? uv.uv_stream_t
--- @field stdout? uv.uv_stream_t
--- @field stderr? uv.uv_stream_t
--- @field stdout_data? string[]
--- @field stderr_data? string[]
--- @field result? vim.SystemCompleted
--- @enum vim.SystemSig
local SIG = {
HUP = 1, -- Hangup
INT = 2, -- Interrupt from keyboard
KILL = 9, -- Kill signal
TERM = 15, -- Termination signal
-- STOP = 17,19,23 -- Stop the process
}
---@param handle uv.uv_handle_t?
local function close_handle(handle)
if handle and not handle:is_closing() then
handle:close()
end
end
---@param state vim.SystemState
local function close_handles(state)
close_handle(state.handle)
close_handle(state.stdin)
close_handle(state.stdout)
close_handle(state.stderr)
close_handle(state.timer)
end
--- @class vim.SystemObj
--- @field pid integer
--- @field private _state vim.SystemState
--- @field wait fun(self: vim.SystemObj, timeout?: integer): vim.SystemCompleted
--- @field kill fun(self: vim.SystemObj, signal: integer|string)
--- @field write fun(self: vim.SystemObj, data?: string|string[])
--- @field is_closing fun(self: vim.SystemObj): boolean?
local SystemObj = {}
--- @param state vim.SystemState
--- @return vim.SystemObj
local function new_systemobj(state)
return setmetatable({
pid = state.pid,
_state = state,
}, { __index = SystemObj })
end
--- @param signal integer|string
function SystemObj:kill(signal)
self._state.handle:kill(signal)
end
--- @package
--- @param signal? vim.SystemSig
function SystemObj:_timeout(signal)
self._state.done = 'timeout'
self:kill(signal or SIG.TERM)
end
local MAX_TIMEOUT = 2 ^ 31
--- @param timeout? integer
--- @return vim.SystemCompleted
function SystemObj:wait(timeout)
local state = self._state
local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
return state.result ~= nil
end)
if not done then
-- Send sigkill since this cannot be caught
self:_timeout(SIG.KILL)
vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
return state.result ~= nil
end)
end
return state.result
end
--- @param data string[]|string|nil
function SystemObj:write(data)
local stdin = self._state.stdin
if not stdin then
error('stdin has not been opened on this object')
end
if type(data) == 'table' then
for _, v in ipairs(data) do
stdin:write(v)
stdin:write('\n')
end
elseif type(data) == 'string' then
stdin:write(data)
elseif data == nil then
-- Shutdown the write side of the duplex stream and then close the pipe.
-- Note shutdown will wait for all the pending write requests to complete
-- TODO(lewis6991): apparently shutdown doesn't behave this way.
-- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
stdin:write('', function()
stdin:shutdown(function()
if stdin then
stdin:close()
end
end)
end)
end
end
--- @return boolean
function SystemObj:is_closing()
local handle = self._state.handle
return handle == nil or handle:is_closing()
end
---@param output fun(err:string?, data: string?)|false
---@return uv.uv_stream_t?
---@return fun(err:string?, data: string?)? Handler
local function setup_output(output)
if output == nil then
return assert(uv.new_pipe(false)), nil
end
if type(output) == 'function' then
return assert(uv.new_pipe(false)), output
end
assert(output == false)
return nil, nil
end
---@param input string|string[]|true|nil
---@return uv.uv_stream_t?
---@return string|string[]?
local function setup_input(input)
if not input then
return
end
local towrite --- @type string|string[]?
if type(input) == 'string' or type(input) == 'table' then
towrite = input
end
return assert(uv.new_pipe(false)), towrite
end
--- @return table<string,string>
local function base_env()
local env = vim.fn.environ() --- @type table<string,string>
env['NVIM'] = vim.v.servername
env['NVIM_LISTEN_ADDRESS'] = nil
return env
end
--- uv.spawn will completely overwrite the environment
--- when we just want to modify the existing one, so
--- make sure to prepopulate it with the current env.
--- @param env? table<string,string|number>
--- @param clear_env? boolean
--- @return string[]?
local function setup_env(env, clear_env)
if clear_env then
return env
end
--- @type table<string,string|number>
env = vim.tbl_extend('force', base_env(), env or {})
local renv = {} --- @type string[]
for k, v in pairs(env) do
renv[#renv + 1] = string.format('%s=%s', k, tostring(v))
end
return renv
end
--- @param stream uv.uv_stream_t
--- @param text? boolean
--- @param bucket string[]
--- @return fun(err: string?, data: string?)
local function default_handler(stream, text, bucket)
return function(err, data)
if err then
error(err)
end
if data ~= nil then
if text then
bucket[#bucket + 1] = data:gsub('\r\n', '\n')
else
bucket[#bucket + 1] = data
end
else
stream:read_stop()
stream:close()
end
end
end
local M = {}
--- @param cmd string
--- @param opts uv.spawn.options
--- @param on_exit fun(code: integer, signal: integer)
--- @param on_error fun()
--- @return uv.uv_process_t, integer
local function spawn(cmd, opts, on_exit, on_error)
local handle, pid_or_err = uv.spawn(cmd, opts, on_exit)
if not handle then
on_error()
error(pid_or_err)
end
return handle, pid_or_err --[[@as integer]]
end
---@param timeout integer
---@param cb fun()
---@return uv.uv_timer_t
local function timer_oneshot(timeout, cb)
local timer = assert(uv.new_timer())
timer:start(timeout, 0, function()
timer:stop()
timer:close()
cb()
end)
return timer
end
--- @param state vim.SystemState
--- @param code integer
--- @param signal integer
--- @param on_exit fun(result: vim.SystemCompleted)?
local function _on_exit(state, code, signal, on_exit)
close_handles(state)
local check = assert(uv.new_check())
check:start(function()
for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
if not pipe:is_closing() then
return
end
end
check:stop()
check:close()
if state.done == nil then
state.done = true
end
if (code == 0 or code == 1) and state.done == 'timeout' then
-- Unix: code == 0
-- Windows: code == 1
code = 124
end
local stdout_data = state.stdout_data
local stderr_data = state.stderr_data
state.result = {
code = code,
signal = signal,
stdout = stdout_data and table.concat(stdout_data) or nil,
stderr = stderr_data and table.concat(stderr_data) or nil,
}
if on_exit then
on_exit(state.result)
end
end)
end
--- Run a system command
---
--- @param cmd string[]
--- @param opts? SystemOpts
--- @param on_exit? fun(out: vim.SystemCompleted)
--- @return vim.SystemObj
function M.run(cmd, opts, on_exit)
vim.validate({
cmd = { cmd, 'table' },
opts = { opts, 'table', true },
on_exit = { on_exit, 'function', true },
})
opts = opts or {}
local stdout, stdout_handler = setup_output(opts.stdout)
local stderr, stderr_handler = setup_output(opts.stderr)
local stdin, towrite = setup_input(opts.stdin)
--- @type vim.SystemState
local state = {
done = false,
cmd = cmd,
timeout = opts.timeout,
stdin = stdin,
stdout = stdout,
stderr = stderr,
}
--- @diagnostic disable-next-line:missing-fields
state.handle, state.pid = spawn(cmd[1], {
args = vim.list_slice(cmd, 2),
stdio = { stdin, stdout, stderr },
cwd = opts.cwd,
--- @diagnostic disable-next-line:assign-type-mismatch
env = setup_env(opts.env, opts.clear_env),
detached = opts.detach,
hide = true,
}, function(code, signal)
_on_exit(state, code, signal, on_exit)
end, function()
close_handles(state)
end)
if stdout then
state.stdout_data = {}
stdout:read_start(stdout_handler or default_handler(stdout, opts.text, state.stdout_data))
end
if stderr then
state.stderr_data = {}
stderr:read_start(stderr_handler or default_handler(stderr, opts.text, state.stderr_data))
end
local obj = new_systemobj(state)
if towrite then
obj:write(towrite)
obj:write(nil) -- close the stream
end
if opts.timeout then
state.timer = timer_oneshot(opts.timeout, function()
if state.handle and state.handle:is_active() then
obj:_timeout()
end
end)
end
return obj
end
return M

View File

@@ -0,0 +1,6 @@
-- Fix typecheck for _system.lua
--- @alias uv.uv_process_t uv_process_t
--- @alias uv.uv_timer_t uv_timer_t
--- @alias uv.uv_stream_t uv_stream_t
--- @alias uv.uv_handle_t uv_handle_t
--- @alias uv.spawn.options table<any, any>

View File

@@ -0,0 +1,56 @@
local M = {}
-- buffer {
--- @param bufnr integer
--- @param name string
--- @return any
M.get_buf_option = function(bufnr, name)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_get_option_value(name, { buf = bufnr })
else
return vim.api.nvim_buf_get_option(bufnr, name)
end
end
--- @param bufnr integer
--- @param name string
--- @param value any
M.set_buf_option = function(bufnr, name, value)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_set_option_value(name, value, { buf = bufnr })
else
return vim.api.nvim_buf_set_option(bufnr, name, value)
end
end
-- buffer }
-- window {
--- @param winnr integer
--- @param name string
--- @return any
M.get_win_option = function(winnr, name)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_get_option_value(name, { win = winnr })
else
return vim.api.nvim_win_get_option(winnr, name)
end
end
--- @param winnr integer
--- @param name string
--- @param value any
--- @return any
M.set_win_option = function(winnr, name, value)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_set_option_value(name, value, { win = winnr })
else
return vim.api.nvim_win_set_option(winnr, name, value)
end
end
-- window }
return M

View File

@@ -0,0 +1,29 @@
local M = {}
--- @deprecated
--- @see commons.apis
--- @param bufnr integer
--- @param name string
--- @return any
M.get_buf_option = function(bufnr, name)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_get_option_value(name, { buf = bufnr })
else
return vim.api.nvim_buf_get_option(bufnr, name)
end
end
--- @deprecated
--- @see commons.apis
--- @param bufnr integer
--- @param name string
--- @param value any
M.set_buf_option = function(bufnr, name, value)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_set_option_value(name, value, { buf = bufnr })
else
return vim.api.nvim_buf_set_option(bufnr, name, value)
end
end
return M

View File

@@ -0,0 +1,327 @@
local M = {}
-- FileLineReader {
--- @class commons.FileLineReader
--- @field filename string file name.
--- @field handler integer file handle.
--- @field filesize integer file size in bytes.
--- @field offset integer current read position.
--- @field batchsize integer chunk size for each read operation running internally.
--- @field buffer string? internal data buffer.
local FileLineReader = {}
--- @param filename string
--- @param batchsize integer?
--- @return commons.FileLineReader?
function FileLineReader:open(filename, batchsize)
local uv = require("gentags.commons.uv")
local handler = uv.fs_open(filename, "r", 438) --[[@as integer]]
if type(handler) ~= "number" then
error(
string.format(
"|commons.fileios - FileLineReader:open| failed to fs_open file: %s",
vim.inspect(filename)
)
)
return nil
end
local fstat = uv.fs_fstat(handler) --[[@as table]]
if type(fstat) ~= "table" then
error(
string.format(
"|commons.fileios - FileLineReader:open| failed to fs_fstat file: %s",
vim.inspect(filename)
)
)
uv.fs_close(handler)
return nil
end
local o = {
filename = filename,
handler = handler,
filesize = fstat.size,
offset = 0,
batchsize = batchsize or 4096,
buffer = nil,
}
setmetatable(o, self)
self.__index = self
return o
end
--- @private
--- @return integer
function FileLineReader:_read_chunk()
local uv = require("gentags.commons.uv")
local chunksize = (self.filesize >= self.offset + self.batchsize)
and self.batchsize
or (self.filesize - self.offset)
if chunksize <= 0 then
return 0
end
local data, --[[@as string?]]
read_err,
read_name =
uv.fs_read(self.handler, chunksize, self.offset)
if read_err then
error(
string.format(
"|commons.fileios - FileLineReader:_read_chunk| failed to fs_read file: %s, read_error:%s, read_name:%s",
vim.inspect(self.filename),
vim.inspect(read_err),
vim.inspect(read_name)
)
)
return -1
end
-- append to buffer
self.buffer = self.buffer and (self.buffer .. data) or data --[[@as string]]
self.offset = self.offset + #data
return #data
end
--- @return boolean
function FileLineReader:has_next()
self:_read_chunk()
return self.buffer ~= nil and string.len(self.buffer) > 0
end
--- @return string?
function FileLineReader:next()
--- @return string?
local function impl()
local strings = require("gentags.commons.strings")
if self.buffer == nil then
return nil
end
local nextpos = strings.find(self.buffer, "\n")
if nextpos then
local line = self.buffer:sub(1, nextpos - 1)
self.buffer = self.buffer:sub(nextpos + 1)
return line
else
return nil
end
end
repeat
local nextline = impl()
if nextline then
return nextline
end
until self:_read_chunk() <= 0
local nextline = impl()
if nextline then
return nextline
else
local buf = self.buffer
self.buffer = nil
return buf
end
end
-- Close the file reader.
function FileLineReader:close()
local uv = require("gentags.commons.uv")
if self.handler then
uv.fs_close(self.handler)
self.handler = nil
end
end
M.FileLineReader = FileLineReader
-- FileLineReader }
--- @param filename string
--- @param opts {trim:boolean?}?
--- @return string?
M.readfile = function(filename, opts)
opts = opts or { trim = false }
opts.trim = type(opts.trim) == "boolean" and opts.trim or false
local f = io.open(filename, "r")
if f == nil then
return nil
end
local content = f:read("*a")
f:close()
return opts.trim and vim.trim(content) or content
end
--- @param filename string file name.
--- @param on_complete fun(data:string?):nil callback on read complete.
--- 1. `data`: the file content.
--- @param opts {trim:boolean?}? options:
--- 1. `trim`: whether to trim whitespaces around text content, by default `false`.
M.asyncreadfile = function(filename, on_complete, opts)
local uv = require("gentags.commons.uv")
opts = opts or { trim = false }
opts.trim = type(opts.trim) == "boolean" and opts.trim or false
uv.fs_open(filename, "r", 438, function(open_err, fd)
if open_err then
error(
string.format(
"failed to open(r) file %s: %s",
vim.inspect(filename),
vim.inspect(open_err)
)
)
return
end
uv.fs_fstat(
---@diagnostic disable-next-line: param-type-mismatch
fd,
function(fstat_err, stat)
if fstat_err then
error(
string.format(
"failed to fstat file %s: %s",
vim.inspect(filename),
vim.inspect(fstat_err)
)
)
return
end
if not stat then
error(
string.format(
"failed to fstat file %s (empty stat): %s",
vim.inspect(filename),
vim.inspect(fstat_err)
)
)
return
end
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_read(fd, stat.size, 0, function(read_err, data)
if read_err then
error(
string.format(
"failed to read file %s: %s",
vim.inspect(filename),
vim.inspect(read_err)
)
)
return
end
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_close(fd, function(close_err)
on_complete(
(opts.trim and type(data) == "string") and vim.trim(data) or data
)
if close_err then
error(
string.format(
"failed to close file %s: %s",
vim.inspect(filename),
vim.inspect(close_err)
)
)
end
end)
end)
end
)
end)
end
--- @param filename string
--- @return string[]|nil
M.readlines = function(filename)
local reader = M.FileLineReader:open(filename) --[[@as commons.FileLineReader]]
if not reader then
return nil
end
local results = {}
while reader:has_next() do
table.insert(results, reader:next())
end
reader:close()
return results
end
--- @param filename string file name.
--- @param content string file content.
--- @return integer returns `0` if success, returns `-1` if failed.
M.writefile = function(filename, content)
local f = io.open(filename, "w")
if not f then
return -1
end
f:write(content)
f:close()
return 0
end
--- @param filename string file name.
--- @param content string file content.
--- @param on_complete fun(bytes:integer?):any callback on write complete.
--- 1. `bytes`: written data bytes.
M.asyncwritefile = function(filename, content, on_complete)
local uv = require("gentags.commons.uv")
uv.fs_open(filename, "w", 438, function(open_err, fd)
if open_err then
error(
string.format(
"failed to open(w) file %s: %s",
vim.inspect(filename),
vim.inspect(open_err)
)
)
return
end
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_write(fd, content, nil, function(write_err, bytes)
if write_err then
error(
string.format(
"failed to write file %s: %s",
vim.inspect(filename),
vim.inspect(write_err)
)
)
return
end
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_close(fd, function(close_err)
if close_err then
error(
string.format(
"failed to close(w) file %s: %s",
vim.inspect(filename),
vim.inspect(close_err)
)
)
return
end
if type(on_complete) == "function" then
on_complete(bytes)
end
end)
end)
end)
end
--- @param filename string file name.
--- @param lines string[] content lines.
--- @return integer returns `0` if success, returns `-1` if failed.
M.writelines = function(filename, lines)
local f = io.open(filename, "w")
if not f then
return -1
end
assert(type(lines) == "table")
for _, line in ipairs(lines) do
assert(type(line) == "string")
f:write(line .. "\n")
end
f:close()
return 0
end
return M

View File

@@ -0,0 +1,29 @@
local M = {}
--- @param t table?
--- @return string?
M.encode = function(t)
if t == nil then
return nil
end
if vim.fn.has("nvim-0.9") and vim.json ~= nil then
return vim.json.encode(t)
else
return require("gentags.commons._json").encode(t)
end
end
--- @param j string?
--- @return table?
M.decode = function(j)
if j == nil then
return nil
end
if vim.fn.has("nvim-0.9") and vim.json ~= nil then
return vim.json.decode(j)
else
return require("gentags.commons._json").decode(j)
end
end
return M

View File

@@ -0,0 +1,676 @@
local IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0
local M = {}
-- see: `lua print(vim.inspect(vim.log.levels))`
--- @enum commons.LogLevels
local LogLevels = {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
OFF = 5,
}
--- @enum commons.LogLevelNames
local LogLevelNames = {
[0] = "TRACE",
[1] = "DEBUG",
[2] = "INFO",
[3] = "WARN",
[4] = "ERROR",
[5] = "OFF",
}
M.LogLevels = LogLevels
M.LogLevelNames = LogLevelNames
local LogHighlights = {
[0] = "Comment",
[1] = "Comment",
[2] = "None",
[3] = "WarningMsg",
[4] = "ErrorMsg",
[5] = "ErrorMsg",
}
-- Formatter {
--- @class commons.logging.Formatter
--- @field fmt string
--- @field datefmt string
--- @field msecsfmt string
local Formatter = {}
--- @param fmt string
--- @param opts {datefmt:string?,msecsfmt:string?}?
--- @return commons.logging.Formatter
function Formatter:new(fmt, opts)
assert(type(fmt) == "string")
opts = opts or { datefmt = "%Y-%m-%d %H:%M:%S", msecsfmt = "%06d" }
opts.datefmt = type(opts.datefmt) == "string" and opts.datefmt
or "%Y-%m-%d %H:%M:%S"
opts.msecsfmt = type(opts.msecsfmt) == "string" and opts.msecsfmt or "%06d"
local o = {
fmt = fmt,
datefmt = opts.datefmt,
msecsfmt = opts.msecsfmt,
}
setmetatable(o, self)
self.__index = self
return o
end
local FORMATTING_TAGS = {
LEVEL_NO = "%(levelno)s",
LEVEL_NAME = "%(levelname)s",
MESSAGE = "%(message)s",
ASCTIME = "%(asctime)s",
MSECS = "%(msecs)d",
NAME = "%(name)s",
PROCESS = "%(process)d",
FILE_NAME = "%(filename)s",
LINE_NO = "%(lineno)d",
FUNC_NAME = "%(funcName)s",
}
--- @param meta table<string,any>
--- @return string
function Formatter:format(meta)
local strings = require("gentags.commons.strings")
local n = string.len(self.fmt)
local function make_detect(tag)
local function impl(idx)
if idx - 1 >= 1 and string.sub(self.fmt, idx - 1, idx) == "%%" then
return false
end
local endpos = idx + string.len(FORMATTING_TAGS[tag]) - 1
if endpos > n then
return false
end
return strings.startswith(
string.sub(self.fmt, idx, endpos),
FORMATTING_TAGS[tag]
)
end
return impl
end
local tags = {
"LEVEL_NO",
"LEVEL_NAME",
"MESSAGE",
"ASCTIME",
"MSECS",
"NAME",
"PROCESS",
"FILE_NAME",
"LINE_NO",
"FUNC_NAME",
}
local builder = {}
local i = 1
local tmp = ""
while i <= n do
local hit = false
for _, tag in ipairs(tags) do
local is_tag = make_detect(tag)
if is_tag(i) then
if string.len(tmp) > 0 then
table.insert(builder, tmp)
tmp = ""
end
i = i + string.len(FORMATTING_TAGS[tag])
hit = true
if tag == "ASCTIME" then
table.insert(builder, os.date(self.datefmt, meta.SECONDS))
elseif tag == "MSECS" then
table.insert(builder, string.format(self.msecsfmt, meta.MSECS))
elseif meta[tag] ~= nil then
table.insert(builder, tostring(meta[tag]))
end
break
end
end
if not hit then
tmp = tmp .. string.sub(self.fmt, i, i)
i = i + 1
end
end
return table.concat(builder, "")
end
M.Formatter = Formatter
-- Formatter }
-- Handler {
--- @class commons.logging.Handler
local Handler = {}
--- @param meta commons.logging._MetaInfo
function Handler:write(meta)
assert(false)
end
-- ConsoleHandler {
--- @class commons.logging.ConsoleHandler : commons.logging.Handler
--- @field formatter commons.logging.Formatter
local ConsoleHandler = {}
--- @param formatter commons.logging.Formatter?
--- @return commons.logging.ConsoleHandler
function ConsoleHandler:new(formatter)
if formatter == nil then
formatter = Formatter:new("[%(name)s] %(message)s")
end
local o = {
formatter = formatter,
}
setmetatable(o, self)
self.__index = self
return o
end
--- @param meta commons.logging._MetaInfo
function ConsoleHandler:write(meta)
if meta.LEVEL_NO < LogLevels.INFO then
return
end
local msg_lines = vim.split(meta.MESSAGE, "\n", { plain = true })
for _, line in ipairs(msg_lines) do
local chunks = {}
local line_meta =
vim.tbl_deep_extend("force", vim.deepcopy(meta), { MESSAGE = line })
local record = self.formatter:format(line_meta)
table.insert(chunks, {
record,
LogHighlights[line_meta.LEVEL_NO],
})
vim.schedule(function()
vim.api.nvim_echo(chunks, false, {})
end)
end
end
M.ConsoleHandler = ConsoleHandler
-- ConsoleHandler }
-- FileHandler {
--- @class commons.logging.FileHandler : commons.logging.Handler
--- @field formatter commons.logging.Formatter
--- @field filepath string
--- @field filemode "a"|"w"
--- @field filehandle any
local FileHandler = {}
--- @param filepath string
--- @param filemode "a"|"w"|nil
--- @param formatter commons.logging.Formatter?
--- @return commons.logging.FileHandler
function FileHandler:new(filepath, filemode, formatter)
assert(type(filepath) == "string")
assert(filemode == "a" or filemode == "w" or filemode == nil)
if formatter == nil then
formatter = Formatter:new(
"%(asctime)s,%(msecs)d [%(filename)s:%(lineno)d] %(levelname)s: %(message)s"
)
end
filemode = filemode ~= nil and string.lower(filemode) or "a"
local filehandle = nil
if filemode == "w" then
filehandle = io.open(filepath, "w")
assert(
filehandle ~= nil,
string.format("failed to open file:%s", vim.inspect(filepath))
)
end
local o = {
formatter = formatter,
filepath = filepath,
filemode = filemode,
filehandle = filehandle,
}
setmetatable(o, self)
self.__index = self
return o
end
function FileHandler:close()
if self.filemode == "w" or self.filehandle ~= nil then
self.filehandle:close()
self.filehandle = nil
end
end
--- @param meta commons.logging._MetaInfo
function FileHandler:write(meta)
local fp = nil
if self.filemode == "w" then
assert(
self.filehandle ~= nil,
string.format("failed to write file log:%s", vim.inspect(self.filepath))
)
fp = self.filehandle
elseif self.filemode == "a" then
fp = io.open(self.filepath, "a")
end
if fp then
local record = self.formatter:format(meta)
fp:write(string.format("%s\n", record))
end
if self.filemode == "a" and fp ~= nil then
fp:close()
end
end
M.FileHandler = FileHandler
-- FileHandler }
-- Handler }
-- Logger {
--- @class commons.logging.Logger
--- @field name string
--- @field level commons.LogLevels
--- @field handlers commons.logging.Handler[]
local Logger = {}
--- @param name string
--- @param level commons.LogLevels
--- @return commons.logging.Logger
function Logger:new(name, level)
assert(type(name) == "string")
assert(type(level) == "number" and LogLevelNames[level] ~= nil)
local o = {
name = name,
level = level,
handlers = {},
}
setmetatable(o, self)
self.__index = self
return o
end
--- @param handler commons.logging.Handler
function Logger:add_handler(handler)
assert(type(handler) == "table")
table.insert(self.handlers, handler)
end
--- @param dbg debuginfo?
--- @param lvl integer
--- @param fmt string
--- @param ... any
function Logger:_log(dbg, lvl, fmt, ...)
assert(type(lvl) == "number" and LogLevelNames[lvl] ~= nil)
local uv = require("gentags.commons.uv")
if lvl < self.level then
return
end
local msg = string.format(fmt, ...)
for _, handler in ipairs(self.handlers) do
local secs, millis = uv.gettimeofday()
--- @alias commons.logging._MetaInfo {LEVEL_NO:commons.LogLevels,LEVEL_NAME:commons.LogLevelNames,MESSAGE:string,SECONDS:integer,MILLISECONDS:integer,FILE_NAME:string,LINE_NO:integer,FUNC_NAME:string}
local meta_info = {
LEVEL_NO = lvl,
LEVEL_NAME = LogLevelNames[lvl],
MESSAGE = msg,
SECONDS = secs,
MSECS = millis,
NAME = self.name,
PROCESS = uv.os_getpid(),
FILE_NAME = dbg ~= nil and (dbg.source or dbg.short_src) or nil,
LINE_NO = dbg ~= nil and (dbg.currentline or dbg.linedefined) or nil,
FUNC_NAME = dbg ~= nil and (dbg.func or dbg.what) or nil,
}
handler:write(meta_info)
end
end
--- @param level integer|string
--- @param fmt string
--- @param ... any
function Logger:log(level, fmt, ...)
if type(level) == "string" then
assert(LogLevels[string.upper(level)] ~= nil)
level = LogLevels[string.upper(level)]
end
assert(type(level) == "number" and LogHighlights[level] ~= nil)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, level, fmt, ...)
end
--- @param fmt string
--- @param ... any
function Logger:debug(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, LogLevels.DEBUG, fmt, ...)
end
--- @param fmt string
--- @param ... any
function Logger:info(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, LogLevels.INFO, fmt, ...)
end
--- @param fmt string
--- @param ... any
function Logger:warn(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, LogLevels.WARN, fmt, ...)
end
--- @param fmt string
--- @param ... any
function Logger:err(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, LogLevels.ERROR, fmt, ...)
end
--- @param fmt string
--- @param ... any
function Logger:throw(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, LogLevels.ERROR, fmt, ...)
error(string.format(fmt, ...))
end
--- @param cond any
--- @param fmt string
--- @param ... any
function Logger:ensure(cond, fmt, ...)
if not cond then
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
self:_log(dbg, LogLevels.ERROR, fmt, ...)
end
assert(cond, string.format(fmt, ...))
end
M.Logger = Logger
-- Logger }
--- @type table<string, commons.logging.Logger>
local NAMESPACE = {}
--- @alias commons.LoggingConfigs {name:string,level:(commons.LogLevels|string)?,console_log:boolean?,file_log:boolean?,file_log_name:string?,file_log_dir:string?,file_log_mode:"a"|"w"|nil}
--- @type commons.LoggingConfigs
local Defaults = {
--- @type string
name = nil,
level = LogLevels.INFO,
console_log = true,
file_log = false,
file_log_name = nil,
file_log_dir = vim.fn.stdpath("data") --[[@as string]],
file_log_mode = "a",
}
--- @param opts commons.LoggingConfigs
M.setup = function(opts)
local conf = vim.tbl_deep_extend("force", vim.deepcopy(Defaults), opts or {})
if type(conf.level) == "string" then
assert(LogLevels[string.upper(conf.level)] ~= nil)
conf.level = LogLevels[string.upper(conf.level)]
end
assert(type(conf.level) == "number" and LogHighlights[conf.level] ~= nil)
local console_handler = ConsoleHandler:new()
local logger = Logger:new(conf.name, conf.level --[[@as commons.LogLevels]])
logger:add_handler(console_handler)
if conf.file_log then
assert(type(conf.file_log_name) == "string")
local SEPARATOR = IS_WINDOWS and "\\" or "/"
local filepath = string.format(
"%s%s",
type(conf.file_log_dir) == "string" and (conf.file_log_dir .. SEPARATOR)
or "",
conf.file_log_name
)
local file_handler = FileHandler:new(filepath, conf.file_log_mode or "a")
logger:add_handler(file_handler)
end
M.add(logger)
end
--- @param name string
--- @return boolean
M.has = function(name)
assert(type(name) == "string")
return NAMESPACE[name] ~= nil
end
--- @param name string
--- @return commons.logging.Logger?
M.get = function(name)
assert(type(name) == "string")
return NAMESPACE[name]
end
--- @param logger commons.logging.Logger
M.add = function(logger)
assert(type(logger) == "table")
assert(
(type(logger.name) == "string" and string.len(logger.name) > 0)
or logger.name ~= nil
)
assert(NAMESPACE[logger.name] == nil)
NAMESPACE[logger.name] = logger
end
local ROOT = "root"
--- @param level integer|string
--- @param fmt string
--- @param ... any
M.log = function(level, fmt, ...)
if type(level) == "string" then
assert(LogLevels[string.upper(level)] ~= nil)
level = LogLevels[string.upper(level)]
end
assert(type(level) == "number" and LogHighlights[level] ~= nil)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
local logger = M.get(ROOT)
assert(logger ~= nil)
logger:_log(dbg, level, fmt, ...)
end
--- @param fmt string
--- @param ... any
M.debug = function(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
local logger = M.get(ROOT)
assert(logger ~= nil)
logger:_log(dbg, LogLevels.DEBUG, fmt, ...)
end
--- @param fmt string
--- @param ... any
M.info = function(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
local logger = M.get(ROOT)
assert(logger ~= nil)
logger:_log(dbg, LogLevels.INFO, fmt, ...)
end
--- @param fmt string
--- @param ... any
M.warn = function(fmt, ...)
local dbg = debug.getinfo(2, "nfSl")
local logger = M.get(ROOT)
assert(logger ~= nil)
logger:_log(dbg, LogLevels.WARN, fmt, ...)
end
--- @param fmt string
--- @param ... any
M.err = function(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
local logger = M.get(ROOT)
assert(logger ~= nil)
logger:_log(dbg, LogLevels.ERROR, fmt, ...)
end
--- @param fmt string
--- @param ... any
M.throw = function(fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
local logger = M.get(ROOT)
assert(logger ~= nil)
logger:_log(dbg, LogLevels.ERROR, fmt, ...)
error(string.format(fmt, ...))
end
--- @param cond any
--- @param fmt string
--- @param ... any
M.ensure = function(cond, fmt, ...)
local dbglvl = 2
local dbg = nil
while true do
dbg = debug.getinfo(dbglvl, "nfSl")
if not dbg or dbg.what ~= "C" then
break
end
dbglvl = dbglvl + 1
end
local logger = M.get(ROOT)
assert(logger ~= nil)
if not cond then
logger:_log(dbg, LogLevels.ERROR, fmt, ...)
end
assert(cond, string.format(fmt, ...))
end
return M

View File

@@ -0,0 +1,182 @@
local M = {}
-- int32 max/min
M.INT32_MAX = 2147483647
M.INT32_MIN = -2147483648
--- @param a number?
--- @param b number?
--- @return boolean
M.eq = function(a, b)
return type(a) == "number" and type(b) == "number" and a == b
end
--- @param a number?
--- @param b number?
--- @return boolean
M.ne = function(a, b)
return not M.eq(a, b)
end
--- @param a number?
--- @param b number?
--- @return boolean
M.gt = function(a, b)
return type(a) == "number" and type(b) == "number" and a > b
end
--- @param a number?
--- @param b number?
--- @return boolean
M.ge = function(a, b)
return M.gt(a, b) or M.eq(a, b)
end
--- @param a number?
--- @param b number?
--- @return boolean
M.lt = function(a, b)
return type(a) == "number" and type(b) == "number" and a < b
end
--- @param a number?
--- @param b number?
--- @return boolean
M.le = function(a, b)
return M.lt(a, b) or M.eq(a, b)
end
--- @param value number
--- @param left number? lower bound, by default INT32_MIN
--- @param right number? upper bound, by default INT32_MAX
--- @return number
M.bound = function(value, left, right)
assert(type(value) == "number")
assert(type(left) == "number" or left == nil)
assert(type(right) == "number" or right == nil)
return math.min(math.max(left or M.INT32_MIN, value), right or M.INT32_MAX)
end
local IncrementalId = 0
--- @return integer
M.auto_incremental_id = function()
if IncrementalId >= M.INT32_MAX then
IncrementalId = 1
else
IncrementalId = IncrementalId + 1
end
return IncrementalId
end
--- @param a integer
--- @param b integer
--- @return integer
M.mod = function(a, b)
return math.floor(math.fmod(a, b))
end
--- @param f fun(v:any):number
--- @param a any
--- @param ... any
--- @return integer, integer
M.max = function(f, a, ...)
assert(
type(f) == "function",
string.format(
"first param 'f' must be unary-function returns number value:%s",
vim.inspect(f)
)
)
local maximal_item = a
local maximal_value = f(a)
local maximal_index = 1
for i, o in ipairs({ ... }) do
if f(o) > maximal_value then
maximal_item = o
maximal_index = i
end
end
return maximal_item, maximal_index
end
--- @param f fun(v:any):number
--- @param a any
--- @param ... any
--- @return integer, integer
M.min = function(f, a, ...)
assert(
type(f) == "function",
string.format(
"first param 'f' must be unary-function returns number value:%s",
vim.inspect(f)
)
)
local minimal_item = a
local minimal_value = f(a)
local minimal_index = 1
for i, o in ipairs({ ... }) do
if f(o) < minimal_value then
minimal_item = o
minimal_index = i
end
end
return minimal_item, minimal_index
end
--- @param m integer?
--- @param n integer?
--- @return number
M.random = function(m, n)
local rand_result, rand_err = require("gentags.commons.uv").random(4)
if rand_result == nil then
if m == nil and n == nil then
return math.random()
elseif m ~= nil and n == nil then
return math.random(m)
else
return math.random(m --[[@as integer]], n --[[@as integer]])
end
end
local bytes = {
string.byte(rand_result --[[@as string]], 1, -1),
}
local total = 0
for _, b in ipairs(bytes) do
total = M.mod(total * 256 + b, M.INT32_MAX)
end
if m == nil and n == nil then
return total / M.INT32_MAX
elseif m ~= nil and n == nil then
assert(type(m) == "number")
assert(m >= 1)
return M.mod(total, m) + 1
else
assert(type(m) == "number")
assert(type(n) == "number")
assert(n >= m)
return M.mod(total, n - m + 1) + m
end
end
--- @param l any[]|string
--- @return any[]|string
M.shuffle = function(l)
assert(type(l) == "table")
local n = #l
local new_l = {}
for i = 1, n do
table.insert(new_l, l[i])
end
for i = n, 1, -1 do
local j = M.random(n)
local tmp = new_l[j]
new_l[j] = new_l[i]
new_l[i] = tmp
end
return new_l
end
return M

View File

@@ -0,0 +1,108 @@
local IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0
local M = {}
M.SEPARATOR = IS_WINDOWS and "\\" or "/"
--- @param p string
--- @param opts {double_backslash:boolean?,expand:boolean?}?
--- @return string
M.normalize = function(p, opts)
opts = opts or { double_backslash = false, expand = false }
opts.double_backslash = type(opts.double_backslash) == "boolean"
and opts.double_backslash
or false
opts.expand = type(opts.expand) == "boolean" and opts.expand or false
-- '\\\\' => '\\'
local function _double_backslash(s)
if string.match(s, [[\\]]) then
s = string.gsub(s, [[\\]], [[\]])
end
return s
end
-- '\\' => '/'
local function _single_backslash(s)
if string.match(s, [[\]]) then
s = string.gsub(s, [[\]], [[/]])
end
return s
end
local result = p
if opts.double_backslash then
result = _double_backslash(result)
end
result = _single_backslash(result)
if opts.expand then
result = vim.fn.expand(vim.trim(result)) --[[@as string]]
if opts.double_backslash then
result = _double_backslash(result)
end
result = _single_backslash(result)
else
result = vim.trim(result)
end
return result
end
--- @param ... any
--- @return string
M.join = function(...)
return table.concat({ ... }, M.SEPARATOR)
end
--- @param p string?
--- @return string
M.reduce2home = function(p)
return vim.fn.fnamemodify(p or vim.fn.getcwd(), ":~") --[[@as string]]
end
--- @param p string?
--- @return string
M.reduce = function(p)
return vim.fn.fnamemodify(p or vim.fn.getcwd(), ":~:.") --[[@as string]]
end
--- @param p string?
--- @return string
M.shorten = function(p)
return vim.fn.pathshorten(M.reduce(p)) --[[@as string]]
end
--- @return string
M.pipename = function()
if IS_WINDOWS then
local function uuid()
local secs, ms = vim.loop.gettimeofday()
return table.concat({
string.format("%x", vim.loop.os_getpid()),
string.format("%x", secs),
string.format("%x", ms),
}, "-")
end
return string.format([[\\.\pipe\nvim-pipe-%s]], uuid())
else
return vim.fn.tempname() --[[@as string]]
end
end
--- @param p string?
--- @return string?
M.parent = function(p)
p = p or vim.fn.getcwd()
local strings = require("gentags.commons.strings")
if strings.endswith(p, "/") or strings.endswith(p, "\\") then
p = string.sub(p, 1, #p - 1)
end
local result = vim.fn.fnamemodify(p, ":h")
return string.len(result) < string.len(p) and result or nil
end
return M

View File

@@ -0,0 +1,227 @@
local M = {}
--- @class commons.RingBuffer
--- @field pos integer
--- @field queue any[]
--- @field size integer
--- @field maxsize integer
local RingBuffer = {}
--- @param maxsize integer?
--- @return commons.RingBuffer
function RingBuffer:new(maxsize)
assert(type(maxsize) == "number" and maxsize > 0)
local o = {
pos = 0,
queue = {},
size = 0,
maxsize = maxsize,
}
setmetatable(o, self)
self.__index = self
return o
end
--- @param idx integer
--- @return integer
function RingBuffer:_inc(idx)
if idx == self.maxsize then
return 1
else
return idx + 1
end
end
--- @param idx integer
--- @return integer
function RingBuffer:_dec(idx)
if idx == 1 then
return self.maxsize
else
return idx - 1
end
end
--- @param item any
--- @return integer
function RingBuffer:push(item)
assert(self.size >= 0 and self.size <= self.maxsize)
if self.size < self.maxsize then
table.insert(self.queue, item)
self.pos = self:_inc(self.pos)
self.size = self.size + 1
else
self.pos = self:_inc(self.pos)
self.queue[self.pos] = item
end
return self.pos
end
--- @return any?
function RingBuffer:pop()
if self.size <= 0 then
return nil
end
local old = self.queue[self.pos]
self.queue[self.pos] = nil
self.size = self.size - 1
self.pos = self:_dec(self.pos)
return old
end
--- @return any?
function RingBuffer:peek()
if self.size <= 0 then
return nil
end
return self.queue[self.pos]
end
--- @return integer
function RingBuffer:clear()
local old = self.size
self.pos = 0
self.queue = {}
self.size = 0
return old
end
-- RingBufferIterator {
-- usage:
--
-- ```lua
-- local it = ringbuf:iterator()
-- local item = nil
-- repeat
-- item = it:next()
-- if item then
-- -- consume item data
-- end
-- until item
-- ```
--
--- @class commons._RingBufferIterator
--- @field ringbuf commons.RingBuffer
--- @field index integer
--- @field initial_index integer
local _RingBufferIterator = {}
--- @param ringbuf commons.RingBuffer
--- @param index integer
--- @return commons._RingBufferIterator
function _RingBufferIterator:new(ringbuf, index)
assert(type(ringbuf) == "table")
local o = {
ringbuf = ringbuf,
index = index,
initial_index = index,
}
setmetatable(o, self)
self.__index = self
return o
end
--- @return boolean
function _RingBufferIterator:has_next()
if self.ringbuf.size == 0 then
return false
end
if self.index <= 0 or self.index > self.ringbuf.size then
return false
end
if
self.index ~= self.initial_index
and self.ringbuf:_inc(self.index) == self.initial_index
then
return false
end
return true
end
--- @return any?
function _RingBufferIterator:next()
assert(self:has_next())
assert(self.index >= 1 and self.index <= self.ringbuf.maxsize)
local item = self.ringbuf.queue[self.index]
self.index = self.ringbuf:_inc(self.index)
return item
end
-- RingBufferIterator }
-- RingBufferRIterator {
--- @class commons._RingBufferRIterator
--- @field ringbuf commons.RingBuffer
--- @field index integer
--- @field initial_index integer
local _RingBufferRIterator = {}
--- @param ringbuf commons.RingBuffer
--- @param index integer
--- @return commons._RingBufferRIterator
function _RingBufferRIterator:new(ringbuf, index)
assert(type(ringbuf) == "table")
local o = {
ringbuf = ringbuf,
index = index,
initial_index = index,
}
setmetatable(o, self)
self.__index = self
return o
end
--- @return boolean
function _RingBufferRIterator:has_next()
if self.ringbuf.size == 0 then
return false
end
if self.index <= 0 or self.index > self.ringbuf.size then
return false
end
if
self.index ~= self.initial_index
and self.ringbuf:_dec(self.index) == self.initial_index
then
return false
end
return true
end
--- @return any?
function _RingBufferRIterator:next()
assert(self:has_next())
assert(self.index >= 1 and self.index <= self.ringbuf.maxsize)
local item = self.ringbuf.queue[self.index]
self.index = self.ringbuf:_dec(self.index)
return item
end
-- RingBufferRIterator }
--- @return commons._RingBufferIterator
function RingBuffer:iterator()
if self.size < self.maxsize then
return _RingBufferIterator:new(self, 0)
else
return _RingBufferIterator:new(self, self:_inc(self.pos))
end
end
--- @return commons._RingBufferRIterator
function RingBuffer:riterator()
return _RingBufferRIterator:new(self, self.pos)
end
M.RingBuffer = RingBuffer
return M

View File

@@ -0,0 +1,124 @@
local M = {}
--- @alias commons.SpawnLineProcessor fun(line:string):any
--- @alias commons.SpawnOpts {on_stdout:commons.SpawnLineProcessor, on_stderr:commons.SpawnLineProcessor, [string]:any}
--- @alias commons.SpawnOnExit fun(completed:vim.SystemCompleted):nil
--- @param cmd string[]
--- @param opts commons.SpawnOpts? by default {text = true}
--- @param on_exit commons.SpawnOnExit?
--- @return vim.SystemObj
M.run = function(cmd, opts, on_exit)
opts = opts or {}
opts.text = type(opts.text) == "boolean" and opts.text or true
assert(type(opts.on_stdout) == "function")
assert(type(opts.on_stderr) == "function")
--- @param buffer string
--- @param fn_line_processor commons.SpawnLineProcessor
--- @return integer
local function _process(buffer, fn_line_processor)
local strings = require("gentags.commons.strings")
local i = 1
while i <= #buffer do
local newline_pos = strings.find(buffer, "\n", i)
if not newline_pos then
break
end
local line = buffer:sub(i, newline_pos - 1)
fn_line_processor(line)
i = newline_pos + 1
end
return i
end
local stdout_buffer = nil
--- @param err string?
--- @param data string?
local function _handle_stdout(err, data)
if err then
error(
string.format(
"failed to read stdout on cmd:%s, error:%s",
vim.inspect(cmd),
vim.inspect(err)
)
)
return
end
if data then
-- append data to buffer
stdout_buffer = stdout_buffer and (stdout_buffer .. data) or data
-- search buffer and process each line
local i = _process(stdout_buffer, opts.on_stdout)
-- truncate the printed lines if found any
stdout_buffer = i <= #stdout_buffer
and stdout_buffer:sub(i, #stdout_buffer)
or nil
elseif stdout_buffer then
-- foreach the data_buffer and find every line
local i = _process(stdout_buffer, opts.on_stdout)
if i <= #stdout_buffer then
local line = stdout_buffer:sub(i, #stdout_buffer)
opts.on_stdout(line)
stdout_buffer = nil
end
end
end
local stderr_buffer = nil
--- @param err string?
--- @param data string?
local function _handle_stderr(err, data)
if err then
error(
string.format(
"failed to read stderr on cmd:%s, error:%s",
vim.inspect(cmd),
vim.inspect(err)
)
)
return
end
if data then
stderr_buffer = stderr_buffer and (stderr_buffer .. data) or data
local i = _process(stderr_buffer, opts.on_stderr)
stderr_buffer = i <= #stderr_buffer
and stderr_buffer:sub(i, #stderr_buffer)
or nil
elseif stderr_buffer then
local i = _process(stderr_buffer, opts.on_stderr)
if i <= #stderr_buffer then
local line = stderr_buffer:sub(i, #stderr_buffer)
opts.on_stderr(line)
stderr_buffer = nil
end
end
end
local _system = (
vim.fn.has("nvim-0.10") > 0 and type(vim.system) == "function"
)
and vim.system
or require("gentags.commons._system").run
return _system(cmd, {
cwd = opts.cwd,
env = opts.env,
clear_env = opts.clear_env,
---@diagnostic disable-next-line: assign-type-mismatch
stdin = opts.stdin,
stdout = _handle_stdout,
stderr = _handle_stderr,
text = opts.text,
timeout = opts.timeout,
detach = opts.detach,
}, on_exit)
end
return M

View File

@@ -0,0 +1,268 @@
local M = {}
--- @param s any
--- @return boolean
M.empty = function(s)
return type(s) ~= "string" or string.len(s) == 0
end
--- @param s any
--- @return boolean
M.not_empty = function(s)
return type(s) == "string" and string.len(s) > 0
end
--- @param s any
--- @return boolean
M.blank = function(s)
return type(s) ~= "string" or string.len(vim.trim(s)) == 0
end
--- @param s any
--- @return boolean
M.not_blank = function(s)
return type(s) == "string" and string.len(vim.trim(s)) > 0
end
--- @param s string
--- @param t string
--- @param start integer? by default start=1
--- @return integer?
M.find = function(s, t, start)
assert(type(s) == "string")
assert(type(t) == "string")
start = start or 1
for i = start, #s do
local match = true
for j = 1, #t do
if i + j - 1 > #s then
match = false
break
end
local a = string.byte(s, i + j - 1)
local b = string.byte(t, j)
if a ~= b then
match = false
break
end
end
if match then
return i
end
end
return nil
end
--- @param s string
--- @param t string
--- @param rstart integer? by default rstart=#s
--- @return integer?
M.rfind = function(s, t, rstart)
assert(type(s) == "string")
assert(type(t) == "string")
rstart = rstart or #s
for i = rstart, 1, -1 do
local match = true
for j = 1, #t do
if i + j - 1 > #s then
match = false
break
end
local a = string.byte(s, i + j - 1)
local b = string.byte(t, j)
if a ~= b then
match = false
break
end
end
if match then
return i
end
end
return nil
end
--- @param s string
--- @param t string? by default is whitespace
--- @return string
M.ltrim = function(s, t)
assert(type(s) == "string")
assert(type(t) == "string" or t == nil)
t = t or "%s+"
---@diagnostic disable-next-line: redundant-return-value
return string.gsub(s, "^" .. t, "")
end
--- @param s string
--- @param t string? by default is whitespace
--- @return string
M.rtrim = function(s, t)
assert(type(s) == "string")
assert(type(t) == "string" or t == nil)
t = t or "%s+"
---@diagnostic disable-next-line: redundant-return-value
return string.gsub(s, t .. "$", "")
end
--- @param s string
--- @param t string? by default is whitespace
--- @return string
M.trim = function(s, t)
assert(type(s) == "string")
assert(type(t) == "string" or t == nil)
return M.rtrim(M.ltrim(s, t), t)
end
--- @param s string
--- @param sep string
--- @param opts {plain:boolean?,trimempty:boolean?}? by default opts={plain=true,trimempty=false}
--- @return string[]
M.split = function(s, sep, opts)
assert(type(s) == "string")
assert(type(sep) == "string")
opts = opts or {
plain = true,
trimempty = true,
}
opts.plain = type(opts.plain) == "boolean" and opts.plain or true
opts.trimempty = type(opts.trimempty) == "boolean" and opts.trimempty or false
return vim.split(s, sep, opts)
end
--- @param s string
--- @param t string
--- @param opts {ignorecase:boolean?}?
--- @return boolean
M.startswith = function(s, t, opts)
assert(type(s) == "string")
assert(type(t) == "string")
opts = opts or { ignorecase = false }
opts.ignorecase = type(opts.ignorecase) == "boolean" and opts.ignorecase
or false
if opts.ignorecase then
return string.len(s) >= string.len(t) and s:sub(1, #t):lower() == t:lower()
else
return string.len(s) >= string.len(t) and s:sub(1, #t) == t
end
end
--- @param s string
--- @param t string
--- @param opts {ignorecase:boolean?}?
--- @return boolean
M.endswith = function(s, t, opts)
assert(type(s) == "string")
assert(type(t) == "string")
opts = opts or { ignorecase = false }
opts.ignorecase = type(opts.ignorecase) == "boolean" and opts.ignorecase
or false
if opts.ignorecase then
return string.len(s) >= string.len(t)
and s:sub(#s - #t + 1):lower() == t:lower()
else
return string.len(s) >= string.len(t) and s:sub(#s - #t + 1) == t
end
end
--- @param c string
--- @return boolean
M.isspace = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%s") ~= nil
end
--- @param c string
--- @return boolean
M.isalnum = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%w") ~= nil
end
--- @param c string
--- @return boolean
M.isdigit = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%d") ~= nil
end
--- @param c string
--- @return boolean
M.isxdigit = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%x") ~= nil
end
--- @param c string
--- @return boolean
M.isalpha = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%a") ~= nil
end
--- @param c string
--- @return boolean
M.islower = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%l") ~= nil
end
--- @param c string
--- @return boolean
M.isupper = function(c)
assert(type(c) == "string")
assert(string.len(c) == 1)
return c:match("%u") ~= nil
end
--- @param s string
--- @param pos integer
--- @param ch string
--- @return string
M.setchar = function(s, pos, ch)
assert(type(s) == "string")
assert(type(pos) == "number")
assert(type(ch) == "string")
assert(string.len(ch) == 1)
local n = string.len(s)
pos = require("gentags.commons.tables").list_index(pos, n)
local buffer = ""
if pos > 1 then
buffer = string.sub(s, 1, pos - 1)
end
buffer = buffer .. ch
if pos < n then
buffer = buffer .. string.sub(s, pos + 1)
end
return buffer
end
--- @param s string
--- @return string[]
M.tochars = function(s)
assert(type(s) == "string")
local l = {}
local n = string.len(s)
for i = 1, n do
table.insert(l, string.sub(s, i, i))
end
return l
end
return M

View File

@@ -0,0 +1,95 @@
local M = {}
--- @param t any?
--- @return boolean
M.tbl_empty = function(t)
return type(t) ~= "table" or vim.tbl_isempty(t)
end
--- @param t any?
--- @return boolean
M.tbl_not_empty = function(t)
return type(t) == "table" and not vim.tbl_isempty(t)
end
--- @param t any?
--- @param ... any
--- @return any
M.tbl_get = function(t, ...)
if vim.fn.has("nvim-0.10") > 0 and type(vim.tbl_get) == "function" then
return type(t) == "table" and vim.tbl_get(t, ...) or nil
end
local e = t --[[@as table]]
for _, k in ipairs({ ... }) do
if M.tbl_not_empty(e) and e[k] ~= nil then
e = e[k]
else
return nil
end
end
return e
end
--- @param t any[]
--- @param v any
--- @param compare (fun(a:any, b:any):boolean)|nil
--- @return boolean
M.tbl_contains = function(t, v, compare)
assert(type(t) == "table")
for k, item in pairs(t) do
if type(compare) == "function" then
if compare(item, v) then
return true
end
else
if item == v then
return true
end
end
end
return false
end
--- @param l any?
--- @return boolean
M.list_empty = function(l)
return type(l) ~= "table" or #l == 0
end
--- @param l any?
--- @return boolean
M.list_not_empty = function(l)
return type(l) == "table" and #l > 0
end
--- @param i integer
--- @param n integer
--- @return integer
M.list_index = function(i, n)
assert(n > 0)
assert((i >= 1 and i <= n) or (i <= -1 and i >= -n))
return i > 0 and i or (n + i + 1)
end
--- @param l any[]
--- @param v any
--- @param compare (fun(a:any, b:any):boolean)|nil
--- @return boolean
M.list_contains = function(l, v, compare)
assert(type(l) == "table")
for _, item in ipairs(l) do
if type(compare) == "function" then
if compare(item, v) then
return true
end
else
if item == v then
return true
end
end
end
return false
end
return M

View File

@@ -0,0 +1,132 @@
local M = {}
--- @package
--- @param attr "fg"|"bg"
--- @param code string
--- @return string
M.escape = function(attr, code)
assert(type(code) == "string")
assert(attr == "bg" or attr == "fg")
local control = attr == "fg" and 38 or 48
local r, g, b = code:match("#(..)(..)(..)")
if r and g and b then
r = tonumber(r, 16)
g = tonumber(g, 16)
b = tonumber(b, 16)
return string.format("%d;2;%d;%d;%d", control, r, g, b)
else
return string.format("%d;5;%s", control, code)
end
end
-- Pre-defined CSS colors
-- Also see: https://www.quackit.com/css/css_color_codes.cfm
local CSS_COLORS = {
black = "0;30",
grey = M.escape("fg", "#808080"),
silver = M.escape("fg", "#c0c0c0"),
white = M.escape("fg", "#ffffff"),
violet = M.escape("fg", "#EE82EE"),
magenta = "0;35",
fuchsia = M.escape("fg", "#FF00FF"),
red = "0;31",
purple = M.escape("fg", "#800080"),
indigo = M.escape("fg", "#4B0082"),
yellow = "0;33",
gold = M.escape("fg", "#FFD700"),
orange = M.escape("fg", "#FFA500"),
chocolate = M.escape("fg", "#D2691E"),
olive = M.escape("fg", "#808000"),
green = "0;32",
lime = M.escape("fg", "#00FF00"),
teal = M.escape("fg", "#008080"),
cyan = "0;36",
aqua = M.escape("fg", "#00FFFF"),
blue = "0;34",
navy = M.escape("fg", "#000080"),
slateblue = M.escape("fg", "#6A5ACD"),
steelblue = M.escape("fg", "#4682B4"),
}
--- @param attr "fg"|"bg"
--- @param hl string
--- @return string?
M.retrieve = function(attr, hl)
assert(type(hl) == "string")
assert(attr == "bg" or attr == "fg")
local gui = vim.fn.has("termguicolors") > 0 and vim.o.termguicolors
local family = gui and "gui" or "cterm"
local pattern = gui and "^#[%l%d]+" or "^[%d]+$"
local code =
vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(hl)), attr, family) --[[@as string]]
if string.find(code, pattern) then
return code
end
return nil
end
--- @param text string the text content to be rendered
--- @param name string the ANSI color name or RGB color codes
--- @param hl string? the highlighting group name
--- @return string
M.render = function(text, name, hl)
local strings = require("gentags.commons.strings")
local fgfmt = nil
local fgcode = strings.not_empty(hl) and M.retrieve("fg", hl --[[@as string]])
or nil
if type(fgcode) == "string" then
fgfmt = M.escape("fg", fgcode)
elseif CSS_COLORS[name] then
fgfmt = CSS_COLORS[name]
else
fgfmt = M.escape("fg", name)
end
local fmt = nil
local bgcode = strings.not_empty(hl) and M.retrieve("bg", hl --[[@as string]])
or nil
if type(bgcode) == "string" then
local bgcolor = M.escape("bg", bgcode)
fmt = string.format("%s;%s", fgfmt, bgcolor)
else
fmt = fgfmt
end
return string.format("[%sm%s", fmt, text)
end
--- @param text string?
--- @return string?
M.erase = function(text)
assert(type(text) == "string")
local result, pos = text
:gsub("\x1b%[%d+m\x1b%[K", "")
:gsub("\x1b%[m\x1b%[K", "")
:gsub("\x1b%[%d+;%d+;%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+m", "")
:gsub("\x1b%[%d+m", "")
:gsub("\x1b%[m", "")
return result
end
--- @type string[]
M.COLOR_NAMES = {}
do
for name, code in pairs(CSS_COLORS) do
--- @param text string
--- @param hl string?
--- @return string
M[name] = function(text, hl)
return M.render(text, name, hl)
end
table.insert(M.COLOR_NAMES, name)
end
end
return M

View File

@@ -0,0 +1,5 @@
-- Luv wrapper
local M = (vim.fn.has("nvim-0.10") > 0 and vim.uv ~= nil) and vim.uv or vim.loop
return M

View File

@@ -0,0 +1 @@
3.5.0

View File

@@ -0,0 +1,32 @@
-- Compatible Neovim window related API
local M = {}
--- @deprecated
--- @see commons.apis
--- @param winnr integer
--- @param name string
--- @return any
M.get_win_option = function(winnr, name)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_get_option_value(name, { win = winnr })
else
return vim.api.nvim_win_get_option(winnr, name)
end
end
--- @deprecated
--- @see commons.apis
--- @param winnr integer
--- @param name string
--- @param value any
--- @return any
M.set_win_option = function(winnr, name, value)
if vim.fn.has("nvim-0.8") > 0 then
return vim.api.nvim_set_option_value(name, value, { win = winnr })
else
return vim.api.nvim_win_set_option(winnr, name, value)
end
end
return M

83
lua/gentags/configs.lua Normal file
View File

@@ -0,0 +1,83 @@
local M = {}
--- @alias gentags.Options table<any, any>
--- @type gentags.Options
local Defaults = {
-- binary name
tool = "ctags",
-- ctags options
ctags = { "--tag-relative=never" },
-- workspace detection
workspace = { ".git", ".svn" },
-- excluded filetypes
exclude_filetypes = { "neo-tree", "NvimTree" },
-- excluded workspace
exclude_workspaces = {},
-- excluded workspace
exclude_files = {},
-- cache directory
-- For *NIX: `~/.cache/nvim/gentags.nvim`.
-- For Windows: `$env:USERPROFILE\AppData\Local\Temp\nvim\gentags.nvim`.
cache_dir = vim.fn.stdpath("cache") .. "/gentags.nvim",
-- garbage collection
gc = {
-- policy:
-- - LRU (least recently used): remove the least recently used.
--- @type "LRU"
policy = "LRU",
-- trigger by:
-- * count: by tags cache count, for example: 100.
-- * size: by tags cache size, for example: 1GB, 300MB, 4096KB, with suffix "GB", "MG", "KB".
--
--- @type {name:"count"|"size",value:string|integer}|nil
trigger = nil,
-- tags cache that will be exclude from gc.
exclude = {},
},
-- user command
command = { name = "GenTags", desc = "Generate tags" },
-- debug options
debug = {
-- enable debug mode
enable = false,
-- print logs to messages.
console_log = true,
-- write logs to file.
-- For *NIX: `~/.local/share/nvim/gentags.log`.
-- For Windows: `$env:USERPROFILE\AppData\Local\nvim-data\gentags.log`.
file_log = false,
},
}
--- @type gentags.Options
local Configs = {}
--- @return gentags.Options
M.get = function()
return Configs
end
--- @param opts gentags.Options?
--- @return gentags.Options
M.setup = function(opts)
local workspace = vim.deepcopy(Defaults.workspace)
Configs = vim.tbl_deep_extend("force", vim.deepcopy(Defaults), opts or {})
Configs.workspace =
vim.list_extend(vim.deepcopy(Configs.workspace), workspace)
return Configs
end
return M

208
lua/gentags/ctags.lua Normal file
View File

@@ -0,0 +1,208 @@
local logging = require("gentags.commons.logging")
local spawn = require("gentags.commons.spawn")
local tables = require("gentags.commons.tables")
local strings = require("gentags.commons.strings")
local configs = require("gentags.configs")
local M = {}
--- @table<integer|string, vim.SystemObj>
local JOBS_MAP = {}
--- @table<string, boolean>
local TAGS_LOCKING_MAP = {}
--- @table<string, boolean>
local TAGS_LOADED_MAP = {}
--- @param ctx gentags.Context
M.load = function(ctx)
local logger = logging.get("gentags") --[[@as commons.logging.Logger]]
logger:debug("|load| ctx:%s", vim.inspect(ctx))
if
strings.not_empty(ctx.tags_file)
and not TAGS_LOADED_MAP[ctx.tags_file]
and vim.fn.filereadable(ctx.tags_file) > 0
then
logger:debug("|load| append tags_file:%s", vim.inspect(ctx.tags_file))
vim.opt.tags:append(ctx.tags_file)
TAGS_LOADED_MAP[ctx.tags_file] = true
end
if
strings.not_empty(ctx.tags_pattern)
and not TAGS_LOADED_MAP[ctx.tags_pattern]
then
logger:debug("|load| append tags_pattern:%s", vim.inspect(ctx.tags_pattern))
vim.opt.tags:append(ctx.tags_pattern)
TAGS_LOADED_MAP[ctx.tags_pattern] = true
end
end
--- @param ctx gentags.Context
M.init = function(ctx)
local logger = logging.get("gentags") --[[@as commons.logging.Logger]]
logger:debug("|run| ctx:%s", vim.inspect(ctx))
-- no tags name
if strings.empty(ctx.tags_file) then
return
end
-- tags name already exist, e.g. already running ctags for this tags
if TAGS_LOCKING_MAP[ctx.tags_file] then
return
end
local tmpfile = vim.fn.tempname() --[[@as string]]
if strings.empty(tmpfile) then
return
end
local system_obj = nil
local function _on_stdout(line)
logger:debug("|run._on_stdout| line:%s", vim.inspect(line))
end
local function _on_stderr(line)
logger:debug("|run._on_stderr| line:%s", vim.inspect(line))
end
local function _close_file(fp)
if fp then
fp:close()
end
end
local function _on_exit(completed)
-- logger:debug(
-- "|run._on_exit| completed:%s, sysobj:%s, JOBS_MAP:%s",
-- vim.inspect(completed),
-- vim.inspect(sysobj),
-- vim.inspect(JOBS_MAP)
-- )
-- swap tmp file and tags file
local fp1 = io.open(ctx.tags_file, "w")
local fp2 = io.open(tmpfile, "r")
if fp1 == nil or fp2 == nil then
if fp1 == nil then
logger:err(
"|init._on_exit| failed to open tags file:%s",
vim.inspect(ctx.tags_file)
)
end
if fp2 == nil then
logger:err(
"|init._on_exit| failed to open tmp file:%s",
vim.inspect(tmpfile)
)
end
_close_file(fp1)
_close_file(fp2)
end
---@diagnostic disable-next-line: need-check-nil
local content = fp2:read("*a")
if content then
---@diagnostic disable-next-line: need-check-nil
fp1:write(content)
end
_close_file(fp1)
_close_file(fp2)
logger:debug(
"|init._on_exit| tags generate completed to:%s",
vim.inspect(ctx.tags_file)
)
if system_obj == nil then
logger:err(
"|init._on_exit| system_obj %s must not be nil!",
vim.inspect(system_obj)
)
end
if system_obj ~= nil then
if JOBS_MAP[system_obj.pid] == nil then
logger:debug(
"|init._on_exit| debug-error! job id %s must exist!",
vim.inspect(system_obj)
)
end
JOBS_MAP[system_obj.pid] = nil
end
if TAGS_LOCKING_MAP[ctx.tags_file] == nil then
logger:debug(
"|init._on_exit| debug-error! tags %s must be locked!",
vim.inspect(ctx)
)
end
TAGS_LOCKING_MAP[ctx.tags_file] = nil
end
local cfg = configs.get()
local opts = vim.deepcopy(tables.tbl_get(cfg, "ctags") or {})
local cwd = nil
if ctx.mode == "workspace" then
assert(strings.not_empty(ctx.workspace))
cwd = ctx.workspace
table.insert(opts, "-R")
else
assert(ctx.mode == "file")
assert(strings.not_empty(ctx.filename))
table.insert(opts, "-L")
table.insert(opts, ctx.filename)
end
-- output tags file
table.insert(opts, "-f")
table.insert(opts, tmpfile)
local cmds = { "ctags", unpack(opts) }
logger:debug("|run| cmds:%s", vim.inspect(cmds))
system_obj = spawn.run(cmds, {
cwd = cwd,
on_stdout = _on_stdout,
on_stderr = _on_stderr,
}, _on_exit)
assert(system_obj ~= nil)
JOBS_MAP[system_obj.pid] = system_obj
TAGS_LOCKING_MAP[ctx.tags_file] = true
end
--- @param ctx gentags.Context
M.update = function(ctx)
M.init(ctx)
end
--- @param ctx gentags.Context
--- @return gentags.StatusInfo
M.status = function(ctx)
local running = tables.tbl_not_empty(JOBS_MAP)
local jobs = 0
for pid, system_obj in pairs(JOBS_MAP) do
jobs = jobs + 1
end
return {
running = running,
jobs = jobs,
}
end
M.terminate = function()
for pid, system_obj in pairs(JOBS_MAP) do
if system_obj ~= nil then
system_obj:kill(9)
end
end
JOBS_MAP = {}
TAGS_LOCKING_MAP = {}
end
return M

122
lua/gentags/dispatcher.lua Normal file
View File

@@ -0,0 +1,122 @@
local logging = require("gentags.commons.logging")
local strings = require("gentags.commons.strings")
local paths = require("gentags.commons.paths")
local tables = require("gentags.commons.tables")
local configs = require("gentags.configs")
local utils = require("gentags.utils")
local M = {}
-- A tool module has these APIs: load/init/update/terminate
--
--- @alias gentags.Context {workspace:string?,filename:string?,filetype:string?,tags_file:string?,tags_handle:string?,tags_pattern:string?,mode:"workspace"|"file"}
--- @alias gentags.LoadMethod fun(ctx:gentags.Context):nil
--- @alias gentags.InitMethod fun(ctx:gentags.Context):nil
--- @alias gentags.UpdateMethod fun(ctx:gentags.Context):nil
--- @alias gentags.TerminateMethod fun(ctx:gentags.Context):nil
--- @alias gentags.StatusInfo {running:boolean,jobs:integer}
--- @alias gentags.StatusMethod fun(ctx:gentags.Context):gentags.StatusInfo
--- @alias gentags.Tool {load:gentags.LoadMethod,init:gentags.InitMethod,update:gentags.UpdateMethod,terminate:gentags.TerminateMethod,status:gentags.StatusMethod}
--- @type table<string, gentags.Tool>
local TOOLS_MAP = {
ctags = require("gentags.ctags"),
}
--- @return gentags.Tool
local function get_tool()
local cfg = configs.get()
local tool = cfg.tool
local toolchain = TOOLS_MAP[string.lower(tool)] --[[@as gentags.Tool]]
assert(
toolchain ~= nil,
string.format("%s is not supported!", vim.inspect(tool))
)
return toolchain
end
--- @return gentags.Context
local function get_context()
local cfg = configs.get()
local logger = logging.get("gentags") --[[@as commons.logging.Logger]]
local workspace = utils.get_workspace()
logger:debug("|get_context| workspace:%s", vim.inspect(workspace))
local filename = utils.get_filename()
local filetype = utils.get_filetype()
local tags_handle = nil
local tags_file = nil
local tags_pattern = nil
if strings.not_empty(workspace) then
tags_handle = utils.get_tags_handle(workspace --[[@as string]])
tags_file = utils.get_tags_file(tags_handle --[[@as string]])
tags_pattern = utils.get_tags_pattern(tags_handle --[[@as string]])
elseif
strings.not_empty(filename)
and not tables.list_contains(cfg.exclude_filetypes or {}, filetype)
then
tags_handle = utils.get_tags_handle(filename)
tags_file = utils.get_tags_file(tags_handle)
tags_pattern = utils.get_tags_pattern(tags_handle --[[@as string]])
end
local mode = strings.not_empty(workspace) and "workspace" or "file"
return {
workspace = workspace,
filename = filename,
filetype = filetype,
tags_file = tags_file,
tags_handle = tags_handle,
tags_pattern = tags_pattern,
mode = mode,
}
end
M.load = function()
vim.schedule(function()
get_tool().load(get_context())
end)
end
M.init = function()
vim.schedule(function()
get_tool().init(get_context())
end)
end
M.update = function()
vim.schedule(function()
get_tool().update(get_context())
end)
end
M.terminate = function()
get_tool().terminate(get_context())
end
M.status = function()
get_tool().status(get_context())
end
local gc_running = false
M.gc = function()
vim.schedule(function()
if gc_running then
return
end
gc_running = true
-- run gc here
vim.schedule(function()
gc_running = false
end)
end)
end
return M

View File

130
lua/gentags/utils.lua Normal file
View File

@@ -0,0 +1,130 @@
local logging = require("gentags.commons.logging")
local paths = require("gentags.commons.paths")
local strings = require("gentags.commons.strings")
local uv = require("gentags.commons.uv")
local configs = require("gentags.configs")
local M = {}
--- @param cwd string?
--- @return string?
M.get_workspace = function(cwd)
-- local logger = logging.get("gentags") --[[@as commons.logging.Logger]]
cwd = cwd or vim.fn.getcwd()
while true do
-- logger:debug("|get_workspace| 0-cwd:%s", vim.inspect(cwd))
for _, pattern in ipairs(configs.get().workspace) do
local target = paths.join(cwd, pattern)
-- logger:debug(
-- "|get_workspace| 1-cwd:%s, target:%s",
-- vim.inspect(cwd),
-- vim.inspect(target)
-- )
target =
paths.normalize(target, { double_backslash = true, expand = true })
local stat_result, stat_err = uv.fs_stat(target)
-- logger:debug(
-- "|get_workspace| 2-cwd:%s, target:%s, stat result:%s, stat err:%s",
-- vim.inspect(cwd),
-- vim.inspect(target),
-- vim.inspect(stat_result),
-- vim.inspect(stat_err)
-- )
if stat_result then
return cwd
end
end
local parent = paths.parent(cwd)
-- logger:debug(
-- "|get_workspace| 3-cwd:%s, parent:%s",
-- vim.inspect(cwd),
-- vim.inspect(parent)
-- )
if strings.blank(parent) then
break
end
cwd = parent
end
return nil
end
--- @return string
M.get_filename = function()
local bufnr = vim.api.nvim_get_current_buf()
local filename = paths.normalize(
vim.api.nvim_buf_get_name(bufnr),
{ double_backslash = true, expand = true }
)
return filename
end
--- @return string
M.get_filetype = function()
return vim.bo.filetype
end
--- @param filepath string?
--- @return string?
M.get_tags_handle = function(filepath)
if strings.empty(filepath) then
return nil
end
while
strings.not_empty(filepath)
and strings.endswith(filepath --[[@as string]], "/")
or strings.endswith(filepath --[[@as string]], "\\")
do
filepath = string.sub(filepath --[[@as string]], 1, #filepath - 1)
end
while
strings.not_empty(filepath)
and strings.startswith(filepath --[[@as string]], "/")
or strings.startswith(filepath --[[@as string]], "\\")
do
filepath = string.sub(filepath --[[@as string]], 2)
end
filepath = paths.normalize(
filepath --[[@as string]],
{ double_backslash = true, expand = true }
)
filepath = filepath:gsub("/", "%-"):gsub(" ", "%-"):gsub(":", "%-")
while strings.startswith(filepath, "-") do
filepath = string.sub(filepath, 2)
end
while strings.endswith(filepath, "-") do
filepath = string.sub(filepath, 1, #filepath - 1)
end
local cache_dir = configs.get().cache_dir
return paths.join(cache_dir, filepath)
end
--- @param tags_handle string?
--- @return string?
M.get_tags_file = function(tags_handle)
if strings.empty(tags_handle) then
return nil
end
return tags_handle .. "-tags"
end
--- @param tags_handle string?
--- @return string?
M.get_tags_pattern = function(tags_handle)
if strings.empty(tags_handle) then
return nil
end
return tags_handle .. "*tags"
end
--- @param filepath string
--- @return boolean
M.tags_exists = function(filepath)
local tags_filename = M.get_tags_file(filepath)
return vim.fn.filereadable(tags_filename) > 0
end
return M

View File

@@ -0,0 +1,24 @@
local cwd = vim.fn.getcwd()
describe("gentags.configs", function()
local assert_eq = assert.is_equal
local assert_true = assert.is_true
local assert_false = assert.is_false
before_each(function()
vim.api.nvim_command("cd " .. cwd)
end)
local github_actions = os.getenv("GITHUB_ACTIONS") == "true"
local configs = require("gentags.configs")
describe("[configs]", function()
it("setup", function()
local cfg = configs.setup()
assert_eq(type(cfg), "table")
local cfg2 = configs.get()
assert_eq(type(cfg2), "table")
assert_true(vim.deep_equal(cfg, cfg2))
end)
end)
end)

View File

@@ -0,0 +1,19 @@
local cwd = vim.fn.getcwd()
describe("gentags.ctags", function()
local assert_eq = assert.is_equal
local assert_true = assert.is_true
local assert_false = assert.is_false
before_each(function()
vim.api.nvim_command("cd " .. cwd)
end)
local github_actions = os.getenv("GITHUB_ACTIONS") == "true"
local ctags = require("gentags.ctags")
require("gentags").setup()
describe("[run]", function()
it("test", function() end)
end)
end)

View File

@@ -0,0 +1,49 @@
local cwd = vim.fn.getcwd()
describe("gentags.utils", function()
local assert_eq = assert.is_equal
local assert_true = assert.is_true
local assert_false = assert.is_false
before_each(function()
vim.api.nvim_command("cd " .. cwd)
end)
local github_actions = os.getenv("GITHUB_ACTIONS") == "true"
local utils = require("gentags.utils")
require("gentags").setup()
describe("[get_workspace]", function()
it("test", function()
local actual1 = utils.get_workspace()
if actual1 then
assert_eq(type(actual1), "string")
assert_true(vim.fn.isdirectory(actual1) > 0)
end
local actual2 = utils.get_workspace(vim.fn.expand("~"))
if actual2 then
assert_eq(type(actual2), "string")
assert_true(vim.fn.isdirectory(actual2) > 0)
end
end)
end)
describe("[get_filename]", function()
it("test", function()
vim.cmd([[edit README.md]])
local actual1 = utils.get_filename()
if actual1 then
assert_eq(type(actual1), "string")
assert_true(vim.fn.filereadable(actual1) > 0)
end
end)
end)
describe("[get_filetype]", function()
it("test", function()
vim.cmd([[edit README.md]])
local actual1 = utils.get_filetype()
if actual1 then
assert_eq(type(actual1), "string")
end
end)
end)
end)

View File

@@ -14,7 +14,12 @@ describe("gentags", function()
local gentags = require("gentags")
describe("[setup]", function()
it("test", function()
gentags.setup()
gentags.setup({
debug = {
enable = true,
file_log = true,
},
})
end)
end)
end)