From 17da90811870660234df243c7a30416e2b7b5221 Mon Sep 17 00:00:00 2001 From: bhagwan Date: Tue, 10 Dec 2024 01:16:36 -0800 Subject: [PATCH] feat(treesitter): enable treesitter for `lines|blines|git_blame` This commits also includes many other changes: - `winopts.treesitter` can also be enabled (disabled by default) in grep/LSP pickers, but will be disabled in `live_grep` as ts highlights will override the regex highlighting - When enabled `fzf_colors` will automatically set `hl,hl+` to `-1:reverse` which changes the background of the character, this is done to make the TS highlights more distingished NOTE1: `reverse` doesn't work as well with skim NOTE2: To disable set `winopts.treesitter.fzf_colors=false` - Formatting changes for `lines,blines`, will match the formatting of `fzf.vim`, `treesitter` picker formatting slightly improved - New highlights: + `FzfLuaBufId`: linked to `TabLine` by default, used to highlight the bufnr in `lines` + `FzfLuaBufLineNr`: linked to `LineNr` by default, used in `lines,blines,treesitter` for line numbers - New option: `lines.show_bufname`, default value is `120`, will only display buffer name (filepath) if neovim's `columns > show_bufname` - Separated `grep_curbuf|lgrep_curbuf` options to its own category - Disabled with fzf-tmux profile as it does nothing (not a neovim buffer and thus can't enable the TS injector) - Fixed Telescope profile gutter color (transparent) - Highlights picker fuzzy matching colors set to "-1:reverse" --- OPTIONS.md | 20 +- README.md | 58 +- doc/fzf-lua-opts.txt | 27 +- lua/fzf-lua/config.lua | 39 +- lua/fzf-lua/core.lua | 12 - lua/fzf-lua/defaults.lua | 105 ++- lua/fzf-lua/init.lua | 24 +- lua/fzf-lua/lib/utf8.lua | 1084 ++++++++++++++++++++++++ lua/fzf-lua/profiles/default-title.lua | 3 +- lua/fzf-lua/profiles/fzf-tmux.lua | 2 + lua/fzf-lua/profiles/telescope.lua | 2 +- lua/fzf-lua/providers/buffers.lua | 151 ++-- lua/fzf-lua/providers/grep.lua | 32 +- lua/fzf-lua/providers/helptags.lua | 161 ++-- lua/fzf-lua/win.lua | 109 ++- 15 files changed, 1549 insertions(+), 280 deletions(-) create mode 100644 lua/fzf-lua/lib/utf8.lua diff --git a/OPTIONS.md b/OPTIONS.md index b5f3ef63..9420f4ff 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -554,25 +554,37 @@ Interactive headers description highlight group, e.g. ` to Disable .giti Type: `string`, Default: `FzfLuaPathLineNr` -Highlight group for the line part of paths, e.g. `file:::`, used in pickers such as `buffers`, `lines`, `quickfix`, `lsp`, `diagnostics`, etc. +Highlight group for the line part of paths, e.g. `file:::`, used in pickers such as `buffers`, `quickfix`, `lsp`, `diagnostics`, etc. #### globals.hls.path_colnr Type: `string`, Default: `FzfLuaPathColNr` -Highlight group for the column part of paths, e.g. `file:::`, used in pickers such as `buffers`, `lines`, `quickfix`, `lsp`, `diagnostics`, etc. +Highlight group for the column part of paths, e.g. `file:::`, used in pickers such as `buffers`, `quickfix`, `lsp`, `diagnostics`, etc. #### globals.hls.buf_name Type: `string`, Default: `FzfLuaBufName` -Highlight group for buffer name in `lines`. +Highlight group for buffer name (filepath) in `lines`. + +#### globals.hls.buf_id + +Type: `string`, Default: `FzfLuaBufId` + +Highlight group for buffer id (number) in `lines`. #### globals.hls.buf_nr Type: `string`, Default: `FzfLuaBufNr` -Highlight group for buffer number in buffer type pickers, i.e. `buffers`, `tabs`, `lines`. +Highlight group for buffer number in `buffers`, `tabs`. + +#### globals.hls.buf_linenr + +Type: `string`, Default: `FzfLuaBufLineNr` + +Highlight group for buffer line number in `lines`, `blines` and `treesitter`. #### globals.hls.buf_flag_cur diff --git a/README.md b/README.md index 43f1a0dd..975c9f57 100644 --- a/README.md +++ b/README.md @@ -585,6 +585,14 @@ require'fzf-lua'.setup { -- title = "Title", -- title_pos = "center", -- 'left', 'center' or 'right' fullscreen = false, -- start fullscreen? + -- enable treesitter highlighting for the main fzf window will only have + -- effect when grep like results are present, i.e. "file:line:col:text" + -- due to highlight color collisions will also override `fzf_colors` + -- set `fzf_colors=false` or `fzf_colors.hl=...` to override + treesitter = { + enable = false, + fzf_colors = { ["hl"] = "-1:reverse", ["hl+"] = "-1:reverse" } + }, preview = { -- default = 'bat', -- override the default previewer? -- default uses the 'builtin' previewer @@ -1106,45 +1114,25 @@ require'fzf-lua'.setup { ["--with-nth"] = '2..', }, }, + -- `blines` has the same defaults as `lines` aside from prompt and `show_bufname` lines = { - previewer = "builtin", -- set to 'false' to disable prompt = 'Lines❯ ', + file_icons = true, + show_bufname = true, -- display buffer name show_unloaded = true, -- show unloaded buffers show_unlisted = false, -- exclude 'help' buffers no_term_buffers = true, -- exclude 'term' buffers + sort_lastused = true, -- sort by most recent + winopts = { treesitter = true }, -- enable TS highlights fzf_opts = { -- do not include bufnr in fuzzy matching -- tiebreak by line no. - ["--delimiter"] = "[\\]:]", - ["--nth"] = '2..', - ["--tiebreak"] = 'index', - ["--tabstop"] = "1", - }, - -- actions inherit from 'actions.files' and merge - actions = { - ["enter"] = actions.buf_edit_or_qf, - ["alt-q"] = actions.buf_sel_to_qf, - ["alt-l"] = actions.buf_sel_to_ll - }, - }, - blines = { - previewer = "builtin", -- set to 'false' to disable - prompt = 'BLines❯ ', - show_unlisted = true, -- include 'help' buffers - no_term_buffers = false, -- include 'term' buffers - -- start = "cursor" -- start display from cursor? - fzf_opts = { - -- hide filename, tiebreak by line no. - ["--delimiter"] = "[:]", - ["--with-nth"] = '2..', - ["--tiebreak"] = 'index', + ["--multi"] = true, + ["--delimiter"] = "[\t]", ["--tabstop"] = "1", - }, - -- actions inherit from 'actions.files' and merge - actions = { - ["enter"] = actions.buf_edit_or_qf, - ["alt-q"] = actions.buf_sel_to_qf, - ["alt-l"] = actions.buf_sel_to_ll + ["--tiebreak"] = "index", + ["--with-nth"] = "2..", + ["--nth"] = "4..", }, }, tags = { @@ -1418,10 +1406,12 @@ temporarily overridden by its corresponding `winopts` option: |FzfLuaHelpBorder |FzfLuaBorder |`hls.help_border` |Help win border| |FzfLuaHeaderBind |*BlanchedAlmond |`hls.header_bind` |Header keybind| |FzfLuaHeaderText |*Brown1 |`hls.header_text` |Header text| -|FzfLuaPathColNr |*CadetBlue1 |`hls.path_colnr` |Path col nr (`lines,qf,lsp,diag`)| -|FzfLuaPathLineNr |*LightGreen |`hls.path_linenr` |Path line nr (`lines,qf,lsp,diag`)| -|FzfLuaBufName |*LightMagenta |`hls.buf_name` |Buffer name (`lines`)| -|FzfLuaBufNr |*BlanchedAlmond |`hls.buf_nr` |Buffer number (all buffers)| +|FzfLuaPathColNr |*CadetBlue1 |`hls.path_colnr` |Path col nr (`qf,lsp,diag`)| +|FzfLuaPathLineNr |*LightGreen |`hls.path_linenr` |Path line nr (`qf,lsp,diag`)| +|FzfLuaBufName |Directory |`hls.buf_name` |Buffer name (`lines`)| +|FzfLuaBufId |TabLine |`hls.buf_id` |Buffer ID (`lines`)| +|FzfLuaBufNr |*BlanchedAlmond |`hls.buf_nr` |Buffer number (`buffers,tabs`)| +|FzfLuaBufLineNr |LineNr |`hls.buf_linenr` |Buffer line nr (`lines,blines`)| |FzfLuaBufFlagCur |*Brown1 |`hls.buf_flag_cur` |Buffer line (`buffers`)| |FzfLuaBufFlagAlt |*CadetBlue1 |`hls.buf_flag_alt` |Buffer line (`buffers`)| |FzfLuaTabTitle |*LightSkyBlue1 |`hls.tab_title` |Tab title (`tabs`)| diff --git a/doc/fzf-lua-opts.txt b/doc/fzf-lua-opts.txt index 4892d794..a58c0594 100644 --- a/doc/fzf-lua-opts.txt +++ b/doc/fzf-lua-opts.txt @@ -1,4 +1,4 @@ -*fzf-lua-opts.txt* For Neovim >= 0.8.0 Last change: 2024 November 18 +*fzf-lua-opts.txt* For Neovim >= 0.8.0 Last change: 2024 December 10 ============================================================================== Table of Contents *fzf-lua-opts-table-of-contents* @@ -736,7 +736,7 @@ globals.hls.path_linenr *fzf-lua-opts-globals.hls.path_linenr* Type: `string`, Default: `FzfLuaPathLineNr` Highlight group for the line part of paths, e.g. `file:::`, used in -pickers such as `buffers`, `lines`, `quickfix`, `lsp`, `diagnostics`, etc. +pickers such as `buffers`, `quickfix`, `lsp`, `diagnostics`, etc. @@ -745,7 +745,7 @@ globals.hls.path_colnr *fzf-lua-opts-globals.hls.path_colnr* Type: `string`, Default: `FzfLuaPathColNr` Highlight group for the column part of paths, e.g. `file:::`, used -in pickers such as `buffers`, `lines`, `quickfix`, `lsp`, `diagnostics`, etc. +in pickers such as `buffers`, `quickfix`, `lsp`, `diagnostics`, etc. @@ -753,7 +753,15 @@ globals.hls.buf_name *fzf-lua-opts-globals.hls.buf_name* Type: `string`, Default: `FzfLuaBufName` -Highlight group for buffer name in `lines`. +Highlight group for buffer name (filepath) in `lines`. + + + +globals.hls.buf_id *fzf-lua-opts-globals.hls.buf_id* + +Type: `string`, Default: `FzfLuaBufId` + +Highlight group for buffer id (number) in `lines`. @@ -761,8 +769,15 @@ globals.hls.buf_nr *fzf-lua-opts-globals.hls.buf_nr* Type: `string`, Default: `FzfLuaBufNr` -Highlight group for buffer number in buffer type pickers, i.e. `buffers`, -`tabs`, `lines`. +Highlight group for buffer number in `buffers`, `tabs`. + + + +globals.hls.buf_linenr *fzf-lua-opts-globals.hls.buf_linenr* + +Type: `string`, Default: `FzfLuaBufLineNr` + +Highlight group for buffer line number in `lines`, `blines` and `treesitter`. diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index d978e38d..4e16bd17 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -217,18 +217,53 @@ function M.normalize_opts(opts, globals, __resume_key) -- merge with provider defaults from globals (defaults + setup options) opts = vim.tbl_deep_extend("keep", opts, utils.tbl_deep_clone(globals)) + -- Running a command and setting `treesitter` sub-values will imply + -- `enable=true`, e.g: + -- `:FzfLua blines winopts.treesitter.fzf_colors=false` + -- `:FzfLua blines winopts.treesitter.fzf_colors={hl="-1:underline"}` + if opts.winopts + and type(opts.winopts.treesitter) == "table" + and opts.winopts.treesitter.enable ~= false + then + opts.winopts.treesitter.enable = true + end + -- Merge required tables from globals for _, k in ipairs({ - "winopts", "keymap", "fzf_opts", "fzf_tmux_opts", "hls" + "winopts", "keymap", "fzf_opts", "fzf_colors", "fzf_tmux_opts", "hls" }) do opts[k] = vim.tbl_deep_extend("keep", -- must clone or map will be saved as reference -- and then overwritten if found in 'backward_compat' type(opts[k]) == "function" and opts[k]() or opts[k] or {}, - type(M.globals[k]) == "function" and M.globals[k]() or + type(M.globals[k]) == "function" and M.globals[k](opts) or type(M.globals[k]) == "table" and utils.tbl_deep_clone(M.globals[k]) or {}) end + -- Adjust main fzf window treesitter settings + -- Disabled unless the picker is TS enabled with `_treesitter=true` + -- Unless `enable=false` is specifically set `true` is asssumed + if not opts._treesitter then opts.winopts.treesitter = nil end + if opts.winopts.treesitter and type(opts.winopts.treesitter) ~= "table" then + opts.winopts.treesitter = {} + end + if not opts.winopts.treesitter or opts.winopts.treesitter.enable == false then + opts.winopts.treesitter = nil + else + assert(type(opts.winopts.treesitter) == "table") + -- Unless the caller specifically disables `fzf_colors` fuzzy matching + -- colors "hl,hl+" will be set to "-1:reverse" which sets the background + -- color for matches to the corresponding original foreground color + -- NOTE: `fzf_colors` inherited from `defaults.winopts.treesitter` + if opts.winopts.treesitter.fzf_colors ~= false then + opts.fzf_colors = vim.tbl_deep_extend("force", + opts.fzf_colors or {}, + M.defaults.winopts.treesitter.fzf_colors, + type(opts.winopts.treesitter.fzf_colors) == "table" + and opts.winopts.treesitter.fzf_colors or {}) + end + end + -- backward compat: no-value flags should be set to `true`, in the past these -- would be set to an empty string which would now translate into a shell escaped -- string as we automatically shell escape all fzf_opts diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua index 3d47c7ab..d2cb0c0c 100644 --- a/lua/fzf-lua/core.lua +++ b/lua/fzf-lua/core.lua @@ -498,9 +498,6 @@ end -- Create fzf --color arguments from a table of vim highlight groups. M.create_fzf_colors = function(opts) local colors = opts and opts.fzf_colors - if type(colors) == "function" then - colors = colors(opts) - end -- auto create `fzf_colors` based on Neovim's current colorscheme if colors == true then colors = { @@ -622,15 +619,6 @@ end ---@param opts table ---@return string[] M.build_fzf_cli = function(opts, fzf_win) - opts.fzf_opts = vim.tbl_extend("force", config.globals.fzf_opts, opts.fzf_opts or {}) - -- copy/merge from globals - for _, o in ipairs({ "fzf_colors", "keymap" }) do - if opts[o] == nil then - opts[o] = config.globals[o] - elseif type(opts[o]) == "table" and type(config.globals[o]) == "table" then - opts[o] = vim.tbl_deep_extend("keep", opts[o], config.globals[o]) - end - end -- below options can be specified directly in opts and will be -- prioritized: opts. is prioritized over fzf_opts["--name"] for _, flag in ipairs({ "query", "prompt", "header", "preview" }) do diff --git a/lua/fzf-lua/defaults.lua b/lua/fzf-lua/defaults.lua index d2e74922..477e6336 100644 --- a/lua/fzf-lua/defaults.lua +++ b/lua/fzf-lua/defaults.lua @@ -49,6 +49,10 @@ M.defaults = { zindex = 50, backdrop = 60, fullscreen = false, + treesitter = { + enable = false, + fzf_colors = { ["hl"] = "-1:reverse", ["hl+"] = "-1:reverse" } + }, preview = { default = "builtin", border = "border", @@ -426,8 +430,11 @@ M.defaults.git = { ["ctrl-t"] = actions.git_buf_tabedit, ["ctrl-y"] = { fn = actions.git_yank_commit, exec_silent = true }, }, + winopts = { treesitter = true }, fzf_opts = { ["--no-multi"] = true }, _multiline = false, + -- `winopts.treesitter==true` line match format + _treesitter = "(%s+)(%d+)%)(.+)$", }, branches = { prompt = "Branches> ", @@ -505,8 +512,20 @@ M.defaults.grep = { -- live_grep_glob options glob_flag = "--iglob", -- for case sensitive globs use '--glob' glob_separator = "%s%-%-", -- query separator pattern (lua): ' --' + _treesitter = true, } +M.defaults.grep_curbuf = vim.tbl_deep_extend("force", M.defaults.grep, { + prompt = "BufRg> ", + rg_glob = false, -- meaningless for single file rg + exec_empty_query = true, -- makes sense to display lines immediately + fzf_opts = { + ["--delimiter"] = "[:]", + ["--with-nth"] = "2..", + ["--nth"] = "2..", + }, +}) + M.defaults.args = { previewer = M._default_previewer_fn, prompt = "Args> ", @@ -542,6 +561,7 @@ M.defaults.quickfix = { fzf_opts = { ["--multi"] = true }, _actions = function() return M.globals.actions.files end, only_valid = false, + _treesitter = true, _cached_hls = { "path_colnr", "path_linenr" }, } @@ -563,6 +583,7 @@ M.defaults.loclist = { fzf_opts = { ["--multi"] = true }, _actions = function() return M.globals.actions.files end, only_valid = false, + _treesitter = true, _cached_hls = { "path_colnr", "path_linenr" }, } @@ -620,51 +641,50 @@ M.defaults.lines = { prompt = "Lines> ", file_icons = true and M._has_devicons, color_icons = true, + show_bufname = 120, show_unloaded = true, show_unlisted = false, no_term_buffers = true, + sort_lastused = true, + winopts = { treesitter = true }, fzf_opts = { ["--multi"] = true, - ["--delimiter"] = "[\\]:]", - ["--nth"] = "2..", + ["--delimiter"] = "[\t]", + ["--tabstop"] = "1", ["--tiebreak"] = "index", + ["--with-nth"] = "2..", + ["--nth"] = "4..", + }, + line_field_index = "{4}", + field_index_expr = "{}", -- For `_fmt.from` to work with `bat_native` + _treesitter = true, + _cached_hls = { "buf_id", "buf_name", "buf_linenr" }, + _fmt = { + -- NOTE: `to` is not needed, we format at the source in `buffer_lines` + to = false, + from = function(s, _) + -- restore the format to something that `path.entry_to_file` can handle + local bufnr, lnum, text = s:match("%[(%d+)%].-(%d+) (.+)$") + if not bufnr then return "" end + return string.format("[%s]%s%s:%s:%s", + bufnr, utils.nbsp, + path.tail(vim.api.nvim_buf_get_name(tonumber(bufnr))), + lnum, text) + end }, - line_field_index = "{3}", _actions = function() return M.globals.actions.buffers or M.globals.actions.files end, - actions = { - ["enter"] = actions.buf_edit_or_qf, - ["alt-q"] = actions.buf_sel_to_qf, - ["alt-l"] = actions.buf_sel_to_ll - }, - _cached_hls = { "buf_name", "buf_nr", "path_linenr" }, } -M.defaults.blines = { - previewer = M._default_previewer_fn, - prompt = "BLines> ", - file_icons = false, - color_icons = false, - show_unlisted = true, - no_term_buffers = false, - fzf_opts = { - ["--multi"] = true, - ["--delimiter"] = "[:]", - ["--with-nth"] = "2..", - ["--tiebreak"] = "index", - }, - line_field_index = "{2}", - _actions = function() - return M.globals.actions.buffers or M.globals.actions.files - end, - actions = { - ["enter"] = actions.buf_edit_or_qf, - ["alt-q"] = actions.buf_sel_to_qf, - ["alt-l"] = actions.buf_sel_to_ll +M.defaults.blines = vim.tbl_deep_extend("force", M.defaults.lines, { + prompt = "BLines> ", + show_bufname = false, + fzf_opts = { + ["--with-nth"] = "4..", + ["--nth"] = "2..", }, - _cached_hls = { "buf_name", "buf_nr", "path_linenr" }, -} +}) M.defaults.treesitter = { previewer = M._default_previewer_fn, @@ -673,6 +693,7 @@ M.defaults.treesitter = { color_icons = false, fzf_opts = { ["--multi"] = true, + ["--tabstop"] = "4", ["--delimiter"] = "[:]", ["--with-nth"] = "2..", }, @@ -680,7 +701,13 @@ M.defaults.treesitter = { _actions = function() return M.globals.actions.buffers or M.globals.actions.files end, - _cached_hls = { "buf_name", "buf_nr", "path_linenr", "path_colnr" }, + _cached_hls = { "buf_name", "buf_nr", "buf_linenr", "path_colnr" }, + _fmt = { + to = false, + from = function(s, _) + return s:gsub("\t\t", ": ") + end + }, } M.defaults.tags = { @@ -735,9 +762,10 @@ M.defaults.colorschemes = { } M.defaults.highlights = { - prompt = "Highlights> ", - fzf_opts = { ["--no-multi"] = true }, - previewer = { _ctor = previewers.builtin.highlights, }, + prompt = "Highlights> ", + fzf_opts = { ["--no-multi"] = true }, + fzf_colors = { ["hl"] = "-1:reverse", ["hl+"] = "-1:reverse" }, + previewer = { _ctor = previewers.builtin.highlights, }, } M.defaults.awesome_colorschemes = { @@ -806,6 +834,7 @@ M.defaults.lsp = { fzf_opts = { ["--multi"] = true }, _actions = function() return M.globals.actions.files end, _cached_hls = { "path_colnr", "path_linenr" }, + _treesitter = true, -- Signals actions to use uri triggering the use of `lsp.util.show_document` _uri = true, } @@ -912,6 +941,7 @@ M.defaults.lsp.finder = { { "outgoing_calls", prefix = utils.ansi_codes.yellow("out ") }, }, fzf_opts = { ["--multi"] = true }, + _treesitter = true, _cached_hls = { "path_colnr", "path_linenr" }, _uri = true, } @@ -964,6 +994,7 @@ M.defaults.profiles = { fzf_opts = { ["--delimiter"] = "[:]", ["--with-nth"] = "-1..", + ["--tiebreak"] = "begin", ["--no-multi"] = true, }, actions = { ["enter"] = actions.apply_profile }, @@ -1189,7 +1220,9 @@ M.defaults.__HLS = { path_colnr = "FzfLuaPathColNr", path_linenr = "FzfLuaPathLineNr", buf_name = "FzfLuaBufName", + buf_id = "FzfLuaBufId", buf_nr = "FzfLuaBufNr", + buf_linenr = "FzfLuaBufLineNr", buf_flag_cur = "FzfLuaBufFlagCur", buf_flag_alt = "FzfLuaBufFlagAlt", tab_title = "FzfLuaTabTitle", diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index 69a4dfb7..db0bb73d 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -81,24 +81,26 @@ function M.setup_highlights(override) { default = default, fg = is_light and "MediumSpringGreen" or "BlanchedAlmond" } }, { "FzfLuaHeaderText", "header_text", { default = default, fg = is_light and "Brown4" or "Brown1" } }, - { "FzfLuaPathColNr", "path_colnr", -- lines|blines|qf|diag|lsp + { "FzfLuaPathColNr", "path_colnr", -- qf|diag|lsp { default = default, fg = is_light and "CadetBlue4" or "CadetBlue1" } }, - { "FzfLuaPathLineNr", "path_linenr", -- lines|blines|qf|diag|lsp + { "FzfLuaPathLineNr", "path_linenr", -- qf|diag|lsp { default = default, fg = is_light and "MediumSpringGreen" or "LightGreen" } }, - { "FzfLuaLiveSym", "live_sym", + { "FzfLuaLiveSym", "live_sym", -- lsp_live_workspace_symbols query { default = default, fg = is_light and "Brown4" or "Brown1" } }, - -- Provider specific highlights - { "FzfLuaBufName", "buf_name", -- lines|blines (hidden) - { default = default, fg = is_light and "DarkOrchid3" or "LightMagenta" } }, - { "FzfLuaBufNr", "buf_nr", -- buffers|tabs|lines|blines + -- lines|blines|treesitter + { "FzfLuaBufId", "buf_id", { default = default, link = "TabLine" } }, + { "FzfLuaBufName", "buf_name", { default = default, link = "Directory" } }, + { "FzfLuaBufLineNr", "buf_linenr", { default = default, link = "LineNr" } }, + -- buffers|tabs + { "FzfLuaBufNr", "buf_nr", { default = default, fg = is_light and "AquaMarine3" or "BlanchedAlmond" } }, - { "FzfLuaBufFlagCur", "buf_flag_cur", -- buffers|tabs + { "FzfLuaBufFlagCur", "buf_flag_cur", { default = default, fg = is_light and "Brown4" or "Brown1" } }, - { "FzfLuaBufFlagAlt", "buf_flag_alt", -- buffers|tabs + { "FzfLuaBufFlagAlt", "buf_flag_alt", { default = default, fg = is_light and "CadetBlue4" or "CadetBlue1" } }, - { "FzfLuaTabTitle", "tab_title", -- tabs + { "FzfLuaTabTitle", "tab_title", -- tabs only { default = default, fg = is_light and "CadetBlue4" or "LightSkyBlue1", bold = true } }, - { "FzfLuaTabMarker", "tab_marker", -- tabs + { "FzfLuaTabMarker", "tab_marker", -- tabs only { default = default, fg = is_light and "MediumSpringGreen" or "BlanchedAlmond", bold = true } }, -- highlight groups for `fzf_colors=true` { "FzfLuaFzfNormal", "fzf.normal", { default = default, link = "FzfLuaNormal" } }, diff --git a/lua/fzf-lua/lib/utf8.lua b/lua/fzf-lua/lib/utf8.lua new file mode 100644 index 00000000..d1348b80 --- /dev/null +++ b/lua/fzf-lua/lib/utf8.lua @@ -0,0 +1,1084 @@ +---@diagnostic disable: codestyle-check +-- https://gist.github.com/subsoap/05c02690956499a84708365337eb0a99 +-- https://github.com/Stepets/utf8.lua +-- $Id: utf8.lua 179 2009-04-03 18:10:03Z pasta $ +-- +-- Provides UTF-8 aware string functions implemented in pure lua: +-- * utf8len(s) +-- * utf8sub(s, i, j) +-- * utf8reverse(s) +-- * utf8char(unicode) +-- * utf8unicode(s, i, j) +-- * utf8gensub(s, sub_len) +-- * utf8find(str, regex, init, plain) +-- * utf8match(str, regex, init) +-- * utf8gmatch(str, regex, all) +-- * utf8gsub(str, regex, repl, limit) +-- +-- If utf8data.lua (containing the lower<->upper case mappings) is loaded, these +-- additional functions are available: +-- * utf8upper(s) +-- * utf8lower(s) +-- +-- All functions behave as their non UTF-8 aware counterparts with the exception +-- that UTF-8 characters are used instead of bytes for all units. + +--[[ +Copyright (c) 2006-2007, Kyle Smith +All rights reserved. + +Contributors: +Alimov Stepan + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +* Neither the name of the author nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--]] + +-- ABNF from RFC 3629 +-- +-- UTF8-octets = *( UTF8-char ) +-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4 +-- UTF8-1 = %x00-7F +-- UTF8-2 = %xC2-DF UTF8-tail +-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / +-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) +-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / +-- %xF4 %x80-8F 2( UTF8-tail ) +-- UTF8-tail = %x80-BF +-- + +local byte = string.byte +local char = string.char +local dump = string.dump +local find = string.find +local format = string.format +local len = string.len +local lower = string.lower +local rep = string.rep +local sub = string.sub +local upper = string.upper + +-- returns the number of bytes used by the UTF-8 character at byte i in s +-- also doubles as a UTF-8 character validator +local function utf8charbytes (s, i) + -- argument defaults + i = i or 1 + + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8charbytes' (string expected, got ".. type(s).. ")") + end + if type(i) ~= "number" then + error("bad argument #2 to 'utf8charbytes' (number expected, got ".. type(i).. ")") + end + + local c = byte(s, i) + + -- determine bytes needed for character, based on RFC 3629 + -- validate byte 1 + if c > 0 and c <= 127 then + -- UTF8-1 + return 1 + + elseif c >= 194 and c <= 223 then + -- UTF8-2 + local c2 = byte(s, i + 1) + + if not c2 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + return 2 + + elseif c >= 224 and c <= 239 then + -- UTF8-3 + local c2 = byte(s, i + 1) + local c3 = byte(s, i + 2) + + if not c2 or not c3 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c == 224 and (c2 < 160 or c2 > 191) then + error("Invalid UTF-8 character") + elseif c == 237 and (c2 < 128 or c2 > 159) then + error("Invalid UTF-8 character") + elseif c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 3 + if c3 < 128 or c3 > 191 then + error("Invalid UTF-8 character") + end + + return 3 + + elseif c >= 240 and c <= 244 then + -- UTF8-4 + local c2 = byte(s, i + 1) + local c3 = byte(s, i + 2) + local c4 = byte(s, i + 3) + + if not c2 or not c3 or not c4 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c == 240 and (c2 < 144 or c2 > 191) then + error("Invalid UTF-8 character") + elseif c == 244 and (c2 < 128 or c2 > 143) then + error("Invalid UTF-8 character") + elseif c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 3 + if c3 < 128 or c3 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 4 + if c4 < 128 or c4 > 191 then + error("Invalid UTF-8 character") + end + + return 4 + + else + error("Invalid UTF-8 character") + end +end + +-- returns the number of characters in a UTF-8 string +local function utf8len (s) + -- argument checking + if type(s) ~= "string" then + for k,v in pairs(s) do print('"',tostring(k),'"',tostring(v),'"') end + error("bad argument #1 to 'utf8len' (string expected, got ".. type(s).. ")") + end + + local pos = 1 + local bytes = len(s) + local length = 0 + + while pos <= bytes do + length = length + 1 + pos = pos + utf8charbytes(s, pos) + end + + return length +end + +-- functions identically to string.sub except that i and j are UTF-8 characters +-- instead of bytes +local function utf8sub (s, i, j) + -- argument defaults + j = j or -1 + + local pos = 1 + local bytes = len(s) + local length = 0 + + -- only set l if i or j is negative + local l = (i >= 0 and j >= 0) or utf8len(s) + local startChar = (i >= 0) and i or l + i + 1 + local endChar = (j >= 0) and j or l + j + 1 + + -- can't have start before end! + if startChar > endChar then + return "" + end + + -- byte offsets to pass to string.sub + local startByte,endByte = 1,bytes + + while pos <= bytes do + length = length + 1 + + if length == startChar then + startByte = pos + end + + pos = pos + utf8charbytes(s, pos) + + if length == endChar then + endByte = pos - 1 + break + end + end + + if startChar > length then startByte = bytes+1 end + if endChar < 1 then endByte = 0 end + + return sub(s, startByte, endByte) +end + +--[[ +-- replace UTF-8 characters based on a mapping table +local function utf8replace (s, mapping) + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8replace' (string expected, got ".. type(s).. ")") + end + if type(mapping) ~= "table" then + error("bad argument #2 to 'utf8replace' (table expected, got ".. type(mapping).. ")") + end + + local pos = 1 + local bytes = len(s) + local charbytes + local newstr = "" + + while pos <= bytes do + charbytes = utf8charbytes(s, pos) + local c = sub(s, pos, pos + charbytes - 1) + + newstr = newstr .. (mapping[c] or c) + + pos = pos + charbytes + end + + return newstr +end + + +-- identical to string.upper except it knows about unicode simple case conversions +local function utf8upper (s) + return utf8replace(s, utf8_lc_uc) +end + +-- identical to string.lower except it knows about unicode simple case conversions +local function utf8lower (s) + return utf8replace(s, utf8_uc_lc) +end +]] + +-- identical to string.reverse except that it supports UTF-8 +local function utf8reverse (s) + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8reverse' (string expected, got ".. type(s).. ")") + end + + local bytes = len(s) + local pos = bytes + local charbytes + local newstr = "" + + while pos > 0 do + local c = byte(s, pos) + while c >= 128 and c <= 191 do + pos = pos - 1 + c = byte(s, pos) + end + + charbytes = utf8charbytes(s, pos) + + newstr = newstr .. sub(s, pos, pos + charbytes - 1) + + pos = pos - 1 + end + + return newstr +end + +-- http://en.wikipedia.org/wiki/Utf8 +-- http://developer.coronalabs.com/code/utf-8-conversion-utility +local function utf8char(unicode) + if unicode <= 0x7F then return char(unicode) end + + if (unicode <= 0x7FF) then + local Byte0 = 0xC0 + math.floor(unicode / 0x40); + local Byte1 = 0x80 + (unicode % 0x40); + return char(Byte0, Byte1); + end; + + if (unicode <= 0xFFFF) then + local Byte0 = 0xE0 + math.floor(unicode / 0x1000); + local Byte1 = 0x80 + (math.floor(unicode / 0x40) % 0x40); + local Byte2 = 0x80 + (unicode % 0x40); + return char(Byte0, Byte1, Byte2); + end; + + if (unicode <= 0x10FFFF) then + local code = unicode + local Byte3= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte2= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte1= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte0= 0xF0 + code; + + return char(Byte0, Byte1, Byte2, Byte3); + end; + + error 'Unicode cannot be greater than U+10FFFF!' +end + +local shift_6 = 2^6 +local shift_12 = 2^12 +local shift_18 = 2^18 + +local utf8unicode +utf8unicode = function(str, i, j, byte_pos) + i = i or 1 + j = j or i + + if i > j then return end + + local ch,bytes + + if byte_pos then + bytes = utf8charbytes(str,byte_pos) + ch = sub(str,byte_pos,byte_pos-1+bytes) + else + ch,byte_pos = utf8sub(str,i,i), 0 + bytes = #ch + end + + local unicode + + if bytes == 1 then unicode = byte(ch) end + if bytes == 2 then + local byte0,byte1 = byte(ch,1,2) + local code0,code1 = byte0-0xC0,byte1-0x80 + unicode = code0*shift_6 + code1 + end + if bytes == 3 then + local byte0,byte1,byte2 = byte(ch,1,3) + local code0,code1,code2 = byte0-0xE0,byte1-0x80,byte2-0x80 + unicode = code0*shift_12 + code1*shift_6 + code2 + end + if bytes == 4 then + local byte0,byte1,byte2,byte3 = byte(ch,1,4) + local code0,code1,code2,code3 = byte0-0xF0,byte1-0x80,byte2-0x80,byte3-0x80 + unicode = code0*shift_18 + code1*shift_12 + code2*shift_6 + code3 + end + + return unicode,utf8unicode(str, i+1, j, byte_pos+bytes) +end + +-- Returns an iterator which returns the next substring and its byte interval +local function utf8gensub(str, sub_len) + sub_len = sub_len or 1 + local byte_pos = 1 + local length = #str + return function(skip) + if skip then byte_pos = byte_pos + skip end + local char_count = 0 + local start = byte_pos + repeat + if byte_pos > length then return end + char_count = char_count + 1 + local bytes = utf8charbytes(str,byte_pos) + byte_pos = byte_pos+bytes + + until char_count == sub_len + + local last = byte_pos-1 + local slice = sub(str,start,last) + return slice, start, last + end +end + +local function binsearch(sortedTable, item, comp) + local head, tail = 1, #sortedTable + local mid = math.floor((head + tail)/2) + if not comp then + while (tail - head) > 1 do + if sortedTable[tonumber(mid)] > item then + tail = mid + else + head = mid + end + mid = math.floor((head + tail)/2) + end + end + if sortedTable[tonumber(head)] == item then + return true, tonumber(head) + elseif sortedTable[tonumber(tail)] == item then + return true, tonumber(tail) + else + return false + end +end +local function classMatchGenerator(class, plain) + local codes = {} + local ranges = {} + local ignore = false + local range = false + local firstletter = true + local unmatch = false + + local it = utf8gensub(class) + + local skip + for c, _, be in it do + skip = be + if not ignore and not plain then + if c == "%" then + ignore = true + elseif c == "-" then + table.insert(codes, utf8unicode(c)) + range = true + elseif c == "^" then + if not firstletter then + error('!!!') + else + unmatch = true + end + elseif c == ']' then + break + else + if not range then + table.insert(codes, utf8unicode(c)) + else + table.remove(codes) -- removing '-' + table.insert(ranges, {table.remove(codes), utf8unicode(c)}) + range = false + end + end + elseif ignore and not plain then + if c == 'a' then -- %a: represents all letters. (ONLY ASCII) + table.insert(ranges, {65, 90}) -- A - Z + table.insert(ranges, {97, 122}) -- a - z + elseif c == 'c' then -- %c: represents all control characters. + table.insert(ranges, {0, 31}) + table.insert(codes, 127) + elseif c == 'd' then -- %d: represents all digits. + table.insert(ranges, {48, 57}) -- 0 - 9 + elseif c == 'g' then -- %g: represents all printable characters except space. + table.insert(ranges, {1, 8}) + table.insert(ranges, {14, 31}) + table.insert(ranges, {33, 132}) + table.insert(ranges, {134, 159}) + table.insert(ranges, {161, 5759}) + table.insert(ranges, {5761, 8191}) + table.insert(ranges, {8203, 8231}) + table.insert(ranges, {8234, 8238}) + table.insert(ranges, {8240, 8286}) + table.insert(ranges, {8288, 12287}) + elseif c == 'l' then -- %l: represents all lowercase letters. (ONLY ASCII) + table.insert(ranges, {97, 122}) -- a - z + elseif c == 'p' then -- %p: represents all punctuation characters. (ONLY ASCII) + table.insert(ranges, {33, 47}) + table.insert(ranges, {58, 64}) + table.insert(ranges, {91, 96}) + table.insert(ranges, {123, 126}) + elseif c == 's' then -- %s: represents all space characters. + table.insert(ranges, {9, 13}) + table.insert(codes, 32) + table.insert(codes, 133) + table.insert(codes, 160) + table.insert(codes, 5760) + table.insert(ranges, {8192, 8202}) + table.insert(codes, 8232) + table.insert(codes, 8233) + table.insert(codes, 8239) + table.insert(codes, 8287) + table.insert(codes, 12288) + elseif c == 'u' then -- %u: represents all uppercase letters. (ONLY ASCII) + table.insert(ranges, {65, 90}) -- A - Z + elseif c == 'w' then -- %w: represents all alphanumeric characters. (ONLY ASCII) + table.insert(ranges, {48, 57}) -- 0 - 9 + table.insert(ranges, {65, 90}) -- A - Z + table.insert(ranges, {97, 122}) -- a - z + elseif c == 'x' then -- %x: represents all hexadecimal digits. + table.insert(ranges, {48, 57}) -- 0 - 9 + table.insert(ranges, {65, 70}) -- A - F + table.insert(ranges, {97, 102}) -- a - f + else + if not range then + table.insert(codes, utf8unicode(c)) + else + table.remove(codes) -- removing '-' + table.insert(ranges, {table.remove(codes), utf8unicode(c)}) + range = false + end + end + ignore = false + else + if not range then + table.insert(codes, utf8unicode(c)) + else + table.remove(codes) -- removing '-' + table.insert(ranges, {table.remove(codes), utf8unicode(c)}) + range = false + end + ignore = false + end + + firstletter = false + end + + table.sort(codes) + + local function inRanges(charCode) + for _,r in ipairs(ranges) do + if r[1] <= charCode and charCode <= r[2] then + return true + end + end + return false + end + if not unmatch then + return function(charCode) + return binsearch(codes, charCode) or inRanges(charCode) + end, skip + else + return function(charCode) + return charCode ~= -1 and not (binsearch(codes, charCode) or inRanges(charCode)) + end, skip + end +end + +--[[ +-- utf8sub with extra argument, and extra result value +local function utf8subWithBytes (s, i, j, sb) + -- argument defaults + j = j or -1 + + local pos = sb or 1 + local bytes = len(s) + local length = 0 + + -- only set l if i or j is negative + local l = (i >= 0 and j >= 0) or utf8len(s) + local startChar = (i >= 0) and i or l + i + 1 + local endChar = (j >= 0) and j or l + j + 1 + + -- can't have start before end! + if startChar > endChar then + return "" + end + + -- byte offsets to pass to string.sub + local startByte,endByte = 1,bytes + + while pos <= bytes do + length = length + 1 + + if length == startChar then + startByte = pos + end + + pos = pos + utf8charbytes(s, pos) + + if length == endChar then + endByte = pos - 1 + break + end + end + + if startChar > length then startByte = bytes+1 end + if endChar < 1 then endByte = 0 end + + return sub(s, startByte, endByte), endByte + 1 +end +]] + +local cache = setmetatable({},{ + __mode = 'kv' +}) +local cachePlain = setmetatable({},{ + __mode = 'kv' +}) +local function matcherGenerator(regex, plain) + local matcher = { + functions = {}, + captures = {} + } + if not plain then + cache[regex] = matcher + else + cachePlain[regex] = matcher + end + local function simple(func) + return function(cC) + if func(cC) then + matcher:nextFunc() + matcher:nextStr() + else + matcher:reset() + end + end + end + local function star(func) + return function(cC) + if func(cC) then + matcher:fullResetOnNextFunc() + matcher:nextStr() + else + matcher:nextFunc() + end + end + end + local function minus(func) + return function(cC) + if func(cC) then + matcher:fullResetOnNextStr() + end + matcher:nextFunc() + end + end + local function question(func) + return function(cC) + if func(cC) then + matcher:fullResetOnNextFunc() + matcher:nextStr() + end + matcher:nextFunc() + end + end + + local function capture(id) + return function(_) + local l = matcher.captures[id][2] - matcher.captures[id][1] + local captured = utf8sub(matcher.string, matcher.captures[id][1], matcher.captures[id][2]) + local check = utf8sub(matcher.string, matcher.str, matcher.str + l) + if captured == check then + for _ = 0, l do + matcher:nextStr() + end + matcher:nextFunc() + else + matcher:reset() + end + end + end + local function captureStart(id) + return function(_) + matcher.captures[id][1] = matcher.str + matcher:nextFunc() + end + end + local function captureStop(id) + return function(_) + matcher.captures[id][2] = matcher.str - 1 + matcher:nextFunc() + end + end + + local function balancer(str) + local sum = 0 + local bc, ec = utf8sub(str, 1, 1), utf8sub(str, 2, 2) + local skip = len(bc) + len(ec) + bc, ec = utf8unicode(bc), utf8unicode(ec) + return function(cC) + if cC == ec and sum > 0 then + sum = sum - 1 + if sum == 0 then + matcher:nextFunc() + end + matcher:nextStr() + elseif cC == bc then + sum = sum + 1 + matcher:nextStr() + else + if sum == 0 or cC == -1 then + sum = 0 + matcher:reset() + else + matcher:nextStr() + end + end + end, skip + end + + matcher.functions[1] = function(_) + matcher:fullResetOnNextStr() + matcher.seqStart = matcher.str + matcher:nextFunc() + if (matcher.str > matcher.startStr and matcher.fromStart) or matcher.str >= matcher.stringLen then + matcher.stop = true + matcher.seqStart = nil + end + end + + local lastFunc + local ignore = false + local skip = nil + local it = (function() + local gen = utf8gensub(regex) + return function() + return gen(skip) + end + end)() + local cs = {} + for c, bs, be in it do + skip = nil + if plain then + table.insert(matcher.functions, simple(classMatchGenerator(c, plain))) + else + if ignore then + if find('123456789', c, 1, true) then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + table.insert(matcher.functions, capture(tonumber(c))) + elseif c == 'b' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + local b + b, skip = balancer(sub(regex, be + 1, be + 9)) + table.insert(matcher.functions, b) + else + lastFunc = classMatchGenerator('%' .. c) + end + ignore = false + else + if c == '*' then + if lastFunc then + table.insert(matcher.functions, star(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '+' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + table.insert(matcher.functions, star(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '-' then + if lastFunc then + table.insert(matcher.functions, minus(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '?' then + if lastFunc then + table.insert(matcher.functions, question(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '^' then + if bs == 1 then + matcher.fromStart = true + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '$' then + if be == len(regex) then + matcher.toEnd = true + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '[' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + lastFunc, skip = classMatchGenerator(sub(regex, be + 1)) + elseif c == '(' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + table.insert(matcher.captures, {}) + table.insert(cs, #matcher.captures) + table.insert(matcher.functions, captureStart(cs[#cs])) + if sub(regex, be + 1, be + 1) == ')' then matcher.captures[#matcher.captures].empty = true end + elseif c == ')' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + local cap = table.remove(cs) + if not cap then + error('invalid capture: "(" missing') + end + table.insert(matcher.functions, captureStop(cap)) + elseif c == '.' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + lastFunc = function(cC) return cC ~= -1 end + elseif c == '%' then + ignore = true + else + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + lastFunc = classMatchGenerator(c) + end + end + end + end + if #cs > 0 then + error('invalid capture: ")" missing') + end + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + + table.insert(matcher.functions, function() + if matcher.toEnd and matcher.str ~= matcher.stringLen then + matcher:reset() + else + matcher.stop = true + end + end) + + matcher.nextFunc = function(self) + self.func = self.func + 1 + end + matcher.nextStr = function(self) + self.str = self.str + 1 + end + matcher.strReset = function(self) + local oldReset = self.reset + local str = self.str + self.reset = function(s) + s.str = str + s.reset = oldReset + end + end + matcher.fullResetOnNextFunc = function(self) + local oldReset = self.reset + local func = self.func +1 + local str = self.str + self.reset = function(s) + s.func = func + s.str = str + s.reset = oldReset + end + end + matcher.fullResetOnNextStr = function(self) + local oldReset = self.reset + local str = self.str + 1 + local func = self.func + self.reset = function(s) + s.func = func + s.str = str + s.reset = oldReset + end + end + + matcher.process = function(self, str, start) + + self.func = 1 + start = start or 1 + self.startStr = (start >= 0) and start or utf8len(str) + start + 1 + self.seqStart = self.startStr + self.str = self.startStr + self.stringLen = utf8len(str) + 1 + self.string = str + self.stop = false + + self.reset = function(s) + s.func = 1 + end + + -- local lastPos = self.str + -- local lastByte + local ch + while not self.stop do + if self.str < self.stringLen then + --[[ if lastPos < self.str then + print('last byte', lastByte) + ch, lastByte = utf8subWithBytes(str, 1, self.str - lastPos - 1, lastByte) + ch, lastByte = utf8subWithBytes(str, 1, 1, lastByte) + lastByte = lastByte - 1 + else + ch, lastByte = utf8subWithBytes(str, self.str, self.str) + end + lastPos = self.str ]] + ch = utf8sub(str, self.str,self.str) + --print('char', ch, utf8unicode(ch)) + self.functions[self.func](utf8unicode(ch)) + else + self.functions[self.func](-1) + end + end + + if self.seqStart then + local captures = {} + for _,pair in pairs(self.captures) do + if pair.empty then + table.insert(captures, pair[1]) + else + table.insert(captures, utf8sub(str, pair[1], pair[2])) + end + end + return self.seqStart, self.str - 1, unpack(captures) + end +end + +return matcher +end + +-- string.find +local function utf8find(str, regex, init, plain) +local matcher = cache[regex] or matcherGenerator(regex, plain) +return matcher:process(str, init) +end + +-- string.match +local function utf8match(str, regex, init) +init = init or 1 +local found = {utf8find(str, regex, init)} +if found[1] then + if found[3] then + return unpack(found, 3) + end + return utf8sub(str, found[1], found[2]) +end +end + +-- string.gmatch +local function utf8gmatch(str, regex, all) +regex = (utf8sub(regex,1,1) ~= '^') and regex or '%' .. regex +local lastChar = 1 +return function() + local found = {utf8find(str, regex, lastChar)} + if found[1] then + lastChar = found[2] + 1 + if found[all and 1 or 3] then + return unpack(found, all and 1 or 3) + end + return utf8sub(str, found[1], found[2]) + end +end +end + +local function replace(repl, args) +local ret = '' +if type(repl) == 'string' then + local ignore = false + local num + for c in utf8gensub(repl) do + if not ignore then + if c == '%' then + ignore = true + else + ret = ret .. c + end + else + num = tonumber(c) + if num then + ret = ret .. args[num] + else + ret = ret .. c + end + ignore = false + end + end +elseif type(repl) == 'table' then + ret = repl[args[1] or args[0]] or '' +elseif type(repl) == 'function' then + if #args > 0 then + ret = repl(unpack(args, 1)) or '' + else + ret = repl(args[0]) or '' + end +end +return ret +end +-- string.gsub +local function utf8gsub(str, regex, repl, limit) +limit = limit or -1 +local ret = '' +local prevEnd = 1 +local it = utf8gmatch(str, regex, true) +local found = {it()} +local n = 0 +while #found > 0 and limit ~= n do + local args = {[0] = utf8sub(str, found[1], found[2]), unpack(found, 3)} + ret = ret .. utf8sub(str, prevEnd, found[1] - 1) + .. replace(repl, args) + prevEnd = found[2] + 1 + n = n + 1 + found = {it()} +end +return ret .. utf8sub(str, prevEnd), n +end + +-- EXPORT + +local M = {} + +function M.len(s) + return utf8len(s) +end +function M.sub(s, i, j) + return utf8sub(s, i, j) +end +function M.reverse(s) + return utf8reverse(s) +end +function M.char(unicode) + return utf8char(unicode) +end +function M.unicode(s, i, j) + return utf8unicode(s, i, j) +end +function M.gensub(s, sub_len) + return utf8gensub(s, sub_len) +end +function M.byte(s, i, j) + return utf8unicode(s, i, j) +end +function M.find(str, regex, init, plain) + return utf8find(str, regex, init, plain) +end +function M.match(str, regex, init) + return utf8match(str, regex, init) +end +function M.gmatch(str, regex, all) + return utf8gmatch(str, regex, all) +end +function M.gsub(str, regex, repl, limit) + return utf8gsub(str, regex, repl, limit) +end +function M.dump(s) + return dump(s) +end +function M.format(s) + return format(s) +end +function M.lower(s) + return lower(s) +end +function M.upper(s) + return upper(s) +end +function M.rep(s) + return rep(s) +end + +return M diff --git a/lua/fzf-lua/profiles/default-title.lua b/lua/fzf-lua/profiles/default-title.lua index 1fa07c71..ecac1d0f 100644 --- a/lua/fzf-lua/profiles/default-title.lua +++ b/lua/fzf-lua/profiles/default-title.lua @@ -18,6 +18,7 @@ return { blines = title("Buffer Lines"), treesitter = title("Treesitter"), grep = title("Grep", { prompt = "> " }), + grep_curbuf = title("Buffer Grep", { prompt = "> " }), git = { files = title("Git Files"), status = title("Git Status"), @@ -43,7 +44,7 @@ return { lsp = { title_prefix = "LSP", winopts = { title_pos = "center" }, - symbols = { title_prefix = "LSP", winopts = { title_pos = "center" } }, + symbols = { prompt = "> ", title_prefix = "LSP", winopts = { title_pos = "center" } }, finder = title("LSP Finder"), code_actions = title("Code Actions"), }, diff --git a/lua/fzf-lua/profiles/fzf-tmux.lua b/lua/fzf-lua/profiles/fzf-tmux.lua index cd31ffd9..c59b2adf 100644 --- a/lua/fzf-lua/profiles/fzf-tmux.lua +++ b/lua/fzf-lua/profiles/fzf-tmux.lua @@ -9,4 +9,6 @@ return { lsp = { code_actions = { previewer = "codeaction_native" } }, tags = { previewer = "bat" }, btags = { previewer = "bat" }, + lines = { ["winopts.treesitter"] = false }, + blines = { ["winopts.treesitter"] = false }, } diff --git a/lua/fzf-lua/profiles/telescope.lua b/lua/fzf-lua/profiles/telescope.lua index e99e8408..3e641614 100644 --- a/lua/fzf-lua/profiles/telescope.lua +++ b/lua/fzf-lua/profiles/telescope.lua @@ -50,7 +50,7 @@ return { ["hl+"] = { "fg", "TelescopeMatching" }, ["info"] = { "fg", "TelescopeMultiSelection" }, ["border"] = { "fg", "TelescopeBorder" }, - ["gutter"] = { "bg", "TelescopeNormal" }, + ["gutter"] = "-1", ["query"] = { "fg", "TelescopePromptNormal" }, ["prompt"] = { "fg", "TelescopePromptPrefix" }, ["pointer"] = { "fg", "TelescopeSelectionCaret" }, diff --git a/lua/fzf-lua/providers/buffers.lua b/lua/fzf-lua/providers/buffers.lua index e9f7d6a3..71504790 100644 --- a/lua/fzf-lua/providers/buffers.lua +++ b/lua/fzf-lua/providers/buffers.lua @@ -59,49 +59,58 @@ local filter_buffers = function(opts, unfiltered) return bufnrs, excluded, max_bufnr end + +local getbuf = function(buf) + return { + bufnr = buf, + flag = (buf == core.CTX().bufnr and "%") + or (buf == core.CTX().alt_bufnr and "#") or " ", + info = utils.getbufinfo(buf), + readonly = vim.bo[buf].readonly + } +end + +-- switching buffers and opening 'buffers' in quick succession +-- can lead to incorrect sort as 'lastused' isn't updated fast +-- enough (neovim bug?), this makes sure the current buffer is +-- always on top (#646) +-- Hopefully this gets solved before the year 2100 +-- DON'T FORCE ME TO UPDATE THIS HACK NEOVIM LOL +local _FUTURE = os.time({ year = 2100, month = 1, day = 1, hour = 0, minute = 00 }) +local get_unixtime = function(buf) + if tonumber(buf) then + -- When called from `buffer_lines` + buf = getbuf(buf) + end + if buf.flag == "%" then + return _FUTURE + elseif buf.flag == "#" then + return _FUTURE - 1 + else + return buf.info.lastused + end +end + local populate_buffer_entries = function(opts, bufnrs, winid) local buffers = {} for _, bufnr in ipairs(bufnrs) do - local flag = (bufnr == core.CTX().bufnr and "%") - or (bufnr == core.CTX().alt_bufnr and "#") or " " - - local element = { - bufnr = bufnr, - flag = flag, - info = utils.getbufinfo(bufnr), - readonly = vim.bo[bufnr].readonly - } + local buf = getbuf(bufnr) -- Get the name for missing/quickfix/location list buffers -- NOTE: we get it here due to `gen_buffer_entry` called within a fast event - if not element.info.name or #element.info.name == 0 then - element.info.name = utils.nvim_buf_get_name(element.bufnr, element.info) + if not buf.info.name or #buf.info.name == 0 then + buf.info.name = utils.nvim_buf_get_name(buf.bufnr, buf.info) end -- get the correct lnum for tabbed buffers if winid then - element.info.lnum = vim.api.nvim_win_get_cursor(winid)[1] + buf.info.lnum = vim.api.nvim_win_get_cursor(winid)[1] end - table.insert(buffers, element) + table.insert(buffers, buf) end + if opts.sort_lastused then - -- switching buffers and opening 'buffers' in quick succession - -- can lead to incorrect sort as 'lastused' isn't updated fast - -- enough (neovim bug?), this makes sure the current buffer is - -- always on top (#646) - -- Hopefully this gets solved before the year 2100 - -- DON'T FORCE ME TO UPDATE THIS HACK NEOVIM LOL - local future = os.time({ year = 2100, month = 1, day = 1, hour = 0, minute = 00 }) - local get_unixtime = function(buf) - if buf.flag == "%" then - return future - elseif buf.flag == "#" then - return future - 1 - else - return buf.info.lastused - end - end table.sort(buffers, function(a, b) return get_unixtime(a) > get_unixtime(b) end) @@ -221,9 +230,6 @@ end M.buffer_lines = function(opts) if not opts then return end - -- formatter doesn't work with lines|blines as only filename is displayed - opts._fmt = false - opts.fn_pre_fzf = function() core.CTX(true) end opts.fn_pre_fzf() @@ -242,10 +248,28 @@ M.buffer_lines = function(opts) local buffers = filter_buffers(opts, opts.current_buffer_only and { core.CTX().bufnr } or core.CTX().buflist) + if opts.sort_lastused and utils.tbl_count(buffers) > 1 then + table.sort(buffers, function(a, b) + return get_unixtime(a) > get_unixtime(b) + end) + end + + local bnames = {} + local longest_bname = 0 + for _, b in ipairs(buffers) do + local bname = utils.nvim_buf_get_name(b) + if not bname:match("^%[") then + bname = path.shorten(vim.fn.fnamemodify(bname, ":~:.")) + end + longest_bname = math.max(longest_bname, #bname) + bnames[tostring(b)] = bname + end + local len_bufnames = math.min(15, longest_bname) + for _, bufnr in ipairs(buffers) do local data = {} - local bufname, buficon, hl - -- use vim.schedule to avoid + + -- Use vim.schedule to avoid -- E5560: vimL function must not be called in a lua loop callback vim.schedule(function() local filepath = vim.api.nvim_buf_get_name(bufnr) @@ -254,22 +278,35 @@ M.buffer_lines = function(opts) elseif vim.fn.filereadable(filepath) ~= 0 then data = vim.fn.readfile(filepath, "") end - bufname = path.basename(filepath) - if opts.file_icons then - buficon, hl = devicons.get_devicon(bufname) - if hl and opts.color_icons then - buficon = utils.ansi_from_rgb(hl, buficon) - end - end - if not bufname or #bufname == 0 then - bufname = utils.nvim_buf_get_name(bufnr) - end coroutine.resume(co) end) -- wait for vim.schedule coroutine.yield() + local bname, bicon = (function() + if not opts.show_bufname + or tonumber(opts.show_bufname) and tonumber(opts.show_bufname) > vim.o.columns + then + return + end + local bicon, hl = "", nil + local bname = bnames[tostring(bufnr)] + assert(bname) + + if #bname > len_bufnames + 1 then + bname = "…" .. bname:sub(#bname - len_bufnames + 2) + end + + if opts.file_icons then + bicon, hl = devicons.get_devicon(bname) + if hl and opts.color_icons then + bicon = utils.ansi_from_rgb(hl, bicon) + end + end + return bname, bicon and bicon .. utils.nbsp or nil + end)() + local offset, lines = 0, #data if opts.current_buffer_only and opts.start == "cursor" then -- start display from current line and wrap from bottom (#822) @@ -281,14 +318,20 @@ M.buffer_lines = function(opts) if lnum > lines then lnum = lnum % lines end - add_entry(string.format("[%s]%s%s%s%s:%s: %s", - utils.ansi_codes[opts.hls.buf_nr](tostring(bufnr)), - utils.nbsp, - buficon or "", - buficon and utils.nbsp or "", - utils.ansi_codes[opts.hls.buf_name](bufname), - utils.ansi_codes[opts.hls.path_linenr](tostring(lnum)), - data[lnum]), co) + + -- NOTE: Space after `lnum` is U+00A0 (decimal: 160) + add_entry(string.format("[%s]\t%s\t%s%s\t%s \t%s", + tostring(bufnr), + utils.ansi_codes[opts.hls.buf_id](string.format("%3d", bufnr)), + bicon or "", + not bname and "" or utils.ansi_codes[opts.hls.buf_name](string.format( + "%" + .. (opts.file_icons and "-" or "") + .. tostring(len_bufnames) .. "s", + bname)), + utils.ansi_codes[opts.hls.buf_linenr](string.format("%5d", lnum)), + data[lnum] + ), co) end end cb(nil) @@ -450,11 +493,11 @@ M.treesitter = function(opts) local lnum, col, _, _ = vim.treesitter.get_node_range(node.node) local node_text = vim.treesitter.get_node_text(node.node, opts.bufnr) local node_kind = node.kind and utils.ansi_from_hl(kind2hl(node.kind), node.kind) - local entry = string.format("[%s]%s%s:%s:%s:\t[%s] %s", + local entry = string.format("[%s]%s%s:%s:%s\t\t[%s] %s", utils.ansi_codes[opts.hls.buf_nr](tostring(opts.bufnr)), utils.nbsp, utils.ansi_codes[opts.hls.buf_name](opts._bufname), - utils.ansi_codes[opts.hls.path_linenr](tostring(lnum + 1)), + utils.ansi_codes[opts.hls.buf_linenr](tostring(lnum + 1)), utils.ansi_codes[opts.hls.path_colnr](tostring(col + 1)), node_kind or "", node_text) diff --git a/lua/fzf-lua/providers/grep.lua b/lua/fzf-lua/providers/grep.lua index 4ed1fa33..17748a52 100644 --- a/lua/fzf-lua/providers/grep.lua +++ b/lua/fzf-lua/providers/grep.lua @@ -163,6 +163,10 @@ M.grep = function(opts) end local function normalize_live_grep_opts(opts) + -- disable treesitter as it collides with cmd regex highlighting + opts = opts or {} + opts._treesitter = false + opts = config.normalize_opts(opts, "grep") if not opts then return end @@ -407,27 +411,25 @@ M.grep_project = function(opts) end M.grep_curbuf = function(opts, lgrep) - if type(opts) == "function" then - opts = opts() - elseif not opts then - opts = {} - end - opts.filename = vim.api.nvim_buf_get_name(0) + -- call `normalize_opts` here as we want to store all previous + -- options in the resume data store under the key "bgrep" + -- 3rd arg is an override for resume data store lookup key + opts = config.normalize_opts(opts, "grep_curbuf", "bgrep") + if not opts then return end + + opts.filename = vim.api.nvim_buf_get_name(core.CTX().bufnr) if #opts.filename == 0 or not uv.fs_stat(opts.filename) then utils.info("Rg current buffer requires file on disk") return else opts.filename = path.relative_to(opts.filename, uv.cwd()) end - -- rg globs are meaningless here since we searching a single file - opts.rg_glob = false - opts.exec_empty_query = opts.exec_empty_query == nil and true - opts.fzf_opts = vim.tbl_extend("keep", opts.fzf_opts or {}, config.globals.blines.fzf_opts) - -- call `normalize_opts` here as we want to store all previous - -- options in the resume data store under the key "bgrep" - -- 3rd arg is an override for resume data store lookup key - opts = config.normalize_opts(opts, "grep", "bgrep") - if not opts then return end + + -- Persist call options so we don't revert to global grep on `grep_lgrep` + opts.__call_opts = vim.tbl_deep_extend("keep", + opts.__call_opts or {}, config.globals.grep_curbuf) + opts.__call_opts.filename = opts.filename + if lgrep then return M.live_grep(opts) else diff --git a/lua/fzf-lua/providers/helptags.lua b/lua/fzf-lua/providers/helptags.lua index 12f2a89a..3f30f26e 100644 --- a/lua/fzf-lua/providers/helptags.lua +++ b/lua/fzf-lua/providers/helptags.lua @@ -3,104 +3,101 @@ local core = require "fzf-lua.core" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" - local M = {} -local fzf_fn = function(cb) - local opts = {} - opts.lang = config.globals.helptags.lang or vim.o.helplang - opts.fallback = config.globals.helptags.fallback == nil and true - or config.globals.helptags.fallback +M.helptags = function(opts) + opts = config.normalize_opts(opts, "helptags") + if not opts then return end - local langs = vim.split(opts.lang, ",") - if opts.fallback and not utils.tbl_contains(langs, "en") then - table.insert(langs, "en") - end - local langs_map = {} - for _, lang in ipairs(langs) do - langs_map[lang] = true - end + local contents = function(cb) + opts.lang = opts.lang or vim.o.helplang + opts.fallback = opts.fallback ~= false and true - local tag_files = {} - local function add_tag_file(lang, file) - if langs_map[lang] then - if tag_files[lang] then - table.insert(tag_files[lang], file) - else - tag_files[lang] = { file } + local langs = vim.split(opts.lang, ",") + if opts.fallback and not utils.tbl_contains(langs, "en") then + table.insert(langs, "en") + end + local langs_map = {} + for _, lang in ipairs(langs) do + langs_map[lang] = true + end + + local tag_files = {} + local function add_tag_file(lang, file) + if langs_map[lang] then + if tag_files[lang] then + table.insert(tag_files[lang], file) + else + tag_files[lang] = { file } + end end end - end - local help_files = {} - local rtp = vim.o.runtimepath - -- If using lazy.nvim, get all the lazy loaded plugin paths (#1296) - local lazy = package.loaded["lazy.core.util"] - if lazy and lazy.get_unloaded_rtp then - local paths = lazy.get_unloaded_rtp("") - rtp = rtp .. "," .. table.concat(paths, ",") - end - local all_files = vim.fn.globpath(rtp, "doc/*", 1, 1) - for _, fullpath in ipairs(all_files) do - local file = path.tail(fullpath) - if file == "tags" then - add_tag_file("en", fullpath) - elseif file:match("^tags%-..$") then - local lang = file:sub(-2) - add_tag_file(lang, fullpath) - else - help_files[file] = fullpath + local help_files = {} + local rtp = vim.o.runtimepath + -- If using lazy.nvim, get all the lazy loaded plugin paths (#1296) + local lazy = package.loaded["lazy.core.util"] + if lazy and lazy.get_unloaded_rtp then + local paths = lazy.get_unloaded_rtp("") + rtp = rtp .. "," .. table.concat(paths, ",") + end + local all_files = vim.fn.globpath(rtp, "doc/*", 1, 1) + for _, fullpath in ipairs(all_files) do + local file = path.tail(fullpath) + if file == "tags" then + add_tag_file("en", fullpath) + elseif file:match("^tags%-..$") then + local lang = file:sub(-2) + add_tag_file(lang, fullpath) + else + help_files[file] = fullpath + end end - end - local hl = (function() - local _, _, fn = utils.ansi_from_hl("Label", "foo") - return function(s) return fn(s) end - end)() + local hl = (function() + local _, _, fn = utils.ansi_from_hl("Label", "foo") + return function(s) return fn(s) end + end)() - local add_tag = function(t, fzf_cb, co) - local tag = string.format("%-80s %s%s%s", hl(t.tag), t.filename, utils.nbsp, t.filepath) - fzf_cb(tag, function() - coroutine.resume(co) - end) - end + local add_tag = function(t, fzf_cb, co) + local tag = string.format("%-80s %s%s%s", hl(t.tag), t.filename, utils.nbsp, t.filepath) + fzf_cb(tag, function() + coroutine.resume(co) + end) + end - coroutine.wrap(function() - local co = coroutine.running() - local tags_map = {} - local delimiter = string.char(9) - for _, lang in ipairs(langs) do - for _, file in ipairs(tag_files[lang] or {}) do - local lines = vim.split(utils.read_file(file), "\n") - for _, line in ipairs(lines) do - -- TODO: also ignore tagComment starting with ';' - if not line:match "^!_TAG_" then - local fields = vim.split(line, delimiter) - if #fields == 3 and not tags_map[fields[1]] then - add_tag({ - tag = fields[1], - filename = fields[2], - filepath = help_files[fields[2]], - cmd = fields[3], - lang = lang, - }, cb, co) - tags_map[fields[1]] = true - -- pause here until we call coroutine.resume() - coroutine.yield() + coroutine.wrap(function() + local co = coroutine.running() + local tags_map = {} + local delimiter = string.char(9) + for _, lang in ipairs(langs) do + for _, file in ipairs(tag_files[lang] or {}) do + local lines = vim.split(utils.read_file(file), "\n") + for _, line in ipairs(lines) do + -- TODO: also ignore tagComment starting with ';' + if not line:match "^!_TAG_" then + local fields = vim.split(line, delimiter) + if #fields == 3 and not tags_map[fields[1]] then + add_tag({ + tag = fields[1], + filename = fields[2], + filepath = help_files[fields[2]], + cmd = fields[3], + lang = lang, + }, cb, co) + tags_map[fields[1]] = true + -- pause here until we call coroutine.resume() + coroutine.yield() + end end end end end - end - cb(nil) - end)() -end - + cb(nil) + end)() + end -M.helptags = function(opts) - opts = config.normalize_opts(opts, "helptags") - if not opts then return end - core.fzf_exec(fzf_fn, opts) + core.fzf_exec(contents, opts) end return M diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua index 2842ca7d..f2c00a3f 100644 --- a/lua/fzf-lua/win.lua +++ b/lua/fzf-lua/win.lua @@ -51,6 +51,7 @@ function TSInjector.clear_cache(buf, noassert) -- If called from fzf-tmux buf will be `nil` (#1556) if not buf then return end TSInjector.cache[buf] = nil + -- If called from `FzfWin.hide` cache will not be empty assert(noassert or utils.tbl_isempty(TSInjector.cache)) end @@ -121,20 +122,26 @@ local _preview_keymaps = { function FzfWin:setup_keybinds() if not self:validate() then return end - if not self.keymap or not self.keymap.builtin then return end - -- find the toggle_preview - if self.keymap.fzf then + 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()" }, + } + -- 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 + if self.winopts.split or not self.previewer_is_builtin and self.winopts.treesitter then for k, v in pairs(self.keymap.fzf) do if v == "toggle-preview" then self._fzf_toggle_prev_bind = utils.fzf_bind_to_neovim(k) + keymap_tbl = vim.tbl_deep_extend("keep", keymap_tbl, { + ["toggle-preview"] = { module = "win", fnc = "toggle_preview()" }, + }) end end end - local keymap_tbl = { - ["hide"] = { module = "win", fnc = "hide()" }, - ["toggle-help"] = { module = "win", fnc = "toggle_help()" }, - ["toggle-fullscreen"] = { module = "win", fnc = "toggle_fullscreen()" }, - } 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) @@ -497,6 +504,8 @@ function FzfWin:new(o) if _self and not _self:hidden() then -- utils.warn("Please close fzf-lua before starting a new instance") _self._reuse = true + -- refersh treesitter settings as new picker might have it disabled + _self._o.winopts.treesitter = o.winopts.treesitter return _self elseif _self and _self:hidden() then -- Clear the hidden buffers @@ -821,27 +830,67 @@ function FzfWin:set_winleave_autocmd() self:_nvim_create_autocmd("WinLeave", self.win_leave, [[require('fzf-lua.win').win_leave()]]) end +function FzfWin:treesitter_detach(buf, noassert) + TSInjector.clear_cache(buf, noassert) + TSInjector.deregister() +end + function FzfWin:treesitter_attach() if not utils.__HAS_NVIM_09 then return end if not self._o.winopts.treesitter then return end + -- local utf8 = require("fzf-lua.lib.utf8") local function trim(s) return (string.gsub(s, "^%s*(.-)%s*$", "%1")) end + local _format = type(self._o._treesitter) == "string" and self._o._treesitter or nil vim.api.nvim_buf_attach(self.fzf_bufnr, false, { - on_lines = function(_, bufnr, _, first_changed, last_changed, last_updated, bc) + on_lines = function(_, bufnr) local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) local regions = {} local empty_regions = {} + -- Adjust treesitter region based on the available main window width + -- otherwise the highlights may interfere with the fzf scrollbar or + -- the native fzf preview window + local min_col, max_col, trim_right = (function() + local min, max, tr = 0, nil, 4 + if not self.preview_hidden + and (not self.previewer_is_builtin or self.winopts.split) + then + local win_width = vim.api.nvim_win_get_width(self.fzf_winid) + local layout = self:fzf_preview_layout_str() + local percent = layout:match("(%d+)%%") or 50 + local prev_width = math.floor(win_width * percent / 100) + if layout:match("left") then + min = prev_width + else -- right + max = win_width - prev_width + end + end + return min, max, tr + end)() for i, line in ipairs(lines) do (function() -- Lines with code can be of the following formats: -- file:line:col:text (grep_xxx) -- file:line:text (grep_project or missing "--column" flag) -- line:col:text (grep_curbuf) - -- line:text (blines) - local filepath, _lnum, text = line:match("(.-):?(%d+):(.+)$") + -- linetext (lines|blines) + local filepath, _lnum, text = line:sub(min_col):match(_format or "(.-):?(%d+)[: ](.+)$") if not text or text == 0 then return end - filepath = trim(filepath) - local ft = #filepath == 0 and vim.bo[utils.CTX().bufnr].ft + text = text:gsub("^%d+:", "") -- remove col nr if exists + filepath = trim(filepath) -- trim spaces + + local ft_bufnr = (function() + -- blines|lines: U+00A0 (decimal: 160) follows the lnum + -- grep_curbuf: formats as line:col:text` thus `#filepath == 0` + if #filepath == 0 or string.byte(text, 1) == 160 then + if string.byte(text, 1) == 160 then text = text:sub(2) end -- remove A0+SPACE + if string.byte(text, 1) == 32 then text = text:sub(2) end -- remove leading SPACE + local b = filepath:match("^%d+") or utils.CTX().bufnr + return vim.api.nvim_buf_is_valid(tonumber(b)) and b or nil + end + end)() + + local ft = ft_bufnr and vim.bo[tonumber(ft_bufnr)].ft or vim.filetype.match({ filename = path.tail(filepath) }) if not ft then return end @@ -849,14 +898,20 @@ function FzfWin:treesitter_attach() local loaded = lang and utils.has_ts_parser(lang) if not loaded then return end - -- With the above line match text can start with "%d+:", remove it - text = text:gsub("^%d+:", "") - - local line_idx, text_pos = i - 1, #line - #text + -- NOTE: if the line contains unicode characters `#line > win_width` + -- as both `#str` and `string.len` count bytes and not characters + -- hence we trim 4 bytes from the right (for the scrollbar) except + -- when using native fzf previewer / split with left preview where + -- we use `max_col` instead (assuming our code isn't unicode) + local line_idx = i - 1 + local line_len = #line + local start_col = math.max(min_col, line_len - #text) + local end_col = max_col and math.min(max_col, line_len) or (line_len - trim_right) regions[lang] = regions[lang] or {} empty_regions[lang] = empty_regions[lang] or {} - table.insert(regions[lang], { { line_idx, text_pos, line_idx, line:len() } }) - -- print(lang, string.format("[%d]%d:%s", line_idx, _lnum, line:sub(text_pos + 1))) + table.insert(regions[lang], { { line_idx, start_col, line_idx, end_col } }) + -- print(lang, string.format("%d:%d [%d] %d:%s", + -- start_col, end_col, line_idx, _lnum, line:sub(start_col + 1, end_col))) end)() end TSInjector.attach(bufnr, empty_regions) @@ -924,6 +979,12 @@ function FzfWin:create() -- create a new tmp buffer for the fzf win self:set_tmp_buffer() self:setup_keybinds() + -- attach/detach treesitter (e.g. `grep_lgrep`) + if self._o.winopts.treesitter then + self:treesitter_attach() + else + self:treesitter_detach(self.fzf_bufnr) + end -- also recall the user's 'on_create' (#394) if self.winopts.on_create and type(self.winopts.on_create) == "function" then @@ -1051,8 +1112,7 @@ function FzfWin:close(fzf_bufnr) vim.api.nvim_buf_delete(self.fzf_bufnr, { force = true }) end -- Clear treesitter buffer cache and deregister decoration callbacks - TSInjector.clear_cache(self.fzf_bufnr, self._hidden_fzf_bufnr) - TSInjector.deregister() + self:treesitter_detach(self.fzf_bufnr, self._hidden_fzf_bufnr) -- when using `split = "belowright new"` closing the fzf -- window may not always return to the correct source win -- depending on the user's split configuration (#397) @@ -1375,9 +1435,14 @@ function FzfWin.toggle_preview() if not _self then return end local self = _self self.preview_hidden = not self.preview_hidden - if self.winopts.split and self._fzf_toggle_prev_bind then + if self._fzf_toggle_prev_bind then -- Toggle the empty preview window (under the neovim preview buffer) utils.feed_keys_termcodes(self._fzf_toggle_prev_bind) + -- This is just a proxy to toggle the native fzf preview when treesitter + -- is enabled, no need to redraw, stop here + if not self.previewer_is_builtin then + return + end end if self.preview_hidden and self:validate_preview() then self:close_preview(true)