From a64612c293e5e15cc034dd81a185652153a3500c Mon Sep 17 00:00:00 2001 From: Ray Andrew Date: Wed, 19 Nov 2025 21:31:42 -0600 Subject: [PATCH] add emacs compile mode and find file --- home/neovim/config/.stylua.toml | 1 + home/neovim/config/init.lua | 216 +++-- home/neovim/config/lua/compile-mode.lua | 879 ++++++++++++++++++ .../{emacs-find-file.lua => find-file.lua} | 163 ++-- 4 files changed, 1078 insertions(+), 181 deletions(-) create mode 100644 home/neovim/config/lua/compile-mode.lua rename home/neovim/config/lua/{emacs-find-file.lua => find-file.lua} (77%) diff --git a/home/neovim/config/.stylua.toml b/home/neovim/config/.stylua.toml index 139e939..edfa506 100644 --- a/home/neovim/config/.stylua.toml +++ b/home/neovim/config/.stylua.toml @@ -4,3 +4,4 @@ indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferSingle" call_parentheses = "None" +collapse_simple_statement = "Always" diff --git a/home/neovim/config/init.lua b/home/neovim/config/init.lua index 2cf6bec..bdd3034 100644 --- a/home/neovim/config/init.lua +++ b/home/neovim/config/init.lua @@ -21,44 +21,47 @@ vim.opt.autoread = true vim.opt.cursorline = true vim.opt.scrolloff = 10 -local path_package = vim.fn.stdpath('data') .. '/site/' +local path_package = vim.fn.stdpath 'data' .. '/site/' local mini_path = path_package .. 'pack/deps/start/mini.nvim' if not vim.uv.fs_stat(mini_path) then - vim.cmd('echo "Installing mini.nvim" | redraw') - vim.fn.system({ - 'git', 'clone', '--filter=blob:none', - 'https://github.com/echasnovski/mini.nvim', mini_path - }) - vim.cmd('packadd mini.nvim | helptags ALL') + vim.cmd 'echo "Installing mini.nvim" | redraw' + vim.fn.system { + 'git', + 'clone', + '--filter=blob:none', + 'https://github.com/echasnovski/mini.nvim', + mini_path, + } + vim.cmd 'packadd mini.nvim | helptags ALL' end -require('mini.deps').setup({ path = { package = path_package } }) +require('mini.deps').setup { path = { package = path_package } } local add, now, later = MiniDeps.add, MiniDeps.now, MiniDeps.later -add('folke/snacks.nvim') -add('stevearc/quicker.nvim') -add('stevearc/oil.nvim') -add('A7Lavinraj/fyler.nvim') -add('EdenEast/nightfox.nvim') -add('williamboman/mason.nvim') -add('williamboman/mason-lspconfig.nvim') -add('neovim/nvim-lspconfig') -add('folke/lazydev.nvim') -add('Bilal2453/luvit-meta') -add('NeogitOrg/neogit') -add('nvim-lua/plenary.nvim') -add('sindrets/diffview.nvim') -add('folke/which-key.nvim') -add({ +add 'folke/snacks.nvim' +add 'stevearc/quicker.nvim' +add 'stevearc/oil.nvim' +add 'A7Lavinraj/fyler.nvim' +add 'EdenEast/nightfox.nvim' +add 'williamboman/mason.nvim' +add 'williamboman/mason-lspconfig.nvim' +add 'neovim/nvim-lspconfig' +add 'folke/lazydev.nvim' +add 'Bilal2453/luvit-meta' +add 'NeogitOrg/neogit' +add 'nvim-lua/plenary.nvim' +add 'sindrets/diffview.nvim' +add 'folke/which-key.nvim' +add { source = 'saghen/blink.cmp', checkout = 'v1.8.0', -}) -add('stevearc/conform.nvim') -add('coder/claudecode.nvim') +} +add 'stevearc/conform.nvim' +add 'coder/claudecode.nvim' now(function() - require('mini.icons').setup({ + require('mini.icons').setup { extension = { lua = { glyph = '󰢱', hl = 'MiniIconsAzure' }, }, @@ -69,13 +72,13 @@ now(function() file = { glyph = '󰈔', hl = 'MiniIconsGrey' }, directory = { glyph = '󰉋', hl = 'MiniIconsBlue' }, }, - }) - require('nightfox').setup({}) - vim.cmd('colorscheme nightfox') + } + require('nightfox').setup {} + vim.cmd 'colorscheme nightfox' end) later(function() - require('snacks').setup({ + require('snacks').setup { picker = { prompt = '󰍉 ', icons = { @@ -89,13 +92,13 @@ later(function() }, }, }, - }) + } - require('quicker').setup({ + require('quicker').setup { keys = { { '>', - function() require('quicker').expand({ before = 2, after = 2, add_to_existing = true }) end, + function() require('quicker').expand { before = 2, after = 2, add_to_existing = true } end, desc = 'Expand quickfix context', }, { @@ -104,7 +107,7 @@ later(function() desc = 'Collapse quickfix context', }, }, - }) + } -- Function to show current directory in oil winbar _G.get_oil_winbar = function() local bufnr = vim.api.nvim_win_get_buf(vim.g.statusline_winid or 0) @@ -116,7 +119,7 @@ later(function() end end - require('oil').setup({ + require('oil').setup { view_options = { show_hidden = true }, columns = { 'icon', @@ -154,7 +157,7 @@ later(function() -- Copy current directory path to clipboard ['gy'] = { callback = function() - local oil = require('oil') + local oil = require 'oil' local dir = oil.get_current_dir() if dir then vim.fn.setreg('+', dir) @@ -164,36 +167,41 @@ later(function() desc = 'Copy current directory path', }, }, - }) + } - require('fyler').setup({ + require('fyler').setup { views = { finder = { confirm_simple = true, close_on_select = false, }, }, - }) + } - require('emacs-find-file').setup({ + require('find-file').setup { max_matches = 8, - on_directory_enter = function(dir) - require('oil').open(dir) - end, - }) + on_directory_enter = function(dir) require('oil').open(dir) end, + } - require('neogit').setup({ + require('compile-mode').setup { + -- split_mode = "vertical-right", + on_exit = function(qf_list, _) + if #qf_list > 0 then require('quicker').open { focus = false } end + end, + } + + require('neogit').setup { integrations = { diffview = true, snacks = true, }, - }) + } - require('diffview').setup({ + require('diffview').setup { enhanced_diff_hl = true, - }) + } - require('which-key').setup({ + require('which-key').setup { preset = 'modern', icons = { separator = '→', @@ -202,11 +210,13 @@ later(function() win = { border = 'rounded', }, - }) + } - require('claudecode').setup({}) + require('claudecode').setup {} +end) - require('conform').setup({ +later(function() + require('conform').setup { formatters_by_ft = { lua = { 'stylua' }, python = { 'isort', 'black' }, @@ -223,25 +233,24 @@ later(function() c = { 'clang_format' }, cpp = { 'clang_format' }, }, - }) + } - -- Format command - vim.api.nvim_create_user_command('Format', function(_) - require('conform').format({ async = true, lsp_format = 'fallback' }) - end, { desc = 'Format current buffer' }) + vim.api.nvim_create_user_command( + 'Format', + function(_) require('conform').format { async = true, lsp_format = 'fallback' } end, + { desc = 'Format current buffer' } + ) end) later(function() local has_words_before = function() local col = vim.api.nvim_win_get_cursor(0)[2] - if col == 0 then - return false - end + if col == 0 then return false end local line = vim.api.nvim_get_current_line() - return line:sub(col, col):match("%s") == nil + return line:sub(col, col):match '%s' == nil end - require('blink.cmp').setup({ + require('blink.cmp').setup { keymap = { preset = 'none', [''] = { 'show', 'show_documentation', 'hide_documentation' }, @@ -249,9 +258,7 @@ later(function() [''] = { 'accept', 'fallback' }, [''] = { function(cmp) - if has_words_before() then - return cmp.insert_next() - end + if has_words_before() then return cmp.insert_next() end end, 'fallback', }, @@ -293,11 +300,11 @@ later(function() border = 'rounded', }, }, - }) + } end) later(function() - require('mason').setup({ + require('mason').setup { ui = { border = 'rounded', icons = { @@ -306,17 +313,28 @@ later(function() package_uninstalled = '✗', }, }, - }) + } - require('mason-lspconfig').setup({ + require('mason-lspconfig').setup { ensure_installed = { - 'lua_ls', -- Lua - 'pyright', -- Python + 'lua_ls', -- Lua + 'pyright', -- Python 'rust_analyzer', -- Rust - 'clangd', -- C/C++ + 'clangd', -- C/C++ }, automatic_installation = true, - }) + } + + -- Ensure formatters are installed + local mason_registry = require 'mason-registry' + local formatters = { + 'stylua', + } + + for _, formatter in ipairs(formatters) do + local package = mason_registry.get_package(formatter) + if not package:is_installed() then package:install() end + end -- LSP keybindings and options vim.api.nvim_create_autocmd('LspAttach', { @@ -327,7 +345,12 @@ later(function() vim.keymap.set('n', 'gD', function() require('snacks').picker.lsp_declarations() end, vim.tbl_extend('force', opts, { desc = 'Goto Declaration' })) vim.keymap.set('n', 'gr', function() require('snacks').picker.lsp_references() end, vim.tbl_extend('force', opts, { desc = 'References', nowait = true })) vim.keymap.set('n', 'gI', function() require('snacks').picker.lsp_implementations() end, vim.tbl_extend('force', opts, { desc = 'Goto Implementation' })) - vim.keymap.set('n', 'gy', function() require('snacks').picker.lsp_type_definitions() end, vim.tbl_extend('force', opts, { desc = 'Goto T[y]pe Definition' })) + vim.keymap.set( + 'n', + 'gy', + function() require('snacks').picker.lsp_type_definitions() end, + vim.tbl_extend('force', opts, { desc = 'Goto T[y]pe Definition' }) + ) vim.keymap.set('n', 'gai', function() require('snacks').picker.lsp_incoming_calls() end, vim.tbl_extend('force', opts, { desc = 'C[a]lls Incoming' })) vim.keymap.set('n', 'gao', function() require('snacks').picker.lsp_outgoing_calls() end, vim.tbl_extend('force', opts, { desc = 'C[a]lls Outgoing' })) vim.keymap.set('n', 'K', vim.lsp.buf.hover, vim.tbl_extend('force', opts, { desc = 'Hover documentation (press K again to focus)' })) @@ -341,7 +364,7 @@ later(function() }) -- Diagnostic UI improvements - vim.diagnostic.config({ + vim.diagnostic.config { virtual_text = true, signs = true, underline = true, @@ -351,7 +374,7 @@ later(function() border = 'rounded', source = 'always', }, - }) + } -- Add borders to hover windows local orig_util_open_floating_preview = vim.lsp.util.open_floating_preview @@ -362,11 +385,11 @@ later(function() end -- Setup lazydev for better plugin type support - require('lazydev').setup({ + require('lazydev').setup { library = { { path = 'luvit-meta/library', words = { 'vim%.uv' } }, }, - }) + } -- Setup language servers using vim.lsp.config -- Lua @@ -390,7 +413,7 @@ later(function() library = { vim.env.VIMRUNTIME, '${3rd}/luv/library', - vim.fn.stdpath('data') .. '/site/pack/deps/start', + vim.fn.stdpath 'data' .. '/site/pack/deps/start', }, checkThirdParty = false, }, @@ -427,18 +450,17 @@ later(function() } -- Enable language servers - vim.lsp.enable({ 'lua_ls', 'ts_ls', 'pyright', 'rust_analyzer', 'clangd' }) + vim.lsp.enable { 'lua_ls', 'ts_ls', 'pyright', 'rust_analyzer', 'clangd' } end) - local map = function(mode, lhs, rhs, opts) if type(opts) == 'string' then opts = { desc = opts } end require('snacks').keymap.set(mode, lhs, rhs, opts) end -map('n', 'ex', function() require('fyler').toggle({ kind = 'split_left_most' }) end, 'Toggle file tree') +map('n', 'ex', function() require('fyler').toggle { kind = 'split_left_most' } end, 'Toggle file tree') map('n', 'fs', 'w', 'Save file') -map('n', 'ff', function() require('emacs-find-file').open() end, 'Find file (emacs-style)') +map('n', 'ff', function() require('find-file').open() end, 'Find file (emacs-style)') map('n', 'bb', function() require('snacks').picker.buffers() end, 'List buffers') map('n', 'bd', function() require('snacks').bufdelete() end, 'Buffer delete') map('n', '.', function() require('snacks').scratch() end, 'Scratch buffer') @@ -449,8 +471,10 @@ map('n', 'ls', function() require('snacks').picker.lsp_symbols() end, 'L map('n', 'lS', function() require('snacks').picker.lsp_workspace_symbols() end, 'LSP Workspace Symbols') map('n', 'zz', function() require('snacks').zen() end, 'Toggle zen mode') map('n', 'zm', function() require('snacks').zen.zoom() end, 'Toggle Zoom') -map('n', 'qq', function() require('quicker').open({ focus = true }) end, 'Open/focus quickfix') -map('n', 'ql', function() require('quicker').toggle({ loclist = true }) end, 'Toggle loclist') +map('n', 'qq', function() require('quicker').open { focus = true } end, 'Open/focus quickfix') +map('n', 'ql', function() require('quicker').toggle { loclist = true } end, 'Toggle loclist') +map('n', 'cc', function() require('compile-mode').compile() end, 'Compile') +map('n', 'cC', function() require('compile-mode').recompile() end, 'Recompile') map('n', 'cr', 'source $MYVIMRC', 'Reload config') map('n', 'ce', 'edit $MYVIMRC', 'Edit config') map('n', 'ch', 'checkhealth', 'Check health') @@ -472,9 +496,7 @@ map('n', 'ad', 'ClaudeCodeDiffDeny', 'Deny diff') -- File tree specific keymap for Claude Code vim.api.nvim_create_autocmd('FileType', { pattern = { 'oil' }, - callback = function() - vim.keymap.set('n', 'as', 'ClaudeCodeTreeAdd', { buffer = true, desc = 'Add file to Claude' }) - end, + callback = function() vim.keymap.set('n', 'as', 'ClaudeCodeTreeAdd', { buffer = true, desc = 'Add file to Claude' }) end, }) map('n', 'wh', 'h', 'Window left') @@ -485,13 +507,19 @@ map('n', 'ws', 'split', 'Split window horizontally') map('n', 'wv', 'vsplit', 'Split window vertically') map('n', 'wq', 'q', 'Close window') +-- Terminal mode keymap to unfocus and return to previous window +vim.keymap.set('t', '', 'p', { desc = 'Unfocus terminal and return to editor' }) + vim.api.nvim_create_autocmd('QuickFixCmdPost', { callback = function() - vim.cmd('redraw!') - vim.schedule(function() - require('quicker').open({ focus = true }) - end) + vim.cmd 'redraw!' + vim.schedule(function() require('quicker').open { focus = true } end) end, }) -vim.opt.shortmess:append('c') +vim.opt.shortmess:append 'c' + +vim.api.nvim_create_autocmd({ 'FocusGained', 'BufEnter' }, { + command = "if mode() != 'c' | checktime | endif", + pattern = '*', +}) diff --git a/home/neovim/config/lua/compile-mode.lua b/home/neovim/config/lua/compile-mode.lua new file mode 100644 index 0000000..39b6426 --- /dev/null +++ b/home/neovim/config/lua/compile-mode.lua @@ -0,0 +1,879 @@ +--- Emacs-style compile mode for Neovim +--- @class CompileMode +local M = {} + +--- @type string? +local last_command = nil + +--- @type string[] +local command_history = {} + +local HISTORY_FILE = vim.fn.stdpath 'data' .. '/compile_history' +local MAX_HISTORY_SIZE = 200 + +--- @class CompileConfig +--- @field on_success? fun(qf_list: table) +--- @field on_error? fun(qf_list: table, exit_code: number) +--- @field on_exit? fun(qf_list: table, exit_code: number) +--- @field keymaps? table +--- @field split_mode? 'auto'|'horizontal'|'vertical-right'|'vertical-left' +--- @field min_width_for_vertical? number +--- @field prompt? string + +--- @type CompileConfig +local config = { + on_success = nil, + on_error = nil, + on_exit = nil, + keymaps = { + close = 'q', + recompile = 'g', + }, + split_mode = 'auto', + min_width_for_vertical = 160, + prompt = 'Compile command', +} + +local function load_history() + local file = io.open(HISTORY_FILE, 'r') + if not file then return end + + command_history = {} + for line in file:lines() do + if line ~= '' then table.insert(command_history, line) end + end + file:close() +end + +local function save_history() + local seen = {} + local deduplicated = {} + + for i = #command_history, 1, -1 do + local cmd = command_history[i] + if not seen[cmd] then + seen[cmd] = true + table.insert(deduplicated, 1, cmd) + end + end + + local start_idx = math.max(1, #deduplicated - MAX_HISTORY_SIZE + 1) + local trimmed = {} + for i = start_idx, #deduplicated do + table.insert(trimmed, deduplicated[i]) + end + + local file = io.open(HISTORY_FILE, 'w') + if not file then + vim.notify('Failed to save compile history', vim.log.levels.WARN) + return + end + + for _, cmd in ipairs(trimmed) do + file:write(cmd .. '\n') + end + file:close() + + command_history = trimmed +end + +--- @param cmd string +local function append_to_history(cmd) + if not cmd or cmd == '' then return end + + table.insert(command_history, cmd) + save_history() +end + +--- Determine if we should use vertical split layout +--- @return boolean +local function should_use_vertical_split() + if config.split_mode == 'vertical-right' or config.split_mode == 'vertical-left' then + return true + elseif config.split_mode == 'horizontal' then + return false + else -- 'auto' + return vim.o.columns >= (config.min_width_for_vertical or 160) + end +end + +--- Create a vertical split on left or right side with two stacked windows +--- @param side 'left'|'right' +--- @param qf_bufnr number +--- @param comp_bufnr number +--- @return number qf_win, number comp_win +local function create_stacked_vertical_split(side, qf_bufnr, comp_bufnr) + local cmd = side == 'left' and 'topleft vertical split' or 'botright vertical split' + + vim.cmd(cmd) + local qf_win = vim.api.nvim_get_current_win() + + vim.api.nvim_win_set_buf(qf_win, qf_bufnr) + vim.bo[qf_bufnr].buftype = 'quickfix' + + vim.cmd 'belowright split' + local comp_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(comp_win, comp_bufnr) + vim.api.nvim_set_option_value('winfixbuf', true, { win = comp_win }) + + local qf_height = vim.api.nvim_win_get_height(qf_win) + local comp_height = vim.api.nvim_win_get_height(comp_win) + local total_height = qf_height + comp_height + local half_height = math.floor(total_height / 2) + + vim.api.nvim_win_set_height(qf_win, half_height) + vim.api.nvim_win_set_height(comp_win, half_height) + + vim.api.nvim_set_current_win(qf_win) + + return qf_win, comp_win +end + +--- Create compilation window on left or right side +--- @param side 'left'|'right' +--- @param bufnr number +--- @param qf_winid number? +--- @return number win +local function create_vertical_compilation_window(side, bufnr, qf_winid) + local has_qf = qf_winid and qf_winid ~= 0 and vim.api.nvim_win_is_valid(qf_winid) + + if has_qf and qf_winid then + vim.api.nvim_set_current_win(qf_winid) + vim.cmd 'belowright split' + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, bufnr) + vim.api.nvim_set_option_value('winfixbuf', true, { win = win }) + return win + else + local cmd = side == 'left' and 'topleft vertical split' or 'botright vertical split' + vim.cmd(cmd) + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, bufnr) + vim.api.nvim_set_option_value('winfixbuf', true, { win = win }) + return win + end +end + +--- @param cmd string +local function run_compile(cmd) + if not cmd or cmd == '' then + vim.notify('No compile command provided', vim.log.levels.WARN) + return + end + + last_command = cmd + vim.fn.setqflist({}, 'r') + + local bufnr = vim.fn.bufnr '*compilation*' + if bufnr == -1 then + bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, '*compilation*') + vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr }) + vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = bufnr }) + vim.api.nvim_set_option_value('swapfile', false, { buf = bufnr }) + vim.api.nvim_set_option_value('buflisted', false, { buf = bufnr }) + vim.api.nvim_set_option_value('filetype', 'compilation', { buf = bufnr }) + end + + vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr }) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {}) + + local use_vertical = should_use_vertical_split() + + local win = vim.fn.bufwinid(bufnr) + if win == -1 then + local qf_info = vim.fn.getqflist { winid = 0 } + local qf_winid = qf_info.winid + local has_qf = qf_winid ~= 0 and vim.api.nvim_win_is_valid(qf_winid) + + if config.split_mode == 'vertical-right' then + win = create_vertical_compilation_window('right', bufnr, qf_winid) + elseif config.split_mode == 'vertical-left' then + win = create_vertical_compilation_window('left', bufnr, qf_winid) + elseif has_qf and use_vertical then + vim.api.nvim_set_current_win(qf_winid) + vim.cmd 'rightbelow vertical split' + win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, bufnr) + vim.api.nvim_set_option_value('winfixbuf', true, { win = win }) + else + vim.cmd 'botright split' + win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, bufnr) + vim.api.nvim_win_set_height(win, 15) + vim.api.nvim_set_option_value('winfixbuf', true, { win = win }) + end + end + + vim.api.nvim_set_current_win(win) + + local start_time = vim.uv.hrtime() + local start_ms = math.floor((start_time % 1000000000) / 1000000) + local header = { + '-*- mode: compilation; default-directory: "' .. vim.fn.getcwd() .. '" -*-', + 'Compilation started at ' .. os.date '%Y-%m-%d %H:%M:%S' .. '.' .. string.format('%03d', start_ms), + '', + cmd, + '', + } + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, header) + + local output_lines = {} + local line_to_qf = {} + + local function append_output(data) + if not data then return end + for _, line in ipairs(data) do + if line ~= '' then + table.insert(output_lines, line) + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line }) + local buf_line = vim.api.nvim_buf_line_count(bufnr) + line_to_qf[buf_line] = #output_lines + end + end + end + + local job_id = vim.fn.jobstart(cmd, { + stdout_buffered = false, + stderr_buffered = false, + on_stdout = function(_, data) append_output(data) end, + on_stderr = function(_, data) append_output(data) end, + on_exit = function(_, exit_code) + local end_time = vim.uv.hrtime() + local end_ms = math.floor((end_time % 1000000000) / 1000000) + local footer = { + '', + 'Compilation ' + .. (exit_code == 0 and 'finished' or 'exited abnormally with code ' .. exit_code) + .. ' at ' + .. os.date '%Y-%m-%d %H:%M:%S' + .. '.' + .. string.format('%03d', end_ms), + } + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, footer) + vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) + + local valid_entries = {} + if #output_lines > 0 then + vim.fn.setqflist({}, 'r', { + lines = output_lines, + }) + local qf_list = vim.fn.getqflist() + valid_entries = vim.tbl_filter(function(item) return item.valid == 1 end, qf_list) + + vim.b[bufnr].line_to_qf = line_to_qf + + vim.schedule(function() + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buftype == 'quickfix' then + local function qf_jump() + local line = vim.api.nvim_win_get_cursor(0)[1] + local qf_entries = vim.fn.getqflist() + + if line <= #qf_entries then + local entry = qf_entries[line] + + if entry.valid == 1 and entry.bufnr > 0 then + vim.cmd('cc ' .. line) + else + for i = line - 1, 1, -1 do + if qf_entries[i].valid == 1 and qf_entries[i].bufnr > 0 then + vim.cmd('cc ' .. i) + return + end + end + for i = line + 1, #qf_entries do + if qf_entries[i].valid == 1 and qf_entries[i].bufnr > 0 then + vim.cmd('cc ' .. i) + return + end + end + end + end + end + + vim.keymap.set('n', '', qf_jump, { buffer = buf, desc = 'Jump to error location' }) + vim.keymap.set('n', '<2-LeftMouse>', qf_jump, { buffer = buf, desc = 'Jump to error location' }) + end + end + end) + end + + vim.schedule(function() + if exit_code == 0 then + vim.notify('Compilation finished successfully', vim.log.levels.INFO) + if config.on_success then config.on_success(valid_entries) end + else + vim.notify('Compilation failed with code ' .. exit_code, vim.log.levels.ERROR) + if config.on_error then config.on_error(valid_entries, exit_code) end + end + + if config.on_exit then config.on_exit(valid_entries, exit_code) end + + vim.schedule(function() + local comp_win = vim.fn.bufwinid(bufnr) + local qf_info = vim.fn.getqflist { winid = 0 } + local qf_winid = qf_info.winid + local has_qf = qf_winid ~= 0 and vim.api.nvim_win_is_valid(qf_winid) + local has_both = comp_win ~= -1 and has_qf + + if exit_code == 0 and has_qf then + vim.cmd 'cclose' + elseif has_both then + if config.split_mode == 'vertical-right' or config.split_mode == 'vertical-left' then + local side = config.split_mode == 'vertical-right' and 'right' or 'left' + local qf_bufnr = vim.fn.getqflist({ qfbufnr = 0 }).qfbufnr + + vim.api.nvim_win_close(comp_win, false) + vim.api.nvim_win_close(qf_winid, false) + + create_stacked_vertical_split(side, qf_bufnr, bufnr) + elseif should_use_vertical_split() then + local comp_config = vim.api.nvim_win_get_config(comp_win) + local qf_config = vim.api.nvim_win_get_config(qf_winid) + if comp_config.relative == '' and qf_config.relative == '' then + vim.api.nvim_win_close(comp_win, false) + + vim.api.nvim_set_current_win(qf_winid) + vim.cmd 'rightbelow vertical split' + local new_comp_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(new_comp_win, bufnr) + vim.api.nvim_set_option_value('winfixbuf', true, { win = new_comp_win }) + + local target_height = math.max(20, math.floor(vim.o.lines / 3)) + vim.api.nvim_win_set_height(qf_winid, target_height) + vim.api.nvim_win_set_height(new_comp_win, target_height) + + vim.api.nvim_set_current_win(qf_winid) + end + else + vim.api.nvim_set_current_win(qf_winid) + end + elseif has_qf then + vim.api.nvim_set_current_win(qf_winid) + end + end) + end) + end, + }) + + if job_id <= 0 then vim.notify('Failed to start compilation', vim.log.levels.ERROR) end +end + +--- Get matches for autocomplete (commands and files) +--- @param input string +--- @return string[] +local function get_matches(input) + local words = vim.split(input, '%s+') + local last_word = words[#words] or '' + + if last_word == '' then return {} end + + local matches = {} + + local is_path = last_word:match '[/.]' or last_word:match '^~' + + if is_path then + local expanded = vim.fn.expand(last_word) + local dir = vim.fn.fnamemodify(expanded, ':h') + local filter = vim.fn.fnamemodify(expanded, ':t') + + if vim.fn.isdirectory(dir) == 1 then + local handle = vim.uv.fs_scandir(dir) + if handle then + while true do + local name, type = vim.uv.fs_scandir_next(handle) + if not name then break end + + if name ~= '.' and name ~= '..' then + if filter == '' or name:lower():find(filter:lower(), 1, true) then + local sep = dir:match '/$' and '' or '/' + local full_path = dir .. sep .. name + if type == 'directory' then full_path = full_path .. '/' end + table.insert(matches, full_path) + end + end + end + end + end + else + local handle = vim.uv.fs_scandir '.' + if handle then + while true do + local name, type = vim.uv.fs_scandir_next(handle) + if not name then break end + + if name ~= '.' and name ~= '..' then + if name:lower():find(last_word:lower(), 1, true) == 1 then + if type == 'directory' then + table.insert(matches, name .. '/') + else + table.insert(matches, name) + end + end + end + end + end + + if #words == 1 then + local path = os.getenv 'PATH' or '' + local path_dirs = vim.split(path, ':') + local seen = {} + + for _, match in ipairs(matches) do + seen[match] = true + end + + for _, dir in ipairs(path_dirs) do + local path_handle = vim.uv.fs_scandir(dir) + if path_handle then + while true do + local name, type = vim.uv.fs_scandir_next(path_handle) + if not name then break end + + if type == 'file' and not seen[name] then + if name:lower():find(last_word:lower(), 1, true) == 1 then + local filepath = dir .. '/' .. name + local stat = vim.uv.fs_stat(filepath) + if stat and stat.mode then + local is_executable = bit.band(stat.mode, tonumber('111', 8)) ~= 0 + if is_executable then + table.insert(matches, name) + seen[name] = true + end + end + end + end + end + end + end + end + end + + table.sort(matches, function(a, b) + local a_is_dir = a:match '/$' + local b_is_dir = b:match '/$' + if a_is_dir == b_is_dir then return a < b end + return a_is_dir + end) + + return matches +end + +function M.compile() + local prev_win = vim.api.nvim_get_current_win() + local input_buf = vim.api.nvim_create_buf(false, true) + + vim.b[input_buf].completion = false + + local prompt_text = config.prompt .. ': ' + local prompt_len = #prompt_text + + ---@return table + local function get_win_config() + local width = vim.o.columns + local max_height = 5 + local row = vim.o.lines - vim.o.cmdheight - max_height - 1 + + return { + relative = 'editor', + width = width, + height = max_height, + row = row, + col = 0, + style = 'minimal', + border = 'none', + } + end + + local input_win = vim.api.nvim_open_win(input_buf, true, get_win_config()) + + vim.api.nvim_set_hl(0, 'CompileModePrompt', { link = 'Title', default = true }) + vim.api.nvim_set_hl(0, 'CompileModeMatch', { link = 'Normal', default = true }) + vim.api.nvim_set_hl(0, 'CompileModeMatchSelected', { link = 'CursorLine', default = true }) + vim.api.nvim_set_hl(0, 'CompileModeMatchChar', { link = 'IncSearch', default = true }) + + vim.api.nvim_set_option_value('buftype', 'nofile', { buf = input_buf }) + vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = input_buf }) + vim.api.nvim_set_option_value('modifiable', true, { buf = input_buf }) + vim.api.nvim_set_option_value('wrap', true, { win = input_win }) + vim.api.nvim_set_option_value('linebreak', true, { win = input_win }) + + local default_cmd = last_command or 'make -k' + local current_input = default_cmd + local matches = {} + local match_index = 0 + local lock = false + local cycling = false + local history_index = 0 + + local ns_id = vim.api.nvim_create_namespace 'compile_mode_prompt' + + local function get_input() + local line = vim.api.nvim_buf_get_lines(input_buf, 0, 1, false)[1] or '' + local pattern = '^' .. vim.pesc(prompt_text) + if line:match(pattern) then return line:sub(prompt_len + 1) end + return '' + end + + local function update_display() + local words = vim.split(current_input, '%s+') + local last_word = words[#words] or '' + + local parts = { prompt_text, current_input } + local match_highlights = {} + + if #matches > 0 and match_index > 0 then + local pos = #table.concat(parts, '') + local max_display = 8 + + for i, match in ipairs(matches) do + if i > max_display then break end + + if i > 1 then + table.insert(parts, ' | ') + pos = pos + 3 + else + table.insert(parts, ' | ') + pos = pos + 3 + end + + local filter_start, filter_end = nil, nil + if last_word ~= '' then + filter_start, filter_end = match:lower():find(last_word:lower(), 1, true) + end + + table.insert(match_highlights, { + start = pos, + finish = pos + #match, + selected = i == match_index, + filter_start = filter_start, + filter_end = filter_end, + }) + + table.insert(parts, match) + pos = pos + #match + end + + if #matches > max_display then table.insert(parts, ' | ...') end + end + + local full_text = table.concat(parts, '') + + lock = true + cycling = true + vim.api.nvim_buf_set_lines(input_buf, 0, -1, false, { full_text }) + + local width = vim.o.columns + local required_lines = math.ceil(#full_text / width) + local needed_height = math.min(required_lines, 5) + + local current_config = vim.api.nvim_win_get_config(input_win) + if current_config.height ~= needed_height then + local new_config = get_win_config() + new_config.height = needed_height + new_config.row = vim.o.lines - vim.o.cmdheight - needed_height - 1 + vim.api.nvim_win_set_config(input_win, new_config) + end + + vim.api.nvim_buf_clear_namespace(input_buf, ns_id, 0, -1) + + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, 0, { + end_col = prompt_len, + hl_group = 'CompileModePrompt', + }) + + if match_index == 0 then + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, prompt_len, { + end_col = prompt_len + #current_input, + hl_group = 'CompileModeMatchSelected', + }) + end + + for _, hl in ipairs(match_highlights) do + if hl.selected then + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, hl.start, { + end_col = hl.finish, + hl_group = 'CompileModeMatchSelected', + }) + else + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, hl.start, { + end_col = hl.finish, + hl_group = 'CompileModeMatch', + }) + end + + if hl.filter_start and hl.filter_end then + local match_start = hl.start + hl.filter_start - 1 + local match_end = hl.start + hl.filter_end + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, match_start, { + end_col = match_end, + hl_group = 'CompileModeMatchChar', + }) + end + end + + local cursor_col = prompt_len + #current_input + vim.api.nvim_win_set_cursor(input_win, { 1, cursor_col }) + + vim.schedule(function() + lock = false + cycling = false + end) + end + + local function close() + if vim.api.nvim_win_is_valid(input_win) then vim.api.nvim_win_close(input_win, true) end + if vim.api.nvim_win_is_valid(prev_win) then vim.api.nvim_set_current_win(prev_win) end + vim.cmd 'stopinsert' + end + + update_display() + vim.schedule(function() vim.api.nvim_win_set_cursor(input_win, { 1, prompt_len + #current_input }) end) + + local last_input = current_input + + vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { + buffer = input_buf, + callback = function() + if lock or cycling then return end + + local line = vim.api.nvim_buf_get_lines(input_buf, 0, 1, false)[1] or '' + + local pattern = '^' .. vim.pesc(prompt_text) + if not line:match(pattern) then + lock = true + vim.api.nvim_buf_set_lines(input_buf, 0, -1, false, { prompt_text .. current_input }) + vim.api.nvim_buf_clear_namespace(input_buf, ns_id, 0, -1) + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, 0, { + end_col = prompt_len, + hl_group = 'CompileModePrompt', + }) + vim.api.nvim_win_set_cursor(input_win, { 1, prompt_len + #current_input }) + vim.schedule(function() lock = false end) + return + end + + local input_part = line:sub(prompt_len + 1) + local pipe_pos = input_part:find ' | ' + if pipe_pos then input_part = input_part:sub(1, pipe_pos - 1) end + + if input_part ~= last_input then + current_input = input_part + last_input = input_part + match_index = 0 + matches = {} + + vim.schedule(update_display) + end + end, + }) + + vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { + buffer = input_buf, + callback = function() + if lock or cycling then return end + local cursor = vim.api.nvim_win_get_cursor(input_win) + + if cursor[2] < prompt_len then vim.api.nvim_win_set_cursor(input_win, { 1, prompt_len }) end + end, + }) + + vim.api.nvim_create_autocmd({ 'VimResized' }, { + callback = function() + if vim.api.nvim_win_is_valid(input_win) then vim.schedule(update_display) end + end, + }) + + local opts = { buffer = input_buf, nowait = true, silent = true } + + vim.keymap.set('i', '', function() + if #matches == 0 then + matches = get_matches(current_input) + if #matches == 0 then return end + end + + match_index = (match_index % #matches) + 1 + local words = vim.split(current_input, '%s+') + words[#words] = matches[match_index] + current_input = table.concat(words, ' ') + last_input = current_input + vim.schedule(update_display) + end, opts) + + vim.keymap.set('i', '', function() + if #matches == 0 then + matches = get_matches(current_input) + if #matches == 0 then return end + end + + match_index = match_index - 1 + if match_index < 1 then match_index = #matches end + local words = vim.split(current_input, '%s+') + words[#words] = matches[match_index] + current_input = table.concat(words, ' ') + last_input = current_input + vim.schedule(update_display) + end, opts) + + vim.keymap.set('i', '', function() + if #command_history > 0 then + if history_index == 0 then + current_input = get_input() + last_input = current_input + end + if history_index < #command_history then + history_index = history_index + 1 + current_input = command_history[#command_history - history_index + 1] + last_input = current_input + vim.schedule(update_display) + end + end + return '' + end, vim.tbl_extend('force', opts, { expr = true })) + + vim.keymap.set('i', '', function() + if history_index > 0 then + history_index = history_index - 1 + if history_index == 0 then + current_input = get_input() + else + current_input = command_history[#command_history - history_index + 1] + end + last_input = current_input + vim.schedule(update_display) + end + return '' + end, vim.tbl_extend('force', opts, { expr = true })) + + vim.keymap.set('i', '', function() + if current_input and current_input ~= '' then + append_to_history(current_input) + vim.schedule(function() + close() + run_compile(current_input) + end) + end + return '' + end, vim.tbl_extend('force', opts, { expr = true })) + + vim.keymap.set({ 'i', 'n' }, '', function() vim.schedule(close) end, opts) + vim.keymap.set({ 'i', 'n' }, '', function() vim.schedule(close) end, opts) + + vim.keymap.set('i', '', function() + local new_pos = 0 + local found_non_sep = false + for i = #current_input, 1, -1 do + local char = current_input:sub(i, i) + local is_sep = char:match '[/%._%- ]' + + if not found_non_sep and not is_sep then + found_non_sep = true + elseif found_non_sep and is_sep then + new_pos = i + break + end + end + + current_input = current_input:sub(1, new_pos) + last_input = current_input + vim.schedule(update_display) + return '' + end, vim.tbl_extend('force', opts, { expr = true })) + + vim.cmd 'startinsert!' +end + +function M.recompile() + if last_command then + run_compile(last_command) + else + M.compile() + end +end + +--- @param opts? CompileConfig +function M.setup(opts) + config = vim.tbl_deep_extend('force', config, opts or {}) + + load_history() + + vim.api.nvim_create_user_command('Compile', function(cmd_opts) + if cmd_opts.args and cmd_opts.args ~= '' then + run_compile(cmd_opts.args) + else + M.compile() + end + end, { nargs = '?', desc = 'Run compile command' }) + + vim.api.nvim_create_user_command('Recompile', M.recompile, { desc = 'Rerun last compile command' }) + + vim.api.nvim_create_autocmd('FileType', { + pattern = 'compilation', + callback = function() + if config.keymaps.close then vim.keymap.set('n', config.keymaps.close, 'close', { buffer = true, desc = 'Close compilation buffer' }) end + if config.keymaps.recompile then vim.keymap.set('n', config.keymaps.recompile, M.recompile, { buffer = true, desc = 'Recompile' }) end + + local function jump_to_error() + local bufnr = vim.api.nvim_get_current_buf() + local line = vim.api.nvim_win_get_cursor(0)[1] + local line_to_qf = vim.b[bufnr].line_to_qf + + if not line_to_qf then return end + + local output_idx = line_to_qf[line] + if not output_idx then return end + + local qf_list = vim.fn.getqflist() + + if output_idx > 0 and output_idx <= #qf_list then + local entry = qf_list[output_idx] + + local valid_idx = nil + if entry.valid == 1 and entry.bufnr > 0 then + valid_idx = output_idx + else + for i = output_idx - 1, 1, -1 do + if qf_list[i].valid == 1 and qf_list[i].bufnr > 0 then + valid_idx = i + break + end + end + if not valid_idx then + for i = output_idx + 1, #qf_list do + if qf_list[i].valid == 1 and qf_list[i].bufnr > 0 then + valid_idx = i + break + end + end + end + end + + if valid_idx then + local target_win = nil + for _, win in ipairs(vim.api.nvim_list_wins()) do + local win_buf = vim.api.nvim_win_get_buf(win) + local bt = vim.bo[win_buf].buftype + if bt == '' or bt == 'acwrite' then + target_win = win + break + end + end + + if target_win then + vim.api.nvim_set_current_win(target_win) + vim.cmd('cc ' .. valid_idx) + else + vim.cmd('cc ' .. valid_idx) + end + end + end + end + + vim.keymap.set('n', '', jump_to_error, { buffer = true, desc = 'Jump to error location' }) + vim.keymap.set('n', '<2-LeftMouse>', jump_to_error, { buffer = true, desc = 'Jump to error location' }) + end, + }) +end + +return M diff --git a/home/neovim/config/lua/emacs-find-file.lua b/home/neovim/config/lua/find-file.lua similarity index 77% rename from home/neovim/config/lua/emacs-find-file.lua rename to home/neovim/config/lua/find-file.lua index fe2f00e..4c5419f 100644 --- a/home/neovim/config/lua/emacs-find-file.lua +++ b/home/neovim/config/lua/find-file.lua @@ -1,12 +1,11 @@ ----@class EmacsFindFileConfig +---@class FindFileConfig ---@field max_matches? number Maximum number of matches to display ---@field auto_cd? boolean Automatically change directory when entering a directory ---@field respect_gitignore? boolean Respect .gitignore files when listing matches (default: true) ----@field highlights? table Highlight configuration (will be set dynamically based on theme) ---@field on_directory_enter? function Optional callback when entering a directory without selecting a file ----@field keymaps? EmacsFindFileKeymaps Keymap configuration +---@field keymaps? FindFileKeymaps Keymap configuration ----@class EmacsFindFileKeymaps +---@class FindFileKeymaps ---@field next string Cycle to next candidate ---@field prev string Cycle to previous candidate ---@field complete string Complete to selected candidate @@ -23,12 +22,11 @@ local M = {} ----@type EmacsFindFileConfig +---@type FindFileConfig local config = { max_matches = 8, auto_cd = true, respect_gitignore = true, - highlights = nil, on_directory_enter = nil, keymaps = { next = '', @@ -43,10 +41,8 @@ local config = { } ---Setup emacs-find-file with custom configuration ----@param opts EmacsFindFileConfig|nil Configuration options -function M.setup(opts) - config = vim.tbl_deep_extend('force', config, opts or {}) -end +---@param opts FindFileConfig|nil Configuration options +function M.setup(opts) config = vim.tbl_deep_extend('force', config, opts or {}) end ---Check if a path should be ignored based on gitignore ---@param path string Full path to check @@ -60,23 +56,17 @@ local function is_gitignored(path, override_respect) should_respect = config.respect_gitignore end - if not should_respect then - return false - end + if not should_respect then return false end -- Extract just the filename from the path local name = vim.fn.fnamemodify(path, ':t') -- Filter out .git directory when respecting gitignore - if name == '.git' then - return true - end + if name == '.git' then return true end -- Use git check-ignore to determine if path is ignored local handle = io.popen('git check-ignore -q "' .. path:gsub('"', '\\"') .. '" 2>/dev/null') - if not handle then - return false - end + if not handle then return false end local _, _, exit_code = handle:close() -- git check-ignore returns 0 if path is ignored, 1 if not ignored @@ -91,14 +81,10 @@ end local function parse_path(input) local expanded = vim.fn.expand(input) - if expanded == '' or expanded == '~' then - return vim.fn.expand('~'), '' - end + if expanded == '' or expanded == '~' then return vim.fn.expand '~', '' end - if input:match('/$') then - if vim.fn.isdirectory(expanded) == 1 then - return expanded, '' - end + if input:match '/$' then + if vim.fn.isdirectory(expanded) == 1 then return expanded, '' end end local dir = vim.fn.fnamemodify(expanded, ':h') @@ -122,7 +108,7 @@ local function get_matches(dir, filter, override_respect) if name ~= '.' and name ~= '..' then if filter == '' or name:lower():find(filter:lower(), 1, true) then - local sep = dir:match('/$') and '' or '/' + local sep = dir:match '/$' and '' or '/' local full_path = dir .. sep .. name -- Skip if gitignored @@ -138,9 +124,7 @@ local function get_matches(dir, filter, override_respect) end table.sort(matches, function(a, b) - if a.type == b.type then - return a.name < b.name - end + if a.type == b.type then return a.name < b.name end return a.type == 'directory' end) @@ -172,11 +156,11 @@ function M.open_dir(start_path) local input_win = vim.api.nvim_open_win(input_buf, true, get_win_config()) - vim.api.nvim_set_hl(0, 'EmacsPrompt', { link = 'Title', default = true }) - vim.api.nvim_set_hl(0, 'EmacsSep', { link = 'Comment', default = true }) - vim.api.nvim_set_hl(0, 'EmacsMatch', { link = 'Normal', default = true }) - vim.api.nvim_set_hl(0, 'EmacsMatchSelected', { link = 'CursorLine', default = true }) - vim.api.nvim_set_hl(0, 'EmacsMatchChar', { link = 'IncSearch', default = true }) + vim.api.nvim_set_hl(0, 'FindFilePrompt', { link = 'Title', default = true }) + vim.api.nvim_set_hl(0, 'FindFileSep', { link = 'Comment', default = true }) + vim.api.nvim_set_hl(0, 'FindFileMatch', { link = 'Normal', default = true }) + vim.api.nvim_set_hl(0, 'FindFileMatchSelected', { link = 'CursorLine', default = true }) + vim.api.nvim_set_hl(0, 'FindFileMatchChar', { link = 'IncSearch', default = true }) vim.api.nvim_set_option_value('buftype', 'nofile', { buf = input_buf }) vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = input_buf }) @@ -193,9 +177,13 @@ function M.open_dir(start_path) local matches = {} local selected = 0 local lock = false + local cycling = false local user_selected = false local show_hidden = false + -- Namespace for highlights + local ns_id = vim.api.nvim_create_namespace 'find_file' + local function update_display() local dir, filter = parse_path(current_input) matches = get_matches(dir, filter, not show_hidden) @@ -206,7 +194,7 @@ function M.open_dir(start_path) local parts = { 'Find file: ', current_input } local match_highlights = {} - local show_matches = current_input:match('/$') or (filter ~= '' and #matches >= 1) + local show_matches = current_input:match '/$' or (filter ~= '' and #matches >= 1) if #matches > 0 and show_matches then local pos = #table.concat(parts, '') @@ -243,9 +231,7 @@ function M.open_dir(start_path) pos = pos + #item end - if #matches > config.max_matches then - table.insert(parts, ' | ...') - end + if #matches > config.max_matches then table.insert(parts, ' | ...') end end local full_text = table.concat(parts, '') @@ -266,30 +252,54 @@ function M.open_dir(start_path) vim.api.nvim_win_set_config(input_win, new_config) end - vim.api.nvim_buf_clear_namespace(input_buf, -1, 0, -1) - vim.api.nvim_buf_add_highlight(input_buf, -1, 'EmacsPrompt', 0, 0, 11) + vim.api.nvim_buf_clear_namespace(input_buf, ns_id, 0, -1) + -- Highlight the prompt + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, 0, { + end_col = 11, + hl_group = 'FindFilePrompt', + }) + + -- Highlight input if no match is selected if selected == 0 or not user_selected then - vim.api.nvim_buf_add_highlight(input_buf, -1, 'EmacsMatchSelected', 0, 11, 11 + #current_input) + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, 11, { + end_col = 11 + #current_input, + hl_group = 'FindFileMatchSelected', + }) end + -- Highlight path separators for i = 1, #current_input do if current_input:sub(i, i) == '/' then - vim.api.nvim_buf_add_highlight(input_buf, -1, 'EmacsSep', 0, 11 + i - 1, 11 + i) + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, 11 + i - 1, { + end_col = 11 + i, + hl_group = 'FindFileSep', + }) end end + -- Highlight matches for _, hl in ipairs(match_highlights) do if hl.selected then - vim.api.nvim_buf_add_highlight(input_buf, -1, 'EmacsMatchSelected', 0, hl.start, hl.finish) + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, hl.start, { + end_col = hl.finish, + hl_group = 'FindFileMatchSelected', + }) else - vim.api.nvim_buf_add_highlight(input_buf, -1, 'EmacsMatch', 0, hl.start, hl.finish) + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, hl.start, { + end_col = hl.finish, + hl_group = 'FindFileMatch', + }) end + -- Highlight the matching characters if hl.filter_start and hl.filter_end then local match_start = hl.start + hl.filter_start - 1 local match_end = hl.start + hl.filter_end - vim.api.nvim_buf_add_highlight(input_buf, -1, 'EmacsMatchChar', 0, match_start, match_end) + vim.api.nvim_buf_set_extmark(input_buf, ns_id, 0, match_start, { + end_col = match_end, + hl_group = 'FindFileMatchChar', + }) end end @@ -304,18 +314,13 @@ function M.open_dir(start_path) local function close() if vim.api.nvim_win_is_valid(input_win) then vim.api.nvim_win_close(input_win, true) end - if vim.api.nvim_win_is_valid(prev_win) then - vim.api.nvim_set_current_win(prev_win) - end - vim.cmd('stopinsert') + if vim.api.nvim_win_is_valid(prev_win) then vim.api.nvim_set_current_win(prev_win) end + vim.cmd 'stopinsert' end update_display() - vim.schedule(function() - vim.api.nvim_win_set_cursor(input_win, { 1, 11 + #current_input }) - end) + vim.schedule(function() vim.api.nvim_win_set_cursor(input_win, { 1, 11 + #current_input }) end) - local cycling = false local last_input = current_input vim.api.nvim_create_autocmd({ 'TextChangedI' }, { @@ -324,13 +329,11 @@ function M.open_dir(start_path) if lock or cycling then return end local line = vim.api.nvim_buf_get_lines(input_buf, 0, 1, false)[1] or '' - if not line:match('^Find file: ') then return end + if not line:match '^Find file: ' then return end local path_part = line:sub(12) - local pipe_pos = path_part:find(' | ') - if pipe_pos then - path_part = path_part:sub(1, pipe_pos - 1) - end + local pipe_pos = path_part:find ' | ' + if pipe_pos then path_part = path_part:sub(1, pipe_pos - 1) end if path_part ~= last_input then current_input = path_part @@ -350,17 +353,13 @@ function M.open_dir(start_path) local cursor = vim.api.nvim_win_get_cursor(input_win) local max_col = 11 + #current_input - if cursor[2] > max_col then - vim.api.nvim_win_set_cursor(input_win, { 1, max_col }) - end + if cursor[2] > max_col then vim.api.nvim_win_set_cursor(input_win, { 1, max_col }) end end, }) vim.api.nvim_create_autocmd({ 'VimResized' }, { callback = function() - if vim.api.nvim_win_is_valid(input_win) then - vim.api.nvim_win_set_config(input_win, get_win_config()) - end + if vim.api.nvim_win_is_valid(input_win) then vim.api.nvim_win_set_config(input_win, get_win_config()) end end, }) @@ -368,9 +367,7 @@ function M.open_dir(start_path) vim.keymap.set('i', config.keymaps.next, function() selected = selected + 1 - if selected > #matches then - selected = 0 - end + if selected > #matches then selected = 0 end user_selected = true vim.schedule(update_display) return '' @@ -378,9 +375,7 @@ function M.open_dir(start_path) vim.keymap.set('i', config.keymaps.prev, function() selected = selected - 1 - if selected < 0 then - selected = #matches - end + if selected < 0 then selected = #matches end user_selected = true vim.schedule(update_display) return '' @@ -390,9 +385,7 @@ function M.open_dir(start_path) if #matches == 0 then return '' end local match = selected > 0 and matches[selected] or matches[1] current_input = match.path - if match.type == 'directory' then - current_input = current_input .. '/' - end + if match.type == 'directory' then current_input = current_input .. '/' end last_input = current_input selected = 0 user_selected = false @@ -401,7 +394,7 @@ function M.open_dir(start_path) end, vim.tbl_extend('force', opts, { expr = true, desc = 'Complete to selected candidate' })) vim.keymap.set('i', config.keymaps.confirm, function() - local in_directory_mode = current_input:match('/$') + local in_directory_mode = current_input:match '/$' local dir, filter = parse_path(current_input) if user_selected and selected > 0 and #matches > 0 then @@ -411,7 +404,7 @@ function M.open_dir(start_path) selected = 0 user_selected = false vim.schedule(update_display) - vim.cmd('startinsert!') + vim.cmd 'startinsert!' return '' else local file = matches[selected].path @@ -426,9 +419,7 @@ function M.open_dir(start_path) local dir_path = vim.fn.expand(current_input) vim.schedule(function() close() - if config.auto_cd then - vim.cmd('cd ' .. vim.fn.fnameescape(dir_path)) - end + if config.auto_cd then vim.cmd('cd ' .. vim.fn.fnameescape(dir_path)) end if config.on_directory_enter then config.on_directory_enter(dir_path) else @@ -455,12 +446,10 @@ function M.open_dir(start_path) return '' end, { buffer = input_buf, nowait = true, expr = true, desc = 'Open file or directory' }) - -- Handle close keymaps (can be a single key or a list) - local close_keys = type(config.keymaps.close) == 'table' and config.keymaps.close or { config.keymaps.close } + ---@type string[] + local close_keys = type(config.keymaps.close) == 'table' and config.keymaps.close --[[@as string[] ]] or { config.keymaps.close } for _, key in ipairs(close_keys) do - vim.keymap.set({ 'n', 'i' }, key, function() - vim.schedule(close) - end, vim.tbl_extend('force', opts, { desc = 'Close find-file' })) + vim.keymap.set({ 'n', 'i' }, key, function() vim.schedule(close) end, vim.tbl_extend('force', opts, { desc = 'Close find-file' })) end vim.keymap.set('i', config.keymaps.delete_component, function() @@ -468,7 +457,7 @@ function M.open_dir(start_path) local found_non_sep = false for i = #current_input, 1, -1 do local char = current_input:sub(i, i) - local is_sep = char:match('[/%._%- ]') + local is_sep = char:match '[/%._%- ]' if not found_non_sep and not is_sep then found_non_sep = true @@ -487,7 +476,7 @@ function M.open_dir(start_path) vim.keymap.set('i', config.keymaps.parent_dir, function() -- Go to parent directory - local dir = current_input:match('/$') and current_input or vim.fn.fnamemodify(current_input, ':h') + local dir = current_input:match '/$' and current_input or vim.fn.fnamemodify(current_input, ':h') local parent = vim.fn.fnamemodify(dir, ':h') if parent ~= '' and parent ~= '/' then current_input = parent .. '/' @@ -509,7 +498,7 @@ function M.open_dir(start_path) return '' end, vim.tbl_extend('force', opts, { expr = true, desc = 'Toggle showing ignored/hidden files' })) - vim.cmd('startinsert!') + vim.cmd 'startinsert!' end ---Open emacs-style find-file interface from current buffer's directory