├── LICENSE
├── Makefile
├── README.md
├── lua
└── automa
│ ├── event.lua
│ ├── init.lua
│ ├── kit
│ ├── App
│ │ ├── Cache.lua
│ │ ├── Character.lua
│ │ ├── Config.lua
│ │ └── Event.lua
│ ├── Async
│ │ ├── AsyncTask.lua
│ │ ├── Worker.lua
│ │ └── init.lua
│ ├── IO
│ │ └── init.lua
│ ├── LSP
│ │ ├── Client.lua
│ │ ├── DocumentSelector.lua
│ │ ├── LanguageId.lua
│ │ ├── Position.lua
│ │ ├── Range.lua
│ │ └── init.lua
│ ├── Spec
│ │ └── init.lua
│ ├── System
│ │ └── init.lua
│ ├── Vim
│ │ ├── FloatingWindow.lua
│ │ ├── Keymap.lua
│ │ ├── RegExp.lua
│ │ └── Syntax.lua
│ └── init.lua
│ ├── query.lua
│ └── query.spec.lua
└── plugin
└── automa.lua
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 hrsh7th
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: lint
2 | lint:
3 | luacheck ./lua
4 |
5 | .PHONY: test
6 | test:
7 | vusted --output=gtest --pattern=.spec ./lua
8 |
9 | .PHONY: pre-commit
10 | pre-commit:
11 | luacheck lua
12 | vusted lua
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | nvim-automa
2 | ============================================================
3 |
4 | Automatic macro recording and playback for Neovim.
5 |
6 | ## Installation
7 |
8 | ```vim
9 | Plug 'nvim-automa'
10 | ```
11 |
12 | ## Usage
13 |
14 | ```lua
15 | local automa = require'automa'
16 | automa.setup({
17 | mapping = {
18 | ['.'] = {
19 | queries = {
20 | -- wide-range dot-repeat definition.
21 | automa.query_v1({ '!n(h,j,k,l)+' }),
22 | }
23 | },
24 | }
25 | })
26 | ```
27 |
28 | ## Status
29 |
30 | It works but query grammar is not stable.
31 |
32 | ## FAQ
33 |
34 | ### What's the benefit of this plugin?
35 |
36 | 1. The macro is more powerful than dot-repeat. Because it covers complex insert-mode key sequences.
37 | For example, if you type `/` in insert-mode, dot-repeat will not work, but the macro will include and repeat `/`.
38 |
39 | 2. This plugin allows you to specify the range of dot-repeat.
40 | For example, the `README.md`'s setting repeats all key sequences up to `h/j/k/l` in normal-mode.
41 | In other words, `h/j/k/l` becomes the macro recording boundary.
42 |
43 |
44 | ### How to debug queries?
45 |
46 | You can use `:AutomaToggleDebugger` for it.
47 |
48 |
49 | ### `README.md`'s setting is not suitable to me.
50 |
51 | You can change query definition by yourself.
52 |
53 | ```lua
54 | local automa = require('automa')
55 | automa.setup({
56 | mapping = {
57 | ['.'] = {
58 | queries = {
59 | -- for `diwi***`
60 | automa.query_v1({ 'n', 'no+', 'n', 'i*' }),
61 | -- for `x`
62 | automa.query_v1({ 'n#' }),
63 | -- for `i***`
64 | automa.query_v1({ 'n', 'i*' }),
65 | -- for `vjjj>`
66 | automa.query_v1({ 'n', 'v*' }),
67 | }
68 | },
69 | }
70 | })
71 | ```
72 |
73 | ### How to replace captured repeat keys?
74 |
75 | There are two ways to accomplish this.
76 |
77 | ##### 1. You can define your own `automa.Query` function.
78 |
79 | ```lua
80 | local automa = require('automa')
81 | automa.setup {
82 | mapping = {
83 | ['.'] = {
84 | queries = {
85 |
86 | ...
87 |
88 | function(events)
89 | local result = automa.query_v1({ 'n', 'V*' })(events)
90 | if result then
91 | return {
92 | s_idx = result.s_idx,
93 | e_idx = result.e_idx,
94 | typed = vim.keycode('normal! .')
95 | }
96 | end
97 | end,
98 |
99 | ...
100 |
101 | }
102 | },
103 | }
104 | }
105 | ```
106 |
107 | ##### 2. You can use `convert` option.
108 |
109 | ```lua
110 | local automa = require('automa')
111 | automa.setup {
112 | mapping = {
113 | ['.'] = {
114 | convert = function(result)
115 | if result.typed:match('[><]$') then
116 | result.typed = vim.keycode('normal! .')
117 | end
118 | return result
119 | end,
120 | queries = {
121 |
122 | ...
123 |
124 | automa.query_v1({ 'n', 'V*' })
125 |
126 | ...
127 |
128 | }
129 | },
130 | }
131 | }
132 | ```
133 |
--------------------------------------------------------------------------------
/lua/automa/event.lua:
--------------------------------------------------------------------------------
1 | local Event = {}
2 |
3 | Event.prototype = {
4 | __tostring = function(self)
5 | return string.format(
6 | '%s%s(%s)%s',
7 | self.separator and '---------- ' or '',
8 | self.mode,
9 | self.char == Event.dummy and 'dummy' or vim.fn.keytrans(self.char),
10 | self.edit and '#' or ''
11 | )
12 | end
13 | }
14 |
15 | ---@type automa.Event
16 | Event.dummy = setmetatable({
17 | separator = true,
18 | fixed = true,
19 | char = '',
20 | mode = '',
21 | edit = false,
22 | bufnr = -1,
23 | changenr = -1,
24 | changedtick = -1,
25 | default_reg = ''
26 | }, Event.prototype)
27 |
28 | return Event
29 |
--------------------------------------------------------------------------------
/lua/automa/init.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 | local Query = require('automa.query')
3 | local Event = require('automa.event')
4 | local Keymap = require('automa.kit.Vim.Keymap')
5 |
6 | ---@class automa.Event
7 | ---@field separator boolean
8 | ---@field fixed boolean
9 | ---@field char string
10 | ---@field mode string
11 | ---@field edit boolean
12 | ---@field bufnr integer
13 | ---@field changenr integer
14 | ---@field changedtick integer
15 | ---@field reginfo string
16 |
17 | ---@class automa.Matcher
18 | ---@field negate boolean
19 | ---@field mode string
20 | ---@field chars string[]
21 | ---@field many boolean
22 | ---@field __call fun(events: automa.Event[], index: integer): boolean, integer
23 |
24 | ---@class automa.QueryResult
25 | ---@field s_idx integer
26 | ---@field e_idx integer
27 | ---@field reginfo string
28 | ---@field typed string
29 | ---@alias automa.Query fun(events: automa.Event[]): automa.QueryResult?
30 |
31 | ---@class automa.Config
32 | ---@field public mapping table
33 | ---@field public on_exec? fun()
34 | ---@field public on_done? fun()
35 |
36 | local P = {
37 | ---@type automa.Config
38 | config = {
39 | mapping = {}
40 | },
41 |
42 | ---@type automa.Event[]
43 | events = {},
44 |
45 | ---@type { regcntents: string[], regtype: string, isunnamed: boolean, points_to: string }
46 | reginfo = vim.fn.getreginfo(),
47 |
48 | ---@type { win: integer, buf: integer }
49 | debugger = {
50 | win = -1,
51 | buf = vim.api.nvim_create_buf(false, true),
52 | },
53 |
54 | ---@type integer
55 | namespace = vim.api.nvim_create_namespace("automa"),
56 |
57 | ---@type automa.Event
58 | prev_event = kit.clone(Event.dummy),
59 | }
60 |
61 | ---Add debug message
62 | ---@param s string
63 | function P.debug(s)
64 | vim.schedule(function()
65 | if vim.api.nvim_win_is_valid(P.debugger.win) then
66 | vim.api.nvim_buf_set_lines(P.debugger.buf, 0, 0, false, { s })
67 | vim.api.nvim_win_set_cursor(P.debugger.win, { 1, 0 })
68 | end
69 | end)
70 | end
71 |
72 | ---On key event
73 | function P.on_key(_, typed)
74 | if typed == '' or typed == nil then
75 | return
76 | end
77 |
78 | local mode = vim.api.nvim_get_mode().mode
79 | local bufnr = vim.api.nvim_get_current_buf()
80 | local changenr = vim.fn.changenr()
81 | local changedtick = vim.api.nvim_buf_get_changedtick(bufnr)
82 | local is_separator = typed == Event.dummy
83 | local is_automa_key = not is_separator and (mode == 'n' and P.config.mapping[Keymap.normalize(vim.fn.keytrans(typed))] ~= nil)
84 |
85 | -- ignore automa defined key.
86 | if is_automa_key then
87 | return
88 | end
89 |
90 | -- fix changedtick & changenr when accepting next key event.
91 | if not P.prev_event.fixed then
92 | local is_undo = P.prev_event.mode and P.prev_event.changenr > changenr
93 | local is_moved = P.prev_event.bufnr ~= bufnr
94 | P.prev_event.separator = P.prev_event.separator or is_undo or is_moved
95 | P.prev_event.fixed = true
96 | P.prev_event.edit = P.prev_event.mode and P.prev_event.changedtick ~= changedtick
97 | P.prev_event.changenr = changenr
98 | P.prev_event.changedtick = changedtick
99 | do
100 | local e = P.prev_event
101 | P.debug(('%s: %s'):format(#P.events, tostring(e)))
102 | end
103 | end
104 |
105 | local e = setmetatable({
106 | separator = is_separator,
107 | fixed = false,
108 | char = typed,
109 | mode = mode,
110 | move = false,
111 | edit = false,
112 | bufnr = bufnr,
113 | changenr = changenr,
114 | changedtick = changedtick,
115 | reginfo = P.reginfo,
116 | }, Event.prototype)
117 | table.insert(P.events, e)
118 | P.prev_event = e
119 | end
120 |
121 | ---The automa Public API.
122 | local M = {}
123 |
124 | ---@param user_config automa.Config
125 | function M.setup(user_config)
126 | user_config = user_config or {}
127 |
128 | local normalized_mapping = {}
129 | for k, v in pairs(user_config.mapping or {}) do
130 | normalized_mapping[Keymap.normalize(k)] = v
131 | end
132 | user_config.mapping = normalized_mapping
133 | P.config = kit.merge(user_config, P.config)
134 |
135 | -- Initialize mappings.
136 | do
137 | for key in pairs(P.config.mapping) do
138 | vim.keymap.set('n', key, function()
139 | return vim.fn.keytrans(M.fetch(key))
140 | end, { silent = true, expr = true, remap = true, replace_keycodes = true })
141 | end
142 | end
143 |
144 | -- Initialize `vim.on_key`
145 | do
146 | vim.on_key(P.on_key, P.namespace)
147 | end
148 |
149 | -- Initialize TextYankPost
150 | do
151 | local augroup = vim.api.nvim_create_augroup('automa', {
152 | clear = true
153 | })
154 | vim.api.nvim_create_autocmd('TextYankPost', {
155 | group = augroup,
156 | pattern = '*',
157 | callback = function()
158 | P.reginfo = vim.fn.getreginfo()
159 | P.debug(('<<>> %s'):format(P.reginfo.regcntents))
160 | end
161 | })
162 | end
163 | end
164 |
165 | ---Query function for the old style configuration.
166 | ---@param query_source string[]
167 | ---@return automa.Query
168 | function M.query_v1(query_source)
169 | return Query.make_query(query_source)
170 | end
171 |
172 | ---Get events
173 | function M.events()
174 | return P.events
175 | end
176 |
177 | ---Clear events
178 | function M.clear()
179 | P.events = {}
180 | for key in pairs(P.config.mapping) do
181 | vim.keymap.del('n', key)
182 | end
183 | end
184 |
185 | ---@param key string
186 | ---@return string
187 | function M.fetch(key)
188 | P.on_key(_, Event.dummy)
189 |
190 | local queries = kit.get(P.config, { 'mapping', key, 'queries' }) --[=[@as automa.Query[]]=]
191 | if not queries then
192 | error('The specified key is not defined in the mapping.')
193 | end
194 |
195 | local candidates = {} ---@type automa.QueryResult[]
196 | for _, query in ipairs(queries) do
197 | local candidate = query(P.events)
198 | if candidate then
199 | table.insert(candidates, candidate)
200 | end
201 | end
202 |
203 | if #candidates == 0 then
204 | return ''
205 | end
206 |
207 | local target = candidates[1]
208 | for _, candidate in ipairs(candidates) do
209 | if target.e_idx < candidate.e_idx then
210 | target = candidate
211 | elseif target.e_idx == candidate.e_idx then
212 | if target.s_idx > candidate.s_idx then
213 | target = candidate
214 | end
215 | end
216 | end
217 |
218 | local convert = kit.get(P.config, { 'mapping', key, 'convert' })
219 | if convert then
220 | target = convert(target)
221 | end
222 |
223 | P.debug(('<<>> %s'):format(target.s_idx, target.e_idx, vim.fn.keytrans(target.typed)))
224 |
225 | local reg = vim.fn.getreg(vim.v.register)
226 | return Keymap.to_sendable(function()
227 | if P.config.on_exec then
228 | P.config.on_exec()
229 | end
230 | vim.fn.setreg(vim.v.register, target.reginfo)
231 | end) .. target.typed .. Keymap.termcodes('') .. Keymap.to_sendable(function()
232 | vim.fn.setreg(vim.v.register, reg)
233 | if P.config.on_done then
234 | P.config.on_done()
235 | end
236 | end)
237 | end
238 |
239 | ---Toggle debug panel.
240 | function M.toggle_debug_panel()
241 | if vim.api.nvim_win_is_valid(P.debugger.win) then
242 | vim.api.nvim_win_close(P.debugger.win, true)
243 | P.debugger.win = -1
244 | else
245 | vim.cmd([[botright vertical 40new +wincmd\ p]])
246 | P.debugger.win = vim.fn.win_getid(vim.fn.winnr('#'))
247 | vim.api.nvim_win_set_buf(P.debugger.win, P.debugger.buf)
248 | end
249 | end
250 |
251 | return M
252 |
--------------------------------------------------------------------------------
/lua/automa/kit/App/Cache.lua:
--------------------------------------------------------------------------------
1 | ---Create cache key.
2 | ---@private
3 | ---@param key string[]|string
4 | ---@return string
5 | local function _key(key)
6 | if type(key) == 'table' then
7 | return table.concat(key, ':')
8 | end
9 | return key
10 | end
11 |
12 | ---@class automa.kit.App.Cache
13 | ---@field private keys table
14 | ---@field private entries table
15 | local Cache = {}
16 | Cache.__index = Cache
17 |
18 | ---Create new cache instance.
19 | function Cache.new()
20 | local self = setmetatable({}, Cache)
21 | self.keys = {}
22 | self.entries = {}
23 | return self
24 | end
25 |
26 | ---Get cache entry.
27 | ---@param key string[]|string
28 | ---@return any
29 | function Cache:get(key)
30 | return self.entries[_key(key)]
31 | end
32 |
33 | ---Set cache entry.
34 | ---@param key string[]|string
35 | ---@param val any
36 | function Cache:set(key, val)
37 | key = _key(key)
38 | self.keys[key] = true
39 | self.entries[key] = val
40 | end
41 |
42 | ---Delete cache entry.
43 | ---@param key string[]|string
44 | function Cache:del(key)
45 | key = _key(key)
46 | self.keys[key] = nil
47 | self.entries[key] = nil
48 | end
49 |
50 | ---Return this cache has the key entry or not.
51 | ---@param key string[]|string
52 | ---@return boolean
53 | function Cache:has(key)
54 | key = _key(key)
55 | return not not self.keys[key]
56 | end
57 |
58 | ---Ensure cache entry.
59 | ---@generic T
60 | ---@generic U
61 | ---@param key string[]|string
62 | ---@param callback function(...: U): T
63 | ---@param ... U
64 | ---@return T
65 | function Cache:ensure(key, callback, ...)
66 | if not self:has(key) then
67 | self:set(key, callback(...))
68 | end
69 | return self:get(key)
70 | end
71 |
72 | return Cache
73 |
--------------------------------------------------------------------------------
/lua/automa/kit/App/Character.lua:
--------------------------------------------------------------------------------
1 | ---@diagnostic disable: discard-returns
2 |
3 | local Character = {}
4 |
5 | ---@type table
6 | Character.alpha = {}
7 | string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
8 | Character.alpha[string.byte(char)] = char
9 | end)
10 |
11 | ---@type table
12 | Character.digit = {}
13 | string.gsub('1234567890', '.', function(char)
14 | Character.digit[string.byte(char)] = char
15 | end)
16 |
17 | ---@type table
18 | Character.white = {}
19 | string.gsub(' \t\n', '.', function(char)
20 | Character.white[string.byte(char)] = char
21 | end)
22 |
23 | ---Return specified byte is alpha or not.
24 | ---@param byte integer
25 | ---@return boolean
26 | function Character.is_alpha(byte)
27 | return Character.alpha[byte] ~= nil or Character.alpha[byte + 32] ~= nil
28 | end
29 |
30 | ---Return specified byte is digit or not.
31 | ---@param byte integer
32 | ---@return boolean
33 | function Character.is_digit(byte)
34 | return Character.digit[byte] ~= nil
35 | end
36 |
37 | ---Return specified byte is alpha or not.
38 | ---@param byte integer
39 | ---@return boolean
40 | function Character.is_alnum(byte)
41 | return Character.is_alpha(byte) or Character.is_digit(byte)
42 | end
43 |
44 | ---Return specified byte is white or not.
45 | ---@param byte integer
46 | ---@return boolean
47 | function Character.is_white(byte)
48 | return Character.white[byte] ~= nil
49 | end
50 |
51 | ---Return specified byte is symbol or not.
52 | ---@param byte integer
53 | ---@return boolean
54 | function Character.is_symbol(byte)
55 | return not Character.is_alnum(byte) and not Character.is_white(byte)
56 | end
57 |
58 | return Character
59 |
--------------------------------------------------------------------------------
/lua/automa/kit/App/Config.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 | local Cache = require('automa.kit.App.Cache')
3 |
4 | ---@class automa.kit.App.Config.Schema
5 |
6 | ---@alias automa.kit.App.Config.SchemaInternal automa.kit.App.Config.Schema|{ revision: integer }
7 |
8 | ---@class automa.kit.App.Config
9 | ---@field private _cache automa.kit.App.Cache
10 | ---@field private _default automa.kit.App.Config.SchemaInternal
11 | ---@field private _global automa.kit.App.Config.SchemaInternal
12 | ---@field private _filetype table
13 | ---@field private _buffer table
14 | local Config = {}
15 | Config.__index = Config
16 |
17 | ---Create new config instance.
18 | ---@param default automa.kit.App.Config.Schema
19 | function Config.new(default)
20 | local self = setmetatable({}, Config)
21 | self._cache = Cache.new()
22 | self._default = default
23 | self._global = {}
24 | self._filetype = {}
25 | self._buffer = {}
26 | return self
27 | end
28 |
29 | ---Update global config.
30 | ---@param config automa.kit.App.Config.Schema
31 | function Config:global(config)
32 | local revision = (self._global.revision or 1) + 1
33 | self._global = config or {}
34 | self._global.revision = revision
35 | end
36 |
37 | ---Update filetype config.
38 | ---@param filetypes string|string[]
39 | ---@param config automa.kit.App.Config.Schema
40 | function Config:filetype(filetypes, config)
41 | for _, filetype in ipairs(kit.to_array(filetypes)) do
42 | local revision = ((self._filetype[filetype] or {}).revision or 1) + 1
43 | self._filetype[filetype] = config or {}
44 | self._filetype[filetype].revision = revision
45 | end
46 | end
47 |
48 | ---Update filetype config.
49 | ---@param bufnr integer
50 | ---@param config automa.kit.App.Config.Schema
51 | function Config:buffer(bufnr, config)
52 | bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
53 | local revision = ((self._buffer[bufnr] or {}).revision or 1) + 1
54 | self._buffer[bufnr] = config or {}
55 | self._buffer[bufnr].revision = revision
56 | end
57 |
58 | ---Get current configuration.
59 | ---@return automa.kit.App.Config.Schema
60 | function Config:get()
61 | local filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 })
62 | local bufnr = vim.api.nvim_get_current_buf()
63 | return self._cache:ensure({
64 | tostring(self._global.revision or 0),
65 | tostring((self._buffer[bufnr] or {}).revision or 0),
66 | tostring((self._filetype[filetype] or {}).revision or 0),
67 | }, function()
68 | local config = self._default
69 | config = kit.merge(self._global, config)
70 | config = kit.merge(self._filetype[filetype] or {}, config)
71 | config = kit.merge(self._buffer[bufnr] or {}, config)
72 | config.revision = nil
73 | return config
74 | end)
75 | end
76 |
77 | ---Create setup interface.
78 | ---@return fun(config: automa.kit.App.Config.Schema)|{ filetype: fun(filetypes: string|string[], config: automa.kit.App.Config.Schema), buffer: fun(bufnr: integer, config: automa.kit.App.Config.Schema) }
79 | function Config:create_setup_interface()
80 | return setmetatable({
81 | ---@param filetypes string|string[]
82 | ---@param config automa.kit.App.Config.Schema
83 | filetype = function(filetypes, config)
84 | self:filetype(filetypes, config)
85 | end,
86 | ---@param bufnr integer
87 | ---@param config automa.kit.App.Config.Schema
88 | buffer = function(bufnr, config)
89 | self:buffer(bufnr, config)
90 | end,
91 | }, {
92 | ---@param config automa.kit.App.Config.Schema
93 | __call = function(_, config)
94 | self:global(config)
95 | end,
96 | })
97 | end
98 |
99 | return Config
100 |
--------------------------------------------------------------------------------
/lua/automa/kit/App/Event.lua:
--------------------------------------------------------------------------------
1 | ---@class automa.kit.App.Event
2 | ---@field private _events table
3 | local Event = {}
4 | Event.__index = Event
5 |
6 | ---Create new Event.
7 | function Event.new()
8 | local self = setmetatable({}, Event)
9 | self._events = {}
10 | return self
11 | end
12 |
13 | ---Register listener.
14 | ---@param name string
15 | ---@param listener function
16 | ---@return function
17 | function Event:on(name, listener)
18 | self._events[name] = self._events[name] or {}
19 | table.insert(self._events[name], listener)
20 | return function()
21 | self:off(name, listener)
22 | end
23 | end
24 |
25 | ---Register once listener.
26 | ---@param name string
27 | ---@param listener function
28 | function Event:once(name, listener)
29 | local off
30 | off = self:on(name, function(...)
31 | listener(...)
32 | off()
33 | end)
34 | end
35 |
36 | ---Off specified listener from event.
37 | ---@param name string
38 | ---@param listener function
39 | function Event:off(name, listener)
40 | self._events[name] = self._events[name] or {}
41 | if not listener then
42 | self._events[name] = nil
43 | else
44 | for i = #self._events[name], 1, -1 do
45 | if self._events[name][i] == listener then
46 | table.remove(self._events[name], i)
47 | break
48 | end
49 | end
50 | end
51 | end
52 |
53 | ---Return if the listener is registered.
54 | ---@param name string
55 | ---@param listener? function
56 | ---@return boolean
57 | function Event:has(name, listener)
58 | self._events[name] = self._events[name] or {}
59 | for _, v in ipairs(self._events[name]) do
60 | if v == listener then
61 | return true
62 | end
63 | end
64 | return false
65 | end
66 |
67 | ---Emit event.
68 | ---@param name string
69 | ---@vararg any
70 | function Event:emit(name, ...)
71 | self._events[name] = self._events[name] or {}
72 | for _, v in ipairs(self._events[name]) do
73 | v(...)
74 | end
75 | end
76 |
77 | return Event
78 |
--------------------------------------------------------------------------------
/lua/automa/kit/Async/AsyncTask.lua:
--------------------------------------------------------------------------------
1 | ---@diagnostic disable: invisible
2 | local uv = require('luv')
3 | local kit = require('automa.kit')
4 |
5 | local is_thread = vim.is_thread()
6 |
7 | ---@class automa.kit.Async.AsyncTask
8 | ---@field private value any
9 | ---@field private status automa.kit.Async.AsyncTask.Status
10 | ---@field private synced boolean
11 | ---@field private chained boolean
12 | ---@field private children (fun(): any)[]
13 | local AsyncTask = {}
14 | AsyncTask.__index = AsyncTask
15 |
16 | ---Settle the specified task.
17 | ---@param task automa.kit.Async.AsyncTask
18 | ---@param status automa.kit.Async.AsyncTask.Status
19 | ---@param value any
20 | local function settle(task, status, value)
21 | task.status = status
22 | task.value = value
23 | for _, c in ipairs(task.children) do
24 | c()
25 | end
26 |
27 | if status == AsyncTask.Status.Rejected then
28 | if not task.chained and not task.synced then
29 | local timer = uv.new_timer()
30 | timer:start(
31 | 0,
32 | 0,
33 | kit.safe_schedule_wrap(function()
34 | timer:stop()
35 | timer:close()
36 | if not task.chained and not task.synced then
37 | AsyncTask.on_unhandled_rejection(value)
38 | end
39 | end)
40 | )
41 | end
42 | end
43 | end
44 |
45 | ---@enum automa.kit.Async.AsyncTask.Status
46 | AsyncTask.Status = {
47 | Pending = 0,
48 | Fulfilled = 1,
49 | Rejected = 2,
50 | }
51 |
52 | ---Handle unhandled rejection.
53 | ---@param err any
54 | function AsyncTask.on_unhandled_rejection(err)
55 | error('AsyncTask.on_unhandled_rejection: ' .. vim.inspect(err), 2)
56 | end
57 |
58 | ---Return the value is AsyncTask or not.
59 | ---@param value any
60 | ---@return boolean
61 | function AsyncTask.is(value)
62 | return getmetatable(value) == AsyncTask
63 | end
64 |
65 | ---Resolve all tasks.
66 | ---@param tasks any[]
67 | ---@return automa.kit.Async.AsyncTask
68 | function AsyncTask.all(tasks)
69 | return AsyncTask.new(function(resolve, reject)
70 | local values = {}
71 | local count = 0
72 | for i, task in ipairs(tasks) do
73 | task:dispatch(function(value)
74 | values[i] = value
75 | count = count + 1
76 | if #tasks == count then
77 | resolve(values)
78 | end
79 | end, reject)
80 | end
81 | end)
82 | end
83 |
84 | ---Resolve first resolved task.
85 | ---@param tasks any[]
86 | ---@return automa.kit.Async.AsyncTask
87 | function AsyncTask.race(tasks)
88 | return AsyncTask.new(function(resolve, reject)
89 | for _, task in ipairs(tasks) do
90 | task:dispatch(resolve, reject)
91 | end
92 | end)
93 | end
94 |
95 | ---Create resolved AsyncTask.
96 | ---@param v any
97 | ---@return automa.kit.Async.AsyncTask
98 | function AsyncTask.resolve(v)
99 | if AsyncTask.is(v) then
100 | return v
101 | end
102 | return AsyncTask.new(function(resolve)
103 | resolve(v)
104 | end)
105 | end
106 |
107 | ---Create new AsyncTask.
108 | ---@NOET: The AsyncTask has similar interface to JavaScript Promise but the AsyncTask can be worked as synchronous.
109 | ---@param v any
110 | ---@return automa.kit.Async.AsyncTask
111 | function AsyncTask.reject(v)
112 | if AsyncTask.is(v) then
113 | return v
114 | end
115 | return AsyncTask.new(function(_, reject)
116 | reject(v)
117 | end)
118 | end
119 |
120 | ---Create new async task object.
121 | ---@param runner fun(resolve?: fun(value: any?), reject?: fun(err: any?))
122 | function AsyncTask.new(runner)
123 | local self = setmetatable({}, AsyncTask)
124 |
125 | self.value = nil
126 | self.status = AsyncTask.Status.Pending
127 | self.synced = false
128 | self.chained = false
129 | self.children = {}
130 | local ok, err = pcall(runner, function(res)
131 | if self.status == AsyncTask.Status.Pending then
132 | settle(self, AsyncTask.Status.Fulfilled, res)
133 | end
134 | end, function(err)
135 | if self.status == AsyncTask.Status.Pending then
136 | settle(self, AsyncTask.Status.Rejected, err)
137 | end
138 | end)
139 | if not ok then
140 | settle(self, AsyncTask.Status.Rejected, err)
141 | end
142 | return self
143 | end
144 |
145 | ---Sync async task.
146 | ---@NOTE: This method uses `vim.wait` so that this can't wait the typeahead to be empty.
147 | ---@param timeout integer
148 | ---@return any
149 | function AsyncTask:sync(timeout)
150 | self.synced = true
151 |
152 | local time = uv.now()
153 | while uv.now() - time <= timeout do
154 | if self.status ~= AsyncTask.Status.Pending then
155 | break
156 | end
157 | if is_thread then
158 | uv.run('once')
159 | else
160 | vim.wait(16)
161 | end
162 | end
163 | if self.status == AsyncTask.Status.Pending then
164 | error('AsyncTask:sync is timeout.', 2)
165 | end
166 | if self.status == AsyncTask.Status.Rejected then
167 | error(self.value, 2)
168 | end
169 | if self.status ~= AsyncTask.Status.Fulfilled then
170 | error('AsyncTask:sync is timeout.', 2)
171 | end
172 | return self.value
173 | end
174 |
175 | ---Await async task.
176 | ---@return any
177 | function AsyncTask:await()
178 | local Async = require('automa.kit.Async')
179 | local in_fast_event = vim.in_fast_event()
180 | local ok, res = pcall(Async.await, self)
181 | if not ok then
182 | error(res, 2)
183 | end
184 | if not in_fast_event and vim.in_fast_event() then
185 | Async.schedule():await()
186 | end
187 | return res
188 | end
189 |
190 | ---Return current state of task.
191 | ---@return { status: automa.kit.Async.AsyncTask.Status, value: any }
192 | function AsyncTask:state()
193 | return {
194 | status = self.status,
195 | value = self.value,
196 | }
197 | end
198 |
199 | ---Register next step.
200 | ---@param on_fulfilled fun(value: any): any
201 | function AsyncTask:next(on_fulfilled)
202 | return self:dispatch(on_fulfilled, function(err)
203 | error(err, 2)
204 | end)
205 | end
206 |
207 | ---Register catch step.
208 | ---@param on_rejected fun(value: any): any
209 | ---@return automa.kit.Async.AsyncTask
210 | function AsyncTask:catch(on_rejected)
211 | return self:dispatch(function(value)
212 | return value
213 | end, on_rejected)
214 | end
215 |
216 | ---Dispatch task state.
217 | ---@param on_fulfilled fun(value: any): any
218 | ---@param on_rejected fun(err: any): any
219 | ---@return automa.kit.Async.AsyncTask
220 | function AsyncTask:dispatch(on_fulfilled, on_rejected)
221 | self.chained = true
222 |
223 | local function dispatch(resolve, reject)
224 | local on_next = self.status == AsyncTask.Status.Fulfilled and on_fulfilled or on_rejected
225 | local ok, res = pcall(on_next, self.value)
226 | if AsyncTask.is(res) then
227 | res:dispatch(resolve, reject)
228 | else
229 | if ok then
230 | resolve(res)
231 | else
232 | reject(res)
233 | end
234 | end
235 | end
236 |
237 | if self.status == AsyncTask.Status.Pending then
238 | return AsyncTask.new(function(resolve, reject)
239 | table.insert(self.children, function()
240 | dispatch(resolve, reject)
241 | end)
242 | end)
243 | end
244 | return AsyncTask.new(dispatch)
245 | end
246 |
247 | return AsyncTask
248 |
--------------------------------------------------------------------------------
/lua/automa/kit/Async/Worker.lua:
--------------------------------------------------------------------------------
1 | local uv = require('luv')
2 | local AsyncTask = require('automa.kit.Async.AsyncTask')
3 |
4 | ---@class automa.kit.Async.WorkerOption
5 | ---@field public runtimepath string[]
6 |
7 | ---@class automa.kit.Async.Worker
8 | ---@field private runner string
9 | local Worker = {}
10 | Worker.__index = Worker
11 |
12 | ---Create a new worker.
13 | ---@param runner function
14 | function Worker.new(runner)
15 | local self = setmetatable({}, Worker)
16 | self.runner = string.dump(runner)
17 | return self
18 | end
19 |
20 | ---Call worker function.
21 | ---@return automa.kit.Async.AsyncTask
22 | function Worker:__call(...)
23 | local args_ = { ... }
24 | return AsyncTask.new(function(resolve, reject)
25 | uv.new_work(function(runner, args, option)
26 | args = vim.mpack.decode(args)
27 | option = vim.mpack.decode(option)
28 |
29 | --Initialize cwd.
30 | require('luv').chdir(option.cwd)
31 |
32 | --Initialize package.loaders.
33 | table.insert(package.loaders, 2, vim._load_package)
34 |
35 | --Run runner function.
36 | local ok, res = pcall(function()
37 | return require('automa.kit.Async.AsyncTask').resolve(assert(loadstring(runner))(unpack(args))):sync(5000)
38 | end)
39 |
40 | res = vim.mpack.encode({ res })
41 |
42 | --Return error or result.
43 | if not ok then
44 | return res, nil
45 | else
46 | return nil, res
47 | end
48 | end, function(err, res)
49 | if err then
50 | reject(vim.mpack.decode(err)[1])
51 | else
52 | resolve(vim.mpack.decode(res)[1])
53 | end
54 | end):queue(
55 | self.runner,
56 | vim.mpack.encode(args_),
57 | vim.mpack.encode({
58 | cwd = uv.cwd(),
59 | })
60 | )
61 | end)
62 | end
63 |
64 | return Worker
65 |
--------------------------------------------------------------------------------
/lua/automa/kit/Async/init.lua:
--------------------------------------------------------------------------------
1 | local AsyncTask = require('automa.kit.Async.AsyncTask')
2 |
3 | local Interrupt = {}
4 |
5 | local Async = {}
6 |
7 | _G.kit = _G.kit or {}
8 | _G.kit.Async = _G.kit.Async or {}
9 | _G.kit.Async.___threads___ = _G.kit.Async.___threads___ or {}
10 |
11 | ---Alias of AsyncTask.all.
12 | ---@param tasks automa.kit.Async.AsyncTask[]
13 | ---@return automa.kit.Async.AsyncTask
14 | function Async.all(tasks)
15 | return AsyncTask.all(tasks)
16 | end
17 |
18 | ---Alias of AsyncTask.race.
19 | ---@param tasks automa.kit.Async.AsyncTask[]
20 | ---@return automa.kit.Async.AsyncTask
21 | function Async.race(tasks)
22 | return AsyncTask.race(tasks)
23 | end
24 |
25 | ---Alias of AsyncTask.resolve(v).
26 | ---@param v any
27 | ---@return automa.kit.Async.AsyncTask
28 | function Async.resolve(v)
29 | return AsyncTask.resolve(v)
30 | end
31 |
32 | ---Alias of AsyncTask.reject(v).
33 | ---@param v any
34 | ---@return automa.kit.Async.AsyncTask
35 | function Async.reject(v)
36 | return AsyncTask.reject(v)
37 | end
38 |
39 | ---Alias of AsyncTask.new(...).
40 | ---@param runner fun(resolve: fun(value: any), reject: fun(err: any))
41 | ---@return automa.kit.Async.AsyncTask
42 | function Async.new(runner)
43 | return AsyncTask.new(runner)
44 | end
45 |
46 | ---Run async function immediately.
47 | ---@generic A: ...
48 | ---@param runner fun(...: A): any
49 | ---@param ...? A
50 | ---@return automa.kit.Async.AsyncTask
51 | function Async.run(runner, ...)
52 | local args = { ... }
53 | if Async.in_context() then
54 | return Async.new(function(resolve, reject)
55 | local o = { pcall(runner, args) }
56 | if o[1] then
57 | resolve(unpack(o, 2))
58 | else
59 | reject(unpack(o, 2))
60 | end
61 | end)
62 | end
63 |
64 | local thread = coroutine.create(runner)
65 | _G.kit.Async.___threads___[thread] = {
66 | thread = thread,
67 | now = os.clock() * 1000,
68 | }
69 | return AsyncTask.new(function(resolve, reject)
70 | local function next_step(ok, v)
71 | if getmetatable(v) == Interrupt then
72 | vim.defer_fn(function()
73 | next_step(coroutine.resume(thread))
74 | end, v.timeout)
75 | return
76 | end
77 |
78 | if coroutine.status(thread) == 'dead' then
79 | _G.kit.Async.___threads___[thread] = nil
80 | if AsyncTask.is(v) then
81 | v:dispatch(resolve, reject)
82 | else
83 | if ok then
84 | resolve(v)
85 | else
86 | reject(v)
87 | end
88 | end
89 | return
90 | end
91 |
92 | v:dispatch(function(...)
93 | next_step(coroutine.resume(thread, true, ...))
94 | end, function(...)
95 | next_step(coroutine.resume(thread, false, ...))
96 | end)
97 | end
98 |
99 | next_step(coroutine.resume(thread, unpack(args)))
100 | end)
101 | end
102 |
103 | ---Return current context is async coroutine or not.
104 | ---@return boolean
105 | function Async.in_context()
106 | return _G.kit.Async.___threads___[coroutine.running()] ~= nil
107 | end
108 |
109 | ---Await async task.
110 | ---@param task automa.kit.Async.AsyncTask
111 | ---@return any
112 | function Async.await(task)
113 | if not _G.kit.Async.___threads___[coroutine.running()] then
114 | error('`Async.await` must be called in async context.')
115 | end
116 | if not AsyncTask.is(task) then
117 | error('`Async.await` must be called with AsyncTask.')
118 | end
119 |
120 | local ok, res = coroutine.yield(task)
121 | if not ok then
122 | error(res, 2)
123 | end
124 | return res
125 | end
126 |
127 | ---Interrupt sync process.
128 | ---@param interval integer
129 | ---@param timeout? integer
130 | function Async.interrupt(interval, timeout)
131 | local thread = coroutine.running()
132 | if not _G.kit.Async.___threads___[thread] then
133 | error('`Async.interrupt` must be called in async context.')
134 | end
135 |
136 | local curr_now = os.clock() * 1000
137 | local prev_now = _G.kit.Async.___threads___[thread].now
138 | if (curr_now - prev_now) > interval then
139 | coroutine.yield(setmetatable({ timeout = timeout or 16 }, Interrupt))
140 | _G.kit.Async.___threads___[thread].now = os.clock() * 1000
141 | end
142 | end
143 |
144 | ---Create vim.schedule task.
145 | ---@return automa.kit.Async.AsyncTask
146 | function Async.schedule()
147 | return AsyncTask.new(function(resolve)
148 | vim.schedule(resolve)
149 | end)
150 | end
151 |
152 | ---Create vim.defer_fn task.
153 | ---@param timeout integer
154 | ---@return automa.kit.Async.AsyncTask
155 | function Async.timeout(timeout)
156 | return AsyncTask.new(function(resolve)
157 | vim.defer_fn(resolve, timeout)
158 | end)
159 | end
160 |
161 | ---Create async function from callback function.
162 | ---@generic T: ...
163 | ---@param runner fun(...: T)
164 | ---@param option? { schedule?: boolean, callback?: integer }
165 | ---@return fun(...: T): automa.kit.Async.AsyncTask
166 | function Async.promisify(runner, option)
167 | option = option or {}
168 | option.schedule = not vim.is_thread() and (option.schedule or false)
169 | option.callback = option.callback or nil
170 | return function(...)
171 | local args = { ... }
172 | return AsyncTask.new(function(resolve, reject)
173 | local max = #args + 1
174 | local pos = math.min(option.callback or max, max)
175 | table.insert(args, pos, function(err, ...)
176 | if option.schedule and vim.in_fast_event() then
177 | resolve = vim.schedule_wrap(resolve)
178 | reject = vim.schedule_wrap(reject)
179 | end
180 | if err then
181 | reject(err)
182 | else
183 | resolve(...)
184 | end
185 | end)
186 | runner(unpack(args))
187 | end)
188 | end
189 | end
190 |
191 | return Async
192 |
--------------------------------------------------------------------------------
/lua/automa/kit/IO/init.lua:
--------------------------------------------------------------------------------
1 | local uv = require('luv')
2 | local Async = require('automa.kit.Async')
3 |
4 | local is_windows = uv.os_uname().sysname:lower() == 'windows'
5 |
6 | ---@see https://github.com/luvit/luvit/blob/master/deps/fs.lua
7 | local IO = {}
8 |
9 | ---@class automa.kit.IO.UV.Stat
10 | ---@field public dev integer
11 | ---@field public mode integer
12 | ---@field public nlink integer
13 | ---@field public uid integer
14 | ---@field public gid integer
15 | ---@field public rdev integer
16 | ---@field public ino integer
17 | ---@field public size integer
18 | ---@field public blksize integer
19 | ---@field public blocks integer
20 | ---@field public flags integer
21 | ---@field public gen integer
22 | ---@field public atime { sec: integer, nsec: integer }
23 | ---@field public mtime { sec: integer, nsec: integer }
24 | ---@field public ctime { sec: integer, nsec: integer }
25 | ---@field public birthtime { sec: integer, nsec: integer }
26 | ---@field public type string
27 |
28 | ---@enum automa.kit.IO.UV.AccessMode
29 | IO.AccessMode = {
30 | r = 'r',
31 | rs = 'rs',
32 | sr = 'sr',
33 | ['r+'] = 'r+',
34 | ['rs+'] = 'rs+',
35 | ['sr+'] = 'sr+',
36 | w = 'w',
37 | wx = 'wx',
38 | xw = 'xw',
39 | ['w+'] = 'w+',
40 | ['wx+'] = 'wx+',
41 | ['xw+'] = 'xw+',
42 | a = 'a',
43 | ax = 'ax',
44 | xa = 'xa',
45 | ['a+'] = 'a+',
46 | ['ax+'] = 'ax+',
47 | ['xa+'] = 'xa+',
48 | }
49 |
50 | ---@enum automa.kit.IO.WalkStatus
51 | IO.WalkStatus = {
52 | SkipDir = 1,
53 | Break = 2,
54 | }
55 |
56 | ---@type fun(path: string): automa.kit.Async.AsyncTask
57 | IO.fs_stat = Async.promisify(uv.fs_stat)
58 |
59 | ---@type fun(path: string): automa.kit.Async.AsyncTask
60 | IO.fs_unlink = Async.promisify(uv.fs_unlink)
61 |
62 | ---@type fun(path: string): automa.kit.Async.AsyncTask
63 | IO.fs_rmdir = Async.promisify(uv.fs_rmdir)
64 |
65 | ---@type fun(path: string, mode: integer): automa.kit.Async.AsyncTask
66 | IO.fs_mkdir = Async.promisify(uv.fs_mkdir)
67 |
68 | ---@type fun(from: string, to: string, option?: { excl?: boolean, ficlone?: boolean, ficlone_force?: boolean }): automa.kit.Async.AsyncTask
69 | IO.fs_copyfile = Async.promisify(uv.fs_copyfile)
70 |
71 | ---@type fun(path: string, flags: automa.kit.IO.UV.AccessMode, mode: integer): automa.kit.Async.AsyncTask
72 | IO.fs_open = Async.promisify(uv.fs_open)
73 |
74 | ---@type fun(fd: userdata): automa.kit.Async.AsyncTask
75 | IO.fs_close = Async.promisify(uv.fs_close)
76 |
77 | ---@type fun(fd: userdata, chunk_size: integer, offset?: integer): automa.kit.Async.AsyncTask
78 | IO.fs_read = Async.promisify(uv.fs_read)
79 |
80 | ---@type fun(fd: userdata, content: string, offset?: integer): automa.kit.Async.AsyncTask
81 | IO.fs_write = Async.promisify(uv.fs_write)
82 |
83 | ---@type fun(fd: userdata, offset: integer): automa.kit.Async.AsyncTask
84 | IO.fs_ftruncate = Async.promisify(uv.fs_ftruncate)
85 |
86 | ---@type fun(path: string, chunk_size?: integer): automa.kit.Async.AsyncTask
87 | IO.fs_opendir = Async.promisify(uv.fs_opendir, { callback = 2 })
88 |
89 | ---@type fun(fd: userdata): automa.kit.Async.AsyncTask
90 | IO.fs_closedir = Async.promisify(uv.fs_closedir)
91 |
92 | ---@type fun(fd: userdata): automa.kit.Async.AsyncTask
93 | IO.fs_readdir = Async.promisify(uv.fs_readdir)
94 |
95 | ---@type fun(path: string): automa.kit.Async.AsyncTask
96 | IO.fs_scandir = Async.promisify(uv.fs_scandir)
97 |
98 | ---@type fun(path: string): automa.kit.Async.AsyncTask
99 | IO.fs_realpath = Async.promisify(uv.fs_realpath)
100 |
101 | ---Return if the path is directory.
102 | ---@param path string
103 | ---@return automa.kit.Async.AsyncTask
104 | function IO.is_directory(path)
105 | path = IO.normalize(path)
106 | return Async.run(function()
107 | return IO.fs_stat(path)
108 | :catch(function()
109 | return {}
110 | end)
111 | :await().type == 'directory'
112 | end)
113 | end
114 |
115 | ---Read file.
116 | ---@param path string
117 | ---@param chunk_size? integer
118 | ---@return automa.kit.Async.AsyncTask
119 | function IO.read_file(path, chunk_size)
120 | chunk_size = chunk_size or 1024
121 | return Async.run(function()
122 | local stat = IO.fs_stat(path):await()
123 | local fd = IO.fs_open(path, IO.AccessMode.r, tonumber('755', 8)):await()
124 | local ok, res = pcall(function()
125 | local chunks = {}
126 | local offset = 0
127 | while offset < stat.size do
128 | local chunk = IO.fs_read(fd, math.min(chunk_size, stat.size - offset), offset):await()
129 | if not chunk then
130 | break
131 | end
132 | table.insert(chunks, chunk)
133 | offset = offset + #chunk
134 | end
135 | return table.concat(chunks, ''):sub(1, stat.size - 1) -- remove EOF.
136 | end)
137 | IO.fs_close(fd):await()
138 | if not ok then
139 | error(res)
140 | end
141 | return res
142 | end)
143 | end
144 |
145 | ---Write file.
146 | ---@param path string
147 | ---@param content string
148 | ---@param chunk_size? integer
149 | function IO.write_file(path, content, chunk_size)
150 | chunk_size = chunk_size or 1024
151 | content = content .. '\n' -- add EOF.
152 | return Async.run(function()
153 | local fd = IO.fs_open(path, IO.AccessMode.w, tonumber('755', 8)):await()
154 | local ok, err = pcall(function()
155 | local offset = 0
156 | while offset < #content do
157 | local chunk = content:sub(offset + 1, offset + chunk_size)
158 | offset = offset + IO.fs_write(fd, chunk, offset):await()
159 | end
160 | IO.fs_ftruncate(fd, offset):await()
161 | end)
162 | IO.fs_close(fd):await()
163 | if not ok then
164 | error(err)
165 | end
166 | end)
167 | end
168 |
169 | ---Create directory.
170 | ---@param path string
171 | ---@param mode integer
172 | ---@param option? { recursive?: boolean }
173 | function IO.mkdir(path, mode, option)
174 | path = IO.normalize(path)
175 | option = option or {}
176 | option.recursive = option.recursive or false
177 | return Async.run(function()
178 | if not option.recursive then
179 | IO.fs_mkdir(path, mode):await()
180 | else
181 | local not_exists = {}
182 | local current = path
183 | while current ~= '/' do
184 | local stat = IO.fs_stat(current):catch(function() end):await()
185 | if stat then
186 | break
187 | end
188 | table.insert(not_exists, 1, current)
189 | current = IO.dirname(current)
190 | end
191 | for _, dir in ipairs(not_exists) do
192 | IO.fs_mkdir(dir, mode):await()
193 | end
194 | end
195 | end)
196 | end
197 |
198 | ---Remove file or directory.
199 | ---@param start_path string
200 | ---@param option? { recursive?: boolean }
201 | function IO.rm(start_path, option)
202 | start_path = IO.normalize(start_path)
203 | option = option or {}
204 | option.recursive = option.recursive or false
205 | return Async.run(function()
206 | local stat = IO.fs_stat(start_path):await()
207 | if stat.type == 'directory' then
208 | local children = IO.scandir(start_path):await()
209 | if not option.recursive and #children > 0 then
210 | error(('IO.rm: `%s` is a directory and not empty.'):format(start_path))
211 | end
212 | IO.walk(start_path, function(err, entry)
213 | if err then
214 | error('IO.rm: ' .. tostring(err))
215 | end
216 | if entry.type == 'directory' then
217 | IO.fs_rmdir(entry.path):await()
218 | else
219 | IO.fs_unlink(entry.path):await()
220 | end
221 | end, { postorder = true }):await()
222 | else
223 | IO.fs_unlink(start_path):await()
224 | end
225 | end)
226 | end
227 |
228 | ---Copy file or directory.
229 | ---@param from any
230 | ---@param to any
231 | ---@param option? { recursive?: boolean }
232 | ---@return automa.kit.Async.AsyncTask
233 | function IO.cp(from, to, option)
234 | from = IO.normalize(from)
235 | to = IO.normalize(to)
236 | option = option or {}
237 | option.recursive = option.recursive or false
238 | return Async.run(function()
239 | local stat = IO.fs_stat(from):await()
240 | if stat.type == 'directory' then
241 | if not option.recursive then
242 | error(('IO.cp: `%s` is a directory.'):format(from))
243 | end
244 | IO.walk(from, function(err, entry)
245 | if err then
246 | error('IO.cp: ' .. tostring(err))
247 | end
248 | local new_path = entry.path:gsub(vim.pesc(from), to)
249 | if entry.type == 'directory' then
250 | IO.mkdir(new_path, tonumber(stat.mode, 10), { recursive = true }):await()
251 | else
252 | IO.fs_copyfile(entry.path, new_path):await()
253 | end
254 | end):await()
255 | else
256 | IO.fs_copyfile(from, to):await()
257 | end
258 | end)
259 | end
260 |
261 | ---Walk directory entries recursively.
262 | ---@param start_path string
263 | ---@param callback fun(err: string|nil, entry: { path: string, type: string }): automa.kit.IO.WalkStatus?
264 | ---@param option? { postorder?: boolean }
265 | function IO.walk(start_path, callback, option)
266 | start_path = IO.normalize(start_path)
267 | option = option or {}
268 | option.postorder = option.postorder or false
269 | return Async.run(function()
270 | local function walk_pre(dir)
271 | local ok, iter_entries = pcall(function()
272 | return IO.iter_scandir(dir.path):await()
273 | end)
274 | if not ok then
275 | return callback(iter_entries, dir)
276 | end
277 | local status = callback(nil, dir)
278 | if status == IO.WalkStatus.SkipDir then
279 | return
280 | elseif status == IO.WalkStatus.Break then
281 | return status
282 | end
283 | for entry in iter_entries do
284 | if entry.type == 'directory' then
285 | if walk_pre(entry) == IO.WalkStatus.Break then
286 | return IO.WalkStatus.Break
287 | end
288 | else
289 | if callback(nil, entry) == IO.WalkStatus.Break then
290 | return IO.WalkStatus.Break
291 | end
292 | end
293 | end
294 | end
295 |
296 | local function walk_post(dir)
297 | local ok, iter_entries = pcall(function()
298 | return IO.iter_scandir(dir.path):await()
299 | end)
300 | if not ok then
301 | return callback(iter_entries, dir)
302 | end
303 | for entry in iter_entries do
304 | if entry.type == 'directory' then
305 | if walk_post(entry) == IO.WalkStatus.Break then
306 | return IO.WalkStatus.Break
307 | end
308 | else
309 | if callback(nil, entry) == IO.WalkStatus.Break then
310 | return IO.WalkStatus.Break
311 | end
312 | end
313 | end
314 | return callback(nil, dir)
315 | end
316 |
317 | if not IO.is_directory(start_path) then
318 | error(('IO.walk: `%s` is not a directory.'):format(start_path))
319 | end
320 | if option.postorder then
321 | walk_post({ path = start_path, type = 'directory' })
322 | else
323 | walk_pre({ path = start_path, type = 'directory' })
324 | end
325 | end)
326 | end
327 |
328 | ---Scan directory entries.
329 | ---@param path string
330 | ---@return automa.kit.Async.AsyncTask
331 | function IO.scandir(path)
332 | path = IO.normalize(path)
333 | return Async.run(function()
334 | local fd = IO.fs_scandir(path):await()
335 | local entries = {}
336 | while true do
337 | local name, type = uv.fs_scandir_next(fd)
338 | if not name then
339 | break
340 | end
341 | table.insert(entries, {
342 | type = type,
343 | path = IO.join(path, name),
344 | })
345 | end
346 | return entries
347 | end)
348 | end
349 |
350 | ---Scan directory entries.
351 | ---@param path any
352 | ---@return automa.kit.Async.AsyncTask
353 | function IO.iter_scandir(path)
354 | path = IO.normalize(path)
355 | return Async.run(function()
356 | local fd = IO.fs_scandir(path):await()
357 | return function()
358 | local name, type = uv.fs_scandir_next(fd)
359 | if name then
360 | return {
361 | type = type,
362 | path = IO.join(path, name),
363 | }
364 | end
365 | end
366 | end)
367 | end
368 |
369 | ---Return normalized path.
370 | ---@param path string
371 | ---@return string
372 | function IO.normalize(path)
373 | if is_windows then
374 | path = path:gsub('\\', '/')
375 | end
376 |
377 | -- remove trailing slash.
378 | if path:sub(-1) == '/' then
379 | path = path:sub(1, -2)
380 | end
381 |
382 | -- skip if the path already absolute.
383 | if IO.is_absolute(path) then
384 | return path
385 | end
386 |
387 | -- homedir.
388 | if path:sub(1, 1) == '~' then
389 | path = IO.join(uv.os_homedir(), path:sub(2))
390 | end
391 |
392 | -- absolute.
393 | if path:sub(1, 1) == '/' then
394 | return path:sub(-1) == '/' and path:sub(1, -2) or path
395 | end
396 |
397 | -- resolve relative path.
398 | local up = uv.cwd()
399 | up = up:sub(-1) == '/' and up:sub(1, -2) or up
400 | while true do
401 | if path:sub(1, 3) == '../' then
402 | path = path:sub(4)
403 | up = IO.dirname(up)
404 | elseif path:sub(1, 2) == './' then
405 | path = path:sub(3)
406 | else
407 | break
408 | end
409 | end
410 | return IO.join(up, path)
411 | end
412 |
413 | ---Join the paths.
414 | ---@param base string
415 | ---@param path string
416 | ---@return string
417 | function IO.join(base, path)
418 | if base:sub(-1) == '/' then
419 | base = base:sub(1, -2)
420 | end
421 | return base .. '/' .. path
422 | end
423 |
424 | ---Return the path of the current working directory.
425 | ---@param path string
426 | ---@return string
427 | function IO.dirname(path)
428 | if path:sub(-1) == '/' then
429 | path = path:sub(1, -2)
430 | end
431 | return (path:gsub('/[^/]+$', ''))
432 | end
433 |
434 | if is_windows then
435 | ---Return the path is absolute or not.
436 | ---@param path string
437 | ---@return boolean
438 | function IO.is_absolute(path)
439 | return path:sub(1, 1) == '/' or path:match('^%a://')
440 | end
441 | else
442 | ---Return the path is absolute or not.
443 | ---@param path string
444 | ---@return boolean
445 | function IO.is_absolute(path)
446 | return path:sub(1, 1) == '/'
447 | end
448 | end
449 |
450 | return IO
451 |
--------------------------------------------------------------------------------
/lua/automa/kit/LSP/Client.lua:
--------------------------------------------------------------------------------
1 | local LSP = require('automa.kit.LSP')
2 | local AsyncTask = require('automa.kit.Async.AsyncTask')
3 |
4 | ---@class automa.kit.LSP.Client
5 | ---@field public client table
6 | local Client = {}
7 | Client.__index = Client
8 |
9 | ---Create LSP Client wrapper.
10 | ---@param client table
11 | ---@return automa.kit.LSP.Client
12 | function Client.new(client)
13 | local self = setmetatable({}, Client)
14 | self.client = client
15 | return self
16 | end
17 |
18 | ---@param params automa.kit.LSP.ImplementationParams
19 | function Client:textDocument_implementation(params)
20 | local that, request_id, reject_ = self, nil, nil
21 | local task = AsyncTask.new(function(resolve, reject)
22 | request_id = self.client.request('textDocument/implementation', params, function(err, res)
23 | if err then
24 | reject(err)
25 | else
26 | resolve(res)
27 | end
28 | end)
29 | reject_ = reject
30 | end)
31 | function task.cancel()
32 | that.client.cancel_request(request_id)
33 | reject_(LSP.ErrorCodes.RequestCancelled)
34 | end
35 | return task
36 | end
37 |
38 | ---@param params automa.kit.LSP.TypeDefinitionParams
39 | function Client:textDocument_typeDefinition(params)
40 | local that, request_id, reject_ = self, nil, nil
41 | local task = AsyncTask.new(function(resolve, reject)
42 | request_id = self.client.request('textDocument/typeDefinition', params, function(err, res)
43 | if err then
44 | reject(err)
45 | else
46 | resolve(res)
47 | end
48 | end)
49 | reject_ = reject
50 | end)
51 | function task.cancel()
52 | that.client.cancel_request(request_id)
53 | reject_(LSP.ErrorCodes.RequestCancelled)
54 | end
55 | return task
56 | end
57 |
58 | ---@param params nil
59 | function Client:workspace_workspaceFolders(params)
60 | local that, request_id, reject_ = self, nil, nil
61 | local task = AsyncTask.new(function(resolve, reject)
62 | request_id = self.client.request('workspace/workspaceFolders', params, function(err, res)
63 | if err then
64 | reject(err)
65 | else
66 | resolve(res)
67 | end
68 | end)
69 | reject_ = reject
70 | end)
71 | function task.cancel()
72 | that.client.cancel_request(request_id)
73 | reject_(LSP.ErrorCodes.RequestCancelled)
74 | end
75 | return task
76 | end
77 |
78 | ---@param params automa.kit.LSP.ConfigurationParams
79 | function Client:workspace_configuration(params)
80 | local that, request_id, reject_ = self, nil, nil
81 | local task = AsyncTask.new(function(resolve, reject)
82 | request_id = self.client.request('workspace/configuration', params, function(err, res)
83 | if err then
84 | reject(err)
85 | else
86 | resolve(res)
87 | end
88 | end)
89 | reject_ = reject
90 | end)
91 | function task.cancel()
92 | that.client.cancel_request(request_id)
93 | reject_(LSP.ErrorCodes.RequestCancelled)
94 | end
95 | return task
96 | end
97 |
98 | ---@param params automa.kit.LSP.DocumentColorParams
99 | function Client:textDocument_documentColor(params)
100 | local that, request_id, reject_ = self, nil, nil
101 | local task = AsyncTask.new(function(resolve, reject)
102 | request_id = self.client.request('textDocument/documentColor', params, function(err, res)
103 | if err then
104 | reject(err)
105 | else
106 | resolve(res)
107 | end
108 | end)
109 | reject_ = reject
110 | end)
111 | function task.cancel()
112 | that.client.cancel_request(request_id)
113 | reject_(LSP.ErrorCodes.RequestCancelled)
114 | end
115 | return task
116 | end
117 |
118 | ---@param params automa.kit.LSP.ColorPresentationParams
119 | function Client:textDocument_colorPresentation(params)
120 | local that, request_id, reject_ = self, nil, nil
121 | local task = AsyncTask.new(function(resolve, reject)
122 | request_id = self.client.request('textDocument/colorPresentation', params, function(err, res)
123 | if err then
124 | reject(err)
125 | else
126 | resolve(res)
127 | end
128 | end)
129 | reject_ = reject
130 | end)
131 | function task.cancel()
132 | that.client.cancel_request(request_id)
133 | reject_(LSP.ErrorCodes.RequestCancelled)
134 | end
135 | return task
136 | end
137 |
138 | ---@param params automa.kit.LSP.FoldingRangeParams
139 | function Client:textDocument_foldingRange(params)
140 | local that, request_id, reject_ = self, nil, nil
141 | local task = AsyncTask.new(function(resolve, reject)
142 | request_id = self.client.request('textDocument/foldingRange', params, function(err, res)
143 | if err then
144 | reject(err)
145 | else
146 | resolve(res)
147 | end
148 | end)
149 | reject_ = reject
150 | end)
151 | function task.cancel()
152 | that.client.cancel_request(request_id)
153 | reject_(LSP.ErrorCodes.RequestCancelled)
154 | end
155 | return task
156 | end
157 |
158 | ---@param params nil
159 | function Client:workspace_foldingRange_refresh(params)
160 | local that, request_id, reject_ = self, nil, nil
161 | local task = AsyncTask.new(function(resolve, reject)
162 | request_id = self.client.request('workspace/foldingRange/refresh', params, function(err, res)
163 | if err then
164 | reject(err)
165 | else
166 | resolve(res)
167 | end
168 | end)
169 | reject_ = reject
170 | end)
171 | function task.cancel()
172 | that.client.cancel_request(request_id)
173 | reject_(LSP.ErrorCodes.RequestCancelled)
174 | end
175 | return task
176 | end
177 |
178 | ---@param params automa.kit.LSP.DeclarationParams
179 | function Client:textDocument_declaration(params)
180 | local that, request_id, reject_ = self, nil, nil
181 | local task = AsyncTask.new(function(resolve, reject)
182 | request_id = self.client.request('textDocument/declaration', params, function(err, res)
183 | if err then
184 | reject(err)
185 | else
186 | resolve(res)
187 | end
188 | end)
189 | reject_ = reject
190 | end)
191 | function task.cancel()
192 | that.client.cancel_request(request_id)
193 | reject_(LSP.ErrorCodes.RequestCancelled)
194 | end
195 | return task
196 | end
197 |
198 | ---@param params automa.kit.LSP.SelectionRangeParams
199 | function Client:textDocument_selectionRange(params)
200 | local that, request_id, reject_ = self, nil, nil
201 | local task = AsyncTask.new(function(resolve, reject)
202 | request_id = self.client.request('textDocument/selectionRange', params, function(err, res)
203 | if err then
204 | reject(err)
205 | else
206 | resolve(res)
207 | end
208 | end)
209 | reject_ = reject
210 | end)
211 | function task.cancel()
212 | that.client.cancel_request(request_id)
213 | reject_(LSP.ErrorCodes.RequestCancelled)
214 | end
215 | return task
216 | end
217 |
218 | ---@param params automa.kit.LSP.WorkDoneProgressCreateParams
219 | function Client:window_workDoneProgress_create(params)
220 | local that, request_id, reject_ = self, nil, nil
221 | local task = AsyncTask.new(function(resolve, reject)
222 | request_id = self.client.request('window/workDoneProgress/create', params, function(err, res)
223 | if err then
224 | reject(err)
225 | else
226 | resolve(res)
227 | end
228 | end)
229 | reject_ = reject
230 | end)
231 | function task.cancel()
232 | that.client.cancel_request(request_id)
233 | reject_(LSP.ErrorCodes.RequestCancelled)
234 | end
235 | return task
236 | end
237 |
238 | ---@param params automa.kit.LSP.CallHierarchyPrepareParams
239 | function Client:textDocument_prepareCallHierarchy(params)
240 | local that, request_id, reject_ = self, nil, nil
241 | local task = AsyncTask.new(function(resolve, reject)
242 | request_id = self.client.request('textDocument/prepareCallHierarchy', params, function(err, res)
243 | if err then
244 | reject(err)
245 | else
246 | resolve(res)
247 | end
248 | end)
249 | reject_ = reject
250 | end)
251 | function task.cancel()
252 | that.client.cancel_request(request_id)
253 | reject_(LSP.ErrorCodes.RequestCancelled)
254 | end
255 | return task
256 | end
257 |
258 | ---@param params automa.kit.LSP.CallHierarchyIncomingCallsParams
259 | function Client:callHierarchy_incomingCalls(params)
260 | local that, request_id, reject_ = self, nil, nil
261 | local task = AsyncTask.new(function(resolve, reject)
262 | request_id = self.client.request('callHierarchy/incomingCalls', params, function(err, res)
263 | if err then
264 | reject(err)
265 | else
266 | resolve(res)
267 | end
268 | end)
269 | reject_ = reject
270 | end)
271 | function task.cancel()
272 | that.client.cancel_request(request_id)
273 | reject_(LSP.ErrorCodes.RequestCancelled)
274 | end
275 | return task
276 | end
277 |
278 | ---@param params automa.kit.LSP.CallHierarchyOutgoingCallsParams
279 | function Client:callHierarchy_outgoingCalls(params)
280 | local that, request_id, reject_ = self, nil, nil
281 | local task = AsyncTask.new(function(resolve, reject)
282 | request_id = self.client.request('callHierarchy/outgoingCalls', params, function(err, res)
283 | if err then
284 | reject(err)
285 | else
286 | resolve(res)
287 | end
288 | end)
289 | reject_ = reject
290 | end)
291 | function task.cancel()
292 | that.client.cancel_request(request_id)
293 | reject_(LSP.ErrorCodes.RequestCancelled)
294 | end
295 | return task
296 | end
297 |
298 | ---@param params automa.kit.LSP.SemanticTokensParams
299 | function Client:textDocument_semanticTokens_full(params)
300 | local that, request_id, reject_ = self, nil, nil
301 | local task = AsyncTask.new(function(resolve, reject)
302 | request_id = self.client.request('textDocument/semanticTokens/full', params, function(err, res)
303 | if err then
304 | reject(err)
305 | else
306 | resolve(res)
307 | end
308 | end)
309 | reject_ = reject
310 | end)
311 | function task.cancel()
312 | that.client.cancel_request(request_id)
313 | reject_(LSP.ErrorCodes.RequestCancelled)
314 | end
315 | return task
316 | end
317 |
318 | ---@param params automa.kit.LSP.SemanticTokensDeltaParams
319 | function Client:textDocument_semanticTokens_full_delta(params)
320 | local that, request_id, reject_ = self, nil, nil
321 | local task = AsyncTask.new(function(resolve, reject)
322 | request_id = self.client.request('textDocument/semanticTokens/full/delta', params, function(err, res)
323 | if err then
324 | reject(err)
325 | else
326 | resolve(res)
327 | end
328 | end)
329 | reject_ = reject
330 | end)
331 | function task.cancel()
332 | that.client.cancel_request(request_id)
333 | reject_(LSP.ErrorCodes.RequestCancelled)
334 | end
335 | return task
336 | end
337 |
338 | ---@param params automa.kit.LSP.SemanticTokensRangeParams
339 | function Client:textDocument_semanticTokens_range(params)
340 | local that, request_id, reject_ = self, nil, nil
341 | local task = AsyncTask.new(function(resolve, reject)
342 | request_id = self.client.request('textDocument/semanticTokens/range', params, function(err, res)
343 | if err then
344 | reject(err)
345 | else
346 | resolve(res)
347 | end
348 | end)
349 | reject_ = reject
350 | end)
351 | function task.cancel()
352 | that.client.cancel_request(request_id)
353 | reject_(LSP.ErrorCodes.RequestCancelled)
354 | end
355 | return task
356 | end
357 |
358 | ---@param params nil
359 | function Client:workspace_semanticTokens_refresh(params)
360 | local that, request_id, reject_ = self, nil, nil
361 | local task = AsyncTask.new(function(resolve, reject)
362 | request_id = self.client.request('workspace/semanticTokens/refresh', params, function(err, res)
363 | if err then
364 | reject(err)
365 | else
366 | resolve(res)
367 | end
368 | end)
369 | reject_ = reject
370 | end)
371 | function task.cancel()
372 | that.client.cancel_request(request_id)
373 | reject_(LSP.ErrorCodes.RequestCancelled)
374 | end
375 | return task
376 | end
377 |
378 | ---@param params automa.kit.LSP.ShowDocumentParams
379 | function Client:window_showDocument(params)
380 | local that, request_id, reject_ = self, nil, nil
381 | local task = AsyncTask.new(function(resolve, reject)
382 | request_id = self.client.request('window/showDocument', params, function(err, res)
383 | if err then
384 | reject(err)
385 | else
386 | resolve(res)
387 | end
388 | end)
389 | reject_ = reject
390 | end)
391 | function task.cancel()
392 | that.client.cancel_request(request_id)
393 | reject_(LSP.ErrorCodes.RequestCancelled)
394 | end
395 | return task
396 | end
397 |
398 | ---@param params automa.kit.LSP.LinkedEditingRangeParams
399 | function Client:textDocument_linkedEditingRange(params)
400 | local that, request_id, reject_ = self, nil, nil
401 | local task = AsyncTask.new(function(resolve, reject)
402 | request_id = self.client.request('textDocument/linkedEditingRange', params, function(err, res)
403 | if err then
404 | reject(err)
405 | else
406 | resolve(res)
407 | end
408 | end)
409 | reject_ = reject
410 | end)
411 | function task.cancel()
412 | that.client.cancel_request(request_id)
413 | reject_(LSP.ErrorCodes.RequestCancelled)
414 | end
415 | return task
416 | end
417 |
418 | ---@param params automa.kit.LSP.CreateFilesParams
419 | function Client:workspace_willCreateFiles(params)
420 | local that, request_id, reject_ = self, nil, nil
421 | local task = AsyncTask.new(function(resolve, reject)
422 | request_id = self.client.request('workspace/willCreateFiles', params, function(err, res)
423 | if err then
424 | reject(err)
425 | else
426 | resolve(res)
427 | end
428 | end)
429 | reject_ = reject
430 | end)
431 | function task.cancel()
432 | that.client.cancel_request(request_id)
433 | reject_(LSP.ErrorCodes.RequestCancelled)
434 | end
435 | return task
436 | end
437 |
438 | ---@param params automa.kit.LSP.RenameFilesParams
439 | function Client:workspace_willRenameFiles(params)
440 | local that, request_id, reject_ = self, nil, nil
441 | local task = AsyncTask.new(function(resolve, reject)
442 | request_id = self.client.request('workspace/willRenameFiles', params, function(err, res)
443 | if err then
444 | reject(err)
445 | else
446 | resolve(res)
447 | end
448 | end)
449 | reject_ = reject
450 | end)
451 | function task.cancel()
452 | that.client.cancel_request(request_id)
453 | reject_(LSP.ErrorCodes.RequestCancelled)
454 | end
455 | return task
456 | end
457 |
458 | ---@param params automa.kit.LSP.DeleteFilesParams
459 | function Client:workspace_willDeleteFiles(params)
460 | local that, request_id, reject_ = self, nil, nil
461 | local task = AsyncTask.new(function(resolve, reject)
462 | request_id = self.client.request('workspace/willDeleteFiles', params, function(err, res)
463 | if err then
464 | reject(err)
465 | else
466 | resolve(res)
467 | end
468 | end)
469 | reject_ = reject
470 | end)
471 | function task.cancel()
472 | that.client.cancel_request(request_id)
473 | reject_(LSP.ErrorCodes.RequestCancelled)
474 | end
475 | return task
476 | end
477 |
478 | ---@param params automa.kit.LSP.MonikerParams
479 | function Client:textDocument_moniker(params)
480 | local that, request_id, reject_ = self, nil, nil
481 | local task = AsyncTask.new(function(resolve, reject)
482 | request_id = self.client.request('textDocument/moniker', params, function(err, res)
483 | if err then
484 | reject(err)
485 | else
486 | resolve(res)
487 | end
488 | end)
489 | reject_ = reject
490 | end)
491 | function task.cancel()
492 | that.client.cancel_request(request_id)
493 | reject_(LSP.ErrorCodes.RequestCancelled)
494 | end
495 | return task
496 | end
497 |
498 | ---@param params automa.kit.LSP.TypeHierarchyPrepareParams
499 | function Client:textDocument_prepareTypeHierarchy(params)
500 | local that, request_id, reject_ = self, nil, nil
501 | local task = AsyncTask.new(function(resolve, reject)
502 | request_id = self.client.request('textDocument/prepareTypeHierarchy', params, function(err, res)
503 | if err then
504 | reject(err)
505 | else
506 | resolve(res)
507 | end
508 | end)
509 | reject_ = reject
510 | end)
511 | function task.cancel()
512 | that.client.cancel_request(request_id)
513 | reject_(LSP.ErrorCodes.RequestCancelled)
514 | end
515 | return task
516 | end
517 |
518 | ---@param params automa.kit.LSP.TypeHierarchySupertypesParams
519 | function Client:typeHierarchy_supertypes(params)
520 | local that, request_id, reject_ = self, nil, nil
521 | local task = AsyncTask.new(function(resolve, reject)
522 | request_id = self.client.request('typeHierarchy/supertypes', params, function(err, res)
523 | if err then
524 | reject(err)
525 | else
526 | resolve(res)
527 | end
528 | end)
529 | reject_ = reject
530 | end)
531 | function task.cancel()
532 | that.client.cancel_request(request_id)
533 | reject_(LSP.ErrorCodes.RequestCancelled)
534 | end
535 | return task
536 | end
537 |
538 | ---@param params automa.kit.LSP.TypeHierarchySubtypesParams
539 | function Client:typeHierarchy_subtypes(params)
540 | local that, request_id, reject_ = self, nil, nil
541 | local task = AsyncTask.new(function(resolve, reject)
542 | request_id = self.client.request('typeHierarchy/subtypes', params, function(err, res)
543 | if err then
544 | reject(err)
545 | else
546 | resolve(res)
547 | end
548 | end)
549 | reject_ = reject
550 | end)
551 | function task.cancel()
552 | that.client.cancel_request(request_id)
553 | reject_(LSP.ErrorCodes.RequestCancelled)
554 | end
555 | return task
556 | end
557 |
558 | ---@param params automa.kit.LSP.InlineValueParams
559 | function Client:textDocument_inlineValue(params)
560 | local that, request_id, reject_ = self, nil, nil
561 | local task = AsyncTask.new(function(resolve, reject)
562 | request_id = self.client.request('textDocument/inlineValue', params, function(err, res)
563 | if err then
564 | reject(err)
565 | else
566 | resolve(res)
567 | end
568 | end)
569 | reject_ = reject
570 | end)
571 | function task.cancel()
572 | that.client.cancel_request(request_id)
573 | reject_(LSP.ErrorCodes.RequestCancelled)
574 | end
575 | return task
576 | end
577 |
578 | ---@param params nil
579 | function Client:workspace_inlineValue_refresh(params)
580 | local that, request_id, reject_ = self, nil, nil
581 | local task = AsyncTask.new(function(resolve, reject)
582 | request_id = self.client.request('workspace/inlineValue/refresh', params, function(err, res)
583 | if err then
584 | reject(err)
585 | else
586 | resolve(res)
587 | end
588 | end)
589 | reject_ = reject
590 | end)
591 | function task.cancel()
592 | that.client.cancel_request(request_id)
593 | reject_(LSP.ErrorCodes.RequestCancelled)
594 | end
595 | return task
596 | end
597 |
598 | ---@param params automa.kit.LSP.InlayHintParams
599 | function Client:textDocument_inlayHint(params)
600 | local that, request_id, reject_ = self, nil, nil
601 | local task = AsyncTask.new(function(resolve, reject)
602 | request_id = self.client.request('textDocument/inlayHint', params, function(err, res)
603 | if err then
604 | reject(err)
605 | else
606 | resolve(res)
607 | end
608 | end)
609 | reject_ = reject
610 | end)
611 | function task.cancel()
612 | that.client.cancel_request(request_id)
613 | reject_(LSP.ErrorCodes.RequestCancelled)
614 | end
615 | return task
616 | end
617 |
618 | ---@param params automa.kit.LSP.InlayHint
619 | function Client:inlayHint_resolve(params)
620 | local that, request_id, reject_ = self, nil, nil
621 | local task = AsyncTask.new(function(resolve, reject)
622 | request_id = self.client.request('inlayHint/resolve', params, function(err, res)
623 | if err then
624 | reject(err)
625 | else
626 | resolve(res)
627 | end
628 | end)
629 | reject_ = reject
630 | end)
631 | function task.cancel()
632 | that.client.cancel_request(request_id)
633 | reject_(LSP.ErrorCodes.RequestCancelled)
634 | end
635 | return task
636 | end
637 |
638 | ---@param params nil
639 | function Client:workspace_inlayHint_refresh(params)
640 | local that, request_id, reject_ = self, nil, nil
641 | local task = AsyncTask.new(function(resolve, reject)
642 | request_id = self.client.request('workspace/inlayHint/refresh', params, function(err, res)
643 | if err then
644 | reject(err)
645 | else
646 | resolve(res)
647 | end
648 | end)
649 | reject_ = reject
650 | end)
651 | function task.cancel()
652 | that.client.cancel_request(request_id)
653 | reject_(LSP.ErrorCodes.RequestCancelled)
654 | end
655 | return task
656 | end
657 |
658 | ---@param params automa.kit.LSP.DocumentDiagnosticParams
659 | function Client:textDocument_diagnostic(params)
660 | local that, request_id, reject_ = self, nil, nil
661 | local task = AsyncTask.new(function(resolve, reject)
662 | request_id = self.client.request('textDocument/diagnostic', params, function(err, res)
663 | if err then
664 | reject(err)
665 | else
666 | resolve(res)
667 | end
668 | end)
669 | reject_ = reject
670 | end)
671 | function task.cancel()
672 | that.client.cancel_request(request_id)
673 | reject_(LSP.ErrorCodes.RequestCancelled)
674 | end
675 | return task
676 | end
677 |
678 | ---@param params automa.kit.LSP.WorkspaceDiagnosticParams
679 | function Client:workspace_diagnostic(params)
680 | local that, request_id, reject_ = self, nil, nil
681 | local task = AsyncTask.new(function(resolve, reject)
682 | request_id = self.client.request('workspace/diagnostic', params, function(err, res)
683 | if err then
684 | reject(err)
685 | else
686 | resolve(res)
687 | end
688 | end)
689 | reject_ = reject
690 | end)
691 | function task.cancel()
692 | that.client.cancel_request(request_id)
693 | reject_(LSP.ErrorCodes.RequestCancelled)
694 | end
695 | return task
696 | end
697 |
698 | ---@param params nil
699 | function Client:workspace_diagnostic_refresh(params)
700 | local that, request_id, reject_ = self, nil, nil
701 | local task = AsyncTask.new(function(resolve, reject)
702 | request_id = self.client.request('workspace/diagnostic/refresh', params, function(err, res)
703 | if err then
704 | reject(err)
705 | else
706 | resolve(res)
707 | end
708 | end)
709 | reject_ = reject
710 | end)
711 | function task.cancel()
712 | that.client.cancel_request(request_id)
713 | reject_(LSP.ErrorCodes.RequestCancelled)
714 | end
715 | return task
716 | end
717 |
718 | ---@param params automa.kit.LSP.InlineCompletionParams
719 | function Client:textDocument_inlineCompletion(params)
720 | local that, request_id, reject_ = self, nil, nil
721 | local task = AsyncTask.new(function(resolve, reject)
722 | request_id = self.client.request('textDocument/inlineCompletion', params, function(err, res)
723 | if err then
724 | reject(err)
725 | else
726 | resolve(res)
727 | end
728 | end)
729 | reject_ = reject
730 | end)
731 | function task.cancel()
732 | that.client.cancel_request(request_id)
733 | reject_(LSP.ErrorCodes.RequestCancelled)
734 | end
735 | return task
736 | end
737 |
738 | ---@param params automa.kit.LSP.RegistrationParams
739 | function Client:client_registerCapability(params)
740 | local that, request_id, reject_ = self, nil, nil
741 | local task = AsyncTask.new(function(resolve, reject)
742 | request_id = self.client.request('client/registerCapability', params, function(err, res)
743 | if err then
744 | reject(err)
745 | else
746 | resolve(res)
747 | end
748 | end)
749 | reject_ = reject
750 | end)
751 | function task.cancel()
752 | that.client.cancel_request(request_id)
753 | reject_(LSP.ErrorCodes.RequestCancelled)
754 | end
755 | return task
756 | end
757 |
758 | ---@param params automa.kit.LSP.UnregistrationParams
759 | function Client:client_unregisterCapability(params)
760 | local that, request_id, reject_ = self, nil, nil
761 | local task = AsyncTask.new(function(resolve, reject)
762 | request_id = self.client.request('client/unregisterCapability', params, function(err, res)
763 | if err then
764 | reject(err)
765 | else
766 | resolve(res)
767 | end
768 | end)
769 | reject_ = reject
770 | end)
771 | function task.cancel()
772 | that.client.cancel_request(request_id)
773 | reject_(LSP.ErrorCodes.RequestCancelled)
774 | end
775 | return task
776 | end
777 |
778 | ---@param params automa.kit.LSP.InitializeParams
779 | function Client:initialize(params)
780 | local that, request_id, reject_ = self, nil, nil
781 | local task = AsyncTask.new(function(resolve, reject)
782 | request_id = self.client.request('initialize', params, function(err, res)
783 | if err then
784 | reject(err)
785 | else
786 | resolve(res)
787 | end
788 | end)
789 | reject_ = reject
790 | end)
791 | function task.cancel()
792 | that.client.cancel_request(request_id)
793 | reject_(LSP.ErrorCodes.RequestCancelled)
794 | end
795 | return task
796 | end
797 |
798 | ---@param params nil
799 | function Client:shutdown(params)
800 | local that, request_id, reject_ = self, nil, nil
801 | local task = AsyncTask.new(function(resolve, reject)
802 | request_id = self.client.request('shutdown', params, function(err, res)
803 | if err then
804 | reject(err)
805 | else
806 | resolve(res)
807 | end
808 | end)
809 | reject_ = reject
810 | end)
811 | function task.cancel()
812 | that.client.cancel_request(request_id)
813 | reject_(LSP.ErrorCodes.RequestCancelled)
814 | end
815 | return task
816 | end
817 |
818 | ---@param params automa.kit.LSP.ShowMessageRequestParams
819 | function Client:window_showMessageRequest(params)
820 | local that, request_id, reject_ = self, nil, nil
821 | local task = AsyncTask.new(function(resolve, reject)
822 | request_id = self.client.request('window/showMessageRequest', params, function(err, res)
823 | if err then
824 | reject(err)
825 | else
826 | resolve(res)
827 | end
828 | end)
829 | reject_ = reject
830 | end)
831 | function task.cancel()
832 | that.client.cancel_request(request_id)
833 | reject_(LSP.ErrorCodes.RequestCancelled)
834 | end
835 | return task
836 | end
837 |
838 | ---@param params automa.kit.LSP.WillSaveTextDocumentParams
839 | function Client:textDocument_willSaveWaitUntil(params)
840 | local that, request_id, reject_ = self, nil, nil
841 | local task = AsyncTask.new(function(resolve, reject)
842 | request_id = self.client.request('textDocument/willSaveWaitUntil', params, function(err, res)
843 | if err then
844 | reject(err)
845 | else
846 | resolve(res)
847 | end
848 | end)
849 | reject_ = reject
850 | end)
851 | function task.cancel()
852 | that.client.cancel_request(request_id)
853 | reject_(LSP.ErrorCodes.RequestCancelled)
854 | end
855 | return task
856 | end
857 |
858 | ---@param params automa.kit.LSP.CompletionParams
859 | function Client:textDocument_completion(params)
860 | local that, request_id, reject_ = self, nil, nil
861 | local task = AsyncTask.new(function(resolve, reject)
862 | request_id = self.client.request('textDocument/completion', params, function(err, res)
863 | if err then
864 | reject(err)
865 | else
866 | resolve(res)
867 | end
868 | end)
869 | reject_ = reject
870 | end)
871 | function task.cancel()
872 | that.client.cancel_request(request_id)
873 | reject_(LSP.ErrorCodes.RequestCancelled)
874 | end
875 | return task
876 | end
877 |
878 | ---@param params automa.kit.LSP.CompletionItem
879 | function Client:completionItem_resolve(params)
880 | local that, request_id, reject_ = self, nil, nil
881 | local task = AsyncTask.new(function(resolve, reject)
882 | request_id = self.client.request('completionItem/resolve', params, function(err, res)
883 | if err then
884 | reject(err)
885 | else
886 | resolve(res)
887 | end
888 | end)
889 | reject_ = reject
890 | end)
891 | function task.cancel()
892 | that.client.cancel_request(request_id)
893 | reject_(LSP.ErrorCodes.RequestCancelled)
894 | end
895 | return task
896 | end
897 |
898 | ---@param params automa.kit.LSP.HoverParams
899 | function Client:textDocument_hover(params)
900 | local that, request_id, reject_ = self, nil, nil
901 | local task = AsyncTask.new(function(resolve, reject)
902 | request_id = self.client.request('textDocument/hover', params, function(err, res)
903 | if err then
904 | reject(err)
905 | else
906 | resolve(res)
907 | end
908 | end)
909 | reject_ = reject
910 | end)
911 | function task.cancel()
912 | that.client.cancel_request(request_id)
913 | reject_(LSP.ErrorCodes.RequestCancelled)
914 | end
915 | return task
916 | end
917 |
918 | ---@param params automa.kit.LSP.SignatureHelpParams
919 | function Client:textDocument_signatureHelp(params)
920 | local that, request_id, reject_ = self, nil, nil
921 | local task = AsyncTask.new(function(resolve, reject)
922 | request_id = self.client.request('textDocument/signatureHelp', params, function(err, res)
923 | if err then
924 | reject(err)
925 | else
926 | resolve(res)
927 | end
928 | end)
929 | reject_ = reject
930 | end)
931 | function task.cancel()
932 | that.client.cancel_request(request_id)
933 | reject_(LSP.ErrorCodes.RequestCancelled)
934 | end
935 | return task
936 | end
937 |
938 | ---@param params automa.kit.LSP.DefinitionParams
939 | function Client:textDocument_definition(params)
940 | local that, request_id, reject_ = self, nil, nil
941 | local task = AsyncTask.new(function(resolve, reject)
942 | request_id = self.client.request('textDocument/definition', params, function(err, res)
943 | if err then
944 | reject(err)
945 | else
946 | resolve(res)
947 | end
948 | end)
949 | reject_ = reject
950 | end)
951 | function task.cancel()
952 | that.client.cancel_request(request_id)
953 | reject_(LSP.ErrorCodes.RequestCancelled)
954 | end
955 | return task
956 | end
957 |
958 | ---@param params automa.kit.LSP.ReferenceParams
959 | function Client:textDocument_references(params)
960 | local that, request_id, reject_ = self, nil, nil
961 | local task = AsyncTask.new(function(resolve, reject)
962 | request_id = self.client.request('textDocument/references', params, function(err, res)
963 | if err then
964 | reject(err)
965 | else
966 | resolve(res)
967 | end
968 | end)
969 | reject_ = reject
970 | end)
971 | function task.cancel()
972 | that.client.cancel_request(request_id)
973 | reject_(LSP.ErrorCodes.RequestCancelled)
974 | end
975 | return task
976 | end
977 |
978 | ---@param params automa.kit.LSP.DocumentHighlightParams
979 | function Client:textDocument_documentHighlight(params)
980 | local that, request_id, reject_ = self, nil, nil
981 | local task = AsyncTask.new(function(resolve, reject)
982 | request_id = self.client.request('textDocument/documentHighlight', params, function(err, res)
983 | if err then
984 | reject(err)
985 | else
986 | resolve(res)
987 | end
988 | end)
989 | reject_ = reject
990 | end)
991 | function task.cancel()
992 | that.client.cancel_request(request_id)
993 | reject_(LSP.ErrorCodes.RequestCancelled)
994 | end
995 | return task
996 | end
997 |
998 | ---@param params automa.kit.LSP.DocumentSymbolParams
999 | function Client:textDocument_documentSymbol(params)
1000 | local that, request_id, reject_ = self, nil, nil
1001 | local task = AsyncTask.new(function(resolve, reject)
1002 | request_id = self.client.request('textDocument/documentSymbol', params, function(err, res)
1003 | if err then
1004 | reject(err)
1005 | else
1006 | resolve(res)
1007 | end
1008 | end)
1009 | reject_ = reject
1010 | end)
1011 | function task.cancel()
1012 | that.client.cancel_request(request_id)
1013 | reject_(LSP.ErrorCodes.RequestCancelled)
1014 | end
1015 | return task
1016 | end
1017 |
1018 | ---@param params automa.kit.LSP.CodeActionParams
1019 | function Client:textDocument_codeAction(params)
1020 | local that, request_id, reject_ = self, nil, nil
1021 | local task = AsyncTask.new(function(resolve, reject)
1022 | request_id = self.client.request('textDocument/codeAction', params, function(err, res)
1023 | if err then
1024 | reject(err)
1025 | else
1026 | resolve(res)
1027 | end
1028 | end)
1029 | reject_ = reject
1030 | end)
1031 | function task.cancel()
1032 | that.client.cancel_request(request_id)
1033 | reject_(LSP.ErrorCodes.RequestCancelled)
1034 | end
1035 | return task
1036 | end
1037 |
1038 | ---@param params automa.kit.LSP.CodeAction
1039 | function Client:codeAction_resolve(params)
1040 | local that, request_id, reject_ = self, nil, nil
1041 | local task = AsyncTask.new(function(resolve, reject)
1042 | request_id = self.client.request('codeAction/resolve', params, function(err, res)
1043 | if err then
1044 | reject(err)
1045 | else
1046 | resolve(res)
1047 | end
1048 | end)
1049 | reject_ = reject
1050 | end)
1051 | function task.cancel()
1052 | that.client.cancel_request(request_id)
1053 | reject_(LSP.ErrorCodes.RequestCancelled)
1054 | end
1055 | return task
1056 | end
1057 |
1058 | ---@param params automa.kit.LSP.WorkspaceSymbolParams
1059 | function Client:workspace_symbol(params)
1060 | local that, request_id, reject_ = self, nil, nil
1061 | local task = AsyncTask.new(function(resolve, reject)
1062 | request_id = self.client.request('workspace/symbol', params, function(err, res)
1063 | if err then
1064 | reject(err)
1065 | else
1066 | resolve(res)
1067 | end
1068 | end)
1069 | reject_ = reject
1070 | end)
1071 | function task.cancel()
1072 | that.client.cancel_request(request_id)
1073 | reject_(LSP.ErrorCodes.RequestCancelled)
1074 | end
1075 | return task
1076 | end
1077 |
1078 | ---@param params automa.kit.LSP.WorkspaceSymbol
1079 | function Client:workspaceSymbol_resolve(params)
1080 | local that, request_id, reject_ = self, nil, nil
1081 | local task = AsyncTask.new(function(resolve, reject)
1082 | request_id = self.client.request('workspaceSymbol/resolve', params, function(err, res)
1083 | if err then
1084 | reject(err)
1085 | else
1086 | resolve(res)
1087 | end
1088 | end)
1089 | reject_ = reject
1090 | end)
1091 | function task.cancel()
1092 | that.client.cancel_request(request_id)
1093 | reject_(LSP.ErrorCodes.RequestCancelled)
1094 | end
1095 | return task
1096 | end
1097 |
1098 | ---@param params automa.kit.LSP.CodeLensParams
1099 | function Client:textDocument_codeLens(params)
1100 | local that, request_id, reject_ = self, nil, nil
1101 | local task = AsyncTask.new(function(resolve, reject)
1102 | request_id = self.client.request('textDocument/codeLens', params, function(err, res)
1103 | if err then
1104 | reject(err)
1105 | else
1106 | resolve(res)
1107 | end
1108 | end)
1109 | reject_ = reject
1110 | end)
1111 | function task.cancel()
1112 | that.client.cancel_request(request_id)
1113 | reject_(LSP.ErrorCodes.RequestCancelled)
1114 | end
1115 | return task
1116 | end
1117 |
1118 | ---@param params automa.kit.LSP.CodeLens
1119 | function Client:codeLens_resolve(params)
1120 | local that, request_id, reject_ = self, nil, nil
1121 | local task = AsyncTask.new(function(resolve, reject)
1122 | request_id = self.client.request('codeLens/resolve', params, function(err, res)
1123 | if err then
1124 | reject(err)
1125 | else
1126 | resolve(res)
1127 | end
1128 | end)
1129 | reject_ = reject
1130 | end)
1131 | function task.cancel()
1132 | that.client.cancel_request(request_id)
1133 | reject_(LSP.ErrorCodes.RequestCancelled)
1134 | end
1135 | return task
1136 | end
1137 |
1138 | ---@param params nil
1139 | function Client:workspace_codeLens_refresh(params)
1140 | local that, request_id, reject_ = self, nil, nil
1141 | local task = AsyncTask.new(function(resolve, reject)
1142 | request_id = self.client.request('workspace/codeLens/refresh', params, function(err, res)
1143 | if err then
1144 | reject(err)
1145 | else
1146 | resolve(res)
1147 | end
1148 | end)
1149 | reject_ = reject
1150 | end)
1151 | function task.cancel()
1152 | that.client.cancel_request(request_id)
1153 | reject_(LSP.ErrorCodes.RequestCancelled)
1154 | end
1155 | return task
1156 | end
1157 |
1158 | ---@param params automa.kit.LSP.DocumentLinkParams
1159 | function Client:textDocument_documentLink(params)
1160 | local that, request_id, reject_ = self, nil, nil
1161 | local task = AsyncTask.new(function(resolve, reject)
1162 | request_id = self.client.request('textDocument/documentLink', params, function(err, res)
1163 | if err then
1164 | reject(err)
1165 | else
1166 | resolve(res)
1167 | end
1168 | end)
1169 | reject_ = reject
1170 | end)
1171 | function task.cancel()
1172 | that.client.cancel_request(request_id)
1173 | reject_(LSP.ErrorCodes.RequestCancelled)
1174 | end
1175 | return task
1176 | end
1177 |
1178 | ---@param params automa.kit.LSP.DocumentLink
1179 | function Client:documentLink_resolve(params)
1180 | local that, request_id, reject_ = self, nil, nil
1181 | local task = AsyncTask.new(function(resolve, reject)
1182 | request_id = self.client.request('documentLink/resolve', params, function(err, res)
1183 | if err then
1184 | reject(err)
1185 | else
1186 | resolve(res)
1187 | end
1188 | end)
1189 | reject_ = reject
1190 | end)
1191 | function task.cancel()
1192 | that.client.cancel_request(request_id)
1193 | reject_(LSP.ErrorCodes.RequestCancelled)
1194 | end
1195 | return task
1196 | end
1197 |
1198 | ---@param params automa.kit.LSP.DocumentFormattingParams
1199 | function Client:textDocument_formatting(params)
1200 | local that, request_id, reject_ = self, nil, nil
1201 | local task = AsyncTask.new(function(resolve, reject)
1202 | request_id = self.client.request('textDocument/formatting', params, function(err, res)
1203 | if err then
1204 | reject(err)
1205 | else
1206 | resolve(res)
1207 | end
1208 | end)
1209 | reject_ = reject
1210 | end)
1211 | function task.cancel()
1212 | that.client.cancel_request(request_id)
1213 | reject_(LSP.ErrorCodes.RequestCancelled)
1214 | end
1215 | return task
1216 | end
1217 |
1218 | ---@param params automa.kit.LSP.DocumentRangeFormattingParams
1219 | function Client:textDocument_rangeFormatting(params)
1220 | local that, request_id, reject_ = self, nil, nil
1221 | local task = AsyncTask.new(function(resolve, reject)
1222 | request_id = self.client.request('textDocument/rangeFormatting', params, function(err, res)
1223 | if err then
1224 | reject(err)
1225 | else
1226 | resolve(res)
1227 | end
1228 | end)
1229 | reject_ = reject
1230 | end)
1231 | function task.cancel()
1232 | that.client.cancel_request(request_id)
1233 | reject_(LSP.ErrorCodes.RequestCancelled)
1234 | end
1235 | return task
1236 | end
1237 |
1238 | ---@param params automa.kit.LSP.DocumentRangesFormattingParams
1239 | function Client:textDocument_rangesFormatting(params)
1240 | local that, request_id, reject_ = self, nil, nil
1241 | local task = AsyncTask.new(function(resolve, reject)
1242 | request_id = self.client.request('textDocument/rangesFormatting', params, function(err, res)
1243 | if err then
1244 | reject(err)
1245 | else
1246 | resolve(res)
1247 | end
1248 | end)
1249 | reject_ = reject
1250 | end)
1251 | function task.cancel()
1252 | that.client.cancel_request(request_id)
1253 | reject_(LSP.ErrorCodes.RequestCancelled)
1254 | end
1255 | return task
1256 | end
1257 |
1258 | ---@param params automa.kit.LSP.DocumentOnTypeFormattingParams
1259 | function Client:textDocument_onTypeFormatting(params)
1260 | local that, request_id, reject_ = self, nil, nil
1261 | local task = AsyncTask.new(function(resolve, reject)
1262 | request_id = self.client.request('textDocument/onTypeFormatting', params, function(err, res)
1263 | if err then
1264 | reject(err)
1265 | else
1266 | resolve(res)
1267 | end
1268 | end)
1269 | reject_ = reject
1270 | end)
1271 | function task.cancel()
1272 | that.client.cancel_request(request_id)
1273 | reject_(LSP.ErrorCodes.RequestCancelled)
1274 | end
1275 | return task
1276 | end
1277 |
1278 | ---@param params automa.kit.LSP.RenameParams
1279 | function Client:textDocument_rename(params)
1280 | local that, request_id, reject_ = self, nil, nil
1281 | local task = AsyncTask.new(function(resolve, reject)
1282 | request_id = self.client.request('textDocument/rename', params, function(err, res)
1283 | if err then
1284 | reject(err)
1285 | else
1286 | resolve(res)
1287 | end
1288 | end)
1289 | reject_ = reject
1290 | end)
1291 | function task.cancel()
1292 | that.client.cancel_request(request_id)
1293 | reject_(LSP.ErrorCodes.RequestCancelled)
1294 | end
1295 | return task
1296 | end
1297 |
1298 | ---@param params automa.kit.LSP.PrepareRenameParams
1299 | function Client:textDocument_prepareRename(params)
1300 | local that, request_id, reject_ = self, nil, nil
1301 | local task = AsyncTask.new(function(resolve, reject)
1302 | request_id = self.client.request('textDocument/prepareRename', params, function(err, res)
1303 | if err then
1304 | reject(err)
1305 | else
1306 | resolve(res)
1307 | end
1308 | end)
1309 | reject_ = reject
1310 | end)
1311 | function task.cancel()
1312 | that.client.cancel_request(request_id)
1313 | reject_(LSP.ErrorCodes.RequestCancelled)
1314 | end
1315 | return task
1316 | end
1317 |
1318 | ---@param params automa.kit.LSP.ExecuteCommandParams
1319 | function Client:workspace_executeCommand(params)
1320 | local that, request_id, reject_ = self, nil, nil
1321 | local task = AsyncTask.new(function(resolve, reject)
1322 | request_id = self.client.request('workspace/executeCommand', params, function(err, res)
1323 | if err then
1324 | reject(err)
1325 | else
1326 | resolve(res)
1327 | end
1328 | end)
1329 | reject_ = reject
1330 | end)
1331 | function task.cancel()
1332 | that.client.cancel_request(request_id)
1333 | reject_(LSP.ErrorCodes.RequestCancelled)
1334 | end
1335 | return task
1336 | end
1337 |
1338 | ---@param params automa.kit.LSP.ApplyWorkspaceEditParams
1339 | function Client:workspace_applyEdit(params)
1340 | local that, request_id, reject_ = self, nil, nil
1341 | local task = AsyncTask.new(function(resolve, reject)
1342 | request_id = self.client.request('workspace/applyEdit', params, function(err, res)
1343 | if err then
1344 | reject(err)
1345 | else
1346 | resolve(res)
1347 | end
1348 | end)
1349 | reject_ = reject
1350 | end)
1351 | function task.cancel()
1352 | that.client.cancel_request(request_id)
1353 | reject_(LSP.ErrorCodes.RequestCancelled)
1354 | end
1355 | return task
1356 | end
1357 |
1358 | return Client
1359 |
--------------------------------------------------------------------------------
/lua/automa/kit/LSP/DocumentSelector.lua:
--------------------------------------------------------------------------------
1 | local LanguageId = require('automa.kit.LSP.LanguageId')
2 |
3 | -- NOTE
4 | --@alias automa.kit.LSP.DocumentSelector automa.kit.LSP.DocumentFilter[]
5 | --@alias automa.kit.LSP.DocumentFilter (automa.kit.LSP.TextDocumentFilter | automa.kit.LSP.NotebookCellTextDocumentFilter)
6 | --@alias automa.kit.LSP.TextDocumentFilter ({ language: string, scheme?: string, pattern?: string } | { language?: string, scheme: string, pattern?: string } | { language?: string, scheme?: string, pattern: string })
7 | --@class automa.kit.LSP.NotebookCellTextDocumentFilter
8 | --@field public notebook (string | automa.kit.LSP.NotebookDocumentFilter) A filter that matches against the notebook
containing the notebook cell. If a string
value is provided it matches against the
notebook type. '*' matches every notebook.
9 | --@field public language? string A language id like `python`.
Will be matched against the language id of the
notebook cell document. '*' matches every language.
10 | --@alias automa.kit.LSP.NotebookDocumentFilter ({ notebookType: string, scheme?: string, pattern?: string } | { notebookType?: string, scheme: string, pattern?: string } | { notebookType?: string, scheme?: string, pattern: string })
11 |
12 | ---@alias automa.kit.LSP.DocumentSelector.NormalizedFilter { notebook_type: string?, scheme: string?, pattern: string, language: string? }
13 |
14 | ---Normalize the filter.
15 | ---@param document_filter automa.kit.LSP.DocumentFilter
16 | ---@return automa.kit.LSP.DocumentSelector.NormalizedFilter | nil
17 | local function normalize_filter(document_filter)
18 | if document_filter.notebook then
19 | local filter = document_filter --[[@as automa.kit.LSP.NotebookCellTextDocumentFilter]]
20 | if type(filter.notebook) == 'string' then
21 | return {
22 | notebook_type = nil,
23 | scheme = nil,
24 | pattern = filter.notebook,
25 | language = filter.language,
26 | }
27 | elseif filter.notebook then
28 | return {
29 | notebook_type = filter.notebook.notebookType,
30 | scheme = filter.notebook.scheme,
31 | pattern = filter.notebook.pattern,
32 | language = filter.language,
33 | }
34 | end
35 | else
36 | local filter = document_filter --[[@as automa.kit.LSP.TextDocumentFilter]]
37 | return {
38 | notebook_type = nil,
39 | scheme = filter.scheme,
40 | pattern = filter.pattern,
41 | language = filter.language,
42 | }
43 | end
44 | end
45 |
46 | ---Return the document filter score.
47 | ---TODO: file-related buffer check is not implemented...
48 | ---TODO: notebook related function is not implemented...
49 | ---@param filter? automa.kit.LSP.DocumentSelector.NormalizedFilter
50 | ---@param uri string
51 | ---@param language string
52 | ---@return integer
53 | local function score(filter, uri, language)
54 | if not filter then
55 | return 0
56 | end
57 |
58 | local s = 0
59 |
60 | if filter.scheme then
61 | if filter.scheme == '*' then
62 | s = 5
63 | elseif filter.scheme == uri:sub(1, #filter.scheme) then
64 | s = 10
65 | else
66 | return 0
67 | end
68 | end
69 |
70 | if filter.language then
71 | if filter.language == '*' then
72 | s = math.max(s, 5)
73 | elseif filter.language == language then
74 | s = 10
75 | else
76 | return 0
77 | end
78 | end
79 |
80 | if filter.pattern then
81 | if vim.glob.to_lpeg(filter.pattern):match(uri) ~= nil then
82 | s = 10
83 | else
84 | return 0
85 | end
86 | end
87 |
88 | return s
89 | end
90 |
91 | local DocumentSelector = {}
92 |
93 | ---Check buffer matches the selector.
94 | ---@see https://github.com/microsoft/vscode/blob/7241eea61021db926c052b657d577ef0d98f7dc7/src/vs/editor/common/languageSelector.ts#L29
95 | ---@param bufnr integer
96 | ---@param document_selector automa.kit.LSP.DocumentSelector
97 | function DocumentSelector.score(bufnr, document_selector)
98 | local uri = vim.uri_from_bufnr(bufnr)
99 | local language = LanguageId.from_filetype(vim.api.nvim_buf_get_option(bufnr, 'filetype'))
100 | local r = 0
101 | for _, document_filter in ipairs(document_selector) do
102 | local filter = normalize_filter(document_filter)
103 | if filter then
104 | local s = score(filter, uri, language)
105 | if s == 10 then
106 | return 10
107 | end
108 | r = math.max(r, s)
109 | end
110 | end
111 | return r
112 | end
113 |
114 | return DocumentSelector
115 |
--------------------------------------------------------------------------------
/lua/automa/kit/LSP/LanguageId.lua:
--------------------------------------------------------------------------------
1 | local mapping = {
2 | ['sh'] = 'shellscript',
3 | ['javascript.tsx'] = 'javascriptreact',
4 | ['typescript.tsx'] = 'typescriptreact',
5 | }
6 |
7 | local LanguageId = {}
8 |
9 | function LanguageId.from_filetype(filetype)
10 | return mapping[filetype] or filetype
11 | end
12 |
13 | return LanguageId
14 |
--------------------------------------------------------------------------------
/lua/automa/kit/LSP/Position.lua:
--------------------------------------------------------------------------------
1 | local LSP = require('automa.kit.LSP')
2 |
3 | local Position = {}
4 |
5 | ---Return the value is position or not.
6 | ---@param v any
7 | ---@return boolean
8 | function Position.is(v)
9 | local is = true
10 | is = is and (type(v) == 'table' and type(v.line) == 'number' and type(v.character) == 'number')
11 | return is
12 | end
13 |
14 | ---Create a cursor position.
15 | ---@param encoding? automa.kit.LSP.PositionEncodingKind
16 | function Position.cursor(encoding)
17 | local r, c = unpack(vim.api.nvim_win_get_cursor(0))
18 | local utf8 = { line = r - 1, character = c }
19 | if encoding == LSP.PositionEncodingKind.UTF8 then
20 | return utf8
21 | else
22 | local text = vim.api.nvim_get_current_line()
23 | if encoding == LSP.PositionEncodingKind.UTF32 then
24 | return Position.to(text, utf8, LSP.PositionEncodingKind.UTF8, LSP.PositionEncodingKind.UTF32)
25 | end
26 | return Position.to(text, utf8, LSP.PositionEncodingKind.UTF8, LSP.PositionEncodingKind.UTF16)
27 | end
28 | end
29 |
30 | ---Convert position to buffer position from specified encoding.
31 | ---@param bufnr integer
32 | ---@param position automa.kit.LSP.Position
33 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
34 | function Position.to_buf(bufnr, position, from_encoding)
35 | from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16
36 | local text = vim.api.nvim_buf_get_lines(bufnr, position.line, position.line + 1, false)[1] or ''
37 | return Position.to(text, position, from_encoding, LSP.PositionEncodingKind.UTF8)
38 | end
39 |
40 | ---Convert position to utf8 from specified encoding.
41 | ---@param text string
42 | ---@param position automa.kit.LSP.Position
43 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
44 | ---@return automa.kit.LSP.Position
45 | function Position.to_utf8(text, position, from_encoding)
46 | from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16
47 | if from_encoding == LSP.PositionEncodingKind.UTF8 then
48 | return position
49 | end
50 | local ok, byteindex = pcall(vim.str_byteindex, text, position.character, from_encoding == LSP.PositionEncodingKind.UTF16)
51 | if ok then
52 | position = { line = position.line, character = byteindex }
53 | end
54 | return position
55 | end
56 |
57 | ---Convert position to utf16 from specified encoding.
58 | ---@param text string
59 | ---@param position automa.kit.LSP.Position
60 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
61 | ---@return automa.kit.LSP.Position
62 | function Position.to_utf16(text, position, from_encoding)
63 | local utf8 = Position.to_utf8(text, position, from_encoding)
64 | for index = utf8.character, 0, -1 do
65 | local ok, _, utf16index = pcall(vim.str_utfindex, text, index)
66 | if ok then
67 | position = { line = utf8.line, character = utf16index }
68 | break
69 | end
70 | end
71 | return position
72 | end
73 |
74 | ---Convert position to utf32 from specified encoding.
75 | ---@param text string
76 | ---@param position automa.kit.LSP.Position
77 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
78 | ---@return automa.kit.LSP.Position
79 | function Position.to_utf32(text, position, from_encoding)
80 | local utf8 = Position.to_utf8(text, position, from_encoding)
81 | for index = utf8.character, 0, -1 do
82 | local ok, utf32index = pcall(vim.str_utfindex, text, index)
83 | if ok then
84 | position = { line = utf8.line, character = utf32index }
85 | break
86 | end
87 | end
88 | return position
89 | end
90 |
91 | ---Convert position to specified encoding from specified encoding.
92 | ---@param text string
93 | ---@param position automa.kit.LSP.Position
94 | ---@param from_encoding automa.kit.LSP.PositionEncodingKind
95 | ---@param to_encoding automa.kit.LSP.PositionEncodingKind
96 | function Position.to(text, position, from_encoding, to_encoding)
97 | if to_encoding == LSP.PositionEncodingKind.UTF8 then
98 | return Position.to_utf8(text, position, from_encoding)
99 | elseif to_encoding == LSP.PositionEncodingKind.UTF16 then
100 | return Position.to_utf16(text, position, from_encoding)
101 | elseif to_encoding == LSP.PositionEncodingKind.UTF32 then
102 | return Position.to_utf32(text, position, from_encoding)
103 | end
104 | error('LSP.Position: Unsupported encoding: ' .. to_encoding)
105 | end
106 |
107 | return Position
108 |
--------------------------------------------------------------------------------
/lua/automa/kit/LSP/Range.lua:
--------------------------------------------------------------------------------
1 | local Position = require('automa.kit.LSP.Position')
2 |
3 | local Range = {}
4 |
5 | ---Return the value is range or not.
6 | ---@param v any
7 | ---@return boolean
8 | function Range.is(v)
9 | return type(v) == 'table' and Position.is(v.start) and Position.is(v['end'])
10 | end
11 |
12 | ---Return the range is empty or not.
13 | ---@param range automa.kit.LSP.Range
14 | ---@return boolean
15 | function Range.empty(range)
16 | return range.start.line == range['end'].line and range.start.character == range['end'].character
17 | end
18 |
19 | ---Return the range is empty or not.
20 | ---@param range automa.kit.LSP.Range
21 | ---@return boolean
22 | function Range.contains(range)
23 | return range.start.line == range['end'].line and range.start.character == range['end'].character
24 | end
25 |
26 | ---Convert range to buffer range from specified encoding.
27 | ---@param bufnr integer
28 | ---@param range automa.kit.LSP.Range
29 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
30 | ---@return automa.kit.LSP.Range
31 | function Range.to_buf(bufnr, range, from_encoding)
32 | return {
33 | start = Position.to_buf(bufnr, range.start, from_encoding),
34 | ['end'] = Position.to_buf(bufnr, range['end'], from_encoding),
35 | }
36 | end
37 |
38 | ---Convert range to utf8 from specified encoding.
39 | ---@param text_start string
40 | ---@param range automa.kit.LSP.Range
41 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
42 | ---@return automa.kit.LSP.Range
43 | function Range.to_utf8(text_start, text_end, range, from_encoding)
44 | return {
45 | start = Position.to_utf8(text_start, range.start, from_encoding),
46 | ['end'] = Position.to_utf8(text_end, range['end'], from_encoding),
47 | }
48 | end
49 |
50 | ---Convert range to utf16 from specified encoding.
51 | ---@param text_start string
52 | ---@param range automa.kit.LSP.Range
53 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
54 | ---@return automa.kit.LSP.Range
55 | function Range.to_utf16(text_start, text_end, range, from_encoding)
56 | return {
57 | start = Position.to_utf16(text_start, range.start, from_encoding),
58 | ['end'] = Position.to_utf16(text_end, range['end'], from_encoding),
59 | }
60 | end
61 |
62 | ---Convert range to utf32 from specified encoding.
63 | ---@param text_start string
64 | ---@param range automa.kit.LSP.Range
65 | ---@param from_encoding? automa.kit.LSP.PositionEncodingKind
66 | ---@return automa.kit.LSP.Range
67 | function Range.to_utf32(text_start, text_end, range, from_encoding)
68 | return {
69 | start = Position.to_utf32(text_start, range.start, from_encoding),
70 | ['end'] = Position.to_utf32(text_end, range['end'], from_encoding),
71 | }
72 | end
73 |
74 | return Range
75 |
--------------------------------------------------------------------------------
/lua/automa/kit/Spec/init.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 | local assert = require('luassert')
3 |
4 | ---@class automa.Spec.SetupOption
5 | ---@field filetype? string
6 | ---@field noexpandtab? boolean
7 | ---@field shiftwidth? integer
8 | ---@field tabstop? integer
9 |
10 | ---@param buffer string|string[]
11 | local function parse_buffer(buffer)
12 | buffer = kit.to_array(buffer)
13 |
14 | for i, line in ipairs(buffer) do
15 | local s = line:find('|', 1, true)
16 | if s then
17 | buffer[i] = line:gsub('|', '')
18 | return buffer, { i, s - 1 }
19 | end
20 | end
21 | error('cursor position is not found.')
22 | end
23 |
24 | local Spec = {}
25 |
26 | ---Setup buffer.
27 | ---@param buffer string|string[]
28 | ---@param option? automa.Spec.SetupOption
29 | function Spec.setup(buffer, option)
30 | option = option or {}
31 |
32 | vim.cmd.enew({ bang = true })
33 | vim.cmd([[ set noswapfile ]])
34 | vim.cmd([[ set virtualedit=onemore ]])
35 | vim.cmd(([[ set shiftwidth=%s ]]):format(option.shiftwidth or 2))
36 | vim.cmd(([[ set tabstop=%s ]]):format(option.tabstop or 2))
37 | if option.noexpandtab then
38 | vim.cmd([[ set noexpandtab ]])
39 | else
40 | vim.cmd([[ set expandtab ]])
41 | end
42 | if option.filetype then
43 | vim.cmd(([[ set filetype=%s ]]):format(option.filetype))
44 | end
45 |
46 | local lines, cursor = parse_buffer(buffer)
47 | vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
48 | vim.api.nvim_win_set_cursor(0, cursor)
49 | end
50 |
51 | ---Expect buffer.
52 | function Spec.expect(buffer)
53 | local lines, cursor = parse_buffer(buffer)
54 | assert.are.same(lines, vim.api.nvim_buf_get_lines(0, 0, -1, false))
55 | assert.are.same(cursor, vim.api.nvim_win_get_cursor(0))
56 | end
57 |
58 | return Spec
59 |
--------------------------------------------------------------------------------
/lua/automa/kit/System/init.lua:
--------------------------------------------------------------------------------
1 | -- luacheck: ignore 212
2 | --
3 | local kit = require('automa.kit')
4 |
5 | local System = {}
6 |
7 | ---@class automa.kit.System.Buffer
8 | ---@field write fun(data: string)
9 | ---@field close fun()
10 |
11 | ---@class automa.kit.System.Buffering
12 | ---@field create fun(self: any, callback: fun(data: string)): automa.kit.System.Buffer
13 |
14 | ---@class automa.kit.System.LineBuffering: automa.kit.System.Buffering
15 | ---@field ignore_empty boolean
16 | System.LineBuffering = {}
17 | System.LineBuffering.__index = System.LineBuffering
18 |
19 | ---Create LineBuffering.
20 | ---@param option { ignore_empty?: boolean }
21 | function System.LineBuffering.new(option)
22 | return setmetatable({
23 | ignore_empty = option.ignore_empty or false,
24 | }, System.LineBuffering)
25 | end
26 |
27 | ---Create LineBuffer object.
28 | function System.LineBuffering:create(callback)
29 | local buffer = {}
30 | return {
31 | write = function(data)
32 | data = (data:gsub('\r\n', '\n'))
33 | data = (data:gsub('\r', '\n'))
34 | table.insert(buffer, data)
35 |
36 | local has = false
37 | for i = #data, 1, -1 do
38 | if data:sub(i, i) == '\n' then
39 | has = true
40 | break
41 | end
42 | end
43 |
44 | if has then
45 | local texts = vim.split(table.concat(buffer, ''), '\n')
46 | buffer = texts[#texts] ~= '' and { table.remove(texts) } or {}
47 | for _, text in ipairs(texts) do
48 | if self.ignore_empty then
49 | if text:gsub('^%s*', ''):gsub('%s*$', '') ~= '' then
50 | callback(text)
51 | end
52 | else
53 | callback(text)
54 | end
55 | end
56 | end
57 | end,
58 | close = function()
59 | if #buffer > 0 then
60 | callback(table.concat(buffer, ''))
61 | end
62 | end
63 | }
64 | end
65 |
66 | ---@class automa.kit.System.PatternBuffering: automa.kit.System.Buffering
67 | ---@field pattern string
68 | System.PatternBuffering = {}
69 | System.PatternBuffering.__index = System.PatternBuffering
70 |
71 | ---Create PatternBuffering.
72 | ---@param option { pattern: string }
73 | function System.PatternBuffering.new(option)
74 | return setmetatable({
75 | pattern = option.pattern,
76 | }, System.PatternBuffering)
77 | end
78 |
79 | ---Create PatternBuffer object.
80 | function System.PatternBuffering:create(callback)
81 | local buffer = {}
82 | return {
83 | write = function(data)
84 | while true do
85 | local s, e = data:find(self.pattern)
86 | if s then
87 | table.insert(buffer, data:sub(1, s - 1))
88 | callback(table.concat(buffer, ''))
89 | buffer = {}
90 |
91 | if e < #data then
92 | data = data:sub(e + 1)
93 | else
94 | break
95 | end
96 | else
97 | table.insert(buffer, data)
98 | break
99 | end
100 | end
101 | end,
102 | close = function()
103 | if #buffer > 0 then
104 | callback(table.concat(buffer, ''))
105 | end
106 | end
107 | }
108 | end
109 |
110 | ---@class automa.kit.System.RawBuffering: automa.kit.System.Buffering
111 | System.RawBuffering = {}
112 | System.RawBuffering.__index = System.RawBuffering
113 |
114 | ---Create RawBuffering.
115 | function System.RawBuffering.new()
116 | return setmetatable({}, System.RawBuffering)
117 | end
118 |
119 | ---Create RawBuffer object.
120 | function System.RawBuffering:create(callback)
121 | return {
122 | write = function(data)
123 | callback(data)
124 | end,
125 | close = function()
126 | -- noop.
127 | end
128 | }
129 | end
130 |
131 | ---Spawn a new process.
132 | ---@class automa.kit.System.SpawnParams
133 | ---@field cwd string
134 | ---@field input? string|string[]
135 | ---@field on_stdout? fun(data: string)
136 | ---@field on_stderr? fun(data: string)
137 | ---@field on_exit? fun(code: integer, signal: integer)
138 | ---@field buffering? automa.kit.System.Buffering
139 | ---@param command string[]
140 | ---@param params automa.kit.System.SpawnParams
141 | ---@return fun(signal?: integer)
142 | function System.spawn(command, params)
143 | command = vim
144 | .iter(command)
145 | :filter(function(c)
146 | return c ~= nil
147 | end)
148 | :totable()
149 |
150 | local cmd = command[1]
151 | local args = {}
152 | for i = 2, #command do
153 | table.insert(args, command[i])
154 | end
155 |
156 | local env = vim.fn.environ()
157 | env.NVIM = vim.v.servername
158 | env.NVIM_LISTEN_ADDRESS = nil
159 |
160 | local env_pairs = {}
161 | for k, v in pairs(env) do
162 | table.insert(env_pairs, string.format('%s=%s', k, tostring(v)))
163 | end
164 |
165 | local buffering = params.buffering or System.RawBuffering.new()
166 | local stdout_buffer = buffering:create(function(text)
167 | if params.on_stdout then
168 | params.on_stdout(text)
169 | end
170 | end)
171 | local stderr_buffer = buffering:create(function(text)
172 | if params.on_stderr then
173 | params.on_stderr(text)
174 | end
175 | end)
176 |
177 | local close --[[@type fun()]]
178 | local stdin = params.input and assert(vim.uv.new_pipe())
179 | local stdout = assert(vim.uv.new_pipe())
180 | local stderr = assert(vim.uv.new_pipe())
181 | local process = vim.uv.spawn(vim.fn.exepath(cmd), {
182 | cwd = vim.fs.normalize(params.cwd),
183 | env = env_pairs,
184 | gid = vim.uv.getgid(),
185 | uid = vim.uv.getuid(),
186 | hide = true,
187 | args = args,
188 | stdio = { stdin, stdout, stderr },
189 | detached = false,
190 | verbatim = false,
191 | } --[[@as any]], function(code, signal)
192 | stdout_buffer.close()
193 | stderr_buffer.close()
194 | close()
195 | if params.on_exit then
196 | params.on_exit(code, signal)
197 | end
198 | end)
199 | stdout:read_start(function(err, data)
200 | if err then
201 | error(err)
202 | end
203 | if data then
204 | stdout_buffer.write(data)
205 | end
206 | end)
207 | stderr:read_start(function(err, data)
208 | if err then
209 | error(err)
210 | end
211 | if data then
212 | stderr_buffer.write(data)
213 | end
214 | end)
215 |
216 | if params.input and stdin then
217 | for _, input in ipairs(kit.to_array(params.input)) do
218 | stdin:write(input)
219 | end
220 | stdin:close()
221 | end
222 |
223 | close = function()
224 | if not stdout:is_closing() then
225 | stdout:close()
226 | end
227 | if not stderr:is_closing() then
228 | stderr:close()
229 | end
230 | if not process:is_closing() then
231 | process:close()
232 | end
233 | end
234 |
235 | return function(signal)
236 | if signal and process:is_active() and not process:is_closing() then
237 | process:kill(signal)
238 | end
239 | close()
240 | end
241 | end
242 |
243 | return System
244 |
--------------------------------------------------------------------------------
/lua/automa/kit/Vim/FloatingWindow.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 |
3 | ---Analyze border size.
4 | ---@param border string | string[]
5 | ---@return { top: integer, right: integer, bottom: integer, left: integer }
6 | local function analyze_border_size(border)
7 | if not border then
8 | return { top = 0, right = 0, bottom = 0, left = 0 }
9 | end
10 | if type(border) == 'string' then
11 | if border == 'none' then
12 | return { top = 0, right = 0, bottom = 0, left = 0 }
13 | elseif border == 'single' then
14 | return { top = 1, right = 1, bottom = 1, left = 1 }
15 | elseif border == 'double' then
16 | return { top = 2, right = 2, bottom = 2, left = 2 }
17 | elseif border == 'rounded' then
18 | return { top = 1, right = 1, bottom = 1, left = 1 }
19 | elseif border == 'solid' then
20 | return { top = 1, right = 1, bottom = 1, left = 1 }
21 | elseif border == 'shadow' then
22 | return { top = 0, right = 1, bottom = 1, left = 0 }
23 | end
24 | return { top = 0, right = 0, bottom = 0, left = 0 }
25 | end
26 | local chars = border --[=[@as string[]]=]
27 | while #chars < 8 do
28 | chars = kit.concat(chars, chars)
29 | end
30 | return {
31 | top = vim.fn.strdisplaywidth(chars[2]),
32 | right = vim.fn.strdisplaywidth(chars[4]),
33 | bottom = vim.fn.strdisplaywidth(chars[6]),
34 | left = vim.fn.strdisplaywidth(chars[8]),
35 | }
36 | end
37 |
38 | ---Returns true if the window is visible
39 | ---@param win? integer
40 | ---@return boolean
41 | local function is_visible(win)
42 | if not win then
43 | return false
44 | end
45 | if not vim.api.nvim_win_is_valid(win) then
46 | return false
47 | end
48 | return true
49 | end
50 |
51 | ---Show the window
52 | ---@param win? integer
53 | ---@param buf integer
54 | ---@param config automa.kit.Vim.FloatingWindow.Config
55 | ---@return integer
56 | local function show_or_move(win, buf, config)
57 | if is_visible(win) then
58 | vim.api.nvim_win_set_config(win --[=[@as integer]=], {
59 | relative = 'editor',
60 | width = config.width,
61 | height = config.height,
62 | row = config.row,
63 | col = config.col,
64 | anchor = config.anchor,
65 | style = config.style,
66 | border = config.border,
67 | zindex = config.zindex,
68 | })
69 | return win --[=[@as integer]=]
70 | else
71 | return vim.api.nvim_open_win(buf, false, {
72 | noautocmd = true,
73 | relative = 'editor',
74 | width = config.width,
75 | height = config.height,
76 | row = config.row,
77 | col = config.col,
78 | anchor = config.anchor,
79 | style = config.style,
80 | border = config.border,
81 | zindex = config.zindex,
82 | })
83 | end
84 | end
85 |
86 | ---Hide the window
87 | ---@param win integer
88 | local function hide(win)
89 | if is_visible(win) then
90 | vim.api.nvim_win_hide(win)
91 | end
92 | end
93 |
94 | ---@class automa.kit.Vim.FloatingWindow.Config
95 | ---@field public row integer 0-indexed utf-8
96 | ---@field public col integer 0-indexed utf-8
97 | ---@field public width integer
98 | ---@field public height integer
99 | ---@field public border? string | string[]
100 | ---@field public anchor? "NW" | "NE" | "SW" | "SE"
101 | ---@field public style? string
102 | ---@field public zindex? integer
103 |
104 | ---@class automa.kit.Vim.FloatingWindow.Analyzed
105 | ---@field public content_width integer buffer content width
106 | ---@field public content_height integer buffer content height
107 | ---@field public inner_width integer window inner width
108 | ---@field public inner_height integer window inner height
109 | ---@field public outer_width integer window outer width that includes border and scrollbar width
110 | ---@field public outer_height integer window outer height that includes border width
111 | ---@field public border_size { top: integer, right: integer, bottom: integer, left: integer }
112 | ---@field public scrollbar boolean
113 |
114 | ---@class automa.kit.Vim.FloatingWindow
115 | ---@field private _augroup string
116 | ---@field private _buf_option table
117 | ---@field private _win_option table
118 | ---@field private _buf integer
119 | ---@field private _scrollbar_track_buf integer
120 | ---@field private _scrollbar_thumb_buf integer
121 | ---@field private _win? integer
122 | ---@field private _scrollbar_track_win? integer
123 | ---@field private _scrollbar_thumb_win? integer
124 | local FloatingWindow = {}
125 | FloatingWindow.__index = FloatingWindow
126 |
127 | ---Create window.
128 | ---@return automa.kit.Vim.FloatingWindow
129 | function FloatingWindow.new()
130 | return setmetatable({
131 | _augroup = vim.api.nvim_create_augroup(('automa.kit.Vim.FloatingWindow:%s'):format(kit.unique_id()), {
132 | clear = true,
133 | }),
134 | _win_option = {},
135 | _buf_option = {},
136 | _buf = vim.api.nvim_create_buf(false, true),
137 | _scrollbar_track_buf = vim.api.nvim_create_buf(false, true),
138 | _scrollbar_thumb_buf = vim.api.nvim_create_buf(false, true),
139 | }, FloatingWindow)
140 | end
141 |
142 | ---Set window option.
143 | ---@param key string
144 | ---@param value any
145 | ---@param kind? 'main' | 'scrollbar_track' | 'scrollbar_thumb'
146 | function FloatingWindow:set_win_option(key, value, kind)
147 | kind = kind or 'main'
148 | self._win_option[kind] = self._win_option[kind] or {}
149 | self._win_option[kind][key] = value
150 | self:_update_option()
151 | end
152 |
153 | ---Get window option.
154 | ---@param key string
155 | ---@param kind? 'main' | 'scrollbar_track' | 'scrollbar_thumb'
156 | ---@return any
157 | function FloatingWindow:get_win_option(key, kind)
158 | kind = kind or 'main'
159 | local win = ({
160 | main = self._win,
161 | scrollbar_track = self._scrollbar_track_win,
162 | scrollbar_thumb = self._scrollbar_thumb_win,
163 | })[kind] --[=[@as integer]=]
164 | if not is_visible(win) then
165 | return self._win_option[kind] and self._win_option[kind][key]
166 | end
167 | return vim.api.nvim_get_option_value(key, { win = win }) or vim.api.nvim_get_option_value(key, { scope = 'global' })
168 | end
169 |
170 | ---Set buffer option.
171 | ---@param key string
172 | ---@param value any
173 | ---@param kind? 'main' | 'scrollbar_track' | 'scrollbar_thumb'
174 | function FloatingWindow:set_buf_option(key, value, kind)
175 | kind = kind or 'main'
176 | self._buf_option[kind] = self._buf_option[kind] or {}
177 | self._buf_option[kind][key] = value
178 | self:_update_option()
179 | end
180 |
181 | ---Get window option.
182 | ---@param key string
183 | ---@param kind? 'main' | 'scrollbar_track' | 'scrollbar_thumb'
184 | ---@return any
185 | function FloatingWindow:get_buf_option(key, kind)
186 | kind = kind or 'main'
187 | local buf = ({
188 | main = self._buf,
189 | scrollbar_track = self._scrollbar_track_buf,
190 | scrollbar_thumb = self._scrollbar_thumb_buf,
191 | })[kind] --[=[@as integer]=]
192 | if not buf then
193 | return self._buf_option[kind] and self._buf_option[kind][key]
194 | end
195 | return vim.api.nvim_get_option_value(key, { buf = buf }) or vim.api.nvim_get_option_value(key, { scope = 'global' })
196 | end
197 |
198 | ---Returns the related bufnr.
199 | function FloatingWindow:get_buf()
200 | return self._buf
201 | end
202 |
203 | ---Returns scrollbar track bufnr.
204 | ---@return integer
205 | function FloatingWindow:get_scrollbar_track_buf()
206 | return self._scrollbar_track_buf
207 | end
208 |
209 | ---Returns scrollbar thumb bufnr.
210 | ---@return integer
211 | function FloatingWindow:get_scrollbar_thumb_buf()
212 | return self._scrollbar_thumb_buf
213 | end
214 |
215 | ---Returns the current win.
216 | function FloatingWindow:get_win()
217 | return self._win
218 | end
219 |
220 | ---Show the window
221 | ---@param config automa.kit.Vim.FloatingWindow.Config
222 | function FloatingWindow:show(config)
223 | local zindex = config.zindex or 1000
224 |
225 | self._win = show_or_move(self._win, self._buf, {
226 | row = config.row,
227 | col = config.col,
228 | width = config.width,
229 | height = config.height,
230 | anchor = config.anchor,
231 | style = config.style,
232 | border = config.border,
233 | zindex = zindex,
234 | })
235 |
236 | vim.api.nvim_clear_autocmds({ group = self._augroup })
237 | vim.api.nvim_create_autocmd('WinScrolled', {
238 | group = self._augroup,
239 | callback = function(event)
240 | if tostring(event.match) == tostring(self._win) then
241 | self:_update_scrollbar()
242 | end
243 | end,
244 | })
245 |
246 | self:_update_scrollbar()
247 | self:_update_option()
248 | end
249 |
250 | ---Hide the window
251 | function FloatingWindow:hide()
252 | vim.api.nvim_clear_autocmds({ group = self._augroup })
253 | hide(self._win)
254 | hide(self._scrollbar_track_win)
255 | hide(self._scrollbar_thumb_win)
256 | end
257 |
258 | ---Scroll the window.
259 | ---@param delta integer
260 | function FloatingWindow:scroll(delta)
261 | if not is_visible(self._win) then
262 | return
263 | end
264 | vim.api.nvim_win_call(self._win, function()
265 | local topline = vim.fn.getwininfo(self._win)[1].height
266 | topline = topline + delta
267 | topline = math.max(topline, 1)
268 | topline = math.min(topline, vim.api.nvim_buf_line_count(self._buf) - vim.api.nvim_win_get_height(self._win) + 1)
269 | vim.api.nvim_command(('normal! %szt'):format(topline))
270 | end)
271 | end
272 |
273 | ---Returns true if the window is visible
274 | function FloatingWindow:is_visible()
275 | return is_visible(self._win)
276 | end
277 |
278 | ---Analyze window layout.
279 | ---@param config { max_width: integer, max_height: integer, border: string | string[] }
280 | ---@return automa.kit.Vim.FloatingWindow.Analyzed
281 | function FloatingWindow:analyze(config)
282 | local border_size = analyze_border_size(config.border)
283 | local text_widths = {} --[=[@as integer[]]=]
284 |
285 | -- calculate content width.
286 | local content_width --[=[@as integer]=]
287 | do
288 | local max_text_width = 0
289 | for i, text in ipairs(vim.api.nvim_buf_get_lines(self._buf, 0, -1, false)) do
290 | text_widths[i] = vim.fn.strdisplaywidth(text)
291 | max_text_width = math.max(max_text_width, text_widths[i])
292 | end
293 | content_width = max_text_width
294 | end
295 |
296 | -- calculate content height.
297 | local content_height --[=[@as integer]=]
298 | do
299 | local possible_inner_width = config.max_width - border_size.left - border_size.right - 1 -- `-1` means possible scollbar width.
300 | local height = 0
301 | for _, text_width in ipairs(text_widths) do
302 | if self:get_win_option('wrap') then
303 | height = height + math.max(1, math.ceil(text_width / possible_inner_width))
304 | else
305 | height = height + 1
306 | end
307 | end
308 | content_height = height
309 | end
310 |
311 | local inner_height = math.min(content_height, config.max_height - border_size.top - border_size.bottom)
312 | local scrollbar = content_height > inner_height
313 | local inner_width = math.min(content_width, config.max_width - border_size.left - border_size.right - (scrollbar and 1 or 0))
314 | return {
315 | content_width = content_width,
316 | content_height = content_height,
317 | inner_width = inner_width,
318 | inner_height = inner_height,
319 | outer_width = inner_width + border_size.left + border_size.right + (scrollbar and 1 or 0),
320 | outer_height = inner_height + border_size.top + border_size.bottom,
321 | border_size = border_size,
322 | scrollbar = scrollbar,
323 | }
324 | end
325 |
326 | ---Update scrollbar.
327 | function FloatingWindow:_update_scrollbar()
328 | if is_visible(self._win) then
329 | local win_width = vim.api.nvim_win_get_width(self._win)
330 | local win_height = vim.api.nvim_win_get_height(self._win)
331 | local win_pos = vim.api.nvim_win_get_position(self._win)
332 | local win_config = vim.api.nvim_win_get_config(self._win)
333 | local border_size = analyze_border_size(win_config.border)
334 |
335 | local analyzed = self:analyze({
336 | max_width = win_width + border_size.left + border_size.right + 1,
337 | max_height = win_height + border_size.top + border_size.bottom,
338 | border = win_config.border,
339 | })
340 |
341 | if analyzed.scrollbar then
342 | do
343 | self._scrollbar_track_win = show_or_move(self._scrollbar_track_win, self._scrollbar_track_buf, {
344 | row = win_pos[1] + analyzed.border_size.top,
345 | col = win_pos[2] + analyzed.outer_width - 1 - analyzed.border_size.right,
346 | width = 1,
347 | height = analyzed.inner_height,
348 | style = 'minimal',
349 | zindex = win_config.zindex + 1,
350 | })
351 | end
352 | do
353 | local topline = vim.fn.getwininfo(self._win)[1].topline
354 | local thumb_height = math.ceil(analyzed.inner_height * (analyzed.inner_height / analyzed.content_height))
355 | local thumb_row = math.floor(analyzed.inner_height * (topline / analyzed.content_height))
356 | self._scrollbar_thumb_win = show_or_move(self._scrollbar_thumb_win, self._scrollbar_thumb_buf, {
357 | row = win_pos[1] + analyzed.border_size.top + thumb_row,
358 | col = win_pos[2] + analyzed.outer_width - 1 - analyzed.border_size.right,
359 | width = 1,
360 | height = thumb_height,
361 | style = 'minimal',
362 | zindex = win_config.zindex + 2,
363 | })
364 | end
365 | return
366 | end
367 | end
368 | hide(self._scrollbar_track_win)
369 | hide(self._scrollbar_thumb_win)
370 | end
371 |
372 | ---Update options.
373 | function FloatingWindow:_update_option()
374 | -- update buf.
375 | for kind, buf in pairs({
376 | main = self._buf,
377 | scrollbar_track = self._scrollbar_track_buf,
378 | scrollbar_thumb = self._scrollbar_thumb_buf,
379 | }) do
380 | for k, v in pairs(self._buf_option[kind] or {}) do
381 | if vim.api.nvim_get_option_value(k, { buf = buf }) ~= v then
382 | vim.api.nvim_set_option_value(k, v, { buf = buf })
383 | end
384 | end
385 | end
386 |
387 | -- update win.
388 | for kind, win in pairs({
389 | main = self._win,
390 | scrollbar_track = self._scrollbar_track_win,
391 | scrollbar_thumb = self._scrollbar_thumb_win,
392 | }) do
393 | if is_visible(win) then
394 | for k, v in pairs(self._win_option[kind] or {}) do
395 | if vim.api.nvim_get_option_value(k, { win = win }) ~= v then
396 | vim.api.nvim_set_option_value(k, v, { win = win })
397 | end
398 | end
399 | end
400 | end
401 | end
402 |
403 | return FloatingWindow
404 |
--------------------------------------------------------------------------------
/lua/automa/kit/Vim/Keymap.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 | local Async = require('automa.kit.Async')
3 |
4 | local buf = vim.api.nvim_create_buf(false, true)
5 |
6 | ---@alias automa.kit.Vim.Keymap.Keys { keys: string, remap?: boolean }
7 | ---@alias automa.kit.Vim.Keymap.KeysSpecifier string|automa.kit.Vim.Keymap.Keys
8 |
9 | ---@param keys automa.kit.Vim.Keymap.KeysSpecifier
10 | ---@return automa.kit.Vim.Keymap.Keys
11 | local function to_keys(keys)
12 | if type(keys) == 'table' then
13 | return keys
14 | end
15 | return { keys = keys, remap = false }
16 | end
17 |
18 | local Keymap = {}
19 |
20 | Keymap._callbacks = {}
21 |
22 | ---Replace termcodes.
23 | ---@param keys string
24 | ---@return string
25 | function Keymap.termcodes(keys)
26 | return vim.api.nvim_replace_termcodes(keys, true, true, true)
27 | end
28 |
29 | ---Normalize keycode.
30 | function Keymap.normalize(s)
31 | local desc = 'automa.kit.Vim.Keymap.normalize'
32 | vim.api.nvim_buf_set_keymap(buf, 't', s, '.', { desc = desc })
33 | for _, map in ipairs(vim.api.nvim_buf_get_keymap(buf, 't')) do
34 | if map.desc == desc then
35 | vim.api.nvim_buf_del_keymap(buf, 't', s)
36 | return map.lhs --[[@as string]]
37 | end
38 | end
39 | vim.api.nvim_buf_del_keymap(buf, 't', s)
40 | return s
41 | end
42 |
43 | ---Set callback for consuming next typeahead.
44 | ---@param callback fun()
45 | ---@return automa.kit.Async.AsyncTask
46 | function Keymap.next(callback)
47 | return Keymap.send(''):next(callback)
48 | end
49 |
50 | ---Send keys.
51 | ---@param keys automa.kit.Vim.Keymap.KeysSpecifier|automa.kit.Vim.Keymap.KeysSpecifier[]
52 | ---@param no_insert? boolean
53 | ---@return automa.kit.Async.AsyncTask
54 | function Keymap.send(keys, no_insert)
55 | local unique_id = kit.unique_id()
56 | return Async.new(function(resolve, _)
57 | Keymap._callbacks[unique_id] = resolve
58 |
59 | local callback = Keymap.termcodes(('lua require("automa.kit.Vim.Keymap")._resolve(%s)'):format(unique_id))
60 | if no_insert then
61 | for _, keys_ in ipairs(kit.to_array(keys)) do
62 | keys_ = to_keys(keys_)
63 | vim.api.nvim_feedkeys(keys_.keys, keys_.remap and 'm' or 'n', true)
64 | end
65 | vim.api.nvim_feedkeys(callback, 'n', true)
66 | else
67 | vim.api.nvim_feedkeys(callback, 'in', true)
68 | for _, keys_ in ipairs(kit.reverse(kit.to_array(keys))) do
69 | keys_ = to_keys(keys_)
70 | vim.api.nvim_feedkeys(keys_.keys, 'i' .. (keys_.remap and 'm' or 'n'), true)
71 | end
72 | end
73 | end):catch(function()
74 | Keymap._callbacks[unique_id] = nil
75 | end)
76 | end
77 |
78 | ---Return sendabke keys with callback function.
79 | ---@param callback fun(...: any): any
80 | ---@return string
81 | function Keymap.to_sendable(callback)
82 | local unique_id = kit.unique_id()
83 | Keymap._callbacks[unique_id] = function()
84 | Async.run(callback)
85 | end
86 | return Keymap.termcodes(('lua require("automa.kit.Vim.Keymap")._resolve(%s)'):format(unique_id))
87 | end
88 |
89 | ---Test spec helper.
90 | ---@param spec fun(): any
91 | function Keymap.spec(spec)
92 | local task = Async.resolve():next(function()
93 | return Async.run(spec)
94 | end)
95 | vim.api.nvim_feedkeys('', 'x', true)
96 | task:sync(5000)
97 | collectgarbage('collect')
98 | vim.wait(200)
99 | end
100 |
101 | ---Resolve running keys.
102 | ---@param unique_id integer
103 | function Keymap._resolve(unique_id)
104 | Keymap._callbacks[unique_id]()
105 | Keymap._callbacks[unique_id] = nil
106 | end
107 |
108 | return Keymap
109 |
--------------------------------------------------------------------------------
/lua/automa/kit/Vim/RegExp.lua:
--------------------------------------------------------------------------------
1 | local RegExp = {}
2 |
3 | ---@type table
4 | RegExp._cache = {}
5 |
6 | ---Create a RegExp object.
7 | ---@param pattern string
8 | ---@return { match_str: fun(self, text: string) }
9 | function RegExp.get(pattern)
10 | if not RegExp._cache[pattern] then
11 | RegExp._cache[pattern] = vim.regex(pattern)
12 | end
13 | return RegExp._cache[pattern]
14 | end
15 |
16 | ---Grep and substitute text.
17 | ---@param text string
18 | ---@param pattern string
19 | ---@param replacement string
20 | ---@return string
21 | function RegExp.gsub(text, pattern, replacement)
22 | return vim.fn.substitute(text, pattern, replacement, 'g')
23 | end
24 |
25 | ---Match pattern in text for specified position.
26 | ---@param text string
27 | ---@param pattern string
28 | ---@param pos integer 1-origin index
29 | ---@return string?, integer?, integer? 1-origin-index
30 | function RegExp.extract_at(text, pattern, pos)
31 | local before_text = text:sub(1, pos - 1)
32 | local after_text = text:sub(pos)
33 | local b_s, _ = RegExp.get(pattern .. '$'):match_str(before_text)
34 | local _, a_e = RegExp.get('^' .. pattern):match_str(after_text)
35 | if b_s or a_e then
36 | b_s = b_s or #before_text
37 | a_e = #before_text + (a_e or 0)
38 | return text:sub(b_s + 1, a_e), b_s + 1, a_e + 1
39 | end
40 | end
41 |
42 | return RegExp
43 |
--------------------------------------------------------------------------------
/lua/automa/kit/Vim/Syntax.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 |
3 | local Syntax = {}
4 |
5 | ---Return the specified position is in the specified syntax.
6 | ---@param cursor { [1]: integer, [2]: integer }
7 | ---@param groups string[]
8 | function Syntax.within(cursor, groups)
9 | for _, group in ipairs(Syntax.get_syntax_groups(cursor)) do
10 | if vim.tbl_contains(groups, group) then
11 | return true
12 | end
13 | end
14 | return false
15 | end
16 |
17 | ---Get all syntax groups for specified position.
18 | ---NOTE: This function accepts 0-origin cursor position.
19 | ---@param cursor { [1]: integer, [2]: integer }
20 | ---@return string[]
21 | function Syntax.get_syntax_groups(cursor)
22 | return kit.concat(Syntax.get_vim_syntax_groups(cursor), Syntax.get_treesitter_syntax_groups(cursor))
23 | end
24 |
25 | ---Get vim's syntax groups for specified position.
26 | ---NOTE: This function accepts 0-origin cursor position.
27 | ---@param cursor { [1]: integer, [2]: integer }
28 | ---@return string[]
29 | function Syntax.get_vim_syntax_groups(cursor)
30 | local unique = {}
31 | local groups = {}
32 | for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do
33 | local name = vim.fn.synIDattr(vim.fn.synIDtrans(syntax_id), 'name')
34 | if not unique[name] then
35 | unique[name] = true
36 | table.insert(groups, name)
37 | end
38 | end
39 | for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do
40 | local name = vim.fn.synIDattr(syntax_id, 'name')
41 | if not unique[name] then
42 | unique[name] = true
43 | table.insert(groups, name)
44 | end
45 | end
46 | return groups
47 | end
48 |
49 | ---Get tree-sitter's syntax groups for specified position.
50 | ---NOTE: This function accepts 0-origin cursor position.
51 | ---@param cursor { [1]: integer, [2]: integer }
52 | ---@return string[]
53 | function Syntax.get_treesitter_syntax_groups(cursor)
54 | local groups = {}
55 | for _, capture in ipairs(vim.treesitter.get_captures_at_pos(0, cursor[1], cursor[2])) do
56 | table.insert(groups, ('@%s'):format(capture.capture))
57 | end
58 | return groups
59 | end
60 |
61 | return Syntax
62 |
--------------------------------------------------------------------------------
/lua/automa/kit/init.lua:
--------------------------------------------------------------------------------
1 | -- luacheck: ignore 512
2 |
3 | local kit = {}
4 |
5 | ---Create gabage collection detector.
6 | ---@param callback fun(...: any): any
7 | ---@return userdata
8 | function kit.gc(callback)
9 | local gc = newproxy(true)
10 | if vim.is_thread() or os.getenv('NODE_ENV') == 'test' then
11 | getmetatable(gc).__gc = callback
12 | else
13 | getmetatable(gc).__gc = vim.schedule_wrap(callback)
14 | end
15 | return gc
16 | end
17 |
18 | ---Safe version of vim.schedule.
19 | ---@param fn fun(...: any): any
20 | function kit.safe_schedule(fn)
21 | if vim.is_thread() then
22 | local timer = assert(vim.uv.new_timer())
23 | timer:start(0, 0, function()
24 | timer:stop()
25 | timer:close()
26 | fn()
27 | end)
28 | else
29 | vim.schedule(fn)
30 | end
31 | end
32 |
33 | ---Safe version of vim.schedule_wrap.
34 | ---@param fn fun(...: any): any
35 | function kit.safe_schedule_wrap(fn)
36 | if vim.is_thread() then
37 | return function(...)
38 | local args = { ... }
39 | local timer = assert(vim.uv.new_timer())
40 | timer:start(0, 0, function()
41 | timer:stop()
42 | timer:close()
43 | fn(unpack(args))
44 | end)
45 | end
46 | else
47 | return vim.schedule_wrap(fn)
48 | end
49 | end
50 |
51 | ---Fast version of vim.schedule.
52 | ---@param fn fun(): any
53 | function kit.fast_schedule(fn)
54 | if vim.in_fast_event() then
55 | kit.safe_schedule(fn)
56 | else
57 | fn()
58 | end
59 | end
60 |
61 | ---Safe version of vim.schedule_wrap.
62 | ---@generic A
63 | ---@param fn fun(...: A)
64 | ---@return fun(...: A)
65 | function kit.fast_schedule_wrap(fn)
66 | return function(...)
67 | local args = { ... }
68 | kit.fast_schedule(function()
69 | fn(unpack(args))
70 | end)
71 | end
72 | end
73 |
74 | ---Find up directory.
75 | ---@param path string
76 | ---@param markers string[]
77 | function kit.findup(path, markers)
78 | path = vim.fs.normalize(path)
79 | if vim.fn.filereadable(path) == 1 then
80 | path = vim.fs.dirname(path)
81 | end
82 | while path ~= '/' do
83 | for _, marker in ipairs(markers) do
84 | local target = vim.fs.joinpath(path, (marker:gsub('/', '')))
85 | if marker:match('/$') and vim.fn.isdirectory(target) == 1 then
86 | return path
87 | elseif vim.fn.filereadable(target) == 1 then
88 | return path
89 | end
90 | end
91 | path = vim.fs.dirname(path)
92 | end
93 | end
94 |
95 | ---Create unique id.
96 | ---@return integer
97 | kit.unique_id = setmetatable({
98 | unique_id = 0,
99 | }, {
100 | __call = function(self)
101 | self.unique_id = self.unique_id + 1
102 | return self.unique_id
103 | end,
104 | })
105 |
106 | ---Map array.
107 | ---@deprecated
108 | ---@param array table
109 | ---@param fn fun(item: unknown, index: integer): unknown
110 | ---@return unknown[]
111 | function kit.map(array, fn)
112 | local new_array = {}
113 | for i, item in ipairs(array) do
114 | table.insert(new_array, fn(item, i))
115 | end
116 | return new_array
117 | end
118 |
119 | ---@generic T
120 | ---@deprecated
121 | ---@param value T?
122 | ---@param default T
123 | function kit.default(value, default)
124 | if value == nil then
125 | return default
126 | end
127 | return value
128 | end
129 |
130 | ---Get object path with default value.
131 | ---@generic T
132 | ---@param value table
133 | ---@param path integer|string|(string|integer)[]
134 | ---@param default? T
135 | ---@return T
136 | function kit.get(value, path, default)
137 | local result = value
138 | for _, key in ipairs(kit.to_array(path)) do
139 | if type(result) == 'table' then
140 | result = result[key]
141 | else
142 | return default
143 | end
144 | end
145 | if result == nil then
146 | return default
147 | end
148 | return result
149 | end
150 |
151 | ---Set object path with new value.
152 | ---@param value table
153 | ---@param path integer|string|(string|integer)[]
154 | ---@param new_value any
155 | function kit.set(value, path, new_value)
156 | local current = value
157 | for i = 1, #path - 1 do
158 | local key = path[i]
159 | if type(current[key]) ~= 'table' then
160 | error('The specified path is not a table.')
161 | end
162 | current = current[key]
163 | end
164 | current[path[#path]] = new_value
165 | end
166 |
167 | ---Create debounced callback.
168 | ---@generic T: fun(...: any): nil
169 | ---@param callback T
170 | ---@param ms integer
171 | ---@return T
172 | function kit.debounce(callback, ms)
173 | local run_id = 0
174 | local timer = assert(vim.uv.new_timer())
175 | return function(...)
176 | local arguments = { ... }
177 |
178 | run_id = run_id + 1
179 | timer:stop()
180 | timer:start(ms, 0, function()
181 | if run_id ~= run_id then
182 | return
183 | end
184 | timer:stop()
185 | callback(unpack(arguments))
186 | end)
187 | end
188 | end
189 |
190 | ---Create throttled callback.
191 | ---@generic T: fun(...: any): nil
192 | ---@param callback T
193 | ---@param throttle_ms integer
194 | function kit.throttle(callback, throttle_ms)
195 | local timer = assert(vim.uv.new_timer())
196 | local arguments = nil
197 | local last_time = vim.uv.now() - throttle_ms
198 | return setmetatable({
199 | throttle_ms = throttle_ms,
200 | }, {
201 | __call = function(self, ...)
202 | arguments = { ... }
203 |
204 | local timeout_ms = self.throttle_ms - (vim.uv.now() - last_time)
205 | if timeout_ms <= 0 then
206 | timer:stop()
207 | callback(unpack(arguments))
208 | last_time = vim.uv.now()
209 | else
210 | timer:stop()
211 | timer:start(timeout_ms, 0, function()
212 | timer:stop()
213 | callback(unpack(arguments))
214 | last_time = vim.uv.now()
215 | end)
216 | end
217 | end,
218 | })
219 | end
220 |
221 | do
222 | ---@generic T
223 | ---@param target T
224 | ---@param seen table
225 | ---@return T
226 | local function do_clone(target, seen)
227 | if type(target) ~= 'table' then
228 | return target
229 | end
230 | if seen[target] then
231 | return seen[target]
232 | end
233 | if kit.is_array(target) then
234 | local new_tbl = {}
235 | seen[target] = new_tbl
236 | for k, v in ipairs(target) do
237 | new_tbl[k] = do_clone(v, seen)
238 | end
239 | return new_tbl
240 | else
241 | local new_tbl = {}
242 | seen[target] = new_tbl
243 | for k, v in pairs(target) do
244 | new_tbl[k] = do_clone(v, seen)
245 | end
246 | return new_tbl
247 | end
248 | end
249 |
250 | ---Clone object.
251 | ---@generic T
252 | ---@param target T
253 | ---@return T
254 | function kit.clone(target)
255 | return do_clone(target, {})
256 | end
257 | end
258 |
259 | ---Merge two tables.
260 | ---@generic T: any[]
261 | ---NOTE: This doesn't merge array-like table.
262 | ---@param tbl1 T
263 | ---@param tbl2 T
264 | ---@return T
265 | function kit.merge(tbl1, tbl2)
266 | local is_dict1 = kit.is_dict(tbl1)
267 | local is_dict2 = kit.is_dict(tbl2)
268 | if is_dict1 and is_dict2 then
269 | local new_tbl = {}
270 | for k, v in pairs(tbl2) do
271 | if tbl1[k] ~= vim.NIL then
272 | new_tbl[k] = kit.merge(tbl1[k], v)
273 | end
274 | end
275 | for k, v in pairs(tbl1) do
276 | if tbl2[k] == nil then
277 | if v ~= vim.NIL then
278 | new_tbl[k] = kit.merge(v, {})
279 | else
280 | new_tbl[k] = nil
281 | end
282 | end
283 | end
284 | return new_tbl
285 | end
286 |
287 | -- premitive like values.
288 | if tbl1 == vim.NIL then
289 | return nil
290 | elseif tbl1 == nil then
291 | return kit.merge(tbl2, {}) -- clone & prevent vim.NIL
292 | end
293 | return tbl1
294 | end
295 |
296 | ---Concatenate two tables.
297 | ---NOTE: This doesn't concatenate dict-like table.
298 | ---@param tbl1 table
299 | ---@param ... table
300 | ---@return table
301 | function kit.concat(tbl1, ...)
302 | local new_tbl = {}
303 | for _, item in ipairs(tbl1) do
304 | table.insert(new_tbl, item)
305 | end
306 | for _, tbl2 in ipairs({ ... }) do
307 | for _, item in ipairs(tbl2) do
308 | table.insert(new_tbl, item)
309 | end
310 | end
311 | return new_tbl
312 | end
313 |
314 | ---Slice the array.
315 | ---@generic T: any[]
316 | ---@param array T
317 | ---@param s integer
318 | ---@param e integer
319 | ---@return T
320 | function kit.slice(array, s, e)
321 | if not kit.is_array(array) then
322 | error('[kit] specified value is not an array.')
323 | end
324 | local new_array = {}
325 | for i = s, e do
326 | table.insert(new_array, array[i])
327 | end
328 | return new_array
329 | end
330 |
331 | ---The value to array.
332 | ---@param value any
333 | ---@return table
334 | function kit.to_array(value)
335 | if type(value) == 'table' then
336 | if kit.is_array(value) then
337 | return value
338 | end
339 | end
340 | return { value }
341 | end
342 |
343 | ---Check the value is array.
344 | ---@param value any
345 | ---@return boolean
346 | function kit.is_array(value)
347 | if type(value) ~= 'table' then
348 | return false
349 | end
350 |
351 | for k, _ in pairs(value) do
352 | if type(k) ~= 'number' then
353 | return false
354 | end
355 | end
356 | return true
357 | end
358 |
359 | ---Check the value is dict.
360 | ---@param value any
361 | ---@return boolean
362 | function kit.is_dict(value)
363 | return type(value) == 'table' and (not kit.is_array(value) or kit.is_empty(value))
364 | end
365 |
366 | ---Check the value is empty.
367 | ---@param value any
368 | ---@return boolean
369 | function kit.is_empty(value)
370 | if type(value) ~= 'table' then
371 | return false
372 | end
373 | for _ in pairs(value) do
374 | return false
375 | end
376 | if #value == 0 then
377 | return true
378 | end
379 | return true
380 | end
381 |
382 | ---Reverse the array.
383 | ---@param array table
384 | ---@return table
385 | function kit.reverse(array)
386 | if not kit.is_array(array) then
387 | error('[kit] specified value is not an array.')
388 | end
389 |
390 | local new_array = {}
391 | for i = #array, 1, -1 do
392 | table.insert(new_array, array[i])
393 | end
394 | return new_array
395 | end
396 |
397 | ---String dedent.
398 | function kit.dedent(s)
399 | local lines = vim.split(s, '\n')
400 | if lines[1]:match('^%s*$') then
401 | table.remove(lines, 1)
402 | end
403 | if lines[#lines]:match('^%s*$') then
404 | table.remove(lines, #lines)
405 | end
406 | local base_indent = lines[1]:match('^%s*')
407 | for i, line in ipairs(lines) do
408 | lines[i] = line:gsub('^' .. base_indent, '')
409 | end
410 | return table.concat(lines, '\n')
411 | end
412 |
413 | return kit
414 |
--------------------------------------------------------------------------------
/lua/automa/query.lua:
--------------------------------------------------------------------------------
1 | local kit = require('automa.kit')
2 |
3 | local Query = {}
4 |
5 | do
6 | local P, Cs, Cg, Ct = vim.lpeg.P, vim.lpeg.Cs, vim.lpeg.Cg, vim.lpeg.Ct
7 |
8 | local function text(chars, esc)
9 | chars = kit.to_array(chars)
10 | esc = esc or '\\'
11 |
12 | local escaped = vim.iter(chars):fold(P(esc .. esc), function(acc, char)
13 | return acc + P(esc .. char)
14 | end) / function(v)
15 | return v:sub(2)
16 | end
17 | local onechar = vim.iter(chars):fold(P(1) - P(esc), function(acc, char)
18 | return acc - P(char)
19 | end)
20 |
21 | return Cs(escaped + onechar)
22 | end
23 |
24 | local mode = Cg(
25 | P('t') +
26 | P('!') +
27 | P('r?') +
28 | P('rm') +
29 | P('r') +
30 | P('cvr') +
31 | P('cv') +
32 | P('cr') +
33 | P('c') +
34 | P('Rvx') +
35 | P('Rvc') +
36 | P('Rv') +
37 | P('Rx') +
38 | P('Rc') +
39 | P('R') +
40 | P('ix') +
41 | P('ic') +
42 | P('i') +
43 | P('') +
44 | P('S') +
45 | P('s') +
46 | P('s') +
47 | P('') +
48 | P('Vs') +
49 | P('V') +
50 | P('vs') +
51 | P('v') +
52 | P('ntT') +
53 | P('nt') +
54 | P('niV') +
55 | P('niR') +
56 | P('niI') +
57 | P('no') +
58 | P('noV') +
59 | P('nov') +
60 | P('no') +
61 | P('n')
62 | ) / function(mode)
63 | if mode == 'C-v' then
64 | return ''
65 | end
66 | return mode
67 | end
68 |
69 | local char1 = P('<') * text('>') ^ 1 * P('>')
70 | local char2 = text({ ',', ')' }) ^ 1
71 | local char = Cs(char1 + char2)
72 |
73 | local chars = P('(') * Ct(char * (P(',') * char) ^ 0) * P(')')
74 |
75 | local edit = Cg(P('#'))
76 |
77 | local negate = Cg(P('!'))
78 |
79 | local many = Cg(P('+') + P('*'))
80 |
81 | local grammer = P({
82 | 'query',
83 | query = (Cg(negate ^ -1) * Cg(mode) * Cg(chars ^ -1) * Cg(edit ^ -1) * Cg(many ^ -1)) / function(_negate, _mode, _chars, _edit, _many)
84 | return {
85 | negate = _negate,
86 | mode = _mode,
87 | chars = _chars,
88 | edit = _edit,
89 | many = _many,
90 | }
91 | end
92 | })
93 |
94 | Query.grammer = grammer
95 | end
96 |
97 |
98 | ---@param q string
99 | ---@return automa.Matcher
100 | local function make_matcher(q)
101 | local parsed = Query.grammer:match(q)
102 | assert(parsed, 'Failed to parse query: ' .. q)
103 | return setmetatable({
104 | negate = parsed.negate,
105 | mode = parsed.mode,
106 | chars = parsed.chars,
107 | edit = parsed.edit,
108 | many = parsed.many,
109 | }, {
110 | ---@param events automa.Event[]
111 | ---@param index integer
112 | ---@return boolean, integer
113 | __call = function(self, events, index)
114 | ---@param idx integer
115 | ---@return boolean
116 | local function match(idx)
117 | local event = events[idx]
118 | if not event then
119 | return false
120 | end
121 | if event.separator then
122 | return false
123 | end
124 |
125 | if self.edit == '#' then
126 | if not event.edit then
127 | return false
128 | end
129 | end
130 |
131 | local result = (function()
132 | if self.mode ~= '' then
133 | if event.mode ~= self.mode then
134 | return false
135 | end
136 | end
137 | if #self.chars > 0 then
138 | if not vim.tbl_contains(vim.iter(self.chars):map(vim.keycode):totable(), event.char) then
139 | return false
140 | end
141 | end
142 | return true
143 | end)()
144 |
145 | -- negate `mode` and `chars` condition.
146 | if self.negate == '!' then
147 | result = not result
148 | end
149 |
150 | return result
151 | end
152 |
153 | -- single match.
154 | if self.many == '' then
155 | if match(index) then
156 | return true, index + 1
157 | end
158 | return false, index
159 | end
160 |
161 | -- many match.
162 | local match_count = 0
163 | while index + match_count <= #events do
164 | if not match(index + match_count) then
165 | break
166 | end
167 | match_count = match_count + 1
168 | end
169 |
170 | if self.many == '+' and match_count == 0 then
171 | return false, index
172 | end
173 |
174 | return true, index + match_count
175 | end
176 | })
177 | end
178 |
179 | ---@param query_source string[]
180 | ---@return fun(events: automa.Event[]): { s_idx: integer, e_idx: integer }?
181 | function Query.make_query(query_source)
182 | local matchers = kit.map(query_source, make_matcher)
183 |
184 | ---@param events automa.Event[]
185 | ---@param index integer
186 | local function match(events, index)
187 | local s_event = events[index]
188 | for _, matcher in ipairs(matchers) do
189 | local matched, next_index = matcher(events, index)
190 | if not matched then
191 | return false, index
192 | end
193 | index = next_index
194 | end
195 |
196 | -- check the range of key-sequence makes text edit.
197 | local e_event = events[index - 1]
198 | if s_event and e_event then
199 | local is_same_event = s_event == e_event
200 | if (is_same_event and not s_event.edit) or (not is_same_event and s_event.changedtick == e_event.changedtick) then
201 | return false, index
202 | end
203 | end
204 | return true, index
205 | end
206 |
207 | ---@type automa.Query
208 | return function(events)
209 | local candidates = {} --[=[@type { s_idx: integer, e_idx: integer }[]]=]
210 |
211 | local s_idx, e_idx = #events, -1
212 | while s_idx > 0 do
213 | local curr_matched, curr_e_idx = match(events, s_idx)
214 | if curr_matched then
215 | if e_idx <= curr_e_idx then
216 | e_idx = curr_e_idx
217 | table.insert(candidates, { s_idx = s_idx, e_idx = e_idx - 1 })
218 | else
219 | break
220 | end
221 | end
222 | s_idx = s_idx - 1
223 | end
224 |
225 | if #candidates == 0 then
226 | return
227 | end
228 |
229 | local target = candidates[1] ---@type automa.QueryResult
230 | for _, candidate in ipairs(candidates) do
231 | if target.e_idx < candidate.e_idx then
232 | target = candidate
233 | elseif target.e_idx == candidate.e_idx then
234 | if target.s_idx > candidate.s_idx then
235 | target = candidate
236 | end
237 | end
238 | end
239 |
240 | target.reginfo = events[target.s_idx].reginfo
241 | target.typed = ''
242 | for i = target.s_idx, target.e_idx do
243 | target.typed = target.typed .. events[i].char
244 | end
245 | return target
246 | end
247 | end
248 |
249 | return Query
250 |
--------------------------------------------------------------------------------
/lua/automa/query.spec.lua:
--------------------------------------------------------------------------------
1 | local Query = require('automa.query')
2 | local automa = require('automa')
3 | local Spec = require('automa.kit.Spec')
4 |
5 | automa.setup({})
6 |
7 | describe('automa.query', function()
8 | it('parse', function()
9 | local query = Query.make_query({ '!n(h,j,k,l)*' })
10 | Spec.setup('fo|o')
11 | vim.api.nvim_feedkeys(vim.keycode('diwibar'), 'tx', true)
12 | Spec.expect('ba|r')
13 | assert.are.same(query(automa.events()), { s_idx = 1, e_idx = 8, typed = vim.keycode('diwibar') })
14 | end)
15 | end)
16 |
--------------------------------------------------------------------------------
/plugin/automa.lua:
--------------------------------------------------------------------------------
1 | vim.api.nvim_create_user_command('AutomaToggleDebugger', function()
2 | require('automa').toggle_debug_panel()
3 | end, {})
4 |
5 |
--------------------------------------------------------------------------------