-- Minimal fuzzy finder + content search for Neovim 0.11+ -- Optional: `fdfind` or `fd` for file listing, and `rg` (ripgrep) for text search. local Fuzzy = {} -------------------------------------------------------------------- -- 🧩 Helpers -------------------------------------------------------------------- -- Collect all files (try fdfind/fd first, then globpath) local function get_file_list() local handle = io.popen('fdfind --type f 2>/dev/null || fd --type f 2>/dev/null') if handle then local result = handle:read('*a') handle:close() if result and result ~= '' then return vim.split(result, '\n', { trimempty = true }) end end return vim.fn.globpath('.', '**/*', false, true) end -- Create floating input + result windows local function open_float(prompt) local input_buf = vim.api.nvim_create_buf(false, true) local result_buf = vim.api.nvim_create_buf(false, true) -- mark both buffers as scratch/unlisted for _, b in ipairs({ input_buf, result_buf }) do vim.bo[b].bufhidden = 'wipe' vim.bo[b].buflisted = false vim.bo[b].swapfile = false end vim.bo[input_buf].buftype = 'prompt' vim.bo[result_buf].buftype = 'nofile' local width = math.floor(vim.o.columns * 0.7) local height = 20 local row = math.floor((vim.o.lines - height) / 2) local col = math.floor((vim.o.columns - width) / 2) local input_win = vim.api.nvim_open_win(input_buf, true, { relative = 'editor', row = row, col = col, width = width, height = 1, style = 'minimal', border = 'rounded', }) vim.fn.prompt_setprompt(input_buf, prompt) local result_win = vim.api.nvim_open_win(result_buf, false, { relative = 'editor', row = row + 2, col = col, width = width, height = height - 2, style = 'minimal', border = 'single', }) return input_buf, result_buf, input_win, result_win end -------------------------------------------------------------------- -- 🔵 Highlight current selection -------------------------------------------------------------------- function Fuzzy:highlight_selection() if not self.result_buf then return end if not self.ns_id then self.ns_id = vim.api.nvim_create_namespace('FuzzyHighlight') end vim.api.nvim_buf_clear_namespace(self.result_buf, self.ns_id, 0, -1) if self.matches and self.matches[self.cursor] then local rel_cursor = self.cursor - (self.scroll or 0) if rel_cursor >= 1 and rel_cursor <= self.page_size then vim.api.nvim_buf_set_extmark(self.result_buf, self.ns_id, rel_cursor - 1, 0, { end_line = rel_cursor, hl_group = 'Visual', hl_eol = true, }) end end end -------------------------------------------------------------------- -- 🔴 Close all floating windows -------------------------------------------------------------------- function Fuzzy.close() local wins = { Fuzzy.input_win, Fuzzy.result_win } for _, win in ipairs(wins) do if win and vim.api.nvim_win_is_valid(win) then vim.api.nvim_win_close(win, true) end end Fuzzy.active = false end -------------------------------------------------------------------- -- 🟢 File finder -------------------------------------------------------------------- function Fuzzy.open() if Fuzzy.active then Fuzzy.close() end Fuzzy.active = true Fuzzy.files = get_file_list() Fuzzy.matches = Fuzzy.files Fuzzy.cursor = 1 Fuzzy.scroll = 0 Fuzzy.page_size = 50 Fuzzy.input_buf, Fuzzy.result_buf, Fuzzy.input_win, Fuzzy.result_win = open_float('Search: ') local function render_results() local total = #Fuzzy.matches if total == 0 then vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, { '-- no matches --' }) return end local start_idx = Fuzzy.scroll + 1 local end_idx = math.min(start_idx + Fuzzy.page_size - 1, total) local display = {} for i = start_idx, end_idx do display[#display + 1] = Fuzzy.matches[i] end vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, display) Fuzzy:highlight_selection() end local function update_results(text) if text == '' then Fuzzy.matches = Fuzzy.files else Fuzzy.matches = vim.fn.matchfuzzy(Fuzzy.files, text) end Fuzzy.cursor, Fuzzy.scroll = 1, 0 render_results() end vim.api.nvim_create_autocmd({ 'TextChangedI', 'TextChangedP' }, { buffer = Fuzzy.input_buf, callback = function() local text = vim.fn.getline('.'):gsub('^Search:%s*', '') update_results(text) end, }) vim.keymap.set('i', '', function() if Fuzzy.cursor < #Fuzzy.matches then Fuzzy.cursor = Fuzzy.cursor + 1 if Fuzzy.cursor > Fuzzy.scroll + Fuzzy.page_size then Fuzzy.scroll = Fuzzy.scroll + 1 end render_results() end end, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', function() if Fuzzy.cursor > 1 then Fuzzy.cursor = Fuzzy.cursor - 1 if Fuzzy.cursor <= Fuzzy.scroll then Fuzzy.scroll = math.max(Fuzzy.scroll - 1, 0) end render_results() end end, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', function() local choice = Fuzzy.matches[Fuzzy.cursor] if choice then Fuzzy.close() vim.cmd.edit(vim.fn.fnameescape(choice)) end end, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', Fuzzy.close, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', Fuzzy.close, { buffer = Fuzzy.input_buf }) vim.keymap.set('n', '', Fuzzy.close, { buffer = Fuzzy.input_buf }) vim.keymap.set('n', 'q', Fuzzy.close, { buffer = Fuzzy.input_buf }) vim.cmd.startinsert() end -------------------------------------------------------------------- -- 🟣 Ripgrep-based content search (scrolling + match highlighting) -------------------------------------------------------------------- function Fuzzy.open_grep() if Fuzzy.active then Fuzzy.close() end Fuzzy.active = true Fuzzy.input_buf, Fuzzy.result_buf, Fuzzy.input_win, Fuzzy.result_win = open_float('Grep: ') Fuzzy.matches, Fuzzy.cursor, Fuzzy.scroll = {}, 1, 0 Fuzzy.page_size = 50 Fuzzy.ns_id = vim.api.nvim_create_namespace('FuzzyHighlight') local function render_results(query) local total = #Fuzzy.matches if total == 0 then vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, { '-- no matches --' }) return end local start_idx = Fuzzy.scroll + 1 local end_idx = math.min(start_idx + Fuzzy.page_size - 1, total) local display = {} for i = start_idx, end_idx do display[#display + 1] = Fuzzy.matches[i] end vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, display) vim.api.nvim_buf_clear_namespace(Fuzzy.result_buf, Fuzzy.ns_id, 0, -1) -- highlight selection local rel_cursor = math.min(Fuzzy.cursor - Fuzzy.scroll, #display) vim.api.nvim_buf_set_extmark(Fuzzy.result_buf, Fuzzy.ns_id, rel_cursor - 1, 0, { end_line = rel_cursor, hl_group = 'Visual', hl_eol = true, }) -- highlight query matches if query and query ~= '' then local pattern = vim.pesc(query) for i, line in ipairs(display) do for s, e in line:gmatch('()' .. pattern .. '()') do vim.api.nvim_buf_set_extmark(Fuzzy.result_buf, Fuzzy.ns_id, i - 1, s - 1, { end_col = e - 1, hl_group = 'Search', }) end end end end local function run_grep(query) if query == '' then vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, { '-- type to search --' }) return end local handle = io.popen('rg --vimgrep --hidden --smart-case ' .. vim.fn.shellescape(query)) if not handle then return end local result = handle:read('*a') handle:close() Fuzzy.matches = vim.split(result, '\n', { trimempty = true }) Fuzzy.cursor, Fuzzy.scroll = 1, 0 render_results(query) end vim.api.nvim_create_autocmd({ 'TextChangedI', 'TextChangedP' }, { buffer = Fuzzy.input_buf, callback = function() local text = vim.fn.getline('.'):gsub('^Grep:%s*', '') run_grep(text) end, }) vim.keymap.set('i', '', function() if Fuzzy.cursor < #Fuzzy.matches then Fuzzy.cursor = Fuzzy.cursor + 1 if Fuzzy.cursor > Fuzzy.scroll + Fuzzy.page_size then Fuzzy.scroll = Fuzzy.scroll + 1 end local query = vim.fn.getline('.'):gsub('^Grep:%s*', '') render_results(query) end end, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', function() if Fuzzy.cursor > 1 then Fuzzy.cursor = Fuzzy.cursor - 1 if Fuzzy.cursor <= Fuzzy.scroll then Fuzzy.scroll = math.max(Fuzzy.scroll - 1, 0) end local query = vim.fn.getline('.'):gsub('^Grep:%s*', '') render_results(query) end end, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', function() local line = Fuzzy.matches[Fuzzy.cursor] if line then local parts = vim.split(line, ':') local file, lnum = parts[1], tonumber(parts[2]) or 1 Fuzzy.close() vim.cmd.edit(vim.fn.fnameescape(file)) vim.api.nvim_win_set_cursor(0, { lnum, 0 }) end end, { buffer = Fuzzy.input_buf }) vim.keymap.set('i', '', function() Fuzzy.close() end, { buffer = Fuzzy.input_buf }) vim.cmd.startinsert() end -------------------------------------------------------------------- -- 🧩 Commands & Keymaps -------------------------------------------------------------------- vim.api.nvim_create_user_command('FuzzyLive', function() Fuzzy.open() end, {}) vim.api.nvim_create_user_command('FuzzyGrep', function() Fuzzy.open_grep() end, {}) vim.keymap.set('n', 'f', function() vim.cmd.FuzzyLive() end, { desc = 'Open fuzzy file finder' }) vim.keymap.set('n', 'g', function() vim.cmd.FuzzyGrep() end, { desc = 'Search file contents with ripgrep' }) return Fuzzy