-
-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathbuffers.lua
More file actions
531 lines (458 loc) · 13.7 KB
/
Copy pathbuffers.lua
File metadata and controls
531 lines (458 loc) · 13.7 KB
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
local has_devicons, rq_devicons = pcall(require, "nvim-web-devicons")
local concat = table.concat
local sort = table.sort
local bo = vim.bo
local cmd = vim.cmd
local diagnostic = vim.diagnostic or vim.lsp.diagnostic
local fn = vim.fn
local split = vim.split
local util = require("cokeline.utils")
local it = require("plenary.iterators")
local iter = it.iter
local enumerate = util.enumerate
---@type bufnr
local current_valid_index
local valid_pick_letters = false
local first_valid = 1
local taken_pick_letters = {}
local taken_pick_indices = {}
local M = {}
---@diagnostic disable: duplicate-doc-class
---@alias valid_index number
---@alias index number
---@alias bufnr number
---@class Buffer
---@field _valid_index valid_index
---@field index index
---@field number bufnr
---@field type string
---@field is_focused boolean
---@field is_modified boolean
---@field is_readonly boolean
---@field path string
---@field unique_prefix string
---@field filename string
---@field filetype string
---@field pick_letter string
---@field devicon table<string, string>
---@field diagnostics table<string, number>
local Buffer = {}
Buffer.__index = Buffer
---@param buffers Iter<Buffer>
---@return Iter<Buffer>
local compute_unique_prefixes = function(buffers)
local is_windows = fn.has("win32") == 1
local path_separator = not is_windows and "/" or "\\"
local prefixes = {}
local paths = {}
buffers = buffers
:map(function(buffer)
prefixes[#prefixes + 1] = {}
paths[#paths + 1] = fn.reverse(
split(
is_windows and buffer.path:gsub("/", "\\") or buffer.path,
path_separator
)
)
return buffer
end)
:tolist()
for i = 1, #paths do
for j = i + 1, #paths do
local k = 1
while paths[i][k] == paths[j][k] and paths[i][k] do
k = k + 1
prefixes[i][k - 1] = prefixes[i][k - 1] or paths[i][k]
prefixes[j][k - 1] = prefixes[j][k - 1] or paths[j][k]
end
if k ~= 1 then
prefixes[i][k - 1] = prefixes[i][k - 1] or paths[i][k]
prefixes[j][k - 1] = prefixes[j][k - 1] or paths[j][k]
end
end
end
return enumerate(iter(buffers)):map(function(i, buffer)
buffer.unique_prefix = concat({
#prefixes[i] == #paths[i] and path_separator or "",
fn.join(fn.reverse(prefixes[i]), path_separator),
#prefixes[i] > 0 and path_separator or "",
})
return i, buffer
end)
end
---@param filename string
---@param bufnr bufnr
---@return string
local get_pick_letter = function(filename, bufnr)
-- Initialize the valid letters string, if not already initialized
if not valid_pick_letters then
valid_pick_letters = _G.cokeline.config.pick.letters
end
-- If the bufnr has already a letter associated to it return that.
if taken_pick_letters[bufnr] then
return taken_pick_letters[bufnr]
end
-- If the config option pick.use_filename is true, and the initial letter
-- of the filename is valid and it hasn't already been assigned return that.
if _G.cokeline.config.pick.use_filename and filename ~= "" then
local init_letter = vim.fn.strcharpart(filename, 0, 1)
local idx = valid_pick_letters:find(init_letter, nil, true)
if idx == nil or taken_pick_indices[idx] then
while
taken_pick_indices[first_valid]
and first_valid <= vim.fn.strcharlen(valid_pick_letters)
do
first_valid = first_valid + 1
end
idx = first_valid
end
if idx then
local letter = vim.fn.strcharpart(valid_pick_letters, idx - 1, 1)
if letter and letter ~= "" then
taken_pick_letters[bufnr] = letter
taken_pick_indices[idx] = true
return letter
end
end
end
-- Return the first valid letter if there is one.
while
taken_pick_indices[first_valid]
and first_valid <= vim.fn.strcharlen(valid_pick_letters)
do
first_valid = first_valid + 1
end
if first_valid <= vim.fn.strcharlen(valid_pick_letters) then
local letter = vim.fn.strcharpart(valid_pick_letters, first_valid - 1, 1)
taken_pick_letters[bufnr] = letter
taken_pick_indices[first_valid] = true
first_valid = first_valid + 1
return letter
end
-- Finally, just return a '?' (this is rarely reached, you'd need to have
-- opened 54 buffers in the same session).
return "?"
end
---@param path string
---@param filename string
---@param buftype string
---@param filetype string
---@return table<string, string>
local get_devicon = function(path, filename, buftype, filetype)
local name = (buftype == "terminal") and "terminal" or filename
local extn = fn.fnamemodify(path, ":e")
local icon, color =
rq_devicons.get_icon_color(name, extn, { default = false })
if not icon then
icon, color =
rq_devicons.get_icon_color_by_filetype(filetype, { default = true })
end
return {
icon = icon .. " ",
color = color,
}
end
---@param bufnr bufnr
---@return table<string, number>
local get_diagnostics = function(bufnr)
local diagnostics = {
errors = 0,
warnings = 0,
infos = 0,
hints = 0,
}
for _, d in ipairs(diagnostic.get(bufnr)) do
if d.severity == 1 then
diagnostics.errors = diagnostics.errors + 1
elseif d.severity == 2 then
diagnostics.warnings = diagnostics.warnings + 1
elseif d.severity == 3 then
diagnostics.infos = diagnostics.infos + 1
elseif d.severity == 4 then
diagnostics.hints = diagnostics.hints + 1
end
end
return diagnostics
end
---@param b table
---@return Buffer
Buffer.new = function(b)
local opts = bo[b.bufnr]
local number = b.bufnr
local buftype = opts.buftype
local path = b.name
local filename = (type == "quickfix" and "quickfix")
or (#path > 0 and fn.fnamemodify(path, ":t"))
local filetype = not (b.variables and b.variables.netrw_browser_active)
and opts.filetype
or "netrw"
local pick_letter = get_pick_letter(filename or "", number)
local devicon = has_devicons
and get_devicon(path, filename, buftype, filetype)
or { icon = "", color = "" }
return setmetatable({
_valid_index = _G.cokeline.buf_order[b.bufnr] or -1,
index = -1,
number = b.bufnr,
type = opts.buftype,
is_focused = (b.bufnr == fn.bufnr("%")),
is_first = false,
is_last = false,
is_modified = opts.modified,
is_readonly = opts.readonly,
is_hovered = false,
path = b.name,
unique_prefix = "",
filename = filename or "[No Name]",
filetype = filetype,
pick_letter = pick_letter,
devicon = devicon,
diagnostics = get_diagnostics(number),
}, Buffer)
end
---@param self Buffer
---Deletes the buffer
function Buffer:delete()
util.buf_delete(self.number)
vim.cmd.redrawtabline()
end
---@param self Buffer
---Focuses the buffer
function Buffer:focus()
vim.api.nvim_set_current_buf(self.number)
end
---@param self Buffer
---@return number
---Returns the number of lines in the buffer
function Buffer:lines()
return vim.api.nvim_buf_line_count(self.number)
end
---@param self Buffer
---@return string[]
---Returns the buffer's lines
function Buffer:text()
return vim.api.nvim_buf_get_lines(self.number, 0, -1, false)
end
---@param buf Buffer
---@return boolean
---Returns true if the buffer is valid
function Buffer:is_valid()
return vim.api.nvim_buf_is_valid(self.number)
end
---@param buffer Buffer
---@return boolean
local is_old = function(buffer)
for _, buf in pairs(_G.cokeline.valid_buffers) do
if buffer.number == buf.number then
return true
end
end
return false
end
---@param buffer Buffer
---@return boolean
local is_new = function(buffer)
return not is_old(buffer)
end
---Sorter used to open new buffers at the end of the bufferline.
---@param buffer1 Buffer
---@param buffer2 Buffer
---@return boolean
local sort_by_new_after_last = function(buffer1, buffer2)
if is_old(buffer1) and is_old(buffer2) then
return buffer1._valid_index < buffer2._valid_index
elseif is_old(buffer1) and is_new(buffer2) then
return true
elseif is_new(buffer1) and is_old(buffer2) then
return false
else
return buffer1.number < buffer2.number
end
end
---Sorter used to open new buffers next to the current buffer.
---@param buffer1 Buffer
---@param buffer2 Buffer
---@return boolean
local sort_by_new_after_current = function(buffer1, buffer2)
if is_old(buffer1) and is_old(buffer2) then
-- If both buffers are either before or after (inclusive) the current
-- buffer, respect the current order.
if
(buffer1._valid_index - current_valid_index)
* (buffer2._valid_index - current_valid_index)
>= 0
then
return buffer1._valid_index < buffer2._valid_index
end
return buffer1._valid_index < current_valid_index
elseif is_old(buffer1) and is_new(buffer2) then
return buffer1._valid_index <= current_valid_index
elseif is_new(buffer1) and is_old(buffer2) then
return current_valid_index < buffer2._valid_index
else
return buffer1.number < buffer2.number
end
end
---Sorter used to order buffers by full path.
---@param buffer1 Buffer
---@param buffer2 Buffer
---@return boolean
local sort_by_directory = function(buffer1, buffer2)
return buffer1.path < buffer2.path
end
---Sorted used to order buffers by number, as in the default tabline.
---@param buffer1 Buffer
---@param buffer2 Buffer
---@return boolean
local sort_by_number = function(buffer1, buffer2)
return buffer1.number < buffer2.number
end
---@param bufnr bufnr
---@return nil
function M.release_taken_letter(bufnr)
if taken_pick_letters[bufnr] then
local idx = valid_pick_letters:find(taken_pick_letters[bufnr])
taken_pick_indices[idx] = nil
taken_pick_letters[bufnr] = nil
end
end
---@param buffer Buffer
---@param target_valid_index valid_index
function M.move_buffer(buffer, target_valid_index)
if buffer._valid_index == target_valid_index then
return
end
_G.cokeline.buf_order[buffer.number] = target_valid_index
if buffer._valid_index < target_valid_index then
for index = (buffer._valid_index + 1), target_valid_index do
_G.cokeline.buf_order[_G.cokeline.valid_buffers[index].number] = index
- 1
end
else
for index = target_valid_index, (buffer._valid_index - 1) do
_G.cokeline.buf_order[_G.cokeline.valid_buffers[index].number] = index
+ 1
end
end
cmd("redrawtabline")
end
---@return Buffer[]
function M.get_valid_buffers()
if not _G.cokeline.buf_order then
_G.cokeline.buf_order = {}
end
local info = fn.getbufinfo({ buflisted = 1 })
---@type Iter<Buffer>
local buffers = iter(info):map(Buffer.new):filter(function(buffer)
return buffer.filetype ~= "netrw"
end)
if _G.cokeline.config.buffers.filter_valid then
---@type Iter<Buffer>
buffers = buffers:filter(_G.cokeline.config.buffers.filter_valid)
end
---@type Buffer[]
buffers = compute_unique_prefixes(buffers)
if current_valid_index == nil then
_G.cokeline.buf_order = {}
buffers = buffers
:map(function(i, buffer)
buffer._valid_index = i
_G.cokeline.buf_order[buffer.number] = buffer._valid_index
if buffer.is_focused then
current_valid_index = i
end
return buffer
end)
:tolist()
else
buffers = buffers
:map(function(_, buf)
return buf
end)
:tolist()
end
if _G.cokeline.config.buffers.new_buffers_position == "last" then
sort(buffers, sort_by_new_after_last)
elseif _G.cokeline.config.buffers.new_buffers_position == "next" then
sort(buffers, sort_by_new_after_current)
elseif _G.cokeline.config.buffers.new_buffers_position == "directory" then
sort(buffers, sort_by_directory)
elseif _G.cokeline.config.buffers.new_buffers_position == "number" then
sort(buffers, sort_by_number)
end
_G.cokeline.buf_order = {}
for i, buffer in ipairs(buffers) do
buffer._valid_index = i
_G.cokeline.buf_order[buffer.number] = buffer._valid_index
if buffer.is_focused then
current_valid_index = i
end
end
return buffers
end
---@param unsorted boolean
---@return Buffer[]
function M.get_visible()
_G.cokeline.valid_buffers = M.get_valid_buffers()
_G.cokeline.valid_lookup = {}
local bufs = iter(_G.cokeline.valid_buffers):map(function(buffer)
_G.cokeline.valid_lookup[buffer.number] = buffer
return buffer
end)
if _G.cokeline.config.buffers.filter_visible then
bufs = bufs:filter(_G.cokeline.config.buffers.filter_visible)
end
bufs = enumerate(bufs):map(function(i, buf)
buf.index = i
return buf
end)
if not _G.cokeline.config.pick.use_filename then
bufs = bufs:map(function(buf)
buf.pick_letter = get_pick_letter(buf.filename, buf.number)
return buf
end)
end
_G.cokeline.visible_buffers = bufs:tolist()
if #_G.cokeline.visible_buffers > 0 then
_G.cokeline.visible_buffers[1].is_first = true
_G.cokeline.visible_buffers[#_G.cokeline.visible_buffers].is_last = true
end
return _G.cokeline.visible_buffers
end
---Get Buffer
---@param bufnr number
---@return Buffer|nil
function M.get_buffer(bufnr)
return _G.cokeline.valid_lookup[bufnr]
end
---Wrapper around `vim.api.nvim_get_current_buf`, returns Buffer object
---@return Buffer|nil
function M.get_current()
local bufnr = vim.api.nvim_get_current_buf()
if bufnr then
return M.get_buffer(bufnr)
end
end
---@param bufnr bufnr
---@return boolean
function M.is_visible(bufnr)
local buf = M.get_buffer(bufnr)
if not buf then
return false
end
if
_G.cokeline.config.buf.filter_valid
and not _G.cokeline.config.buffers.filter_valid(buf)
then
return false
end
if
_G.cokeline.config.buffers.filter_visible
and not _G.cokeline.config.buffers.filter_visible(buf)
then
return false
end
return true
end
M.Buffer = Buffer
return M