Skip to content

Commit

Permalink
feat(nvim): Add fancy search/replace UI powered by nvim-spectre
Browse files Browse the repository at this point in the history
  • Loading branch information
mrjones2014 committed Mar 28, 2024
1 parent ec7e02f commit d698cf7
Show file tree
Hide file tree
Showing 7 changed files with 502 additions and 1 deletion.
2 changes: 1 addition & 1 deletion home-manager/modules/fish.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ in {
};

home.packages = with pkgs;
[ thefuck tealdeer tokei cachix _1password btop ]
[ tealdeer tokei cachix _1password btop ]
++ lib.lists.optionals isLinux [ xclip ];

imports = [ inputs._1password-shell-plugins.hmModules.default ];
Expand Down
6 changes: 6 additions & 0 deletions home-manager/modules/nvim.nix
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ in {
clippy
glow
mysql
gnused
# for macOS + nvim-spectre, it expects the gsed package
(pkgs.writeScriptBin "gsed" ''
#!${pkgs.bash}/bin/bash
${pkgs.gnused}/bin/sed "$@"
'')
];
};
}
158 changes: 158 additions & 0 deletions nvim/lua/my/configure/spectre/components/search-tree.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
local n = require('nui-components')

local function replace_handler(tree, node)
return {
on_done = function(result)
if result.ref then
node.ref = result.ref
tree:render()
end
end,
on_error = function(_) end,
}
end

local function mappings(search_query, replace_query)
local spectre_state_utils = require('spectre.state_utils')

return function(component)
return {
{
mode = { 'n' },
key = 'r',
handler = function()
local tree = component:get_tree()
local focused_node = component:get_focused_node()

if not focused_node then
return
end

local has_children = focused_node:has_children()

if not has_children then
local replacer_creator = spectre_state_utils.get_replace_creator()
local replacer =
replacer_creator:new(spectre_state_utils.get_replace_engine_config(), replace_handler(tree, focused_node))

local entry = focused_node.entry

replacer:replace({
lnum = entry.lnum,
col = entry.col,
cwd = vim.fn.getcwd(),
display_lnum = 0,
filename = entry.filename,
search_text = search_query:get_value(),
replace_text = replace_query:get_value(),
})
end
end,
},
}
end
end

local function prepare_node(node, line, component)
local _, devicons = pcall(require, 'nvim-web-devicons')
local has_children = node:has_children()

line:append(string.rep(' ', node:get_depth() - 1))

if has_children then
local icon, icon_highlight = devicons.get_icon(node.text, string.match(node.text, '%a+$'), { default = true })

line:append(node:is_expanded() and '' or '', component:hl_group('SpectreIcon'))
line:append(icon .. ' ', icon_highlight)
line:append(node.text, component:hl_group('SpectreFileName'))

return line
end

local is_replacing = #node.diff.replace > 0
local search_highlight_group = component:hl_group(is_replacing and 'SpectreSearchOldValue' or 'SpectreSearchValue')
local default_text_highlight = component:hl_group('SpectreCodeLine')

local _, empty_spaces = string.find(node.diff.text, '^%s*')
local ref = node.ref

if ref then
line:append('', component:hl_group('SpectreReplaceSuccess'))
end

if #node.diff.search > 0 then
local code_text = vim.trim(node.diff.text)

vim.iter(ipairs(node.diff.search)):each(function(index, value)
local start = value[1] - empty_spaces
local end_ = value[2] - empty_spaces

if index == 1 then
line:append(string.sub(code_text, 1, start), default_text_highlight)
end

local search_text = string.sub(code_text, start + 1, end_)
line:append(search_text, search_highlight_group)

local replace_diff_value = node.diff.replace[index]

if replace_diff_value then
local replace_text =
string.sub(code_text, replace_diff_value[1] + 1 - empty_spaces, replace_diff_value[2] - empty_spaces)
line:append(replace_text, component:hl_group('SpectreSearchNewValue'))
end_ = replace_diff_value[2] - empty_spaces
end

if index == #node.diff.search then
line:append(string.sub(code_text, end_ + 1), default_text_highlight)
end
end)
end

