├── 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 | --------------------------------------------------------------------------------