ui.lua (9574B)
1 local M = {} 2 3 ---@class vim.ui.select.Opts 4 ---@inlinedoc 5 --- 6 --- Text of the prompt. Defaults to `Select one of:` 7 ---@field prompt? string 8 --- 9 --- Function to format an 10 --- individual item from `items`. Defaults to `tostring`. 11 ---@field format_item? fun(item: any):string 12 --- 13 --- Arbitrary hint string indicating the item shape. 14 --- Plugins reimplementing `vim.ui.select` may wish to 15 --- use this to infer the structure or semantics of 16 --- `items`, or the context in which select() was called. 17 ---@field kind? string 18 19 --- Prompts the user to pick from a list of items, allowing arbitrary (potentially asynchronous) 20 --- work until `on_choice`. 21 --- 22 --- Example: 23 --- 24 --- ```lua 25 --- vim.ui.select({ 'tabs', 'spaces' }, { 26 --- prompt = 'Select tabs or spaces:', 27 --- format_item = function(item) 28 --- return "I'd like to choose " .. item 29 --- end, 30 --- }, function(choice) 31 --- if choice == 'spaces' then 32 --- vim.o.expandtab = true 33 --- else 34 --- vim.o.expandtab = false 35 --- end 36 --- end) 37 --- ``` 38 --- 39 ---@generic T 40 ---@param items T[] Arbitrary items 41 ---@param opts vim.ui.select.Opts Additional options 42 ---@param on_choice fun(item: T|nil, idx: integer|nil) 43 --- Called once the user made a choice. 44 --- `idx` is the 1-based index of `item` within `items`. 45 --- `nil` if the user aborted the dialog. 46 function M.select(items, opts, on_choice) 47 vim.validate('items', items, 'table') 48 vim.validate('on_choice', on_choice, 'function') 49 opts = opts or {} 50 local choices = { opts.prompt or 'Select one of:' } 51 local format_item = opts.format_item or tostring 52 for i, item in 53 ipairs(items --[[@as any[] ]]) 54 do 55 table.insert(choices, string.format('%d: %s', i, format_item(item))) 56 end 57 local choice = vim.fn.inputlist(choices) 58 if choice < 1 or choice > #items then 59 on_choice(nil, nil) 60 else 61 on_choice(items[choice], choice) 62 end 63 end 64 65 ---@class vim.ui.input.Opts 66 ---@inlinedoc 67 --- 68 ---Text of the prompt 69 ---@field prompt? string 70 --- 71 ---Default reply to the input 72 ---@field default? string 73 --- 74 ---Specifies type of completion supported 75 ---for input. Supported types are the same 76 ---that can be supplied to a user-defined 77 ---command using the "-complete=" argument. 78 ---See |:command-completion| 79 ---@field completion? string 80 --- 81 ---Function that will be used for highlighting 82 ---user inputs. 83 ---@field highlight? function 84 85 --- Prompts the user for input, allowing arbitrary (potentially asynchronous) work until 86 --- `on_confirm`. 87 --- 88 --- Example: 89 --- 90 --- ```lua 91 --- vim.ui.input({ prompt = 'Enter value for shiftwidth: ' }, function(input) 92 --- vim.o.shiftwidth = tonumber(input) 93 --- end) 94 --- ``` 95 --- 96 ---@param opts? vim.ui.input.Opts Additional options. See |input()| 97 ---@param on_confirm fun(input?: string) 98 --- Called once the user confirms or abort the input. 99 --- `input` is what the user typed (it might be 100 --- an empty string if nothing was entered), or 101 --- `nil` if the user aborted the dialog. 102 function M.input(opts, on_confirm) 103 vim.validate('opts', opts, 'table', true) 104 vim.validate('on_confirm', on_confirm, 'function') 105 106 opts = (opts and not vim.tbl_isempty(opts)) and opts or vim.empty_dict() 107 108 -- Note that vim.fn.input({}) returns an empty string when cancelled. 109 -- vim.ui.input() should distinguish aborting from entering an empty string. 110 local _canceled = vim.NIL 111 opts = vim.tbl_extend('keep', opts, { cancelreturn = _canceled }) 112 113 local ok, input = pcall(vim.fn.input, opts) 114 if not ok or input == _canceled then 115 on_confirm(nil) 116 else 117 on_confirm(input) 118 end 119 end 120 121 ---@class vim.ui.open.Opts 122 ---@inlinedoc 123 --- 124 --- Command used to open the path or URL. 125 ---@field cmd? string[] 126 127 --- Opens `path` with the system default handler (macOS `open`, Windows `explorer.exe`, Linux 128 --- `xdg-open`, …), or returns (but does not show) an error message on failure. 129 --- 130 --- Can also be invoked with `:Open`. [:Open]() 131 --- 132 --- Expands "~/" and environment variables in filesystem paths. 133 --- 134 --- Examples: 135 --- 136 --- ```lua 137 --- -- Asynchronous. 138 --- vim.ui.open("https://neovim.io/") 139 --- vim.ui.open("~/path/to/file") 140 --- -- Use the "osurl" command to handle the path or URL. 141 --- vim.ui.open("gh#neovim/neovim!29490", { cmd = { 'osurl' } }) 142 --- -- Synchronous (wait until the process exits). 143 --- local cmd, err = vim.ui.open("$VIMRUNTIME") 144 --- if cmd then 145 --- cmd:wait() 146 --- end 147 --- ``` 148 --- 149 ---@param path string Path or URL to open 150 ---@param opt? vim.ui.open.Opts Options 151 --- 152 ---@return vim.SystemObj|nil # Command object, or nil if not found. 153 ---@return nil|string # Error message on failure, or nil on success. 154 --- 155 ---@see |vim.system()| 156 function M.open(path, opt) 157 vim.validate('path', path, 'string') 158 local is_uri = path:match('%w+:') 159 if not is_uri then 160 path = vim.fs.normalize(path) 161 end 162 163 opt = opt or {} 164 local cmd ---@type string[] 165 local job_opt = { text = true, detach = true } --- @type vim.SystemOpts 166 167 if opt.cmd then 168 cmd = vim.list_extend(opt.cmd --[[@as string[] ]], { path }) 169 else 170 local open_cmd, err = M._get_open_cmd() 171 if err then 172 return nil, err 173 end 174 ---@cast open_cmd string[] 175 if open_cmd[1] == 'xdg-open' then 176 job_opt.stdout = false 177 job_opt.stderr = false 178 end 179 cmd = vim.list_extend(open_cmd, { path }) 180 end 181 182 return vim.system(cmd, job_opt), nil 183 end 184 185 --- Get an available command used to open the path or URL. 186 --- 187 --- @return string[]|nil # Command, or nil if not found. 188 --- @return nil|string # Error message on failure, or nil on success. 189 function M._get_open_cmd() 190 if vim.fn.has('mac') == 1 then 191 return { 'open' }, nil 192 elseif vim.fn.has('win32') == 1 then 193 return { 'cmd.exe', '/c', 'start', '' }, nil 194 elseif vim.fn.executable('xdg-open') == 1 then 195 return { 'xdg-open' }, nil 196 elseif vim.fn.executable('wslview') == 1 then 197 return { 'wslview' }, nil 198 elseif vim.fn.executable('explorer.exe') == 1 then 199 return { 'explorer.exe' }, nil 200 elseif vim.fn.executable('lemonade') == 1 then 201 return { 'lemonade', 'open' }, nil 202 else 203 return nil, 'vim.ui.open: no handler found (tried: wslview, explorer.exe, xdg-open, lemonade)' 204 end 205 end 206 207 --- @param bufnr integer 208 local get_lsp_urls = function(bufnr) 209 local has_lsp_support = false 210 for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do 211 has_lsp_support = has_lsp_support or client:supports_method('textDocument/documentLink', bufnr) 212 end 213 if not has_lsp_support then 214 return {} 215 end 216 local params = { textDocument = vim.lsp.util.make_text_document_params(bufnr) } 217 local results = vim.lsp.buf_request_sync(bufnr, 'textDocument/documentLink', params) 218 219 local urls = {} 220 for client_id, result in pairs(results or {}) do 221 if result.error then 222 vim.lsp.log.error(result.error) 223 else 224 local client = assert(vim.lsp.get_client_by_id(client_id)) 225 local lsp_position = vim.lsp.util.make_position_params(0, client.offset_encoding).position 226 local position = vim.pos.lsp(bufnr, lsp_position, client.offset_encoding) 227 228 local document_links = result.result or {} ---@type lsp.DocumentLink[] 229 for _, document_link in ipairs(document_links) do 230 local range = vim.range.lsp(bufnr, document_link.range, client.offset_encoding) 231 if document_link.target and range:has(position) then 232 local target = document_link.target ---@type string 233 if vim.startswith(target, 'file://') then 234 target = vim.uri_to_fname(target) 235 end 236 table.insert(urls, target) 237 end 238 end 239 end 240 end 241 return urls 242 end 243 244 --- Returns all URLs at cursor, if any. 245 --- @return string[] 246 function M._get_urls() 247 local urls = {} ---@type string[] 248 249 local bufnr = vim.api.nvim_get_current_buf() 250 local cursor = vim.api.nvim_win_get_cursor(0) 251 local row = cursor[1] - 1 252 local col = cursor[2] 253 254 urls = vim.list_extend(urls, get_lsp_urls(bufnr)) 255 256 local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, { row, col }, { row, col }, { 257 details = true, 258 type = 'highlight', 259 overlap = true, 260 }) 261 for _, v in ipairs(extmarks) do 262 local details = v[4] 263 if details and details.url then 264 urls[#urls + 1] = details.url 265 end 266 end 267 268 local highlighter = vim.treesitter.highlighter.active[bufnr] 269 if highlighter then 270 local range = { row, col, row, col } 271 local ltree = highlighter.tree:language_for_range(range) 272 local lang = ltree:lang() 273 local query = vim.treesitter.query.get(lang, 'highlights') 274 if query then 275 local tree = assert(ltree:tree_for_range(range)) 276 for _, match, metadata in query:iter_matches(tree:root(), bufnr, row, row + 1) do 277 for id, nodes in pairs(match) do 278 for _, node in ipairs(nodes) do 279 if vim.treesitter.node_contains(node, range) then 280 local url = metadata[id] and metadata[id].url 281 if url and match[url] then 282 for _, n in 283 ipairs(match[url] --[[@as TSNode[] ]]) 284 do 285 urls[#urls + 1] = 286 vim.treesitter.get_node_text(n, bufnr, { metadata = metadata[url] }) 287 end 288 end 289 end 290 end 291 end 292 end 293 end 294 end 295 296 if #urls == 0 then 297 -- If all else fails, use the filename under the cursor 298 table.insert( 299 urls, 300 vim._with({ go = { isfname = vim.o.isfname .. ',@-@' } }, function() 301 return vim.fn.expand('<cfile>') 302 end) 303 ) 304 end 305 306 return urls 307 end 308 309 return M