aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/_inspector.lua
blob: 92d380b08c2fb496e9cac04f44adc084bf290017 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
---@class InspectorFilter
---@field syntax boolean include syntax based highlight groups (defaults to true)
---@field treesitter boolean include treesitter based highlight groups (defaults to true)
---@field extmarks boolean|"all" include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true)
---@field semantic_tokens boolean include semantic token highlights (defaults to true)
local defaults = {
  syntax = true,
  treesitter = true,
  extmarks = true,
  semantic_tokens = true,
}

---Get all the items at a given buffer position.
---
---Can also be pretty-printed with `:Inspect!`. *:Inspect!*
---
---@param bufnr? integer defaults to the current buffer
---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor
---@param filter? InspectorFilter (table|nil) a table with key-value pairs to filter the items
---               - syntax (boolean): include syntax based highlight groups (defaults to true)
---               - treesitter (boolean): include treesitter based highlight groups (defaults to true)
---               - extmarks (boolean|"all"): include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true)
---               - semantic_tokens (boolean): include semantic tokens (defaults to true)
---@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":
---               - treesitter: a list of treesitter captures
---               - syntax: a list of syntax groups
---               - semantic_tokens: a list of semantic tokens
---               - extmarks: a list of extmarks
---               - buffer: the buffer used to get the items
---               - row: the row used to get the items
---               - col: the col used to get the items
function vim.inspect_pos(bufnr, row, col, filter)
  filter = vim.tbl_deep_extend('force', defaults, filter or {})

  bufnr = bufnr or 0
  if row == nil or col == nil then
    -- get the row/col from the first window displaying the buffer
    local win = bufnr == 0 and vim.api.nvim_get_current_win() or vim.fn.bufwinid(bufnr)
    if win == -1 then
      error('row/col is required for buffers not visible in a window')
    end
    local cursor = vim.api.nvim_win_get_cursor(win)
    row, col = cursor[1] - 1, cursor[2]
  end
  bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr

  local results = {
    treesitter = {},
    syntax = {},
    extmarks = {},
    semantic_tokens = {},
    buffer = bufnr,
    row = row,
    col = col,
  }

  -- resolve hl links
  ---@private
  local function resolve_hl(data)
    if data.hl_group then
      local hlid = vim.api.nvim_get_hl_id_by_name(data.hl_group)
      local name = vim.fn.synIDattr(vim.fn.synIDtrans(hlid), 'name')
      data.hl_group_link = name
    end
    return data
  end

  -- treesitter
  if filter.treesitter then
    for _, capture in pairs(vim.treesitter.get_captures_at_pos(bufnr, row, col)) do
      capture.hl_group = '@' .. capture.capture .. '.' .. capture.lang
      table.insert(results.treesitter, resolve_hl(capture))
    end
  end

  -- syntax
  if filter.syntax then
    for _, i1 in ipairs(vim.fn.synstack(row + 1, col + 1)) do
      table.insert(results.syntax, resolve_hl({ hl_group = vim.fn.synIDattr(i1, 'name') }))
    end
  end

  --- Convert an extmark tuple into a map-like table
  --- @private
  local function to_map(extmark)
    extmark = {
      id = extmark[1],
      row = extmark[2],
      col = extmark[3],
      opts = resolve_hl(extmark[4]),
    }
    extmark.end_row = extmark.opts.end_row or extmark.row -- inclusive
    extmark.end_col = extmark.opts.end_col or (extmark.col + 1) -- exclusive
    return extmark
  end

  --- Check if an extmark overlaps this position
  --- @private
  local function is_here(extmark)
    return (row >= extmark.row and row <= extmark.end_row) -- within the rows of the extmark
      and (row > extmark.row or col >= extmark.col) -- either not the first row, or in range of the col
      and (row < extmark.end_row or col < extmark.end_col) -- either not in the last row or in range of the col
  end

  -- all extmarks at this position
  local extmarks = {}
  for ns, nsid in pairs(vim.api.nvim_get_namespaces()) do
    local ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, nsid, 0, -1, { details = true })
    ns_marks = vim.tbl_map(to_map, ns_marks)
    ns_marks = vim.tbl_filter(is_here, ns_marks)
    for _, mark in ipairs(ns_marks) do
      mark.ns_id = nsid
      mark.ns = ns
    end
    vim.list_extend(extmarks, ns_marks)
  end

  if filter.semantic_tokens then
    results.semantic_tokens = vim.tbl_filter(function(extmark)
      return extmark.ns:find('vim_lsp_semantic_tokens') == 1
    end, extmarks)
  end

  if filter.extmarks then
    results.extmarks = vim.tbl_filter(function(extmark)
      return extmark.ns:find('vim_lsp_semantic_tokens') ~= 1
        and (filter.extmarks == 'all' or extmark.opts.hl_group)
    end, extmarks)
  end

  return results
end

---Show all the items at a given buffer position.
---
---Can also be shown with `:Inspect`. *:Inspect*
---
---@param bufnr? integer defaults to the current buffer
---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor
---@param filter? InspectorFilter (table|nil) see |vim.inspect_pos()|
function vim.show_pos(bufnr, row, col, filter)
  local items = vim.inspect_pos(bufnr, row, col, filter)

  local lines = { {} }

  ---@private
  local function append(str, hl)
    table.insert(lines[#lines], { str, hl })
  end

  ---@private
  local function nl()
    table.insert(lines, {})
  end

  ---@private
  local function item(data, comment)
    append('  - ')
    append(data.hl_group, data.hl_group)
    append(' ')
    if data.hl_group ~= data.hl_group_link then
      append('links to ', 'MoreMsg')
      append(data.hl_group_link, data.hl_group_link)
      append(' ')
    end
    if comment then
      append(comment, 'Comment')
    end
    nl()
  end

  -- treesitter
  if #items.treesitter > 0 then
    append('Treesitter', 'Title')
    nl()
    for _, capture in ipairs(items.treesitter) do
      item(capture, capture.lang)
    end
    nl()
  end

  -- semantic tokens
  if #items.semantic_tokens > 0 then
    append('Semantic Tokens', 'Title')
    nl()
    local sorted_marks = vim.fn.sort(items.semantic_tokens, function(left, right)
      local left_first = left.opts.priority < right.opts.priority
        or left.opts.priority == right.opts.priority and left.opts.hl_group < right.opts.hl_group
      return left_first and -1 or 1
    end)
    for _, extmark in ipairs(sorted_marks) do
      item(extmark.opts, 'priority: ' .. extmark.opts.priority)
    end
    nl()
  end

  -- syntax
  if #items.syntax > 0 then
    append('Syntax', 'Title')
    nl()
    for _, syn in ipairs(items.syntax) do
      item(syn)
    end
    nl()
  end

  -- extmarks
  if #items.extmarks > 0 then
    append('Extmarks', 'Title')
    nl()
    for _, extmark in ipairs(items.extmarks) do
      if extmark.opts.hl_group then
        item(extmark.opts, extmark.ns)
      else
        append('  - ')
        append(extmark.ns, 'Comment')
        nl()
      end
    end
    nl()
  end

  if #lines[#lines] == 0 then
    table.remove(lines)
  end

  local chunks = {}
  for _, line in ipairs(lines) do
    vim.list_extend(chunks, line)
    table.insert(chunks, { '\n' })
  end
  if #chunks == 0 then
    chunks = {
      {
        'No items found at position '
          .. items.row
          .. ','
          .. items.col
          .. ' in buffer '
          .. items.buffer,
      },
    }
  end
  vim.api.nvim_echo(chunks, false, {})
end