_lsp.lua (8318B)
1 local M = {} 2 3 local capabilities = { 4 codeActionProvider = true, 5 documentSymbolProvider = true, 6 executeCommandProvider = { commands = { 'delete_plugin', 'update_plugin', 'skip_update_plugin' } }, 7 hoverProvider = true, 8 } 9 --- @type table<string,function> 10 local methods = {} 11 12 --- @param callback function 13 function methods.initialize(_, callback) 14 return callback(nil, { capabilities = capabilities }) 15 end 16 17 --- @param callback function 18 function methods.shutdown(_, callback) 19 return callback(nil, nil) 20 end 21 22 local get_confirm_bufnr = function(uri) 23 return tonumber(uri:match('^nvim%-pack://confirm#(%d+)$')) 24 end 25 26 local group_header_pattern = '^# (%S+)' 27 local plugin_header_pattern = '^## (.+)$' 28 29 --- @return { group: string?, name: string?, from: integer?, to: integer? } 30 local get_plug_data_at_lnum = function(bufnr, lnum) 31 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 32 --- @type string, string, integer, integer 33 local group, name, from, to 34 for i = lnum, 1, -1 do 35 group = group or lines[i]:match(group_header_pattern) --[[@as string]] 36 -- If group is found earlier than name - `lnum` is for group header line 37 -- If later - proper group header line. 38 if group then 39 break 40 end 41 name = name or lines[i]:match(plugin_header_pattern) --[[@as string]] 42 from = (not from and name) and i or from --[[@as integer]] 43 end 44 if not (group and name and from) then 45 return {} 46 end 47 --- @cast group string 48 --- @cast from integer 49 50 for i = lnum + 1, #lines do 51 if lines[i]:match(group_header_pattern) or lines[i]:match(plugin_header_pattern) then 52 -- Do not include blank line before next section 53 to = i - 2 54 break 55 end 56 end 57 to = to or #lines 58 59 if not (from <= lnum and lnum <= to) then 60 return {} 61 end 62 return { group = group, name = name:gsub(' %(not active%)$', ''), from = from, to = to } 63 end 64 65 --- @alias vim.pack.lsp.Position { line: integer, character: integer } 66 --- @alias vim.pack.lsp.Range { start: vim.pack.lsp.Position, end: vim.pack.lsp.Position } 67 68 --- @param params { textDocument: { uri: string } } 69 --- @param callback function 70 methods['textDocument/documentSymbol'] = function(params, callback) 71 local bufnr = get_confirm_bufnr(params.textDocument.uri) 72 if bufnr == nil then 73 return callback(nil, {}) 74 end 75 76 --- @alias vim.pack.lsp.Symbol { 77 --- name: string, 78 --- kind: number, 79 --- range: vim.pack.lsp.Range, 80 --- selectionRange: vim.pack.lsp.Range, 81 --- children: vim.pack.lsp.Symbol[]?, 82 --- } 83 84 --- @return vim.pack.lsp.Symbol? 85 local new_symbol = function(name, start_line, end_line, kind) 86 if name == nil then 87 return nil 88 end 89 local range = { 90 start = { line = start_line, character = 0 }, 91 ['end'] = { line = end_line, character = 0 }, 92 } 93 return { name = name, kind = kind, range = range, selectionRange = range } 94 end 95 96 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 97 98 --- @return vim.pack.lsp.Symbol[] 99 local parse_headers = function(pattern, start_line, end_line, kind) 100 local res, cur_match, cur_start = {}, nil, nil 101 for i = start_line, end_line do 102 local m = lines[i + 1]:match(pattern) 103 if m ~= nil and m ~= cur_match then 104 table.insert(res, new_symbol(cur_match, cur_start, i, kind)) 105 cur_match, cur_start = m, i 106 end 107 end 108 table.insert(res, new_symbol(cur_match, cur_start, end_line, kind)) 109 return res 110 end 111 112 local group_kind = vim.lsp.protocol.SymbolKind.Namespace 113 local symbols = parse_headers(group_header_pattern, 0, #lines - 1, group_kind) 114 115 local plug_kind = vim.lsp.protocol.SymbolKind.Module 116 for _, group in ipairs(symbols) do 117 local start_line, end_line = group.range.start.line, group.range['end'].line 118 group.children = parse_headers(plugin_header_pattern, start_line, end_line, plug_kind) 119 end 120 121 return callback(nil, symbols) 122 end 123 124 --- @alias vim.pack.lsp.CodeActionContext { diagnostics: table, only: table?, triggerKind: integer? } 125 126 --- @param params { textDocument: { uri: string }, range: vim.pack.lsp.Range, context: vim.pack.lsp.CodeActionContext } 127 --- @param callback function 128 methods['textDocument/codeAction'] = function(params, callback) 129 local bufnr = get_confirm_bufnr(params.textDocument.uri) 130 local empty_kind = vim.lsp.protocol.CodeActionKind.Empty 131 local only = params.context.only or { empty_kind } 132 if not (bufnr and vim.tbl_contains(only, empty_kind)) then 133 return callback(nil, {}) 134 end 135 local plug_data = get_plug_data_at_lnum(bufnr, params.range.start.line + 1) 136 if not plug_data.name then 137 return callback(nil, {}) 138 end 139 140 local function new_action(title, command) 141 return { 142 title = ('%s `%s`'):format(title, plug_data.name), 143 command = { title = title, command = command, arguments = { bufnr, plug_data } }, 144 } 145 end 146 147 local res = {} 148 if plug_data.group == 'Update' then 149 vim.list_extend(res, { 150 new_action('Update', 'update_plugin'), 151 new_action('Skip updating', 'skip_update_plugin'), 152 }, 0) 153 end 154 if not vim.pack.get({ plug_data.name })[1].active then 155 vim.list_extend(res, { new_action('Delete', 'delete_plugin') }) 156 end 157 callback(nil, res) 158 end 159 160 local commands = { 161 update_plugin = function(plug_data) 162 vim.pack.update({ plug_data.name }, { force = true, offline = true }) 163 end, 164 skip_update_plugin = function(_) end, 165 delete_plugin = function(plug_data) 166 vim.pack.del({ plug_data.name }) 167 end, 168 } 169 170 -- NOTE: Use `vim.schedule_wrap` to avoid press-enter after choosing code 171 -- action via built-in `vim.fn.inputlist()` 172 --- @param params { command: string, arguments: table } 173 --- @param callback function 174 methods['workspace/executeCommand'] = vim.schedule_wrap(function(params, callback) 175 --- @type integer, table 176 local bufnr, plug_data = unpack(params.arguments) 177 local ok, err = pcall(commands[params.command], plug_data) 178 if not ok then 179 return callback({ code = 1, message = err }, {}) 180 end 181 182 -- Remove plugin lines (including blank line) to not later act on plugin 183 vim.bo[bufnr].modifiable = true 184 vim.api.nvim_buf_set_lines(bufnr, plug_data.from - 2, plug_data.to, false, {}) 185 vim.bo[bufnr].modifiable, vim.bo[bufnr].modified = false, false 186 callback(nil, {}) 187 end) 188 189 --- @param params { textDocument: { uri: string }, position: vim.pack.lsp.Position } 190 --- @param callback function 191 methods['textDocument/hover'] = function(params, callback) 192 local bufnr = get_confirm_bufnr(params.textDocument.uri) 193 if bufnr == nil then 194 return 195 end 196 197 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 198 local lnum = params.position.line + 1 199 local commit = lines[lnum]:match('^[<>] (%x+) │') or lines[lnum]:match('^Revision.*:%s+(%x+)') 200 local tag = lines[lnum]:match('^• (.+)$') 201 if commit == nil and tag == nil then 202 return 203 end 204 205 local path, path_lnum = nil, lnum - 1 206 while path == nil and path_lnum >= 1 do 207 path = lines[path_lnum]:match('^Path:%s+(.+)$') 208 path_lnum = path_lnum - 1 209 end 210 if path == nil then 211 return 212 end 213 214 local cmd = { 'git', 'show', '--no-color', commit or tag } 215 --- @param sys_out vim.SystemCompleted 216 local on_exit = function(sys_out) 217 local markdown = '```diff\n' .. sys_out.stdout .. '\n```' 218 local res = { contents = { kind = vim.lsp.protocol.MarkupKind.Markdown, value = markdown } } 219 callback(nil, res) 220 end 221 vim.system(cmd, { cwd = path }, vim.schedule_wrap(on_exit)) 222 end 223 224 local dispatchers = {} 225 226 -- TODO: Simplify after `vim.lsp.server` is a thing 227 -- https://github.com/neovim/neovim/pull/24338 228 local cmd = function(disp) 229 -- Store dispatchers to use for showing progress notifications 230 dispatchers = disp 231 local res, closing, request_id = {}, false, 0 232 233 function res.request(method, params, callback) 234 local method_impl = methods[method] 235 if method_impl ~= nil then 236 method_impl(params, callback) 237 end 238 request_id = request_id + 1 239 return true, request_id 240 end 241 242 function res.notify(method, _) 243 if method == 'exit' then 244 dispatchers.on_exit(0, 15) 245 end 246 return false 247 end 248 249 function res.is_closing() 250 return closing 251 end 252 253 function res.terminate() 254 closing = true 255 end 256 257 return res 258 end 259 260 M.client_id = assert( 261 vim.lsp.start({ cmd = cmd, name = 'vim.pack', root_dir = vim.uv.cwd() }, { attach = false }) 262 ) 263 264 return M