feat(mvp): MVP (#3)
This commit is contained in:
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -41,3 +41,6 @@ luac.out
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# tags
|
||||
tags
|
||||
|
||||
2
.luacov
2
.luacov
@@ -4,5 +4,5 @@ modules = {
|
||||
}
|
||||
|
||||
exclude = {
|
||||
"lua/actboy168_json.lua",
|
||||
"lua/gentags/commons/*.lua",
|
||||
}
|
||||
|
||||
1
.styluaignore
Normal file
1
.styluaignore
Normal file
@@ -0,0 +1 @@
|
||||
lua/gentags/commons/*.lua
|
||||
91
README.md
91
README.md
@@ -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 ≥ 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
|
||||
|
||||
@@ -7,4 +7,4 @@ coverage:
|
||||
default:
|
||||
threshold: 90%
|
||||
ignore:
|
||||
- "lua/actboy168_json.lua"
|
||||
- "lua/gentags/commons/*.lua"
|
||||
|
||||
@@ -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
|
||||
|
||||
374
lua/gentags/commons/_system.lua
Normal file
374
lua/gentags/commons/_system.lua
Normal 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
|
||||
6
lua/gentags/commons/_system_types.lua
Normal file
6
lua/gentags/commons/_system_types.lua
Normal 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>
|
||||
56
lua/gentags/commons/apis.lua
Normal file
56
lua/gentags/commons/apis.lua
Normal 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
|
||||
29
lua/gentags/commons/buffers.lua
Normal file
29
lua/gentags/commons/buffers.lua
Normal 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
|
||||
327
lua/gentags/commons/fileios.lua
Normal file
327
lua/gentags/commons/fileios.lua
Normal 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
|
||||
29
lua/gentags/commons/jsons.lua
Normal file
29
lua/gentags/commons/jsons.lua
Normal 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
|
||||
676
lua/gentags/commons/logging.lua
Normal file
676
lua/gentags/commons/logging.lua
Normal 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
|
||||
182
lua/gentags/commons/numbers.lua
Normal file
182
lua/gentags/commons/numbers.lua
Normal 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
|
||||
108
lua/gentags/commons/paths.lua
Normal file
108
lua/gentags/commons/paths.lua
Normal 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
|
||||
227
lua/gentags/commons/ringbuf.lua
Normal file
227
lua/gentags/commons/ringbuf.lua
Normal 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
|
||||
124
lua/gentags/commons/spawn.lua
Normal file
124
lua/gentags/commons/spawn.lua
Normal 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
|
||||
268
lua/gentags/commons/strings.lua
Normal file
268
lua/gentags/commons/strings.lua
Normal 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
|
||||
95
lua/gentags/commons/tables.lua
Normal file
95
lua/gentags/commons/tables.lua
Normal 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
|
||||
132
lua/gentags/commons/termcolors.lua
Normal file
132
lua/gentags/commons/termcolors.lua
Normal 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[0m", 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
|
||||
5
lua/gentags/commons/uv.lua
Normal file
5
lua/gentags/commons/uv.lua
Normal 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
|
||||
1
lua/gentags/commons/version.txt
Normal file
1
lua/gentags/commons/version.txt
Normal file
@@ -0,0 +1 @@
|
||||
3.5.0
|
||||
32
lua/gentags/commons/windows.lua
Normal file
32
lua/gentags/commons/windows.lua
Normal 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
83
lua/gentags/configs.lua
Normal 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
208
lua/gentags/ctags.lua
Normal 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
122
lua/gentags/dispatcher.lua
Normal 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
|
||||
130
lua/gentags/utils.lua
Normal file
130
lua/gentags/utils.lua
Normal 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
|
||||
24
test/gentags/configs_spec.lua
Normal file
24
test/gentags/configs_spec.lua
Normal 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)
|
||||
19
test/gentags/ctags_spec.lua
Normal file
19
test/gentags/ctags_spec.lua
Normal 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)
|
||||
49
test/gentags/utils_spec.lua
Normal file
49
test/gentags/utils_spec.lua
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user