summaryrefslogtreecommitdiff
path: root/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua
blob: f5bbb87faa7b61ca9072d45f550f43512747de16 (plain)
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
local util = require("quicker.util")

local M = {}

---@class (exact) quicker.QFContext
---@field num_before integer
---@field num_after integer

---@class (exact) quicker.ExpandOpts
---@field before? integer Number of lines of context to show before the line (default 2)
---@field after? integer Number of lines of context to show after the line (default 2)
---@field add_to_existing? boolean
---@field loclist_win? integer

---@param item QuickFixItem
---@param new_text string
local function update_item_text_keep_diagnostics(item, new_text)
  -- If this is an "error" item, replace the text with the source line and store that text
  -- in the user data so we can add it as virtual text later
  if item.type ~= "" and not vim.endswith(new_text, item.text) then
    local user_data = util.get_user_data(item)
    if not user_data.error_text then
      user_data.error_text = item.text
      item.user_data = user_data
    end
  end
  item.text = new_text
end

---@param opts? quicker.ExpandOpts
function M.expand(opts)
  opts = opts or {}
  if not opts.loclist_win and util.get_win_type(0) == "l" then
    opts.loclist_win = vim.api.nvim_get_current_win()
  end
  local qf_list
  if opts.loclist_win then
    qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 })
  else
    qf_list = vim.fn.getqflist({ all = 0 })
  end
  local winid = qf_list.winid
  if not winid then
    vim.notify("Cannot find quickfix window", vim.log.levels.ERROR)
    return
  end
  local ctx = qf_list.context or {}
  if type(ctx) ~= "table" then
    -- If the quickfix had a non-table context, we're going to have to overwrite it
    ctx = {}
  end
  ---@type quicker.QFContext
  local quicker_ctx = ctx.quicker
  if not quicker_ctx then
    quicker_ctx = { num_before = 0, num_after = 0 }
    ctx.quicker = quicker_ctx
  end
  local curpos = vim.api.nvim_win_get_cursor(winid)[1]
  local cur_item = qf_list.items[curpos]
  local newpos

  -- calculate the number of lines to show before and after the current line
  local num_before = opts.before or 2
  if opts.add_to_existing then
    num_before = num_before + quicker_ctx.num_before
  end
  num_before = math.max(0, num_before)
  quicker_ctx.num_before = num_before
  local num_after = opts.after or 2
  if opts.add_to_existing then
    num_after = num_after + quicker_ctx.num_after
  end
  num_after = math.max(0, num_after)
  quicker_ctx.num_after = num_after

  local items = {}
  ---@type nil|QuickFixItem
  local prev_item
  ---@param i integer
  ---@return nil|QuickFixItem
  local function get_next_item(i)
    local item = qf_list.items[i]
    for j = i + 1, #qf_list.items do
      local next_item = qf_list.items[j]
      -- Next valid item that is on a different line (since we dedupe same-line items)
      if
        next_item.valid == 1 and (item.bufnr ~= next_item.bufnr or item.lnum ~= next_item.lnum)
      then
        return next_item
      end
    end
  end

  for i, item in ipairs(qf_list.items) do
    (function()
      ---@cast item QuickFixItem
      if item.valid == 0 or item.bufnr == 0 then
        return
      end

      if not vim.api.nvim_buf_is_loaded(item.bufnr) then
        vim.fn.bufload(item.bufnr)
      end

      local overlaps_previous = false
      local header_type = "hard"
      local low = math.max(0, item.lnum - 1 - num_before)
      if prev_item then
        if prev_item.bufnr == item.bufnr then
          -- If this is the second match on the same line, skip this item
          if prev_item.lnum == item.lnum then
            return
          end
          header_type = "soft"
          if prev_item.lnum + num_after >= low then
            low = math.min(item.lnum - 1, prev_item.lnum + num_after)
            overlaps_previous = true
          end
        end
      end

      local high = item.lnum + num_after
      local next_item = get_next_item(i)
      if next_item then
        if next_item.bufnr == item.bufnr and next_item.lnum <= high then
          high = next_item.lnum - 1
        end
      end

      local item_start_idx = #items
      local lines = vim.api.nvim_buf_get_lines(item.bufnr, low, high, false)
      for j, line in ipairs(lines) do
        if j + low == item.lnum then
          update_item_text_keep_diagnostics(item, line)
          table.insert(items, item)
        else
          table.insert(items, {
            bufnr = item.bufnr,
            lnum = low + j,
            text = line,
            valid = 0,
            user_data = { lnum = low + j },
          })
        end
        if cur_item.bufnr == item.bufnr and cur_item.lnum == low + j then
          newpos = #items
        end
      end

      -- Add the header to the first item in this sequence, if one is needed
      if prev_item and not overlaps_previous then
        local first_item = items[item_start_idx + 1]
        if first_item then
          first_item.user_data = first_item.user_data or {}
          first_item.user_data.header = header_type
        end
      end

      prev_item = item
    end)()

    if i == curpos and not newpos then
      newpos = #items
    end
  end

  if opts.loclist_win then
    vim.fn.setloclist(
      opts.loclist_win,
      {},
      "r",
      { items = items, title = qf_list.title, context = ctx }
    )
  else
    vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = ctx })
  end

  pcall(vim.api.nvim_win_set_cursor, qf_list.winid, { newpos, 0 })
