local M = {} M.options = { hotkey = "tq", quote_chars = { "'", '"', "`" }, } ---Merges user-provided options with default options. ---@param user_options table|nil User-provided options function M.setup(user_options) if user_options and type(user_options) == "table" then for k, v in pairs(user_options) do if v ~= nil then M.options[k] = v end end end local map_opts = { desc = "Swaps the quotes around", silent = true, noremap = true, } vim.keymap.set("n", M.options.hotkey, M.toggle_quotes, map_opts) end --- Checks if a given character is one of the predefined quote characters. --- @param char string The character to check. --- @return boolean true if the character is a quote character, false otherwise. local function is_defined_quote_char(char) for _, qc in ipairs(M.options.quote_chars) do if char == qc then return true end end return false end --- Finds the starting and ending indices of a quoted string on the current line --- that contains the cursor, using a predefined set of quote characters. --- @param line string The current line of text. --- @param cursor_col number The column index of the cursor (0-based). --- @return number start_index The 0-based starting index of the quote, or -1 if not found. --- @return number end_index The 0-based ending index of the quote, or -1 if not found. --- @return string current_quote_char The character of the identified quote, or nil. local function find_quoted_string_at_cursor(line, cursor_col) local start_quote_index = -1 local end_quote_index = -1 local current_quote_char = nil -- Iterate through the line to find a matching pair of quotes that contain the cursor. for i = 0, #line - 1 do local char = line:sub(i + 1, i + 1) if not is_defined_quote_char(char) then goto continue_loop -- Not a quote character, skip. end -- If we haven't found an opening quote yet, this is a potential candidate. if start_quote_index == -1 then start_quote_index = i current_quote_char = char elseif char == current_quote_char then -- Found a closing quote that matches the current opening quote. end_quote_index = i -- Check if the cursor is within this identified quoted string (inclusive of quotes). if (cursor_col >= start_quote_index and cursor_col <= end_quote_index) or (cursor_col <= start_quote_index and cursor_col <= end_quote_index) then return start_quote_index, end_quote_index, current_quote_char else -- Cursor is not in this string, reset to look for the next potential string. start_quote_index = -1 end_quote_index = -1 current_quote_char = nil end end ::continue_loop:: end -- No relevant quoted string found that contains the cursor. return -1, -1, nil end --- Cycles to the next quote character in the predefined list. --- @param current_char string The current quote character. --- @return string The next quote character, or nil if current_char is not found. local function get_next_quote_char(current_char) local current_char_list_index = -1 for i, qc in ipairs(M.options.quote_chars) do if qc == current_char then current_char_list_index = i break end end if current_char_list_index == -1 then return nil -- Current character not in our defined list. end local next_char_list_index = (current_char_list_index % #M.options.quote_chars) + 1 return M.options.quote_chars[next_char_list_index] end --- Toggles the quotes around the string at the cursor position --- by cycling through a predefined list of quote characters. function M.toggle_quotes() local current_line = vim.api.nvim_get_current_line() local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) local start_idx, end_idx, current_char = find_quoted_string_at_cursor(current_line, cursor_col) -- If no quoted string containing the cursor was found, do nothing. if start_idx == -1 or end_idx == -1 or not current_char then return end local next_char = get_next_quote_char(current_char) if not next_char then -- This should ideally not happen if current_char came from find_quoted_string_at_cursor -- and QUOTE_CHARS is consistent, but acts as a safeguard. return end -- Reconstruct the line with the new quote characters. local new_line = current_line:sub(1, start_idx) .. next_char .. current_line:sub(start_idx + 2, end_idx) .. next_char .. current_line:sub(end_idx + 2) -- Update Neovim's buffer and cursor position. vim.api.nvim_set_current_line(new_line) vim.api.nvim_win_set_cursor(0, { cursor_row, cursor_col }) end return M