From af98249a9dedccd929235da8a565be1d0cfcc058 Mon Sep 17 00:00:00 2001 From: psychhim Date: Fri, 10 Oct 2025 19:27:20 +0530 Subject: [PATCH] Add feature: replace selection with all occurances in visual mode. Fix: replace word under cursor with all occurances. Fix: Telescope again starts in insert mode. --- lua/custom/plugins/telescope.lua | 1 - lua/keymaps.lua | 90 +++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/lua/custom/plugins/telescope.lua b/lua/custom/plugins/telescope.lua index 3f42364..2f8381a 100644 --- a/lua/custom/plugins/telescope.lua +++ b/lua/custom/plugins/telescope.lua @@ -22,7 +22,6 @@ return { -- See `:help telescope` and `:help telescope.setup()` require('telescope').setup { defaults = { - initial_mode = 'normal', -- Start in normal mode instead of insert mappings = { i = { [''] = false, diff --git a/lua/keymaps.lua b/lua/keymaps.lua index 72d1b58..e89a8a4 100644 --- a/lua/keymaps.lua +++ b/lua/keymaps.lua @@ -7,22 +7,84 @@ vim.keymap.set('n', 'k', 'zz', { desc = 'Scroll up and center curso vim.keymap.set('n', 'n', 'nzzzv') vim.keymap.set('n', 'N', 'Nzzzv') --- [[ Replace all occurrences of the word under cursor ]] +-- [[ Replace all occurrences of word under cursor (normal) or selection (visual) ]] +-- Normal mode vim.keymap.set('n', 'F', function() - -- Save current cursor position - local pos = vim.api.nvim_win_get_cursor(0) - -- Get the word under the cursor + -- Get word under cursor local word = vim.fn.expand '' - -- Ask user for the replacement - local replacement = vim.fn.input("Replace '" .. word .. "' with: ") - -- If user typed something, do the substitution - if replacement ~= '' then - -- %%s/.../.../gI = substitute globally, case-insensitive - vim.cmd(string.format('%%s/\\<%s\\>/%s/gI', word, replacement)) + if word == '' then + return end - -- Restore cursor position - vim.api.nvim_win_set_cursor(0, pos) -end, { desc = 'Replace all occurrences of word under cursor' }) + -- Escape for literal search + local esc_word = vim.fn.escape(word, '/\\') + -- Count occurrences in the buffer (case-insensitive) + local occurrences = vim.fn.searchcount({ pattern = '\\<' .. esc_word .. '\\>', maxcount = 0, exact = 1 }).total + -- Prompt with only the count + local replacement = vim.fn.input(string.format('Replace %d occurrences with: ', occurrences)) + if replacement == '' then + return + end + -- Escape replacement + local esc_replacement = vim.fn.escape(replacement, '\\/&') + -- Save cursor position before substitution + local original_pos = vim.api.nvim_win_get_cursor(0) + -- Perform global, case-insensitive substitution + vim.cmd(string.format('silent! %%s/\\<%s\\>/%s/gI', esc_word, esc_replacement)) + -- Move cursor at the last character of the first replaced occurrence + vim.schedule(function() + local pattern = '\\<' .. esc_word .. '\\>' + vim.fn.cursor(original_pos) + vim.cmd 'silent! normal! n' + local row, col = unpack(vim.api.nvim_win_get_cursor(0)) + pcall(vim.api.nvim_win_set_cursor, 0, { row, col + #replacement + 1 }) + end) +end, { desc = 'Replace all occurrences of word under cursor', silent = true }) +-- Visual mode +vim.keymap.set('x', 'F', function() + -- Exit visual mode so input() works + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 'nx', false) + vim.schedule(function() + local mode = vim.fn.visualmode() + local start_pos = vim.api.nvim_buf_get_mark(0, '<') + local end_pos = vim.api.nvim_buf_get_mark(0, '>') + local start_row, start_col = start_pos[1] - 1, start_pos[2] + local end_row, end_col = end_pos[1] - 1, end_pos[2] + 1 + if mode == 'V' then + start_col = 0 + local line = vim.api.nvim_buf_get_lines(0, end_row, end_row + 1, false)[1] or '' + end_col = #line + end + local lines = vim.api.nvim_buf_get_text(0, start_row, start_col, end_row, end_col, {}) + if not lines or #lines == 0 then + return + end + local selection = table.concat(lines, '\n') + if selection == '' then + return + end + -- Escape selection for literal search + local esc_selection = vim.fn.escape(selection, '/\\'):gsub('\n', '\\n') + -- Count occurrences in the buffer + local count = vim.fn.searchcount({ pattern = esc_selection, maxcount = 0, exact = 1 }).total + -- Show prompt with only number of occurrences + local replacement = vim.fn.input(string.format('Replace %d occurrences with: ', count)) + if replacement == '' then + return + end + local esc_replacement = vim.fn.escape(replacement, '\\/&'):gsub('\n', '\\n') + local original_pos = { start_row + 1, start_col + 1 } + -- Silent global substitution + vim.cmd(string.format('silent! %%s/\\V%s/%s/g', esc_selection, esc_replacement)) + -- Move cursor at the last character of the first replaced occurrence + vim.schedule(function() + vim.fn.cursor(original_pos) + vim.cmd 'silent! normal! n' + local found_row, found_col = unpack(vim.api.nvim_win_get_cursor(0)) + -- +1 to put cursor after the last character of replacement + pcall(vim.api.nvim_win_set_cursor, 0, { found_row, found_col + #replacement }) + end) + end) +end, { desc = 'Replace all occurrences of selection', silent = true }) -- [[ Basic Keymaps ]] -- Keymaps for better default experience @@ -335,7 +397,7 @@ vim.keymap.set('n', 'gf', function() smart_open_file(path) end, { desc = 'Smart gf: open file under cursor in new tab or reuse buffer' }) --- [[ Function to exit insert/visual/command/terminal modes ]] +-- [[ Remap Tab+q to exit insert/visual/command/terminal modes ]] local function tab_q_escape() local mode = vim.api.nvim_get_mode().mode if mode:match '[iIcR]' then