-
Notifications
You must be signed in to change notification settings - Fork 249
Expand file tree
/
Copy pathwin.lua
More file actions
1493 lines (1374 loc) · 55.5 KB
/
win.lua
File metadata and controls
1493 lines (1374 loc) · 55.5 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
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local utils = require "fzf-lua.utils"
local libuv = require "fzf-lua.libuv"
local actions = require "fzf-lua.actions"
local path = require "fzf-lua.path"
local api = vim.api
local fn = vim.fn
---@class fzf-lua.PathShortener
---@field _ns integer?
---@field _wins table<integer, { bufnr: integer, shorten_len: integer }>
local PathShortener = {}
PathShortener._wins = {}
-- Hoist constant byte values for performance
local DOT_BYTE = path.dot_byte
function PathShortener.setup()
if PathShortener._ns then return end
PathShortener._ns = api.nvim_create_namespace("fzf-lua.win.path_shorten")
-- Register decoration provider with ephemeral extmarks
api.nvim_set_decoration_provider(PathShortener._ns, {
on_win = function(_, winid, bufnr, _topline, _botline)
-- Only process registered fzf windows
local win_data = PathShortener._wins[winid]
if not win_data or win_data.bufnr ~= bufnr then
return false
end
return true -- Continue to on_line callbacks
end,
on_line = function(_, winid, bufnr, row)
local win_data = PathShortener._wins[winid]
if not win_data then return end
PathShortener._apply_line(bufnr, row, win_data.shorten_len)
end,
})
end
---Apply path shortening to a single line using ephemeral extmarks
---@param buf integer buffer number
---@param row integer 0-indexed line number
---@param shorten_len integer number of characters to keep
function PathShortener._apply_line(buf, row, shorten_len)
local lines = api.nvim_buf_get_lines(buf, row, row + 1, false)
local line = lines[1]
if not line or #line == 0 then return end
-- Find the path portion of the line
-- Lines may have prefixes like icons separated by nbsp (U+2002)
-- Format: [fzf_pointer] [git_icon nbsp] [file_icon nbsp] path[:line:col:text]
-- When file_icons=false, there's no nbsp separator before the path
-- The fzf terminal also adds pointer/marker prefix (e.g., "> " or " ")
local path_start = 1
local last_nbsp = line:find(utils.nbsp, 1, true)
if last_nbsp then
-- Find the last nbsp (there may be multiple with git_icons + file_icons)
repeat
path_start = last_nbsp + #utils.nbsp
last_nbsp = line:find(utils.nbsp, path_start, true)
until not last_nbsp
else
-- No nbsp means no icons - skip fzf's pointer/marker prefix
-- Paths start with: alphanumeric, `/`, `~`, `.`, or drive letter (Windows)
-- Skip leading whitespace and pointer characters until we hit a path char
local first_path_char = line:find("[%w/~%.]")
if first_path_char then
path_start = first_path_char
end
end
-- Find where the path ends (at first colon after path_start, if any)
-- But be careful with Windows paths like C:\...
local path_end = #line
local colon_search_start = path_start
-- On Windows, skip the drive letter colon (e.g., C:)
-- Check if char at path_start+1 is colon AND char at path_start+2 is a path separator
if string.byte(line, path_start + 1) == path.colon_byte
and path.byte_is_separator(string.byte(line, path_start + 2)) then
colon_search_start = path_start + 2
end
local colon_pos = line:find(":", colon_search_start)
if colon_pos then
path_end = colon_pos - 1
end
-- Now process the path portion for shortening
-- We need to conceal directory components, keeping only `shorten_len` chars
local path_portion = line:sub(path_start, path_end)
-- Use path.find_next_separator to iterate through directory components
local prev_sep = 0
local sep_pos = path.find_next_separator(path_portion, 1)
while sep_pos do
local component_start = prev_sep + 1
local component = path_portion:sub(component_start, sep_pos - 1)
local component_len = #component
-- Count UTF-8 characters using vim.str_utfindex
-- Use "utf-32" to count code points (actual characters), not bytes.
-- "utf-8" would give byte positions, "utf-16" gives UTF-16 code units.
local _, component_charlen = vim.str_utfindex(component, "utf-32")
component_charlen = component_charlen or component_len -- fallback to byte length
-- Only conceal if the component has more characters than shorten_len
if component_charlen > shorten_len then
-- Handle special case: component starts with '.' (hidden files/dirs)
local keep_chars = shorten_len
if string.byte(component, 1) == DOT_BYTE and component_charlen > shorten_len + 1 then
-- Keep the dot plus shorten_len characters
keep_chars = shorten_len + 1
end
-- Bounds check to prevent errors
keep_chars = math.min(keep_chars, component_charlen)
-- Convert character count to byte offset using vim.str_byteindex
local keep_bytes = vim.str_byteindex(component, "utf-32", keep_chars, false)
keep_bytes = keep_bytes or keep_chars -- fallback to character count (ASCII approximation)
-- Calculate 0-indexed byte positions in the full line for extmark
-- path_start is 1-indexed, component_start is 1-indexed within path_portion
local line_offset = path_start - 1 + component_start - 1
local conceal_start = line_offset + keep_bytes
local conceal_end = line_offset + component_len -- end of component (before separator)
if conceal_end > conceal_start then
pcall(api.nvim_buf_set_extmark, buf, PathShortener._ns, row, conceal_start, {
end_col = conceal_end,
conceal = "",
ephemeral = true,
})
end
end
prev_sep = sep_pos
sep_pos = path.find_next_separator(path_portion, sep_pos + 1)
end
end
---Register a window for path shortening
---@param winid integer window ID
---@param bufnr integer buffer number
---@param shorten_len integer|boolean number of characters to keep
function PathShortener.attach(winid, bufnr, shorten_len)
if not winid or not bufnr then return end
PathShortener.setup()
shorten_len = (shorten_len == true) and 1 or tonumber(shorten_len) or 1
PathShortener._wins[winid] = { bufnr = bufnr, shorten_len = shorten_len }
end
---Unregister a window from path shortening
---@param winid integer window ID
function PathShortener.detach(winid)
if not winid then return end
PathShortener._wins[winid] = nil
end
---@alias fzf-lua.win.previewPos "up"|"down"|"left"|"right"
---@alias fzf-lua.win.previewLayout { pos: fzf-lua.win.previewPos, size: number, str: string }
---@class fzf-lua.config.WinoptsResolved: fzf-lua.config.Winopts
---@field height integer
---@field width integer
---@field row integer
---@field col integer
---@field zindex integer
---@field preview fzf-lua.config.PreviewOpts
---@class fzf-lua.Win
---@field winopts fzf-lua.config.WinoptsResolved
---@field actions fzf-lua.config.Actions|{}
---@field hls fzf-lua.config.HLS
---@field fzf_bufnr integer
---@field fzf_winid integer
---@field preview_hidden? boolean
---@field preview_wrap? boolean
---@field fullscreen? boolean
---@field layout? fzf-lua.WinLayout
---@field tsinjector? fzf-lua.TSInjector
---@field previewer? fun(...)|table|string
---@field _hidden_fzf_bufnr? integer
---@field toggle_behavior? "extend"|"default"
---@field _previewer? fzf-lua.previewer.Builtin|fzf-lua.previewer.Fzf
---@field _preview_pos_force? fzf-lua.win.previewPos
---@field last_view? [integer, integer, integer]
---@field on_closes table<any, function>
---@field _o fzf-lua.config.Resolved
local FzfWin = {}
-- singleton instance used by "_exported_wapi"
---@type fzf-lua.Win?
local _self = nil
function FzfWin.__SELF()
return _self
end
local _preview_keymaps = {
["toggle-preview-wrap"] = { module = "win", fnc = "toggle_preview_wrap()" },
["toggle-preview-ts-ctx"] = { module = "win", fnc = "toggle_preview_ts_ctx()" },
["toggle-preview-undo"] = { module = "win", fnc = "toggle_preview_undo_diff()" },
["preview-ts-ctx-inc"] = { module = "win", fnc = "preview_ts_ctx_inc_dec(1)" },
["preview-ts-ctx-dec"] = { module = "win", fnc = "preview_ts_ctx_inc_dec(-1)" },
["preview-up"] = { module = "win", fnc = "preview_scroll('line-up')" },
["preview-down"] = { module = "win", fnc = "preview_scroll('line-down')" },
["preview-page-up"] = { module = "win", fnc = "preview_scroll('page-up')" },
["preview-page-down"] = { module = "win", fnc = "preview_scroll('page-down')" },
["preview-half-page-up"] = { module = "win", fnc = "preview_scroll('half-page-up')" },
["preview-half-page-down"] = { module = "win", fnc = "preview_scroll('half-page-down')" },
["preview-reset"] = { module = "win", fnc = "preview_scroll('reset')" },
["preview-top"] = { module = "win", fnc = "preview_scroll('top')" },
["preview-bottom"] = { module = "win", fnc = "preview_scroll('bottom')" },
["focus-preview"] = { module = "win", fnc = "focus_preview()" },
}
function FzfWin:setup_keybinds()
self.keymap = type(self.keymap) == "table" and self.keymap or {}
self.keymap.fzf = type(self.keymap.fzf) == "table" and self.keymap.fzf or {}
self.keymap.builtin = type(self.keymap.builtin) == "table" and self.keymap.builtin or {}
local keymap_tbl = {
["hide"] = { module = "win", fnc = "hide()" },
["toggle-help"] = { module = "win", fnc = "toggle_help()" },
["toggle-fullscreen"] = { module = "win", fnc = "toggle_fullscreen()" },
["toggle-preview"] = { module = "win", fnc = "toggle_preview()" },
["toggle-preview-cw"] = { module = "win", fnc = "toggle_preview_cw(1)" },
["toggle-preview-ccw"] = { module = "win", fnc = "toggle_preview_cw(-1)" },
["toggle-preview-behavior"] = { module = "win", fnc = "toggle_preview_behavior()" },
}
-- use signal when user bind toggle-preview in FZF_DEFAULT_OPTS/FZF_DEFAULT_FILE_OPTS
local function on_SIGWINCH_toggle_preview()
if utils.__IS_WINDOWS then return end -- not sure why ci fail on windows
self.on_SIGWINCH(self._o, "toggle-preview", function(args)
-- hide if visible but do not toggle if hidden as we want to
-- make sure the right layout is set if user rotated the preview
if tonumber(args[1]) then
return "toggle-preview"
else
-- NOTE: always equals?
-- self = _self or self -- may differ with `... resume previewer=...`
return string.format("change-preview-window(%s)", self:normalize_preview_layout().str)
end
end)
end
local function on_SIGWINCH_toggle_preview_cw()
if utils.__IS_WINDOWS then return end -- not sure why ci fail on windows
self.on_SIGWINCH(self._o, "toggle-preview-cw", function(args)
-- only set the layout if preview isn't hidden
if not tonumber(args[1]) then return end
-- NOTE: always equals?
-- self = _self or self -- may differ with `... resume previewer=...`
return string.format("change-preview-window(%s)", self:normalize_preview_layout().str)
end)
end
-- find the toggle_preview keybind, to be sent when using a split for the native
-- pseudo fzf preview window or when using native and treesitter is enabled
self._fzf_toggle_prev_bind = nil
-- use fzf-native binds in fzf-tmux or cli (shell) profile
if self._o._is_fzf_tmux or _G.fzf_jobstart then
for k, v in pairs(self.keymap.builtin) do
if type(v) == "string" and v:match("toggle%-preview%-c?cw") then
k = utils.neovim_bind_to_fzf(k)
self.keymap.fzf[k] = "transform:" .. FzfLua.shell.stringify_data(function(args, _, _)
-- only set the layout if preview isn't hidden
if not tonumber(args[1]) then return end
assert(keymap_tbl[v])
assert(loadstring(string.format([[require("fzf-lua.%s").%s]],
keymap_tbl[v].module, keymap_tbl[v].fnc)))()
return string.format("change-preview-window(%s)", self:normalize_preview_layout().str)
end, {}, utils.__IS_WINDOWS and "%FZF_PREVIEW_LINES%" or "$FZF_PREVIEW_LINES")
end
end
elseif self.winopts.split or not self.previewer_is_builtin then
-- sync toggle-preview
-- 1. always run the toggle-preview(), and self._fzf_toggle_prev_bind
for k, v in pairs(self.keymap.builtin) do
if v == "toggle-preview" then
on_SIGWINCH_toggle_preview()
self.keymap.fzf[utils.neovim_bind_to_fzf(k)] = v
end
if type(v) == "string" and v:match("toggle%-preview%-c?cw") then
on_SIGWINCH_toggle_preview_cw()
end
end
for k, v in pairs(self.keymap.fzf) do
if v == "toggle-preview" then
on_SIGWINCH_toggle_preview()
self._fzf_toggle_prev_bind = utils.fzf_bind_to_neovim(k)
self.keymap.builtin[self._fzf_toggle_prev_bind] = v
end
if type(v) == "string" and v:match("toggle%-preview%-c?cw") then
on_SIGWINCH_toggle_preview_cw()
self.keymap.fzf[k] = nil -- invalid fzf bind, user set bind by mistake
end
end
self._fzf_toggle_prev_bind = self._fzf_toggle_prev_bind or true
end
if self.previewer_is_builtin then
-- These maps are only valid for the builtin previewer
keymap_tbl = vim.tbl_deep_extend("keep", keymap_tbl, _preview_keymaps)
end
local function funcref_str(keymap)
return ([[<Cmd>lua require('fzf-lua.%s').%s<CR>]]):format(keymap.module, keymap.fnc)
end
for key, action in pairs(self.keymap.builtin) do
local keymap = keymap_tbl[action]
if keymap and not utils.tbl_isempty(keymap) and action ~= false then
vim.keymap.set("t", key, funcref_str(keymap), { nowait = true, buffer = self.fzf_bufnr })
end
end
-- If the user did not override the Esc action ensure it's
-- not bound to anything else such as `<C-\><C-n>` (#663)
if self.actions["esc"] == actions.dummy_abort and not self.keymap.builtin["<esc>"] then
vim.keymap.set("t", "<Esc>", "<Esc>", { buffer = self.fzf_bufnr, nowait = true })
end
end
-- check if previewer useable (not matter if it's hidden)
function FzfWin:has_previewer()
return self._o.preview or self._previewer and true or false
end
---@return fzf-lua.win.previewLayout
function FzfWin:normalize_preview_layout()
local preview_str, pos ---@type string, fzf-lua.win.previewPos
if self._preview_pos_force then
-- Get the correct layout string and size when set from `:toggle_preview_cw`
preview_str = assert((self._preview_pos_force == "up" or self._preview_pos_force == "down")
and self.winopts.preview.vertical or self.winopts.preview.horizontal)
pos = self._preview_pos_force
else
preview_str = self:fzf_preview_layout_str()
pos = preview_str:match("[^:]+") or "right"
end
local percent = tonumber(preview_str:match(":(%d+)%%"))
local abs = not percent and tonumber(preview_str:match(":(%d+)")) or nil
percent = percent or 50
return {
pos = pos,
size = abs or (percent / 100),
str = string.format("%s:%s", pos, tostring(abs or percent) .. ((not abs) and "%" or ""))
}
end
---@return integer nwin, boolean preview
function FzfWin:normalize_layout()
-- when to use full fzf layout
-- 1. no previewer (always)
-- 2. builtin previewer (hidden and not "extend")
-- 3. fzf previewer (not "extend" or not hidden)
if not self:has_previewer()
or (self.previewer_is_builtin and (self.preview_hidden and self.toggle_behavior ~= "extend"))
or (not self.previewer_is_builtin and (self.toggle_behavior ~= "extend" or not self.preview_hidden)) then
return 1, false
end
-- has previewer, but when nwin=1, reduce fzf main layout as if the previewer is displayed
local nwin = self.preview_hidden and self.toggle_behavior == "extend" and 1 or 2
return nwin, true
end
---@class fzf-lua.WinLayout
---@field fzf vim.api.keyset.win_config
---@field preview? vim.api.keyset.win_config
---@return fzf-lua.WinLayout
function FzfWin:generate_layout()
local winopts = self:normalize_winopts()
local nwin, preview = self:normalize_layout()
local layout = self:normalize_preview_layout()
local border, h, w = self:normalize_border(self._o.winopts.border, {
type = "nvim",
name = "fzf",
layout = preview and layout.pos or nil,
nwin = nwin,
opts = self._o
})
if not preview then
return {
fzf = {
row = winopts.row,
col = winopts.col,
width = winopts.width,
height = winopts.height,
border = border,
style = "minimal",
relative = winopts.relative or "editor",
zindex = winopts.zindex,
hide = winopts.hide,
}
}
end
if self.previewer_is_builtin and winopts.split then
local wininfo = utils.__HAS_NVIM_010 and api.nvim_win_get_config(self.fzf_winid) or
assert(fn.getwininfo(self.fzf_winid)[1])
-- no signcolumn/number/relativenumber (in set_style_minimal)
---@diagnostic disable-next-line: missing-fields
winopts = {
height = wininfo.height,
width = wininfo.width,
split = winopts.split,
row = 0,
col = 0,
}
end
local pwopts
local row, col = winopts.row, winopts.col
local height, width = winopts.height, winopts.width
local preview_pos, preview_size = layout.pos, layout.size
local pborder, ph, pw = self:normalize_border(self._o.winopts.preview.border,
{ type = "nvim", name = "prev", layout = preview and layout.pos, nwin = nwin, opts = self._o })
if winopts.split then
-- Custom "split"
pwopts = { relative = "win", anchor = "NW", row = 0, col = 0 }
if preview_pos == "down" or preview_pos == "up" then
pwopts.width = width - pw
pwopts.height = self:normalize_size(preview_size, height) - ph
if preview_pos == "down" then
pwopts.row = height - pwopts.height - ph
end
else -- left|right
pwopts.height = height - ph
pwopts.width = self:normalize_size(preview_size, width) - pw
if preview_pos == "right" then
pwopts.col = width - pwopts.width + pw
end
end
else
-- Float window
pwopts = { relative = "editor" }
if preview_pos == "down" or preview_pos == "up" then
pwopts.col = col
pwopts.width = width
-- https://github.com/junegunn/fzf/blob/1afd14381079a35eac0a4c2a5cacb86e2a3f476b/src/terminal.go#L1820
-- fzf's previewer border is draw inside preview window, so shrink builtin previewer if it have "top border"
-- to ensure the fzf list height is the same between fzf/builtin
local off = (preview_size < 1 and self.previewer_is_builtin and ph > 0) and 1
or (preview_size >= 1 and not self.previewer_is_builtin and ph > 0) and -ph
or 0
pwopts.height = self:normalize_size(preview_size, (height + off)) - off
height = height - pwopts.height
if preview_pos == "down" then
-- next row
pwopts.row = row + h + height
else -- up
pwopts.row = row
row = pwopts.row + ph + pwopts.height
end
-- enlarge the height to align fzf with preview win
if self.previewer_is_builtin then
width = width + math.max(pw - w, 0)
pwopts.width = pwopts.width + math.max(w - pw, 0)
end
else -- left|right
pwopts.row = row
pwopts.height = height
local off = (preview_size < 1 and self.previewer_is_builtin and pw > 0) and 1
or (preview_size >= 1 and not self.previewer_is_builtin and pw > 0) and -pw
or 0
pwopts.width = self:normalize_size(preview_size, (width + off)) - off
width = width - pwopts.width
if preview_pos == "right" then
-- next col
pwopts.col = col + w + width
else -- left
pwopts.col = col
col = pwopts.col + pw + pwopts.width
end
-- enlarge the height to align fzf with preview win
if self.previewer_is_builtin then
height = height + math.max(ph - h, 0)
pwopts.height = pwopts.height + math.max(h - ph, 0)
end
end
end
return {
fzf = vim.tbl_extend("force", { row = row, col = col, height = height, width = width }, {
style = "minimal",
border = border,
relative = winopts.relative or "editor",
zindex = winopts.zindex,
hide = winopts.hide,
}),
preview = vim.tbl_extend("force", pwopts, {
style = "minimal",
zindex = winopts.zindex,
border = pborder,
focusable = true,
hide = winopts.hide,
}),
}
end
function FzfWin:tmux_columns()
local is_popup, is_hsplit, opt_val = (function()
-- Backward compat using "fzf-tmux" script
if self._o._is_fzf_tmux == 1 then
for _, flag in ipairs({ "-l", "-r" }) do
if self._o.fzf_tmux_opts[flag] then
-- left/right split, not a popup, is an hsplit
return false, true, self._o.fzf_tmux_opts[flag]
end
end
for _, flag in ipairs({ "-u", "-d" }) do
if self._o.fzf_tmux_opts[flag] then
-- up/down split, not a popup, not an hsplit
return false, false, self._o.fzf_tmux_opts[flag]
end
end
-- Default is a popup with "-p" or without
return true, false, self._o.fzf_tmux_opts["-p"]
else
return true, false, self._o.fzf_opts["--tmux"]
end
end)()
local out = utils.io_system({
"tmux", "display-message", "-p",
is_popup and "#{window_width}" or "#{pane_width}"
})
local cols = tonumber(out:match("%d+"))
-- Calc the correct width when using tmux popup or left|right splits
-- fzf's defaults to "--tmux" is "center,50%" or "50%" for splits
if is_popup or is_hsplit then
local percent = type(opt_val) == "string" and tonumber(opt_val:match("(%d+)%%")) or 50
cols = math.floor(assert(cols) * percent / 100)
end
return cols
end
function FzfWin:columns(no_fullscreen)
-- When called from `core.preview_window` we need to get the no-fullscreen columns
-- in order to get an accurate alternate layout trigger that will also be consistent
-- when starting with `winopts.fullscreen == true`
local winopts = no_fullscreen and self._o.winopts or self.winopts
return self._o._is_fzf_tmux and self:tmux_columns()
or vim.is_callable(_G.fzf_tty_get_width) and _G.fzf_tty_get_width()
or winopts.split and api.nvim_win_get_width(self.fzf_winid or 0)
or self:normalize_size(winopts.width, vim.o.columns)
end
function FzfWin:fzf_preview_layout_str()
local columns = self:columns()
local is_hsplit = self.winopts.preview.layout == "horizontal"
or self.winopts.preview.layout == "flex" and columns > self.winopts.preview.flip_columns
return is_hsplit and self._o.winopts.preview.horizontal or self._o.winopts.preview.vertical
end
---@param border any
---@param metadata fzf-lua.win.borderMetadata
---@return fzf-lua.winborder, integer, integer
function FzfWin:normalize_border(border, metadata)
return require("fzf-lua.win.border").nvim(border, metadata, self._o.silent)
end
---@param size number|integer
---@param max integer
---@return integer
function FzfWin:normalize_size(size, max)
local _ = self
if size <= 1 then return math.floor(max * size) end
---@cast size integer
return math.min(size, max)
end
---@return fzf-lua.config.WinoptsResolved
function FzfWin:normalize_winopts()
-- make a local copy of winopts so we don't pollute the user's options
self.winopts = utils.tbl_deep_clone(self._o.winopts or {}) or {}
local winopts = self.winopts
if self.fullscreen then
-- NOTE: we set `winopts.relative=editor` so fullscreen
-- works even when the user set `winopts.relative=cursor`
winopts.relative = "editor"
winopts.row = 1
winopts.col = 1
winopts.width = 1
winopts.height = 1
end
local nwin, preview = self:normalize_layout()
local preview_pos = preview and self:normalize_preview_layout().pos or nil
if preview and self.previewer_is_builtin then nwin = 2 end
local _, h, w = self:normalize_border(self._o.winopts.border,
{ type = "nvim", name = "fzf", layout = preview_pos, nwin = nwin, opts = self._o })
if preview and self.previewer_is_builtin then
local _, ph, pw = self:normalize_border(self._o.winopts.preview.border,
{ type = "nvim", name = "prev", layout = preview_pos, nwin = nwin, opts = self._o })
if preview_pos == "up" or preview_pos == "down" then
h, w = h + ph, math.max(w, pw)
else -- left|right
h, w = math.max(h, ph), w + pw
end
end
-- #2121 we can suppress cmdline area when zindex >= 200
local ch = winopts.zindex >= 200 and 0 or vim.o.cmdheight
local max_width = vim.o.columns
local max_height = vim.o.lines - ch
winopts.width = self:normalize_size(assert(tonumber(winopts.width)), max_width)
winopts.height = self:normalize_size(assert(tonumber(winopts.height)), max_height)
if winopts.relative == "cursor" then
-- convert cursor relative to absolute ('editor'),
-- this solves the preview positioning seamlessly
-- use the calling window context for correct pos
local winid = utils.CTX().winid
local pos = api.nvim_win_get_cursor(winid)
local screenpos = fn.screenpos(winid, pos[1], pos[2])
winopts.row = math.floor((winopts.row or 0) + screenpos.row - 1)
winopts.col = math.floor((winopts.col or 0) + screenpos.col - 1)
winopts.relative = nil
end
-- make row close to the center of screen (include cmdheight)
-- avoid breaking existing test
winopts.row = self:normalize_size(assert(tonumber(winopts.row)), vim.o.lines - winopts.height)
winopts.col = self:normalize_size(assert(tonumber(winopts.col)), max_width - winopts.width)
winopts.row = math.min(winopts.row, max_height - winopts.height)
-- width/height can be used for text area
winopts.width = math.max(1, winopts.width - w)
winopts.height = math.max(1, winopts.height - h)
---@type fzf-lua.config.WinoptsResolved
return winopts
end
---@param winhls table<string, string|false>|string
---@return string
local function make_winhl(winhls)
if type(winhls) == "string" then return winhls end
local winhl = {}
for k, h in pairs(winhls) do
if h then winhl[#winhl + 1] = ("%s:%s"):format(k, h) end
end
return table.concat(winhl, ",")
end
---@param win integer
---@param pwinhl? table<string, string|false>|string preview winhl
function FzfWin:reset_winhl(win, pwinhl)
-- derive the highlights from the window type
local hls = self.hls
local winhls = pwinhl or {
Normal = hls.normal,
NormalFloat = hls.normal,
FloatBorder = hls.border,
CursorLine = hls.cursorline,
CursorLineNr = hls.cursorlinenr,
}
(pwinhl and utils.wo[win] or utils.wo[win][0]).winhl = make_winhl(winhls)
end
---@param exit_code integer
---@param fzf_bufnr integer?
function FzfWin:check_exit_status(exit_code, fzf_bufnr)
-- see the comment in `FzfWin:close` for more info
if fzf_bufnr and fzf_bufnr ~= self.fzf_bufnr then
return
end
if not self:validate() then return end
-- from 'man fzf':
-- 0 Normal exit
-- 1 No match
-- 2 Error
-- 130 Interrupted with CTRL-C or ESC
if exit_code == 2 then
local lines = api.nvim_buf_get_lines(self.fzf_bufnr, 0, 1, false)
utils.error("fzf error %d: %s", exit_code, lines and #lines[1] > 0 and lines[1] or "<null>")
end
end
function FzfWin:set_autoclose(autoclose)
self._autoclose = autoclose
end
function FzfWin:autoclose()
return self._autoclose
end
function FzfWin:set_backdrop()
-- No backdrop for split, only floats / tmux
if self.winopts.split then return end
self.on_closes.backdrop = require("fzf-lua.win.backdrop").open(self.winopts.backdrop,
self.winopts.zindex - 2, self.hls)
end
---@param o fzf-lua.config.Resolved
---@return fzf-lua.Win
function FzfWin.new(o)
if not _self then
elseif _self._hidden_fzf_bufnr then
_self:close_buf(_self._hidden_fzf_bufnr)
_self = nil
elseif not _self:hidden() then
-- utils.warn("Please close fzf-lua before starting a new instance")
_self._reuse = true
-- switch to fzf-lua's main window in case the user switched out
-- NOTE: `self.fzf_winid == nil` when using fzf-tmux
if _self.fzf_winid and _self.fzf_winid ~= api.nvim_get_current_win() then
api.nvim_set_current_win(_self.fzf_winid)
end
-- Update main win title, required for toggle action flags
_self:update_main_title(o.winopts.title)
-- refersh treesitter settings as new picker might have it disabled
-- detach previewer and refresh signal handler
-- e.g. when switch from fzf previewer to builtin previewer
_self._o = o
o.winopts.preview.hidden = _self.preview_hidden
_self:attach_previewer(nil)
return _self
end
o = o or {} ---@type fzf-lua.config.Resolved
---@type fzf-lua.Win
local self = utils.setmetatable({}, -- gc is unused now, only used to test _self is nullrified
{ __index = FzfWin, __gc = function() _G._fzf_lua_gc_called = true end })
self._o = o
self.hls = o.hls
self.actions = o.actions
self.fullscreen = o.winopts.fullscreen
self.toggle_behavior = o.winopts.toggle_behavior
self.preview_wrap = not not o.winopts.preview.wrap -- force boolean
self.preview_hidden = not not o.winopts.preview.hidden -- force boolean
self.keymap = o.keymap
self.previewer = o.previewer
self:set_autoclose(vim.F.if_nil(o.autoclose, true))
self.winopts = self:normalize_winopts()
self.on_closes = {}
_self = self
return self
end
---@param win integer
---@param opts vim.wo|{}
---@return vim.wo|{}
function FzfWin:get_winopts(win, opts)
local _ = self
if not win or not api.nvim_win_is_valid(win) then return {} end
local ret = {}
for opt, _ in pairs(opts) do
ret[opt] = utils.wo[win][opt]
end
return ret
end
---@param win integer
---@param opts vim.wo|{}
---@param ignore_events boolean?
---@param global boolean?
function FzfWin:set_winopts(win, opts, ignore_events, global)
local _ = self
if not win or not api.nvim_win_is_valid(win) then return end
-- NOTE: Do not trigger "OptionSet" as this will trigger treesitter-context's
-- `update_single_context` which will in turn close our treesitter-context
local ei = ignore_events and "all" or vim.o.eventignore
local wo = global ~= false and utils.wo[win] or utils.wo[win][0]
utils.eventignore(function()
for opt, value in pairs(opts) do
wo[opt] = value
end
end, ei)
end
---@param previewer fzf-lua.previewer.Builtin|fzf-lua.previewer.Fzf? nil to "detach" previewer
function FzfWin:attach_previewer(previewer)
if previewer then
previewer.win = self
previewer.delay = self.winopts.preview.delay or 100
previewer.title = self.winopts.preview.title
previewer.title_pos = self.winopts.preview.title_pos
previewer.winopts = self.winopts.preview.winopts
end
-- clear the previous previewer if existed
if self._previewer and self._previewer.close then
-- if we press ctrl-g too quickly 'previewer.preview_bufnr' will be nil
-- and even though the temp buffer is set to 'bufhidden:wipe' the buffer
-- won't be closed properly and remain lingering (visible in `:ls!`)
-- make sure the previewer is aware of this buffer
if not self._previewer.preview_bufnr and self:validate_preview() then
self._previewer.preview_bufnr = api.nvim_win_get_buf(self.preview_winid)
end
if self.on_closes.preview then self.on_closes.preview() end
end
-- This makes sure previewer.base:close is always called on :close
-- (1) Used by swiper/ivy/custom previewers
-- (2) Overwritten (extended) in builtin previewer (in :redraw_preview)
self.on_closes.preview = function(hide) self:close_preview(hide) end
self._previewer = previewer
self.previewer_is_builtin = previewer and previewer.type == "builtin"
self.toggle_behavior = previewer and previewer.toggle_behavior or self.toggle_behavior
self:normalize_winopts()
end
function FzfWin:validate_preview()
return not self.closing
and self.preview_winid
and api.nvim_win_is_valid(self.preview_winid)
end
function FzfWin:redraw_preview()
if not self.previewer_is_builtin or self.preview_hidden then
return
end
local previewer = self._previewer ---@cast previewer fzf-lua.previewer.Builtin
-- Close the exisiting scrollbar
self:close_preview_scrollbar()
-- Generate the preview layout
self.layout = self:generate_layout()
local preview = assert(self.layout.preview)
if self:validate_preview() then
utils.win_set_config(self.preview_winid, preview)
else
local tmp_buf = previewer:get_tmp_buffer()
-- No autocmds, can only be sent with 'nvim_open_win'
self.preview_winid = api.nvim_open_win(tmp_buf, false,
vim.tbl_extend("force", preview, { noautocmd = true }))
-- Add win local var for the preview|border windows
api.nvim_win_set_var(self.preview_winid, "fzf_lua_preview", true)
end
previewer:reset_winhl(self.preview_winid)
previewer:display_last_entry()
previewer:update_ts_context()
local release = previewer:copy_extmarks()
self.on_closes.preview = function(hide)
if release then release() end
self:close_preview(hide)
end
end
function FzfWin:validate()
return self.fzf_winid and self.fzf_winid > 0
and api.nvim_win_is_valid(self.fzf_winid)
end
function FzfWin:redraw()
self:normalize_winopts()
self:set_backdrop()
if self:validate() then
self:redraw_main()
end
if self:validate_preview() then
self:redraw_preview()
end
end
---@param title any
---@param hl? string|false hl will also be used as fallback, if title part don't have hl
---@return [string, string][]
local function make_title(title, hl)
if type(title) == "string" then return { { title, type(hl) == "string" and hl or "FloatTitle" } } end
return type(title) ~= "table" and { { "", hl or "FloatTitle" } }
or vim.tbl_map(function(p) return { p[1], p[2] or hl or "FloatTitle" } end, title)
end
function FzfWin:redraw_main()
if self.winopts.split then return end
self.layout = self:generate_layout()
local winopts = vim.tbl_extend("keep", {
title = make_title(self.winopts.title, self.hls.title),
title_pos = self.winopts.title_pos,
}, self.layout.fzf)
if self:validate() then
local prev = self._previewer
if prev and prev.clear_on_redraw then
if prev.clear_preview_buf then prev:clear_preview_buf(true) end
if prev.bcache then prev.bcache:clear() end
end
utils.win_set_config(self.fzf_winid, winopts)
else
self.fzf_bufnr = self.fzf_bufnr or api.nvim_create_buf(false, true)
-- save 'cursorline' setting prior to opening the popup
-- `:help nvim_open_win`
-- 'minimal' sets 'nocursorline', normally this shouldn't
-- be an issue but for some reason this is affecting opening
-- buffers in new splits and causes them to open with
-- 'nocursorline', see discussion in #254
local cursorline = vim.o.cursorline
self.fzf_winid = utils.nvim_open_win(self.fzf_bufnr, true, winopts)
---@diagnostic disable-next-line: preferred-local-alias
if not utils.__HAS_NVIM_0116 and vim.o.cursorline ~= cursorline then
vim.o.cursorline = cursorline
end
-- disable search highlights as they interfere with fzf's highlights
if vim.o.hlsearch and vim.v.hlsearch == 1 then
vim.cmd("nohls")
-- use `vim.o.hlsearch` as `vim.cmd("hls")` is invalid
self.on_closes.hlsearch = function() vim.o.hlsearch = true end
end
end
end
function FzfWin:on(e, callback, global)
api.nvim_create_autocmd(e, {
group = api.nvim_create_augroup("FzfLua" .. e, { clear = true }),
buffer = global ~= true and self.fzf_bufnr or nil,
callback = callback,
})
end
function FzfWin:setup_autocmds()
-- automatically resize fzf window
self:on("VimResized", function() self:redraw() end)
-- verify the preview is closed, this can happen
-- when running async LSP with 'jump1'
self:on("WinClosed", function() self:close() end)
-- Workaround for using `:wqa` with "hide"
-- https://github.com/neovim/neovim/issues/14061
self:on("ExitPre", function() self:close() end, true)
end
-- attach/detach treesitter (e.g. `grep_lgrep`)
-- Use treesitter to highlight results on the main fzf window
function FzfWin:treesitter_attach()
if not self._o.winopts.treesitter then
if self.tsinjector then self.tsinjector.detach(self.fzf_bufnr) end
return
end
self.tsinjector = require("fzf-lua.win.tsinjector")
self.on_closes.tsinjector = self.tsinjector.attach(self, self.fzf_bufnr, self._o._treesitter)
end
function FzfWin:path_shorten_detach()
PathShortener.detach(self.fzf_winid)
-- Reset conceallevel when path shortening is disabled (e.g., on window reuse)
if api.nvim_win_is_valid(self.fzf_winid) then
vim.wo[self.fzf_winid].conceallevel = 0
vim.wo[self.fzf_winid].concealcursor = ""
end
end
function FzfWin:path_shorten_attach()
local path_shorten = self._o.winopts.path_shorten
if not path_shorten then return end
-- Enable conceallevel for the fzf window to show concealed text
if api.nvim_win_is_valid(self.fzf_winid) then
vim.wo[self.fzf_winid].conceallevel = 2
vim.wo[self.fzf_winid].concealcursor = "nvic"
end
PathShortener.attach(self.fzf_winid, self.fzf_bufnr, path_shorten)
end
---@param buf integer
function FzfWin:close_buf(buf)
utils.nvim_buf_delete(buf, { force = true })
if self.tsinjector then self.tsinjector.clear_cache(buf) end
end
function FzfWin:set_tmp_buffer()
local detached = self.fzf_bufnr
-- detach the buffer, and kill it after win_set_buf (#1850)
-- If called from fzf-tmux/split fzf_bufnr will be `nil` (#1556)
if detached then vim.bo[detached].bufhidden = "hide" end
-- replace the attached buffer with a new temp buffer, setting `self.fzf_bufnr`
-- makes sure the call to `fzf_win:close` (which is triggered by the buf del)
-- won't trigger a close due to mismatched buffers condition on `self:close`
self.fzf_bufnr = api.nvim_create_buf(false, true)
utils.win_set_buf_noautocmd(self.fzf_winid, self.fzf_bufnr)
-- close the previous fzf term buffer without triggering autocmds
-- this also kills the previous fzf process if its still running
if detached then self:close_buf(detached) end
return self.fzf_bufnr
end
function FzfWin:save_style_minimal(winid)
return self:get_winopts(winid, {
number = true,
relativenumber = true,
cursorline = true,
cursorcolumn = true,
spell = true,
list = true,
signcolumn = true,
foldcolumn = true,
colorcolumn = true,
winhl = true, -- for `winopts.split=enew`
})
end
---@param winid integer
---@param global boolean If true, 'wo' can be inherited by other windows/buffers
function FzfWin:set_style_minimal(winid, global)
local _ = self
if not tonumber(winid) or not api.nvim_win_is_valid(winid) then return end
self:set_winopts(winid, {
number = false,
relativenumber = false,
-- BUG(upstream): causes issues with winopts.split=enew
-- https://github.com/neovim/neovim/issues/37484
-- cursorline = false,
cursorcolumn = false,
spell = false,
list = false,
signcolumn = "no",
foldcolumn = "0",