--- 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 CompileInputKeymaps --- @field next string? Cycle to next completion candidate --- @field prev string? Cycle to previous completion candidate --- @field confirm string? Confirm and run compile command --- @field close (string|string[])? Close compile prompt --- @field delete_component string? Delete path component --- @field history_prev string? Previous command in history --- @field history_next string? Next command in history --- @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 buffer_keymaps? table Keymaps for compilation buffer --- @field input_keymaps? CompileInputKeymaps Keymaps for input prompt --- @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 = function(qf_list, _) if #qf_list > 0 then vim.cmd 'copen' end end, buffer_keymaps = { close = 'q', recompile = 'g', }, input_keymaps = { next = '', prev = '', complete = '', confirm = '', close = { '', '' }, delete_component = '', history_prev = '', history_next = '', }, 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 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 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 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 -- Add opening bracket table.insert(parts, ' [') pos = pos + 2 for i, match in ipairs(matches) do if i > max_display then break 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 -- Add pipe separator between candidates (but not after the last one shown) if i < math.min(#matches, max_display) then table.insert(parts, ' | ') pos = pos + 3 end end -- Add ellipsis if there are more matches than max_display if #matches > max_display then table.insert(parts, ' | ...') pos = pos + 6 end -- Add closing bracket table.insert(parts, ']') 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 local saved_input = nil 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 bracket_pos = input_part:find ' %[' if bracket_pos then input_part = input_part:sub(1, bracket_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 } -- Cycle to next completion candidate if config.input_keymaps.next then local next_fn = 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 vim.keymap.set('i', config.input_keymaps.next, next_fn, opts) vim.keymap.set('n', config.input_keymaps.next, next_fn, opts) end -- Cycle to previous completion candidate if config.input_keymaps.prev then local prev_fn = 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 vim.keymap.set('i', config.input_keymaps.prev, prev_fn, opts) vim.keymap.set('n', config.input_keymaps.prev, prev_fn, opts) end -- Complete to selected candidate if config.input_keymaps.complete then vim.keymap.set('i', config.input_keymaps.complete, function() if #matches == 0 then matches = get_matches(current_input) if #matches == 0 then return '' end end local match = match_index > 0 and matches[match_index] or matches[1] local words = vim.split(current_input, '%s+') words[#words] = match current_input = table.concat(words, ' ') last_input = current_input match_index = 0 matches = {} vim.schedule(update_display) return '' end, vim.tbl_extend('force', opts, { expr = true })) end -- Previous command in history if config.input_keymaps.history_prev then vim.keymap.set('i', config.input_keymaps.history_prev, function() if #command_history > 0 then if history_index == 0 then saved_input = get_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 })) end -- Next command in history if config.input_keymaps.history_next then vim.keymap.set('i', config.input_keymaps.history_next, function() if history_index > 0 then history_index = history_index - 1 if history_index == 0 then current_input = saved_input or get_input() saved_input = nil 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 })) end -- Confirm and run compile command if config.input_keymaps.confirm then vim.keymap.set('i', config.input_keymaps.confirm, 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 })) end -- Close compile prompt ---@type string[] local close_keys = type(config.input_keymaps.close) == 'table' and config.input_keymaps.close --[[@as string[] ]] or { config.input_keymaps.close } for _, key in ipairs(close_keys) do if key then -- In insert mode: exit to normal mode first vim.keymap.set('i', key, '', opts) -- In normal mode: close the prompt vim.keymap.set('n', key, function() vim.schedule(close) end, opts) end end -- Delete path component if config.input_keymaps.delete_component then vim.keymap.set('i', config.input_keymaps.delete_component, 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 })) end 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.buffer_keymaps.close then vim.keymap.set('n', config.buffer_keymaps.close, 'close', { buffer = true, desc = 'Close compilation buffer' }) end if config.buffer_keymaps.recompile then vim.keymap.set('n', config.buffer_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, }) vim.api.nvim_create_autocmd('FileType', { pattern = 'qf', callback = function() 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 = true, desc = 'Jump to error location' }) vim.keymap.set('n', '<2-LeftMouse>', qf_jump, { buffer = true, desc = 'Jump to error location' }) end, }) end return M