end

---@class (exact) quicker.CollapseArgs
---@field loclist_win? integer
---
function M.collapse(opts)
  opts = opts or {}
  if not opts.loclist_win and util.get_win_type(0) == "l" then
    opts.loclist_win = vim.api.nvim_get_current_win()
  end
  local curpos = vim.api.nvim_win_get_cursor(0)[1]
  local qf_list
  if opts.loclist_win then
    qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 })
  else
    qf_list = vim.fn.getqflist({ all = 0 })
  end
  local items = {}
  local last_item
  for i, item in ipairs(qf_list.items) do
    if item.valid == 1 then
      if item.user_data then
        -- Clear the header, if present
        item.user_data.header = nil
      end
      table.insert(items, item)
      if i <= curpos then
        last_item = #items
      end
    end
  end

  vim.tbl_filter(function(item)
    return item.valid == 1
  end, qf_list.items)

  local ctx = qf_list.context or {}
  if type(ctx) == "table" then
    local quicker_ctx = ctx.quicker
    if quicker_ctx then
      quicker_ctx = { num_before = 0, num_after = 0 }
      ctx.quicker = quicker_ctx
    end
  end

  if opts.loclist_win then
    vim.fn.setloclist(
      opts.loclist_win,
      {},
      "r",
      { items = items, title = qf_list.title, context = qf_list.context }
    )
  else
    vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context })
  end
  if qf_list.winid then
    if last_item then
      vim.api.nvim_win_set_cursor(qf_list.winid, { last_item, 0 })
    end
  end
end

---@param opts? quicker.ExpandOpts
function M.toggle(opts)
  opts = opts or {}
  local ctx
  if opts.loclist_win then
    ctx = vim.fn.getloclist(opts.loclist_win, { context = 0 }).context
  else
    ctx = vim.fn.getqflist({ context = 0 }).context
  end

  if
    type(ctx) == "table"
    and ctx.quicker
    and (ctx.quicker.num_before > 0 or ctx.quicker.num_after > 0)
  then
    M.collapse()
  else
    M.expand(opts)
  end
end

---@class (exact) quicker.RefreshOpts
---@field keep_diagnostics? boolean If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source.

---@param loclist_win? integer
---@param opts? quicker.RefreshOpts
function M.refresh(loclist_win, opts)
  opts = vim.tbl_extend("keep", opts or {}, { keep_diagnostics = true })
  if not loclist_win then
    local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 })
    if ok and qf.filewinid and qf.filewinid ~= 0 then
      loclist_win = qf.filewinid
    end
  end

  local qf_list
  if loclist_win then
    qf_list = vim.fn.getloclist(loclist_win, { all = 0 })
  else
    qf_list = vim.fn.getqflist({ all = 0 })
  end

  local items = {}
  for _, item in ipairs(qf_list.items) do
    if item.bufnr ~= 0 and item.lnum ~= 0 then
      if not vim.api.nvim_buf_is_loaded(item.bufnr) then
        vim.fn.bufload(item.bufnr)
      end
      local line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1]
      if line then
        if opts.keep_diagnostics then
          update_item_text_keep_diagnostics(item, line)
        else
          item.text = line
        end
        table.insert(items, item)
      end
    else
      table.insert(items, item)
    end
  end

  if loclist_win then
    vim.fn.setloclist(
      loclist_win,
      {},
      "r",
      { items = items, title = qf_list.title, context = qf_list.context }
    )
  else
    vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context })
  end
end

return M