local M = {} -- Default options local defaults = { hotkey = "tq", quote_chars = { "'", '"', "`" }, } -- Initialize options with a deep copy of the defaults M.options = vim.deepcopy(defaults) --- 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. --- @param line string The current line of text. --- @param cursor_col number The column index of the cursor (0-based). --- @return number|nil start_index The 0-based starting index of the quote, or nil if not found. --- @return number|nil end_index The 0-based ending index of the quote, or nil if not found. --- @return string|nil current_quote_char The character of the identified quote, or nil. local function find_quoted_string_at_cursor(line, cursor_col) local quote_positions = {} for i = 1, #line do local char = line:sub(i, i) if is_defined_quote_char(char) then table.insert(quote_positions, { char = char, index = i - 1 }) end end for i = 1, #quote_positions do local start_quote = quote_positions[i] for j = i + 1, #quote_positions do local end_quote = quote_positions[j] if start_quote.char == end_quote.char then if cursor_col >= start_quote.index and cursor_col <= end_quote.index then return start_quote.index, end_quote.index, start_quote.char end -- Found a pair, so we skip the inner quote for the next outer loop iteration i = j goto next_outer_loop end end ::next_outer_loop:: end return nil, nil, 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. local function get_next_quote_char(current_char) local num_chars = #M.options.quote_chars for i = 1, num_chars do if M.options.quote_chars[i] == current_char then return M.options.quote_chars[(i % num_chars) + 1] end end return M.options.quote_chars[1] end --- Toggles the quotes around the string at the cursor position. 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 not start_idx then return end local next_char = get_next_quote_char(current_char) 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) vim.api.nvim_set_current_line(new_line) vim.api.nvim_win_set_cursor(0, { cursor_row, cursor_col }) end --- Merges user-provided options with default options. --- @param user_options table|nil User-provided options function M.setup(user_options) user_options = user_options or {} M.options = vim.tbl_deep_extend("force", vim.deepcopy(defaults), user_options) -- Ensure quote_chars is never nil if not M.options.quote_chars or #M.options.quote_chars == 0 then M.options.quote_chars = vim.deepcopy(defaults.quote_chars) end local map_opts = { desc = "JQuote: Swap quote characters", silent = true, noremap = true, } vim.keymap.set("n", M.options.hotkey, M.toggle_quotes, map_opts) end return M