init.lua 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. local M = {
  2. state = {
  3. buf = nil,
  4. win = nil,
  5. parent_win = nil,
  6. last_signature = nil,
  7. cache = { row = -1, height = -1 }
  8. },
  9. config = {
  10. borders = { "⌌", "-", "⌍", "", "", "", "", "" },
  11. icons = pcall(require, "status-beast.maps") and
  12. require("status-beast.maps").diagnostic_icons or {},
  13. severities = { "Error", "Warn", "Info", "Hint" },
  14. ns = vim.api.nvim_create_namespace("DiagnosticDock"),
  15. max_height = 20,
  16. min_height = 1,
  17. compact_height = 3,
  18. }
  19. }
  20. local function get_diagnostics(bufnr, winid)
  21. if winid == vim.api.nvim_get_current_win() then
  22. local mode = vim.api.nvim_get_mode().mode
  23. if mode:match("[vV\22]") then
  24. local v_line = vim.fn.line("v") - 1
  25. local cur_line = vim.fn.line(".") - 1
  26. local start_l = math.min(v_line, cur_line)
  27. local end_l = math.max(v_line, cur_line)
  28. local all = vim.diagnostic.get(bufnr)
  29. local filtered = {}
  30. for _, d in ipairs(all) do
  31. if d.lnum >= start_l and d.lnum <= end_l then
  32. table.insert(filtered, d)
  33. end
  34. end
  35. return filtered
  36. end
  37. end
  38. local cursor = vim.api.nvim_win_get_cursor(winid)
  39. return vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 })
  40. end
  41. local function ensure_dock_buffer()
  42. if M.state.buf and vim.api.nvim_buf_is_valid(M.state.buf) then
  43. return M.state.buf
  44. end
  45. local buf = vim.api.nvim_create_buf(false, true)
  46. vim.bo[buf].filetype = "diagnostic-dock"
  47. vim.bo[buf].buftype = "nofile"
  48. vim.bo[buf].swapfile = false
  49. vim.bo[buf].bufhidden = "hide"
  50. local close_dock = function()
  51. M.close()
  52. if M.state.parent_win and vim.api.nvim_win_is_valid(M.state.parent_win) then
  53. vim.api.nvim_set_current_win(M.state.parent_win)
  54. end
  55. end
  56. vim.keymap.set("n", "q", close_dock, { buffer = buf, nowait = true })
  57. vim.keymap.set("n", "<Esc>", close_dock, { buffer = buf, nowait = true })
  58. M.state.buf = buf
  59. M.state.last_signature = nil
  60. return buf
  61. end
  62. local function render(diags)
  63. local buf = ensure_dock_buffer()
  64. local sig = vim.inspect(diags)
  65. if M.state.last_signature == sig then
  66. return buf, vim.api.nvim_buf_line_count(buf)
  67. end
  68. M.state.last_signature = sig
  69. local lines = {}
  70. local highlights = {}
  71. for _, d in ipairs(diags) do
  72. local sev = M.config.severities[d.severity] or "Info"
  73. local icon = M.config.icons[sev] or "●"
  74. local msg = vim.split(d.message, "\n")
  75. local src = d.source and (" " .. d.source) or ""
  76. local header = string.format(" %s %s%s", icon, msg[1], src)
  77. table.insert(lines, header)
  78. table.insert(highlights, {
  79. group = "StatusBeastDiagnostic" .. sev,
  80. line = #lines - 1,
  81. col_start = 1,
  82. col_end = 4
  83. })
  84. if d.source then
  85. local s_start = header:find(d.source, 1, true)
  86. if s_start then
  87. table.insert(highlights, {
  88. group = "Comment",
  89. line = #lines - 1,
  90. col_start = s_start - 1,
  91. col_end = -1
  92. })
  93. end
  94. end
  95. for i = 2, #msg do
  96. table.insert(lines, " " .. msg[i])
  97. end
  98. end
  99. table.insert(lines, "")
  100. vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  101. vim.api.nvim_buf_clear_namespace(buf, M.config.ns, 0, -1)
  102. for _, hl in ipairs(highlights) do
  103. vim.api.nvim_buf_add_highlight(buf, M.config.ns, hl.group, hl.line,
  104. hl.col_start, hl.col_end)
  105. end
  106. return buf, #lines
  107. end
  108. local function ensure_view_clearance(win, dock_height)
  109. local win_h = vim.api.nvim_win_get_height(win)
  110. local visible_h = win_h - dock_height
  111. local cursor_row = vim.fn.winline()
  112. if cursor_row > visible_h then
  113. local view = vim.api.nvim_win_call(win, vim.fn.winsaveview)
  114. view.topline = view.topline + (cursor_row - visible_h)
  115. vim.api.nvim_win_call(win, function() vim.fn.winrestview(view) end)
  116. end
  117. end
  118. function M.close()
  119. if M.state.win and vim.api.nvim_win_is_valid(M.state.win) then
  120. vim.api.nvim_win_close(M.state.win, true)
  121. end
  122. M.state.win = nil
  123. M.state.cache = { row = -1, height = -1 }
  124. end
  125. function M.update_layout(target_win, content_len, focused)
  126. local win_w = vim.api.nvim_win_get_width(target_win)
  127. local win_h = vim.api.nvim_win_get_height(target_win)
  128. local height
  129. if focused then
  130. local limit = math.min(M.config.max_height, math.floor(win_h / 2))
  131. height = math.max(M.config.min_height, math.min(content_len, limit))
  132. else
  133. height = math.min(content_len, M.config.compact_height)
  134. end
  135. local border_h = (M.config.borders[2] ~= "" and 1 or 0) +
  136. (M.config.borders[6] ~= "" and 1 or 0)
  137. local total_h = height + border_h
  138. local row = win_h - total_h
  139. local opts = {
  140. relative = "win",
  141. win = target_win,
  142. row = row,
  143. col = 0,
  144. width = win_w,
  145. height = height,
  146. style = "minimal",
  147. border = M.config.borders,
  148. zindex = 50
  149. }
  150. if M.state.win and vim.api.nvim_win_is_valid(M.state.win) then
  151. if M.state.cache.row ~= row or M.state.cache.height ~= height then
  152. vim.api.nvim_win_set_config(M.state.win, opts)
  153. M.state.cache = { row = row, height = height }
  154. end
  155. else
  156. M.state.win = vim.api.nvim_open_win(M.state.buf, false, opts)
  157. vim.wo[M.state.win].winhl = "Normal:Normal,FloatBorder:WinSeparator"
  158. M.state.cache = { row = row, height = height }
  159. end
  160. vim.wo[M.state.win].wrap = focused
  161. if not focused then
  162. ensure_view_clearance(target_win, total_h)
  163. end
  164. end
  165. function M.open()
  166. local curr_win = vim.api.nvim_get_current_win()
  167. local is_dock_focused = (M.state.win and curr_win == M.state.win)
  168. if is_dock_focused then
  169. if M.state.buf and vim.api.nvim_buf_is_valid(M.state.buf) then
  170. M.update_layout(M.state.parent_win,
  171. vim.api.nvim_buf_line_count(M.state.buf), true)
  172. end
  173. return
  174. end
  175. M.state.parent_win = curr_win
  176. local target_buf = vim.api.nvim_win_get_buf(curr_win)
  177. if vim.bo[target_buf].filetype == "diagnostic-dock" or vim.bo[target_buf].filetype == "butterfly" then
  178. return
  179. end
  180. local diags = get_diagnostics(target_buf, curr_win)
  181. if #diags == 0 then return M.close() end
  182. local _, content_len = render(diags)
  183. M.update_layout(curr_win, content_len, false)
  184. if not vim.b[target_buf].diagnostic_dock_mapped then
  185. vim.keymap.set({ "n", "x" }, "<C-w>", function()
  186. if M.state.win and vim.api.nvim_win_is_valid(M.state.win) then
  187. vim.api.nvim_set_current_win(M.state.win)
  188. end
  189. end, { buffer = target_buf, nowait = true })
  190. vim.b[target_buf].diagnostic_dock_mapped = true
  191. end
  192. end
  193. function M.setup()
  194. local group = vim.api.nvim_create_augroup("DiagnosticDock", { clear = true })
  195. vim.api.nvim_create_autocmd(
  196. { "CursorHold", "ModeChanged", "BufEnter", "WinEnter" }, {
  197. group = group,
  198. callback = function() vim.schedule(M.open) end
  199. })
  200. end
  201. return M