ui.lua 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. local M = {}
  2. --- Prompts the user to pick from a list of items, allowing arbitrary (potentially asynchronous)
  3. --- work until `on_choice`.
  4. ---
  5. --- Example:
  6. ---
  7. --- ```lua
  8. --- vim.ui.select({ 'tabs', 'spaces' }, {
  9. --- prompt = 'Select tabs or spaces:',
  10. --- format_item = function(item)
  11. --- return "I'd like to choose " .. item
  12. --- end,
  13. --- }, function(choice)
  14. --- if choice == 'spaces' then
  15. --- vim.o.expandtab = true
  16. --- else
  17. --- vim.o.expandtab = false
  18. --- end
  19. --- end)
  20. --- ```
  21. ---
  22. ---@generic T
  23. ---@param items T[] Arbitrary items
  24. ---@param opts table Additional options
  25. --- - prompt (string|nil)
  26. --- Text of the prompt. Defaults to `Select one of:`
  27. --- - format_item (function item -> text)
  28. --- Function to format an
  29. --- individual item from `items`. Defaults to `tostring`.
  30. --- - kind (string|nil)
  31. --- Arbitrary hint string indicating the item shape.
  32. --- Plugins reimplementing `vim.ui.select` may wish to
  33. --- use this to infer the structure or semantics of
  34. --- `items`, or the context in which select() was called.
  35. ---@param on_choice fun(item: T|nil, idx: integer|nil)
  36. --- Called once the user made a choice.
  37. --- `idx` is the 1-based index of `item` within `items`.
  38. --- `nil` if the user aborted the dialog.
  39. function M.select(items, opts, on_choice)
  40. vim.validate('items', items, 'table')
  41. vim.validate('on_choice', on_choice, 'function')
  42. opts = opts or {}
  43. local choices = { opts.prompt or 'Select one of:' }
  44. local format_item = opts.format_item or tostring
  45. for i, item in
  46. ipairs(items --[[@as any[] ]])
  47. do
  48. table.insert(choices, string.format('%d: %s', i, format_item(item)))
  49. end
  50. local choice = vim.fn.inputlist(choices)
  51. if choice < 1 or choice > #items then
  52. on_choice(nil, nil)
  53. else
  54. on_choice(items[choice], choice)
  55. end
  56. end
  57. --- Prompts the user for input, allowing arbitrary (potentially asynchronous) work until
  58. --- `on_confirm`.
  59. ---
  60. --- Example:
  61. ---
  62. --- ```lua
  63. --- vim.ui.input({ prompt = 'Enter value for shiftwidth: ' }, function(input)
  64. --- vim.o.shiftwidth = tonumber(input)
  65. --- end)
  66. --- ```
  67. ---
  68. ---@param opts table? Additional options. See |input()|
  69. --- - prompt (string|nil)
  70. --- Text of the prompt
  71. --- - default (string|nil)
  72. --- Default reply to the input
  73. --- - completion (string|nil)
  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. --- - highlight (function)
  80. --- Function that will be used for highlighting
  81. --- user inputs.
  82. ---@param on_confirm function ((input|nil) -> ())
  83. --- Called once the user confirms or abort the input.
  84. --- `input` is what the user typed (it might be
  85. --- an empty string if nothing was entered), or
  86. --- `nil` if the user aborted the dialog.
  87. function M.input(opts, on_confirm)
  88. vim.validate('opts', opts, 'table', true)
  89. vim.validate('on_confirm', on_confirm, 'function')
  90. opts = (opts and not vim.tbl_isempty(opts)) and opts or vim.empty_dict()
  91. -- Note that vim.fn.input({}) returns an empty string when cancelled.
  92. -- vim.ui.input() should distinguish aborting from entering an empty string.
  93. local _canceled = vim.NIL
  94. opts = vim.tbl_extend('keep', opts, { cancelreturn = _canceled })
  95. local ok, input = pcall(vim.fn.input, opts)
  96. if not ok or input == _canceled then
  97. on_confirm(nil)
  98. else
  99. on_confirm(input)
  100. end
  101. end
  102. --- Opens `path` with the system default handler (macOS `open`, Windows `explorer.exe`, Linux
  103. --- `xdg-open`, …), or returns (but does not show) an error message on failure.
  104. ---
  105. --- Expands "~/" and environment variables in filesystem paths.
  106. ---
  107. --- Examples:
  108. ---
  109. --- ```lua
  110. --- -- Asynchronous.
  111. --- vim.ui.open("https://neovim.io/")
  112. --- vim.ui.open("~/path/to/file")
  113. --- -- Use the "osurl" command to handle the path or URL.
  114. --- vim.ui.open("gh#neovim/neovim!29490", { cmd = { 'osurl' } })
  115. --- -- Synchronous (wait until the process exits).
  116. --- local cmd, err = vim.ui.open("$VIMRUNTIME")
  117. --- if cmd then
  118. --- cmd:wait()
  119. --- end
  120. --- ```
  121. ---
  122. ---@param path string Path or URL to open
  123. ---@param opt? { cmd?: string[] } Options
  124. --- - cmd string[]|nil Command used to open the path or URL.
  125. ---
  126. ---@return vim.SystemObj|nil # Command object, or nil if not found.
  127. ---@return nil|string # Error message on failure, or nil on success.
  128. ---
  129. ---@see |vim.system()|
  130. function M.open(path, opt)
  131. vim.validate('path', path, 'string')
  132. local is_uri = path:match('%w+:')
  133. if not is_uri then
  134. path = vim.fs.normalize(path)
  135. end
  136. opt = opt or {}
  137. local cmd ---@type string[]
  138. local job_opt = { text = true, detach = true } --- @type vim.SystemOpts
  139. if opt.cmd then
  140. cmd = vim.list_extend(opt.cmd --[[@as string[] ]], { path })
  141. elseif vim.fn.has('mac') == 1 then
  142. cmd = { 'open', path }
  143. elseif vim.fn.has('win32') == 1 then
  144. if vim.fn.executable('rundll32') == 1 then
  145. cmd = { 'rundll32', 'url.dll,FileProtocolHandler', path }
  146. else
  147. return nil, 'vim.ui.open: rundll32 not found'
  148. end
  149. elseif vim.fn.executable('xdg-open') == 1 then
  150. cmd = { 'xdg-open', path }
  151. job_opt.stdout = false
  152. job_opt.stderr = false
  153. elseif vim.fn.executable('wslview') == 1 then
  154. cmd = { 'wslview', path }
  155. elseif vim.fn.executable('explorer.exe') == 1 then
  156. cmd = { 'explorer.exe', path }
  157. elseif vim.fn.executable('lemonade') == 1 then
  158. cmd = { 'lemonade', 'open', path }
  159. else
  160. return nil, 'vim.ui.open: no handler found (tried: wslview, explorer.exe, xdg-open, lemonade)'
  161. end
  162. return vim.system(cmd, job_opt), nil
  163. end
  164. --- Returns all URLs at cursor, if any.
  165. --- @return string[]
  166. function M._get_urls()
  167. local urls = {} ---@type string[]
  168. local bufnr = vim.api.nvim_get_current_buf()
  169. local cursor = vim.api.nvim_win_get_cursor(0)
  170. local row = cursor[1] - 1
  171. local col = cursor[2]
  172. local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, { row, col }, { row, col }, {
  173. details = true,
  174. type = 'highlight',
  175. overlap = true,
  176. })
  177. for _, v in ipairs(extmarks) do
  178. local details = v[4]
  179. if details and details.url then
  180. urls[#urls + 1] = details.url
  181. end
  182. end
  183. local highlighter = vim.treesitter.highlighter.active[bufnr]
  184. if highlighter then
  185. local range = { row, col, row, col }
  186. local ltree = highlighter.tree:language_for_range(range)
  187. local lang = ltree:lang()
  188. local query = vim.treesitter.query.get(lang, 'highlights')
  189. if query then
  190. local tree = assert(ltree:tree_for_range(range))
  191. for _, match, metadata in query:iter_matches(tree:root(), bufnr, row, row + 1) do
  192. for id, nodes in pairs(match) do
  193. for _, node in ipairs(nodes) do
  194. if vim.treesitter.node_contains(node, range) then
  195. local url = metadata[id] and metadata[id].url
  196. if url and match[url] then
  197. for _, n in
  198. ipairs(match[url] --[[@as TSNode[] ]])
  199. do
  200. urls[#urls + 1] =
  201. vim.treesitter.get_node_text(n, bufnr, { metadata = metadata[url] })
  202. end
  203. end
  204. end
  205. end
  206. end
  207. end
  208. end
  209. end
  210. if #urls == 0 then
  211. -- If all else fails, use the filename under the cursor
  212. table.insert(
  213. urls,
  214. vim._with({ go = { isfname = vim.o.isfname .. ',@-@' } }, function()
  215. return vim.fn.expand('<cfile>')
  216. end)
  217. )
  218. end
  219. return urls
  220. end
  221. return M