953 lines
30 KiB
Lua
953 lines
30 KiB
Lua
--- 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<string, string|false> 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 = '<C-n>',
|
|
prev = '<C-p>',
|
|
complete = '<Tab>',
|
|
confirm = '<CR>',
|
|
close = { '<C-c>', '<Esc>' },
|
|
delete_component = '<M-BS>',
|
|
history_prev = '<M-p>',
|
|
history_next = '<M-n>',
|
|
},
|
|
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
|
|
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
|
|
vim.keymap.set('i', config.input_keymaps.next, 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)
|
|
end
|
|
|
|
-- Cycle to previous completion candidate
|
|
if config.input_keymaps.prev then
|
|
vim.keymap.set('i', config.input_keymaps.prev, 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)
|
|
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 vim.keymap.set({ 'i', '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, '<cmd>close<cr>', { 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', '<CR>', 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', '<CR>', 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
|