neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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