1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-19 09:23:44 +08:00
SpaceVim/bundle/flygrep.nvim/lua/flygrep.lua
Eric Wong 3829f764fa
fix(flygrep): fix result count
if no result, count extmark is cleared.
2025-02-03 13:04:54 +08:00

457 lines
15 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--=============================================================================
-- flygrep.lua
-- Copyright 2025 Eric Wong
-- Author: Eric Wong < wsdjeg@outlook.com >
-- URL: https://spacevim.org
-- License: GPLv3
--=============================================================================
local M = {}
local conf = require('flygrep.config')
local job = require('spacevim.api.job')
local ok, cmp = pcall(require, 'cmp')
if not ok then
vim.cmd('doautocmd InsertEnter')
ok, cmp = pcall(require, 'cmp')
end
local grep_timer_id = -1
local grep_input = ''
local search_jobid = -1
local search_hi_id = -1
local fix_string = false
local include_hidden_file = false
-- all buffers
local result_bufid = -1
local result_winid = -1
local prompt_bufid = -1
local prompt_winid = -1
local preview_winid = -1
local preview_bufid = -1
local preview_timer_id = -1
local prompt_count_id
local extns = vim.api.nvim_create_namespace('floatgrep_ext')
local function update_result_count()
local count = vim.api.nvim_buf_line_count(result_bufid)
local line = vim.api.nvim_win_get_cursor(result_winid)[1]
prompt_count_id = vim.api.nvim_buf_set_extmark(prompt_bufid, extns, 0, 0, {
id = prompt_count_id,
virt_text = { { string.format('%d/%d', line, count), 'Comment' } },
virt_text_pos = 'right_align',
})
return prompt_count_id
end
local function build_grep_command()
local cmd = { conf.command.execute }
for _, v in ipairs(conf.command.default_opts) do
table.insert(cmd, v)
end
if include_hidden_file then
table.insert(cmd, conf.command.hidden_opt)
end
if fix_string then
table.insert(cmd, conf.command.fixed_string_opt)
else
table.insert(cmd, conf.command.expr_opt)
end
table.insert(cmd, grep_input)
table.insert(cmd, '.')
return cmd
end
-- 搜索结果行转换成文件名、光标位置
local function get_file_pos(line)
local filename = vim.fn.fnameescape(vim.fn.split(line, [[:\d\+:]])[1])
local linenr = vim.fn.str2nr(string.sub(vim.fn.matchstr(line, [[:\d\+:]]), 2, -2))
local colum = vim.fn.str2nr(string.sub(vim.fn.matchstr(line, [[\(:\d\+\)\@<=:\d\+:]]), 2, -2))
return filename, linenr, colum
end
local function preview_timer(t)
-- if preview win does exists, return
if not vim.api.nvim_win_is_valid(preview_winid) then
return
end
local cursor = vim.api.nvim_win_get_cursor(result_winid)
local line = vim.api.nvim_buf_get_lines(result_bufid, cursor[1] - 1, cursor[1], false)[1]
if line == '' then
return
end
local filename, liner, colum = get_file_pos(line)
vim.api.nvim_buf_set_lines(preview_bufid, 0, -1, false, vim.fn.readfile(filename, ''))
local ft = vim.filetype.match({ filename = filename })
if ft then
vim.api.nvim_buf_set_option(preview_bufid, 'syntax', ft)
else
local ftdetect_autocmd = vim.api.nvim_get_autocmds({
group = 'filetypedetect',
event = 'BufRead',
pattern = '*.' .. vim.fn.fnamemodify(filename, ':e'),
})
-- logger.info(vim.inspect(ftdetect_autocmd))
if ftdetect_autocmd[1] then
if
ftdetect_autocmd[1].command and vim.startswith(ftdetect_autocmd[1].command, 'set filetype=')
then
ft = ftdetect_autocmd[1].command:gsub('set filetype=', '')
vim.api.nvim_buf_set_option(preview_bufid, 'syntax', ft)
end
end
end
vim.api.nvim_win_set_cursor(preview_winid, { liner, colum })
end
local function grep_timer(t)
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, {})
if prompt_count_id then
pcall(vim.api.nvim_buf_del_extmark, prompt_bufid, extns, prompt_count_id)
prompt_count_id = update_result_count()
end
search_jobid = job.start(build_grep_command(), {
on_stdout = function(id, data)
if
id == search_jobid
and vim.api.nvim_buf_is_valid(prompt_bufid)
and vim.api.nvim_win_is_valid(prompt_winid)
then
if vim.fn.getbufline(result_bufid, 1)[1] == '' then
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, data)
if conf.enable_preview then
vim.fn.timer_stop(preview_timer_id)
preview_timer_id = vim.fn.timer_start(500, preview_timer, { ['repeat'] = 1 })
end
else
vim.api.nvim_buf_set_lines(result_bufid, -1, -1, false, data)
end
update_result_count()
end
end,
})
end
local function build_prompt_title()
local t = {}
table.insert(t, { ' FlyGrep ', 'FlyGrep_a' })
table.insert(t, { '', 'FlyGrep_a_FlyGrep_b' })
if not fix_string then
table.insert(t, { ' expr ', 'FlyGrep_b' })
else
table.insert(t, { ' string ', 'FlyGrep_b' })
end
table.insert(t, { '', 'FlyGrep_b' })
table.insert(t, { ' ' .. vim.fn.getcwd() .. ' ', 'FlyGrep_b' })
table.insert(t, { '', 'FlyGrep_b_Normal' })
-- return {{}, {}, {}}
return t
end
local function toggle_hidden_file()
include_hidden_file = not include_hidden_file
vim.cmd('doautocmd TextChangedI')
end
local function toggle_fix_string()
fix_string = not fix_string
vim.cmd('doautocmd TextChangedI')
local conf = vim.api.nvim_win_get_config(prompt_winid)
conf.title = build_prompt_title()
vim.api.nvim_win_set_config(prompt_winid, conf)
end
local function toggle_preview_win()
conf.enable_preview = not conf.enable_preview
local screen_width = math.floor(vim.o.columns * 0.8)
-- 起始位位置: lines * 10%, columns * 10%
local start_col = math.floor(vim.o.columns * 0.1)
local start_row = math.floor(vim.o.lines * 0.1)
-- 整体高度lines 的 80%
local screen_height = math.floor(vim.o.lines * 0.8)
if conf.enable_preview then
if not vim.api.nvim_buf_is_valid(preview_bufid) then
preview_bufid = vim.api.nvim_create_buf(false, true)
end
preview_winid = vim.api.nvim_open_win(preview_bufid, false, {
relative = 'editor',
width = screen_width,
height = math.floor((screen_height - 5) / 2),
col = start_col,
row = start_row,
focusable = false,
border = 'rounded',
-- title = 'Result',
-- title_pos = 'center',
-- noautocmd = true,
})
vim.api.nvim_set_option_value('cursorline', true, { win = preview_winid })
local winopt = vim.api.nvim_win_get_config(result_winid)
winopt.row = start_row + math.floor((screen_height - 5) / 2) + 2
winopt.height = screen_height - 5 - math.floor((screen_height - 5) / 2) - 2
vim.api.nvim_win_set_config(result_winid, winopt)
vim.fn.timer_stop(preview_timer_id)
preview_timer_id = vim.fn.timer_start(500, preview_timer, { ['repeat'] = 1 })
else
vim.api.nvim_win_close(preview_winid, true)
local winopt = vim.api.nvim_win_get_config(result_winid)
winopt.row = start_row
winopt.height = screen_height - 5
vim.api.nvim_win_set_config(result_winid, winopt)
end
end
local function next_item()
local line_number = vim.api.nvim_win_get_cursor(result_winid)[1]
if line_number == vim.api.nvim_buf_line_count(result_bufid) then
pcall(vim.api.nvim_win_set_cursor, result_winid, { 1, 0 })
else
pcall(vim.api.nvim_win_set_cursor, result_winid, { line_number + 1, 0 })
end
if conf.enable_preview then
vim.fn.timer_stop(preview_timer_id)
preview_timer_id = vim.fn.timer_start(500, preview_timer, { ['repeat'] = 1 })
end
update_result_count()
end
local function previous_item()
local line_number = vim.api.nvim_win_get_cursor(result_winid)[1]
if line_number == 1 then
pcall(
vim.api.nvim_win_set_cursor,
result_winid,
{ vim.api.nvim_buf_line_count(result_bufid), 0 }
)
else
pcall(vim.api.nvim_win_set_cursor, result_winid, { line_number - 1, 0 })
end
if conf.enable_preview then
vim.fn.timer_stop(preview_timer_id)
preview_timer_id = vim.fn.timer_start(500, preview_timer, { ['repeat'] = 1 })
end
update_result_count()
end
local function open_win()
require('flygrep.highlight').def_higroup()
-- 窗口位置
-- 宽度: columns 的 80%
local screen_width = math.floor(vim.o.columns * 0.8)
-- 起始位位置: lines * 10%, columns * 10%
local start_col = math.floor(vim.o.columns * 0.1)
local start_row = math.floor(vim.o.lines * 0.1)
-- 整体高度lines 的 80%
local screen_height = math.floor(vim.o.lines * 0.8)
prompt_bufid = vim.api.nvim_create_buf(false, true)
prompt_winid = vim.api.nvim_open_win(prompt_bufid, true, {
relative = 'editor',
width = screen_width,
height = 1,
col = start_col,
row = start_row + screen_height - 3,
focusable = true,
border = 'rounded',
title = build_prompt_title(),
title_pos = 'left',
-- noautocmd = true,
})
vim.api.nvim_set_option_value(
'winhighlight',
'NormalFloat:Normal,FloatBorder:WinSeparator',
{ win = prompt_winid }
)
vim.api.nvim_set_option_value('number', false, { win = prompt_winid })
vim.api.nvim_set_option_value('relativenumber', false, { win = prompt_winid })
vim.api.nvim_set_option_value('cursorline', false, { win = prompt_winid })
vim.api.nvim_set_option_value('signcolumn', 'yes', { win = prompt_winid })
vim.api.nvim_buf_set_extmark(prompt_bufid, extns, 0, 0, {
sign_text = '>',
sign_hl_group = 'Error',
})
if conf.enable_preview then
if not vim.api.nvim_buf_is_valid(preview_bufid) then
preview_bufid = vim.api.nvim_create_buf(false, true)
end
preview_winid = vim.api.nvim_open_win(preview_bufid, false, {
relative = 'editor',
width = screen_width,
height = math.floor((screen_height - 5) / 2),
col = start_col,
row = start_row,
focusable = false,
border = 'rounded',
-- title = 'Result',
-- title_pos = 'center',
-- noautocmd = true,
})
vim.api.nvim_set_option_value('cursorline', true, { win = preview_winid })
result_bufid = vim.api.nvim_create_buf(false, true)
result_winid = vim.api.nvim_open_win(result_bufid, false, {
relative = 'editor',
width = screen_width,
height = screen_height - 5 - math.floor((screen_height - 5) / 2) - 2,
col = start_col,
row = start_row + math.floor((screen_height - 5) / 2) + 2,
focusable = false,
border = 'rounded',
-- title = 'Result',
-- title_pos = 'center',
-- noautocmd = true,
})
else
result_bufid = vim.api.nvim_create_buf(false, true)
result_winid = vim.api.nvim_open_win(result_bufid, false, {
relative = 'editor',
width = screen_width,
height = screen_height - 5,
col = start_col,
row = start_row,
focusable = false,
border = 'rounded',
-- title = 'Result',
-- title_pos = 'center',
-- noautocmd = true,
})
end
vim.api.nvim_set_option_value(
'winhighlight',
'NormalFloat:Normal,FloatBorder:WinSeparator',
{ win = result_winid }
)
vim.api.nvim_set_option_value('cursorline', true, { win = result_winid })
if ok then
cmp.setup.buffer({
completion = {
autocomplete = false,
},
})
end
vim.cmd('noautocmd startinsert')
local augroup = vim.api.nvim_create_augroup('floatgrep', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'TextChangedI' }, {
group = augroup,
buffer = prompt_bufid,
callback = function(ev)
grep_input = vim.api.nvim_buf_get_lines(prompt_bufid, 0, 1, false)[1]
if grep_input ~= '' then
pcall(vim.fn.matchdelete, search_hi_id, result_winid)
pcall(vim.fn.timer_stop, grep_timer_id)
job.stop(search_jobid)
search_hi_id =
vim.fn.matchadd(conf.matched_higroup, grep_input:gsub('~', '\\~'), 10, -1, { window = result_winid })
grep_timer_id = vim.fn.timer_start(conf.timeout, grep_timer, { ['repeat'] = 1 })
else
pcall(vim.fn.matchdelete, search_hi_id, result_winid)
pcall(vim.fn.timer_stop, grep_timer_id)
job.stop(search_jobid)
search_jobid = 0
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, {})
if conf.enable_preview and vim.api.nvim_buf_is_valid(preview_bufid) then
vim.api.nvim_buf_set_lines(preview_bufid, 0, -1, false, {})
end
end
update_result_count()
end,
})
-- 使用 Esc/C-c 关闭整个界面
for _, k in ipairs({ '<Esc>', '<C-c>' }) do
vim.keymap.set('i', k, function()
vim.cmd('noautocmd stopinsert')
vim.api.nvim_win_close(prompt_winid, true)
vim.api.nvim_buf_set_lines(prompt_bufid, 0, -1, false, {})
vim.api.nvim_win_close(result_winid, true)
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, {})
if conf.enable_preview then
vim.api.nvim_win_close(preview_winid, true)
vim.api.nvim_buf_set_lines(preview_bufid, 0, -1, false, {})
end
end, { buffer = prompt_bufid })
end
-- 使用回车键打开光标所在的搜索结果,同时关闭界面
local function open_item(cmd)
vim.cmd('noautocmd stopinsert')
-- 获取搜索结果光表行
local line_number = vim.api.nvim_win_get_cursor(result_winid)[1]
local filename, linenr, colum =
get_file_pos(vim.api.nvim_buf_get_lines(result_bufid, line_number - 1, line_number, false)[1])
vim.api.nvim_win_close(prompt_winid, true)
vim.api.nvim_buf_set_lines(prompt_bufid, 0, -1, false, {})
vim.api.nvim_win_close(result_winid, true)
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, {})
if conf.enable_preview then
vim.api.nvim_win_close(preview_winid, true)
vim.api.nvim_buf_set_lines(preview_bufid, 0, -1, false, {})
end
vim.cmd(cmd .. ' ' .. filename)
vim.api.nvim_win_set_cursor(0, { linenr, colum })
end
vim.keymap.set('i', '<Enter>', function()
open_item('edit')
end, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-v>', function()
open_item('vsplit')
end, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-s>', function()
open_item('split')
end, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-t>', function()
open_item('tabedit')
end, { buffer = prompt_bufid })
-- 避免使用 jk 切换到 normal 模式
-- https://github.com/neovim/neovim/discussions/32208
-- vim.keymap.del('i', 'jk', {buffer = prompt_bufid})
if vim.fn.hasmapto('j', 'i') == 1 then
vim.keymap.set('i', 'j', 'j', {
nowait = true,
buffer = prompt_bufid,
})
end
-- 使用 Tab/Shift-Tab and Ctrl-jk 上下移动搜素结果
vim.keymap.set('i', '<Tab>', next_item, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-j>', next_item, { buffer = prompt_bufid })
vim.keymap.set('i', '<S-Tab>', previous_item, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-k>', previous_item, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-e>', function()
toggle_fix_string()
update_result_count()
end, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-h>', function()
toggle_hidden_file()
update_result_count()
end, { buffer = prompt_bufid })
vim.keymap.set('i', '<C-p>', function()
toggle_preview_win()
end, { buffer = prompt_bufid })
-- 高亮文件名及位置
vim.fn.matchadd(
'Comment',
[[\([A-Z]:\)\?[^:]*:\d\+:\(\d\+:\)\?]],
11,
-1,
{ window = result_winid }
)
end
function M.open()
open_win()
end
function M.setup(conf)
require('flygrep.config').setup(conf)
end
return M