├── .github └── workflows │ └── test.yml ├── .gitignore ├── .huskyrc.json ├── .luacheckrc ├── .luarc.typecheck.json ├── LICENSE ├── README.md ├── fixture ├── IO │ ├── read_file.txt │ └── scandir │ │ └── a │ │ ├── 0 │ │ └── 1 │ │ ├── 1 │ │ ├── 2 │ │ └── 3 └── LSP │ └── DocumentSelector │ └── dummy.js ├── lua ├── ___kit___ │ └── kit │ │ ├── App │ │ ├── Cache.lua │ │ ├── Cache.spec.lua │ │ ├── Character.lua │ │ ├── Character.spec.lua │ │ ├── Command.lua │ │ ├── Command.spec.lua │ │ ├── Config.lua │ │ ├── Config.spec.lua │ │ ├── Event.lua │ │ └── Event.spec.lua │ │ ├── Async │ │ ├── AsyncTask.lua │ │ ├── AsyncTask.spec.lua │ │ ├── ScheduledTimer.lua │ │ ├── Worker.lua │ │ ├── Worker.spec.lua │ │ ├── init.lua │ │ └── init.spec.lua │ │ ├── IO │ │ ├── init.lua │ │ └── init.spec.lua │ │ ├── LSP │ │ ├── Client.lua │ │ ├── DocumentSelector.lua │ │ ├── DocumentSelector.spec.lua │ │ ├── LanguageId.lua │ │ ├── LanguageId.spec.lua │ │ ├── Position.lua │ │ ├── Position.spec.lua │ │ ├── Range.lua │ │ ├── Range.spec.lua │ │ └── init.lua │ │ ├── RPC │ │ └── JSON │ │ │ ├── init.lua │ │ │ └── init.spec.lua │ │ ├── Spec │ │ └── init.lua │ │ ├── System │ │ ├── init.lua │ │ └── init.spec.lua │ │ ├── Vim │ │ ├── FloatingWindow.lua │ │ ├── Keymap.lua │ │ ├── Keymap.spec.lua │ │ ├── RegExp.lua │ │ ├── RegExp.spec.lua │ │ ├── Syntax.lua │ │ ├── Syntax.spec.lua │ │ └── WinSaveView.lua │ │ ├── init.lua │ │ └── init.spec.lua └── kit │ ├── helper.lua │ └── install.lua ├── package-lock.json ├── package.json ├── plugin └── kit.lua ├── script ├── bench │ ├── Async │ │ └── stack.lua │ └── IO │ │ └── walk.lua └── lsp │ ├── generate-client.ts │ ├── generate-protocol.ts │ └── index.ts ├── stylua.toml └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | 18 | runs-on: ${{matrix.os}} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: Install neovim 24 | uses: rhysd/action-setup-vim@v1 25 | with: 26 | neovim: true 27 | version: nightly 28 | 29 | - uses: leafo/gh-actions-lua@v9.1.0 30 | with: 31 | luaVersion: "5.1" 32 | 33 | - name: Install luarocks 34 | uses: leafo/gh-actions-luarocks@v4 35 | 36 | - name: Install node.js 37 | uses: actions/setup-node@v2 38 | with: 39 | node-version: '18' 40 | cache: npm 41 | 42 | - name: Setup tools 43 | run: | 44 | luarocks install vusted 45 | 46 | - name: Run tests 47 | run: | 48 | npm ci 49 | npm run update 50 | npm run test 51 | 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | fixture/**/*.ignore 3 | node_modules 4 | !.gitkeep 5 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm run check" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'assert', 'async' } 2 | max_line_length = false 3 | 4 | -------------------------------------------------------------------------------- /.luarc.typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", 3 | "diagnostics.globals": [ 4 | "vim", 5 | "before_each", 6 | "after_each", 7 | "describe", 8 | "it" 9 | ], 10 | "runtime.version": "LuaJIT", 11 | "runtime.path": [ 12 | "lua/?.lua", 13 | "lua/?/init.lua" 14 | ], 15 | "workspace.library": [ 16 | "${3rd}/luv/library" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-kit 2 | 3 | My personal Lua utilities for neovim. 4 | 5 | # Usage 6 | 7 | You can install this utilities via `KitInstall` command (Testd only in Mac). 8 | 9 | ``` 10 | :KitInstall 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /fixture/IO/read_file.txt: -------------------------------------------------------------------------------- 1 | read_file 2 | read_file 3 | read_file 4 | read_file 5 | read_file 6 | read_file 7 | read_file 8 | read_file 9 | read_file 10 | read_file 11 | -------------------------------------------------------------------------------- /fixture/IO/scandir/a/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrsh7th/nvim-kit/f9351759a0aab748fa2a8f534143d59f47e73d23/fixture/IO/scandir/a/0/1 -------------------------------------------------------------------------------- /fixture/IO/scandir/a/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrsh7th/nvim-kit/f9351759a0aab748fa2a8f534143d59f47e73d23/fixture/IO/scandir/a/1 -------------------------------------------------------------------------------- /fixture/IO/scandir/a/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrsh7th/nvim-kit/f9351759a0aab748fa2a8f534143d59f47e73d23/fixture/IO/scandir/a/2 -------------------------------------------------------------------------------- /fixture/IO/scandir/a/3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrsh7th/nvim-kit/f9351759a0aab748fa2a8f534143d59f47e73d23/fixture/IO/scandir/a/3 -------------------------------------------------------------------------------- /fixture/LSP/DocumentSelector/dummy.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrsh7th/nvim-kit/f9351759a0aab748fa2a8f534143d59f47e73d23/fixture/LSP/DocumentSelector/dummy.js -------------------------------------------------------------------------------- /lua/___kit___/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 ___kit___.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/___kit___/kit/App/Cache.spec.lua: -------------------------------------------------------------------------------- 1 | local Cache = require('___kit___.kit.App.Cache') 2 | 3 | describe('kit.App.Cache', function() 4 | it('should works {get,set,has,del}', function() 5 | local cache = Cache.new() 6 | assert.equal(cache:get('unknown'), nil) 7 | assert.equal(cache:has('unknown'), false) 8 | cache:set('known', nil) 9 | assert.equal(cache:get('known'), nil) 10 | assert.equal(cache:has('known'), true) 11 | cache:del('known') 12 | assert.equal(cache:get('known'), nil) 13 | assert.equal(cache:has('known'), false) 14 | end) 15 | 16 | it('should work ensure', function() 17 | local ensure = setmetatable({ 18 | count = 0, 19 | }, { 20 | __call = function(self) 21 | self.count = self.count + 1 22 | end, 23 | }) --[[@as function]] 24 | local cache = Cache.new() 25 | 26 | -- Ensure the value. 27 | assert.equal(cache:ensure('key', ensure), nil) 28 | assert.equal(cache:has('key'), true) 29 | assert.equal(ensure.count, 1) 30 | 31 | -- Doesn't call when the value was ensured. 32 | assert.equal(cache:ensure('key', ensure), nil) 33 | assert.equal(ensure.count, 1) 34 | 35 | -- Call after delete. 36 | cache:del('key') 37 | assert.equal(cache:ensure('key', ensure), nil) 38 | assert.equal(ensure.count, 2) 39 | end) 40 | end) 41 | -------------------------------------------------------------------------------- /lua/___kit___/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.upper = {} 13 | string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char) 14 | Character.upper[string.byte(char)] = char 15 | end) 16 | 17 | ---@type table 18 | Character.digit = {} 19 | string.gsub('1234567890', '.', function(char) 20 | Character.digit[string.byte(char)] = char 21 | end) 22 | 23 | ---@type table 24 | Character.white = {} 25 | string.gsub(' \t\n', '.', function(char) 26 | Character.white[string.byte(char)] = char 27 | end) 28 | 29 | ---Return specified byte is alpha or not. 30 | ---@param byte integer 31 | ---@return boolean 32 | function Character.is_alpha(byte) 33 | return not not (Character.alpha[byte] ~= nil or (byte and Character.alpha[byte + 32] ~= nil)) 34 | end 35 | 36 | ---Return specified byte is digit or not. 37 | ---@param byte integer 38 | ---@return boolean 39 | function Character.is_digit(byte) 40 | return Character.digit[byte] ~= nil 41 | end 42 | 43 | ---Return specified byte is alpha or not. 44 | ---@param byte integer 45 | ---@return boolean 46 | function Character.is_alnum(byte) 47 | return Character.is_alpha(byte) or Character.is_digit(byte) 48 | end 49 | 50 | ---Return specified byte is alpha or not. 51 | ---@param byte integer 52 | ---@return boolean 53 | function Character.is_upper(byte) 54 | return Character.upper[byte] ~= nil 55 | end 56 | 57 | ---Return specified byte is alpha or not. 58 | ---@param byte integer 59 | ---@return boolean 60 | function Character.is_lower(byte) 61 | return Character.alpha[byte] ~= nil 62 | end 63 | 64 | ---Return specified byte is white or not. 65 | ---@param byte integer 66 | ---@return boolean 67 | function Character.is_white(byte) 68 | return Character.white[byte] ~= nil 69 | end 70 | 71 | ---Return specified byte is symbol or not. 72 | ---@param byte integer 73 | ---@return boolean 74 | function Character.is_symbol(byte) 75 | return not Character.is_alnum(byte) and not Character.is_white(byte) 76 | end 77 | 78 | ---@param a integer 79 | ---@param b integer 80 | function Character.match_ignorecase(a, b) 81 | if a == b then 82 | return true 83 | elseif Character.is_alpha(a) and Character.is_alpha(b) then 84 | return (a == b + 32) or (a == b - 32) 85 | end 86 | return false 87 | end 88 | 89 | ---@param text string 90 | ---@param index integer 91 | ---@return boolean 92 | function Character.is_semantic_index(text, index) 93 | if index <= 1 then 94 | return true 95 | end 96 | 97 | local prev = string.byte(text, index - 1) 98 | local curr = string.byte(text, index) 99 | 100 | if not Character.is_upper(prev) and Character.is_upper(curr) then 101 | return true 102 | end 103 | if Character.is_symbol(curr) or Character.is_white(curr) then 104 | return true 105 | end 106 | if not Character.is_alpha(prev) and Character.is_alpha(curr) then 107 | return true 108 | end 109 | if not Character.is_digit(prev) and Character.is_digit(curr) then 110 | return true 111 | end 112 | return false 113 | end 114 | 115 | ---@param text string 116 | ---@param current_index integer 117 | ---@return integer 118 | function Character.get_next_semantic_index(text, current_index) 119 | for i = current_index + 1, #text do 120 | if Character.is_semantic_index(text, i) then 121 | return i 122 | end 123 | end 124 | return #text + 1 125 | end 126 | 127 | return Character 128 | -------------------------------------------------------------------------------- /lua/___kit___/kit/App/Character.spec.lua: -------------------------------------------------------------------------------- 1 | local Character = require('___kit___.kit.App.Character') 2 | 3 | describe('kit.App.Character', function() 4 | it('is_alpha', function() 5 | assert.is_true(Character.is_alpha(('a'):byte())) 6 | assert.is_true(Character.is_alpha(('A'):byte())) 7 | assert.is_false(Character.is_alpha(('0'):byte())) 8 | assert.is_false(Character.is_alpha((' '):byte())) 9 | assert.is_false(Character.is_alpha(('*'):byte())) 10 | end) 11 | it('is_digit', function() 12 | assert.is_false(Character.is_digit(('a'):byte())) 13 | assert.is_false(Character.is_digit(('A'):byte())) 14 | assert.is_true(Character.is_digit(('0'):byte())) 15 | assert.is_false(Character.is_digit((' '):byte())) 16 | assert.is_false(Character.is_digit(('*'):byte())) 17 | end) 18 | it('is_alnum', function() 19 | assert.is_true(Character.is_alnum(('a'):byte())) 20 | assert.is_true(Character.is_alnum(('A'):byte())) 21 | assert.is_true(Character.is_alnum(('0'):byte())) 22 | assert.is_false(Character.is_alnum((' '):byte())) 23 | assert.is_false(Character.is_alnum(('*'):byte())) 24 | end) 25 | it('is_white', function() 26 | assert.is_false(Character.is_white(('a'):byte())) 27 | assert.is_false(Character.is_white(('A'):byte())) 28 | assert.is_false(Character.is_white(('0'):byte())) 29 | assert.is_true(Character.is_white((' '):byte())) 30 | assert.is_false(Character.is_white(('*'):byte())) 31 | end) 32 | it('is_symbol', function() 33 | assert.is_false(Character.is_symbol(('a'):byte())) 34 | assert.is_false(Character.is_symbol(('A'):byte())) 35 | assert.is_false(Character.is_symbol(('0'):byte())) 36 | assert.is_false(Character.is_symbol((' '):byte())) 37 | assert.is_true(Character.is_symbol(('*'):byte())) 38 | end) 39 | end) 40 | -------------------------------------------------------------------------------- /lua/___kit___/kit/App/Command.lua: -------------------------------------------------------------------------------- 1 | ---@class ___kit___.kit.App.Command.SubCommand.Argument 2 | ---@field public complete? fun(prefix: string):string[] 3 | ---@field public required? boolean 4 | 5 | ---@class ___kit___.kit.App.Command.SubCommandSpecifier 6 | ---@field public desc? string 7 | ---@field public args? table 8 | ---@field public execute fun(params: ___kit___.kit.App.Command.ExecuteParams, arguments: table) 9 | 10 | ---@class ___kit___.kit.App.Command.SubCommand: ___kit___.kit.App.Command.SubCommandSpecifier 11 | ---@field public name string 12 | ---@field public args table 13 | 14 | ---@class ___kit___.kit.App.Command 15 | ---@field public name string 16 | ---@field public subcommands table 17 | local Command = {} 18 | Command.__index = Command 19 | 20 | ---Create a new command. 21 | ---@param name string 22 | ---@param subcommand_specifiers table 23 | function Command.new(name, subcommand_specifiers) 24 | -- normalize subcommand specifiers. 25 | local subcommands = {} 26 | for subcommand_name, subcommand_specifier in pairs(subcommand_specifiers) do 27 | subcommands[subcommand_name] = { 28 | name = subcommand_name, 29 | args = subcommand_specifier.args or {}, 30 | execute = subcommand_specifier.execute, 31 | } 32 | end 33 | 34 | -- create command. 35 | return setmetatable({ 36 | name = name, 37 | subcommands = subcommands, 38 | }, Command) 39 | end 40 | 41 | ---@class ___kit___.kit.App.Command.ExecuteParams 42 | ---@field public name string 43 | ---@field public args string 44 | ---@field public fargs string[] 45 | ---@field public nargs string 46 | ---@field public bang boolean 47 | ---@field public line1 integer 48 | ---@field public line2 integer 49 | ---@field public range 0|1|2 50 | ---@field public count integer 51 | ---@field public req string 52 | ---@field public mods string 53 | ---@field public smods string[] 54 | ---Execute command. 55 | ---@param params ___kit___.kit.App.Command.ExecuteParams 56 | function Command:execute(params) 57 | local parsed = self._parse(params.args) 58 | 59 | local subcommand = self.subcommands[parsed[1].text] 60 | if not subcommand then 61 | error(('Unknown subcommand: %s'):format(parsed[1].text)) 62 | end 63 | 64 | local arguments = {} 65 | 66 | local pos = 1 67 | for i, part in ipairs(parsed) do 68 | if i > 1 then 69 | local is_named_argument = vim.iter(pairs(subcommand.args)):any(function(name) 70 | return type(name) == 'string' and part.text:sub(1, #name + 1) == ('%s='):format(name) 71 | end) 72 | if is_named_argument then 73 | local s = part.text:find('=', 1, true) 74 | if s then 75 | local name = part.text:sub(1, s - 1) 76 | local value = part.text:sub(s + 1) 77 | arguments[name] = value 78 | end 79 | else 80 | arguments[pos] = part.text 81 | pos = pos + 1 82 | end 83 | end 84 | end 85 | 86 | -- check required arguments. 87 | for name, arg in pairs(subcommand.args or {}) do 88 | if arg.required and not arguments[name] then 89 | error(('Argument %s is required.'):format(name)) 90 | end 91 | end 92 | 93 | subcommand.execute(params, arguments) 94 | end 95 | 96 | ---Complete command. 97 | ---@param cmdline string 98 | ---@param cursor integer 99 | function Command:complete(cmdline, cursor) 100 | local parsed = self._parse(cmdline) 101 | 102 | -- check command. 103 | if parsed[1].text ~= self.name then 104 | return {} 105 | end 106 | 107 | -- complete subcommand names. 108 | if parsed[2] and parsed[2].s <= cursor and cursor <= parsed[2].e then 109 | return vim 110 | .iter(pairs(self.subcommands)) 111 | :map(function(_, subcommand) 112 | return subcommand.name 113 | end) 114 | :totable() 115 | end 116 | 117 | -- check subcommand is exists. 118 | local subcommand = self.subcommands[parsed[2].text] 119 | if not subcommand then 120 | return {} 121 | end 122 | 123 | -- check subcommand arguments. 124 | local pos = 1 125 | for i, part in ipairs(parsed) do 126 | if i > 2 then 127 | local is_named_argument_name = vim.regex([=[^--\?[^=]*$]=]):match_str(part.text) ~= nil 128 | local is_named_argument_value = vim.iter(pairs(subcommand.args)):any(function(name) 129 | name = tostring(name) 130 | return part.text:sub(1, #name + 1) == ('%s='):format(name) 131 | end) 132 | 133 | -- current cursor argument. 134 | if part.s <= cursor and cursor <= part.e then 135 | if is_named_argument_name then 136 | -- return named-argument completion. 137 | return vim 138 | .iter(pairs(subcommand.args)) 139 | :map(function(name) 140 | return name 141 | end) 142 | :filter(function(name) 143 | return type(name) == 'string' 144 | end) 145 | :totable() 146 | elseif is_named_argument_value then 147 | -- return specific named-argument value completion. 148 | for name, argument in pairs(subcommand.args) do 149 | if type(name) == 'string' then 150 | if part.text:sub(1, #name + 1) == ('%s='):format(name) then 151 | if argument.complete then 152 | return argument.complete(part.text:sub(#name + 2)) 153 | end 154 | return {} 155 | end 156 | end 157 | end 158 | elseif subcommand.args[pos] then 159 | local argument = subcommand.args[pos] 160 | if argument.complete then 161 | return argument.complete(part.text) 162 | end 163 | return {} 164 | end 165 | end 166 | 167 | -- increment positional argument. 168 | if not is_named_argument_name and not is_named_argument_value then 169 | pos = pos + 1 170 | end 171 | end 172 | end 173 | end 174 | 175 | ---Parse command line. 176 | ---@param cmdline string 177 | ---@return { text: string, s: integer, e: integer }[] 178 | function Command._parse(cmdline) 179 | ---@type { text: string, s: integer, e: integer }[] 180 | local parsed = {} 181 | 182 | local part = {} 183 | local s = 1 184 | local i = 1 185 | while i <= #cmdline do 186 | local c = cmdline:sub(i, i) 187 | if c == '\\' then 188 | table.insert(part, cmdline:sub(i + 1, i + 1)) 189 | i = i + 1 190 | elseif c == ' ' then 191 | if #part > 0 then 192 | table.insert(parsed, { 193 | text = table.concat(part), 194 | s = s - 1, 195 | e = i - 1, 196 | }) 197 | part = {} 198 | s = i + 1 199 | end 200 | else 201 | table.insert(part, c) 202 | end 203 | i = i + 1 204 | end 205 | 206 | if #part then 207 | table.insert(parsed, { 208 | text = table.concat(part), 209 | s = s - 1, 210 | e = i - 1, 211 | }) 212 | return parsed 213 | end 214 | 215 | table.insert(parsed, { 216 | text = '', 217 | s = #cmdline, 218 | e = #cmdline + 1, 219 | }) 220 | 221 | return parsed 222 | end 223 | 224 | return Command 225 | -------------------------------------------------------------------------------- /lua/___kit___/kit/App/Command.spec.lua: -------------------------------------------------------------------------------- 1 | local Command = require('___kit___.kit.App.Command') 2 | 3 | describe('kit.App.Command', function() 4 | local command = Command.new('Misc', { 5 | month = { 6 | args = { 7 | ['--lang'] = { 8 | complete = function() 9 | return { 10 | 'en', 11 | 'ja', 12 | } 13 | end, 14 | }, 15 | [1] = { 16 | complete = function() 17 | return { 18 | 'January', 19 | 'February', 20 | 'March', 21 | 'April', 22 | 'May', 23 | 'June', 24 | 'July', 25 | 'August', 26 | 'September', 27 | 'October', 28 | 'November', 29 | 'December', 30 | } 31 | end, 32 | }, 33 | }, 34 | execute = function() 35 | vim.print('Misc') 36 | end, 37 | }, 38 | }) 39 | 40 | it('complete', function() 41 | assert.are.same({ 42 | 'January', 43 | 'February', 44 | 'March', 45 | 'April', 46 | 'May', 47 | 'June', 48 | 'July', 49 | 'August', 50 | 'September', 51 | 'October', 52 | 'November', 53 | 'December', 54 | }, command:complete('Misc month ', 11)) 55 | assert.are.same({ 56 | '--lang', 57 | }, command:complete('Misc month -', 12)) 58 | assert.are.same({ 59 | 'en', 60 | 'ja', 61 | }, command:complete('Misc month --lang=', 18)) 62 | assert.are.same({ 63 | 'January', 64 | 'February', 65 | 'March', 66 | 'April', 67 | 'May', 68 | 'June', 69 | 'July', 70 | 'August', 71 | 'September', 72 | 'October', 73 | 'November', 74 | 'December', 75 | }, command:complete('Misc month --lang=ja ', 21)) 76 | end) 77 | 78 | it('_parse', function() 79 | assert.are.same({ 80 | { text = 'Misc', s = 0, e = 4 }, 81 | { text = 'month', s = 5, e = 10 }, 82 | { text = 'j', s = 11, e = 12 }, 83 | }, command._parse('Misc month j')) 84 | assert.are.same({ 85 | { text = 'Misc', s = 0, e = 4 }, 86 | { text = 'month', s = 5, e = 10 }, 87 | { text = '', s = 11, e = 11 }, 88 | }, command._parse('Misc month ')) 89 | end) 90 | end) 91 | -------------------------------------------------------------------------------- /lua/___kit___/kit/App/Config.lua: -------------------------------------------------------------------------------- 1 | local kit = require('___kit___.kit') 2 | local Cache = require('___kit___.kit.App.Cache') 3 | 4 | ---@class ___kit___.kit.App.Config.Schema 5 | 6 | ---@alias ___kit___.kit.App.Config.SchemaInternal ___kit___.kit.App.Config.Schema|{ revision: integer } 7 | 8 | ---@class ___kit___.kit.App.Config 9 | ---@field private _cache ___kit___.kit.App.Cache 10 | ---@field private _default ___kit___.kit.App.Config.SchemaInternal 11 | ---@field private _global ___kit___.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 ___kit___.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 ___kit___.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 ___kit___.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 ___kit___.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 ___kit___.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: ___kit___.kit.App.Config.Schema)|{ filetype: fun(filetypes: string|string[], config: ___kit___.kit.App.Config.Schema), buffer: fun(bufnr: integer, config: ___kit___.kit.App.Config.Schema) } 79 | function Config:create_setup_interface() 80 | return setmetatable({ 81 | ---@param filetypes string|string[] 82 | ---@param config ___kit___.kit.App.Config.Schema 83 | filetype = function(filetypes, config) 84 | self:filetype(filetypes, config) 85 | end, 86 | ---@param bufnr integer 87 | ---@param config ___kit___.kit.App.Config.Schema 88 | buffer = function(bufnr, config) 89 | self:buffer(bufnr, config) 90 | end, 91 | }, { 92 | ---@param config ___kit___.kit.App.Config.Schema 93 | __call = function(_, config) 94 | self:global(config) 95 | end, 96 | }) 97 | end 98 | 99 | return Config 100 | -------------------------------------------------------------------------------- /lua/___kit___/kit/App/Config.spec.lua: -------------------------------------------------------------------------------- 1 | local Config = require('___kit___.kit.App.Config') 2 | 3 | describe('kit.App.Config', function() 4 | before_each(function() 5 | vim.cmd([[enew]]) 6 | end) 7 | 8 | it('should {setup,get} global config', function() 9 | local config = Config.new({}) 10 | config:global({ key = 1 }) 11 | assert.are.same(config:get(), { key = 1 }) 12 | end) 13 | 14 | it('should {setup,get} filetype config', function() 15 | local config = Config.new({}) 16 | vim.cmd([[set filetype=lua]]) 17 | config:filetype('lua', { key = 1 }) 18 | assert.are.same(config:get(), { key = 1 }) 19 | vim.cmd([[set filetype=]]) 20 | assert.are.same(config:get(), {}) 21 | end) 22 | 23 | it('should {setup,get} buffer config', function() 24 | local config = Config.new({}) 25 | config:buffer(0, { key = 1 }) 26 | assert.are.same(config:get(), { key = 1 }) 27 | vim.cmd([[new]]) 28 | assert.are.same(config:get(), {}) 29 | end) 30 | 31 | it('should merge configuration', function() 32 | local config = Config.new({}) 33 | local bufnr = vim.api.nvim_get_current_buf() 34 | vim.cmd([[set filetype=lua]]) 35 | config:global({ global = 1 }) 36 | config:filetype('lua', { filetype = 1 }) 37 | config:buffer(0, { buffer = 1 }) 38 | assert.are.same(config:get(), { global = 1, filetype = 1, buffer = 1 }) 39 | vim.cmd([[set filetype=]]) 40 | assert.are.same(config:get(), { global = 1, buffer = 1 }) 41 | vim.cmd([[new]]) 42 | assert.are.same(config:get(), { global = 1 }) 43 | vim.cmd(([[%sbuffer]]):format(bufnr)) 44 | assert.are.same(config:get(), { global = 1, buffer = 1 }) 45 | vim.cmd([[set filetype=lua]]) 46 | assert.are.same(config:get(), { global = 1, filetype = 1, buffer = 1 }) 47 | end) 48 | 49 | it('should create accessors', function() 50 | local config = Config.new({}) 51 | local setup = config:create_setup_interface() 52 | local bufnr = vim.api.nvim_get_current_buf() 53 | vim.cmd([[set filetype=lua]]) 54 | setup({ global = 1 }) 55 | setup.filetype('lua', { filetype = 1 }) 56 | setup.buffer(0, { buffer = 1 }) 57 | assert.are.same(config:get(), { global = 1, filetype = 1, buffer = 1 }) 58 | vim.cmd([[set filetype=]]) 59 | assert.are.same(config:get(), { global = 1, buffer = 1 }) 60 | vim.cmd([[new]]) 61 | assert.are.same(config:get(), { global = 1 }) 62 | vim.cmd(([[%sbuffer]]):format(bufnr)) 63 | assert.are.same(config:get(), { global = 1, buffer = 1 }) 64 | vim.cmd([[set filetype=lua]]) 65 | assert.are.same(config:get(), { global = 1, filetype = 1, buffer = 1 }) 66 | end) 67 | end) 68 | -------------------------------------------------------------------------------- /lua/___kit___/kit/App/Event.lua: -------------------------------------------------------------------------------- 1 | ---@class ___kit___.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/___kit___/kit/App/Event.spec.lua: -------------------------------------------------------------------------------- 1 | local Event = require('___kit___.kit.App.Event') 2 | 3 | local step = setmetatable({ 4 | steps = {}, 5 | }, { 6 | __call = function(self, ...) 7 | table.insert(self.steps, { ... }) 8 | end, 9 | }) --[[@as function]] 10 | 11 | describe('kit.App.Event', function() 12 | before_each(function() 13 | vim.cmd([[enew]]) 14 | end) 15 | 16 | it('should work', function() 17 | local event = Event.new() 18 | event:on('e', function(...) 19 | step(1, ...) 20 | end) 21 | event:on('e', function(...) 22 | step(2, ...) 23 | end) 24 | event:on('e', function(...) 25 | step(3, ...) 26 | end) 27 | event:once('e', function(...) 28 | step(4, ...) 29 | end) 30 | event:on('e', function(...) 31 | step(5, ...) 32 | end)() 33 | 34 | event:emit('e', 'payload1') 35 | event:emit('e', 'payload2') 36 | assert.are.same(step.steps, { 37 | { 1, 'payload1' }, 38 | { 2, 'payload1' }, 39 | { 3, 'payload1' }, 40 | { 4, 'payload1' }, 41 | { 1, 'payload2' }, 42 | { 2, 'payload2' }, 43 | { 3, 'payload2' }, 44 | }) 45 | end) 46 | end) 47 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Async/AsyncTask.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: invisible 2 | local uv = require('luv') 3 | local kit = require('___kit___.kit') 4 | 5 | local is_thread = vim.is_thread() 6 | 7 | ---@class ___kit___.kit.Async.AsyncTask 8 | ---@field private value any 9 | ---@field private status ___kit___.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 ___kit___.kit.Async.AsyncTask 18 | ---@param status ___kit___.kit.Async.AsyncTask.Status 19 | ---@param value any 20 | local function settle(task, status, value) 21 | task.status = status 22 | task.value = value 23 | if task.children then 24 | for _, c in ipairs(task.children) do 25 | c() 26 | end 27 | end 28 | 29 | if status == AsyncTask.Status.Rejected then 30 | if not task.chained and not task.synced then 31 | local timer = uv.new_timer() 32 | timer:start( 33 | 0, 34 | 0, 35 | kit.safe_schedule_wrap(function() 36 | timer:stop() 37 | timer:close() 38 | if not task.chained and not task.synced then 39 | AsyncTask.on_unhandled_rejection(value) 40 | end 41 | end) 42 | ) 43 | end 44 | end 45 | end 46 | 47 | ---@enum ___kit___.kit.Async.AsyncTask.Status 48 | AsyncTask.Status = { 49 | Pending = 0, 50 | Fulfilled = 1, 51 | Rejected = 2, 52 | } 53 | 54 | ---Handle unhandled rejection. 55 | ---@param err any 56 | function AsyncTask.on_unhandled_rejection(err) 57 | error('AsyncTask.on_unhandled_rejection: ' .. (type(err) == 'table' and vim.inspect(err) or tostring(err)), 2) 58 | end 59 | 60 | ---Return the value is AsyncTask or not. 61 | ---@param value any 62 | ---@return boolean 63 | function AsyncTask.is(value) 64 | return getmetatable(value) == AsyncTask 65 | end 66 | 67 | ---Resolve all tasks. 68 | ---@param tasks any[] 69 | ---@return ___kit___.kit.Async.AsyncTask 70 | function AsyncTask.all(tasks) 71 | return AsyncTask.new(function(resolve, reject) 72 | if #tasks == 0 then 73 | resolve({}) 74 | return 75 | end 76 | 77 | local values = {} 78 | local count = 0 79 | for i, task in ipairs(tasks) do 80 | task:dispatch(function(value) 81 | values[i] = value 82 | count = count + 1 83 | if #tasks == count then 84 | resolve(values) 85 | end 86 | end, reject) 87 | end 88 | end) 89 | end 90 | 91 | ---Resolve first resolved task. 92 | ---@param tasks any[] 93 | ---@return ___kit___.kit.Async.AsyncTask 94 | function AsyncTask.race(tasks) 95 | return AsyncTask.new(function(resolve, reject) 96 | for _, task in ipairs(tasks) do 97 | task:dispatch(resolve, reject) 98 | end 99 | end) 100 | end 101 | 102 | ---Create resolved AsyncTask. 103 | ---@param v any 104 | ---@return ___kit___.kit.Async.AsyncTask 105 | function AsyncTask.resolve(v) 106 | if AsyncTask.is(v) then 107 | return v 108 | end 109 | return AsyncTask.new(function(resolve) 110 | resolve(v) 111 | end) 112 | end 113 | 114 | ---Create new AsyncTask. 115 | ---@NOET: The AsyncTask has similar interface to JavaScript Promise but the AsyncTask can be worked as synchronous. 116 | ---@param v any 117 | ---@return ___kit___.kit.Async.AsyncTask 118 | function AsyncTask.reject(v) 119 | if AsyncTask.is(v) then 120 | return v 121 | end 122 | return AsyncTask.new(function(_, reject) 123 | reject(v) 124 | end) 125 | end 126 | 127 | ---Create new async task object. 128 | ---@param runner fun(resolve?: fun(value: any?), reject?: fun(err: any?)) 129 | function AsyncTask.new(runner) 130 | local self = setmetatable({ 131 | value = nil, 132 | status = AsyncTask.Status.Pending, 133 | synced = false, 134 | chained = false, 135 | children = nil, 136 | }, AsyncTask) 137 | local ok, err = pcall(runner, function(res) 138 | if self.status == AsyncTask.Status.Pending then 139 | settle(self, AsyncTask.Status.Fulfilled, res) 140 | end 141 | end, function(err) 142 | if self.status == AsyncTask.Status.Pending then 143 | settle(self, AsyncTask.Status.Rejected, err) 144 | end 145 | end) 146 | if not ok then 147 | settle(self, AsyncTask.Status.Rejected, err) 148 | end 149 | return self 150 | end 151 | 152 | ---Sync async task. 153 | ---@NOTE: This method uses `vim.wait` so that this can't wait the typeahead to be empty. 154 | ---@param timeout integer 155 | ---@return any 156 | function AsyncTask:sync(timeout) 157 | self.synced = true 158 | 159 | local time = uv.now() 160 | while uv.now() - time <= timeout do 161 | if self.status ~= AsyncTask.Status.Pending then 162 | break 163 | end 164 | if is_thread then 165 | uv.run('once') 166 | else 167 | vim.wait(0) 168 | end 169 | end 170 | if self.status == AsyncTask.Status.Pending then 171 | error('AsyncTask:sync is timeout.', 2) 172 | end 173 | if self.status == AsyncTask.Status.Rejected then 174 | error(self.value, 2) 175 | end 176 | if self.status ~= AsyncTask.Status.Fulfilled then 177 | error('AsyncTask:sync is timeout.', 2) 178 | end 179 | return self.value 180 | end 181 | 182 | ---Await async task. 183 | ---@return any 184 | function AsyncTask:await() 185 | local Async = require('___kit___.kit.Async') 186 | local in_fast_event = vim.in_fast_event() 187 | local ok, res = pcall(Async.await, self) 188 | if not ok then 189 | error(res, 2) 190 | end 191 | if not in_fast_event and vim.in_fast_event() then 192 | Async.schedule():await() 193 | end 194 | return res 195 | end 196 | 197 | ---Return current state of task. 198 | ---@return { status: ___kit___.kit.Async.AsyncTask.Status, value: any } 199 | function AsyncTask:state() 200 | return { 201 | status = self.status, 202 | value = self.value, 203 | } 204 | end 205 | 206 | ---Register next step. 207 | ---@param on_fulfilled fun(value: any): any 208 | function AsyncTask:next(on_fulfilled) 209 | return self:dispatch(on_fulfilled, function(err) 210 | error(err, 2) 211 | end) 212 | end 213 | 214 | ---Register catch step. 215 | ---@param on_rejected fun(value: any): any 216 | ---@return ___kit___.kit.Async.AsyncTask 217 | function AsyncTask:catch(on_rejected) 218 | return self:dispatch(function(value) 219 | return value 220 | end, on_rejected) 221 | end 222 | 223 | ---Dispatch task state. 224 | ---@param on_fulfilled fun(value: any): any 225 | ---@param on_rejected fun(err: any): any 226 | ---@return ___kit___.kit.Async.AsyncTask 227 | function AsyncTask:dispatch(on_fulfilled, on_rejected) 228 | self.chained = true 229 | 230 | local function dispatch(resolve, reject) 231 | local on_next = self.status == AsyncTask.Status.Fulfilled and on_fulfilled or on_rejected 232 | local ok, res = pcall(on_next, self.value) 233 | if AsyncTask.is(res) then 234 | res:dispatch(resolve, reject) 235 | else 236 | if ok then 237 | resolve(res) 238 | else 239 | reject(res) 240 | end 241 | end 242 | end 243 | 244 | if self.status == AsyncTask.Status.Pending then 245 | return AsyncTask.new(function(resolve, reject) 246 | local function dispatcher() 247 | return dispatch(resolve, reject) 248 | end 249 | if self.children then 250 | table.insert(self.children, dispatcher) 251 | else 252 | self.children = { dispatcher } 253 | end 254 | end) 255 | end 256 | return AsyncTask.new(dispatch) 257 | end 258 | 259 | return AsyncTask 260 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Async/AsyncTask.spec.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 311 2 | 3 | local AsyncTask = require('___kit___.kit.Async.AsyncTask') 4 | 5 | describe('kit.Async', function() 6 | local once = function(fn) 7 | local done = false 8 | return function(...) 9 | if done then 10 | error('already called') 11 | end 12 | done = true 13 | return fn(...) 14 | end 15 | end 16 | 17 | it('should work AsyncTask:{next/catch}', function() 18 | -- first. 19 | local one_task = AsyncTask.new(once(function(resolve) 20 | vim.schedule(function() 21 | resolve(1) 22 | end) 23 | end)) 24 | assert.are.equal(one_task:sync(5000), 1) 25 | 26 | -- next with return value. 27 | local two_task = one_task:next(once(function(value) 28 | return value + 1 29 | end)) 30 | assert.are.equal(two_task:sync(5000), 2) 31 | 32 | -- next with return AsyncTask. 33 | local three_task = two_task:next(once(function(value) 34 | return AsyncTask.new(function(resolve) 35 | vim.schedule(function() 36 | resolve(value + 1) 37 | end) 38 | end) 39 | end)) 40 | assert.are.equal(three_task:sync(5000), 3) 41 | 42 | -- throw error. 43 | local err_task = three_task:next(once(function() 44 | error('error') 45 | end)) 46 | local _, err = pcall(function() 47 | return err_task:sync(5000) 48 | end) 49 | assert.are_not.equal(string.match(err, 'error$'), nil) 50 | 51 | -- skip rejected task's next. 52 | local steps = {} 53 | local catch_task = err_task 54 | :next(once(function() 55 | table.insert(steps, 1) 56 | end)) 57 | :next(once(function() 58 | table.insert(steps, 2) 59 | end)) 60 | :catch(function() 61 | return 'catch' 62 | end) 63 | :next(function(value) 64 | table.insert(steps, 3) 65 | return value 66 | end) 67 | assert.are.same(steps, { 3 }) 68 | assert.are.equal(catch_task:sync(5000), 'catch') 69 | end) 70 | 71 | it('should reject if resolve called with rejected task', function() 72 | local task = AsyncTask.new(function(_, reject) 73 | vim.defer_fn(reject, 10) 74 | end) 75 | local ok 76 | ok = pcall(function() 77 | AsyncTask.resolve(task):sync(5000) 78 | end) 79 | assert.is_false(ok) 80 | ok = pcall(function() 81 | AsyncTask.resolve(task):sync(5000) 82 | end) 83 | assert.is_false(ok) 84 | end) 85 | 86 | it('should throw timeout error', function() 87 | local task = AsyncTask.new(function(resolve) 88 | vim.defer_fn(resolve, 500) 89 | end) 90 | local ok = pcall(function() 91 | return task:sync(100) 92 | end) 93 | assert.is_false(ok) 94 | end) 95 | 96 | it('should work AsyncTask.all', function() 97 | local now = vim.loop.now() 98 | local values = AsyncTask.all({ 99 | AsyncTask.new(function(resolve) 100 | vim.defer_fn(function() 101 | resolve(1) 102 | end, 300) 103 | end), 104 | AsyncTask.new(function(resolve) 105 | vim.defer_fn(function() 106 | resolve(2) 107 | end, 200) 108 | end), 109 | AsyncTask.new(function(resolve) 110 | vim.defer_fn(function() 111 | resolve(3) 112 | end, 100) 113 | end), 114 | }):sync(5000) 115 | assert.are.same(values, { 1, 2, 3 }) 116 | assert.is_true((vim.loop.now() - now) - 300 < 10) 117 | end) 118 | 119 | it('should work AsyncTask.race', function() 120 | local now = vim.loop.now() 121 | local value = AsyncTask.race({ 122 | AsyncTask.new(function(resolve) 123 | vim.defer_fn(function() 124 | resolve(1) 125 | end, 500) 126 | end), 127 | AsyncTask.new(function(resolve) 128 | vim.defer_fn(function() 129 | resolve(2) 130 | end, 300) 131 | end), 132 | AsyncTask.new(function(resolve) 133 | vim.defer_fn(function() 134 | resolve(3) 135 | end, 100) 136 | end), 137 | }):sync(5000) 138 | assert.are.same(value, 3) 139 | assert.is_true((vim.loop.now() - now) - 100 < 10) 140 | end) 141 | 142 | it('should return current state of task', function() 143 | local success = AsyncTask.new(function(resolve) 144 | vim.defer_fn(function() 145 | resolve(1) 146 | end, 100) 147 | end) 148 | assert.are.same(success:state(), { status = AsyncTask.Status.Pending }) 149 | success:sync(5000) 150 | assert.are.same(success:state(), { status = AsyncTask.Status.Fulfilled, value = 1 }) 151 | 152 | local failure = AsyncTask.new(function(_, reject) 153 | vim.defer_fn(function() 154 | reject(1) 155 | end, 100) 156 | end) 157 | assert.are.same(failure:state(), { status = AsyncTask.Status.Pending }) 158 | pcall(function() 159 | failure:sync(5000) 160 | end) 161 | assert.are.same(failure:state(), { status = AsyncTask.Status.Rejected, value = 1 }) 162 | end) 163 | 164 | it('should work AsyncTask.on_unhandled_rejection', function() 165 | local called = false 166 | 167 | ---@diagnostic disable-next-line: duplicate-set-field 168 | function AsyncTask.on_unhandled_rejection() 169 | called = true 170 | end 171 | 172 | -- has no catched. 173 | AsyncTask.new(function() 174 | error('error') 175 | end) 176 | called = false 177 | vim.wait(1) 178 | assert.are.equal(called, true) 179 | 180 | -- has no catched. 181 | AsyncTask.new(function() 182 | error('error') 183 | end):next(function() 184 | -- ignore 185 | end) 186 | called = false 187 | vim.wait(1) 188 | assert.are.equal(called, true) 189 | 190 | -- has no catched. 191 | AsyncTask.new(function(resolve) 192 | resolve(nil) 193 | end):next(function() 194 | error('error') 195 | end) 196 | called = false 197 | vim.wait(1) 198 | assert.are.equal(called, true) 199 | 200 | -- has no catched. 201 | AsyncTask.new(function(_, reject) 202 | reject('error') 203 | end):catch(function(err) 204 | error(err) 205 | end) 206 | called = false 207 | vim.wait(1) 208 | assert.are.equal(called, true) 209 | 210 | -- catched. 211 | AsyncTask.new(function() 212 | error('error') 213 | end):catch(function() 214 | -- ignore 215 | end) 216 | called = false 217 | vim.wait(1) 218 | assert.are.equal(called, false) 219 | 220 | -- has no catched task but synced. 221 | local task = AsyncTask.new(function() 222 | error('error') 223 | end) 224 | pcall(function() 225 | task:sync(5000) 226 | end) 227 | called = false 228 | vim.wait(1) 229 | assert.are.equal(called, false) 230 | end) 231 | end) 232 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Async/ScheduledTimer.lua: -------------------------------------------------------------------------------- 1 | ---@class ___kit___.kit.Async.ScheduledTimer 2 | ---@field private _timer uv.uv_timer_t 3 | ---@field private _running boolean 4 | ---@field private _revision integer 5 | local ScheduledTimer = {} 6 | ScheduledTimer.__index = ScheduledTimer 7 | 8 | ---Create new timer. 9 | function ScheduledTimer.new() 10 | return setmetatable({ 11 | _timer = assert(vim.uv.new_timer()), 12 | _running = false, 13 | _revision = 0, 14 | }, ScheduledTimer) 15 | end 16 | 17 | ---Check if timer is running. 18 | ---@return boolean 19 | function ScheduledTimer:is_running() 20 | return self._running 21 | end 22 | 23 | ---Start timer. 24 | function ScheduledTimer:start(ms, repeat_ms, callback) 25 | self._timer:stop() 26 | self._running = true 27 | self._revision = self._revision + 1 28 | local revision = self._revision 29 | 30 | local on_tick 31 | local tick 32 | 33 | on_tick = function() 34 | if revision ~= self._revision then 35 | return 36 | end 37 | if vim.in_fast_event() then 38 | vim.schedule(tick) 39 | else 40 | tick() 41 | end 42 | end 43 | 44 | tick = function() 45 | if revision ~= self._revision then 46 | return 47 | end 48 | callback() -- `callback()` can restart timer, so it need to check revision here again. 49 | if revision ~= self._revision then 50 | return 51 | end 52 | if repeat_ms ~= 0 then 53 | self._timer:start(repeat_ms, 0, on_tick) 54 | else 55 | self._running = false 56 | end 57 | end 58 | 59 | if ms == 0 then 60 | on_tick() 61 | return 62 | end 63 | self._timer:stop() 64 | self._timer:start(ms, 0, on_tick) 65 | end 66 | 67 | ---Stop timer. 68 | function ScheduledTimer:stop() 69 | self._timer:stop() 70 | self._running = false 71 | self._revision = self._revision + 1 72 | end 73 | 74 | return ScheduledTimer 75 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Async/Worker.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local AsyncTask = require('___kit___.kit.Async.AsyncTask') 3 | 4 | ---@class ___kit___.kit.Async.WorkerOption 5 | ---@field public runtimepath string[] 6 | 7 | ---@class ___kit___.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 ___kit___.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('___kit___.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/___kit___/kit/Async/Worker.spec.lua: -------------------------------------------------------------------------------- 1 | local Worker = require('___kit___.kit.Async.Worker') 2 | 3 | describe('kit.Thread.Worker', function() 4 | it('should work basic usage', function() 5 | print('start') 6 | local worker = Worker.new(function(path) 7 | require('___kit___.kit.IO') 8 | .walk(path, function(_, entry) 9 | print(entry.path) 10 | end) 11 | :sync(5000) 12 | end) 13 | worker('./fixture'):sync(5000) 14 | print('finish') 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Async/init.lua: -------------------------------------------------------------------------------- 1 | local AsyncTask = require('___kit___.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 ___kit___.kit.Async.AsyncTask[] 13 | ---@return ___kit___.kit.Async.AsyncTask 14 | function Async.all(tasks) 15 | return AsyncTask.all(tasks) 16 | end 17 | 18 | ---Alias of AsyncTask.race. 19 | ---@param tasks ___kit___.kit.Async.AsyncTask[] 20 | ---@return ___kit___.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 ___kit___.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 ___kit___.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 ___kit___.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 ___kit___.kit.Async.AsyncTask 51 | function Async.run(runner, ...) 52 | local args = { ... } 53 | 54 | local thread_parent = Async.in_context() and coroutine.running() or nil 55 | 56 | local thread = coroutine.create(runner) 57 | _G.kit.Async.___threads___[thread] = { 58 | thread = thread, 59 | thread_parent = thread_parent, 60 | now = vim.uv.hrtime() / 1000000, 61 | } 62 | return AsyncTask.new(function(resolve, reject) 63 | local function next_step(ok, v) 64 | if getmetatable(v) == Interrupt then 65 | vim.defer_fn(function() 66 | next_step(coroutine.resume(thread)) 67 | end, v.timeout) 68 | return 69 | end 70 | 71 | if coroutine.status(thread) == 'dead' then 72 | if AsyncTask.is(v) then 73 | v:dispatch(resolve, reject) 74 | else 75 | if ok then 76 | resolve(v) 77 | else 78 | reject(v) 79 | end 80 | end 81 | _G.kit.Async.___threads___[thread] = nil 82 | return 83 | end 84 | 85 | v:dispatch(function(...) 86 | next_step(coroutine.resume(thread, true, ...)) 87 | end, function(...) 88 | next_step(coroutine.resume(thread, false, ...)) 89 | end) 90 | end 91 | 92 | next_step(coroutine.resume(thread, unpack(args))) 93 | end) 94 | end 95 | 96 | ---Return current context is async coroutine or not. 97 | ---@return boolean 98 | function Async.in_context() 99 | return _G.kit.Async.___threads___[coroutine.running()] ~= nil 100 | end 101 | 102 | ---Await async task. 103 | ---@param task ___kit___.kit.Async.AsyncTask 104 | ---@return any 105 | function Async.await(task) 106 | if not _G.kit.Async.___threads___[coroutine.running()] then 107 | error('`Async.await` must be called in async context.') 108 | end 109 | if not AsyncTask.is(task) then 110 | error('`Async.await` must be called with AsyncTask.') 111 | end 112 | 113 | local ok, res = coroutine.yield(task) 114 | if not ok then 115 | error(res, 2) 116 | end 117 | return res 118 | end 119 | 120 | ---Interrupt sync process. 121 | ---@param interval integer 122 | ---@param timeout? integer 123 | function Async.interrupt(interval, timeout) 124 | local thread = coroutine.running() 125 | if not _G.kit.Async.___threads___[thread] then 126 | error('`Async.interrupt` must be called in async context.') 127 | end 128 | 129 | local thread_parent = thread 130 | while true do 131 | local next_thread_parent = _G.kit.Async.___threads___[thread_parent].thread_parent 132 | if not next_thread_parent then 133 | break 134 | end 135 | if not _G.kit.Async.___threads___[next_thread_parent] then 136 | break 137 | end 138 | thread_parent = next_thread_parent 139 | end 140 | 141 | local prev_now = _G.kit.Async.___threads___[thread_parent].now 142 | local curr_now = vim.uv.hrtime() / 1000000 143 | if (curr_now - prev_now) > interval then 144 | coroutine.yield(setmetatable({ timeout = timeout or 16 }, Interrupt)) 145 | if _G.kit.Async.___threads___[thread_parent] then 146 | _G.kit.Async.___threads___[thread_parent].now = vim.uv.hrtime() / 1000000 147 | end 148 | end 149 | end 150 | 151 | ---Create vim.schedule task. 152 | ---@return ___kit___.kit.Async.AsyncTask 153 | function Async.schedule() 154 | return AsyncTask.new(function(resolve) 155 | vim.schedule(resolve) 156 | end) 157 | end 158 | 159 | ---Create vim.defer_fn task. 160 | ---@param timeout integer 161 | ---@return ___kit___.kit.Async.AsyncTask 162 | function Async.timeout(timeout) 163 | return AsyncTask.new(function(resolve) 164 | vim.defer_fn(resolve, timeout) 165 | end) 166 | end 167 | 168 | ---Create async function from callback function. 169 | ---@generic T: ... 170 | ---@param runner fun(...: T) 171 | ---@param option? { schedule?: boolean, callback?: integer } 172 | ---@return fun(...: T): ___kit___.kit.Async.AsyncTask 173 | function Async.promisify(runner, option) 174 | option = option or {} 175 | option.schedule = not vim.is_thread() and (option.schedule or false) 176 | option.callback = option.callback or nil 177 | return function(...) 178 | local args = { ... } 179 | return AsyncTask.new(function(resolve, reject) 180 | local max = #args + 1 181 | local pos = math.min(option.callback or max, max) 182 | table.insert(args, pos, function(err, ...) 183 | if option.schedule and vim.in_fast_event() then 184 | resolve = vim.schedule_wrap(resolve) 185 | reject = vim.schedule_wrap(reject) 186 | end 187 | if err then 188 | reject(err) 189 | else 190 | resolve(...) 191 | end 192 | end) 193 | runner(unpack(args)) 194 | end) 195 | end 196 | end 197 | 198 | return Async 199 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Async/init.spec.lua: -------------------------------------------------------------------------------- 1 | local Async = require('___kit___.kit.Async') 2 | local AsyncTask = require('___kit___.kit.Async.AsyncTask') 3 | 4 | describe('kit.Async', function() 5 | local multiply = function(v) 6 | return AsyncTask.new(function(resolve) 7 | vim.schedule(function() 8 | resolve(v * v) 9 | end) 10 | end) 11 | end 12 | 13 | it('should detect async context', function() 14 | assert.are.equal(Async.in_context(), false) 15 | Async.run(function() 16 | assert.are.equal(Async.in_context(), true) 17 | end) 18 | end) 19 | 20 | it('should work like JavaScript Promise', function() 21 | local num = Async.run(function() 22 | local num = 2 23 | num = multiply(num):await() 24 | num = multiply(num):await() 25 | return num 26 | end):sync(5000) 27 | assert.are.equal(num, 16) 28 | end) 29 | 30 | it('should work with exception', function() 31 | pcall(function() 32 | Async.run(function() 33 | error('error') 34 | end):sync(5000) 35 | end) 36 | end) 37 | 38 | it('should work with exception (nested)', function() 39 | Async.run(function() 40 | return Async.run(function() 41 | Async.run(function() 42 | error('error') 43 | end):await() 44 | assert.is_true(false) -- should not reach here. 45 | end):catch(function() 46 | assert.is_true(true) 47 | end) 48 | end):sync(5000) 49 | end) 50 | 51 | describe('.promisify', function() 52 | it('shoud wrap callback function', function() 53 | local function wait(ms, callback) 54 | vim.defer_fn(function() 55 | callback(nil, 'timeout') 56 | end, ms) 57 | end 58 | 59 | local wait_async = Async.promisify(wait) 60 | assert.equal(wait_async(100):sync(5000), 'timeout') 61 | end) 62 | it('shoud wrap callback function with rest arguments', function() 63 | local function wait(ms, callback, result) 64 | vim.defer_fn(function() 65 | callback(nil, result) 66 | end, ms) 67 | end 68 | 69 | local wait_async = Async.promisify(wait, { callback = 2 }) 70 | assert.equal(wait_async(100, 'timeout'):sync(5000), 'timeout') 71 | end) 72 | end) 73 | end) 74 | -------------------------------------------------------------------------------- /lua/___kit___/kit/IO/init.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.uv 2 | local Async = require('___kit___.kit.Async') 3 | 4 | local bytes = { 5 | backslash = string.byte('\\'), 6 | slash = string.byte('/'), 7 | tilde = string.byte('~'), 8 | dot = string.byte('.'), 9 | } 10 | 11 | ---@param path string 12 | ---@return string 13 | local function sep(path) 14 | for i = 1, #path do 15 | local c = path:byte(i) 16 | if c == bytes.slash then 17 | return path 18 | end 19 | if c == bytes.backslash then 20 | return (path:gsub('\\', '/')) 21 | end 22 | end 23 | return path 24 | end 25 | 26 | local home = sep(assert(vim.uv.os_homedir())) 27 | 28 | ---@see https://github.com/luvit/luvit/blob/master/deps/fs.lua 29 | local IO = {} 30 | 31 | ---@class ___kit___.kit.IO.UV.Stat 32 | ---@field public dev integer 33 | ---@field public mode integer 34 | ---@field public nlink integer 35 | ---@field public uid integer 36 | ---@field public gid integer 37 | ---@field public rdev integer 38 | ---@field public ino integer 39 | ---@field public size integer 40 | ---@field public blksize integer 41 | ---@field public blocks integer 42 | ---@field public flags integer 43 | ---@field public gen integer 44 | ---@field public atime { sec: integer, nsec: integer } 45 | ---@field public mtime { sec: integer, nsec: integer } 46 | ---@field public ctime { sec: integer, nsec: integer } 47 | ---@field public birthtime { sec: integer, nsec: integer } 48 | ---@field public type string 49 | 50 | ---@enum ___kit___.kit.IO.UV.AccessMode 51 | IO.AccessMode = { 52 | r = 'r', 53 | rs = 'rs', 54 | sr = 'sr', 55 | ['r+'] = 'r+', 56 | ['rs+'] = 'rs+', 57 | ['sr+'] = 'sr+', 58 | w = 'w', 59 | wx = 'wx', 60 | xw = 'xw', 61 | ['w+'] = 'w+', 62 | ['wx+'] = 'wx+', 63 | ['xw+'] = 'xw+', 64 | a = 'a', 65 | ax = 'ax', 66 | xa = 'xa', 67 | ['a+'] = 'a+', 68 | ['ax+'] = 'ax+', 69 | ['xa+'] = 'xa+', 70 | } 71 | 72 | ---@enum ___kit___.kit.IO.WalkStatus 73 | IO.WalkStatus = { 74 | SkipDir = 1, 75 | Break = 2, 76 | } 77 | 78 | ---@type fun(path: string): ___kit___.kit.Async.AsyncTask 79 | local uv_fs_stat = Async.promisify(uv.fs_stat) 80 | 81 | ---@type fun(path: string): ___kit___.kit.Async.AsyncTask 82 | local uv_fs_unlink = Async.promisify(uv.fs_unlink) 83 | 84 | ---@type fun(path: string): ___kit___.kit.Async.AsyncTask 85 | local uv_fs_rmdir = Async.promisify(uv.fs_rmdir) 86 | 87 | ---@type fun(path: string, mode: integer): ___kit___.kit.Async.AsyncTask 88 | local uv_fs_mkdir = Async.promisify(uv.fs_mkdir) 89 | 90 | ---@type fun(from: string, to: string, option?: { excl?: boolean, ficlone?: boolean, ficlone_force?: boolean }): ___kit___.kit.Async.AsyncTask 91 | local uv_fs_copyfile = Async.promisify(uv.fs_copyfile) 92 | 93 | ---@type fun(path: string, flags: ___kit___.kit.IO.UV.AccessMode, mode: integer): ___kit___.kit.Async.AsyncTask 94 | local uv_fs_open = Async.promisify(uv.fs_open) 95 | 96 | ---@type fun(fd: userdata): ___kit___.kit.Async.AsyncTask 97 | local uv_fs_close = Async.promisify(uv.fs_close) 98 | 99 | ---@type fun(fd: userdata, chunk_size: integer, offset?: integer): ___kit___.kit.Async.AsyncTask 100 | local uv_fs_read = Async.promisify(uv.fs_read) 101 | 102 | ---@type fun(fd: userdata, content: string, offset?: integer): ___kit___.kit.Async.AsyncTask 103 | local uv_fs_write = Async.promisify(uv.fs_write) 104 | 105 | ---@type fun(fd: userdata, offset: integer): ___kit___.kit.Async.AsyncTask 106 | local uv_fs_ftruncate = Async.promisify(uv.fs_ftruncate) 107 | 108 | ---@type fun(path: string): ___kit___.kit.Async.AsyncTask 109 | local uv_fs_scandir = Async.promisify(uv.fs_scandir) 110 | 111 | ---@type fun(path: string): ___kit___.kit.Async.AsyncTask 112 | local uv_fs_realpath = Async.promisify(uv.fs_realpath) 113 | 114 | ---Return if the path is directory. 115 | ---@param path string 116 | ---@return ___kit___.kit.Async.AsyncTask 117 | function IO.is_directory(path) 118 | path = IO.normalize(path) 119 | return Async.run(function() 120 | return uv_fs_stat(path) 121 | :catch(function() 122 | return {} 123 | end) 124 | :await().type == 'directory' 125 | end) 126 | end 127 | 128 | ---Return if the path is exists. 129 | ---@param path string 130 | ---@return ___kit___.kit.Async.AsyncTask 131 | function IO.exists(path) 132 | path = IO.normalize(path) 133 | return Async.run(function() 134 | return uv_fs_stat(path) 135 | :next(function() 136 | return true 137 | end) 138 | :catch(function() 139 | return false 140 | end) 141 | :await() 142 | end) 143 | end 144 | 145 | ---Get realpath. 146 | ---@param path string 147 | ---@return ___kit___.kit.Async.AsyncTask 148 | function IO.realpath(path) 149 | path = IO.normalize(path) 150 | return Async.run(function() 151 | return IO.normalize(uv_fs_realpath(path):await()) 152 | end) 153 | end 154 | 155 | ---Return file stats or throw error. 156 | ---@param path string 157 | ---@return ___kit___.kit.Async.AsyncTask 158 | function IO.stat(path) 159 | path = IO.normalize(path) 160 | return Async.run(function() 161 | return uv_fs_stat(path):await() 162 | end) 163 | end 164 | 165 | ---Read file. 166 | ---@param path string 167 | ---@param chunk_size? integer 168 | ---@return ___kit___.kit.Async.AsyncTask 169 | function IO.read_file(path, chunk_size) 170 | path = IO.normalize(path) 171 | chunk_size = chunk_size or 1024 172 | return Async.run(function() 173 | local stat = uv_fs_stat(path):await() 174 | local fd = uv_fs_open(path, IO.AccessMode.r, tonumber('755', 8)):await() 175 | local ok, res = pcall(function() 176 | local chunks = {} 177 | local offset = 0 178 | while offset < stat.size do 179 | local chunk = uv_fs_read(fd, math.min(chunk_size, stat.size - offset), offset):await() 180 | if not chunk then 181 | break 182 | end 183 | table.insert(chunks, chunk) 184 | offset = offset + #chunk 185 | end 186 | return table.concat(chunks, ''):sub(1, stat.size - 1) -- remove EOF. 187 | end) 188 | uv_fs_close(fd):await() 189 | if not ok then 190 | error(res) 191 | end 192 | return res 193 | end) 194 | end 195 | 196 | ---Write file. 197 | ---@param path string 198 | ---@param content string 199 | ---@param chunk_size? integer 200 | function IO.write_file(path, content, chunk_size) 201 | path = IO.normalize(path) 202 | content = content .. '\n' -- add EOF. 203 | chunk_size = chunk_size or 1024 204 | return Async.run(function() 205 | local fd = uv_fs_open(path, IO.AccessMode.w, tonumber('755', 8)):await() 206 | local ok, err = pcall(function() 207 | local offset = 0 208 | while offset < #content do 209 | local chunk = content:sub(offset + 1, offset + chunk_size) 210 | offset = offset + uv_fs_write(fd, chunk, offset):await() 211 | end 212 | uv_fs_ftruncate(fd, offset):await() 213 | end) 214 | uv_fs_close(fd):await() 215 | if not ok then 216 | error(err) 217 | end 218 | end) 219 | end 220 | 221 | ---Create directory. 222 | ---@param path string 223 | ---@param mode integer 224 | ---@param option? { recursive?: boolean } 225 | function IO.mkdir(path, mode, option) 226 | path = IO.normalize(path) 227 | option = option or {} 228 | option.recursive = option.recursive or false 229 | return Async.run(function() 230 | if not option.recursive then 231 | uv_fs_mkdir(path, mode):await() 232 | else 233 | local not_exists = {} 234 | local current = path 235 | while current ~= '/' do 236 | local stat = uv_fs_stat(current):catch(function() end):await() 237 | if stat then 238 | break 239 | end 240 | table.insert(not_exists, 1, current) 241 | current = IO.dirname(current) 242 | end 243 | for _, dir in ipairs(not_exists) do 244 | uv_fs_mkdir(dir, mode):await() 245 | end 246 | end 247 | end) 248 | end 249 | 250 | ---Remove file or directory. 251 | ---@param start_path string 252 | ---@param option? { recursive?: boolean } 253 | function IO.rm(start_path, option) 254 | start_path = IO.normalize(start_path) 255 | option = option or {} 256 | option.recursive = option.recursive or false 257 | return Async.run(function() 258 | local stat = uv_fs_stat(start_path):await() 259 | if stat.type == 'directory' then 260 | local children = IO.scandir(start_path):await() 261 | if not option.recursive and #children > 0 then 262 | error(('IO.rm: `%s` is a directory and not empty.'):format(start_path)) 263 | end 264 | IO.walk(start_path, function(err, entry) 265 | if err then 266 | error('IO.rm: ' .. tostring(err)) 267 | end 268 | if entry.type == 'directory' then 269 | uv_fs_rmdir(entry.path):await() 270 | else 271 | uv_fs_unlink(entry.path):await() 272 | end 273 | end, { postorder = true }):await() 274 | else 275 | uv_fs_unlink(start_path):await() 276 | end 277 | end) 278 | end 279 | 280 | ---Copy file or directory. 281 | ---@param from any 282 | ---@param to any 283 | ---@param option? { recursive?: boolean } 284 | ---@return ___kit___.kit.Async.AsyncTask 285 | function IO.cp(from, to, option) 286 | from = IO.normalize(from) 287 | to = IO.normalize(to) 288 | option = option or {} 289 | option.recursive = option.recursive or false 290 | return Async.run(function() 291 | local stat = uv_fs_stat(from):await() 292 | if stat.type == 'directory' then 293 | if not option.recursive then 294 | error(('IO.cp: `%s` is a directory.'):format(from)) 295 | end 296 | local from_pat = ('^%s'):format(vim.pesc(from)) 297 | IO.walk(from, function(err, entry) 298 | if err then 299 | error('IO.cp: ' .. tostring(err)) 300 | end 301 | local new_path = entry.path:gsub(from_pat, to) 302 | if entry.type == 'directory' then 303 | IO.mkdir(new_path, tonumber(stat.mode, 10), { recursive = true }):await() 304 | else 305 | uv_fs_copyfile(entry.path, new_path):await() 306 | end 307 | end):await() 308 | else 309 | uv_fs_copyfile(from, to):await() 310 | end 311 | end) 312 | end 313 | 314 | ---Walk directory entries recursively. 315 | ---@param start_path string 316 | ---@param callback fun(err: string|nil, entry: { path: string, type: string }): ___kit___.kit.IO.WalkStatus? 317 | ---@param option? { postorder?: boolean } 318 | function IO.walk(start_path, callback, option) 319 | start_path = IO.normalize(start_path) 320 | option = option or {} 321 | option.postorder = option.postorder or false 322 | return Async.run(function() 323 | local function walk_pre(dir) 324 | local ok, iter_entries = pcall(function() 325 | return IO.iter_scandir(dir.path):await() 326 | end) 327 | if not ok then 328 | return callback(iter_entries, dir) 329 | end 330 | local status = callback(nil, dir) 331 | if status == IO.WalkStatus.SkipDir then 332 | return 333 | elseif status == IO.WalkStatus.Break then 334 | return status 335 | end 336 | for entry in iter_entries do 337 | if entry.type == 'directory' then 338 | if walk_pre(entry) == IO.WalkStatus.Break then 339 | return IO.WalkStatus.Break 340 | end 341 | else 342 | if callback(nil, entry) == IO.WalkStatus.Break then 343 | return IO.WalkStatus.Break 344 | end 345 | end 346 | end 347 | end 348 | 349 | local function walk_post(dir) 350 | local ok, iter_entries = pcall(function() 351 | return IO.iter_scandir(dir.path):await() 352 | end) 353 | if not ok then 354 | return callback(iter_entries, dir) 355 | end 356 | for entry in iter_entries do 357 | if entry.type == 'directory' then 358 | if walk_post(entry) == IO.WalkStatus.Break then 359 | return IO.WalkStatus.Break 360 | end 361 | else 362 | if callback(nil, entry) == IO.WalkStatus.Break then 363 | return IO.WalkStatus.Break 364 | end 365 | end 366 | end 367 | return callback(nil, dir) 368 | end 369 | 370 | if not IO.is_directory(start_path) then 371 | error(('IO.walk: `%s` is not a directory.'):format(start_path)) 372 | end 373 | if option.postorder then 374 | walk_post({ path = start_path, type = 'directory' }) 375 | else 376 | walk_pre({ path = start_path, type = 'directory' }) 377 | end 378 | end) 379 | end 380 | 381 | ---Scan directory entries. 382 | ---@param path string 383 | ---@return ___kit___.kit.Async.AsyncTask 384 | function IO.scandir(path) 385 | path = IO.normalize(path) 386 | return Async.run(function() 387 | local fd = uv_fs_scandir(path):await() 388 | local entries = {} 389 | while true do 390 | local name, type = uv.fs_scandir_next(fd) 391 | if not name then 392 | break 393 | end 394 | table.insert(entries, { 395 | type = type, 396 | path = IO.join(path, name), 397 | }) 398 | end 399 | return entries 400 | end) 401 | end 402 | 403 | ---Scan directory entries. 404 | ---@param path any 405 | ---@return ___kit___.kit.Async.AsyncTask 406 | function IO.iter_scandir(path) 407 | path = IO.normalize(path) 408 | return Async.run(function() 409 | local fd = uv_fs_scandir(path):await() 410 | return function() 411 | local name, type = uv.fs_scandir_next(fd) 412 | if name then 413 | return { 414 | type = type, 415 | path = IO.join(path, name), 416 | } 417 | end 418 | end 419 | end) 420 | end 421 | 422 | ---Return normalized path. 423 | ---@param path string 424 | ---@return string 425 | function IO.normalize(path) 426 | path = sep(path) 427 | 428 | -- remove trailing slash. 429 | if #path > 1 and path:byte(-1) == bytes.slash then 430 | path = path:sub(1, -2) 431 | end 432 | 433 | -- homedir. 434 | if path:byte(1) == bytes.tilde and path:byte(2) == bytes.slash then 435 | path = (path:gsub('^~/', home)) 436 | end 437 | 438 | -- absolute. 439 | if IO.is_absolute(path) then 440 | return path 441 | end 442 | 443 | -- resolve relative path. 444 | return IO.join(IO.cwd(), path) 445 | end 446 | 447 | do 448 | local cache = { 449 | raw = nil, 450 | fix = nil 451 | } 452 | 453 | ---Return the current working directory. 454 | ---@return string 455 | function IO.cwd() 456 | local cwd = assert(uv.cwd()) 457 | if cache.raw == cwd then 458 | return cache.fix 459 | end 460 | cache.raw = cwd 461 | cache.fix = sep(cwd) 462 | return cache.fix 463 | end 464 | end 465 | 466 | do 467 | local cache_pat = {} 468 | 469 | ---Join the paths. 470 | ---@param base string 471 | ---@vararg string 472 | ---@return string 473 | function IO.join(base, ...) 474 | base = sep(base) 475 | 476 | -- remove trailing slash. 477 | -- ./ → ./ 478 | -- aaa/ → aaa 479 | if not (base == './' or base == '../') and base:byte(-1) == bytes.slash then 480 | base = base:sub(1, -2) 481 | end 482 | 483 | for i = 1, select('#', ...) do 484 | local path = sep(select(i, ...)) 485 | local path_s = 1 486 | if path:byte(path_s) == bytes.dot and path:byte(path_s + 1) == bytes.slash then 487 | path_s = path_s + 2 488 | end 489 | local up_count = 0 490 | while path:byte(path_s) == bytes.dot and path:byte(path_s + 1) == bytes.dot and path:byte(path_s + 2) == bytes.slash do 491 | up_count = up_count + 1 492 | path_s = path_s + 3 493 | end 494 | if path_s > 1 then 495 | cache_pat[path_s] = cache_pat[path_s] or ('^%s'):format(('.'):rep(path_s - 2)) 496 | end 497 | 498 | -- optimize for avoiding new string creation. 499 | if path_s == 1 then 500 | base = ('%s/%s'):format(IO.dirname(base, up_count), path) 501 | else 502 | base = path:gsub(cache_pat[path_s], IO.dirname(base, up_count)) 503 | end 504 | end 505 | return base 506 | end 507 | end 508 | 509 | ---Return the path of the current working directory. 510 | ---@param path string 511 | ---@param level? integer 512 | ---@return string 513 | function IO.dirname(path, level) 514 | path = sep(path) 515 | level = level or 1 516 | 517 | if level == 0 then 518 | return path 519 | end 520 | 521 | for i = #path - 1, 1, -1 do 522 | if path:byte(i) == bytes.slash then 523 | if level == 1 then 524 | return path:sub(1, i - 1) 525 | end 526 | level = level - 1 527 | end 528 | end 529 | return path 530 | end 531 | 532 | ---Return the path is absolute or not. 533 | ---@param path string 534 | ---@return boolean 535 | function IO.is_absolute(path) 536 | path = sep(path) 537 | return path:byte(1) == bytes.slash or path:match('^%a:/') 538 | end 539 | 540 | return IO 541 | -------------------------------------------------------------------------------- /lua/___kit___/kit/IO/init.spec.lua: -------------------------------------------------------------------------------- 1 | local IO = require('___kit___.kit.IO') 2 | 3 | local tmpdir = '/tmp/nvim-kit/IO' 4 | 5 | describe('kit.IO', function() 6 | describe('.walk', function() 7 | it('should walk directory entires (preorder)', function() 8 | local entries = {} 9 | IO.walk('./fixture/IO/scandir/a', function(_, entry) 10 | table.insert(entries, entry) 11 | end):sync(5000) 12 | assert.are.same({ 13 | { 14 | path = IO.normalize('./fixture/IO/scandir/a'), 15 | type = 'directory', 16 | }, 17 | { 18 | path = IO.normalize('./fixture/IO/scandir/a/0'), 19 | type = 'directory', 20 | }, 21 | { 22 | path = IO.normalize('./fixture/IO/scandir/a/0/1'), 23 | type = 'file', 24 | }, 25 | { 26 | path = IO.normalize('./fixture/IO/scandir/a/1'), 27 | type = 'file', 28 | }, 29 | { 30 | path = IO.normalize('./fixture/IO/scandir/a/2'), 31 | type = 'file', 32 | }, 33 | { 34 | path = IO.normalize('./fixture/IO/scandir/a/3'), 35 | type = 'file', 36 | }, 37 | }, entries) 38 | end) 39 | it('should walk directory entires (postorder)', function() 40 | local entries = {} 41 | IO.walk('./fixture/IO/scandir/a', function(_, entry) 42 | table.insert(entries, entry) 43 | end, { postorder = true }):sync(5000) 44 | assert.are.same({ 45 | { 46 | path = IO.normalize('./fixture/IO/scandir/a/0/1'), 47 | type = 'file', 48 | }, 49 | { 50 | path = IO.normalize('./fixture/IO/scandir/a/0'), 51 | type = 'directory', 52 | }, 53 | { 54 | path = IO.normalize('./fixture/IO/scandir/a/1'), 55 | type = 'file', 56 | }, 57 | { 58 | path = IO.normalize('./fixture/IO/scandir/a/2'), 59 | type = 'file', 60 | }, 61 | { 62 | path = IO.normalize('./fixture/IO/scandir/a/3'), 63 | type = 'file', 64 | }, 65 | { 66 | path = IO.normalize('./fixture/IO/scandir/a'), 67 | type = 'directory', 68 | }, 69 | }, entries) 70 | end) 71 | it('should break if found specified path (pre)', function() 72 | local count = 0 73 | IO.walk('./fixture/IO/scandir/a', function(_, entry) 74 | count = count + 1 75 | if IO.normalize('./fixture/IO/scandir/a/1') == entry.path then 76 | return IO.WalkStatus.Break 77 | end 78 | end):sync(5000) 79 | assert.equal(count, 4) 80 | end) 81 | it('should break if found specified path (post)', function() 82 | local count = 0 83 | IO.walk('./fixture/IO/scandir/a', function(_, entry) 84 | count = count + 1 85 | if IO.normalize('./fixture/IO/scandir/a/1') == entry.path then 86 | return IO.WalkStatus.Break 87 | end 88 | end, { postorder = true }):sync(5000) 89 | assert.equal(count, 3) 90 | end) 91 | end) 92 | 93 | describe('.read_file', function() 94 | it('should read the file', function() 95 | assert.are.equal( 96 | IO.read_file('./fixture/IO/read_file.txt', 5):sync(5000), 97 | table.concat({ 98 | 'read_file', 99 | 'read_file', 100 | 'read_file', 101 | 'read_file', 102 | 'read_file', 103 | 'read_file', 104 | 'read_file', 105 | 'read_file', 106 | 'read_file', 107 | 'read_file', 108 | }, '\n') 109 | ) 110 | end) 111 | end) 112 | 113 | describe('.write_file', function() 114 | it('should write the file', function() 115 | if IO.stat('./fixture/IO/write_file.txt'):catch(function() end):sync(5000) then 116 | IO.rm('./fixture/IO/write_file.txt'):sync(5000) 117 | end 118 | IO.write_file('./fixture/IO/write_file.txt', IO.read_file('./fixture/IO/read_file.txt', 5):sync(5000)) 119 | local contents1 = IO.read_file('./fixture/IO/read_file.txt', 5):sync(5000) 120 | local contents2 = IO.read_file('./fixture/IO/write_file.txt', 5):sync(5000) 121 | assert.are.equal(#contents1, #contents2) 122 | IO.rm('./fixture/IO/write_file.txt'):sync(5000) 123 | end) 124 | end) 125 | 126 | describe('.scandir', function() 127 | it('should return entries', function() 128 | local entries = IO.scandir('./fixture/IO/scandir/a'):sync(5000) 129 | assert.are.same({ 130 | { 131 | path = IO.normalize('./fixture/IO/scandir/a/0'), 132 | type = 'directory', 133 | }, 134 | { 135 | path = IO.normalize('./fixture/IO/scandir/a/1'), 136 | type = 'file', 137 | }, 138 | { 139 | path = IO.normalize('./fixture/IO/scandir/a/2'), 140 | type = 'file', 141 | }, 142 | { 143 | path = IO.normalize('./fixture/IO/scandir/a/3'), 144 | type = 'file', 145 | }, 146 | }, entries) 147 | end) 148 | end) 149 | 150 | describe('.cp', function() 151 | it('should copy directory recursively', function() 152 | IO.cp('./fixture/IO/scandir/a', './fixture/IO/scandir/b', { recursive = true }):sync(5000) 153 | assert.is_true(IO.is_directory('./fixture/IO/scandir/b'):sync(5000)) 154 | IO.rm('./fixture/IO/scandir/b', { recursive = true }):sync(5000) 155 | end) 156 | end) 157 | 158 | describe('.rm', function() 159 | it('should remove dir or file', function() 160 | local target_dir = tmpdir .. '/rm' 161 | if not IO.is_directory(target_dir):sync(5000) then 162 | IO.mkdir(target_dir, tonumber('777', 8), { recursive = true }):sync(5000) 163 | end 164 | assert.is_true(IO.is_directory(target_dir):sync(5000)) 165 | IO.rm(target_dir, { recursive = true }):sync(5000) 166 | assert.is_false(IO.is_directory(target_dir):sync(5000)) 167 | end) 168 | end) 169 | 170 | describe('.mkdir', function() 171 | it('should create dir', function() 172 | local target_dir = tmpdir .. '/mkdir' 173 | if IO.is_directory(target_dir):sync(5000) then 174 | IO.rm(target_dir, { recursive = true }):sync(5000) 175 | end 176 | assert.is_false(IO.is_directory(target_dir):sync(5000)) 177 | IO.mkdir(target_dir, tonumber('777', 8), { recursive = true }):sync(5000) 178 | assert.is_true(IO.is_directory(target_dir):sync(5000)) 179 | end) 180 | end) 181 | 182 | describe('.join', function() 183 | it('should join paths', function() 184 | assert.are.equal(IO.join('a', 'b', 'c'), 'a/b/c') 185 | end) 186 | it('should handle current directory pattern prefix', function() 187 | assert.are.equal(IO.join('a/b/c', './d', './e'), 'a/b/c/d/e') 188 | end) 189 | it('should handle parent directory pattern prefixes', function() 190 | assert.are.equal(IO.join('a/b/c', '../d'), 'a/b/d') 191 | assert.are.equal(IO.join('a/b/c', '../../d'), 'a/d') 192 | assert.are.equal(IO.join('a/b/c', '../d', '../../e'), 'a/e') 193 | end) 194 | end) 195 | end) 196 | -------------------------------------------------------------------------------- /lua/___kit___/kit/LSP/DocumentSelector.lua: -------------------------------------------------------------------------------- 1 | local LanguageId = require('___kit___.kit.LSP.LanguageId') 2 | 3 | -- NOTE 4 | --@alias ___kit___.kit.LSP.DocumentSelector ___kit___.kit.LSP.DocumentFilter[] 5 | --@alias ___kit___.kit.LSP.DocumentFilter (___kit___.kit.LSP.TextDocumentFilter | ___kit___.kit.LSP.NotebookCellTextDocumentFilter) 6 | --@alias ___kit___.kit.LSP.TextDocumentFilter ({ language: string, scheme?: string, pattern?: string } | { language?: string, scheme: string, pattern?: string } | { language?: string, scheme?: string, pattern: string }) 7 | --@class ___kit___.kit.LSP.NotebookCellTextDocumentFilter 8 | --@field public notebook (string | ___kit___.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 ___kit___.kit.LSP.NotebookDocumentFilter ({ notebookType: string, scheme?: string, pattern?: string } | { notebookType?: string, scheme: string, pattern?: string } | { notebookType?: string, scheme?: string, pattern: string }) 11 | 12 | ---@alias ___kit___.kit.LSP.DocumentSelector.NormalizedFilter { notebook_type: string?, scheme: string?, pattern: string, language: string? } 13 | 14 | ---Normalize the filter. 15 | ---@param document_filter ___kit___.kit.LSP.DocumentFilter 16 | ---@return ___kit___.kit.LSP.DocumentSelector.NormalizedFilter | nil 17 | local function normalize_filter(document_filter) 18 | if document_filter.notebook then 19 | local filter = document_filter --[[@as ___kit___.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 ___kit___.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? ___kit___.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 ___kit___.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/___kit___/kit/LSP/DocumentSelector.spec.lua: -------------------------------------------------------------------------------- 1 | local DocumentSelector = require('___kit___.kit.LSP.DocumentSelector') 2 | 3 | -- @see https://github.com/microsoft/vscode/blob/7241eea61021db926c052b657d577ef0d98f7dc7/src/vs/editor/test/common/modes/languageSelector.test.ts 4 | describe('kit.LSP.DocumentSelector', function() 5 | before_each(function() 6 | vim.cmd([[ 7 | e ./fixture/LSP/DocumentSelector/dummy.js 8 | set filetype=javascript 9 | ]]) 10 | end) 11 | 12 | it('score, any language', function() 13 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { language = 'javascript' } }), 10) 14 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { language = '*' } }), 5) 15 | end) 16 | 17 | it('score, default schemes', function() 18 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { scheme = 'file' } }), 10) 19 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { scheme = '*' } }), 5) 20 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { language = 'javascript', scheme = 'file' } }), 10) 21 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { language = 'javascript', scheme = '*' } }), 10) 22 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { language = 'javascript', scheme = '' } }), 10) 23 | end) 24 | 25 | it('score, filter', function() 26 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { pattern = '**/*.js' } }), 10) 27 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { pattern = '**/*.js', scheme = 'file' } }), 10) 28 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { pattern = '**/*.js', scheme = 'http' } }), 0) 29 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { pattern = '**/*.ts' } }), 0) 30 | assert.equal(DocumentSelector.score(vim.api.nvim_get_current_buf(), { { pattern = '**/*.ts', scheme = 'file' } }), 0) 31 | end) 32 | end) 33 | -------------------------------------------------------------------------------- /lua/___kit___/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/___kit___/kit/LSP/LanguageId.spec.lua: -------------------------------------------------------------------------------- 1 | local LanguageId = require('___kit___.kit.LSP.LanguageId') 2 | 3 | describe('kit.LSP.LanguageId', function() 4 | it('mapping', function() 5 | assert.equal(LanguageId.from_filetype('sh'), 'shellscript') 6 | assert.equal(LanguageId.from_filetype('javascript.tsx'), 'javascriptreact') 7 | assert.equal(LanguageId.from_filetype('typescript.tsx'), 'typescriptreact') 8 | end) 9 | end) 10 | -------------------------------------------------------------------------------- /lua/___kit___/kit/LSP/Position.lua: -------------------------------------------------------------------------------- 1 | local LSP = require('___kit___.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 to_encoding? ___kit___.kit.LSP.PositionEncodingKind 16 | function Position.cursor(to_encoding) 17 | local row, col = unpack(vim.api.nvim_win_get_cursor(0)) 18 | local u8 = { line = row - 1, character = col } 19 | if to_encoding == LSP.PositionEncodingKind.UTF8 then 20 | return u8 21 | end 22 | local text = vim.api.nvim_get_current_line() 23 | if to_encoding == LSP.PositionEncodingKind.UTF32 then 24 | return Position.to_utf32(text, u8, LSP.PositionEncodingKind.UTF8) 25 | end 26 | return Position.to_utf16(text, u8, LSP.PositionEncodingKind.UTF8) 27 | end 28 | 29 | ---Convert position to buffer position from specified encoding. 30 | ---@param bufnr integer 31 | ---@param position ___kit___.kit.LSP.Position 32 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 33 | function Position.to_buf(bufnr, position, from_encoding) 34 | from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16 35 | local text = vim.api.nvim_buf_get_lines(bufnr, position.line, position.line + 1, false)[1] or '' 36 | return Position.to(text, position, from_encoding, LSP.PositionEncodingKind.UTF8) 37 | end 38 | 39 | ---Convert position to utf8 from specified encoding. 40 | ---@param text string 41 | ---@param position ___kit___.kit.LSP.Position 42 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 43 | ---@return ___kit___.kit.LSP.Position 44 | function Position.to_utf8(text, position, from_encoding) 45 | from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16 46 | if from_encoding == LSP.PositionEncodingKind.UTF8 then 47 | return position 48 | end 49 | local ok, character = pcall(vim.str_byteindex, text, from_encoding, position.character) 50 | if ok then 51 | return { line = position.line, character = character } 52 | end 53 | return position 54 | end 55 | 56 | ---Convert position to utf16 from specified encoding. 57 | ---@param text string 58 | ---@param position ___kit___.kit.LSP.Position 59 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 60 | ---@return ___kit___.kit.LSP.Position 61 | function Position.to_utf16(text, position, from_encoding) 62 | local u8 = Position.to_utf8(text, position, from_encoding) 63 | for index = u8.character, 0, -1 do 64 | local ok, character = pcall(vim.str_utfindex, text, 'utf-16', index) 65 | if ok then 66 | return { line = u8.line, character = character } 67 | end 68 | end 69 | return position 70 | end 71 | 72 | ---Convert position to utf32 from specified encoding. 73 | ---@param text string 74 | ---@param position ___kit___.kit.LSP.Position 75 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 76 | ---@return ___kit___.kit.LSP.Position 77 | function Position.to_utf32(text, position, from_encoding) 78 | local u8 = Position.to_utf8(text, position, from_encoding) 79 | for index = u8.character, 0, -1 do 80 | local ok, character = pcall(vim.str_utfindex, text, 'utf-32', index) 81 | if ok then 82 | return { line = u8.line, character = character } 83 | end 84 | end 85 | return position 86 | end 87 | 88 | ---Convert position to specified encoding from specified encoding. 89 | ---@param text string 90 | ---@param position ___kit___.kit.LSP.Position 91 | ---@param from_encoding ___kit___.kit.LSP.PositionEncodingKind 92 | ---@param to_encoding ___kit___.kit.LSP.PositionEncodingKind 93 | function Position.to(text, position, from_encoding, to_encoding) 94 | if to_encoding == LSP.PositionEncodingKind.UTF8 then 95 | return Position.to_utf8(text, position, from_encoding) 96 | elseif to_encoding == LSP.PositionEncodingKind.UTF16 then 97 | return Position.to_utf16(text, position, from_encoding) 98 | elseif to_encoding == LSP.PositionEncodingKind.UTF32 then 99 | return Position.to_utf32(text, position, from_encoding) 100 | end 101 | error('LSP.Position: Unsupported encoding: ' .. to_encoding) 102 | end 103 | 104 | return Position 105 | -------------------------------------------------------------------------------- /lua/___kit___/kit/LSP/Position.spec.lua: -------------------------------------------------------------------------------- 1 | local LSP = require('___kit___.kit.LSP') 2 | local Position = require('___kit___.kit.LSP.Position') 3 | 4 | describe('kit.LSP.Position', function() 5 | local text = '🗿🗿🗿' 6 | local utf8_character = #text 7 | local utf16_character = vim.str_utfindex(text, 'utf-16', utf8_character) 8 | local utf32_character = vim.str_utfindex(text, 'utf-32', utf8_character) 9 | 10 | before_each(function() 11 | vim.cmd(([[ 12 | enew! 13 | setlocal noswapfile 14 | setlocal virtualedit=onemore 15 | call setline(1, ['%s']) 16 | call cursor(1, %s + 1) 17 | ]]):format(text, #text)) 18 | end) 19 | 20 | describe('.is', function() 21 | it('should accept position', function() 22 | local position = { line = 0, character = 0 } 23 | assert.is_true(Position.is(position)) 24 | assert.is_false(Position.is(nil)) 25 | assert.is_false(Position.is(vim.NIL)) 26 | assert.is_false(Position.is(true)) 27 | assert.is_false(Position.is(1)) 28 | assert.is_false(Position.is('1')) 29 | end) 30 | end) 31 | 32 | describe('.cursor', function() 33 | it('should return utf8 position', function() 34 | vim.api.nvim_buf_set_lines(0, 0, -1, false, { text }) 35 | vim.api.nvim_win_set_cursor(0, { 1, #text }) 36 | assert.equal(Position.cursor(LSP.PositionEncodingKind.UTF8).character, utf8_character) 37 | end) 38 | it('should return utf16 position', function() 39 | vim.api.nvim_buf_set_lines(0, 0, -1, false, { text }) 40 | vim.api.nvim_win_set_cursor(0, { 1, #text }) 41 | assert.equal(Position.cursor(LSP.PositionEncodingKind.UTF16).character, utf16_character) 42 | end) 43 | it('should return utf32 position', function() 44 | vim.api.nvim_buf_set_lines(0, 0, -1, false, { text }) 45 | vim.api.nvim_win_set_cursor(0, { 1, #text }) 46 | assert.equal(Position.cursor(LSP.PositionEncodingKind.UTF32).character, utf32_character) 47 | end) 48 | end) 49 | 50 | for _, to in ipairs({ 51 | { 52 | method = 'to_utf8', 53 | encoding = LSP.PositionEncodingKind.UTF8, 54 | character = utf8_character, 55 | }, 56 | { 57 | method = 'to_utf16', 58 | encoding = LSP.PositionEncodingKind.UTF16, 59 | character = utf16_character, 60 | }, 61 | { 62 | method = 'to_utf32', 63 | encoding = LSP.PositionEncodingKind.UTF32, 64 | character = utf32_character, 65 | }, 66 | }) do 67 | for _, from in ipairs({ 68 | { character = utf8_character, encoding = LSP.PositionEncodingKind.UTF8 }, 69 | { character = utf16_character, encoding = LSP.PositionEncodingKind.UTF16 }, 70 | { character = utf32_character, encoding = LSP.PositionEncodingKind.UTF32 }, 71 | }) do 72 | it(('should convert %s <- %s'):format(to.encoding, from.encoding), function() 73 | local converted1 = Position[to.method](text, { line = 1, character = from.character }, from.encoding) 74 | assert.are.same(to.character, converted1.character) 75 | assert.is_true(Position.is(converted1)) 76 | local converted2 = Position.to(text, { line = 1, character = from.character }, from.encoding, to.encoding) 77 | assert.are.same(to.character, converted2.character) 78 | assert.is_true(Position.is(converted2)) 79 | end) 80 | end 81 | end 82 | end) 83 | -------------------------------------------------------------------------------- /lua/___kit___/kit/LSP/Range.lua: -------------------------------------------------------------------------------- 1 | local Position = require('___kit___.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 ___kit___.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 ___kit___.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 ___kit___.kit.LSP.Range 29 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 30 | ---@return ___kit___.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 ___kit___.kit.LSP.Range 41 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 42 | ---@return ___kit___.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 ___kit___.kit.LSP.Range 53 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 54 | ---@return ___kit___.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 ___kit___.kit.LSP.Range 65 | ---@param from_encoding? ___kit___.kit.LSP.PositionEncodingKind 66 | ---@return ___kit___.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/___kit___/kit/LSP/Range.spec.lua: -------------------------------------------------------------------------------- 1 | local Range = require('___kit___.kit.LSP.Range') 2 | 3 | describe('kit.LSP.Range', function() 4 | it('should return the range is empty or not', function() 5 | local position1 = { line = 0, character = 0 } 6 | local position2 = { line = 0, character = 1 } 7 | assert.are.equal(Range.empty({ start = position1, ['end'] = position1 }), true) 8 | assert.are.equal(Range.empty({ start = position1, ['end'] = position2 }), false) 9 | end) 10 | end) 11 | -------------------------------------------------------------------------------- /lua/___kit___/kit/RPC/JSON/init.lua: -------------------------------------------------------------------------------- 1 | local kit = require('___kit___.kit') 2 | local Async = require('___kit___.kit.Async') 3 | 4 | ---@class ___kit___.kit.RPC.JSON.Transport 5 | ---@field send fun(self: ___kit___.kit.RPC.JSON.Transport, data: table): ___kit___.kit.Async.AsyncTask 6 | ---@field on_message fun(self: ___kit___.kit.RPC.JSON.Transport, callback: fun(data: table)) 7 | ---@field start fun(self: ___kit___.kit.RPC.JSON.Transport) 8 | ---@field close fun(self: ___kit___.kit.RPC.JSON.Transport): ___kit___.kit.Async.AsyncTask 9 | 10 | ---@class ___kit___.kit.RPC.JSON.Transport.LineDelimitedPipe: ___kit___.kit.RPC.JSON.Transport 11 | ---@field private _buffer ___kit___.kit.buffer.Buffer 12 | ---@field private _reader uv.uv_pipe_t 13 | ---@field private _writer uv.uv_pipe_t 14 | ---@field private _on_message fun(data: table) 15 | local LineDelimitedPipe = {} 16 | LineDelimitedPipe.__index = LineDelimitedPipe 17 | 18 | ---Create new LineDelimitedPipe instance. 19 | ---@param reader uv.uv_pipe_t 20 | ---@param writer uv.uv_pipe_t 21 | function LineDelimitedPipe.new(reader, writer) 22 | return setmetatable({ 23 | _buffer = kit.buffer(), 24 | _reader = reader, 25 | _writer = writer, 26 | _on_message = nil, 27 | }, LineDelimitedPipe) 28 | end 29 | 30 | ---Send data. 31 | ---@param message table 32 | ---@return ___kit___.kit.Async.AsyncTask 33 | function LineDelimitedPipe:send(message) 34 | return Async.new(function(resolve, reject) 35 | self._writer:write(vim.json.encode(message) .. '\n', function(err) 36 | if err then 37 | return reject(err) 38 | else 39 | resolve() 40 | end 41 | end) 42 | end) 43 | end 44 | 45 | ---Set message callback. 46 | ---@param callback fun(data: table) 47 | function LineDelimitedPipe:on_message(callback) 48 | self._on_message = callback 49 | end 50 | 51 | ---Start transport. 52 | function LineDelimitedPipe:start() 53 | self._reader:read_start(function(err, data) 54 | if err then 55 | return 56 | end 57 | self._buffer.put(data) 58 | 59 | local found = data:find('\n', 1, true) 60 | if found then 61 | for i, byte in self._buffer.iter_bytes() do 62 | if byte == 10 then 63 | local message = vim.json.decode(self._buffer.get(i - 1), { object = true, array = true }) 64 | self._buffer.skip(1) 65 | self._on_message(message) 66 | end 67 | end 68 | end 69 | end) 70 | end 71 | 72 | ---Close transport. 73 | ---@return ___kit___.kit.Async.AsyncTask 74 | function LineDelimitedPipe:close() 75 | self._reader:read_stop() 76 | 77 | local p = Async.resolve() 78 | p = p:next(function() 79 | if not self._reader:is_closing() and self._reader:is_active() then 80 | return Async.new(function(resolve) 81 | self._reader:close(resolve) 82 | end) 83 | end 84 | end) 85 | p = p:next(function() 86 | if not self._writer:is_closing() and self._writer:is_active() then 87 | return Async.new(function(resolve) 88 | self._writer:close(resolve) 89 | end) 90 | end 91 | end) 92 | return p 93 | end 94 | 95 | ---@class ___kit___.kit.RPC.JSON.RPC 96 | ---@field private _transport ___kit___.kit.RPC.JSON.Transport 97 | ---@field private _next_requet_id number 98 | ---@field private _pending_callbacks table 99 | ---@field private _on_request_map table 100 | ---@field private _on_notification_map table 101 | local RPC = { 102 | Transport = { 103 | LineDelimitedPipe = LineDelimitedPipe, 104 | }, 105 | } 106 | RPC.__index = RPC 107 | 108 | ---Create new RPC instance. 109 | ---@param params { transport: ___kit___.kit.RPC.JSON.Transport } 110 | function RPC.new(params) 111 | return setmetatable({ 112 | _transport = params.transport, 113 | _next_requet_id = 0, 114 | _pending_callbacks = {}, 115 | _on_request_map = {}, 116 | _on_notification_map = {}, 117 | }, RPC) 118 | end 119 | 120 | ---Start RPC. 121 | function RPC:start() 122 | self._transport:on_message(function(data) 123 | if data.id then 124 | if data.method then 125 | -- request. 126 | local request_callback = self._on_request_map[data.method] 127 | if request_callback then 128 | Async.resolve():next(function() 129 | return request_callback(data) 130 | end):dispatch(function(res) 131 | -- request success. 132 | self._transport:send({ 133 | jsonrpc = '2.0', 134 | id = data.id, 135 | result = res, 136 | }) 137 | end, function(err) 138 | -- request failure. 139 | self._transport:send({ 140 | jsonrpc = '2.0', 141 | id = data.id, 142 | error = { 143 | code = -32603, 144 | message = tostring(err), 145 | }, 146 | }) 147 | end) 148 | else 149 | -- request not found. 150 | self._transport:send({ 151 | jsonrpc = "2.0", 152 | id = data.id, 153 | error = { 154 | code = -32601, 155 | message = ('Method not found: %s'):format(data.method), 156 | }, 157 | }) 158 | end 159 | else 160 | -- response. 161 | local pending_callback = self._pending_callbacks[data.id] 162 | if pending_callback then 163 | pending_callback(data) 164 | self._pending_callbacks[data.id] = nil 165 | end 166 | end 167 | else 168 | -- notification. 169 | local notification_callbacks = self._on_notification_map[data.method] 170 | if notification_callbacks then 171 | for _, callback in ipairs(notification_callbacks) do 172 | pcall(callback, { params = data.params }) 173 | end 174 | end 175 | end 176 | end) 177 | self._transport:start() 178 | end 179 | 180 | ---Close RPC. 181 | ---@return ___kit___.kit.Async.AsyncTask 182 | function RPC:close() 183 | return self._transport:close() 184 | end 185 | 186 | ---Set request callback. 187 | ---@param method string 188 | ---@param callback fun(ctx: { params: table }): table 189 | function RPC:on_request(method, callback) 190 | if self._on_request_map[method] then 191 | error('Method already exists: ' .. method) 192 | end 193 | self._on_request_map[method] = callback 194 | end 195 | 196 | ---Set notification callback. 197 | ---@param method string 198 | ---@param callback fun(ctx: { params: table }) 199 | function RPC:on_notification(method, callback) 200 | if not self._on_notification_map[method] then 201 | self._on_notification_map[method] = {} 202 | end 203 | table.insert(self._on_notification_map[method], callback) 204 | end 205 | 206 | ---Request. 207 | ---@param method string 208 | ---@param params table 209 | ---@return ___kit___.kit.Async.AsyncTask| { cancel: fun() } 210 | function RPC:request(method, params) 211 | self._next_requet_id = self._next_requet_id + 1 212 | 213 | local request_id = self._next_requet_id 214 | 215 | local p = Async.new(function(resolve, reject) 216 | self._pending_callbacks[request_id] = function(response) 217 | if response.error then 218 | reject(response.error) 219 | else 220 | resolve(response.result) 221 | end 222 | end 223 | self._transport:send({ 224 | jsonrpc = '2.0', 225 | id = request_id, 226 | method = method, 227 | params = params, 228 | }) 229 | end) 230 | 231 | ---@diagnostic disable-next-line: inject-field 232 | p.cancel = function() 233 | self._pending_callbacks[request_id] = nil 234 | end 235 | 236 | return p 237 | end 238 | 239 | ---Notify. 240 | ---@param method string 241 | ---@param params table 242 | function RPC:notify(method, params) 243 | self._transport:send({ 244 | jsonrpc = '2.0', 245 | method = method, 246 | params = params, 247 | }) 248 | end 249 | 250 | return RPC 251 | -------------------------------------------------------------------------------- /lua/___kit___/kit/RPC/JSON/init.spec.lua: -------------------------------------------------------------------------------- 1 | local RPC = require('___kit___.kit.RPC.JSON') 2 | local Async = require('___kit___.kit.Async') 3 | 4 | describe('kit.RPC.JSON', function() 5 | local function create_pair() 6 | local fds_a = assert(vim.uv.socketpair(nil, nil, { nonblock = true }, { nonblock = true })) 7 | local fd_a1 = assert(vim.uv.new_tcp()) 8 | fd_a1:open(fds_a[1]) 9 | local fd_a2 = assert(vim.uv.new_tcp()) 10 | fd_a2:open(fds_a[2]) 11 | 12 | local fds_b = assert(vim.uv.socketpair(nil, nil, { nonblock = true }, { nonblock = true })) 13 | local fd_b1 = assert(vim.uv.new_tcp()) 14 | fd_b1:open(fds_b[1]) 15 | local fd_b2 = assert(vim.uv.new_tcp()) 16 | fd_b2:open(fds_b[2]) 17 | 18 | local a = RPC.new({ transport = RPC.Transport.LineDelimitedPipe.new(fd_a1, fd_b2) }) 19 | a:start() 20 | local b = RPC.new({ transport = RPC.Transport.LineDelimitedPipe.new(fd_b1, fd_a2) }) 21 | b:start() 22 | return a, b 23 | end 24 | 25 | it('should work with request', function() 26 | local a, b = create_pair() 27 | b:on_request('sum', function(ctx) 28 | return ctx.params.a + ctx.params.b 29 | end) 30 | 31 | assert.are.equal(a:request('sum', { 32 | a = 1, 33 | b = 2 34 | }):sync(200), 3) 35 | end) 36 | 37 | it('should work with notification', function() 38 | local a, b = create_pair() 39 | 40 | Async.new(function(resolve) 41 | b:on_notification('notification', resolve) 42 | a:notify('notification', {}) 43 | end):sync(200) 44 | end) 45 | end) 46 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Spec/init.lua: -------------------------------------------------------------------------------- 1 | local kit = require('___kit___.kit') 2 | local assert = require('luassert') 3 | 4 | ---@class ___kit___.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? ___kit___.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/___kit___/kit/System/init.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 212 2 | 3 | local kit = require('___kit___.kit') 4 | local Async = require('___kit___.kit.Async') 5 | 6 | local bytes = { 7 | ['\n'] = 10, 8 | ['\r'] = 13, 9 | } 10 | 11 | local System = {} 12 | 13 | ---@class ___kit___.kit.System.Buffer 14 | ---@field write fun(data: string) 15 | ---@field close fun() 16 | 17 | ---@class ___kit___.kit.System.Buffering 18 | ---@field create fun(self: any, callback: fun(data: string)): ___kit___.kit.System.Buffer 19 | 20 | ---@class ___kit___.kit.System.LineBuffering: ___kit___.kit.System.Buffering 21 | ---@field ignore_empty boolean 22 | System.LineBuffering = {} 23 | System.LineBuffering.__index = System.LineBuffering 24 | 25 | ---Create LineBuffering. 26 | ---@param option { ignore_empty?: boolean } 27 | function System.LineBuffering.new(option) 28 | return setmetatable({ 29 | ignore_empty = option.ignore_empty or false, 30 | }, System.LineBuffering) 31 | end 32 | 33 | ---Create LineBuffer object. 34 | ---@param callback fun(data: string) 35 | function System.LineBuffering:create(callback) 36 | local callback_wrapped = callback 37 | if self.ignore_empty then 38 | ---@param data string 39 | function callback_wrapped(data) 40 | if data ~= '' then 41 | return callback(data) 42 | end 43 | end 44 | end 45 | 46 | local buffer = kit.buffer() 47 | local iter = buffer.iter_bytes() 48 | ---@type ___kit___.kit.System.Buffer 49 | return { 50 | write = function(data) 51 | buffer.put(data) 52 | local found = true 53 | while found do 54 | found = false 55 | for i, byte in iter do 56 | if byte == bytes['\n'] then 57 | if buffer.peek(i - 1) == bytes['\r'] then 58 | callback_wrapped(buffer.get(i - 2)) 59 | buffer.skip(2) 60 | else 61 | callback_wrapped(buffer.get(i - 1)) 62 | buffer.skip(1) 63 | end 64 | iter = buffer.iter_bytes() 65 | found = true 66 | break 67 | end 68 | end 69 | if not found then 70 | break 71 | end 72 | end 73 | end, 74 | close = function() 75 | for byte, i in buffer.iter_bytes() do 76 | if byte == bytes['\n'] then 77 | if buffer.peek(i - 1) == bytes['\r'] then 78 | callback_wrapped(buffer.get(i - 2)) 79 | buffer.skip(2) 80 | else 81 | callback_wrapped(buffer.get(i - 1)) 82 | buffer.skip(1) 83 | end 84 | end 85 | end 86 | callback_wrapped(buffer.get()) 87 | end, 88 | } 89 | end 90 | 91 | ---@class ___kit___.kit.System.DelimiterBuffering: ___kit___.kit.System.Buffering 92 | ---@field delimiter string 93 | System.DelimiterBuffering = {} 94 | System.DelimiterBuffering.__index = System.DelimiterBuffering 95 | 96 | ---Create Buffering. 97 | ---@param option { delimiter: string } 98 | function System.DelimiterBuffering.new(option) 99 | return setmetatable({ 100 | delimiter = option.delimiter, 101 | }, System.DelimiterBuffering) 102 | end 103 | 104 | ---Create Delimiter object. 105 | function System.DelimiterBuffering:create(callback) 106 | local state = { 107 | buffer = {}, 108 | buffer_pos = 1, 109 | delimiter_pos = 1, 110 | match_pos = nil --[[@as integer?]], 111 | } 112 | 113 | local function len() 114 | local l = 0 115 | for i = 1, #state.buffer do 116 | l = l + #state.buffer[i] 117 | end 118 | return l 119 | end 120 | 121 | local function split(s, e) 122 | local before = {} 123 | local after = {} 124 | local off = 0 125 | for i = 1, #state.buffer do 126 | local l = #state.buffer[i] 127 | local sep_s = s - off 128 | local sep_e = e - off 129 | local buf_s = 1 130 | local buf_e = l 131 | 132 | if buf_e < sep_s then 133 | table.insert(before, state.buffer[i]) 134 | elseif sep_e < buf_s then 135 | table.insert(after, state.buffer[i]) 136 | else 137 | if buf_s < sep_s then 138 | table.insert(before, state.buffer[i]:sub(buf_s, sep_s - 1)) 139 | end 140 | if sep_e < buf_e then 141 | table.insert(after, state.buffer[i]:sub(sep_e + 1, buf_e)) 142 | end 143 | end 144 | 145 | off = off + l 146 | end 147 | return before, after 148 | end 149 | 150 | local function get(at) 151 | local off = 0 152 | for i = 1, #state.buffer do 153 | local l = #state.buffer[i] 154 | if at <= off + l then 155 | local idx = at - off 156 | return state.buffer[i]:sub(idx, idx) 157 | end 158 | off = off + l 159 | end 160 | return nil 161 | end 162 | 163 | local buffer_len = 0 164 | local delimiter_len = #self.delimiter 165 | local buffer 166 | buffer = { 167 | write = function(data) 168 | table.insert(state.buffer, data) 169 | buffer_len = len() 170 | 171 | while state.buffer_pos <= buffer_len do 172 | local b = get(state.buffer_pos) 173 | local d = self.delimiter:sub(state.delimiter_pos, state.delimiter_pos) 174 | if b == d then 175 | if state.delimiter_pos == delimiter_len then 176 | local before, after = split(state.match_pos, state.buffer_pos) 177 | callback(table.concat(before, '')) 178 | state.buffer = after 179 | state.buffer_pos = 1 180 | state.delimiter_pos = 1 181 | state.match_pos = nil 182 | buffer_len = len() 183 | else 184 | if state.delimiter_pos == 1 then 185 | state.match_pos = state.buffer_pos 186 | end 187 | state.buffer_pos = state.buffer_pos + 1 188 | state.delimiter_pos = state.delimiter_pos + 1 189 | end 190 | else 191 | state.buffer_pos = state.match_pos and state.match_pos + 1 or state.buffer_pos + 1 192 | state.delimiter_pos = 1 193 | state.match_pos = nil 194 | end 195 | end 196 | end, 197 | close = function() 198 | if #state.buffer > 0 then 199 | callback(table.concat(state.buffer, '')) 200 | end 201 | end, 202 | } 203 | return buffer 204 | end 205 | 206 | ---@class ___kit___.kit.System.RawBuffering: ___kit___.kit.System.Buffering 207 | System.RawBuffering = {} 208 | System.RawBuffering.__index = System.RawBuffering 209 | 210 | ---Create RawBuffering. 211 | function System.RawBuffering.new() 212 | return setmetatable({}, System.RawBuffering) 213 | end 214 | 215 | ---Create RawBuffer object. 216 | function System.RawBuffering:create(callback) 217 | return { 218 | write = function(data) 219 | callback(data) 220 | end, 221 | close = function() 222 | -- noop. 223 | end, 224 | } 225 | end 226 | 227 | ---Spawn a new process. 228 | ---@class ___kit___.kit.System.SpawnParams 229 | ---@field cwd string 230 | ---@field env? table 231 | ---@field input? string|string[] 232 | ---@field on_stdout? fun(data: string) 233 | ---@field on_stderr? fun(data: string) 234 | ---@field on_exit? fun(code: integer, signal: integer) 235 | ---@field buffering? ___kit___.kit.System.Buffering 236 | ---@param command string[] 237 | ---@param params ___kit___.kit.System.SpawnParams 238 | ---@return fun(signal?: integer) 239 | function System.spawn(command, params) 240 | command = vim 241 | .iter(command) 242 | :filter(function(c) 243 | return c ~= nil 244 | end) 245 | :totable() 246 | 247 | local cmd = command[1] 248 | local args = {} 249 | for i = 2, #command do 250 | table.insert(args, command[i]) 251 | end 252 | 253 | local env = params.env 254 | if not env then 255 | env = vim.fn.environ() 256 | env.NVIM = vim.v.servername 257 | env.NVIM_LISTEN_ADDRESS = nil 258 | end 259 | 260 | local env_pairs = {} 261 | for k, v in pairs(env) do 262 | table.insert(env_pairs, string.format('%s=%s', k, tostring(v))) 263 | end 264 | 265 | local buffering = params.buffering or System.RawBuffering.new() 266 | local stdout_buffer = buffering:create(function(text) 267 | if params.on_stdout then 268 | params.on_stdout(text) 269 | end 270 | end) 271 | local stderr_buffer = buffering:create(function(text) 272 | if params.on_stderr then 273 | params.on_stderr(text) 274 | end 275 | end) 276 | 277 | local close --[[@type fun(signal?: integer): ___kit___.kit.Async.AsyncTask]] 278 | local stdin = params.input and assert(vim.uv.new_pipe()) 279 | local stdout = assert(vim.uv.new_pipe()) 280 | local stderr = assert(vim.uv.new_pipe()) 281 | local process = vim.uv.spawn(vim.fn.exepath(cmd), { 282 | cwd = vim.fs.normalize(params.cwd), 283 | env = env_pairs, 284 | hide = true, 285 | args = args, 286 | stdio = { stdin, stdout, stderr }, 287 | detached = false, 288 | verbatim = false, 289 | } --[[@as any]], function(code, signal) 290 | stdout_buffer.close() 291 | stderr_buffer.close() 292 | close():next(function() 293 | if params.on_exit then 294 | params.on_exit(code, signal) 295 | end 296 | end) 297 | end) 298 | stdout:read_start(function(err, data) 299 | if err then 300 | error(err) 301 | end 302 | if data then 303 | stdout_buffer.write(data) 304 | end 305 | end) 306 | stderr:read_start(function(err, data) 307 | if err then 308 | error(err) 309 | end 310 | if data then 311 | stderr_buffer.write(data) 312 | end 313 | end) 314 | 315 | local stdin_closing = Async.new(function(resolve) 316 | if stdin then 317 | for _, input in ipairs(kit.to_array(params.input)) do 318 | stdin:write(input) 319 | end 320 | stdin:shutdown(function() 321 | stdin:close(resolve) 322 | end) 323 | else 324 | resolve() 325 | end 326 | end) 327 | 328 | close = function(signal) 329 | local closing = { stdin_closing } 330 | table.insert( 331 | closing, 332 | Async.new(function(resolve) 333 | if not stdout:is_closing() then 334 | stdout:close(resolve) 335 | else 336 | resolve() 337 | end 338 | end) 339 | ) 340 | table.insert( 341 | closing, 342 | Async.new(function(resolve) 343 | if not stderr:is_closing() then 344 | stderr:close(resolve) 345 | else 346 | resolve() 347 | end 348 | end) 349 | ) 350 | table.insert( 351 | closing, 352 | Async.new(function(resolve) 353 | if signal and process:is_active() then 354 | process:kill(signal) 355 | end 356 | if process and not process:is_closing() then 357 | process:close(resolve) 358 | else 359 | resolve() 360 | end 361 | end) 362 | ) 363 | 364 | local closing_task = Async.resolve() 365 | for _, task in ipairs(closing) do 366 | closing_task = closing_task:next(function() 367 | return task 368 | end) 369 | end 370 | return closing_task 371 | end 372 | 373 | return function(signal) 374 | close(signal) 375 | end 376 | end 377 | 378 | return System 379 | -------------------------------------------------------------------------------- /lua/___kit___/kit/System/init.spec.lua: -------------------------------------------------------------------------------- 1 | local System = require('___kit___.kit.System') 2 | 3 | describe('kit.System', function() 4 | describe('LineBuffering', function() 5 | it('should buffering by line (no ignore empty)', function() 6 | local expects = { 7 | '1', 8 | '', 9 | '2', 10 | '', 11 | '', 12 | '3', 13 | '', 14 | '', 15 | } 16 | local c = 1 17 | local buffer = System.LineBuffering.new({ 18 | ignore_empty = false 19 | }):create(function(text) 20 | assert.are.equal(text, expects[c]) 21 | c = c + 1 22 | end) 23 | buffer.write('1\n') 24 | buffer.write('\n') 25 | buffer.write('2') 26 | buffer.write('\n\n') 27 | buffer.write('\n3') 28 | buffer.write('\n') 29 | buffer.write('\n') 30 | buffer.close() 31 | assert.are.equal(c, 9) 32 | end) 33 | 34 | it('should buffering by line (ignore empty)', function() 35 | local expects = { 36 | '1', 37 | '2', 38 | '3', 39 | } 40 | local c = 1 41 | local buffer = System.LineBuffering.new({ 42 | ignore_empty = true 43 | }):create(function(text) 44 | assert.are.equal(text, expects[c]) 45 | c = c + 1 46 | end) 47 | buffer.write('1\n') 48 | buffer.write('\n') 49 | buffer.write('2') 50 | buffer.write('\n\n') 51 | buffer.write('\n3') 52 | buffer.close() 53 | end) 54 | end) 55 | describe('DelimiterBuffering', function() 56 | it('should buffering by delimiter', function() 57 | local buffer, consumed 58 | consumed = {} 59 | buffer = System.DelimiterBuffering.new({ delimiter = '\t\t\n' }):create(function(text) 60 | table.insert(consumed, text) 61 | end) 62 | buffer.write('1') 63 | buffer.write('\t\t\n2\t') 64 | buffer.write('\t\n3\t\t') 65 | buffer.write('\n4\t\t\n') 66 | buffer.write('5') 67 | buffer.write('\t\t\n') 68 | buffer.write('6') 69 | buffer.write('\t\t\n') 70 | buffer.write('\t') 71 | buffer.write('\t') 72 | buffer.write('\n') 73 | buffer.write('7') 74 | buffer.close() 75 | assert.are.same(consumed, { '1', '2', '3', '4', '5', '6', '', '7' }) 76 | 77 | consumed = {} 78 | buffer = System.DelimiterBuffering.new({ delimiter = '\t\t\n' }):create(function(text) 79 | table.insert(consumed, text) 80 | end) 81 | buffer.write('1\t\t\n2\t\t\n3\t\t\n4\t\t\n5\t\t\n6\t\t\n\t\t\n7') 82 | buffer.close() 83 | assert.are.same(consumed, { '1', '2', '3', '4', '5', '6', '', '7' }) 84 | end) 85 | end) 86 | end) 87 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Vim/FloatingWindow.lua: -------------------------------------------------------------------------------- 1 | local kit = require('___kit___.kit') 2 | 3 | ---@alias ___kit___.kit.Vim.FloatingWindow.WindowKind 'main' | 'scrollbar_track' | 'scrollbar_thumb' 4 | 5 | ---@class ___kit___.kit.Vim.FloatingWindow.BorderSize 6 | ---@field public top integer 7 | ---@field public left integer 8 | ---@field public right integer 9 | ---@field public bottom integer 10 | ---@field public h integer 11 | ---@field public v integer 12 | 13 | ---@class ___kit___.kit.Vim.FloatingWindow.ContentSize 14 | ---@field public width integer 15 | ---@field public height integer 16 | 17 | ---@class ___kit___.kit.Vim.FloatingWindow.WindowConfig 18 | ---@field public row integer 0-indexed utf-8 19 | ---@field public col integer 0-indexed utf-8 20 | ---@field public width integer 21 | ---@field public height integer 22 | ---@field public border? string | string[] 23 | ---@field public anchor? "NW" | "NE" | "SW" | "SE" 24 | ---@field public style? string 25 | ---@field public title? string 26 | ---@field public title_pos? 'left' | 'right' | 'center' 27 | ---@field public footer? string 28 | ---@field public footer_pos? 'left' | 'right' | 'center' 29 | ---@field public zindex? integer 30 | 31 | ---@class ___kit___.kit.Vim.FloatingWindow.Viewport 32 | ---@field public row integer 33 | ---@field public col integer 34 | ---@field public inner_width integer window inner width 35 | ---@field public inner_height integer window inner height 36 | ---@field public outer_width integer window outer width that includes border and scrollbar width 37 | ---@field public outer_height integer window outer height that includes border width 38 | ---@field public border_size ___kit___.kit.Vim.FloatingWindow.BorderSize 39 | ---@field public content_size ___kit___.kit.Vim.FloatingWindow.ContentSize 40 | ---@field public scrollbar boolean 41 | ---@field public ui_width integer 42 | ---@field public ui_height integer 43 | ---@field public border string | string[] | nil 44 | ---@field public zindex integer 45 | 46 | ---@class ___kit___.kit.Vim.FloatingWindow.Config 47 | ---@field public markdown? boolean 48 | 49 | ---@class ___kit___.kit.Vim.FloatingWindow 50 | ---@field private _augroup string 51 | ---@field private _config ___kit___.kit.Vim.FloatingWindow.Config 52 | ---@field private _buf_option table 53 | ---@field private _win_option table 54 | ---@field private _buf integer 55 | ---@field private _scrollbar_track_buf integer 56 | ---@field private _scrollbar_thumb_buf integer 57 | ---@field private _win? integer 58 | ---@field private _scrollbar_track_win? integer 59 | ---@field private _scrollbar_thumb_win? integer 60 | local FloatingWindow = {} 61 | FloatingWindow.__index = FloatingWindow 62 | 63 | ---Returns true if the window is visible 64 | ---@param win? integer 65 | ---@return boolean 66 | local function is_visible(win) 67 | if not win then 68 | return false 69 | end 70 | if not vim.api.nvim_win_is_valid(win) then 71 | return false 72 | end 73 | return true 74 | end 75 | 76 | ---Show the window 77 | ---@param win? integer 78 | ---@param buf integer 79 | ---@param win_config ___kit___.kit.Vim.FloatingWindow.WindowConfig 80 | ---@return integer 81 | local function show_or_move(win, buf, win_config) 82 | win_config.border = win_config.border or (vim.o.winborder --[[@as string]]) 83 | 84 | local border_size = FloatingWindow.get_border_size(win_config.border) 85 | if win_config.anchor == 'NE' then 86 | win_config.col = win_config.col - win_config.width - border_size.right - border_size.left 87 | elseif win_config.anchor == 'SW' then 88 | win_config.row = win_config.row - win_config.height - border_size.top - border_size.bottom 89 | elseif win_config.anchor == 'SE' then 90 | win_config.row = win_config.row - win_config.height - border_size.top - border_size.bottom 91 | win_config.col = win_config.col - win_config.width - border_size.right - border_size.left 92 | end 93 | win_config.anchor = 'NW' 94 | 95 | if is_visible(win) then 96 | vim.api.nvim_win_set_config(win --[=[@as integer]=], { 97 | relative = 'editor', 98 | row = win_config.row, 99 | col = win_config.col, 100 | width = win_config.width, 101 | height = win_config.height, 102 | anchor = 'NW', 103 | style = win_config.style, 104 | border = win_config.border, 105 | title = win_config.title, 106 | title_pos = win_config.title_pos, 107 | footer = win_config.footer, 108 | footer_pos = win_config.footer_pos, 109 | zindex = win_config.zindex, 110 | }) 111 | return win --[=[@as integer]=] 112 | else 113 | return vim.api.nvim_open_win(buf, false, { 114 | noautocmd = true, 115 | relative = 'editor', 116 | row = win_config.row, 117 | col = win_config.col, 118 | width = win_config.width, 119 | height = win_config.height, 120 | anchor = 'NW', 121 | style = win_config.style, 122 | border = win_config.border, 123 | title = win_config.title, 124 | title_pos = win_config.title_pos, 125 | footer = win_config.footer, 126 | footer_pos = win_config.footer_pos, 127 | zindex = win_config.zindex, 128 | }) 129 | end 130 | end 131 | 132 | ---Hide the window 133 | ---@param win integer 134 | local function hide(win) 135 | if is_visible(win) then 136 | vim.api.nvim_win_hide(win) 137 | end 138 | end 139 | 140 | ---Get border size. 141 | ---@param border nil | string | string[] 142 | ---@return ___kit___.kit.Vim.FloatingWindow.BorderSize 143 | function FloatingWindow.get_border_size(border) 144 | border = border or (vim.o.winborder --[[@as string]]) 145 | 146 | local maybe_border_size = (function() 147 | if not border or border == '' then 148 | return { top = 0, right = 0, bottom = 0, left = 0 } 149 | end 150 | if type(border) == 'string' then 151 | if border == 'none' then 152 | return { top = 0, right = 0, bottom = 0, left = 0 } 153 | elseif border == 'single' then 154 | return { top = 1, right = 1, bottom = 1, left = 1 } 155 | elseif border == 'double' then 156 | return { top = 2, right = 2, bottom = 2, left = 2 } 157 | elseif border == 'rounded' then 158 | return { top = 1, right = 1, bottom = 1, left = 1 } 159 | elseif border == 'solid' then 160 | return { top = 1, right = 1, bottom = 1, left = 1 } 161 | elseif border == 'shadow' then 162 | return { top = 0, right = 1, bottom = 1, left = 0 } 163 | end 164 | return { top = 0, right = 0, bottom = 0, left = 0 } 165 | end 166 | local chars = border --[=[@as string[]]=] 167 | while #chars < 8 do 168 | chars = kit.concat(chars, chars) 169 | end 170 | return { 171 | top = vim.api.nvim_strwidth(chars[2]), 172 | right = vim.api.nvim_strwidth(chars[4]), 173 | bottom = vim.api.nvim_strwidth(chars[6]), 174 | left = vim.api.nvim_strwidth(chars[8]), 175 | } 176 | end)() 177 | maybe_border_size.v = maybe_border_size.top + maybe_border_size.bottom 178 | maybe_border_size.h = maybe_border_size.left + maybe_border_size.right 179 | return maybe_border_size 180 | end 181 | 182 | ---Get content size. 183 | ---@param params { bufnr: integer, wrap: boolean, max_inner_width: integer, markdown?: boolean } 184 | ---@return ___kit___.kit.Vim.FloatingWindow.ContentSize 185 | function FloatingWindow.get_content_size(params) 186 | --- compute content width. 187 | local content_width --[=[@as integer]=] 188 | do 189 | local max_text_width = 0 190 | for _, text in ipairs(vim.api.nvim_buf_get_lines(params.bufnr, 0, -1, false)) do 191 | local text_width = math.max(1, vim.api.nvim_strwidth(text)) 192 | if params.markdown then 193 | local j = 1 194 | local s, e = text:find('%b[]%b()', j) 195 | if s then 196 | text_width = text_width - (#text:match('%b[]', j) - 2) 197 | j = e + 1 198 | end 199 | end 200 | max_text_width = math.max(max_text_width, text_width) 201 | end 202 | content_width = max_text_width 203 | end 204 | 205 | --- compute content height. 206 | local content_height --[=[@as integer]=] 207 | do 208 | if params.wrap then 209 | local max_width = math.min(params.max_inner_width, content_width) 210 | local height = 0 211 | for _, text in ipairs(vim.api.nvim_buf_get_lines(params.bufnr, 0, -1, false)) do 212 | local text_width = math.max(1, vim.api.nvim_strwidth(text)) 213 | height = height + math.max(1, math.ceil(text_width / max_width)) 214 | end 215 | content_height = height 216 | else 217 | content_height = vim.api.nvim_buf_line_count(params.bufnr) 218 | end 219 | 220 | for _, extmark in 221 | ipairs(vim.api.nvim_buf_get_extmarks(params.bufnr, -1, 0, -1, { 222 | details = true, 223 | })) 224 | do 225 | if extmark[4] and extmark[4].virt_lines then 226 | content_height = content_height + #extmark[4].virt_lines 227 | end 228 | end 229 | end 230 | 231 | return { 232 | width = content_width, 233 | height = content_height, 234 | } 235 | end 236 | 237 | ---Guess viewport information. 238 | ---@param params { border_size: ___kit___.kit.Vim.FloatingWindow.BorderSize, content_size: ___kit___.kit.Vim.FloatingWindow.ContentSize, max_outer_width: integer, max_outer_height: integer } 239 | ---@return { inner_width: integer, inner_height: integer, outer_width: integer, outer_height: integer, scrollbar: boolean } 240 | function FloatingWindow.compute_restricted_size(params) 241 | local inner_size = { 242 | width = math.min(params.content_size.width, params.max_outer_width - params.border_size.h), 243 | height = math.min(params.content_size.height, params.max_outer_height - params.border_size.v), 244 | } 245 | 246 | local scrollbar = inner_size.height < params.content_size.height 247 | 248 | return { 249 | outer_width = inner_size.width + params.border_size.h + (scrollbar and 1 or 0), 250 | outer_height = inner_size.height + params.border_size.v, 251 | inner_width = inner_size.width, 252 | inner_height = inner_size.height, 253 | scrollbar = scrollbar, 254 | } 255 | end 256 | 257 | ---Create window. 258 | ---@return ___kit___.kit.Vim.FloatingWindow 259 | function FloatingWindow.new() 260 | return setmetatable({ 261 | _augroup = vim.api.nvim_create_augroup(('___kit___.kit.Vim.FloatingWindow:%s'):format(kit.unique_id()), { 262 | clear = true, 263 | }), 264 | _config = { 265 | markdown = false, 266 | }, 267 | _win_option = {}, 268 | _buf_option = {}, 269 | _buf = vim.api.nvim_create_buf(false, true), 270 | _scrollbar_track_buf = vim.api.nvim_create_buf(false, true), 271 | _scrollbar_thumb_buf = vim.api.nvim_create_buf(false, true), 272 | }, FloatingWindow) 273 | end 274 | 275 | ---Get config. 276 | ---@return ___kit___.kit.Vim.FloatingWindow.Config 277 | function FloatingWindow:get_config() 278 | return self._config 279 | end 280 | 281 | ---Set config. 282 | ---@param config ___kit___.kit.Vim.FloatingWindow.Config 283 | function FloatingWindow:set_config(config) 284 | self._config = kit.merge(config, self._config) 285 | end 286 | 287 | ---Set window option. 288 | ---@param key string 289 | ---@param value any 290 | ---@param kind? ___kit___.kit.Vim.FloatingWindow.WindowKind 291 | function FloatingWindow:set_win_option(key, value, kind) 292 | kind = kind or 'main' 293 | self._win_option[kind] = self._win_option[kind] or {} 294 | self._win_option[kind][key] = value 295 | self:_update_option() 296 | end 297 | 298 | ---Get window option. 299 | ---@param key string 300 | ---@param kind? ___kit___.kit.Vim.FloatingWindow.WindowKind 301 | ---@return any 302 | function FloatingWindow:get_win_option(key, kind) 303 | kind = kind or 'main' 304 | local win = ({ 305 | main = self._win, 306 | scrollbar_track = self._scrollbar_track_win, 307 | scrollbar_thumb = self._scrollbar_thumb_win, 308 | })[kind] --[=[@as integer]=] 309 | if not is_visible(win) then 310 | return self._win_option[kind] and self._win_option[kind][key] 311 | end 312 | return vim.api.nvim_get_option_value(key, { win = win }) or vim.api.nvim_get_option_value(key, { scope = 'global' }) 313 | end 314 | 315 | ---Set buffer option. 316 | ---@param key string 317 | ---@param value any 318 | ---@param kind? ___kit___.kit.Vim.FloatingWindow.WindowKind 319 | function FloatingWindow:set_buf_option(key, value, kind) 320 | kind = kind or 'main' 321 | self._buf_option[kind] = self._buf_option[kind] or {} 322 | self._buf_option[kind][key] = value 323 | self:_update_option() 324 | end 325 | 326 | ---Get window option. 327 | ---@param key string 328 | ---@param kind? ___kit___.kit.Vim.FloatingWindow.WindowKind 329 | ---@return any 330 | function FloatingWindow:get_buf_option(key, kind) 331 | kind = kind or 'main' 332 | local buf = ({ 333 | main = self._buf, 334 | scrollbar_track = self._scrollbar_track_buf, 335 | scrollbar_thumb = self._scrollbar_thumb_buf, 336 | })[kind] --[=[@as integer]=] 337 | if not buf then 338 | return self._buf_option[kind] and self._buf_option[kind][key] 339 | end 340 | return vim.api.nvim_get_option_value(key, { buf = buf }) or vim.api.nvim_get_option_value(key, { scope = 'global' }) 341 | end 342 | 343 | ---Returns the related bufnr. 344 | ---@param kind? ___kit___.kit.Vim.FloatingWindow.WindowKind 345 | ---@return integer 346 | function FloatingWindow:get_buf(kind) 347 | if kind == 'scrollbar_track' then 348 | return self._scrollbar_track_buf 349 | elseif kind == 'scrollbar_thumb' then 350 | return self._scrollbar_thumb_buf 351 | end 352 | return self._buf 353 | end 354 | 355 | ---Returns the current win. 356 | ---@param kind? ___kit___.kit.Vim.FloatingWindow.WindowKind 357 | ---@return integer? 358 | function FloatingWindow:get_win(kind) 359 | if kind == 'scrollbar_track' then 360 | return self._scrollbar_track_win 361 | elseif kind == 'scrollbar_thumb' then 362 | return self._scrollbar_thumb_win 363 | end 364 | return self._win 365 | end 366 | 367 | ---Show the window 368 | ---@param win_config ___kit___.kit.Vim.FloatingWindow.WindowConfig 369 | function FloatingWindow:show(win_config) 370 | self._win = show_or_move(self._win, self._buf, { 371 | row = win_config.row, 372 | col = win_config.col, 373 | width = win_config.width, 374 | height = win_config.height, 375 | anchor = win_config.anchor, 376 | style = win_config.style, 377 | border = win_config.border, 378 | title = win_config.title, 379 | title_pos = win_config.title_pos, 380 | footer = win_config.footer, 381 | footer_pos = win_config.footer_pos, 382 | zindex = win_config.zindex or 1000, 383 | }) 384 | 385 | vim.api.nvim_clear_autocmds({ group = self._augroup }) 386 | vim.api.nvim_create_autocmd({ 'WinResized', 'WinScrolled' }, { 387 | group = self._augroup, 388 | callback = function() 389 | self:_update_scrollbar() 390 | end, 391 | }) 392 | 393 | self:_update_scrollbar() 394 | self:_update_option() 395 | end 396 | 397 | ---Hide the window 398 | function FloatingWindow:hide() 399 | vim.api.nvim_clear_autocmds({ group = self._augroup }) 400 | hide(self._win) 401 | hide(self._scrollbar_track_win) 402 | hide(self._scrollbar_thumb_win) 403 | end 404 | 405 | ---Scroll the window. 406 | ---@param delta integer 407 | function FloatingWindow:scroll(delta) 408 | if not is_visible(self._win) then 409 | return 410 | end 411 | local viewport = self:get_viewport() 412 | vim.api.nvim_win_call(self._win, function() 413 | local topline = vim.fn.getwininfo(self._win)[1].topline 414 | topline = math.max(1, topline + delta) 415 | topline = math.min(viewport.content_size.height - vim.api.nvim_win_get_height(self._win) + 1, topline) 416 | vim.cmd.normal({ 417 | ('%szt'):format(topline), 418 | bang = true, 419 | mods = { 420 | keepmarks = true, 421 | keepjumps = true, 422 | keepalt = true, 423 | noautocmd = true, 424 | }, 425 | }) 426 | end) 427 | end 428 | 429 | ---Returns true if the window is visible 430 | function FloatingWindow:is_visible() 431 | return is_visible(self._win) 432 | end 433 | 434 | ---Get window viewport. 435 | ---NOTE: this method can only be called if window is showing. 436 | ---@return ___kit___.kit.Vim.FloatingWindow.Viewport 437 | function FloatingWindow:get_viewport() 438 | if not self:is_visible() then 439 | error('this method can only be called if window is showing.') 440 | end 441 | 442 | local win_config = vim.api.nvim_win_get_config(self:get_win() --[[@as integer]]) 443 | local win_position = vim.api.nvim_win_get_position(self:get_win() --[[@as integer]]) 444 | local border_size = FloatingWindow.get_border_size(win_config.border) 445 | local content_size = FloatingWindow.get_content_size({ 446 | bufnr = self:get_buf(), 447 | wrap = self:get_win_option('wrap'), 448 | max_inner_width = win_config.width, 449 | markdown = self:get_config().markdown, 450 | }) 451 | local scrollbar = win_config.height < content_size.height 452 | 453 | local ui_width = border_size.h + (scrollbar and 1 or 0) 454 | local ui_height = border_size.v 455 | return { 456 | row = win_position[1], 457 | col = win_position[2], 458 | inner_width = win_config.width, 459 | inner_height = win_config.height, 460 | outer_width = win_config.width + ui_width, 461 | outer_height = win_config.height + ui_height, 462 | ui_width = ui_width, 463 | ui_height = ui_height, 464 | border_size = border_size, 465 | content_size = content_size, 466 | scrollbar = scrollbar, 467 | border = win_config.border, 468 | zindex = win_config.zindex, 469 | } 470 | end 471 | 472 | ---Update scrollbar. 473 | function FloatingWindow:_update_scrollbar() 474 | if is_visible(self._win) then 475 | local viewport = self:get_viewport() 476 | if viewport.scrollbar then 477 | do 478 | self._scrollbar_track_win = show_or_move(self._scrollbar_track_win, self._scrollbar_track_buf, { 479 | row = viewport.row + viewport.border_size.top, 480 | col = viewport.col + viewport.outer_width - 1, 481 | width = 1, 482 | height = viewport.inner_height, 483 | style = 'minimal', 484 | zindex = viewport.zindex + 1, 485 | border = 'none', 486 | }) 487 | end 488 | do 489 | local topline = vim.fn.getwininfo(self._win)[1].topline 490 | local ratio = topline / (viewport.content_size.height - viewport.inner_height) 491 | local thumb_height = viewport.inner_height / viewport.content_size.height * viewport.inner_height 492 | local thumb_row = (viewport.inner_height - thumb_height) * ratio 493 | thumb_row = math.floor(math.min(viewport.inner_height - thumb_height, thumb_row)) 494 | self._scrollbar_thumb_win = show_or_move(self._scrollbar_thumb_win, self._scrollbar_thumb_buf, { 495 | row = viewport.row + viewport.border_size.top + thumb_row, 496 | col = viewport.col + viewport.outer_width - 1, 497 | width = 1, 498 | height = math.ceil(thumb_height), 499 | style = 'minimal', 500 | zindex = viewport.zindex + 2, 501 | border = 'none', 502 | }) 503 | end 504 | return 505 | end 506 | end 507 | hide(self._scrollbar_track_win) 508 | hide(self._scrollbar_thumb_win) 509 | end 510 | 511 | ---Update options. 512 | function FloatingWindow:_update_option() 513 | -- update buf. 514 | for kind, buf in pairs({ 515 | main = self._buf, 516 | scrollbar_track = self._scrollbar_track_buf, 517 | scrollbar_thumb = self._scrollbar_thumb_buf, 518 | }) do 519 | for k, v in pairs(self._buf_option[kind] or {}) do 520 | if vim.api.nvim_get_option_value(k, { buf = buf }) ~= v then 521 | vim.api.nvim_set_option_value(k, v, { buf = buf }) 522 | end 523 | end 524 | end 525 | 526 | -- update win. 527 | for kind, win in pairs({ 528 | main = self._win, 529 | scrollbar_track = self._scrollbar_track_win, 530 | scrollbar_thumb = self._scrollbar_thumb_win, 531 | }) do 532 | if is_visible(win) then 533 | for k, v in pairs(self._win_option[kind] or {}) do 534 | if vim.api.nvim_get_option_value(k, { win = win }) ~= v then 535 | vim.api.nvim_set_option_value(k, v, { win = win }) 536 | end 537 | end 538 | end 539 | end 540 | end 541 | 542 | return FloatingWindow 543 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Vim/Keymap.lua: -------------------------------------------------------------------------------- 1 | local kit = require('___kit___.kit') 2 | local Async = require('___kit___.kit.Async') 3 | 4 | local buf = vim.api.nvim_create_buf(false, true) 5 | 6 | local resolve_key = vim.api.nvim_replace_termcodes('lua require("___kit___.kit.Vim.Keymap")._resolve(%s)', true, true, true) 7 | 8 | ---@alias ___kit___.kit.Vim.Keymap.Keys { keys: string, remap?: boolean } 9 | ---@alias ___kit___.kit.Vim.Keymap.KeysSpecifier string|___kit___.kit.Vim.Keymap.Keys 10 | 11 | ---@param keys ___kit___.kit.Vim.Keymap.KeysSpecifier 12 | ---@return ___kit___.kit.Vim.Keymap.Keys 13 | local function to_keys(keys) 14 | if type(keys) == 'table' then 15 | return keys 16 | end 17 | return { keys = keys, remap = false } 18 | end 19 | 20 | local Keymap = {} 21 | 22 | _G.kit = _G.kit or {} 23 | _G.kit.Vim = _G.kit.Vim or {} 24 | _G.kit.Vim.Keymap = _G.kit.Vim.Keymap or {} 25 | _G.kit.Vim.Keymap.callbacks = _G.kit.Vim.Keymap.callbacks or {} 26 | 27 | ---Replace termcodes. 28 | ---@param keys string 29 | ---@return string 30 | function Keymap.termcodes(keys) 31 | return vim.api.nvim_replace_termcodes(keys, true, true, true) 32 | end 33 | 34 | ---Normalize keycode. 35 | function Keymap.normalize(s) 36 | local desc = '___kit___.kit.Vim.Keymap.normalize' 37 | vim.api.nvim_buf_set_keymap(buf, 't', s, '.', { desc = desc }) 38 | for _, map in ipairs(vim.api.nvim_buf_get_keymap(buf, 't')) do 39 | if map.desc == desc then 40 | vim.api.nvim_buf_del_keymap(buf, 't', s) 41 | return map.lhs --[[@as string]] 42 | end 43 | end 44 | vim.api.nvim_buf_del_keymap(buf, 't', s) 45 | return s 46 | end 47 | 48 | ---Set callback for consuming next typeahead. 49 | ---@param callback fun() 50 | ---@return ___kit___.kit.Async.AsyncTask 51 | function Keymap.next(callback) 52 | return Keymap.send(''):next(callback) 53 | end 54 | 55 | ---Send keys. 56 | ---@param keys ___kit___.kit.Vim.Keymap.KeysSpecifier|___kit___.kit.Vim.Keymap.KeysSpecifier[] 57 | ---@param no_insert? boolean 58 | ---@return ___kit___.kit.Async.AsyncTask 59 | function Keymap.send(keys, no_insert) 60 | local unique_id = kit.unique_id() 61 | return Async.new(function(resolve, _) 62 | _G.kit.Vim.Keymap.callbacks[unique_id] = resolve 63 | 64 | local callback = resolve_key:format(unique_id) 65 | if no_insert then 66 | for _, keys_ in ipairs(kit.to_array(keys)) do 67 | keys_ = to_keys(keys_) 68 | vim.api.nvim_feedkeys(keys_.keys, keys_.remap and 'm' or 'n', true) 69 | end 70 | vim.api.nvim_feedkeys(callback, 'n', true) 71 | else 72 | vim.api.nvim_feedkeys(callback, 'in', true) 73 | for _, keys_ in ipairs(kit.reverse(kit.to_array(keys))) do 74 | keys_ = to_keys(keys_) 75 | vim.api.nvim_feedkeys(keys_.keys, 'i' .. (keys_.remap and 'm' or 'n'), true) 76 | end 77 | end 78 | end):catch(function() 79 | _G.kit.Vim.Keymap.callbacks[unique_id] = nil 80 | end) 81 | end 82 | 83 | ---Return sendabke keys with callback function. 84 | ---@param callback fun(...: any): any 85 | ---@return string 86 | function Keymap.to_sendable(callback) 87 | local unique_id = kit.unique_id() 88 | _G.kit.Vim.Keymap.callbacks[unique_id] = function() 89 | Async.run(callback) 90 | end 91 | return resolve_key:format(unique_id) 92 | end 93 | 94 | ---Test spec helper. 95 | ---@param spec fun(): any 96 | function Keymap.spec(spec) 97 | local task = Async.resolve():next(function() 98 | return Async.run(spec) 99 | end) 100 | vim.api.nvim_feedkeys('', 'x', true) 101 | task:sync(5000) 102 | collectgarbage('collect') 103 | vim.wait(200) 104 | end 105 | 106 | ---Resolve running keys. 107 | ---@param unique_id integer 108 | function Keymap._resolve(unique_id) 109 | _G.kit.Vim.Keymap.callbacks[unique_id]() 110 | _G.kit.Vim.Keymap.callbacks[unique_id] = nil 111 | end 112 | 113 | return Keymap 114 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Vim/Keymap.spec.lua: -------------------------------------------------------------------------------- 1 | local Async = require('___kit___.kit.Async') 2 | local Keymap = require('___kit___.kit.Vim.Keymap') 3 | 4 | describe('kit.Vim.Keymap', function() 5 | before_each(function() 6 | vim.cmd([[ 7 | enew! 8 | set noswapfile 9 | ]]) 10 | end) 11 | 12 | it('should send multiple keys in sequence', function() 13 | vim.keymap.set('i', '(', function() 14 | return Async.run(function() 15 | Keymap.send('('):await() 16 | assert.equal('[(', vim.api.nvim_get_current_line()) 17 | 18 | Keymap.send(')'):await() 19 | assert.equal('[()', vim.api.nvim_get_current_line()) 20 | 21 | Keymap.send(Keymap.termcodes('a')):await() 22 | assert.equal('[(a)', vim.api.nvim_get_current_line()) 23 | end) 24 | end) 25 | 26 | Keymap.spec(function() 27 | Keymap.send('i'):await() 28 | Keymap.send({ '[', { keys = '(', remap = true }, ']' }):await() 29 | assert.equal('[(a)]', vim.api.nvim_get_current_line()) 30 | end) 31 | end) 32 | 33 | it('should work with async-await and exceptions', function() 34 | local _, err = pcall(function() 35 | Keymap.spec(function() 36 | Keymap.send('iinsert'):await() 37 | error('error') 38 | end) 39 | end) 40 | assert.are_not.equal(string.match(err --[[@as string]], 'error$'), nil) 41 | end) 42 | 43 | it('should create sendable key notation', function() 44 | local key = Keymap.to_sendable(function() 45 | Keymap.send('iin_sendable') 46 | end) 47 | Keymap.spec(function() 48 | Keymap.send(key):await() 49 | end) 50 | assert.equal('in_sendable', vim.api.nvim_get_current_line()) 51 | end) 52 | 53 | it('should return correct key notation', function() 54 | assert.equal(' ', Keymap.normalize('')) 55 | assert.equal(' ', Keymap.normalize('')) 56 | assert.equal('', Keymap.normalize('')) 57 | assert.equal('', Keymap.normalize('')) 58 | assert.equal('', Keymap.normalize('')) 59 | end) 60 | end) 61 | -------------------------------------------------------------------------------- /lua/___kit___/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/___kit___/kit/Vim/RegExp.spec.lua: -------------------------------------------------------------------------------- 1 | local RegExp = require('___kit___.kit.Vim.RegExp') 2 | 3 | describe('kit.Vim.RegExp', function() 4 | describe('.gsub', function() 5 | it('should replace with vim regex', function() 6 | assert.are.equal(RegExp.gsub('aaa bbbaaa aaaaaa bbaaaa', [[\%(bbb\)\@ 0 then 22 | return treesitter 23 | end 24 | return Syntax.get_vim_syntax_groups(cursor) -- it might be heavy. 25 | end 26 | 27 | ---Get vim's syntax groups for specified position. 28 | ---NOTE: This function accepts 0-origin cursor position. 29 | ---@param cursor { [1]: integer, [2]: integer } 30 | ---@return string[] 31 | function Syntax.get_vim_syntax_groups(cursor) 32 | local unique = {} 33 | local groups = {} 34 | for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do 35 | local name = vim.fn.synIDattr(vim.fn.synIDtrans(syntax_id), 'name') 36 | if not unique[name] then 37 | unique[name] = true 38 | table.insert(groups, name) 39 | end 40 | end 41 | for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do 42 | local name = vim.fn.synIDattr(syntax_id, 'name') 43 | if not unique[name] then 44 | unique[name] = true 45 | table.insert(groups, name) 46 | end 47 | end 48 | return groups 49 | end 50 | 51 | ---Get tree-sitter's syntax groups for specified position. 52 | ---NOTE: This function accepts 0-origin cursor position. 53 | ---@param cursor { [1]: integer, [2]: integer } 54 | ---@return string[] 55 | function Syntax.get_treesitter_syntax_groups(cursor) 56 | local groups = {} 57 | for _, capture in ipairs(vim.treesitter.get_captures_at_pos(0, cursor[1], cursor[2])) do 58 | table.insert(groups, ('@%s'):format(capture.capture)) 59 | end 60 | return groups 61 | end 62 | 63 | return Syntax 64 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Vim/Syntax.spec.lua: -------------------------------------------------------------------------------- 1 | local helper = require('kit.helper') 2 | local Syntax = require('___kit___.kit.Vim.Syntax') 3 | 4 | describe('kit.Vim.Syntax', function() 5 | before_each(function() 6 | vim.cmd([[ 7 | enew! 8 | set filetype=vim 9 | call setline(1, ['let var = 1']) 10 | ]]) 11 | end) 12 | 13 | it('should return vim syntax group', function() 14 | vim.cmd([[ syntax on ]]) 15 | assert.are.same(Syntax.get_syntax_groups({ 0, 3 }), {}) 16 | assert.are.same(Syntax.get_syntax_groups({ 0, 4 }), { 'Normal', 'vimVar' }) 17 | assert.are.same(Syntax.get_syntax_groups({ 0, 6 }), { 'Normal', 'vimVar' }) 18 | assert.are.same(Syntax.get_syntax_groups({ 0, 7 }), {}) 19 | end) 20 | 21 | it('should return treesitter syntax group', function() 22 | helper.ensure_treesitter_parser('vim') 23 | vim.cmd([[ syntax off ]]) 24 | assert.are.same(Syntax.get_syntax_groups({ 0, 3 }), {}) 25 | assert.are.same(Syntax.get_syntax_groups({ 0, 4 }), { '@variable' }) 26 | assert.are.same(Syntax.get_syntax_groups({ 0, 6 }), { '@variable' }) 27 | assert.are.same(Syntax.get_syntax_groups({ 0, 7 }), {}) 28 | end) 29 | end) 30 | -------------------------------------------------------------------------------- /lua/___kit___/kit/Vim/WinSaveView.lua: -------------------------------------------------------------------------------- 1 | ---@class ___kit___.kit.Vim.WinSaveView 2 | ---@field private _mode string 3 | ---@field private _view table 4 | ---@field private _cmd string 5 | ---@field private _win number 6 | ---@field private _cur table 7 | local WinSaveView = {} 8 | WinSaveView.__index = WinSaveView 9 | 10 | ---Create WinSaveView. 11 | function WinSaveView.new() 12 | return setmetatable({ 13 | _mode = vim.api.nvim_get_mode().mode, 14 | _view = vim.fn.winsaveview(), 15 | _cmd = vim.fn.winrestcmd(), 16 | _win = vim.api.nvim_get_current_win(), 17 | _cur = vim.api.nvim_win_get_cursor(0), 18 | }, WinSaveView) 19 | end 20 | 21 | function WinSaveView:restore() 22 | vim.api.nvim_set_current_win(self._win) 23 | 24 | -- restore modes. 25 | if vim.api.nvim_get_mode().mode ~= self._mode then 26 | if self._mode == 'i' then 27 | vim.cmd.startinsert() 28 | elseif vim.tbl_contains({ 'v', 'V', vim.keycode('') }, self._mode) then 29 | vim.cmd.normal({ 'gv', bang = true }) 30 | end 31 | end 32 | 33 | vim.api.nvim_win_set_cursor(0, self._cur) 34 | vim.cmd(self._cmd) 35 | vim.fn.winrestview(self._view) 36 | end 37 | 38 | return WinSaveView 39 | -------------------------------------------------------------------------------- /lua/___kit___/kit/init.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 512 2 | 3 | ---@class ___kit___.kit.buffer.Buffer 4 | ---@field put fun(data: string) 5 | ---@field get fun(byte_size?: integer): string 6 | ---@field len integer 7 | ---@field skip fun(byte_size: integer) 8 | ---@field peek fun(index: integer): integer? 9 | ---@field clear fun() 10 | ---@field reserve fun(byte_size: integer) 11 | ---@field iter_bytes fun(): fun(): integer, integer 12 | 13 | local kit = {} 14 | 15 | do 16 | local buffer = package.preload['string.buffer'] and require('string.buffer') 17 | if buffer then 18 | ---Create buffer object. 19 | ---@return ___kit___.kit.buffer.Buffer 20 | function kit.buffer_jit() 21 | local buf = buffer.new() 22 | local ptr, len = buf:ref() 23 | ---@type ___kit___.kit.buffer.Buffer 24 | return setmetatable({ 25 | put = function(data) 26 | buf:put(data) 27 | ptr, len = buf:ref() 28 | end, 29 | get = function(byte_size) 30 | local o = buf:get(byte_size) 31 | ptr, len = buf:ref() 32 | return o 33 | end, 34 | len = function() 35 | return len 36 | end, 37 | skip = function(byte_size) 38 | buf:skip(byte_size) 39 | ptr, len = buf:ref() 40 | end, 41 | clear = function() 42 | buf:reset() 43 | ptr, len = buf:ref() 44 | end, 45 | peek = function(index) 46 | if index < 1 or index > len then 47 | return nil 48 | end 49 | return ptr[index - 1] 50 | end, 51 | reserve = function(byte_size) 52 | buf:reserve(byte_size) 53 | end, 54 | iter_bytes = function() 55 | local i = 0 56 | return function() 57 | if i < len then 58 | local byte = ptr[i] 59 | i = i + 1 60 | return i, byte 61 | end 62 | end 63 | end, 64 | }, { 65 | __tostring = function() 66 | return buf:tostring() 67 | end, 68 | }) 69 | end 70 | end 71 | end 72 | 73 | ---Create buffer object. 74 | ---@return ___kit___.kit.buffer.Buffer 75 | function kit.buffer_tbl() 76 | local tmp_get = {} 77 | local buf = {} 78 | ---@type ___kit___.kit.buffer.Buffer 79 | local buffer 80 | buffer = setmetatable({ 81 | put = function(data) 82 | table.insert(buf, data) 83 | end, 84 | get = function(byte_size) 85 | if byte_size == nil then 86 | local data = table.concat(buf, '') 87 | kit.clear(buf) 88 | return data 89 | end 90 | if byte_size == 0 then 91 | return '' 92 | end 93 | 94 | kit.clear(tmp_get) 95 | local off = 0 96 | local i = 1 97 | while i <= #buf do 98 | local b = buf[i] 99 | if off + #b >= byte_size then 100 | local data_size = byte_size - off 101 | if #b <= data_size then 102 | table.insert(tmp_get, b) 103 | table.remove(buf, i) 104 | else 105 | table.insert(tmp_get, b:sub(1, data_size)) 106 | buf[i] = b:sub(data_size + 1) 107 | end 108 | break 109 | end 110 | i = i + 1 111 | off = off + #b 112 | end 113 | return table.concat(tmp_get, '') 114 | end, 115 | len = function() 116 | local len = 0 117 | for _, data in ipairs(buf) do 118 | len = len + #data 119 | end 120 | return len 121 | end, 122 | skip = function(byte_size) 123 | buffer.get(byte_size) 124 | end, 125 | peek = function(index) 126 | local i = 1 127 | while i <= #buf do 128 | if index <= #buf[i] then 129 | return buf[i]:byte(index) 130 | end 131 | index = index - #buf[i] 132 | i = i + 1 133 | end 134 | end, 135 | clear = function() 136 | kit.clear(buf) 137 | end, 138 | reserve = function() 139 | -- noop 140 | end, 141 | iter_bytes = function() 142 | local i = 1 143 | local j = 1 144 | local c = 1 145 | return function() 146 | while i <= #buf do 147 | local data = buf[i] 148 | if j <= #data then 149 | local byte = data:byte(j) 150 | j = j + 1 151 | c = c + 1 152 | return (c - 1), byte 153 | end 154 | i = i + 1 155 | j = 1 156 | end 157 | return nil 158 | end 159 | end, 160 | }, { 161 | __tostring = function() 162 | return table.concat(buf) 163 | end, 164 | }) 165 | return buffer 166 | end 167 | 168 | ---Create buffer object. 169 | ---@return ___kit___.kit.buffer.Buffer 170 | kit.buffer = function() 171 | return kit.buffer_jit and kit.buffer_jit() or kit.buffer_tbl() 172 | end 173 | 174 | do 175 | local clear = package.preload['table.clear'] and require('table.clear') 176 | 177 | ---Clear table. 178 | ---@generic T: table 179 | ---@param tbl T 180 | ---@return T 181 | kit.clear = function(tbl) 182 | if type(tbl) ~= 'table' then 183 | return tbl 184 | end 185 | if clear then 186 | clear(tbl) 187 | else 188 | for k, _ in pairs(tbl) do 189 | tbl[k] = nil 190 | end 191 | end 192 | return tbl 193 | end 194 | end 195 | 196 | ---Check shallow equals. 197 | ---@param a any 198 | ---@param b any 199 | ---@return boolean 200 | function kit.shallow_equals(a, b) 201 | if type(a) ~= type(b) then 202 | return false 203 | end 204 | if type(a) ~= 'table' then 205 | return a == b 206 | end 207 | for k, v in pairs(a) do 208 | if v ~= b[k] then 209 | return false 210 | end 211 | end 212 | for k, v in pairs(b) do 213 | if v ~= a[k] then 214 | return false 215 | end 216 | end 217 | return true 218 | end 219 | 220 | ---Create gabage collection detector. 221 | ---@param callback fun(...: any): any 222 | ---@return userdata 223 | function kit.gc(callback) 224 | local gc = newproxy(true) 225 | if vim.is_thread() or os.getenv('NODE_ENV') == 'test' then 226 | getmetatable(gc).__gc = callback 227 | else 228 | getmetatable(gc).__gc = vim.schedule_wrap(callback) 229 | end 230 | return gc 231 | end 232 | 233 | ---Safe version of vim.schedule. 234 | ---@param fn fun(...: any): any 235 | function kit.safe_schedule(fn) 236 | if vim.is_thread() then 237 | local timer = assert(vim.uv.new_timer()) 238 | timer:start(0, 0, function() 239 | timer:stop() 240 | timer:close() 241 | fn() 242 | end) 243 | else 244 | vim.schedule(fn) 245 | end 246 | end 247 | 248 | ---Safe version of vim.schedule_wrap. 249 | ---@param fn fun(...: any): any 250 | function kit.safe_schedule_wrap(fn) 251 | if vim.is_thread() then 252 | return function(...) 253 | local args = { ... } 254 | local timer = assert(vim.uv.new_timer()) 255 | timer:start(0, 0, function() 256 | timer:stop() 257 | timer:close() 258 | fn(unpack(args)) 259 | end) 260 | end 261 | else 262 | return vim.schedule_wrap(fn) 263 | end 264 | end 265 | 266 | ---Fast version of vim.schedule. 267 | ---@param fn fun(): any 268 | function kit.fast_schedule(fn) 269 | if vim.in_fast_event() then 270 | kit.safe_schedule(fn) 271 | else 272 | fn() 273 | end 274 | end 275 | 276 | ---Safe version of vim.schedule_wrap. 277 | ---@generic A 278 | ---@param fn fun(...: A) 279 | ---@return fun(...: A) 280 | function kit.fast_schedule_wrap(fn) 281 | return function(...) 282 | local args = { ... } 283 | kit.fast_schedule(function() 284 | fn(unpack(args)) 285 | end) 286 | end 287 | end 288 | 289 | ---Find up directory. 290 | ---@param path string 291 | ---@param markers string[] 292 | ---@return string? 293 | function kit.findup(path, markers) 294 | path = vim.fs.normalize(path) 295 | if vim.fn.filereadable(path) == 1 then 296 | path = vim.fs.dirname(path) 297 | end 298 | while path ~= '/' do 299 | for _, marker in ipairs(markers) do 300 | local target = vim.fs.joinpath(path, (marker:gsub('/', ''))) 301 | if vim.fn.isdirectory(target) == 1 or vim.fn.filereadable(target) == 1 then 302 | return path 303 | end 304 | end 305 | path = vim.fs.dirname(path) 306 | end 307 | end 308 | 309 | do 310 | _G.kit = _G.kit or {} 311 | _G.kit.unique_id = 0 312 | 313 | ---Create unique id. 314 | ---@return integer 315 | kit.unique_id = function() 316 | _G.kit.unique_id = _G.kit.unique_id + 1 317 | return _G.kit.unique_id 318 | end 319 | end 320 | 321 | ---Map array. 322 | ---@deprecated 323 | ---@param array table 324 | ---@param fn fun(item: unknown, index: integer): unknown 325 | ---@return unknown[] 326 | function kit.map(array, fn) 327 | local new_array = {} 328 | for i, item in ipairs(array) do 329 | table.insert(new_array, fn(item, i)) 330 | end 331 | return new_array 332 | end 333 | 334 | ---@generic T 335 | ---@deprecated 336 | ---@param value T? 337 | ---@param default T 338 | function kit.default(value, default) 339 | if value == nil then 340 | return default 341 | end 342 | return value 343 | end 344 | 345 | ---Get object path with default value. 346 | ---@generic T 347 | ---@param value table 348 | ---@param path integer|string|(string|integer)[] 349 | ---@param default? T 350 | ---@return T 351 | function kit.get(value, path, default) 352 | local result = value 353 | for _, key in ipairs(kit.to_array(path)) do 354 | if type(result) == 'table' then 355 | result = result[key] 356 | else 357 | return default 358 | end 359 | end 360 | if result == nil then 361 | return default 362 | end 363 | return result 364 | end 365 | 366 | ---Set object path with new value. 367 | ---@param value table 368 | ---@param path integer|string|(string|integer)[] 369 | ---@param new_value any 370 | function kit.set(value, path, new_value) 371 | local current = value 372 | for i = 1, #path - 1 do 373 | local key = path[i] 374 | if type(current[key]) ~= 'table' then 375 | error('The specified path is not a table.') 376 | end 377 | current = current[key] 378 | end 379 | current[path[#path]] = new_value 380 | end 381 | 382 | ---Create debounced callback. 383 | ---@generic T: fun(...: any): nil 384 | ---@param callback T 385 | ---@param timeout_ms integer 386 | ---@return T 387 | function kit.debounce(callback, timeout_ms) 388 | local timer = assert(vim.uv.new_timer()) 389 | return setmetatable({ 390 | timeout_ms = timeout_ms, 391 | is_running = function() 392 | return timer:is_active() 393 | end, 394 | stop = function() 395 | timer:stop() 396 | end, 397 | }, { 398 | __call = function(self, ...) 399 | local arguments = { ... } 400 | 401 | self.running = true 402 | timer:stop() 403 | timer:start(self.timeout_ms, 0, function() 404 | self.running = false 405 | timer:stop() 406 | callback(unpack(arguments)) 407 | end) 408 | end, 409 | }) 410 | end 411 | 412 | ---Create throttled callback. 413 | ---First call will be called immediately. 414 | ---@generic T: fun(...: any): nil 415 | ---@param callback T 416 | ---@param timeout_ms integer 417 | function kit.throttle(callback, timeout_ms) 418 | local timer = assert(vim.uv.new_timer()) 419 | local arguments = nil 420 | local last_time = (vim.uv.hrtime() / 1000000) - timeout_ms - 1 421 | return setmetatable({ 422 | timeout_ms = timeout_ms, 423 | is_running = function() 424 | return timer:is_active() 425 | end, 426 | stop = function() 427 | timer:stop() 428 | end, 429 | }, { 430 | __call = function(self, ...) 431 | arguments = { ... } 432 | 433 | if self.is_running() then 434 | timer:stop() 435 | end 436 | local delay_ms = self.timeout_ms - ((vim.uv.hrtime() / 1000000) - last_time) 437 | if delay_ms > 0 then 438 | timer:start(delay_ms, 0, function() 439 | timer:stop() 440 | last_time = (vim.uv.hrtime() / 1000000) 441 | callback(unpack(arguments)) 442 | end) 443 | else 444 | last_time = (vim.uv.hrtime() / 1000000) 445 | callback(unpack(arguments)) 446 | end 447 | end, 448 | }) 449 | end 450 | 451 | do 452 | ---@generic T 453 | ---@param target T 454 | ---@param seen table 455 | ---@return T 456 | local function do_clone(target, seen) 457 | if type(target) ~= 'table' then 458 | return target 459 | end 460 | if seen[target] then 461 | return seen[target] 462 | end 463 | if kit.is_array(target) then 464 | local new_tbl = {} 465 | seen[target] = new_tbl 466 | for k, v in ipairs(target) do 467 | new_tbl[k] = do_clone(v, seen) 468 | end 469 | return new_tbl 470 | else 471 | local new_tbl = {} 472 | local meta = getmetatable(target) 473 | if meta then 474 | setmetatable(new_tbl, meta) 475 | end 476 | seen[target] = new_tbl 477 | for k, v in pairs(target) do 478 | new_tbl[k] = do_clone(v, seen) 479 | end 480 | return new_tbl 481 | end 482 | end 483 | 484 | ---Clone object. 485 | ---@generic T 486 | ---@param target T 487 | ---@return T 488 | function kit.clone(target) 489 | return do_clone(target, {}) 490 | end 491 | end 492 | 493 | ---Merge two tables. 494 | ---@generic T: any[] 495 | ---NOTE: This doesn't merge array-like table. 496 | ---@param tbl1 T 497 | ---@param tbl2 T 498 | ---@return T 499 | function kit.merge(tbl1, tbl2) 500 | local is_dict1 = kit.is_dict(tbl1) and getmetatable(tbl1) == nil 501 | local is_dict2 = kit.is_dict(tbl2) and getmetatable(tbl2) == nil 502 | if is_dict1 and is_dict2 then 503 | local new_tbl = {} 504 | for k, v in pairs(tbl2) do 505 | if tbl1[k] ~= vim.NIL then 506 | new_tbl[k] = kit.merge(tbl1[k], v) 507 | end 508 | end 509 | for k, v in pairs(tbl1) do 510 | if tbl2[k] == nil then 511 | if v ~= vim.NIL then 512 | new_tbl[k] = kit.merge(v, {}) 513 | else 514 | new_tbl[k] = nil 515 | end 516 | end 517 | end 518 | return new_tbl 519 | end 520 | 521 | -- premitive like values. 522 | if tbl1 == vim.NIL then 523 | return nil 524 | elseif tbl1 == nil then 525 | return kit.merge(tbl2, {}) -- clone & prevent vim.NIL 526 | end 527 | return tbl1 528 | end 529 | 530 | ---Concatenate two tables. 531 | ---NOTE: This doesn't concatenate dict-like table. 532 | ---@param tbl1 table 533 | ---@param ... table 534 | ---@return table 535 | function kit.concat(tbl1, ...) 536 | local new_tbl = {} 537 | 538 | local off = 0 539 | for _, item in pairs(tbl1) do 540 | new_tbl[off + 1] = item 541 | off = off + 1 542 | end 543 | 544 | for _, tbl2 in ipairs({ ... }) do 545 | for _, item in pairs(kit.to_array(tbl2)) do 546 | new_tbl[off + 1] = item 547 | off = off + 1 548 | end 549 | end 550 | return new_tbl 551 | end 552 | 553 | ---Slice the array. 554 | ---@generic T: any[] 555 | ---@param array T 556 | ---@param s integer 557 | ---@param e integer 558 | ---@return T 559 | function kit.slice(array, s, e) 560 | if not kit.is_array(array) then 561 | error('[kit] specified value is not an array.') 562 | end 563 | local new_array = {} 564 | for i = s, e do 565 | table.insert(new_array, array[i]) 566 | end 567 | return new_array 568 | end 569 | 570 | ---The value to array. 571 | ---@param value any 572 | ---@return table 573 | function kit.to_array(value) 574 | if type(value) == 'table' then 575 | if kit.is_array(value) then 576 | return value 577 | end 578 | end 579 | return { value } 580 | end 581 | 582 | ---Check the value is array. 583 | ---@param value any 584 | ---@return boolean 585 | function kit.is_array(value) 586 | if type(value) ~= 'table' then 587 | return false 588 | end 589 | 590 | for k, _ in pairs(value) do 591 | if type(k) ~= 'number' then 592 | return false 593 | end 594 | end 595 | return true 596 | end 597 | 598 | ---Check the value is dict. 599 | ---@param value any 600 | ---@return boolean 601 | function kit.is_dict(value) 602 | return type(value) == 'table' and (not kit.is_array(value) or kit.is_empty(value)) 603 | end 604 | 605 | ---Check the value is empty. 606 | ---@param value any 607 | ---@return boolean 608 | function kit.is_empty(value) 609 | if type(value) ~= 'table' then 610 | return false 611 | end 612 | for _ in pairs(value) do 613 | return false 614 | end 615 | if #value == 0 then 616 | return true 617 | end 618 | return true 619 | end 620 | 621 | ---Reverse the array. 622 | ---@param array table 623 | ---@return table 624 | function kit.reverse(array) 625 | if not kit.is_array(array) then 626 | error('[kit] specified value is not an array.') 627 | end 628 | 629 | local new_array = {} 630 | for i = #array, 1, -1 do 631 | table.insert(new_array, array[i]) 632 | end 633 | return new_array 634 | end 635 | 636 | ---String dedent. 637 | function kit.dedent(s) 638 | local lines = vim.split(s, '\n') 639 | if lines[1]:match('^%s*$') then 640 | table.remove(lines, 1) 641 | end 642 | if lines[#lines]:match('^%s*$') then 643 | table.remove(lines, #lines) 644 | end 645 | local base_indent = lines[1]:match('^%s*') 646 | for i, line in ipairs(lines) do 647 | lines[i] = line:gsub('^' .. base_indent, '') 648 | end 649 | return table.concat(lines, '\n') 650 | end 651 | 652 | return kit 653 | -------------------------------------------------------------------------------- /lua/___kit___/kit/init.spec.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 311 2 | local kit = require('___kit___.kit') 3 | local Async = require('___kit___.kit.Async') 4 | 5 | describe('kit', function() 6 | describe('buffer', function() 7 | for _, new in ipairs({ 'buffer_jit', 'buffer_tbl' }) do 8 | if new ~= 'buffer_jit' or kit.buffer_jit then 9 | it(('work with %s'):format(new), function() 10 | local buffer = kit[new]() ---@type ___kit___.kit.buffer.Buffer 11 | 12 | -- put/get. 13 | buffer.put('foo') 14 | buffer.put('bar') 15 | buffer.put('baz') 16 | assert.are.equal(buffer.len(), 9) 17 | assert.are.equal(tostring(buffer), 'foobarbaz') 18 | assert.are.equal(buffer.get(1), 'f') 19 | assert.are.equal(buffer.get(1), 'o') 20 | assert.are.equal(buffer.get(1), 'o') 21 | assert.are.equal(buffer.len(), 6) 22 | assert.are.equal(buffer.get(3), 'bar') 23 | assert.are.equal(buffer.len(), 3) 24 | assert.are.equal(buffer.get(3), 'baz') 25 | assert.are.equal(buffer.len(), 0) 26 | assert.are.equal(buffer.get(3), '') 27 | 28 | -- peek. 29 | buffer.clear() 30 | buffer.put('foo') 31 | assert.are.equal(buffer.peek(0), nil) 32 | assert.are.equal(buffer.peek(1), ('f'):byte()) 33 | assert.are.equal(buffer.peek(2), ('o'):byte()) 34 | assert.are.equal(buffer.peek(3), ('o'):byte()) 35 | assert.are.equal(buffer.peek(4), nil) 36 | assert.are.equal(buffer.len(), 3) 37 | assert.are.equal(buffer.get(), 'foo') 38 | 39 | -- iter_bytes. 40 | buffer.clear() 41 | buffer.put('foo') 42 | buffer.put('bar') 43 | buffer.put('baz') 44 | assert.are.equal(buffer.len(), 9) 45 | 46 | local len = buffer.len() 47 | for i, byte in buffer.iter_bytes() do 48 | assert.are.equal(byte, ('foobarbaz'):byte(i)) 49 | len = len - 1 50 | end 51 | assert.are.equal(len, 0) 52 | assert.are.equal(buffer.len(), 9) 53 | end) 54 | end 55 | end 56 | end) 57 | 58 | describe('debounce', function() 59 | it('should callback after timeout', function() 60 | Async.run(function() 61 | local count = 0 62 | local fn = kit.debounce(function() 63 | count = count + 1 64 | end, 200) 65 | fn() 66 | assert.are.equal(count, 0) 67 | fn() 68 | assert.are.equal(count, 0) 69 | vim.wait(200) 70 | assert.are.equal(count, 1) 71 | fn() 72 | fn() 73 | assert.are.equal(count, 1) 74 | vim.wait(200) 75 | assert.are.equal(count, 2) 76 | vim.wait(200) 77 | assert.are.equal(count, 2) 78 | end):sync(5000) 79 | end) 80 | end) 81 | 82 | describe('throttle', function() 83 | it('should callback after timeout', function() 84 | Async.run(function() 85 | local count = 0 86 | local throttled = kit.throttle(function() 87 | count = count + 1 88 | end, 200) 89 | throttled() 90 | assert.are.equal(count, 1) 91 | throttled() 92 | assert.are.equal(count, 1) 93 | vim.wait(200) 94 | assert.are.equal(count, 2) 95 | throttled() 96 | throttled() 97 | assert.are.equal(count, 2) 98 | vim.wait(200) 99 | assert.are.equal(count, 3) 100 | vim.wait(200) 101 | assert.are.equal(count, 3) 102 | end):sync(5000) 103 | end) 104 | end) 105 | 106 | describe('.gc', function() 107 | it('should detect gc timing.', function() 108 | local called = false 109 | local object = { 110 | marker = kit.gc(function() 111 | called = true 112 | end), 113 | } 114 | object = nil 115 | collectgarbage('collect') 116 | vim.wait(200, function() 117 | return object 118 | end) 119 | assert.are.equal(object, nil) 120 | assert.are.equal(called, true) 121 | end) 122 | end) 123 | describe('.merge', function() 124 | it('should merge two dict', function() 125 | assert.are.same( 126 | kit.merge({ 127 | a = true, 128 | b = { 129 | c = vim.NIL, 130 | }, 131 | d = { 132 | e = 3, 133 | }, 134 | h = false, 135 | i = { 1, 2, 3 }, 136 | j = { 137 | k = vim.NIL, 138 | }, 139 | }, { 140 | a = false, 141 | b = { 142 | c = true, 143 | }, 144 | d = { 145 | f = { 146 | g = vim.NIL, 147 | }, 148 | }, 149 | i = { 2, 3, 4 }, 150 | }), 151 | { 152 | a = true, 153 | b = {}, 154 | d = { 155 | e = 3, 156 | f = {}, 157 | }, 158 | h = false, 159 | i = { 1, 2, 3 }, 160 | j = {}, 161 | } 162 | ) 163 | end) 164 | end) 165 | 166 | describe('.concat', function() 167 | it('should concat', function() 168 | assert.are.same({ 1, 2, 3, 4, 5, 6 }, kit.concat({ 1, 2, 3 }, { 4, 5, 6 })) 169 | end) 170 | it('should concat nil contained list', function() 171 | assert.are.same({ 1, 3, 4, 6 }, kit.concat({ 1, nil, 3 }, { 4, nil, 6 })) 172 | end) 173 | end) 174 | 175 | describe('.to_array', function() 176 | it('should convert value to array', function() 177 | assert.are.same(kit.to_array(1), { 1 }) 178 | assert.are.same(kit.to_array({ 1, 2, 3 }), { 1, 2, 3 }) 179 | assert.are.same(kit.to_array({}), {}) 180 | assert.are.same(kit.to_array({ a = 1 }), { { a = 1 } }) 181 | end) 182 | end) 183 | 184 | describe('.is_array', function() 185 | it('should check array or not', function() 186 | assert.are.equal(kit.is_array({}), true) 187 | assert.are.equal(kit.is_array({ 1 }), true) 188 | assert.are.equal(kit.is_array({ a = 1 }), false) 189 | assert.are.equal(kit.is_array({ 1, a = 1 }), false) 190 | assert.are.equal(kit.is_array(1), false) 191 | end) 192 | end) 193 | 194 | describe('.is_dict', function() 195 | it('should check array or not', function() 196 | assert.are.equal(kit.is_dict({}), true) 197 | assert.are.equal(kit.is_dict({ 1 }), false) 198 | assert.are.equal(kit.is_dict({ a = 1 }), true) 199 | assert.are.equal(kit.is_dict({ 1, a = 1 }), true) 200 | assert.are.equal(kit.is_dict(1), false) 201 | end) 202 | end) 203 | 204 | describe('.reverse', function() 205 | it('should reverse the array', function() 206 | assert.are.same(kit.reverse({ 1, 2, 3 }), { 3, 2, 1 }) 207 | end) 208 | end) 209 | end) 210 | -------------------------------------------------------------------------------- /lua/kit/helper.lua: -------------------------------------------------------------------------------- 1 | local helper = {} 2 | 3 | function helper.ensure_treesitter_parser(name) 4 | vim.cmd(([[ 5 | syntax off 6 | set filetype=%s 7 | ]]):format(name)) 8 | 9 | vim.o.runtimepath = vim.o.runtimepath .. ',' .. vim.fn.fnamemodify('./tmp/nvim-treesitter', ':p') 10 | require('nvim-treesitter').setup() 11 | require('nvim-treesitter.configs').setup({ 12 | highlight = { 13 | enable = true, 14 | }, 15 | }) 16 | if not require('nvim-treesitter.parsers').has_parser(name) then 17 | vim.cmd(([[TSInstallSync! %s]]):format(name)) 18 | else 19 | vim.cmd(([[TSUpdateSync %s]]):format(name)) 20 | end 21 | vim.treesitter.get_parser(0, name):parse() 22 | end 23 | 24 | return helper 25 | -------------------------------------------------------------------------------- /lua/kit/install.lua: -------------------------------------------------------------------------------- 1 | local IO = require('___kit___.kit.IO') 2 | local Async = require('___kit___.kit.Async') 3 | local RegExp = require('___kit___.kit.Vim.RegExp') 4 | 5 | ---Return nvim-kit path. 6 | ---@return string 7 | local function get_kit_path() 8 | local script_path = IO.normalize(debug.getinfo(2, 'S').source:sub(2):match('(.*/)')) 9 | return vim.fn.fnamemodify(script_path, ':h:h') .. '/lua/___kit___/kit' 10 | end 11 | 12 | ---Show confirm prompt. 13 | ---@param force boolean 14 | ---@param prompt string 15 | local function confirm(force, prompt) 16 | if force then 17 | print(prompt) 18 | return 19 | end 20 | local answer = vim.fn.input(prompt .. ' [y/N]: ') 21 | if answer ~= 'y' and answer ~= 'Y' then 22 | error('Cancelled') 23 | end 24 | end 25 | 26 | ---Install kit. 27 | ---@param bang boolean 28 | ---@param plugin_path? string 29 | ---@param plugin_name? string 30 | return function(bang, plugin_path, plugin_name) 31 | Async.run(function() 32 | -- Resolve `plugin_path`. 33 | if not plugin_path or plugin_path == '' then 34 | vim.cmd([[redraw]]) 35 | plugin_path = IO.normalize(vim.fn.input('plugin_path: ', '', 'file')) 36 | if plugin_path == '' then 37 | vim.cmd([[redraw]]) 38 | return vim.api.nvim_echo({ { 'Cancelled.', 'Normal' } }, true, {}) 39 | end 40 | end 41 | 42 | -- Check `plugin_path` is a directory. 43 | if not IO.is_directory(plugin_path):await() then 44 | vim.cmd([[redraw]]) 45 | return vim.api.nvim_echo({ { '`plugin_path` is not a directory.', 'ErrorMsg' } }, true, {}) 46 | end 47 | 48 | -- Resolve `plugin_name`. 49 | if not plugin_name or plugin_name == '' then 50 | vim.cmd([[redraw]]) 51 | plugin_name = vim.fn.input('plugin_name: ') 52 | if plugin_name == '' then 53 | vim.cmd([[redraw]]) 54 | return vim.api.nvim_echo({ { 'Cancelled.', 'Normal' } }, true, {}) 55 | end 56 | end 57 | 58 | -- Check `plugin_name` is valid. 59 | if not string.match(plugin_name, '[%a_]+') then 60 | vim.cmd([[redraw]]) 61 | return vim.api.nvim_echo({ { '`plugin_name` must be [a-zA-Z_].', 'ErrorMsg' } }, true, {}) 62 | end 63 | 64 | -- Start install process. 65 | vim.api.nvim_echo({ { ('\n[%s] Installing...\n'):format(plugin_name), 'Normal' } }, true, {}) 66 | 67 | -- Remove old kit if need. 68 | local kit_install_path = ([[%s/lua/%s/kit]]):format(plugin_path, plugin_name) 69 | if IO.is_directory(kit_install_path):await() then 70 | confirm(bang, ('[%s] rm -rf %s'):format(plugin_name, kit_install_path)) 71 | IO.rm(kit_install_path, { recursive = true }):await() 72 | end 73 | 74 | -- Copy new kit. 75 | confirm(bang, ('[%s] cp -r %s %s'):format(plugin_name, get_kit_path(), kit_install_path)) 76 | IO.cp(get_kit_path(), kit_install_path, { recursive = true }):await() 77 | 78 | -- Remove `*.spec.lue` files. 79 | IO.walk(kit_install_path, function(err, entry) 80 | if err then 81 | vim.api.nvim_echo({ { ('\n[%s] Convert error... %s\n'):format(plugin_name, err), 'Normal' } }, true, {}) 82 | end 83 | if entry.type == 'file' then 84 | if entry.path:match('%.spec%.lua$') then 85 | IO.rm(entry.path):await() 86 | else 87 | local content = IO.read_file(entry.path):await() 88 | content = RegExp.gsub(content, [=[\V___kit___\m]=], plugin_name) 89 | content = RegExp.gsub(content, [=[[^\n]*kit\.macro\.remove[^\n]*[[:space:]]*]=], '') 90 | IO.write_file(entry.path, content):await() 91 | end 92 | end 93 | end):await() 94 | end) 95 | :catch(vim.schedule_wrap(function(err) 96 | if err:match('Cancelled$') then 97 | vim.api.nvim_echo({ { '\nCancelled.', 'WarningMsg' } }, true, {}) 98 | else 99 | vim.api.nvim_echo({ { err, 'ErrorMsg' } }, true, {}) 100 | end 101 | end)) 102 | :sync(5000) 103 | end 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "preivate": true, 3 | "name": "nvim-kit", 4 | "version": "1.0.0", 5 | "description": "My personal Lua utilities for neovim.", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "check": "npm run test && npm run lint && npm run typecheck && npm run format", 9 | "test": "NODE_ENV=test vusted --output=gtest --pattern=.spec ./lua", 10 | "lint": "luacheck --codes ./lua", 11 | "format": "stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua", 12 | "typecheck": "rm -Rf $(pwd)/tmp/typecheck; lua-language-server --check $(pwd)/lua --configpath=$(pwd)/.luarc.typecheck.json --logpath=$(pwd)/tmp/typecheck > /dev/null; cat ./tmp/typecheck/check.json 2> /dev/null", 13 | "generate": "run-s generate:* && npm run format", 14 | "generate:language-server-protocol": "ts-node ./script/lsp/generate-protocol.ts", 15 | "generate:language-server-client": "ts-node ./script/lsp/generate-client.ts", 16 | "update": "run-s update:**", 17 | "update:nvim-treesitter:clean": "rimraf ./tmp/nvim-treesitter", 18 | "update:nvim-treesitter:clone": "git clone https://github.com/nvim-treesitter/nvim-treesitter ./tmp/nvim-treesitter", 19 | "update:nvim-treesitter:pull": "git -C ./tmp/nvim-treesitter pull --rebase", 20 | "update:language-server-protocol:clean": "rimraf ./tmp/language-server-protocol", 21 | "update:language-server-protocol:clone": "git clone https://github.com/microsoft/language-server-protocol ./tmp/language-server-protocol", 22 | "update:language-server-protocol:pull": "git -C ./tmp/language-server-protocol pull --rebase" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/hrsh7th/nvim-kit.git" 27 | }, 28 | "author": "hrsh7th", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "husky": "^8.0.3", 32 | "npm-run-all": "^4.1.5", 33 | "rimraf": "^3.0.2", 34 | "ts-dedent": "^2.2.0", 35 | "ts-node": "^10.9.1", 36 | "tslib": "^2.4.1", 37 | "typescript": "^4.8.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugin/kit.lua: -------------------------------------------------------------------------------- 1 | if vim.g.loaded_kit then 2 | return 3 | end 4 | vim.g.loaded_kit = true 5 | 6 | vim.api.nvim_create_user_command("KitInstall", function(option) 7 | local plugin_path, plugin_name = unpack(option.fargs) 8 | require("kit.install")(option.bang, plugin_path, plugin_name) 9 | end, { 10 | bang = true, 11 | nargs = "*", 12 | }) 13 | -------------------------------------------------------------------------------- /script/bench/Async/stack.lua: -------------------------------------------------------------------------------- 1 | local Async = require('___kit___.kit.Async') 2 | 3 | local function fib(n) 4 | return Async.run(function() 5 | if n < 2 then 6 | return n 7 | end 8 | return fib(n - 1):await() + fib(n - 2):await() 9 | end) 10 | end 11 | 12 | collectgarbage('collect') 13 | local s = vim.uv.hrtime() / 1000000 14 | fib(26):next(function() 15 | print('time: ', vim.uv.hrtime() / 1000000 - s) 16 | print('memory: ', collectgarbage("count") / 1024) 17 | print('count: ', Async.count) 18 | end) 19 | -------------------------------------------------------------------------------- /script/bench/IO/walk.lua: -------------------------------------------------------------------------------- 1 | local Worker = require('___kit___.kit.Thread.Worker') 2 | 3 | Worker.new(function() 4 | local count = 0 5 | local IO = require('___kit___.kit.IO') 6 | return IO.walk('~/', function(err, _) 7 | if err then 8 | print(err) 9 | end 10 | count = count + 1 11 | if count % 10000 == 0 then 12 | print(count) 13 | end 14 | end):next(function(res) 15 | print(res) 16 | end):catch(function(err) 17 | print(err) 18 | end) 19 | end)() 20 | -------------------------------------------------------------------------------- /script/lsp/generate-client.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import dedent from 'ts-dedent'; 3 | import { toTypeNotation } from '.'; 4 | import metaModel from '../../tmp/language-server-protocol/_specifications/lsp/3.18/metaModel/metaModel.json'; 5 | 6 | (() => { 7 | const client = dedent` 8 | local LSP = require('___kit___.kit.LSP') 9 | local AsyncTask = require('___kit___.kit.Async.AsyncTask') 10 | 11 | ---@class ___kit___.kit.LSP.Client 12 | ---@field public client vim.lsp.Client 13 | local Client = {} 14 | Client.__index = Client 15 | 16 | ---Create LSP Client wrapper. 17 | ---@param client vim.lsp.Client 18 | ---@return ___kit___.kit.LSP.Client 19 | function Client.new(client) 20 | local self = setmetatable({}, Client) 21 | self.client = client 22 | return self 23 | end 24 | 25 | ---Send request. 26 | ---@param method string 27 | ---@param params table 28 | ---@return ___kit___.kit.Async.AsyncTask|{cancel: fun()} 29 | function Client:request(method, params) 30 | local that, _, request_id, reject_ = self, nil, nil, nil 31 | ---@type ___kit___.kit.Async.AsyncTask|{cancel: fun()} 32 | local task = AsyncTask.new(function(resolve, reject) 33 | reject_ = reject 34 | _, request_id = self.client:request(method, params, function(err, res) 35 | if err then 36 | reject(err) 37 | else 38 | resolve(res) 39 | end 40 | end) 41 | end) 42 | function task.cancel() 43 | that.client:cancel_request(request_id) 44 | reject_(LSP.ErrorCodes.RequestCancelled) 45 | end 46 | return task 47 | end 48 | 49 | ${metaModel.requests.map(request => { 50 | const params = (() => { 51 | if (request.params) { 52 | const depends = [] as string[]; 53 | depends.push(`---@param params ${toTypeNotation(request.params as any, depends)}`); 54 | return depends.join('\n'); 55 | } 56 | return '---@param params nil'; 57 | })(); 58 | return dedent` 59 | ${params} 60 | ---@return ___kit___.kit.Async.AsyncTask|{cancel: fun()} 61 | function Client:${request.method.replace(/\//g, '_')}(params) 62 | local that, _, request_id, reject_ = self, nil, nil, nil 63 | ---@type ___kit___.kit.Async.AsyncTask|{cancel: fun()} 64 | local task = AsyncTask.new(function(resolve, reject) 65 | reject_ = reject 66 | _, request_id = self.client:request('${request.method}', params, function(err, res) 67 | if err then 68 | reject(err) 69 | else 70 | resolve(res) 71 | end 72 | end) 73 | end) 74 | function task.cancel() 75 | that.client:cancel_request(request_id) 76 | reject_(LSP.ErrorCodes.RequestCancelled) 77 | end 78 | return task 79 | end 80 | `; 81 | }).join('\n\n').trim() 82 | } 83 | 84 | ---Send notification. 85 | ---@param method string 86 | ---@param params table 87 | function Client:notify(method, params) 88 | self.client:notify(method, params) 89 | end 90 | 91 | ${metaModel.notifications.map(notification => { 92 | metaModel.notifications 93 | const params = (() => { 94 | if (notification.params) { 95 | const depends = [] as string[]; 96 | depends.push(`---@param params ${toTypeNotation(notification.params as any, depends)}`); 97 | return depends.join('\n'); 98 | } 99 | return '---@param params nil'; 100 | })(); 101 | return dedent` 102 | ${params} 103 | function Client:${notification.method.replace(/\//g, '_').replace(/\$/g, 'dollar')}(params) 104 | self.client:notify('${notification.method}', params) 105 | end 106 | `; 107 | }).join('\n\n').trim() 108 | } 109 | 110 | return Client 111 | `; 112 | fs.writeFileSync(`${__dirname}/../../lua/___kit___/kit/LSP/Client.lua`, client, 'utf-8'); 113 | })(); 114 | 115 | -------------------------------------------------------------------------------- /script/lsp/generate-protocol.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import dedent from 'ts-dedent'; 3 | import { generateAlias, generateEnum, generateStruct, method2type } from '.'; 4 | import * as MetaModel from '../../tmp/language-server-protocol/_specifications/lsp/3.18/metaModel/metaModel'; 5 | import metaModel from '../../tmp/language-server-protocol/_specifications/lsp/3.18/metaModel/metaModel.json'; 6 | 7 | (() => { 8 | const definitions = dedent` 9 | local LSP = {} 10 | 11 | ${generate(metaModel as unknown as MetaModel.MetaModel)} 12 | 13 | return LSP 14 | `;; 15 | fs.writeFileSync(`${__dirname}/../../lua/___kit___/kit/LSP/init.lua`, definitions, 'utf-8'); 16 | })(); 17 | 18 | /** 19 | * Generate all type definitions. 20 | */ 21 | function generate(metaModel: MetaModel.MetaModel) { 22 | return dedent` 23 | ${metaModel.enumerations.map(enums => { 24 | return generateEnum(enums); 25 | }).join('\n\n').trim()} 26 | 27 | ${metaModel.structures.map(struct => { 28 | return generateStruct(struct); 29 | }).join('\n\n').trim()} 30 | 31 | ${metaModel.requests.map(request => { 32 | return generateAlias({ 33 | name: method2type(request.method), 34 | type: request.result, 35 | }); 36 | }).join('\n\n').trim()} 37 | 38 | ${metaModel.typeAliases.map(alias => { 39 | return generateAlias(alias); 40 | }).join('\n\n').trim()} 41 | `; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /script/lsp/index.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'ts-dedent'; 2 | import * as MetaModel from '../../tmp/language-server-protocol/_specifications/lsp/3.18/metaModel/metaModel'; 3 | 4 | const state = { 5 | count: 0, 6 | } 7 | 8 | /** 9 | * Get lua-language-server's notation. 10 | */ 11 | export function toTypeNotation(type: MetaModel.Type, depends: string[] = []): string { 12 | depends = depends ?? []; 13 | 14 | if (type.kind === 'base') { 15 | return ({ 16 | DocumentUri: 'string', 17 | null: 'nil', 18 | uinteger: 'integer', 19 | integer: 'integer', 20 | decimal: 'integer', 21 | string: 'string', 22 | boolean: 'boolean', 23 | RegExp: 'string', 24 | URI: 'string', 25 | } as Record)[type.name]; 26 | } else if (type.kind === 'reference') { 27 | return toPackageName(type.name); 28 | } else if (type.kind === 'array') { 29 | return `${toTypeNotation(type.element, depends)}[]`; 30 | } else if (type.kind === 'tuple') { 31 | // TODO: Tuple notation is not supported propery. 32 | console.log('The `tuple` kind is limited support.') 33 | return `(${type.items.map(v => toTypeNotation(v, depends)).join(' | ')})[]`; 34 | } else if (type.kind === 'literal') { 35 | return `{ ${type.value.properties.map(prop => { 36 | return `${prop.name}${prop.optional ? '?' : ''}: ${toTypeNotation(prop.type, depends)}`; 37 | }).join(', ')} }`; 38 | } else if (type.kind === 'booleanLiteral') { 39 | return type.value ? 'true' : 'false'; 40 | } else if (type.kind === 'stringLiteral') { 41 | return JSON.stringify(type.value); 42 | } else if (type.kind === 'integerLiteral') { 43 | return type.value.toString(); 44 | } else if (type.kind === 'and') { 45 | console.log('The `and` kind is experimental support.') 46 | const name = 'IntersectionType' + ('0000' + ++state.count).slice(-2); 47 | depends.push(generateStruct({ 48 | name: name, 49 | properties: [], 50 | extends: type.items 51 | })); 52 | return toPackageName(name); 53 | } else if (type.kind === 'or') { 54 | return `(${type.items.map(v => toTypeNotation(v, depends)).join(' | ')})`; 55 | } else if (type.kind === 'map') { 56 | return `table<${toTypeNotation(type.key, depends)}, ${toTypeNotation(type.value, depends)}>`; 57 | } 58 | throw new Error(`Invalid type: ${JSON.stringify(type)}`); 59 | } 60 | 61 | /** 62 | * Generate enum definitions. 63 | */ 64 | export function generateEnum(enums: MetaModel.Enumeration) { 65 | return dedent` 66 | ---@enum ${toPackageName(enums.name)} 67 | LSP.${enums.name} = { 68 | ${enums.values.map((value: typeof enums.values[number]) => { 69 | switch (toTypeNotation(enums.type)) { 70 | case 'string': { 71 | return `${escapeLuaKeyword(value.name)} = ${JSON.stringify(value.value)},`; 72 | } 73 | case 'integer': { 74 | return `${escapeLuaKeyword(value.name)} = ${value.value},`; 75 | } 76 | } 77 | throw new Error(`Invalid enumeration type: ${enums.type.name}`); 78 | }).join('\n').trim()} 79 | } 80 | `; 81 | } 82 | 83 | /** 84 | * Generate struct definitions. 85 | */ 86 | export function generateStruct(struct: MetaModel.Structure): string { 87 | const extend = (() => { 88 | const parents = [ 89 | ...(struct.extends?.map(extend => { 90 | return extend.kind === 'reference' ? toPackageName(extend.name) : ''; 91 | }) ?? []), 92 | ...(struct.mixins?.map(mixin => { 93 | return mixin.kind === 'reference' ? toPackageName(mixin.name) : ''; 94 | }) ?? []) 95 | ].filter(Boolean); 96 | if (parents.length === 0) { 97 | return ''; 98 | } 99 | return dedent` : ${parents.join(', ')}`; 100 | })(); 101 | 102 | const mergedStruct = [] as string[]; 103 | 104 | const mainStruct = dedent` 105 | ---@class ${toPackageName(struct.name)}${extend} 106 | ${struct.properties 107 | .map((prop: MetaModel.Property) => { 108 | const documentation = prop.documentation ? ` ${formatDocumentation(prop.documentation ?? '')}` : ''; 109 | if (prop.type.kind === 'literal') { 110 | return `---@field public ${prop.name}${prop.optional ? '?' : ''} ${toPackageName(`${struct.name}.${prop.name}`)}${documentation}`; 111 | } 112 | return `---@field public ${prop.name}${prop.optional ? '?' : ''} ${toTypeNotation(prop.type, mergedStruct)}${documentation}`; 113 | }).join('\n').trim() 114 | } 115 | 116 | `; 117 | const literalStruct = dedent` 118 | ${struct.properties 119 | .filter(prop => prop.type.kind === 'literal') 120 | .map((prop: MetaModel.Property) => { 121 | if (prop.type.kind !== 'literal') { 122 | return; 123 | } 124 | return generateStruct({ 125 | name: `${struct.name}.${prop.name}`, 126 | ...prop.type.value, 127 | }); 128 | }).join('\n\n').trim() 129 | } 130 | `; 131 | return dedent` 132 | ${mergedStruct.join('\n\n').trim()} 133 | ${mainStruct} 134 | ${literalStruct} 135 | `; 136 | } 137 | 138 | /** 139 | * Generate alias definitions. 140 | */ 141 | export function generateAlias(alias: MetaModel.TypeAlias) { 142 | return dedent` 143 | ---@alias ${toPackageName(alias.name)} ${toTypeNotation(alias.type)} 144 | `; 145 | } 146 | 147 | /** 148 | * Get type name with namespace. 149 | */ 150 | export function toPackageName(name: string) { 151 | return `___kit___.kit.LSP.${name}`; 152 | } 153 | 154 | 155 | /** 156 | * Format documentation. 157 | */ 158 | export function formatDocumentation(s: string) { 159 | return s.replace(/(\r\n|\r|\n)/g, '
').trim(); 160 | } 161 | 162 | /** 163 | * Escape Lua keywords. 164 | */ 165 | function escapeLuaKeyword(field: string) { 166 | if (!field.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { 167 | return "['" + field + "']"; 168 | } 169 | 170 | return ({ 171 | 'function': "['function']", 172 | 'local': "['local']", 173 | 'while': "['while']", 174 | 'end': "['end']", 175 | 'repeat': "['repeat']", 176 | 'until': "['until']", 177 | 'if': "['if']", 178 | 'then': "['then']", 179 | 'else': "['else']", 180 | 'elseif': "['elseif']", 181 | 'for': "['for']", 182 | 'in': "['in']", 183 | 'do': "['do']", 184 | 'return': "['return']", 185 | 'break': "['break']", 186 | 'goto': "['goto']", 187 | 'not': "['not']", 188 | 'and': "['and']", 189 | 'or': "['or']", 190 | 'true': "['true']", 191 | 'false': "['false']", 192 | 'nil': "['nil']", 193 | })[field] ?? field; 194 | } 195 | 196 | export function method2type(method: string) { 197 | return method 198 | .replace(/^./g, s => { 199 | return s.toUpperCase(); 200 | }) 201 | .replace(/\/./g, s => { 202 | return s[1].toUpperCase(); 203 | }) + 'Response'; 204 | } 205 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | column_width = 1200 4 | quote_style = "AutoPreferSingle" 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "lib": ["esnext", "dom"], 7 | "jsx": "preserve", 8 | "noEmitHelpers": true, 9 | "importHelpers": true, 10 | "isolatedModules": true, 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "esModuleInterop": true, 17 | "experimentalDecorators": true, 18 | "newLine": "LF", 19 | "sourceMap": true, 20 | "declaration": false, 21 | "resolveJsonModule": true, 22 | }, 23 | "include": ["src/**/*", "tmp/**/*", "typings/**/*"] 24 | } 25 | --------------------------------------------------------------------------------