return line
end

local function on_select(origin_winid)
return function(node, component)
local tree = component:get_tree()

if node:has_children() then
if node:is_expanded() then
node:collapse()
else
node:expand()
end

return tree:render()
end

local entry = node.entry

if vim.api.nvim_win_is_valid(origin_winid) then
local escaped_filename = vim.fn.fnameescape(entry.filename)

vim.api.nvim_set_current_win(origin_winid)
vim.api.nvim_command([[execute "normal! m` "]])
vim.cmd('e ' .. escaped_filename)
vim.api.nvim_win_set_cursor(0, { entry.lnum, entry.col - 1 })
end
end
end

local function search_tree(props)
return n.tree({
border_style = 'none',
flex = 1,
padding = {
left = 1,
right = 1,
},
hidden = props.hidden,
data = props.data,
mappings = mappings(props.search_query, props.replace_query),
prepare_node = prepare_node,
on_select = on_select(props.origin_winid),
})
end

return search_tree
113 changes: 113 additions & 0 deletions nvim/lua/my/configure/spectre/engine.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
local spectre_search = require('spectre.search')
local spectre_state = require('spectre.state')
local spectre_state_utils = require('spectre.state_utils')
local spectre_utils = require('spectre.utils')

local Tree = require('nui.tree')

local M = {}

function M.process(options)
options = options or {}

return vim
.iter(spectre_state.groups)
:map(function(filename, group)
local children = vim
.iter(ipairs(group))
:map(function(_, entry)
local id = tostring(math.random())

local diff = spectre_utils.get_hl_line_text({
search_query = options.search_query,
replace_query = options.replace_query,
search_text = entry.text,
padding = 0,
}, spectre_state.regex)

return Tree.Node({ text = diff.text, _id = id, diff = diff, entry = entry })
end)
:totable()

local id = tostring(math.random())
local node = Tree.Node({ text = filename:gsub('^./', ''), _id = id }, children)

node:expand()

return node
end)
:totable()
end

local function search_handler(options, signal)
local start_time = 0
local total = 0

spectre_state.groups = {}

return {
on_start = function()
spectre_state.is_running = true
start_time = vim.loop.hrtime()
end,
on_result = function(item)
if not spectre_state.is_running then
return
end

if not spectre_state.groups[item.filename] then
spectre_state.groups[item.filename] = {}
end

table.insert(spectre_state.groups[item.filename], item)
total = total + 1
end,
on_error = function(_) end,
on_finish = function()
if not spectre_state.is_running then
return
end

local end_time = (vim.loop.hrtime() - start_time) / 1E9

signal.search_results = M.process(options)
signal.search_info = string.format('Total: %s match, time: %ss', total, end_time)

spectre_state.finder_instance = nil
spectre_state.is_running = false
end,
}
end

function M.stop()
if not spectre_state.finder_instance then
return
end

spectre_state.finder_instance:stop()
spectre_state.finder_instance = nil
end

function M.search(options, signal)
options = options or {}

M.stop()

local search_engine = spectre_search['rg']
spectre_state.options['ignore-case'] = not options.is_case_insensitive_checked
spectre_state.finder_instance =
search_engine:new(spectre_state_utils.get_search_engine_config(), search_handler(options, signal))
spectre_state.regex = require('spectre.regex.vim')

pcall(function()
spectre_state.finder_instance:search({
cwd = vim.fn.getcwd(),
search_text = options.search_query,
replace_query = options.replace_query,
-- path = spectre_state.query.path,
search_paths = #options.search_paths > 0 and options.search_paths or nil,
})
end)
end

return M
16 changes: 16 additions & 0 deletions nvim/lua/my/configure/spectre/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
return {
'nvim-pack/nvim-spectre',
dependencies = {
{ 'grapp-dev/nui-components.nvim', dependencies = { 'MunifTanjim/nui.nvim' } },
},
keys = {
{
'<C-f>',
function()
require('my.configure.spectre.ui').toggle()
end,
desc = 'Global Search & Replace',
},
},
opts = {},
}
Loading

0 comments on commit d698cf7

Please sign in to comment.