_extui.lua 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. --- @brief
  2. ---
  3. ---WARNING: This is an experimental interface intended to replace the message
  4. ---grid in the TUI.
  5. ---
  6. ---To enable the experimental UI (default opts shown):
  7. ---```lua
  8. ---require('vim._extui').enable({
  9. --- enable = true, -- Whether to enable or disable the UI.
  10. --- msg = { -- Options related to the message module.
  11. --- ---@type 'cmd'|'msg' Where to place regular messages, either in the
  12. --- ---cmdline or in a separate ephemeral message window.
  13. --- target = 'cmd',
  14. --- timeout = 4000, -- Time a message is visible in the message window.
  15. --- },
  16. ---})
  17. ---```
  18. ---
  19. ---There are four separate window types used by this interface:
  20. ---- "cmd": The cmdline window; also used for 'showcmd', 'showmode', 'ruler', and
  21. --- messages if 'cmdheight' > 0.
  22. ---- "msg": The message window; used for messages when 'cmdheight' == 0.
  23. ---- "pager": The pager window; used for |:messages| and certain messages
  24. --- that should be shown in full.
  25. ---- "dialog": The dialog window; used for prompt messages that expect user input.
  26. ---
  27. ---These four windows are assigned the "cmd", "msg", "pager" and "dialog"
  28. ---'filetype' respectively. Use a |FileType| autocommand to configure any local
  29. ---options for these windows and their respective buffers.
  30. ---
  31. ---Rather than a |hit-enter-prompt|, messages shown in the cmdline area that do
  32. ---not fit are appended with a `[+x]` "spill" indicator, where `x` indicates the
  33. ---spilled lines. To see the full message, the |g<| command can be used.
  34. local api = vim.api
  35. local ext = require('vim._extui.shared')
  36. ext.msg = require('vim._extui.messages')
  37. ext.cmd = require('vim._extui.cmdline')
  38. local M = {}
  39. local function ui_callback(event, ...)
  40. local handler = ext.msg[event] or ext.cmd[event]
  41. ext.check_targets()
  42. handler(...)
  43. api.nvim__redraw({
  44. flush = handler ~= ext.cmd.cmdline_hide or nil,
  45. cursor = handler == ext.cmd[event] and true or nil,
  46. win = handler == ext.cmd[event] and ext.wins.cmd or nil,
  47. })
  48. end
  49. local scheduled_ui_callback = vim.schedule_wrap(ui_callback)
  50. ---@nodoc
  51. function M.enable(opts)
  52. vim.validate('opts', opts, 'table', true)
  53. if opts.msg then
  54. vim.validate('opts.msg.pos', opts.msg.pos, 'nil', true, 'nil: "pos" moved to opts.target')
  55. vim.validate('opts.msg.box', opts.msg.box, 'nil', true, 'nil: "timeout" moved to opts.msg')
  56. vim.validate('opts.msg.target', opts.msg.target, function(tar)
  57. return tar == 'cmd' or tar == 'msg'
  58. end, "'cmd'|'msg'")
  59. end
  60. ext.cfg = vim.tbl_deep_extend('keep', opts, ext.cfg)
  61. if ext.cfg.enable == false then
  62. -- Detach and cleanup windows, buffers and autocommands.
  63. for _, win in pairs(ext.wins) do
  64. api.nvim_win_close(win, true)
  65. end
  66. for _, buf in pairs(ext.bufs) do
  67. api.nvim_buf_delete(buf, {})
  68. end
  69. api.nvim_clear_autocmds({ group = ext.augroup })
  70. vim.ui_detach(ext.ns)
  71. return
  72. end
  73. vim.ui_attach(ext.ns, { ext_messages = true, set_cmdheight = false }, function(event, ...)
  74. if not (ext.msg[event] or ext.cmd[event]) then
  75. return
  76. end
  77. if vim.in_fast_event() then
  78. scheduled_ui_callback(event, ...)
  79. else
  80. ui_callback(event, ...)
  81. end
  82. return true
  83. end)
  84. -- The visibility and appearance of the cmdline and message window is
  85. -- dependent on some option values. Reconfigure windows when option value
  86. -- has changed and after VimEnter when the user configured value is known.
  87. -- TODO: Reconsider what is needed when this module is enabled by default early in startup.
  88. local function check_cmdheight(value)
  89. ext.check_targets()
  90. -- 'cmdheight' set; (un)hide cmdline window and set its height.
  91. local cfg = { height = math.max(value, 1), hide = value == 0 }
  92. api.nvim_win_set_config(ext.wins.cmd, cfg)
  93. -- Change message position when 'cmdheight' was or becomes 0.
  94. if value == 0 or ext.cmdheight == 0 then
  95. ext.cfg.msg.target = value == 0 and 'msg' or 'cmd'
  96. ext.msg.prev_msg = ''
  97. end
  98. ext.cmdheight = value
  99. end
  100. if vim.v.vim_did_enter == 0 then
  101. vim.schedule(function()
  102. check_cmdheight(vim.o.cmdheight)
  103. end)
  104. end
  105. api.nvim_create_autocmd('OptionSet', {
  106. group = ext.augroup,
  107. pattern = { 'cmdheight' },
  108. callback = function()
  109. check_cmdheight(vim.v.option_new)
  110. ext.msg.set_pos()
  111. end,
  112. desc = 'Set cmdline and message window dimensions for changed option values.',
  113. })
  114. api.nvim_create_autocmd({ 'VimResized', 'TabEnter' }, {
  115. group = ext.augroup,
  116. callback = ext.msg.set_pos,
  117. desc = 'Set cmdline and message window dimensions after shell resize or tabpage change.',
  118. })
  119. api.nvim_create_autocmd('WinEnter', {
  120. callback = function()
  121. local win = api.nvim_get_current_win()
  122. if vim.tbl_contains(ext.wins, win) and api.nvim_win_get_config(win).hide then
  123. vim.cmd.wincmd('p')
  124. end
  125. end,
  126. desc = 'Make sure hidden extui window is never current.',
  127. })
  128. end
  129. return M