_inspector.lua 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. --- @diagnostic disable:no-unknown
  2. --- @class vim._inspector.Filter
  3. --- @inlinedoc
  4. ---
  5. --- Include syntax based highlight groups.
  6. --- (default: `true`)
  7. --- @field syntax boolean
  8. ---
  9. --- Include treesitter based highlight groups.
  10. --- (default: `true`)
  11. --- @field treesitter boolean
  12. ---
  13. --- Include extmarks. When `all`, then extmarks without a `hl_group` will also be included.
  14. --- (default: true)
  15. --- @field extmarks boolean|"all"
  16. ---
  17. --- Include semantic token highlights.
  18. --- (default: true)
  19. --- @field semantic_tokens boolean
  20. local defaults = {
  21. syntax = true,
  22. treesitter = true,
  23. extmarks = true,
  24. semantic_tokens = true,
  25. }
  26. ---Get all the items at a given buffer position.
  27. ---
  28. ---Can also be pretty-printed with `:Inspect!`. [:Inspect!]()
  29. ---
  30. ---@since 11
  31. ---@param bufnr? integer defaults to the current buffer
  32. ---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
  33. ---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor
  34. ---@param filter? vim._inspector.Filter Table with key-value pairs to filter the items
  35. ---@return {treesitter:table,syntax:table,extmarks:table,semantic_tokens:table,buffer:integer,col:integer,row:integer} (table) a table with the following key-value pairs. Items are in "traversal order":
  36. --- - treesitter: a list of treesitter captures
  37. --- - syntax: a list of syntax groups
  38. --- - semantic_tokens: a list of semantic tokens
  39. --- - extmarks: a list of extmarks
  40. --- - buffer: the buffer used to get the items
  41. --- - row: the row used to get the items
  42. --- - col: the col used to get the items
  43. function vim.inspect_pos(bufnr, row, col, filter)
  44. filter = vim.tbl_deep_extend('force', defaults, filter or {})
  45. bufnr = bufnr or 0
  46. if row == nil or col == nil then
  47. -- get the row/col from the first window displaying the buffer
  48. local win = bufnr == 0 and vim.api.nvim_get_current_win() or vim.fn.bufwinid(bufnr)
  49. if win == -1 then
  50. error('row/col is required for buffers not visible in a window')
  51. end
  52. local cursor = vim.api.nvim_win_get_cursor(win)
  53. row, col = cursor[1] - 1, cursor[2]
  54. end
  55. bufnr = vim._resolve_bufnr(bufnr)
  56. local results = {
  57. treesitter = {}, --- @type table[]
  58. syntax = {}, --- @type table[]
  59. extmarks = {},
  60. semantic_tokens = {},
  61. buffer = bufnr,
  62. row = row,
  63. col = col,
  64. }
  65. -- resolve hl links
  66. local function resolve_hl(data)
  67. if data.hl_group then
  68. local hlid = vim.api.nvim_get_hl_id_by_name(data.hl_group)
  69. local name = vim.fn.synIDattr(vim.fn.synIDtrans(hlid), 'name')
  70. data.hl_group_link = name
  71. end
  72. return data
  73. end
  74. -- treesitter
  75. if filter.treesitter then
  76. for _, capture in pairs(vim.treesitter.get_captures_at_pos(bufnr, row, col)) do
  77. --- @diagnostic disable-next-line: inject-field
  78. capture.hl_group = '@' .. capture.capture .. '.' .. capture.lang
  79. results.treesitter[#results.treesitter + 1] = resolve_hl(capture)
  80. end
  81. end
  82. -- syntax
  83. if filter.syntax and vim.api.nvim_buf_is_valid(bufnr) then
  84. vim._with({ buf = bufnr }, function()
  85. for _, i1 in ipairs(vim.fn.synstack(row + 1, col + 1)) do
  86. results.syntax[#results.syntax + 1] =
  87. resolve_hl({ hl_group = vim.fn.synIDattr(i1, 'name') })
  88. end
  89. end)
  90. end
  91. -- namespace id -> name map
  92. local nsmap = {} --- @type table<integer,string>
  93. for name, id in pairs(vim.api.nvim_get_namespaces()) do
  94. nsmap[id] = name
  95. end
  96. --- Convert an extmark tuple into a table
  97. local function to_map(extmark)
  98. local opts = resolve_hl(extmark[4])
  99. return {
  100. id = extmark[1],
  101. row = extmark[2],
  102. col = extmark[3],
  103. end_row = opts.end_row or extmark[2],
  104. end_col = opts.end_col or extmark[3],
  105. opts = opts,
  106. ns_id = opts.ns_id,
  107. ns = nsmap[opts.ns_id] or '',
  108. }
  109. end
  110. --- Exclude end_col and unpaired marks from the overlapping marks, unless
  111. --- filter.extmarks == 'all' (a highlight is drawn until end_col - 1).
  112. local function exclude_end_col(extmark)
  113. return filter.extmarks == 'all' or row < extmark.end_row or col < extmark.end_col
  114. end
  115. -- All overlapping extmarks at this position:
  116. local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, { row, col }, { row, col }, {
  117. details = true,
  118. overlap = true,
  119. })
  120. extmarks = vim.tbl_map(to_map, extmarks)
  121. extmarks = vim.tbl_filter(exclude_end_col, extmarks)
  122. if filter.semantic_tokens then
  123. results.semantic_tokens = vim.tbl_filter(function(extmark)
  124. return extmark.ns:find('nvim.lsp.semantic_tokens') == 1
  125. end, extmarks)
  126. end
  127. if filter.extmarks then
  128. results.extmarks = vim.tbl_filter(function(extmark)
  129. return extmark.ns:find('nvim.lsp.semantic_tokens') ~= 1
  130. and (filter.extmarks == 'all' or extmark.opts.hl_group)
  131. end, extmarks)
  132. end
  133. return results
  134. end
  135. ---Show all the items at a given buffer position.
  136. ---
  137. ---Can also be shown with `:Inspect`. [:Inspect]()
  138. ---
  139. ---Example: To bind this function to the vim-scriptease
  140. ---inspired `zS` in Normal mode:
  141. ---
  142. ---```lua
  143. ---vim.keymap.set('n', 'zS', vim.show_pos)
  144. ---```
  145. ---
  146. ---@since 11
  147. ---@param bufnr? integer defaults to the current buffer
  148. ---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
  149. ---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor
  150. ---@param filter? vim._inspector.Filter
  151. function vim.show_pos(bufnr, row, col, filter)
  152. local items = vim.inspect_pos(bufnr, row, col, filter)
  153. local lines = { {} }
  154. local function append(str, hl)
  155. table.insert(lines[#lines], { str, hl })
  156. end
  157. local function nl()
  158. table.insert(lines, {})
  159. end
  160. local function item(data, comment)
  161. append(' - ')
  162. append(data.hl_group, data.hl_group)
  163. append(' ')
  164. if data.hl_group ~= data.hl_group_link then
  165. append('links to ', 'MoreMsg')
  166. append(data.hl_group_link, data.hl_group_link)
  167. append(' ')
  168. end
  169. if comment then
  170. append(comment, 'Comment')
  171. end
  172. nl()
  173. end
  174. -- treesitter
  175. if #items.treesitter > 0 then
  176. append('Treesitter', 'Title')
  177. nl()
  178. for _, capture in ipairs(items.treesitter) do
  179. item(
  180. capture,
  181. string.format(
  182. 'priority: %d language: %s',
  183. capture.metadata.priority or vim.hl.priorities.treesitter,
  184. capture.lang
  185. )
  186. )
  187. end
  188. nl()
  189. end
  190. -- semantic tokens
  191. if #items.semantic_tokens > 0 then
  192. append('Semantic Tokens', 'Title')
  193. nl()
  194. local sorted_marks = vim.fn.sort(items.semantic_tokens, function(left, right)
  195. local left_first = left.opts.priority < right.opts.priority
  196. or left.opts.priority == right.opts.priority and left.opts.hl_group < right.opts.hl_group
  197. return left_first and -1 or 1
  198. end)
  199. for _, extmark in ipairs(sorted_marks) do
  200. item(extmark.opts, 'priority: ' .. extmark.opts.priority)
  201. end
  202. nl()
  203. end
  204. -- syntax
  205. if #items.syntax > 0 then
  206. append('Syntax', 'Title')
  207. nl()
  208. for _, syn in ipairs(items.syntax) do
  209. item(syn)
  210. end
  211. nl()
  212. end
  213. -- extmarks
  214. if #items.extmarks > 0 then
  215. append('Extmarks', 'Title')
  216. nl()
  217. for _, extmark in ipairs(items.extmarks) do
  218. if extmark.opts.hl_group then
  219. item(extmark.opts, extmark.ns)
  220. else
  221. append(' - ')
  222. append(extmark.ns, 'Comment')
  223. nl()
  224. end
  225. end
  226. nl()
  227. end
  228. if #lines[#lines] == 0 then
  229. table.remove(lines)
  230. end
  231. local chunks = {}
  232. for _, line in ipairs(lines) do
  233. vim.list_extend(chunks, line)
  234. table.insert(chunks, { '\n' })
  235. end
  236. if #chunks == 0 then
  237. chunks = {
  238. {
  239. 'No items found at position '
  240. .. items.row
  241. .. ','
  242. .. items.col
  243. .. ' in buffer '
  244. .. items.buffer,
  245. },
  246. }
  247. end
  248. vim.api.nvim_echo(chunks, false, { kind = 'list_cmd' })
  249. end