├── .github └── workflows │ └── integration.yml ├── .gitignore ├── .luacheckrc ├── .luarc.json ├── Makefile ├── README.md ├── lua └── linkedit │ ├── init.lua │ ├── init.spec.lua │ ├── kit │ ├── App │ │ ├── Cache.lua │ │ ├── Character.lua │ │ ├── Config.lua │ │ └── Event.lua │ ├── Async │ │ ├── AsyncTask.lua │ │ ├── RPC │ │ │ ├── Session.lua │ │ │ └── Thread │ │ │ │ └── init.lua │ │ ├── Worker.lua │ │ └── init.lua │ ├── IO │ │ └── init.lua │ ├── LSP │ │ ├── Client.lua │ │ ├── Position.lua │ │ ├── Range.lua │ │ └── init.lua │ ├── Spec │ │ └── init.lua │ ├── Vim │ │ ├── Keymap.lua │ │ ├── RegExp.lua │ │ └── Syntax.lua │ └── init.lua │ └── source │ ├── lsp_linked_editing_range.lua │ └── lsp_rename.lua ├── plugin └── linkedit.lua └── stylua.toml /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: integration 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest] 20 | 21 | runs-on: ${{matrix.os}} 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Install node.js 27 | uses: actions/setup-node@v2 28 | 29 | - name: Install neovim 30 | uses: rhysd/action-setup-vim@v1 31 | with: 32 | neovim: true 33 | version: nightly 34 | 35 | - name: Install LuaJIT 36 | uses: leafo/gh-actions-lua@v9.1.0 37 | with: 38 | luaVersion: 5.1 39 | 40 | - name: Install luarocks 41 | uses: leafo/gh-actions-luarocks@v4 42 | 43 | - name: Install luarocks packages 44 | run: | 45 | luarocks install luacheck 46 | luarocks install vusted 47 | 48 | - name: Format code 49 | uses: JohnnyMorganz/stylua-action@v2 50 | with: 51 | token: ${{ secrets.GITHUB_TOKEN }} 52 | version: latest 53 | args: --config-path stylua.toml --glob 'lua/**/*.lua' -- lua 54 | 55 | - name: Run check 56 | run: | 57 | make check 58 | 59 | - name: Auto commit if needed 60 | if: ${{ github.ref_name }} == 'main' 61 | uses: stefanzweifel/git-auto-commit-action@v4 62 | with: 63 | file_pattern: 'lua/**/*' 64 | commit_message: Auto generated commit 65 | 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | max_line_length = false 2 | globals = { 'vim', 'before_each', 'after_each', 'describe', 'it', 'assert' } 3 | ignore = { '311', '421', '431', '212' } 4 | 5 | -------------------------------------------------------------------------------- /.luarc.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 | "assert" 10 | ], 11 | "runtime.version": "LuaJIT", 12 | "runtime.path": [ 13 | "lua/?.lua", 14 | "lua/?/init.lua" 15 | ], 16 | "workspace.library": [ 17 | "${3rd}/luv/library" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | lint: 3 | luacheck --codes ./lua 4 | 5 | .PHONY: test 6 | test: 7 | vusted --output=gtest --pattern=.spec ./lua 8 | 9 | .PHONY: format 10 | format: 11 | stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua 12 | 13 | .PHONY: typecheck 14 | typecheck: 15 | rm -Rf $(pwd)/tmp/typecheck ||:; lua-language-server --check $(pwd)/lua --configpath=$(pwd)/.luarc.json --logpath=$(pwd)/tmp/typecheck > /dev/null; cat ./tmp/typecheck/check.json 2> /dev/null ||: 16 | 17 | .PHONY: check 18 | check: 19 | $(MAKE) lint 20 | $(MAKE) test 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linkedit 2 | 3 | The plugin supports `textDocument/linkedEditingRange` that defines in LSP spec. 4 | 5 | # Usage 6 | 7 | ```lua 8 | -- The default configuration. 9 | require('linkedit').setup { 10 | enabled = true, 11 | fetch_timeout = 500, 12 | keyword_pattern = [[\k*]], 13 | debug = false, 14 | sources = { 15 | { 16 | name = 'lsp_linked_editing_range', 17 | on = { 'insert', 'operator' }, 18 | }, 19 | }, 20 | } 21 | 22 | -- The filetype specific configuration example. 23 | require('linkedit').setup.filetype('yaml', { 24 | enabled = false, 25 | }) 26 | ``` 27 | 28 | # Built-in 29 | 30 | ### lsp_linked_editing_range (default: enabled) 31 | 32 | The `textDocument/linkedEditingRange` source. 33 | This source works only if your language server supports that method. 34 | 35 | ### lsp_rename (default: disabled) 36 | 37 | The `textDocument/rename` source. 38 | This source works only if your language server supports that method. 39 | 40 | The LSP's rename request is supporting multifile rename. 41 | But this plugin does not support it. 42 | 43 | 44 | -------------------------------------------------------------------------------- /lua/linkedit/init.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | local RegExp = require('linkedit.kit.Vim.RegExp') 3 | local Async = require('linkedit.kit.Async') 4 | local Config = require('linkedit.kit.App.Config') 5 | local ns = vim.api.nvim_create_namespace('linkedit') 6 | 7 | ---@class linkedit.Source 8 | ---@field public fetch fun(self: unknown, params: linkedit.kit.LSP.LinkedEditingRangeParams): linkedit.kit.Async.AsyncTask linkedit.kit.LSP.TextDocumentLinkedEditingRangeResponse 9 | 10 | ---@class linkedit.kit.App.Config.Schema 11 | ---@field public enabled boolean 12 | ---@field public sources { name: string }[] 13 | ---@field public fetch_timeout number 14 | ---@field public keyword_pattern string 15 | ---@field public debug? boolean 16 | 17 | ---Get range from mark_id. 18 | ---@param mark_id number 19 | ---@return { s: { [1]: number, [2]: number }, e: { [1]: number, [2]: number } } 20 | local function get_range_from_mark(mark_id) 21 | local mark = vim.api.nvim_buf_get_extmark_by_id(0, ns, mark_id, { 22 | details = true, 23 | }) 24 | local s = { mark[1], mark[2] } 25 | local e = { mark[3].end_row, mark[3].end_col } 26 | if s[1] > e[1] or (s[1] == e[1] and s[2] > e[2]) then 27 | local t = s 28 | s = e 29 | e = t 30 | end 31 | return { s = s, e = e } 32 | end 33 | 34 | ---Get text from mark_id. 35 | ---@param mark_id number 36 | ---@return string 37 | local function get_text_from_mark(mark_id) 38 | local range = get_range_from_mark(mark_id) 39 | local lines = vim.api.nvim_buf_get_text(0, range.s[1], range.s[2], range.e[1], range.e[2], {}) 40 | return table.concat(lines, '\n') 41 | end 42 | 43 | ---Set text for mark_id. 44 | ---@param mark_id number 45 | ---@param text string 46 | local function set_text_for_mark(mark_id, text) 47 | local range = get_range_from_mark(mark_id) 48 | vim.api.nvim_buf_set_text(0, range.s[1], range.s[2], range.e[1], range.e[2], vim.split(text, '\n', { plain = true })) 49 | end 50 | 51 | local linkedit = { 52 | config = Config.new({ 53 | enabled = true, 54 | fetch_timeout = 500, 55 | keyword_pattern = [[\h\?\%(\w*\)\%(\.\w*\)*]], 56 | debug = false, 57 | sources = { 58 | { 59 | name = 'lsp_linked_editing_range', 60 | on = { 'insert', 'operator' }, 61 | }, 62 | }, 63 | }) 64 | } 65 | 66 | ---Setup interface for global/filetype/buffer. 67 | linkedit.setup = linkedit.config:create_setup_interface() 68 | linkedit.setup.filetype('php', { 69 | keyword_pattern = [[\$\?\k*]], 70 | }) 71 | 72 | ---@type table 73 | linkedit.registry = {} 74 | 75 | ---Clear current state. 76 | function linkedit.clear() 77 | vim.api.nvim_buf_clear_namespace(0, ns, 0, -1) 78 | end 79 | 80 | ---Fetch linked editing ranges. 81 | function linkedit.fetch(on) 82 | linkedit.clear() 83 | 84 | local ok, res = pcall(function() 85 | Async.run(function() 86 | local params = vim.lsp.util.make_position_params() 87 | 88 | ---@type linkedit.kit.LSP.TextDocumentLinkedEditingRangeResponse 89 | local response 90 | for _, source_config in ipairs(linkedit.config:get().sources) do 91 | local on_config = kit.get(source_config, { 'on' }, { 'insert', 'operator' }) 92 | if vim.tbl_contains(on_config, on) then 93 | local source = linkedit.registry[source_config.name] 94 | if source then 95 | response = source:fetch(params):catch(function(err) 96 | if linkedit.config:get().debug then 97 | vim.print(err) 98 | end 99 | return nil 100 | end):await() 101 | if response and response.ranges and #response.ranges > 0 then 102 | break 103 | end 104 | end 105 | end 106 | end 107 | if not response then 108 | return 109 | end 110 | if #response.ranges < 2 then 111 | return 112 | end 113 | 114 | local unique = {} 115 | for _, range in ipairs(response.ranges --[=[@as linkedit.kit.LSP.Range[]]=]) do 116 | local range_id = table.concat({ range.start.line, range.start.character, range['end'].line, range['end'].character }, ':') 117 | if not unique[range_id] then 118 | unique[range_id] = true 119 | vim.api.nvim_buf_set_extmark(0, ns, range.start.line, range.start.character, { 120 | end_line = range['end'].line, 121 | end_col = range['end'].character, 122 | hl_group = 'LinkedEditingRange', 123 | right_gravity = false, 124 | end_right_gravity = true, 125 | }) 126 | end 127 | end 128 | end):sync(linkedit.config:get().fetch_timeout) 129 | end) 130 | if not ok then 131 | if linkedit.config:get().debug then 132 | vim.print(res) 133 | end 134 | end 135 | end 136 | 137 | ---Sync all linked editing range. 138 | function linkedit.sync() 139 | local cursor = vim.api.nvim_win_get_cursor(0) 140 | cursor[1] = cursor[1] - 1 141 | 142 | -- ignote changes that occurred by undo/redo. 143 | local undotree = vim.fn.undotree() 144 | if undotree.seq_last ~= undotree.seq_cur then 145 | return 146 | end 147 | 148 | ---@type number[] 149 | local mark_ids = kit.map(vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, {}), function(mark) 150 | return mark[1] 151 | end) 152 | 153 | -- get origin mark. 154 | local origin_mark_id = nil 155 | for _, mark_id in ipairs(mark_ids) do 156 | local range = get_range_from_mark(mark_id) 157 | local included = true 158 | included = included and (range.s[1] < cursor[1] or (range.s[1] == cursor[1] and range.s[2] <= cursor[2])) 159 | included = included and (range.e[1] > cursor[1] or (range.e[1] == cursor[1] and range.e[2] >= cursor[2])) 160 | if included then 161 | origin_mark_id = mark_id 162 | break 163 | end 164 | end 165 | if not origin_mark_id then 166 | return 167 | end 168 | 169 | -- get origin text. 170 | local origin_text = get_text_from_mark(origin_mark_id) 171 | if RegExp.get('^' .. linkedit.config:get().keyword_pattern .. '$'):match_str(origin_text) ~= 0 then 172 | return linkedit.clear() 173 | end 174 | 175 | -- apply all marks. 176 | for _, mark_id in ipairs(mark_ids) do 177 | if mark_id ~= origin_mark_id then 178 | set_text_for_mark(mark_id, origin_text) 179 | end 180 | end 181 | end 182 | 183 | linkedit.registry['lsp_linked_editing_range'] = require('linkedit.source.lsp_linked_editing_range').new() 184 | linkedit.registry['lsp_rename'] = require('linkedit.source.lsp_rename').new() 185 | 186 | return linkedit 187 | -------------------------------------------------------------------------------- /lua/linkedit/init.spec.lua: -------------------------------------------------------------------------------- 1 | describe('linkedit', function() 2 | it('should ok', function() 3 | assert.is_true(true) 4 | end) 5 | end) 6 | -------------------------------------------------------------------------------- /lua/linkedit/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 linkedit.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 | ---@param key string[]|string 61 | ---@param callback function(): T 62 | ---@return T 63 | function Cache:ensure(key, callback) 64 | if not self:has(key) then 65 | self:set(key, callback()) 66 | end 67 | return self:get(key) 68 | end 69 | 70 | return Cache 71 | -------------------------------------------------------------------------------- /lua/linkedit/kit/App/Character.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: discard-returns 2 | 3 | local Character = {} 4 | 5 | ---@type table 6 | Character.alpha = {} 7 | string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char) 8 | Character.alpha[string.byte(char)] = char 9 | end) 10 | 11 | ---@type table 12 | Character.digit = {} 13 | string.gsub('1234567890', '.', function(char) 14 | Character.digit[string.byte(char)] = char 15 | end) 16 | 17 | ---@type table 18 | Character.white = {} 19 | string.gsub(' \t\n', '.', function(char) 20 | Character.white[string.byte(char)] = char 21 | end) 22 | 23 | ---Return specified byte is alpha or not. 24 | ---@param byte integer 25 | ---@return boolean 26 | function Character.is_alpha(byte) 27 | return Character.alpha[byte] ~= nil or Character.alpha[byte + 32] ~= nil 28 | end 29 | 30 | ---Return specified byte is digit or not. 31 | ---@param byte integer 32 | ---@return boolean 33 | function Character.is_digit(byte) 34 | return Character.digit[byte] ~= nil 35 | end 36 | 37 | ---Return specified byte is alpha or not. 38 | ---@param byte integer 39 | ---@return boolean 40 | function Character.is_alnum(byte) 41 | return Character.is_alpha(byte) or Character.is_digit(byte) 42 | end 43 | 44 | ---Return specified byte is white or not. 45 | ---@param byte integer 46 | ---@return boolean 47 | function Character.is_white(byte) 48 | return Character.white[byte] ~= nil 49 | end 50 | 51 | ---Return specified byte is symbol or not. 52 | ---@param byte integer 53 | ---@return boolean 54 | function Character.is_symbol(byte) 55 | return not Character.is_alnum(byte) and not Character.is_white(byte) 56 | end 57 | 58 | return Character 59 | -------------------------------------------------------------------------------- /lua/linkedit/kit/App/Config.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | local Cache = require('linkedit.kit.App.Cache') 3 | 4 | ---@class linkedit.kit.App.Config.Schema 5 | 6 | ---@alias linkedit.kit.App.Config.SchemaInternal linkedit.kit.App.Config.Schema|{ revision: integer } 7 | 8 | ---@class linkedit.kit.App.Config 9 | ---@field private _cache linkedit.kit.App.Cache 10 | ---@field private _default linkedit.kit.App.Config.SchemaInternal 11 | ---@field private _global linkedit.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 linkedit.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 linkedit.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 linkedit.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 linkedit.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 linkedit.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: linkedit.kit.App.Config.Schema)|{ filetype: fun(filetypes: string|string[], config: linkedit.kit.App.Config.Schema), buffer: fun(bufnr: integer, config: linkedit.kit.App.Config.Schema) } 79 | function Config:create_setup_interface() 80 | return setmetatable({ 81 | ---@param filetypes string|string[] 82 | ---@param config linkedit.kit.App.Config.Schema 83 | filetype = function(filetypes, config) 84 | self:filetype(filetypes, config) 85 | end, 86 | ---@param bufnr integer 87 | ---@param config linkedit.kit.App.Config.Schema 88 | buffer = function(bufnr, config) 89 | self:buffer(bufnr, config) 90 | end, 91 | }, { 92 | ---@param config linkedit.kit.App.Config.Schema 93 | __call = function(_, config) 94 | self:global(config) 95 | end, 96 | }) 97 | end 98 | 99 | return Config 100 | -------------------------------------------------------------------------------- /lua/linkedit/kit/App/Event.lua: -------------------------------------------------------------------------------- 1 | ---@class linkedit.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/linkedit/kit/Async/AsyncTask.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: invisible 2 | local uv = require('luv') 3 | local kit = require('linkedit.kit') 4 | 5 | local is_thread = vim.is_thread() 6 | 7 | ---@class linkedit.kit.Async.AsyncTask 8 | ---@field private value any 9 | ---@field private status linkedit.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 linkedit.kit.Async.AsyncTask 18 | ---@param status linkedit.kit.Async.AsyncTask.Status 19 | ---@param value any 20 | local function settle(task, status, value) 21 | task.status = status 22 | task.value = value 23 | for _, c in ipairs(task.children) do 24 | c() 25 | end 26 | 27 | if status == AsyncTask.Status.Rejected then 28 | if not task.chained and not task.synced then 29 | local timer = uv.new_timer() 30 | timer:start( 31 | 0, 32 | 0, 33 | kit.safe_schedule_wrap(function() 34 | timer:stop() 35 | timer:close() 36 | if not task.chained and not task.synced then 37 | AsyncTask.on_unhandled_rejection(value) 38 | end 39 | end) 40 | ) 41 | end 42 | end 43 | end 44 | 45 | ---@enum linkedit.kit.Async.AsyncTask.Status 46 | AsyncTask.Status = { 47 | Pending = 0, 48 | Fulfilled = 1, 49 | Rejected = 2, 50 | } 51 | 52 | ---Handle unhandled rejection. 53 | ---@param err any 54 | function AsyncTask.on_unhandled_rejection(err) 55 | error('AsyncTask.on_unhandled_rejection: ' .. tostring(err)) 56 | end 57 | 58 | ---Return the value is AsyncTask or not. 59 | ---@param value any 60 | ---@return boolean 61 | function AsyncTask.is(value) 62 | return getmetatable(value) == AsyncTask 63 | end 64 | 65 | ---Resolve all tasks. 66 | ---@param tasks any[] 67 | ---@return linkedit.kit.Async.AsyncTask 68 | function AsyncTask.all(tasks) 69 | return AsyncTask.new(function(resolve, reject) 70 | local values = {} 71 | local count = 0 72 | for i, task in ipairs(tasks) do 73 | task:dispatch(function(value) 74 | values[i] = value 75 | count = count + 1 76 | if #tasks == count then 77 | resolve(values) 78 | end 79 | end, reject) 80 | end 81 | end) 82 | end 83 | 84 | ---Resolve first resolved task. 85 | ---@param tasks any[] 86 | ---@return linkedit.kit.Async.AsyncTask 87 | function AsyncTask.race(tasks) 88 | return AsyncTask.new(function(resolve, reject) 89 | for _, task in ipairs(tasks) do 90 | task:dispatch(resolve, reject) 91 | end 92 | end) 93 | end 94 | 95 | ---Create resolved AsyncTask. 96 | ---@param v any 97 | ---@return linkedit.kit.Async.AsyncTask 98 | function AsyncTask.resolve(v) 99 | if AsyncTask.is(v) then 100 | return v 101 | end 102 | return AsyncTask.new(function(resolve) 103 | resolve(v) 104 | end) 105 | end 106 | 107 | ---Create new AsyncTask. 108 | ---@NOET: The AsyncTask has similar interface to JavaScript Promise but the AsyncTask can be worked as synchronous. 109 | ---@param v any 110 | ---@return linkedit.kit.Async.AsyncTask 111 | function AsyncTask.reject(v) 112 | if AsyncTask.is(v) then 113 | return v 114 | end 115 | return AsyncTask.new(function(_, reject) 116 | reject(v) 117 | end) 118 | end 119 | 120 | ---Create new async task object. 121 | ---@param runner fun(resolve?: fun(value: any?), reject?: fun(err: any?)) 122 | function AsyncTask.new(runner) 123 | local self = setmetatable({}, AsyncTask) 124 | 125 | self.value = nil 126 | self.status = AsyncTask.Status.Pending 127 | self.synced = false 128 | self.chained = false 129 | self.children = {} 130 | local ok, err = pcall(runner, function(res) 131 | if self.status == AsyncTask.Status.Pending then 132 | settle(self, AsyncTask.Status.Fulfilled, res) 133 | end 134 | end, function(err) 135 | if self.status == AsyncTask.Status.Pending then 136 | settle(self, AsyncTask.Status.Rejected, err) 137 | end 138 | end) 139 | if not ok then 140 | settle(self, AsyncTask.Status.Rejected, err) 141 | end 142 | return self 143 | end 144 | 145 | ---Sync async task. 146 | ---@NOTE: This method uses `vim.wait` so that this can't wait the typeahead to be empty. 147 | ---@param timeout? number 148 | ---@return any 149 | function AsyncTask:sync(timeout) 150 | timeout = timeout or 1000 151 | 152 | self.synced = true 153 | 154 | local time = uv.now() 155 | while uv.now() - time <= timeout do 156 | if self.status ~= AsyncTask.Status.Pending then 157 | break 158 | end 159 | if is_thread then 160 | uv.run('once') 161 | else 162 | vim.wait(0) 163 | end 164 | end 165 | if self.status == AsyncTask.Status.Rejected then 166 | error(self.value, 2) 167 | end 168 | if self.status ~= AsyncTask.Status.Fulfilled then 169 | error('AsyncTask:sync is timeout.', 2) 170 | end 171 | return self.value 172 | end 173 | 174 | ---Await async task. 175 | ---@param schedule? boolean 176 | ---@return any 177 | function AsyncTask:await(schedule) 178 | local Async = require('linkedit.kit.Async') 179 | local ok, res = pcall(Async.await, self) 180 | if not ok then 181 | error(res, 2) 182 | end 183 | if schedule then 184 | Async.await(Async.schedule()) 185 | end 186 | return res 187 | end 188 | 189 | ---Return current state of task. 190 | ---@return { status: linkedit.kit.Async.AsyncTask.Status, value: any } 191 | function AsyncTask:state() 192 | return { 193 | status = self.status, 194 | value = self.value, 195 | } 196 | end 197 | 198 | ---Register next step. 199 | ---@param on_fulfilled fun(value: any): any 200 | function AsyncTask:next(on_fulfilled) 201 | return self:dispatch(on_fulfilled, function(err) 202 | error(err, 2) 203 | end) 204 | end 205 | 206 | ---Register catch step. 207 | ---@param on_rejected fun(value: any): any 208 | ---@return linkedit.kit.Async.AsyncTask 209 | function AsyncTask:catch(on_rejected) 210 | return self:dispatch(function(value) 211 | return value 212 | end, on_rejected) 213 | end 214 | 215 | ---Dispatch task state. 216 | ---@param on_fulfilled fun(value: any): any 217 | ---@param on_rejected fun(err: any): any 218 | ---@return linkedit.kit.Async.AsyncTask 219 | function AsyncTask:dispatch(on_fulfilled, on_rejected) 220 | self.chained = true 221 | 222 | local function dispatch(resolve, reject) 223 | local on_next = self.status == AsyncTask.Status.Fulfilled and on_fulfilled or on_rejected 224 | local ok, res = pcall(on_next, self.value) 225 | if AsyncTask.is(res) then 226 | res:dispatch(resolve, reject) 227 | else 228 | if ok then 229 | resolve(res) 230 | else 231 | reject(res) 232 | end 233 | end 234 | end 235 | 236 | if self.status == AsyncTask.Status.Pending then 237 | return AsyncTask.new(function(resolve, reject) 238 | table.insert(self.children, function() 239 | dispatch(resolve, reject) 240 | end) 241 | end) 242 | end 243 | return AsyncTask.new(dispatch) 244 | end 245 | 246 | return AsyncTask 247 | -------------------------------------------------------------------------------- /lua/linkedit/kit/Async/RPC/Session.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: invisible, redefined-local 2 | local kit = require('linkedit.kit') 3 | local mpack = require('mpack') 4 | local Async = require('linkedit.kit.Async') 5 | 6 | ---@class linkedit.kit.Async.RPC.Session 7 | ---@field private mpack_session any 8 | ---@field private stdin uv.uv_pipe_t 9 | ---@field private stdout uv.uv_pipe_t 10 | ---@field private _on_request table 11 | ---@field private _on_notification table 12 | local Session = {} 13 | Session.__index = Session 14 | 15 | ---Create new session. 16 | ---@return linkedit.kit.Async.RPC.Session 17 | function Session.new() 18 | local self = setmetatable({}, Session) 19 | self.mpack_session = mpack.Session({ unpack = kit.Unpacker }) 20 | self.stdin = nil 21 | self.stdout = nil 22 | self._on_request = {} 23 | self._on_notification = {} 24 | return self 25 | end 26 | 27 | ---Connect stdin/stdout. 28 | ---@param stdin uv.uv_pipe_t 29 | ---@param stdout uv.uv_pipe_t 30 | function Session:connect(stdin, stdout) 31 | self.stdin = stdin 32 | self.stdout = stdout 33 | 34 | self.stdin:read_start(function(err, data) 35 | if err then 36 | error(err) 37 | end 38 | if not data then 39 | return 40 | end 41 | 42 | local offset = 1 43 | local length = #data 44 | while offset <= length do 45 | local type, id_or_cb, method_or_error, params_or_result, new_offset = self.mpack_session:receive(data, offset) 46 | if type == 'request' then 47 | local request_id, method, params = id_or_cb, method_or_error, params_or_result 48 | Async.resolve():next(function() 49 | return Async.run(function() 50 | return self._on_request[method](params) 51 | end) 52 | end):next(function(res) 53 | self.stdout:write(self.mpack_session:reply(request_id) .. kit.pack(mpack.NIL) .. kit.pack(res)) 54 | end):catch(function(err_) 55 | self.stdout:write(self.mpack_session:reply(request_id) .. kit.pack(err_) .. kit.pack(mpack.NIL)) 56 | end) 57 | elseif type == 'notification' then 58 | local method, params = method_or_error, params_or_result 59 | self._on_notification[method](params) 60 | elseif type == 'response' then 61 | local callback, err_, res = id_or_cb, method_or_error, params_or_result 62 | if err_ == mpack.NIL then 63 | callback(nil, res) 64 | else 65 | callback(err_, nil) 66 | end 67 | end 68 | offset = new_offset 69 | end 70 | end) 71 | end 72 | 73 | ---Close session. 74 | function Session:close() 75 | self.stdin:close() 76 | self.stdout:close() 77 | self.stdin = nil 78 | self.stdout = nil 79 | end 80 | 81 | ---Add request handler. 82 | ---@param method string 83 | ---@param callback fun(params: table): any 84 | function Session:on_request(method, callback) 85 | self._on_request[method] = callback 86 | end 87 | 88 | ---Add notification handler. 89 | ---@param method string 90 | ---@param callback fun(params: table) 91 | function Session:on_notification(method, callback) 92 | self._on_notification[method] = callback 93 | end 94 | 95 | ---Send request to the peer. 96 | ---@param method string 97 | ---@param params table 98 | ---@return linkedit.kit.Async.AsyncTask 99 | function Session:request(method, params) 100 | return Async.new(function(resolve, reject) 101 | local request = self.mpack_session:request(function(err, res) 102 | if err then 103 | reject(err) 104 | else 105 | resolve(res) 106 | end 107 | end) 108 | self.stdout:write(request .. kit.pack(method) .. kit.pack(params)) 109 | end) 110 | end 111 | 112 | ---Send notification to the peer. 113 | ---@param method string 114 | ---@param params table 115 | function Session:notify(method, params) 116 | self.stdout:write(self.mpack_session:notify() .. kit.pack(method) .. kit.pack(params)) 117 | end 118 | 119 | return Session 120 | -------------------------------------------------------------------------------- /lua/linkedit/kit/Async/RPC/Thread/init.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: redefined-local 2 | 3 | local uv = require('luv') 4 | local Session = require('linkedit.kit.Async.RPC.Session') 5 | 6 | ---@class linkedit.kit.Async.RPC.Thread 7 | ---@field private thread uv.luv_thread_t 8 | ---@field private session linkedit.kit.Async.RPC.Session 9 | local Thread = {} 10 | Thread.__index = Thread 11 | 12 | ---Create new thread. 13 | ---@param dispatcher_ fun(session: linkedit.kit.Async.RPC.Session) 14 | function Thread.new(dispatcher_) 15 | local self = setmetatable({}, Thread) 16 | 17 | -- prepare server connections. 18 | local client2server_fds = uv.socketpair(nil, nil, { nonblock = true }, { nonblock = true }) 19 | local server2client_fds = uv.socketpair(nil, nil, { nonblock = true }, { nonblock = true }) 20 | self.thread = uv.new_thread(function(dispatcher, client2server_fd, server2client_fd) 21 | local uv = require('luv') 22 | local running = true 23 | 24 | local server2client_write = uv.new_tcp() 25 | server2client_write:open(server2client_fd) 26 | local client2server_read = uv.new_tcp() 27 | client2server_read:open(client2server_fd) 28 | local Session = require('linkedit.kit.Async.RPC.Session') 29 | local session = Session.new() 30 | session:on_request('$/exit', function() 31 | running = false 32 | end) 33 | assert(loadstring(dispatcher))(session) 34 | session:connect(client2server_read, server2client_write) 35 | while running do 36 | uv.run('once') 37 | end 38 | end, string.dump(dispatcher_), client2server_fds[2], server2client_fds[1]) 39 | 40 | -- prepare client connections. 41 | local client2server_write = uv.new_tcp() 42 | client2server_write:open(client2server_fds[1]) 43 | local server2client_read = uv.new_tcp() 44 | server2client_read:open(server2client_fds[2]) 45 | self.session = Session.new() 46 | self.session:connect(server2client_read, client2server_write) 47 | return self 48 | end 49 | 50 | ---Add request handler. 51 | ---@param method string 52 | ---@param callback fun(params: table): any 53 | function Thread:on_request(method, callback) 54 | self.session:on_request(method, callback) 55 | end 56 | 57 | ---Add notification handler. 58 | ---@param method string 59 | ---@param callback fun(params: table) 60 | function Thread:on_notification(method, callback) 61 | self.session:on_notification(method, callback) 62 | end 63 | 64 | ---Send request to the peer. 65 | ---@param method string 66 | ---@param params table 67 | ---@return linkedit.kit.Async.AsyncTask 68 | function Thread:request(method, params) 69 | return self.session:request(method, params) 70 | end 71 | 72 | ---Send notification to the peer. 73 | ---@param method string 74 | ---@param params table 75 | function Thread:notify(method, params) 76 | self.session:notify(method, params) 77 | end 78 | 79 | ---Close session. 80 | function Thread:close() 81 | return self:request('$/exit', {}):next(function() 82 | self.session:close() 83 | end) 84 | end 85 | 86 | return Thread 87 | -------------------------------------------------------------------------------- /lua/linkedit/kit/Async/Worker.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local AsyncTask = require('linkedit.kit.Async.AsyncTask') 3 | 4 | ---@class linkedit.kit.Async.WorkerOption 5 | ---@field public runtimepath string[] 6 | 7 | ---@class linkedit.kit.Async.Worker 8 | local Worker = {} 9 | Worker.__index = Worker 10 | 11 | ---Create a new worker. 12 | ---@param runner function 13 | function Worker.new(runner) 14 | local self = setmetatable({}, Worker) 15 | self.runner = string.dump(runner) 16 | return self 17 | end 18 | 19 | ---Call worker function. 20 | ---@return linkedit.kit.Async.AsyncTask 21 | function Worker:__call(...) 22 | local args_ = { ... } 23 | return AsyncTask.new(function(resolve, reject) 24 | uv.new_work(function(runner, args, option) 25 | args = vim.mpack.decode(args) 26 | option = vim.mpack.decode(option) 27 | 28 | --Initialize cwd. 29 | require('luv').chdir(option.cwd) 30 | 31 | --Initialize package.loaders. 32 | table.insert(package.loaders, 2, vim._load_package) 33 | 34 | --Run runner function. 35 | local ok, res = pcall(function() 36 | return require('linkedit.kit.Async.AsyncTask').resolve(assert(loadstring(runner))(unpack(args))):sync() 37 | end) 38 | 39 | res = vim.mpack.encode({ res }) 40 | 41 | --Return error or result. 42 | if not ok then 43 | return res, nil 44 | else 45 | return nil, res 46 | end 47 | end, function(err, res) 48 | if err then 49 | reject(vim.mpack.decode(err)[1]) 50 | else 51 | resolve(vim.mpack.decode(res)[1]) 52 | end 53 | end):queue( 54 | self.runner, 55 | vim.mpack.encode(args_), 56 | vim.mpack.encode({ 57 | cwd = uv.cwd(), 58 | }) 59 | ) 60 | end) 61 | end 62 | 63 | return Worker 64 | -------------------------------------------------------------------------------- /lua/linkedit/kit/Async/init.lua: -------------------------------------------------------------------------------- 1 | local AsyncTask = require('linkedit.kit.Async.AsyncTask') 2 | 3 | local Async = {} 4 | 5 | ---@type table 6 | Async.___threads___ = {} 7 | 8 | ---Alias of AsyncTask.all. 9 | ---@param tasks linkedit.kit.Async.AsyncTask[] 10 | ---@return linkedit.kit.Async.AsyncTask 11 | function Async.all(tasks) 12 | return AsyncTask.all(tasks) 13 | end 14 | 15 | ---Alias of AsyncTask.race. 16 | ---@param tasks linkedit.kit.Async.AsyncTask[] 17 | ---@return linkedit.kit.Async.AsyncTask 18 | function Async.race(tasks) 19 | return AsyncTask.race(tasks) 20 | end 21 | 22 | ---Alias of AsyncTask.resolve(v). 23 | ---@param v any 24 | ---@return linkedit.kit.Async.AsyncTask 25 | function Async.resolve(v) 26 | return AsyncTask.resolve(v) 27 | end 28 | 29 | ---Alias of AsyncTask.reject(v). 30 | ---@param v any 31 | ---@return linkedit.kit.Async.AsyncTask 32 | function Async.reject(v) 33 | return AsyncTask.reject(v) 34 | end 35 | 36 | ---Alias of AsyncTask.new(...). 37 | ---@param runner fun(resolve: fun(value: any), reject: fun(err: any)) 38 | ---@return linkedit.kit.Async.AsyncTask 39 | function Async.new(runner) 40 | return AsyncTask.new(runner) 41 | end 42 | 43 | ---Run async function immediately. 44 | ---@generic T: fun(): linkedit.kit.Async.AsyncTask 45 | ---@param runner T 46 | ---@return linkedit.kit.Async.AsyncTask 47 | function Async.run(runner) 48 | return Async.async(runner)() 49 | end 50 | 51 | ---Return current context is async coroutine or not. 52 | ---@return boolean 53 | function Async.in_context() 54 | return Async.___threads___[coroutine.running()] ~= nil 55 | end 56 | 57 | ---Create async function. 58 | ---@generic T: fun(...): linkedit.kit.Async.AsyncTask 59 | ---@param runner T 60 | ---@return T 61 | function Async.async(runner) 62 | return function(...) 63 | local args = { ... } 64 | 65 | local thread = coroutine.create(runner) 66 | return AsyncTask.new(function(resolve, reject) 67 | Async.___threads___[thread] = 1 68 | 69 | local function next_step(ok, v) 70 | if coroutine.status(thread) == 'dead' then 71 | Async.___threads___[thread] = nil 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 | return 82 | end 83 | 84 | v:dispatch(function(...) 85 | next_step(coroutine.resume(thread, true, ...)) 86 | end, function(...) 87 | next_step(coroutine.resume(thread, false, ...)) 88 | end) 89 | end 90 | 91 | next_step(coroutine.resume(thread, unpack(args))) 92 | end) 93 | end 94 | end 95 | 96 | ---Await async task. 97 | ---@param task linkedit.kit.Async.AsyncTask 98 | ---@return any 99 | function Async.await(task) 100 | if not Async.___threads___[coroutine.running()] then 101 | error('`Async.await` must be called in async context.') 102 | end 103 | if not AsyncTask.is(task) then 104 | error('`Async.await` must be called with AsyncTask.') 105 | end 106 | 107 | local ok, res = coroutine.yield(task) 108 | if not ok then 109 | error(res, 2) 110 | end 111 | return res 112 | end 113 | 114 | ---Create vim.schedule task. 115 | ---@return linkedit.kit.Async.AsyncTask 116 | function Async.schedule() 117 | return AsyncTask.new(function(resolve) 118 | vim.schedule(resolve) 119 | end) 120 | end 121 | 122 | ---Create vim.defer_fn task. 123 | ---@param timeout integer 124 | ---@return linkedit.kit.Async.AsyncTask 125 | function Async.timeout(timeout) 126 | return AsyncTask.new(function(resolve) 127 | vim.defer_fn(resolve, timeout) 128 | end) 129 | end 130 | 131 | ---Create async function from callback function. 132 | ---@generic T: ... 133 | ---@param runner fun(...: T) 134 | ---@param option? { schedule?: boolean, callback?: integer } 135 | ---@return fun(...: T): linkedit.kit.Async.AsyncTask 136 | function Async.promisify(runner, option) 137 | option = option or {} 138 | option.schedule = not vim.is_thread() and (option.schedule or false) 139 | option.callback = option.callback or nil 140 | return function(...) 141 | local args = { ... } 142 | return AsyncTask.new(function(resolve, reject) 143 | local max = #args + 1 144 | local pos = math.min(option.callback or max, max) 145 | table.insert(args, pos, function(err, ...) 146 | if option.schedule and vim.in_fast_event() then 147 | resolve = vim.schedule_wrap(resolve) 148 | reject = vim.schedule_wrap(reject) 149 | end 150 | if err then 151 | reject(err) 152 | else 153 | resolve(...) 154 | end 155 | end) 156 | runner(unpack(args)) 157 | end) 158 | end 159 | end 160 | 161 | return Async 162 | -------------------------------------------------------------------------------- /lua/linkedit/kit/IO/init.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local Async = require('linkedit.kit.Async') 3 | 4 | local is_windows = uv.os_uname().sysname:lower() == 'windows' 5 | 6 | ---@see https://github.com/luvit/luvit/blob/master/deps/fs.lua 7 | local IO = {} 8 | 9 | ---@class linkedit.kit.IO.UV.Stat 10 | ---@field public dev integer 11 | ---@field public mode integer 12 | ---@field public nlink integer 13 | ---@field public uid integer 14 | ---@field public gid integer 15 | ---@field public rdev integer 16 | ---@field public ino integer 17 | ---@field public size integer 18 | ---@field public blksize integer 19 | ---@field public blocks integer 20 | ---@field public flags integer 21 | ---@field public gen integer 22 | ---@field public atime { sec: integer, nsec: integer } 23 | ---@field public mtime { sec: integer, nsec: integer } 24 | ---@field public ctime { sec: integer, nsec: integer } 25 | ---@field public birthtime { sec: integer, nsec: integer } 26 | ---@field public type string 27 | 28 | ---@enum linkedit.kit.IO.UV.AccessMode 29 | IO.AccessMode = { 30 | r = 'r', 31 | rs = 'rs', 32 | sr = 'sr', 33 | ['r+'] = 'r+', 34 | ['rs+'] = 'rs+', 35 | ['sr+'] = 'sr+', 36 | w = 'w', 37 | wx = 'wx', 38 | xw = 'xw', 39 | ['w+'] = 'w+', 40 | ['wx+'] = 'wx+', 41 | ['xw+'] = 'xw+', 42 | a = 'a', 43 | ax = 'ax', 44 | xa = 'xa', 45 | ['a+'] = 'a+', 46 | ['ax+'] = 'ax+', 47 | ['xa+'] = 'xa+', 48 | } 49 | 50 | ---@enum linkedit.kit.IO.WalkStatus 51 | IO.WalkStatus = { 52 | SkipDir = 1, 53 | Break = 2, 54 | } 55 | 56 | ---@type fun(path: string): linkedit.kit.Async.AsyncTask 57 | IO.fs_stat = Async.promisify(uv.fs_stat) 58 | 59 | ---@type fun(path: string): linkedit.kit.Async.AsyncTask 60 | IO.fs_unlink = Async.promisify(uv.fs_unlink) 61 | 62 | ---@type fun(path: string): linkedit.kit.Async.AsyncTask 63 | IO.fs_rmdir = Async.promisify(uv.fs_rmdir) 64 | 65 | ---@type fun(path: string, mode: integer): linkedit.kit.Async.AsyncTask 66 | IO.fs_mkdir = Async.promisify(uv.fs_mkdir) 67 | 68 | ---@type fun(from: string, to: string, option?: { excl?: boolean, ficlone?: boolean, ficlone_force?: boolean }): linkedit.kit.Async.AsyncTask 69 | IO.fs_copyfile = Async.promisify(uv.fs_copyfile) 70 | 71 | ---@type fun(path: string, flags: linkedit.kit.IO.UV.AccessMode, mode: integer): linkedit.kit.Async.AsyncTask 72 | IO.fs_open = Async.promisify(uv.fs_open) 73 | 74 | ---@type fun(fd: userdata): linkedit.kit.Async.AsyncTask 75 | IO.fs_close = Async.promisify(uv.fs_close) 76 | 77 | ---@type fun(fd: userdata, chunk_size: integer, offset?: integer): linkedit.kit.Async.AsyncTask 78 | IO.fs_read = Async.promisify(uv.fs_read) 79 | 80 | ---@type fun(fd: userdata, content: string, offset?: integer): linkedit.kit.Async.AsyncTask 81 | IO.fs_write = Async.promisify(uv.fs_write) 82 | 83 | ---@type fun(fd: userdata, offset: integer): linkedit.kit.Async.AsyncTask 84 | IO.fs_ftruncate = Async.promisify(uv.fs_ftruncate) 85 | 86 | ---@type fun(path: string, chunk_size?: integer): linkedit.kit.Async.AsyncTask 87 | IO.fs_opendir = Async.promisify(uv.fs_opendir, { callback = 2 }) 88 | 89 | ---@type fun(fd: userdata): linkedit.kit.Async.AsyncTask 90 | IO.fs_closedir = Async.promisify(uv.fs_closedir) 91 | 92 | ---@type fun(fd: userdata): linkedit.kit.Async.AsyncTask 93 | IO.fs_readdir = Async.promisify(uv.fs_readdir) 94 | 95 | ---@type fun(path: string): linkedit.kit.Async.AsyncTask 96 | IO.fs_scandir = Async.promisify(uv.fs_scandir) 97 | 98 | ---@type fun(path: string): linkedit.kit.Async.AsyncTask 99 | IO.fs_realpath = Async.promisify(uv.fs_realpath) 100 | 101 | ---Return if the path is directory. 102 | ---@param path string 103 | ---@return linkedit.kit.Async.AsyncTask 104 | function IO.is_directory(path) 105 | path = IO.normalize(path) 106 | return Async.run(function() 107 | return IO.fs_stat(path):catch(function() 108 | return {} 109 | end):await().type == 'directory' 110 | end) 111 | end 112 | 113 | ---Read file. 114 | ---@param path string 115 | ---@param chunk_size? integer 116 | ---@return linkedit.kit.Async.AsyncTask 117 | function IO.read_file(path, chunk_size) 118 | chunk_size = chunk_size or 1024 119 | return Async.run(function() 120 | local stat = IO.fs_stat(path):await() 121 | local fd = IO.fs_open(path, IO.AccessMode.r, tonumber('755', 8)):await() 122 | local ok, res = pcall(function() 123 | local chunks = {} 124 | local offset = 0 125 | while offset < stat.size do 126 | local chunk = IO.fs_read(fd, math.min(chunk_size, stat.size - offset), offset):await() 127 | if not chunk then 128 | break 129 | end 130 | table.insert(chunks, chunk) 131 | offset = offset + #chunk 132 | end 133 | return table.concat(chunks, ''):sub(1, stat.size - 1) -- remove EOF. 134 | end) 135 | IO.fs_close(fd):await() 136 | if not ok then 137 | error(res) 138 | end 139 | return res 140 | end) 141 | end 142 | 143 | ---Write file. 144 | ---@param path string 145 | ---@param content string 146 | ---@param chunk_size? integer 147 | function IO.write_file(path, content, chunk_size) 148 | chunk_size = chunk_size or 1024 149 | content = content .. '\n' -- add EOF. 150 | return Async.run(function() 151 | local fd = IO.fs_open(path, IO.AccessMode.w, tonumber('755', 8)):await() 152 | local ok, err = pcall(function() 153 | local offset = 0 154 | while offset < #content do 155 | local chunk = content:sub(offset + 1, offset + chunk_size) 156 | offset = offset + IO.fs_write(fd, chunk, offset):await() 157 | end 158 | IO.fs_ftruncate(fd, offset):await() 159 | end) 160 | IO.fs_close(fd):await() 161 | if not ok then 162 | error(err) 163 | end 164 | end) 165 | end 166 | 167 | ---Create directory. 168 | ---@param path string 169 | ---@param mode integer 170 | ---@param option? { recursive?: boolean } 171 | function IO.mkdir(path, mode, option) 172 | path = IO.normalize(path) 173 | option = option or {} 174 | option.recursive = option.recursive or false 175 | return Async.run(function() 176 | if not option.recursive then 177 | IO.fs_mkdir(path, mode):await() 178 | else 179 | local not_exists = {} 180 | local current = path 181 | while current ~= '/' do 182 | local stat = IO.fs_stat(current):catch(function() end):await() 183 | if stat then 184 | break 185 | end 186 | table.insert(not_exists, 1, current) 187 | current = IO.dirname(current) 188 | end 189 | for _, dir in ipairs(not_exists) do 190 | IO.fs_mkdir(dir, mode):await() 191 | end 192 | end 193 | end) 194 | end 195 | 196 | ---Remove file or directory. 197 | ---@param start_path string 198 | ---@param option? { recursive?: boolean } 199 | function IO.rm(start_path, option) 200 | start_path = IO.normalize(start_path) 201 | option = option or {} 202 | option.recursive = option.recursive or false 203 | return Async.run(function() 204 | local stat = IO.fs_stat(start_path):await() 205 | if stat.type == 'directory' then 206 | local children = IO.scandir(start_path):await() 207 | if not option.recursive and #children > 0 then 208 | error(('IO.rm: `%s` is a directory and not empty.'):format(start_path)) 209 | end 210 | IO.walk(start_path, function(err, entry) 211 | if err then 212 | error('IO.rm: ' .. tostring(err)) 213 | end 214 | if entry.type == 'directory' then 215 | IO.fs_rmdir(entry.path):await() 216 | else 217 | IO.fs_unlink(entry.path):await() 218 | end 219 | end, { postorder = true }):await() 220 | else 221 | IO.fs_unlink(start_path):await() 222 | end 223 | end) 224 | end 225 | 226 | ---Copy file or directory. 227 | ---@param from any 228 | ---@param to any 229 | ---@param option? { recursive?: boolean } 230 | ---@return linkedit.kit.Async.AsyncTask 231 | function IO.cp(from, to, option) 232 | from = IO.normalize(from) 233 | to = IO.normalize(to) 234 | option = option or {} 235 | option.recursive = option.recursive or false 236 | return Async.run(function() 237 | local stat = IO.fs_stat(from):await() 238 | if stat.type == 'directory' then 239 | if not option.recursive then 240 | error(('IO.cp: `%s` is a directory.'):format(from)) 241 | end 242 | IO.walk(from, function(err, entry) 243 | if err then 244 | error('IO.cp: ' .. tostring(err)) 245 | end 246 | local new_path = entry.path:gsub(vim.pesc(from), to) 247 | if entry.type == 'directory' then 248 | IO.mkdir(new_path, tonumber(stat.mode, 10), { recursive = true }):await() 249 | else 250 | IO.fs_copyfile(entry.path, new_path):await() 251 | end 252 | end):await() 253 | else 254 | IO.fs_copyfile(from, to):await() 255 | end 256 | end) 257 | end 258 | 259 | ---Walk directory entries recursively. 260 | ---@param start_path string 261 | ---@param callback fun(err: string|nil, entry: { path: string, type: string }): linkedit.kit.IO.WalkStatus? 262 | ---@param option? { postorder?: boolean } 263 | function IO.walk(start_path, callback, option) 264 | start_path = IO.normalize(start_path) 265 | option = option or {} 266 | option.postorder = option.postorder or false 267 | return Async.run(function() 268 | local function walk_pre(dir) 269 | local ok, iter_entries = pcall(function() 270 | return IO.iter_scandir(dir.path):await() 271 | end) 272 | if not ok then 273 | return callback(iter_entries, dir) 274 | end 275 | local status = callback(nil, dir) 276 | if status == IO.WalkStatus.SkipDir then 277 | return 278 | elseif status == IO.WalkStatus.Break then 279 | return status 280 | end 281 | for entry in iter_entries do 282 | if entry.type == 'directory' then 283 | if walk_pre(entry) == IO.WalkStatus.Break then 284 | return IO.WalkStatus.Break 285 | end 286 | else 287 | if callback(nil, entry) == IO.WalkStatus.Break then 288 | return IO.WalkStatus.Break 289 | end 290 | end 291 | end 292 | end 293 | 294 | local function walk_post(dir) 295 | local ok, iter_entries = pcall(function() 296 | return IO.iter_scandir(dir.path):await() 297 | end) 298 | if not ok then 299 | return callback(iter_entries, dir) 300 | end 301 | for entry in iter_entries do 302 | if entry.type == 'directory' then 303 | if walk_post(entry) == IO.WalkStatus.Break then 304 | return IO.WalkStatus.Break 305 | end 306 | else 307 | if callback(nil, entry) == IO.WalkStatus.Break then 308 | return IO.WalkStatus.Break 309 | end 310 | end 311 | end 312 | return callback(nil, dir) 313 | end 314 | 315 | if not IO.is_directory(start_path) then 316 | error(('IO.walk: `%s` is not a directory.'):format(start_path)) 317 | end 318 | if option.postorder then 319 | walk_post({ path = start_path, type = 'directory' }) 320 | else 321 | walk_pre({ path = start_path, type = 'directory' }) 322 | end 323 | end) 324 | end 325 | 326 | ---Scan directory entries. 327 | ---@param path string 328 | ---@return linkedit.kit.Async.AsyncTask 329 | function IO.scandir(path) 330 | path = IO.normalize(path) 331 | return Async.run(function() 332 | local fd = IO.fs_scandir(path):await() 333 | local entries = {} 334 | while true do 335 | local name, type = uv.fs_scandir_next(fd) 336 | if not name then 337 | break 338 | end 339 | table.insert(entries, { 340 | type = type, 341 | path = IO.join(path, name), 342 | }) 343 | end 344 | return entries 345 | end) 346 | end 347 | 348 | ---Scan directory entries. 349 | ---@param path any 350 | ---@return linkedit.kit.Async.AsyncTask 351 | function IO.iter_scandir(path) 352 | path = IO.normalize(path) 353 | return Async.run(function() 354 | local fd = IO.fs_scandir(path):await() 355 | return function() 356 | local name, type = uv.fs_scandir_next(fd) 357 | if name then 358 | return { 359 | type = type, 360 | path = IO.join(path, name), 361 | } 362 | end 363 | end 364 | end) 365 | end 366 | 367 | ---Return normalized path. 368 | ---@param path string 369 | ---@return string 370 | function IO.normalize(path) 371 | if is_windows then 372 | path = path:gsub('\\', '/') 373 | end 374 | 375 | -- remove trailing slash. 376 | if path:sub(-1) == '/' then 377 | path = path:sub(1, -2) 378 | end 379 | 380 | -- skip if the path already absolute. 381 | if IO.is_absolute(path) then 382 | return path 383 | end 384 | 385 | -- homedir. 386 | if path:sub(1, 1) == '~' then 387 | path = IO.join(uv.os_homedir(), path:sub(2)) 388 | end 389 | 390 | -- absolute. 391 | if path:sub(1, 1) == '/' then 392 | return path:sub(-1) == '/' and path:sub(1, -2) or path 393 | end 394 | 395 | -- resolve relative path. 396 | local up = uv.cwd() 397 | up = up:sub(-1) == '/' and up:sub(1, -2) or up 398 | while true do 399 | if path:sub(1, 3) == '../' then 400 | path = path:sub(4) 401 | up = IO.dirname(up) 402 | elseif path:sub(1, 2) == './' then 403 | path = path:sub(3) 404 | else 405 | break 406 | end 407 | end 408 | return IO.join(up, path) 409 | end 410 | 411 | ---Join the paths. 412 | ---@param base string 413 | ---@param path string 414 | ---@return string 415 | function IO.join(base, path) 416 | if base:sub(-1) == '/' then 417 | base = base:sub(1, -2) 418 | end 419 | return base .. '/' .. path 420 | end 421 | 422 | ---Return the path of the current working directory. 423 | ---@param path string 424 | ---@return string 425 | function IO.dirname(path) 426 | if path:sub(-1) == '/' then 427 | path = path:sub(1, -2) 428 | end 429 | return (path:gsub('/[^/]+$', '')) 430 | end 431 | 432 | if is_windows then 433 | ---Return the path is absolute or not. 434 | ---@param path string 435 | ---@return boolean 436 | function IO.is_absolute(path) 437 | return path:sub(1, 1) == '/' or path:match('^%a://') 438 | end 439 | else 440 | ---Return the path is absolute or not. 441 | ---@param path string 442 | ---@return boolean 443 | function IO.is_absolute(path) 444 | return path:sub(1, 1) == '/' 445 | end 446 | end 447 | 448 | return IO 449 | -------------------------------------------------------------------------------- /lua/linkedit/kit/LSP/Client.lua: -------------------------------------------------------------------------------- 1 | local LSP = require('linkedit.kit.LSP') 2 | local AsyncTask = require('linkedit.kit.Async.AsyncTask') 3 | 4 | ---@class linkedit.kit.LSP.Client 5 | ---@field public client table 6 | local Client = {} 7 | Client.__index = Client 8 | 9 | ---Create LSP Client wrapper. 10 | ---@param client table 11 | ---@return linkedit.kit.LSP.Client 12 | function Client.new(client) 13 | local self = setmetatable({}, Client) 14 | self.client = client 15 | return self 16 | end 17 | 18 | ---@param params linkedit.kit.LSP.ImplementationParams 19 | function Client:textDocument_implementation(params) 20 | local that, request_id, reject_ = self, nil, nil 21 | local task = AsyncTask.new(function(resolve, reject) 22 | request_id = self.client.request('textDocument/implementation', params, function(err, res) 23 | if err then 24 | reject(err) 25 | else 26 | resolve(res) 27 | end 28 | end) 29 | reject_ = reject 30 | end) 31 | function task.cancel() 32 | that.client.cancel_request(request_id) 33 | reject_(LSP.ErrorCodes.RequestCancelled) 34 | end 35 | return task 36 | end 37 | 38 | ---@param params linkedit.kit.LSP.TypeDefinitionParams 39 | function Client:textDocument_typeDefinition(params) 40 | local that, request_id, reject_ = self, nil, nil 41 | local task = AsyncTask.new(function(resolve, reject) 42 | request_id = self.client.request('textDocument/typeDefinition', params, function(err, res) 43 | if err then 44 | reject(err) 45 | else 46 | resolve(res) 47 | end 48 | end) 49 | reject_ = reject 50 | end) 51 | function task.cancel() 52 | that.client.cancel_request(request_id) 53 | reject_(LSP.ErrorCodes.RequestCancelled) 54 | end 55 | return task 56 | end 57 | 58 | ---@param params nil 59 | function Client:workspace_workspaceFolders(params) 60 | local that, request_id, reject_ = self, nil, nil 61 | local task = AsyncTask.new(function(resolve, reject) 62 | request_id = self.client.request('workspace/workspaceFolders', params, function(err, res) 63 | if err then 64 | reject(err) 65 | else 66 | resolve(res) 67 | end 68 | end) 69 | reject_ = reject 70 | end) 71 | function task.cancel() 72 | that.client.cancel_request(request_id) 73 | reject_(LSP.ErrorCodes.RequestCancelled) 74 | end 75 | return task 76 | end 77 | 78 | ---@class linkedit.kit.LSP.IntersectionType01 : linkedit.kit.LSP.ConfigurationParams, linkedit.kit.LSP.PartialResultParams 79 | 80 | ---@param params linkedit.kit.LSP.IntersectionType01 81 | function Client:workspace_configuration(params) 82 | local that, request_id, reject_ = self, nil, nil 83 | local task = AsyncTask.new(function(resolve, reject) 84 | request_id = self.client.request('workspace/configuration', params, function(err, res) 85 | if err then 86 | reject(err) 87 | else 88 | resolve(res) 89 | end 90 | end) 91 | reject_ = reject 92 | end) 93 | function task.cancel() 94 | that.client.cancel_request(request_id) 95 | reject_(LSP.ErrorCodes.RequestCancelled) 96 | end 97 | return task 98 | end 99 | 100 | ---@param params linkedit.kit.LSP.DocumentColorParams 101 | function Client:textDocument_documentColor(params) 102 | local that, request_id, reject_ = self, nil, nil 103 | local task = AsyncTask.new(function(resolve, reject) 104 | request_id = self.client.request('textDocument/documentColor', params, function(err, res) 105 | if err then 106 | reject(err) 107 | else 108 | resolve(res) 109 | end 110 | end) 111 | reject_ = reject 112 | end) 113 | function task.cancel() 114 | that.client.cancel_request(request_id) 115 | reject_(LSP.ErrorCodes.RequestCancelled) 116 | end 117 | return task 118 | end 119 | 120 | ---@param params linkedit.kit.LSP.ColorPresentationParams 121 | function Client:textDocument_colorPresentation(params) 122 | local that, request_id, reject_ = self, nil, nil 123 | local task = AsyncTask.new(function(resolve, reject) 124 | request_id = self.client.request('textDocument/colorPresentation', params, function(err, res) 125 | if err then 126 | reject(err) 127 | else 128 | resolve(res) 129 | end 130 | end) 131 | reject_ = reject 132 | end) 133 | function task.cancel() 134 | that.client.cancel_request(request_id) 135 | reject_(LSP.ErrorCodes.RequestCancelled) 136 | end 137 | return task 138 | end 139 | 140 | ---@param params linkedit.kit.LSP.FoldingRangeParams 141 | function Client:textDocument_foldingRange(params) 142 | local that, request_id, reject_ = self, nil, nil 143 | local task = AsyncTask.new(function(resolve, reject) 144 | request_id = self.client.request('textDocument/foldingRange', params, function(err, res) 145 | if err then 146 | reject(err) 147 | else 148 | resolve(res) 149 | end 150 | end) 151 | reject_ = reject 152 | end) 153 | function task.cancel() 154 | that.client.cancel_request(request_id) 155 | reject_(LSP.ErrorCodes.RequestCancelled) 156 | end 157 | return task 158 | end 159 | 160 | ---@param params linkedit.kit.LSP.DeclarationParams 161 | function Client:textDocument_declaration(params) 162 | local that, request_id, reject_ = self, nil, nil 163 | local task = AsyncTask.new(function(resolve, reject) 164 | request_id = self.client.request('textDocument/declaration', params, function(err, res) 165 | if err then 166 | reject(err) 167 | else 168 | resolve(res) 169 | end 170 | end) 171 | reject_ = reject 172 | end) 173 | function task.cancel() 174 | that.client.cancel_request(request_id) 175 | reject_(LSP.ErrorCodes.RequestCancelled) 176 | end 177 | return task 178 | end 179 | 180 | ---@param params linkedit.kit.LSP.SelectionRangeParams 181 | function Client:textDocument_selectionRange(params) 182 | local that, request_id, reject_ = self, nil, nil 183 | local task = AsyncTask.new(function(resolve, reject) 184 | request_id = self.client.request('textDocument/selectionRange', params, function(err, res) 185 | if err then 186 | reject(err) 187 | else 188 | resolve(res) 189 | end 190 | end) 191 | reject_ = reject 192 | end) 193 | function task.cancel() 194 | that.client.cancel_request(request_id) 195 | reject_(LSP.ErrorCodes.RequestCancelled) 196 | end 197 | return task 198 | end 199 | 200 | ---@param params linkedit.kit.LSP.WorkDoneProgressCreateParams 201 | function Client:window_workDoneProgress_create(params) 202 | local that, request_id, reject_ = self, nil, nil 203 | local task = AsyncTask.new(function(resolve, reject) 204 | request_id = self.client.request('window/workDoneProgress/create', params, function(err, res) 205 | if err then 206 | reject(err) 207 | else 208 | resolve(res) 209 | end 210 | end) 211 | reject_ = reject 212 | end) 213 | function task.cancel() 214 | that.client.cancel_request(request_id) 215 | reject_(LSP.ErrorCodes.RequestCancelled) 216 | end 217 | return task 218 | end 219 | 220 | ---@param params linkedit.kit.LSP.CallHierarchyPrepareParams 221 | function Client:textDocument_prepareCallHierarchy(params) 222 | local that, request_id, reject_ = self, nil, nil 223 | local task = AsyncTask.new(function(resolve, reject) 224 | request_id = self.client.request('textDocument/prepareCallHierarchy', params, function(err, res) 225 | if err then 226 | reject(err) 227 | else 228 | resolve(res) 229 | end 230 | end) 231 | reject_ = reject 232 | end) 233 | function task.cancel() 234 | that.client.cancel_request(request_id) 235 | reject_(LSP.ErrorCodes.RequestCancelled) 236 | end 237 | return task 238 | end 239 | 240 | ---@param params linkedit.kit.LSP.CallHierarchyIncomingCallsParams 241 | function Client:callHierarchy_incomingCalls(params) 242 | local that, request_id, reject_ = self, nil, nil 243 | local task = AsyncTask.new(function(resolve, reject) 244 | request_id = self.client.request('callHierarchy/incomingCalls', params, function(err, res) 245 | if err then 246 | reject(err) 247 | else 248 | resolve(res) 249 | end 250 | end) 251 | reject_ = reject 252 | end) 253 | function task.cancel() 254 | that.client.cancel_request(request_id) 255 | reject_(LSP.ErrorCodes.RequestCancelled) 256 | end 257 | return task 258 | end 259 | 260 | ---@param params linkedit.kit.LSP.CallHierarchyOutgoingCallsParams 261 | function Client:callHierarchy_outgoingCalls(params) 262 | local that, request_id, reject_ = self, nil, nil 263 | local task = AsyncTask.new(function(resolve, reject) 264 | request_id = self.client.request('callHierarchy/outgoingCalls', params, function(err, res) 265 | if err then 266 | reject(err) 267 | else 268 | resolve(res) 269 | end 270 | end) 271 | reject_ = reject 272 | end) 273 | function task.cancel() 274 | that.client.cancel_request(request_id) 275 | reject_(LSP.ErrorCodes.RequestCancelled) 276 | end 277 | return task 278 | end 279 | 280 | ---@param params linkedit.kit.LSP.SemanticTokensParams 281 | function Client:textDocument_semanticTokens_full(params) 282 | local that, request_id, reject_ = self, nil, nil 283 | local task = AsyncTask.new(function(resolve, reject) 284 | request_id = self.client.request('textDocument/semanticTokens/full', params, function(err, res) 285 | if err then 286 | reject(err) 287 | else 288 | resolve(res) 289 | end 290 | end) 291 | reject_ = reject 292 | end) 293 | function task.cancel() 294 | that.client.cancel_request(request_id) 295 | reject_(LSP.ErrorCodes.RequestCancelled) 296 | end 297 | return task 298 | end 299 | 300 | ---@param params linkedit.kit.LSP.SemanticTokensDeltaParams 301 | function Client:textDocument_semanticTokens_full_delta(params) 302 | local that, request_id, reject_ = self, nil, nil 303 | local task = AsyncTask.new(function(resolve, reject) 304 | request_id = self.client.request('textDocument/semanticTokens/full/delta', params, function(err, res) 305 | if err then 306 | reject(err) 307 | else 308 | resolve(res) 309 | end 310 | end) 311 | reject_ = reject 312 | end) 313 | function task.cancel() 314 | that.client.cancel_request(request_id) 315 | reject_(LSP.ErrorCodes.RequestCancelled) 316 | end 317 | return task 318 | end 319 | 320 | ---@param params linkedit.kit.LSP.SemanticTokensRangeParams 321 | function Client:textDocument_semanticTokens_range(params) 322 | local that, request_id, reject_ = self, nil, nil 323 | local task = AsyncTask.new(function(resolve, reject) 324 | request_id = self.client.request('textDocument/semanticTokens/range', params, function(err, res) 325 | if err then 326 | reject(err) 327 | else 328 | resolve(res) 329 | end 330 | end) 331 | reject_ = reject 332 | end) 333 | function task.cancel() 334 | that.client.cancel_request(request_id) 335 | reject_(LSP.ErrorCodes.RequestCancelled) 336 | end 337 | return task 338 | end 339 | 340 | ---@param params nil 341 | function Client:workspace_semanticTokens_refresh(params) 342 | local that, request_id, reject_ = self, nil, nil 343 | local task = AsyncTask.new(function(resolve, reject) 344 | request_id = self.client.request('workspace/semanticTokens/refresh', params, function(err, res) 345 | if err then 346 | reject(err) 347 | else 348 | resolve(res) 349 | end 350 | end) 351 | reject_ = reject 352 | end) 353 | function task.cancel() 354 | that.client.cancel_request(request_id) 355 | reject_(LSP.ErrorCodes.RequestCancelled) 356 | end 357 | return task 358 | end 359 | 360 | ---@param params linkedit.kit.LSP.ShowDocumentParams 361 | function Client:window_showDocument(params) 362 | local that, request_id, reject_ = self, nil, nil 363 | local task = AsyncTask.new(function(resolve, reject) 364 | request_id = self.client.request('window/showDocument', params, function(err, res) 365 | if err then 366 | reject(err) 367 | else 368 | resolve(res) 369 | end 370 | end) 371 | reject_ = reject 372 | end) 373 | function task.cancel() 374 | that.client.cancel_request(request_id) 375 | reject_(LSP.ErrorCodes.RequestCancelled) 376 | end 377 | return task 378 | end 379 | 380 | ---@param params linkedit.kit.LSP.LinkedEditingRangeParams 381 | function Client:textDocument_linkedEditingRange(params) 382 | local that, request_id, reject_ = self, nil, nil 383 | local task = AsyncTask.new(function(resolve, reject) 384 | request_id = self.client.request('textDocument/linkedEditingRange', params, function(err, res) 385 | if err then 386 | reject(err) 387 | else 388 | resolve(res) 389 | end 390 | end) 391 | reject_ = reject 392 | end) 393 | function task.cancel() 394 | that.client.cancel_request(request_id) 395 | reject_(LSP.ErrorCodes.RequestCancelled) 396 | end 397 | return task 398 | end 399 | 400 | ---@param params linkedit.kit.LSP.CreateFilesParams 401 | function Client:workspace_willCreateFiles(params) 402 | local that, request_id, reject_ = self, nil, nil 403 | local task = AsyncTask.new(function(resolve, reject) 404 | request_id = self.client.request('workspace/willCreateFiles', params, function(err, res) 405 | if err then 406 | reject(err) 407 | else 408 | resolve(res) 409 | end 410 | end) 411 | reject_ = reject 412 | end) 413 | function task.cancel() 414 | that.client.cancel_request(request_id) 415 | reject_(LSP.ErrorCodes.RequestCancelled) 416 | end 417 | return task 418 | end 419 | 420 | ---@param params linkedit.kit.LSP.RenameFilesParams 421 | function Client:workspace_willRenameFiles(params) 422 | local that, request_id, reject_ = self, nil, nil 423 | local task = AsyncTask.new(function(resolve, reject) 424 | request_id = self.client.request('workspace/willRenameFiles', params, function(err, res) 425 | if err then 426 | reject(err) 427 | else 428 | resolve(res) 429 | end 430 | end) 431 | reject_ = reject 432 | end) 433 | function task.cancel() 434 | that.client.cancel_request(request_id) 435 | reject_(LSP.ErrorCodes.RequestCancelled) 436 | end 437 | return task 438 | end 439 | 440 | ---@param params linkedit.kit.LSP.DeleteFilesParams 441 | function Client:workspace_willDeleteFiles(params) 442 | local that, request_id, reject_ = self, nil, nil 443 | local task = AsyncTask.new(function(resolve, reject) 444 | request_id = self.client.request('workspace/willDeleteFiles', params, function(err, res) 445 | if err then 446 | reject(err) 447 | else 448 | resolve(res) 449 | end 450 | end) 451 | reject_ = reject 452 | end) 453 | function task.cancel() 454 | that.client.cancel_request(request_id) 455 | reject_(LSP.ErrorCodes.RequestCancelled) 456 | end 457 | return task 458 | end 459 | 460 | ---@param params linkedit.kit.LSP.MonikerParams 461 | function Client:textDocument_moniker(params) 462 | local that, request_id, reject_ = self, nil, nil 463 | local task = AsyncTask.new(function(resolve, reject) 464 | request_id = self.client.request('textDocument/moniker', params, function(err, res) 465 | if err then 466 | reject(err) 467 | else 468 | resolve(res) 469 | end 470 | end) 471 | reject_ = reject 472 | end) 473 | function task.cancel() 474 | that.client.cancel_request(request_id) 475 | reject_(LSP.ErrorCodes.RequestCancelled) 476 | end 477 | return task 478 | end 479 | 480 | ---@param params linkedit.kit.LSP.TypeHierarchyPrepareParams 481 | function Client:textDocument_prepareTypeHierarchy(params) 482 | local that, request_id, reject_ = self, nil, nil 483 | local task = AsyncTask.new(function(resolve, reject) 484 | request_id = self.client.request('textDocument/prepareTypeHierarchy', params, function(err, res) 485 | if err then 486 | reject(err) 487 | else 488 | resolve(res) 489 | end 490 | end) 491 | reject_ = reject 492 | end) 493 | function task.cancel() 494 | that.client.cancel_request(request_id) 495 | reject_(LSP.ErrorCodes.RequestCancelled) 496 | end 497 | return task 498 | end 499 | 500 | ---@param params linkedit.kit.LSP.TypeHierarchySupertypesParams 501 | function Client:typeHierarchy_supertypes(params) 502 | local that, request_id, reject_ = self, nil, nil 503 | local task = AsyncTask.new(function(resolve, reject) 504 | request_id = self.client.request('typeHierarchy/supertypes', params, function(err, res) 505 | if err then 506 | reject(err) 507 | else 508 | resolve(res) 509 | end 510 | end) 511 | reject_ = reject 512 | end) 513 | function task.cancel() 514 | that.client.cancel_request(request_id) 515 | reject_(LSP.ErrorCodes.RequestCancelled) 516 | end 517 | return task 518 | end 519 | 520 | ---@param params linkedit.kit.LSP.TypeHierarchySubtypesParams 521 | function Client:typeHierarchy_subtypes(params) 522 | local that, request_id, reject_ = self, nil, nil 523 | local task = AsyncTask.new(function(resolve, reject) 524 | request_id = self.client.request('typeHierarchy/subtypes', params, function(err, res) 525 | if err then 526 | reject(err) 527 | else 528 | resolve(res) 529 | end 530 | end) 531 | reject_ = reject 532 | end) 533 | function task.cancel() 534 | that.client.cancel_request(request_id) 535 | reject_(LSP.ErrorCodes.RequestCancelled) 536 | end 537 | return task 538 | end 539 | 540 | ---@param params linkedit.kit.LSP.InlineValueParams 541 | function Client:textDocument_inlineValue(params) 542 | local that, request_id, reject_ = self, nil, nil 543 | local task = AsyncTask.new(function(resolve, reject) 544 | request_id = self.client.request('textDocument/inlineValue', params, function(err, res) 545 | if err then 546 | reject(err) 547 | else 548 | resolve(res) 549 | end 550 | end) 551 | reject_ = reject 552 | end) 553 | function task.cancel() 554 | that.client.cancel_request(request_id) 555 | reject_(LSP.ErrorCodes.RequestCancelled) 556 | end 557 | return task 558 | end 559 | 560 | ---@param params nil 561 | function Client:workspace_inlineValue_refresh(params) 562 | local that, request_id, reject_ = self, nil, nil 563 | local task = AsyncTask.new(function(resolve, reject) 564 | request_id = self.client.request('workspace/inlineValue/refresh', params, function(err, res) 565 | if err then 566 | reject(err) 567 | else 568 | resolve(res) 569 | end 570 | end) 571 | reject_ = reject 572 | end) 573 | function task.cancel() 574 | that.client.cancel_request(request_id) 575 | reject_(LSP.ErrorCodes.RequestCancelled) 576 | end 577 | return task 578 | end 579 | 580 | ---@param params linkedit.kit.LSP.InlayHintParams 581 | function Client:textDocument_inlayHint(params) 582 | local that, request_id, reject_ = self, nil, nil 583 | local task = AsyncTask.new(function(resolve, reject) 584 | request_id = self.client.request('textDocument/inlayHint', params, function(err, res) 585 | if err then 586 | reject(err) 587 | else 588 | resolve(res) 589 | end 590 | end) 591 | reject_ = reject 592 | end) 593 | function task.cancel() 594 | that.client.cancel_request(request_id) 595 | reject_(LSP.ErrorCodes.RequestCancelled) 596 | end 597 | return task 598 | end 599 | 600 | ---@param params linkedit.kit.LSP.InlayHint 601 | function Client:inlayHint_resolve(params) 602 | local that, request_id, reject_ = self, nil, nil 603 | local task = AsyncTask.new(function(resolve, reject) 604 | request_id = self.client.request('inlayHint/resolve', params, function(err, res) 605 | if err then 606 | reject(err) 607 | else 608 | resolve(res) 609 | end 610 | end) 611 | reject_ = reject 612 | end) 613 | function task.cancel() 614 | that.client.cancel_request(request_id) 615 | reject_(LSP.ErrorCodes.RequestCancelled) 616 | end 617 | return task 618 | end 619 | 620 | ---@param params nil 621 | function Client:workspace_inlayHint_refresh(params) 622 | local that, request_id, reject_ = self, nil, nil 623 | local task = AsyncTask.new(function(resolve, reject) 624 | request_id = self.client.request('workspace/inlayHint/refresh', params, function(err, res) 625 | if err then 626 | reject(err) 627 | else 628 | resolve(res) 629 | end 630 | end) 631 | reject_ = reject 632 | end) 633 | function task.cancel() 634 | that.client.cancel_request(request_id) 635 | reject_(LSP.ErrorCodes.RequestCancelled) 636 | end 637 | return task 638 | end 639 | 640 | ---@param params linkedit.kit.LSP.DocumentDiagnosticParams 641 | function Client:textDocument_diagnostic(params) 642 | local that, request_id, reject_ = self, nil, nil 643 | local task = AsyncTask.new(function(resolve, reject) 644 | request_id = self.client.request('textDocument/diagnostic', params, function(err, res) 645 | if err then 646 | reject(err) 647 | else 648 | resolve(res) 649 | end 650 | end) 651 | reject_ = reject 652 | end) 653 | function task.cancel() 654 | that.client.cancel_request(request_id) 655 | reject_(LSP.ErrorCodes.RequestCancelled) 656 | end 657 | return task 658 | end 659 | 660 | ---@param params linkedit.kit.LSP.WorkspaceDiagnosticParams 661 | function Client:workspace_diagnostic(params) 662 | local that, request_id, reject_ = self, nil, nil 663 | local task = AsyncTask.new(function(resolve, reject) 664 | request_id = self.client.request('workspace/diagnostic', params, function(err, res) 665 | if err then 666 | reject(err) 667 | else 668 | resolve(res) 669 | end 670 | end) 671 | reject_ = reject 672 | end) 673 | function task.cancel() 674 | that.client.cancel_request(request_id) 675 | reject_(LSP.ErrorCodes.RequestCancelled) 676 | end 677 | return task 678 | end 679 | 680 | ---@param params nil 681 | function Client:workspace_diagnostic_refresh(params) 682 | local that, request_id, reject_ = self, nil, nil 683 | local task = AsyncTask.new(function(resolve, reject) 684 | request_id = self.client.request('workspace/diagnostic/refresh', params, function(err, res) 685 | if err then 686 | reject(err) 687 | else 688 | resolve(res) 689 | end 690 | end) 691 | reject_ = reject 692 | end) 693 | function task.cancel() 694 | that.client.cancel_request(request_id) 695 | reject_(LSP.ErrorCodes.RequestCancelled) 696 | end 697 | return task 698 | end 699 | 700 | ---@param params linkedit.kit.LSP.RegistrationParams 701 | function Client:client_registerCapability(params) 702 | local that, request_id, reject_ = self, nil, nil 703 | local task = AsyncTask.new(function(resolve, reject) 704 | request_id = self.client.request('client/registerCapability', params, function(err, res) 705 | if err then 706 | reject(err) 707 | else 708 | resolve(res) 709 | end 710 | end) 711 | reject_ = reject 712 | end) 713 | function task.cancel() 714 | that.client.cancel_request(request_id) 715 | reject_(LSP.ErrorCodes.RequestCancelled) 716 | end 717 | return task 718 | end 719 | 720 | ---@param params linkedit.kit.LSP.UnregistrationParams 721 | function Client:client_unregisterCapability(params) 722 | local that, request_id, reject_ = self, nil, nil 723 | local task = AsyncTask.new(function(resolve, reject) 724 | request_id = self.client.request('client/unregisterCapability', params, function(err, res) 725 | if err then 726 | reject(err) 727 | else 728 | resolve(res) 729 | end 730 | end) 731 | reject_ = reject 732 | end) 733 | function task.cancel() 734 | that.client.cancel_request(request_id) 735 | reject_(LSP.ErrorCodes.RequestCancelled) 736 | end 737 | return task 738 | end 739 | 740 | ---@param params linkedit.kit.LSP.InitializeParams 741 | function Client:initialize(params) 742 | local that, request_id, reject_ = self, nil, nil 743 | local task = AsyncTask.new(function(resolve, reject) 744 | request_id = self.client.request('initialize', params, function(err, res) 745 | if err then 746 | reject(err) 747 | else 748 | resolve(res) 749 | end 750 | end) 751 | reject_ = reject 752 | end) 753 | function task.cancel() 754 | that.client.cancel_request(request_id) 755 | reject_(LSP.ErrorCodes.RequestCancelled) 756 | end 757 | return task 758 | end 759 | 760 | ---@param params nil 761 | function Client:shutdown(params) 762 | local that, request_id, reject_ = self, nil, nil 763 | local task = AsyncTask.new(function(resolve, reject) 764 | request_id = self.client.request('shutdown', params, function(err, res) 765 | if err then 766 | reject(err) 767 | else 768 | resolve(res) 769 | end 770 | end) 771 | reject_ = reject 772 | end) 773 | function task.cancel() 774 | that.client.cancel_request(request_id) 775 | reject_(LSP.ErrorCodes.RequestCancelled) 776 | end 777 | return task 778 | end 779 | 780 | ---@param params linkedit.kit.LSP.ShowMessageRequestParams 781 | function Client:window_showMessageRequest(params) 782 | local that, request_id, reject_ = self, nil, nil 783 | local task = AsyncTask.new(function(resolve, reject) 784 | request_id = self.client.request('window/showMessageRequest', params, function(err, res) 785 | if err then 786 | reject(err) 787 | else 788 | resolve(res) 789 | end 790 | end) 791 | reject_ = reject 792 | end) 793 | function task.cancel() 794 | that.client.cancel_request(request_id) 795 | reject_(LSP.ErrorCodes.RequestCancelled) 796 | end 797 | return task 798 | end 799 | 800 | ---@param params linkedit.kit.LSP.WillSaveTextDocumentParams 801 | function Client:textDocument_willSaveWaitUntil(params) 802 | local that, request_id, reject_ = self, nil, nil 803 | local task = AsyncTask.new(function(resolve, reject) 804 | request_id = self.client.request('textDocument/willSaveWaitUntil', params, function(err, res) 805 | if err then 806 | reject(err) 807 | else 808 | resolve(res) 809 | end 810 | end) 811 | reject_ = reject 812 | end) 813 | function task.cancel() 814 | that.client.cancel_request(request_id) 815 | reject_(LSP.ErrorCodes.RequestCancelled) 816 | end 817 | return task 818 | end 819 | 820 | ---@param params linkedit.kit.LSP.CompletionParams 821 | function Client:textDocument_completion(params) 822 | local that, request_id, reject_ = self, nil, nil 823 | local task = AsyncTask.new(function(resolve, reject) 824 | request_id = self.client.request('textDocument/completion', params, function(err, res) 825 | if err then 826 | reject(err) 827 | else 828 | resolve(res) 829 | end 830 | end) 831 | reject_ = reject 832 | end) 833 | function task.cancel() 834 | that.client.cancel_request(request_id) 835 | reject_(LSP.ErrorCodes.RequestCancelled) 836 | end 837 | return task 838 | end 839 | 840 | ---@param params linkedit.kit.LSP.CompletionItem 841 | function Client:completionItem_resolve(params) 842 | local that, request_id, reject_ = self, nil, nil 843 | local task = AsyncTask.new(function(resolve, reject) 844 | request_id = self.client.request('completionItem/resolve', params, function(err, res) 845 | if err then 846 | reject(err) 847 | else 848 | resolve(res) 849 | end 850 | end) 851 | reject_ = reject 852 | end) 853 | function task.cancel() 854 | that.client.cancel_request(request_id) 855 | reject_(LSP.ErrorCodes.RequestCancelled) 856 | end 857 | return task 858 | end 859 | 860 | ---@param params linkedit.kit.LSP.HoverParams 861 | function Client:textDocument_hover(params) 862 | local that, request_id, reject_ = self, nil, nil 863 | local task = AsyncTask.new(function(resolve, reject) 864 | request_id = self.client.request('textDocument/hover', params, function(err, res) 865 | if err then 866 | reject(err) 867 | else 868 | resolve(res) 869 | end 870 | end) 871 | reject_ = reject 872 | end) 873 | function task.cancel() 874 | that.client.cancel_request(request_id) 875 | reject_(LSP.ErrorCodes.RequestCancelled) 876 | end 877 | return task 878 | end 879 | 880 | ---@param params linkedit.kit.LSP.SignatureHelpParams 881 | function Client:textDocument_signatureHelp(params) 882 | local that, request_id, reject_ = self, nil, nil 883 | local task = AsyncTask.new(function(resolve, reject) 884 | request_id = self.client.request('textDocument/signatureHelp', params, function(err, res) 885 | if err then 886 | reject(err) 887 | else 888 | resolve(res) 889 | end 890 | end) 891 | reject_ = reject 892 | end) 893 | function task.cancel() 894 | that.client.cancel_request(request_id) 895 | reject_(LSP.ErrorCodes.RequestCancelled) 896 | end 897 | return task 898 | end 899 | 900 | ---@param params linkedit.kit.LSP.DefinitionParams 901 | function Client:textDocument_definition(params) 902 | local that, request_id, reject_ = self, nil, nil 903 | local task = AsyncTask.new(function(resolve, reject) 904 | request_id = self.client.request('textDocument/definition', params, function(err, res) 905 | if err then 906 | reject(err) 907 | else 908 | resolve(res) 909 | end 910 | end) 911 | reject_ = reject 912 | end) 913 | function task.cancel() 914 | that.client.cancel_request(request_id) 915 | reject_(LSP.ErrorCodes.RequestCancelled) 916 | end 917 | return task 918 | end 919 | 920 | ---@param params linkedit.kit.LSP.ReferenceParams 921 | function Client:textDocument_references(params) 922 | local that, request_id, reject_ = self, nil, nil 923 | local task = AsyncTask.new(function(resolve, reject) 924 | request_id = self.client.request('textDocument/references', params, function(err, res) 925 | if err then 926 | reject(err) 927 | else 928 | resolve(res) 929 | end 930 | end) 931 | reject_ = reject 932 | end) 933 | function task.cancel() 934 | that.client.cancel_request(request_id) 935 | reject_(LSP.ErrorCodes.RequestCancelled) 936 | end 937 | return task 938 | end 939 | 940 | ---@param params linkedit.kit.LSP.DocumentHighlightParams 941 | function Client:textDocument_documentHighlight(params) 942 | local that, request_id, reject_ = self, nil, nil 943 | local task = AsyncTask.new(function(resolve, reject) 944 | request_id = self.client.request('textDocument/documentHighlight', params, function(err, res) 945 | if err then 946 | reject(err) 947 | else 948 | resolve(res) 949 | end 950 | end) 951 | reject_ = reject 952 | end) 953 | function task.cancel() 954 | that.client.cancel_request(request_id) 955 | reject_(LSP.ErrorCodes.RequestCancelled) 956 | end 957 | return task 958 | end 959 | 960 | ---@param params linkedit.kit.LSP.DocumentSymbolParams 961 | function Client:textDocument_documentSymbol(params) 962 | local that, request_id, reject_ = self, nil, nil 963 | local task = AsyncTask.new(function(resolve, reject) 964 | request_id = self.client.request('textDocument/documentSymbol', params, function(err, res) 965 | if err then 966 | reject(err) 967 | else 968 | resolve(res) 969 | end 970 | end) 971 | reject_ = reject 972 | end) 973 | function task.cancel() 974 | that.client.cancel_request(request_id) 975 | reject_(LSP.ErrorCodes.RequestCancelled) 976 | end 977 | return task 978 | end 979 | 980 | ---@param params linkedit.kit.LSP.CodeActionParams 981 | function Client:textDocument_codeAction(params) 982 | local that, request_id, reject_ = self, nil, nil 983 | local task = AsyncTask.new(function(resolve, reject) 984 | request_id = self.client.request('textDocument/codeAction', params, function(err, res) 985 | if err then 986 | reject(err) 987 | else 988 | resolve(res) 989 | end 990 | end) 991 | reject_ = reject 992 | end) 993 | function task.cancel() 994 | that.client.cancel_request(request_id) 995 | reject_(LSP.ErrorCodes.RequestCancelled) 996 | end 997 | return task 998 | end 999 | 1000 | ---@param params linkedit.kit.LSP.CodeAction 1001 | function Client:codeAction_resolve(params) 1002 | local that, request_id, reject_ = self, nil, nil 1003 | local task = AsyncTask.new(function(resolve, reject) 1004 | request_id = self.client.request('codeAction/resolve', params, function(err, res) 1005 | if err then 1006 | reject(err) 1007 | else 1008 | resolve(res) 1009 | end 1010 | end) 1011 | reject_ = reject 1012 | end) 1013 | function task.cancel() 1014 | that.client.cancel_request(request_id) 1015 | reject_(LSP.ErrorCodes.RequestCancelled) 1016 | end 1017 | return task 1018 | end 1019 | 1020 | ---@param params linkedit.kit.LSP.WorkspaceSymbolParams 1021 | function Client:workspace_symbol(params) 1022 | local that, request_id, reject_ = self, nil, nil 1023 | local task = AsyncTask.new(function(resolve, reject) 1024 | request_id = self.client.request('workspace/symbol', params, function(err, res) 1025 | if err then 1026 | reject(err) 1027 | else 1028 | resolve(res) 1029 | end 1030 | end) 1031 | reject_ = reject 1032 | end) 1033 | function task.cancel() 1034 | that.client.cancel_request(request_id) 1035 | reject_(LSP.ErrorCodes.RequestCancelled) 1036 | end 1037 | return task 1038 | end 1039 | 1040 | ---@param params linkedit.kit.LSP.WorkspaceSymbol 1041 | function Client:workspaceSymbol_resolve(params) 1042 | local that, request_id, reject_ = self, nil, nil 1043 | local task = AsyncTask.new(function(resolve, reject) 1044 | request_id = self.client.request('workspaceSymbol/resolve', params, function(err, res) 1045 | if err then 1046 | reject(err) 1047 | else 1048 | resolve(res) 1049 | end 1050 | end) 1051 | reject_ = reject 1052 | end) 1053 | function task.cancel() 1054 | that.client.cancel_request(request_id) 1055 | reject_(LSP.ErrorCodes.RequestCancelled) 1056 | end 1057 | return task 1058 | end 1059 | 1060 | ---@param params linkedit.kit.LSP.CodeLensParams 1061 | function Client:textDocument_codeLens(params) 1062 | local that, request_id, reject_ = self, nil, nil 1063 | local task = AsyncTask.new(function(resolve, reject) 1064 | request_id = self.client.request('textDocument/codeLens', params, function(err, res) 1065 | if err then 1066 | reject(err) 1067 | else 1068 | resolve(res) 1069 | end 1070 | end) 1071 | reject_ = reject 1072 | end) 1073 | function task.cancel() 1074 | that.client.cancel_request(request_id) 1075 | reject_(LSP.ErrorCodes.RequestCancelled) 1076 | end 1077 | return task 1078 | end 1079 | 1080 | ---@param params linkedit.kit.LSP.CodeLens 1081 | function Client:codeLens_resolve(params) 1082 | local that, request_id, reject_ = self, nil, nil 1083 | local task = AsyncTask.new(function(resolve, reject) 1084 | request_id = self.client.request('codeLens/resolve', params, function(err, res) 1085 | if err then 1086 | reject(err) 1087 | else 1088 | resolve(res) 1089 | end 1090 | end) 1091 | reject_ = reject 1092 | end) 1093 | function task.cancel() 1094 | that.client.cancel_request(request_id) 1095 | reject_(LSP.ErrorCodes.RequestCancelled) 1096 | end 1097 | return task 1098 | end 1099 | 1100 | ---@param params nil 1101 | function Client:workspace_codeLens_refresh(params) 1102 | local that, request_id, reject_ = self, nil, nil 1103 | local task = AsyncTask.new(function(resolve, reject) 1104 | request_id = self.client.request('workspace/codeLens/refresh', params, function(err, res) 1105 | if err then 1106 | reject(err) 1107 | else 1108 | resolve(res) 1109 | end 1110 | end) 1111 | reject_ = reject 1112 | end) 1113 | function task.cancel() 1114 | that.client.cancel_request(request_id) 1115 | reject_(LSP.ErrorCodes.RequestCancelled) 1116 | end 1117 | return task 1118 | end 1119 | 1120 | ---@param params linkedit.kit.LSP.DocumentLinkParams 1121 | function Client:textDocument_documentLink(params) 1122 | local that, request_id, reject_ = self, nil, nil 1123 | local task = AsyncTask.new(function(resolve, reject) 1124 | request_id = self.client.request('textDocument/documentLink', params, function(err, res) 1125 | if err then 1126 | reject(err) 1127 | else 1128 | resolve(res) 1129 | end 1130 | end) 1131 | reject_ = reject 1132 | end) 1133 | function task.cancel() 1134 | that.client.cancel_request(request_id) 1135 | reject_(LSP.ErrorCodes.RequestCancelled) 1136 | end 1137 | return task 1138 | end 1139 | 1140 | ---@param params linkedit.kit.LSP.DocumentLink 1141 | function Client:documentLink_resolve(params) 1142 | local that, request_id, reject_ = self, nil, nil 1143 | local task = AsyncTask.new(function(resolve, reject) 1144 | request_id = self.client.request('documentLink/resolve', params, function(err, res) 1145 | if err then 1146 | reject(err) 1147 | else 1148 | resolve(res) 1149 | end 1150 | end) 1151 | reject_ = reject 1152 | end) 1153 | function task.cancel() 1154 | that.client.cancel_request(request_id) 1155 | reject_(LSP.ErrorCodes.RequestCancelled) 1156 | end 1157 | return task 1158 | end 1159 | 1160 | ---@param params linkedit.kit.LSP.DocumentFormattingParams 1161 | function Client:textDocument_formatting(params) 1162 | local that, request_id, reject_ = self, nil, nil 1163 | local task = AsyncTask.new(function(resolve, reject) 1164 | request_id = self.client.request('textDocument/formatting', params, function(err, res) 1165 | if err then 1166 | reject(err) 1167 | else 1168 | resolve(res) 1169 | end 1170 | end) 1171 | reject_ = reject 1172 | end) 1173 | function task.cancel() 1174 | that.client.cancel_request(request_id) 1175 | reject_(LSP.ErrorCodes.RequestCancelled) 1176 | end 1177 | return task 1178 | end 1179 | 1180 | ---@param params linkedit.kit.LSP.DocumentRangeFormattingParams 1181 | function Client:textDocument_rangeFormatting(params) 1182 | local that, request_id, reject_ = self, nil, nil 1183 | local task = AsyncTask.new(function(resolve, reject) 1184 | request_id = self.client.request('textDocument/rangeFormatting', params, function(err, res) 1185 | if err then 1186 | reject(err) 1187 | else 1188 | resolve(res) 1189 | end 1190 | end) 1191 | reject_ = reject 1192 | end) 1193 | function task.cancel() 1194 | that.client.cancel_request(request_id) 1195 | reject_(LSP.ErrorCodes.RequestCancelled) 1196 | end 1197 | return task 1198 | end 1199 | 1200 | ---@param params linkedit.kit.LSP.DocumentOnTypeFormattingParams 1201 | function Client:textDocument_onTypeFormatting(params) 1202 | local that, request_id, reject_ = self, nil, nil 1203 | local task = AsyncTask.new(function(resolve, reject) 1204 | request_id = self.client.request('textDocument/onTypeFormatting', params, function(err, res) 1205 | if err then 1206 | reject(err) 1207 | else 1208 | resolve(res) 1209 | end 1210 | end) 1211 | reject_ = reject 1212 | end) 1213 | function task.cancel() 1214 | that.client.cancel_request(request_id) 1215 | reject_(LSP.ErrorCodes.RequestCancelled) 1216 | end 1217 | return task 1218 | end 1219 | 1220 | ---@param params linkedit.kit.LSP.RenameParams 1221 | function Client:textDocument_rename(params) 1222 | local that, request_id, reject_ = self, nil, nil 1223 | local task = AsyncTask.new(function(resolve, reject) 1224 | request_id = self.client.request('textDocument/rename', params, function(err, res) 1225 | if err then 1226 | reject(err) 1227 | else 1228 | resolve(res) 1229 | end 1230 | end) 1231 | reject_ = reject 1232 | end) 1233 | function task.cancel() 1234 | that.client.cancel_request(request_id) 1235 | reject_(LSP.ErrorCodes.RequestCancelled) 1236 | end 1237 | return task 1238 | end 1239 | 1240 | ---@param params linkedit.kit.LSP.PrepareRenameParams 1241 | function Client:textDocument_prepareRename(params) 1242 | local that, request_id, reject_ = self, nil, nil 1243 | local task = AsyncTask.new(function(resolve, reject) 1244 | request_id = self.client.request('textDocument/prepareRename', params, function(err, res) 1245 | if err then 1246 | reject(err) 1247 | else 1248 | resolve(res) 1249 | end 1250 | end) 1251 | reject_ = reject 1252 | end) 1253 | function task.cancel() 1254 | that.client.cancel_request(request_id) 1255 | reject_(LSP.ErrorCodes.RequestCancelled) 1256 | end 1257 | return task 1258 | end 1259 | 1260 | ---@param params linkedit.kit.LSP.ExecuteCommandParams 1261 | function Client:workspace_executeCommand(params) 1262 | local that, request_id, reject_ = self, nil, nil 1263 | local task = AsyncTask.new(function(resolve, reject) 1264 | request_id = self.client.request('workspace/executeCommand', params, function(err, res) 1265 | if err then 1266 | reject(err) 1267 | else 1268 | resolve(res) 1269 | end 1270 | end) 1271 | reject_ = reject 1272 | end) 1273 | function task.cancel() 1274 | that.client.cancel_request(request_id) 1275 | reject_(LSP.ErrorCodes.RequestCancelled) 1276 | end 1277 | return task 1278 | end 1279 | 1280 | ---@param params linkedit.kit.LSP.ApplyWorkspaceEditParams 1281 | function Client:workspace_applyEdit(params) 1282 | local that, request_id, reject_ = self, nil, nil 1283 | local task = AsyncTask.new(function(resolve, reject) 1284 | request_id = self.client.request('workspace/applyEdit', params, function(err, res) 1285 | if err then 1286 | reject(err) 1287 | else 1288 | resolve(res) 1289 | end 1290 | end) 1291 | reject_ = reject 1292 | end) 1293 | function task.cancel() 1294 | that.client.cancel_request(request_id) 1295 | reject_(LSP.ErrorCodes.RequestCancelled) 1296 | end 1297 | return task 1298 | end 1299 | 1300 | return Client 1301 | -------------------------------------------------------------------------------- /lua/linkedit/kit/LSP/Position.lua: -------------------------------------------------------------------------------- 1 | local LSP = require('linkedit.kit.LSP') 2 | 3 | local Position = {} 4 | 5 | ---Return the value is position or not. 6 | ---@param v any 7 | ---@return boolean 8 | function Position.is(v) 9 | local is = true 10 | is = is and (type(v) == 'table' and type(v.line) == 'number' and type(v.character) == 'number') 11 | return is 12 | end 13 | 14 | ---Create a cursor position. 15 | ---@param encoding? linkedit.kit.LSP.PositionEncodingKind 16 | function Position.cursor(encoding) 17 | local r, c = unpack(vim.api.nvim_win_get_cursor(0)) 18 | local utf8 = { line = r - 1, character = c } 19 | if encoding == LSP.PositionEncodingKind.UTF8 then 20 | return utf8 21 | else 22 | local text = vim.api.nvim_get_current_line() 23 | if encoding == LSP.PositionEncodingKind.UTF32 then 24 | return Position.to(text, utf8, LSP.PositionEncodingKind.UTF8, LSP.PositionEncodingKind.UTF32) 25 | end 26 | return Position.to(text, utf8, LSP.PositionEncodingKind.UTF8, LSP.PositionEncodingKind.UTF16) 27 | end 28 | end 29 | 30 | ---Convert position to buffer position from specified encoding. 31 | ---@param bufnr number 32 | ---@param position linkedit.kit.LSP.Position 33 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 34 | function Position.to_buf(bufnr, position, from_encoding) 35 | from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16 36 | local text = vim.api.nvim_buf_get_lines(bufnr, position.line, position.line + 1, false)[1] or '' 37 | return Position.to(text, position, from_encoding, LSP.PositionEncodingKind.UTF8) 38 | end 39 | 40 | ---Convert position to utf8 from specified encoding. 41 | ---@param text string 42 | ---@param position linkedit.kit.LSP.Position 43 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 44 | ---@return linkedit.kit.LSP.Position 45 | function Position.to_utf8(text, position, from_encoding) 46 | from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16 47 | if from_encoding == LSP.PositionEncodingKind.UTF8 then 48 | return position 49 | end 50 | local ok, byteindex = pcall(function() 51 | return vim.str_byteindex(text, position.character, from_encoding == LSP.PositionEncodingKind.UTF16) 52 | end) 53 | if ok then 54 | position = { line = position.line, character = byteindex } 55 | end 56 | return position 57 | end 58 | 59 | ---Convert position to utf16 from specified encoding. 60 | ---@param text string 61 | ---@param position linkedit.kit.LSP.Position 62 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 63 | ---@return linkedit.kit.LSP.Position 64 | function Position.to_utf16(text, position, from_encoding) 65 | local utf8 = Position.to_utf8(text, position, from_encoding) 66 | for index = utf8.character, 0, -1 do 67 | local ok, utf16index = pcall(function() 68 | return select(2, vim.str_utfindex(text, index)) 69 | end) 70 | if ok then 71 | position = { line = utf8.line, character = utf16index } 72 | break 73 | end 74 | end 75 | return position 76 | end 77 | 78 | ---Convert position to utf32 from specified encoding. 79 | ---@param text string 80 | ---@param position linkedit.kit.LSP.Position 81 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 82 | ---@return linkedit.kit.LSP.Position 83 | function Position.to_utf32(text, position, from_encoding) 84 | local utf8 = Position.to_utf8(text, position, from_encoding) 85 | for index = utf8.character, 0, -1 do 86 | local ok, utf32index = pcall(function() 87 | return select(1, vim.str_utfindex(text, index)) 88 | end) 89 | if ok then 90 | position = { line = utf8.line, character = utf32index } 91 | break 92 | end 93 | end 94 | return position 95 | end 96 | 97 | ---Convert position to specified encoding from specified encoding. 98 | ---@param text string 99 | ---@param position linkedit.kit.LSP.Position 100 | ---@param from_encoding linkedit.kit.LSP.PositionEncodingKind 101 | ---@param to_encoding linkedit.kit.LSP.PositionEncodingKind 102 | function Position.to(text, position, from_encoding, to_encoding) 103 | if to_encoding == LSP.PositionEncodingKind.UTF8 then 104 | return Position.to_utf8(text, position, from_encoding) 105 | elseif to_encoding == LSP.PositionEncodingKind.UTF16 then 106 | return Position.to_utf16(text, position, from_encoding) 107 | elseif to_encoding == LSP.PositionEncodingKind.UTF32 then 108 | return Position.to_utf32(text, position, from_encoding) 109 | end 110 | error('LSP.Position: Unsupported encoding: ' .. to_encoding) 111 | end 112 | 113 | return Position 114 | -------------------------------------------------------------------------------- /lua/linkedit/kit/LSP/Range.lua: -------------------------------------------------------------------------------- 1 | local Position = require('linkedit.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 linkedit.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 linkedit.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 number 28 | ---@param range linkedit.kit.LSP.Range 29 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 30 | ---@return linkedit.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 linkedit.kit.LSP.Range 41 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 42 | ---@return linkedit.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 linkedit.kit.LSP.Range 53 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 54 | ---@return linkedit.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 linkedit.kit.LSP.Range 65 | ---@param from_encoding? linkedit.kit.LSP.PositionEncodingKind 66 | ---@return linkedit.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/linkedit/kit/Spec/init.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | local assert = require('luassert') 3 | 4 | ---@class linkedit.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? linkedit.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 | 47 | local lines, cursor = parse_buffer(buffer) 48 | vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) 49 | vim.api.nvim_win_set_cursor(0, cursor) 50 | end 51 | 52 | ---Expect buffer. 53 | function Spec.expect(buffer) 54 | local lines, cursor = parse_buffer(buffer) 55 | assert.are.same(lines, vim.api.nvim_buf_get_lines(0, 0, -1, false)) 56 | assert.are.same(cursor, vim.api.nvim_win_get_cursor(0)) 57 | end 58 | 59 | return Spec 60 | -------------------------------------------------------------------------------- /lua/linkedit/kit/Vim/Keymap.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | local Async = require('linkedit.kit.Async') 3 | 4 | ---@alias linkedit.kit.Vim.Keymap.Keys { keys: string, remap?: boolean } 5 | ---@alias linkedit.kit.Vim.Keymap.KeysSpecifier string|linkedit.kit.Vim.Keymap.Keys 6 | 7 | ---@param keys linkedit.kit.Vim.Keymap.KeysSpecifier 8 | ---@return linkedit.kit.Vim.Keymap.Keys 9 | local function to_keys(keys) 10 | if type(keys) == 'table' then 11 | return keys 12 | end 13 | return { keys = keys, remap = false } 14 | end 15 | 16 | local Keymap = {} 17 | 18 | Keymap._callbacks = {} 19 | 20 | ---Replace termcodes. 21 | ---@param keys string 22 | ---@return string 23 | function Keymap.termcodes(keys) 24 | return vim.api.nvim_replace_termcodes(keys, true, true, true) 25 | end 26 | 27 | ---Normalize keycode. 28 | function Keymap.normalize(s) 29 | return vim.fn.keytrans(Keymap.termcodes(s)) 30 | end 31 | 32 | ---Set callback for consuming next typeahead. 33 | ---@param callback fun() 34 | ---@return linkedit.kit.Async.AsyncTask 35 | function Keymap.next(callback) 36 | return Keymap.send(''):next(callback) 37 | end 38 | 39 | ---Send keys. 40 | ---@param keys linkedit.kit.Vim.Keymap.KeysSpecifier|linkedit.kit.Vim.Keymap.KeysSpecifier[] 41 | ---@param no_insert? boolean 42 | ---@return linkedit.kit.Async.AsyncTask 43 | function Keymap.send(keys, no_insert) 44 | local unique_id = kit.unique_id() 45 | return Async.new(function(resolve, _) 46 | Keymap._callbacks[unique_id] = resolve 47 | 48 | local callback = Keymap.termcodes(('lua require("linkedit.kit.Vim.Keymap")._resolve(%s)'):format(unique_id)) 49 | if no_insert then 50 | for _, keys_ in ipairs(kit.to_array(keys)) do 51 | keys_ = to_keys(keys_) 52 | vim.api.nvim_feedkeys(keys_.keys, keys_.remap and 'm' or 'n', true) 53 | end 54 | vim.api.nvim_feedkeys(callback, 'n', true) 55 | else 56 | vim.api.nvim_feedkeys(callback, 'in', true) 57 | for _, keys_ in ipairs(kit.reverse(kit.to_array(keys))) do 58 | keys_ = to_keys(keys_) 59 | vim.api.nvim_feedkeys(keys_.keys, 'i' .. (keys_.remap and 'm' or 'n'), true) 60 | end 61 | end 62 | end):catch(function() 63 | Keymap._callbacks[unique_id] = nil 64 | end) 65 | end 66 | 67 | ---Return sendabke keys with callback function. 68 | ---@param callback fun(...: any): any 69 | ---@return string 70 | function Keymap.to_sendable(callback) 71 | local unique_id = kit.unique_id() 72 | Keymap._callbacks[unique_id] = Async.async(callback) 73 | return Keymap.termcodes(('lua require("linkedit.kit.Vim.Keymap")._resolve(%s)'):format(unique_id)) 74 | end 75 | 76 | ---Test spec helper. 77 | ---@param spec fun(): any 78 | function Keymap.spec(spec) 79 | local task = Async.resolve():next(Async.async(spec)) 80 | vim.api.nvim_feedkeys('', 'x', true) 81 | task:sync() 82 | collectgarbage('collect') 83 | vim.wait(200) 84 | end 85 | 86 | ---Resolve running keys. 87 | ---@param unique_id integer 88 | function Keymap._resolve(unique_id) 89 | Keymap._callbacks[unique_id]() 90 | Keymap._callbacks[unique_id] = nil 91 | end 92 | 93 | return Keymap 94 | -------------------------------------------------------------------------------- /lua/linkedit/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 number 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/linkedit/kit/Vim/Syntax.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | 3 | local Syntax = {} 4 | 5 | ---Return the specified position is in the specified syntax. 6 | ---@param cursor { [1]: integer, [2]: integer } 7 | ---@param groups string[] 8 | function Syntax.within(cursor, groups) 9 | for _, group in ipairs(Syntax.get_syntax_groups(cursor)) do 10 | if vim.tbl_contains(groups, group) then 11 | return true 12 | end 13 | end 14 | return false 15 | end 16 | 17 | ---Get all syntax groups for specified position. 18 | ---NOTE: This function accepts 0-origin cursor position. 19 | ---@param cursor { [1]: integer, [2]: integer } 20 | ---@return string[] 21 | function Syntax.get_syntax_groups(cursor) 22 | return kit.concat(Syntax.get_vim_syntax_groups(cursor), Syntax.get_treesitter_syntax_groups(cursor)) 23 | end 24 | 25 | ---Get vim's syntax groups for specified position. 26 | ---NOTE: This function accepts 0-origin cursor position. 27 | ---@param cursor { [1]: integer, [2]: integer } 28 | ---@return string[] 29 | function Syntax.get_vim_syntax_groups(cursor) 30 | local unique = {} 31 | local groups = {} 32 | for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do 33 | local name = vim.fn.synIDattr(vim.fn.synIDtrans(syntax_id), 'name') 34 | if not unique[name] then 35 | unique[name] = true 36 | table.insert(groups, name) 37 | end 38 | end 39 | for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do 40 | local name = vim.fn.synIDattr(syntax_id, 'name') 41 | if not unique[name] then 42 | unique[name] = true 43 | table.insert(groups, name) 44 | end 45 | end 46 | return groups 47 | end 48 | 49 | ---Get tree-sitter's syntax groups for specified position. 50 | ---NOTE: This function accepts 0-origin cursor position. 51 | ---@param cursor { [1]: integer, [2]: integer } 52 | ---@return string[] 53 | function Syntax.get_treesitter_syntax_groups(cursor) 54 | local groups = {} 55 | for _, capture in ipairs(vim.treesitter.get_captures_at_pos(0, cursor[1], cursor[2])) do 56 | table.insert(groups, ('@%s'):format(capture.capture)) 57 | end 58 | return groups 59 | end 60 | 61 | return Syntax 62 | -------------------------------------------------------------------------------- /lua/linkedit/kit/init.lua: -------------------------------------------------------------------------------- 1 | local kit = {} 2 | 3 | local islist = vim.islist or vim.tbl_islist 4 | local isempty = vim.tbl_isempty 5 | 6 | ---Create gabage collection detector. 7 | ---@param callback fun(...: any): any 8 | ---@return userdata 9 | function kit.gc(callback) 10 | local gc = newproxy(true) 11 | if vim.is_thread() or os.getenv('NODE_ENV') == 'test' then 12 | getmetatable(gc).__gc = callback 13 | else 14 | getmetatable(gc).__gc = vim.schedule_wrap(callback) 15 | end 16 | return gc 17 | end 18 | 19 | do 20 | local mpack = require('mpack') 21 | 22 | local MpackFunctionType = {} 23 | MpackFunctionType.__index = MpackFunctionType 24 | 25 | kit.Packer = mpack.Packer({ 26 | ext = { 27 | [MpackFunctionType] = function(data) 28 | return 5, string.dump(data.fn) 29 | end 30 | } 31 | }) 32 | 33 | kit.Unpacker = mpack.Unpacker({ 34 | ext = { 35 | [5] = function(_, data) 36 | return loadstring(data) 37 | end 38 | } 39 | }) 40 | 41 | ---Serialize object like values. 42 | ---@param target any 43 | ---@return string 44 | function kit.pack(target) 45 | if type(target) == 'nil' then 46 | return kit.Packer(mpack.NIL) 47 | end 48 | if type(target) == 'table' then 49 | local copy = kit.clone(target) 50 | kit.traverse(copy, function(v, parent, path) 51 | if type(v) == 'function' then 52 | if parent == nil then 53 | error('The root value cannot be a function.') 54 | end 55 | kit.set(parent, path, setmetatable({ fn = v }, MpackFunctionType)) 56 | end 57 | end) 58 | return kit.Packer(copy) 59 | end 60 | return kit.Packer(target) 61 | end 62 | 63 | ---Deserialize object like values. 64 | ---@param target string 65 | ---@return any 66 | function kit.unpack(target) 67 | return kit.Unpacker(target) 68 | end 69 | end 70 | 71 | ---Bind arguments for function. 72 | ---@param fn fun(...: any): any 73 | ---@vararg any 74 | ---@return fun(...: any): any 75 | function kit.bind(fn, ...) 76 | local args = { ... } 77 | return function(...) 78 | return fn(unpack(args), ...) 79 | end 80 | end 81 | 82 | ---Safe version of vim.schedule. 83 | ---@param fn fun(...: any): any 84 | function kit.safe_schedule(fn) 85 | if vim.is_thread() then 86 | fn() 87 | else 88 | vim.schedule(fn) 89 | end 90 | end 91 | 92 | ---Safe version of vim.schedule_wrap. 93 | ---@param fn fun(...: any): any 94 | function kit.safe_schedule_wrap(fn) 95 | if vim.is_thread() then 96 | return fn 97 | else 98 | return vim.schedule_wrap(fn) 99 | end 100 | end 101 | 102 | ---Traverse object tree. 103 | ---@param root_node any 104 | ---@param root_callback fun(v: any, parent: table, path: string[]) 105 | function kit.traverse(root_node, root_callback) 106 | local function traverse(node, callback, parent, path) 107 | if type(node) == 'table' then 108 | for k, v in pairs(node) do 109 | traverse(v, callback, node, kit.concat(path, { k })) 110 | end 111 | else 112 | callback(node, parent, path) 113 | end 114 | end 115 | traverse(root_node, root_callback, nil, {}) 116 | end 117 | 118 | ---Create unique id. 119 | ---@return integer 120 | kit.unique_id = setmetatable({ 121 | unique_id = 0, 122 | }, { 123 | __call = function(self) 124 | self.unique_id = self.unique_id + 1 125 | return self.unique_id 126 | end, 127 | }) 128 | 129 | ---Clone object. 130 | ---@generic T 131 | ---@param target T 132 | ---@return T 133 | function kit.clone(target) 134 | if kit.is_array(target) then 135 | local new_tbl = {} 136 | for k, v in ipairs(target) do 137 | new_tbl[k] = kit.clone(v) 138 | end 139 | return new_tbl 140 | elseif kit.is_dict(target) then 141 | local new_tbl = {} 142 | for k, v in pairs(target) do 143 | new_tbl[k] = kit.clone(v) 144 | end 145 | return new_tbl 146 | end 147 | return target 148 | end 149 | 150 | ---Merge two tables. 151 | ---@generic T: any[] 152 | ---NOTE: This doesn't merge array-like table. 153 | ---@param tbl1 T 154 | ---@param tbl2 T 155 | ---@return T 156 | function kit.merge(tbl1, tbl2) 157 | local is_dict1 = kit.is_dict(tbl1) 158 | local is_dict2 = kit.is_dict(tbl2) 159 | if is_dict1 and is_dict2 then 160 | local new_tbl = {} 161 | for k, v in pairs(tbl2) do 162 | if tbl1[k] ~= vim.NIL then 163 | new_tbl[k] = kit.merge(tbl1[k], v) 164 | end 165 | end 166 | for k, v in pairs(tbl1) do 167 | if tbl2[k] == nil then 168 | if v ~= vim.NIL then 169 | new_tbl[k] = kit.merge(v, {}) 170 | else 171 | new_tbl[k] = nil 172 | end 173 | end 174 | end 175 | return new_tbl 176 | end 177 | 178 | if tbl1 == vim.NIL then 179 | return nil 180 | elseif tbl1 == nil then 181 | return kit.merge(tbl2, {}) 182 | else 183 | return tbl1 184 | end 185 | end 186 | 187 | ---Map array. 188 | ---@param array table 189 | ---@param fn fun(item: unknown, index: integer): unknown 190 | ---@return unknown[] 191 | function kit.map(array, fn) 192 | local new_array = {} 193 | for i, item in ipairs(array) do 194 | table.insert(new_array, fn(item, i)) 195 | end 196 | return new_array 197 | end 198 | 199 | ---Concatenate two tables. 200 | ---NOTE: This doesn't concatenate dict-like table. 201 | ---@param tbl1 table 202 | ---@param tbl2 table 203 | ---@return table 204 | function kit.concat(tbl1, tbl2) 205 | local new_tbl = {} 206 | for _, item in ipairs(tbl1) do 207 | table.insert(new_tbl, item) 208 | end 209 | for _, item in ipairs(tbl2) do 210 | table.insert(new_tbl, item) 211 | end 212 | return new_tbl 213 | end 214 | 215 | ---Return true if v is contained in array. 216 | ---@param array any[] 217 | ---@param v any 218 | ---@return boolean 219 | function kit.contains(array, v) 220 | for _, item in ipairs(array) do 221 | if item == v then 222 | return true 223 | end 224 | end 225 | return false 226 | end 227 | 228 | ---Slice the array. 229 | ---@generic T: any[] 230 | ---@param array T 231 | ---@param s integer 232 | ---@param e integer 233 | ---@return T 234 | function kit.slice(array, s, e) 235 | if not kit.is_array(array) then 236 | error('[kit] specified value is not an array.') 237 | end 238 | local new_array = {} 239 | for i = s, e do 240 | table.insert(new_array, array[i]) 241 | end 242 | return new_array 243 | end 244 | 245 | ---The value to array. 246 | ---@param value any 247 | ---@return table 248 | function kit.to_array(value) 249 | if type(value) == 'table' then 250 | if islist(value) or isempty(value) then 251 | return value 252 | end 253 | end 254 | return { value } 255 | end 256 | 257 | ---Check the value is array. 258 | ---@param value any 259 | ---@return boolean 260 | function kit.is_array(value) 261 | return not not (type(value) == 'table' and (islist(value) or isempty(value))) 262 | end 263 | 264 | ---Check the value is dict. 265 | ---@param value any 266 | ---@return boolean 267 | function kit.is_dict(value) 268 | return type(value) == 'table' and (not islist(value) or isempty(value)) 269 | end 270 | 271 | ---Reverse the array. 272 | ---@param array table 273 | ---@return table 274 | function kit.reverse(array) 275 | if not kit.is_array(array) then 276 | error('[kit] specified value is not an array.') 277 | end 278 | 279 | local new_array = {} 280 | for i = #array, 1, -1 do 281 | table.insert(new_array, array[i]) 282 | end 283 | return new_array 284 | end 285 | 286 | ---@generic T 287 | ---@param value T? 288 | ---@param default T 289 | function kit.default(value, default) 290 | if value == nil then 291 | return default 292 | end 293 | return value 294 | end 295 | 296 | ---Get object path with default value. 297 | ---@generic T 298 | ---@param value table 299 | ---@param path integer|string|(string|integer)[] 300 | ---@param default? T 301 | ---@return T 302 | function kit.get(value, path, default) 303 | local result = value 304 | for _, key in ipairs(kit.to_array(path)) do 305 | if type(result) == 'table' then 306 | result = result[key] 307 | else 308 | return default 309 | end 310 | end 311 | if result == nil then 312 | return default 313 | end 314 | return result 315 | end 316 | 317 | ---Set object path with new value. 318 | ---@param value table 319 | ---@param path integer|string|(string|integer)[] 320 | ---@param new_value any 321 | function kit.set(value, path, new_value) 322 | local current = value 323 | for i = 1, #path - 1 do 324 | local key = path[i] 325 | if type(current[key]) ~= 'table' then 326 | error('The specified path is not a table.') 327 | end 328 | current = current[key] 329 | end 330 | current[path[#path]] = new_value 331 | end 332 | 333 | ---String dedent. 334 | function kit.dedent(s) 335 | local lines = vim.split(s, '\n') 336 | if lines[1]:match('^%s*$') then 337 | table.remove(lines, 1) 338 | end 339 | if lines[#lines]:match('^%s*$') then 340 | table.remove(lines, #lines) 341 | end 342 | local base_indent = lines[1]:match('^%s*') 343 | for i, line in ipairs(lines) do 344 | lines[i] = line:gsub('^' .. base_indent, '') 345 | end 346 | return table.concat(lines, '\n') 347 | end 348 | 349 | return kit 350 | -------------------------------------------------------------------------------- /lua/linkedit/source/lsp_linked_editing_range.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | local Async = require('linkedit.kit.Async') 3 | local Client = require('linkedit.kit.LSP.Client') 4 | local Range = require('linkedit.kit.LSP.Range') 5 | 6 | ---@type linkedit.Source 7 | local Source = {} 8 | Source.__index = Source 9 | 10 | function Source.new() 11 | return setmetatable({}, Source) 12 | end 13 | 14 | ---@param params linkedit.kit.LSP.LinkedEditingRangeParams 15 | ---@return linkedit.kit.Async.AsyncTask linkedit.kit.LSP.TextDocumentLinkedEditingRangeResponse 16 | function Source:fetch(params) 17 | return Async.run(function() 18 | ---@type linkedit.kit.LSP.Client? 19 | local client 20 | for _, client_ in ipairs(vim.lsp.get_clients({ bufnr = 0 })) do 21 | if kit.get(client_.server_capabilities, { 'linkedEditingRangeProvider' }) then 22 | client = Client.new(client_) 23 | break 24 | end 25 | end 26 | if not client then 27 | return Async.resolve(nil) 28 | end 29 | 30 | ---@type linkedit.kit.LSP.TextDocumentLinkedEditingRangeResponse? 31 | local response = client:textDocument_linkedEditingRange(params):await() 32 | if not response then 33 | return Async.resolve(nil) 34 | end 35 | 36 | local bufnr = vim.uri_to_bufnr(params.textDocument.uri) 37 | for i in ipairs(response.ranges) do 38 | local lines = vim.api.nvim_buf_get_lines(bufnr, response.ranges[i].start.line, response.ranges[i]['end'].line + 1, false) 39 | response.ranges[i] = Range.to_utf8(lines[1], lines[#lines], response.ranges[i], client.client.offset_encoding) 40 | end 41 | return Async.resolve(response) 42 | end) 43 | end 44 | 45 | return Source 46 | -------------------------------------------------------------------------------- /lua/linkedit/source/lsp_rename.lua: -------------------------------------------------------------------------------- 1 | local kit = require('linkedit.kit') 2 | local Async = require('linkedit.kit.Async') 3 | local Client = require('linkedit.kit.LSP.Client') 4 | local Range = require('linkedit.kit.LSP.Range') 5 | 6 | ---@type linkedit.Source 7 | local Source = {} 8 | Source.__index = Source 9 | 10 | function Source.new() 11 | return setmetatable({}, Source) 12 | end 13 | 14 | ---@param params linkedit.kit.LSP.LinkedEditingRangeParams 15 | ---@return linkedit.kit.Async.AsyncTask linkedit.kit.LSP.TextDocumentLinkedEditingRangeResponse 16 | function Source:fetch(params) 17 | return Async.run(function() 18 | ---@type linkedit.kit.LSP.Client? 19 | local client 20 | for _, client_ in ipairs(vim.lsp.get_clients({ bufnr = 0 })) do 21 | if kit.get(client_.server_capabilities, { 'renameProvider' }) then 22 | client = Client.new(client_) 23 | break 24 | end 25 | end 26 | if not client then 27 | return Async.resolve(nil) 28 | end 29 | 30 | ---@type linkedit.kit.LSP.TextDocumentRenameResponse? 31 | local response = client:textDocument_rename({ 32 | textDocument = params.textDocument, 33 | position = params.position, 34 | newName = 'dummy', 35 | }):await() 36 | if not response then 37 | return Async.resolve(nil) 38 | end 39 | 40 | ---@type linkedit.kit.LSP.Range[] 41 | local ranges = (function() 42 | if response.documentChanges then 43 | local ranges = {} 44 | for _, change in ipairs(response.documentChanges) do 45 | if change.textDocument.uri == params.textDocument.uri and change.edits then 46 | for _, edit in ipairs(change.edits) do 47 | table.insert(ranges, edit.range) 48 | end 49 | end 50 | end 51 | return ranges 52 | end 53 | if response.changes then 54 | local ranges = {} 55 | for uri, edits in pairs(response.changes) do 56 | if uri == params.textDocument.uri then 57 | for _, edit in ipairs(edits) do 58 | table.insert(ranges, edit.range) 59 | end 60 | end 61 | end 62 | return ranges 63 | end 64 | return {} 65 | end)() 66 | 67 | if #ranges == 0 then 68 | return Async.resolve(nil) 69 | end 70 | 71 | local bufnr = vim.uri_to_bufnr(params.textDocument.uri) 72 | for i in ipairs(ranges) do 73 | local lines = vim.api.nvim_buf_get_lines(bufnr, ranges[i].start.line, ranges[i]['end'].line + 1, false) 74 | ranges[i] = Range.to_utf8(lines[1], lines[#lines], ranges[i], client.client.offset_encoding) 75 | end 76 | 77 | return Async.resolve({ 78 | ranges = ranges, 79 | }) 80 | end) 81 | end 82 | 83 | return Source 84 | 85 | -------------------------------------------------------------------------------- /plugin/linkedit.lua: -------------------------------------------------------------------------------- 1 | local memoize = setmetatable({ 2 | bufnr = nil, 3 | changedtick = nil, 4 | cursor_row = nil, 5 | cursor_col = nil, 6 | clear = function(self) 7 | self.bufnr = nil 8 | self.changedtick = nil 9 | self.cursor_row = nil 10 | self.cursor_col = nil 11 | end, 12 | update = function(self) 13 | self.bufnr = vim.api.nvim_get_current_buf() 14 | self.changedtick = vim.api.nvim_buf_get_changedtick(self.bufnr) 15 | self.cursor_row, self.cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) 16 | end 17 | }, { 18 | __call = function(self, f) 19 | return function(...) 20 | local new_state = {} 21 | new_state.bufnr = vim.api.nvim_get_current_buf() 22 | new_state.changedtick = vim.api.nvim_buf_get_changedtick(new_state.bufnr) 23 | new_state.cursor_row, new_state.cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) 24 | if self.bufnr ~= new_state.bufnr or self.changedtick ~= new_state.changedtick or self.cursor_row ~= new_state.cursor_row or self.cursor_col ~= new_state.cursor_col then 25 | return f(...) 26 | end 27 | end 28 | end 29 | }) 30 | 31 | local function is_insert_mode() 32 | return vim.api.nvim_get_mode().mode:sub(1, 1) == 'i' 33 | end 34 | 35 | local group = vim.api.nvim_create_augroup('linkedit', { 36 | clear = true, 37 | }) 38 | 39 | -- Fetch handling. 40 | vim.api.nvim_create_autocmd({ 'ModeChanged' }, { 41 | group = group, 42 | pattern = { 43 | '*:no', 44 | '*:nov', 45 | '*:noV', 46 | '*:no', 47 | 'n:i' 48 | }, 49 | callback = memoize(function(e) 50 | local linkedit = require('linkedit') 51 | if linkedit.config:get().enabled then 52 | linkedit.fetch(e.match == 'n:i' and 'insert' or 'operator') 53 | vim.cmd.redraw() 54 | memoize:update() 55 | end 56 | end) 57 | }) 58 | 59 | -- Clear handling. 60 | vim.api.nvim_create_autocmd({ 'ModeChanged' }, { 61 | group = group, 62 | pattern = { 63 | 'no:*', 64 | 'nov:*', 65 | 'noV:*', 66 | 'no:*', 67 | 'i:n', 68 | }, 69 | callback = memoize(function(e) 70 | vim.schedule(function() 71 | local linkedit = require('linkedit') 72 | if e.match == 'i:n' then 73 | linkedit.clear() 74 | memoize:clear() 75 | return 76 | end 77 | 78 | -- operator-pending-mode -> not insert-mode. 79 | if not is_insert_mode() then 80 | vim.api.nvim_create_autocmd('CursorMoved', { 81 | pattern = '*', 82 | once = true, 83 | callback = function() 84 | vim.schedule(function() 85 | if is_insert_mode() then 86 | vim.api.nvim_create_autocmd('InsertLeave', { 87 | pattern = '*', 88 | once = true, 89 | callback = function() 90 | linkedit.clear() 91 | memoize:clear() 92 | end 93 | }) 94 | else 95 | linkedit.clear() 96 | memoize:clear() 97 | end 98 | end) 99 | end 100 | }) 101 | end 102 | end) 103 | end) 104 | }) 105 | 106 | vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, { 107 | group = group, 108 | pattern = '*', 109 | callback = memoize(function() 110 | local linkedit = require('linkedit') 111 | if linkedit.config:get().enabled then 112 | linkedit.sync() 113 | memoize:update() 114 | end 115 | end) 116 | }) 117 | 118 | vim.api.nvim_set_hl(0, 'LinkedEditingRange', { link = 'Substitute', default = true }) 119 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | column_width = 1200 4 | quote_style = "AutoPreferSingle" 5 | --------------------------------------------------------------------------------