-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathinit.lua
More file actions
364 lines (338 loc) · 11.1 KB
/
Copy pathinit.lua
File metadata and controls
364 lines (338 loc) · 11.1 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
local api, UP, DOWN, INVALID = vim.api, -1, 1, -1
local buf_set_extmark, set_provider = api.nvim_buf_set_extmark, api.nvim_set_decoration_provider
local ns = api.nvim_create_namespace('IndentLine')
local ffi, treesitter = require('ffi'), vim.treesitter
local opt = {
only_current = false,
exclude = { 'dashboard', 'lazy', 'help', 'nofile', 'terminal', 'prompt', 'qf' },
exclude_nodetype = {},
config = {
virt_text_pos = 'overlay',
hl_mode = 'combine',
ephemeral = true,
},
}
local enabled = true
ffi.cdef([[
typedef struct {} Error;
typedef int colnr_T;
typedef struct window_S win_T;
typedef struct file_buffer buf_T;
buf_T *find_buffer_by_handle(int buffer, Error *err);
int get_sw_value(buf_T *buf);
typedef int32_t linenr_T;
int get_indent_lnum(linenr_T lnum);
char *ml_get(linenr_T lnum);
]])
local C = ffi.C
local ml_get = C.ml_get
local find_buffer_by_handle = C.find_buffer_by_handle
local get_sw_value, get_indent_lnum = C.get_sw_value, C.get_indent_lnum
--- @class Context
--- @field snapshot table<integer, integer>
local context = { snapshot = {} }
--- check text only has space or tab see bench/space_or_tab.lua
--- @param text string
--- @return boolean true only have space or tab
local function only_spaces_or_tabs(text)
for i = 1, #text do
local byte = string.byte(text, i)
if byte ~= 32 and byte ~= 9 then -- 32 is space, 9 is tab
return false
end
end
return true
end
--- @param bufnr integer
--- @return integer the shiftwidth value of bufnr
local function get_shiftw_value(bufnr)
local handle = find_buffer_by_handle(bufnr, ffi.new('Error'))
return get_sw_value(handle)
end
-- Bit operations for snapshot packing/unpacking
-- empty(1) | indent(6) | indent_cols(9)
local function pack_snapshot(empty, indent, indent_cols)
return bit.bor(
bit.lshift(empty and 1 or 0, 15),
bit.lshift(bit.band(indent, 0x3F), 9),
bit.band(indent_cols, 0x1FF)
)
end
---@param packed integer
---@return table
local function unpack_snapshot(packed)
return {
is_empty = bit.band(bit.rshift(packed, 15), 1) == 1,
indent = bit.band(bit.rshift(packed, 9), 0x3F),
indent_cols = bit.band(packed, 0x1FF),
}
end
--- store the line data in snapshot and update the blank line indent
--- @param lnum integer
--- @return table
local function make_snapshot(lnum)
local line_text = ffi.string(ml_get(lnum))
local is_empty = #line_text == 0 or only_spaces_or_tabs(line_text)
if is_empty and context.has_ts then
local node = treesitter.get_node({ pos = { lnum - 1, 0 } })
if node then
context.tree_root = context.tree_root or node:tree():root():type()
local indent = 0
if context.tree_root and node:type() ~= context.tree_root then
local parent = node:parent()
if parent then
local p_srow = parent:range()
-- usually top of file does not have indent so skiped
if p_srow > 0 then
indent = get_indent_lnum(p_srow + 1)
indent = indent + context.step
end
end
end
local packed = pack_snapshot(true, indent, indent)
context.snapshot[lnum] = packed
return unpack_snapshot(packed)
end
end
local indent = is_empty and 0 or get_indent_lnum(lnum)
-- adjust indent
if not is_empty and not context.mixup and (indent % context.step) ~= 0 then
local col = api.nvim_win_get_cursor(0)[2]
if col > 0 and not api.nvim_get_current_line():sub(1, col):find('%w') then
indent = indent
local n = math.floor((indent - context.step) / context.step)
indent = context.step + n * context.step
end
end
if is_empty then
local prev_lnum = lnum - 1
while prev_lnum >= 1 do
local prev_packed = context.snapshot[prev_lnum]
local sp = prev_packed and unpack_snapshot(prev_packed) or make_snapshot(prev_lnum)
if (not sp.is_empty and sp.indent == 0) or (sp.indent > 0) then
if sp.indent > 0 then
indent = sp.indent
end
break
end
prev_lnum = prev_lnum - 1
end
end
local prev_packed = context.snapshot[lnum - 1]
if prev_packed then
local prev = unpack_snapshot(prev_packed)
if prev.is_empty and prev.indent < indent then
local prev_lnum = lnum - 1
while prev_lnum >= 1 do
local sp_packed = context.snapshot[prev_lnum]
if not sp_packed then
break
end
local sp = unpack_snapshot(sp_packed)
if not sp.is_empty or sp.indent >= indent then
break
end
context.snapshot[prev_lnum] = pack_snapshot(sp.is_empty, indent, indent)
prev_lnum = prev_lnum - 1
end
end
end
local indent_cols = line_text:find('[^ \t]')
indent_cols = indent_cols and indent_cols - 1 or INVALID
if is_empty then
indent_cols = indent
end
local packed = pack_snapshot(is_empty, indent, indent_cols)
context.snapshot[lnum] = packed
return unpack_snapshot(packed)
end
local function find_in_snapshot(lnum)
local packed = context.snapshot[lnum]
if not packed then
return make_snapshot(lnum)
end
return unpack_snapshot(packed)
end
--- @param row integer
--- @param direction integer UP or DOWN
--- @return integer
--- @return integer
local function range_in_snapshot(row, direction, fn)
while row >= 0 and row < context.count do
local sp = find_in_snapshot(row + 1)
if fn(sp.indent, sp.is_empty, row) then
return sp.indent, row
end
row = row + direction
end
return INVALID, INVALID
end
local function out_current_range(row)
return opt.only_current
and context.range_srow
and context.range_erow
and (row < context.range_srow or row > context.range_erow)
end
local function find_current_range(currow_indent, currow)
local curlevel = math.ceil(currow_indent / context.tabstop) -- for mixup
local range_fn = function(indent, empty, row)
local level = math.ceil(indent / context.tabstop)
if
((not empty and not context.mixup) and indent < currow_indent)
or (context.mixup and level < curlevel)
then
if row < context.currow then
context.range_srow = row
else
context.range_erow = row
end
return true
end
end
range_in_snapshot(context.currow - 1, UP, range_fn)
range_in_snapshot(context.currow + 1, DOWN, range_fn)
if context.range_srow and not context.range_erow then
context.range_erow = context.count - 1
end
context.cur_inlevel = context.mixup and math.ceil(currow_indent / context.tabstop)
or math.floor(currow_indent / context.step)
end
local function on_line(_, _, bufnr, row)
local sp = find_in_snapshot(row + 1)
if sp.indent == 0 or out_current_range(row) then
return
end
local currow_insert = api.nvim_get_mode().mode == 'i' and context.currow == row
-- mixup like vim code has modeline vi:set ts=8 sts=4 sw=4 noet:
-- 4 8 12 16 20 24
-- 1 1 2 2 3 3
local total = context.mixup and math.ceil(sp.indent / context.tabstop) or sp.indent - 1
local step = context.mixup and 1 or context.step
for i = 1, total, step do
local col = i - 1
local level = context.mixup and i or math.floor(col / context.step) + 1
if context.is_tab and not context.mixup then
col = level - 1
end
if context.has_ts then
local node =
treesitter.get_node({ bufnr = bufnr, pos = { row, col }, ignore_injections = true })
while node do
if vim.tbl_contains(opt.exclude_nodetype, node:type()) then
goto continue
end
node = node:parent()
end
end
if
col >= context.leftcol
and level >= opt.minlevel
and (not opt.only_current or level == context.cur_inlevel)
and col < sp.indent_cols
and (not currow_insert or col ~= context.curcol)
then
local row_in_curblock = context.range_srow
and (row > context.range_srow and row <= context.range_erow)
local higroup = row_in_curblock and level == context.cur_inlevel and 'IndentLineCurrent'
or 'IndentLine'
opt.config.virt_text[1][2] = higroup
if sp.is_empty and col > 0 then
opt.config.virt_text_win_col = not context.mixup and i - 1 - context.leftcol
or (i - 1) * context.tabstop
end
buf_set_extmark(bufnr, ns, row, col, opt.config)
opt.config.virt_text_win_col = nil
end
::continue::
end
end
local function on_win(_, winid, bufnr, toprow, botrow)
if not enabled then
return false
end
if
bufnr ~= api.nvim_get_current_buf()
or vim.iter(opt.exclude):find(function(v)
return v == vim.bo[bufnr].ft or v == vim.bo[bufnr].buftype
end)
then
return false
end
opt.config.virt_text_repeat_linebreak = vim.wo[winid].wrap and vim.wo[winid].breakindent
---@diagnostic disable-next-line: missing-fields
context = { snapshot = {} }
context.is_tab = not vim.bo[bufnr].expandtab
context.step = get_shiftw_value(bufnr)
context.tabstop = vim.bo[bufnr].tabstop
context.softtabstop = vim.bo[bufnr].softtabstop
context.win_width = api.nvim_win_get_width(winid)
context.mixup = context.is_tab and context.tabstop > context.softtabstop
context.has_ts = pcall(treesitter.get_parser, bufnr)
for i = toprow, botrow do
make_snapshot(i + 1)
end
api.nvim_win_set_hl_ns(winid, ns)
context.leftcol = vim.fn.winsaveview().leftcol
context.count = api.nvim_buf_line_count(bufnr)
local pos = api.nvim_win_get_cursor(winid)
context.currow = pos[1] - 1
context.curcol = pos[2]
local cur_indent = find_in_snapshot(context.currow + 1).indent
local next_indent = (context.currow + 1 < context.count)
and find_in_snapshot(context.currow + 2).indent
or 0
-- We only want to look backwards if we are closing a block
local line_text = api.nvim_get_current_line()
local is_closer = line_text:find('^%s*[})%]]') or line_text:find('^%s*end')
local target_indent = cur_indent
if next_indent > cur_indent then
target_indent = next_indent
elseif is_closer then
local prev_indent = context.currow > 0 and find_in_snapshot(context.currow).indent or 0
if prev_indent > cur_indent then
target_indent = prev_indent
end
end
find_current_range(target_indent, context.currow)
end
local M = {}
function M.toggle()
enabled = not enabled
vim.api.nvim__redraw({
buf = vim.api.nvim_get_current_buf(),
valid = false,
})
end
function M.enable()
enabled = true
vim.api.nvim__redraw({
buf = vim.api.nvim_get_current_buf(),
valid = false,
})
end
function M.disable()
enabled = false
vim.api.nvim__redraw({
buf = vim.api.nvim_get_current_buf(),
valid = false,
})
end
--- @param conf table|nil IndentMini config
function M.setup(conf)
conf = conf or {}
enabled = conf.enabled ~= false
opt.only_current = conf.only_current or false
opt.exclude = vim.list_extend(opt.exclude, conf.exclude or {})
opt.exclude_nodetype = vim.list_extend(opt.exclude_nodetype, conf.exclude_nodetype or {})
opt.config.virt_text = { { conf.char or '│' } }
opt.minlevel = conf.minlevel or 1
set_provider(ns, { on_win = on_win, on_line = on_line })
if conf.key then
vim.keymap.set('n', conf.key, '<Cmd>IndentToggle<CR>', {
desc = 'Toggle indent guides',
noremap = true,
silent = true,
})
end
end
return M