diff --git a/lua/jquote/init.lua b/lua/jquote/init.lua index 0726a50..f4b1512 100644 --- a/lua/jquote/init.lua +++ b/lua/jquote/init.lua @@ -1,25 +1,13 @@ local M = {} -M.options = {} - +-- Default options local defaults = { hotkey = "tq", quote_chars = { "'", '"', "`" }, } ----Merges user-provided options with default options. ----@param user_options table|nil User-provided options -function M.setup(user_options) - M.options = vim.tbl_deep_extend("force", {}, defaults, user_options or {}) - - 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 +-- 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. @@ -34,92 +22,67 @@ local function is_defined_quote_char(char) 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. +--- 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 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. +--- @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 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. + 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 - - -- 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 + 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, or nil if current_char is not found. +--- @return string The next quote character. 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 + 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 - - 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] + return M.options.quote_chars[1] end ---- Toggles the quotes around the string at the cursor position ---- by cycling through a predefined list of quote characters. +--- 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) + 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 + if not start_idx 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 .. @@ -127,9 +90,22 @@ function M.toggle_quotes() 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 +--- Merges user-provided options with default options. +--- @param user_options table|nil User-provided options +function M.setup(user_options) + M.options = vim.tbl_deep_extend("force", vim.deepcopy(defaults), user_options or {}) + + 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 \ No newline at end of file