refactor: Refactor everything
BREAKING CHANGE: Actually move to the next pair of quotes
This commit is contained in:
parent
9622219679
commit
6d065862b0
6 changed files with 1044 additions and 27 deletions
|
@ -5,9 +5,24 @@
|
|||
|
||||
local M = {}
|
||||
|
||||
--- Plugin constants
|
||||
--- @type table
|
||||
local CONSTANTS = {
|
||||
JUMP_BEHAVIORS = {
|
||||
AUTO = "auto",
|
||||
MANUAL = "manual",
|
||||
},
|
||||
LOG_LEVELS = vim.log.levels,
|
||||
PLUGIN_NAME = "JQuote",
|
||||
MIN_QUOTE_CHARS = 1,
|
||||
MAX_LINE_LENGTH = 10000, -- Safety limit for line processing
|
||||
VERSION_FALLBACK = "unknown",
|
||||
}
|
||||
|
||||
--- @class JQuoteOptions
|
||||
--- @field hotkey string The hotkey for toggling quotes (default: "<leader>tq")
|
||||
--- @field quote_chars string[] List of quote characters to cycle through
|
||||
--- @field quote_chars string[] List of quote characters to cycle through (minimum 1 character required)
|
||||
--- @field jump_behavior "auto"|"manual" Whether to auto-jump to next pair when not inside quotes (default: "auto")
|
||||
local JQuoteOptions = {}
|
||||
|
||||
--- Default configuration options
|
||||
|
@ -15,6 +30,7 @@ local JQuoteOptions = {}
|
|||
local DEFAULT_OPTIONS = {
|
||||
hotkey = "<leader>tq",
|
||||
quote_chars = { "'", '"', "`" },
|
||||
jump_behavior = CONSTANTS.JUMP_BEHAVIORS.AUTO,
|
||||
}
|
||||
|
||||
--- Current plugin options
|
||||
|
@ -30,10 +46,17 @@ M._name = "jquote"
|
|||
--- @return boolean true if the character is a configured quote character, false otherwise
|
||||
--- @private
|
||||
local function is_defined_quote_char(char)
|
||||
-- Input validation with early returns
|
||||
if type(char) ~= "string" or #char ~= 1 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Defensive programming: ensure quote_chars exists and is valid
|
||||
if not M.options.quote_chars or type(M.options.quote_chars) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Use explicit loop for better readability and performance
|
||||
for _, quote_char in ipairs(M.options.quote_chars) do
|
||||
if char == quote_char then
|
||||
return true
|
||||
|
@ -42,6 +65,56 @@ local function is_defined_quote_char(char)
|
|||
return false
|
||||
end
|
||||
|
||||
--- Finds the next quote pair forwards from the current cursor position
|
||||
--- @param line string The text line to analyze
|
||||
--- @param cursor_col number The 0-based cursor column position
|
||||
--- @return number|nil start_index The 0-based start position of the opening quote
|
||||
--- @return number|nil end_index The 0-based end position of the closing quote
|
||||
--- @return string|nil quote_char The quote character used, or nil if not found
|
||||
--- @private
|
||||
local function find_next_quote_pair_forward(line, cursor_col)
|
||||
-- Comprehensive input validation
|
||||
if type(line) ~= "string" or type(cursor_col) ~= "number" or cursor_col < 0 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
-- Safety check for excessively long lines
|
||||
if #line > CONSTANTS.MAX_LINE_LENGTH then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- @type table[] Array of quote position records
|
||||
local quote_positions = {}
|
||||
|
||||
-- Scan line for quote characters starting from cursor position
|
||||
-- Use math.min to prevent out-of-bounds access
|
||||
local start_pos = math.min(cursor_col + 2, #line)
|
||||
for i = start_pos, #line do
|
||||
local char = line:sub(i, i)
|
||||
if is_defined_quote_char(char) then
|
||||
table.insert(quote_positions, {
|
||||
char = char,
|
||||
index = i - 1 -- Convert to 0-based indexing
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Find the first matching quote pair using optimized algorithm
|
||||
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]
|
||||
|
||||
-- Match opening and closing quotes of same type
|
||||
if start_quote.char == end_quote.char then
|
||||
return start_quote.index, end_quote.index, start_quote.char
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- Locates the quoted string boundaries containing the cursor position
|
||||
--- @param line string The text line to analyze
|
||||
--- @param cursor_col number The 0-based cursor column position
|
||||
|
@ -50,10 +123,16 @@ end
|
|||
--- @return string|nil quote_char The quote character used, or nil if not found
|
||||
--- @private
|
||||
local function find_quoted_string_at_cursor(line, cursor_col)
|
||||
-- Comprehensive input validation
|
||||
if type(line) ~= "string" or type(cursor_col) ~= "number" or cursor_col < 0 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
-- Safety check for excessively long lines
|
||||
if #line > CONSTANTS.MAX_LINE_LENGTH then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- @type table[] Array of quote position records
|
||||
local quote_positions = {}
|
||||
|
||||
|
@ -76,7 +155,8 @@ local function find_quoted_string_at_cursor(line, cursor_col)
|
|||
|
||||
-- Match opening and closing quotes of same type
|
||||
if start_quote.char == end_quote.char then
|
||||
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
|
||||
-- Improved cursor boundary check logic
|
||||
if cursor_col >= start_quote.index and cursor_col <= end_quote.index then
|
||||
return start_quote.index, end_quote.index, start_quote.char
|
||||
end
|
||||
|
||||
|
@ -91,17 +171,38 @@ local function find_quoted_string_at_cursor(line, cursor_col)
|
|||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- Utility function for logging with consistent formatting
|
||||
--- @param message string The message to log
|
||||
--- @param level number The log level (from vim.log.levels)
|
||||
--- @private
|
||||
local function log_message(message, level)
|
||||
if type(message) ~= "string" then
|
||||
message = tostring(message)
|
||||
end
|
||||
local formatted_message = string.format("%s: %s", CONSTANTS.PLUGIN_NAME, message)
|
||||
vim.notify(formatted_message, level or CONSTANTS.LOG_LEVELS.INFO)
|
||||
end
|
||||
|
||||
--- Determines the next quote character in the rotation sequence
|
||||
--- @param current_char string The current quote character
|
||||
--- @return string The next quote character in the configured sequence
|
||||
--- @private
|
||||
local function get_next_quote_char(current_char)
|
||||
-- Input validation with defensive programming
|
||||
if type(current_char) ~= "string" then
|
||||
return M.options.quote_chars[1]
|
||||
log_message("Invalid current_char type, using fallback", CONSTANTS.LOG_LEVELS.WARN)
|
||||
return M.options.quote_chars[1] or "'"
|
||||
end
|
||||
|
||||
-- Ensure quote_chars is valid
|
||||
if not M.options.quote_chars or type(M.options.quote_chars) ~= "table" or #M.options.quote_chars == 0 then
|
||||
log_message("Invalid quote_chars configuration, using fallback", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return "'"
|
||||
end
|
||||
|
||||
local quote_count = #M.options.quote_chars
|
||||
|
||||
-- Optimized search with explicit bounds checking
|
||||
for i = 1, quote_count do
|
||||
if M.options.quote_chars[i] == current_char then
|
||||
-- Cycle to next character (wrapping around to first if at end)
|
||||
|
@ -111,41 +212,65 @@ local function get_next_quote_char(current_char)
|
|||
end
|
||||
|
||||
-- Fallback to first quote character if current not found
|
||||
log_message("Current quote character not found in configuration, using first available", CONSTANTS.LOG_LEVELS.WARN)
|
||||
return M.options.quote_chars[1]
|
||||
end
|
||||
|
||||
--- Toggles quote characters around the string at the cursor position
|
||||
--- Changes quotes in a cyclic manner based on the configured quote_chars sequence
|
||||
--- @return boolean true if quotes were successfully toggled, false otherwise
|
||||
--- If cursor is not inside a quote pair, automatically jumps to the next pair forwards
|
||||
--- @return boolean true if quotes were successfully toggled or cursor moved, false otherwise
|
||||
function M.toggle_quotes()
|
||||
-- Enhanced error handling with detailed logging
|
||||
local success, current_line = pcall(vim.api.nvim_get_current_line)
|
||||
if not success or type(current_line) ~= "string" then
|
||||
vim.notify("JQuote: Failed to get current line", vim.log.levels.ERROR)
|
||||
log_message("Failed to get current line", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local success_cursor, cursor_pos = pcall(vim.api.nvim_win_get_cursor, 0)
|
||||
if not success_cursor or type(cursor_pos) ~= "table" or #cursor_pos < 2 then
|
||||
vim.notify("JQuote: Failed to get cursor position", vim.log.levels.ERROR)
|
||||
log_message("Failed to get cursor position", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local cursor_row, cursor_col = cursor_pos[1], cursor_pos[2]
|
||||
local start_idx, end_idx, current_char = find_quoted_string_at_cursor(current_line, cursor_col)
|
||||
|
||||
-- If cursor is not inside a quote pair, handle based on jump behavior setting
|
||||
if not start_idx or not end_idx or not current_char then
|
||||
vim.notify("JQuote: No quoted string found at cursor position", vim.log.levels.INFO)
|
||||
return false
|
||||
if M.options.jump_behavior == CONSTANTS.JUMP_BEHAVIORS.AUTO then
|
||||
local next_start_idx, next_end_idx, _ = find_next_quote_pair_forward(current_line, cursor_col)
|
||||
|
||||
if next_start_idx and next_end_idx then
|
||||
-- Move cursor to the start of the next quote pair
|
||||
local success_move = pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, next_start_idx })
|
||||
if success_move then
|
||||
log_message("Jumped to next quote pair", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return true
|
||||
else
|
||||
log_message("Failed to move cursor to next quote pair", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log_message("No quote pairs found forwards on current line", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log_message("No quoted string found at cursor position", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Toggle quotes for current pair
|
||||
local next_char = get_next_quote_char(current_char)
|
||||
if next_char == current_char then
|
||||
vim.notify("JQuote: No alternative quote character available", vim.log.levels.WARN)
|
||||
log_message("No alternative quote character available", CONSTANTS.LOG_LEVELS.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Construct new line with replaced quote characters
|
||||
local new_line = table.concat({
|
||||
-- Construct new line with replaced quote characters using safe string operations
|
||||
local success_construct, new_line = pcall(table.concat, {
|
||||
current_line:sub(1, start_idx),
|
||||
next_char,
|
||||
current_line:sub(start_idx + 2, end_idx),
|
||||
|
@ -153,14 +278,24 @@ function M.toggle_quotes()
|
|||
current_line:sub(end_idx + 2)
|
||||
})
|
||||
|
||||
local success_set = pcall(vim.api.nvim_set_current_line, new_line)
|
||||
if not success_set then
|
||||
vim.notify("JQuote: Failed to update line", vim.log.levels.ERROR)
|
||||
if not success_construct or type(new_line) ~= "string" then
|
||||
log_message("Failed to construct new line with updated quotes", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Restore cursor position
|
||||
pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, cursor_col })
|
||||
local success_set = pcall(vim.api.nvim_set_current_line, new_line)
|
||||
if not success_set then
|
||||
log_message("Failed to update line", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Restore cursor position with error handling
|
||||
local success_restore = pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, cursor_col })
|
||||
if not success_restore then
|
||||
log_message("Warning: Failed to restore cursor position", CONSTANTS.LOG_LEVELS.WARN)
|
||||
end
|
||||
|
||||
log_message("Quote characters toggled successfully", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return true
|
||||
end
|
||||
|
||||
|
@ -183,8 +318,8 @@ local function validate_options(options)
|
|||
return false, "quote_chars must be a table"
|
||||
end
|
||||
|
||||
if #options.quote_chars == 0 then
|
||||
return false, "quote_chars cannot be empty"
|
||||
if #options.quote_chars < CONSTANTS.MIN_QUOTE_CHARS then
|
||||
return false, string.format("quote_chars must contain at least %d character(s)", CONSTANTS.MIN_QUOTE_CHARS)
|
||||
end
|
||||
|
||||
for i, char in ipairs(options.quote_chars) do
|
||||
|
@ -194,6 +329,16 @@ local function validate_options(options)
|
|||
end
|
||||
end
|
||||
|
||||
if options.jump_behavior then
|
||||
if type(options.jump_behavior) ~= "string" then
|
||||
return false, "jump_behavior must be a string"
|
||||
end
|
||||
|
||||
if options.jump_behavior ~= CONSTANTS.JUMP_BEHAVIORS.AUTO and options.jump_behavior ~= CONSTANTS.JUMP_BEHAVIORS.MANUAL then
|
||||
return false, "jump_behavior must be either 'auto' or 'manual'"
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
|
@ -203,25 +348,25 @@ end
|
|||
function M.setup(user_options)
|
||||
user_options = user_options or {}
|
||||
|
||||
-- Validate user options
|
||||
-- Validate user options with enhanced error reporting
|
||||
local is_valid, error_msg = validate_options(user_options)
|
||||
if not is_valid then
|
||||
vim.notify(string.format("JQuote setup error: %s", error_msg), vim.log.levels.ERROR)
|
||||
log_message(string.format("Setup error: %s", error_msg), CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Merge with defaults using safe deep extend
|
||||
-- Merge with defaults using safe deep extend with enhanced error handling
|
||||
local success, merged_options = pcall(vim.tbl_deep_extend, "force", vim.deepcopy(DEFAULT_OPTIONS), user_options)
|
||||
if not success then
|
||||
vim.notify("JQuote: Failed to merge configuration options", vim.log.levels.ERROR)
|
||||
log_message("Failed to merge configuration options", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
M.options = merged_options
|
||||
|
||||
-- Defensive check for quote_chars integrity
|
||||
if not M.options.quote_chars or #M.options.quote_chars == 0 then
|
||||
vim.notify("JQuote: quote_chars corrupted, restoring defaults", vim.log.levels.WARN)
|
||||
-- Enhanced defensive check for quote_chars integrity
|
||||
if not M.options.quote_chars or type(M.options.quote_chars) ~= "table" or #M.options.quote_chars < CONSTANTS.MIN_QUOTE_CHARS then
|
||||
log_message("quote_chars corrupted or invalid, restoring defaults", CONSTANTS.LOG_LEVELS.WARN)
|
||||
M.options.quote_chars = vim.deepcopy(DEFAULT_OPTIONS.quote_chars)
|
||||
end
|
||||
|
||||
|
@ -234,14 +379,45 @@ function M.setup(user_options)
|
|||
|
||||
local success_keymap = pcall(vim.keymap.set, "n", M.options.hotkey, M.toggle_quotes, keymap_opts)
|
||||
if not success_keymap then
|
||||
vim.notify(string.format("JQuote: Failed to set keymap for '%s'", M.options.hotkey), vim.log.levels.ERROR)
|
||||
log_message(string.format("Failed to set keymap for '%s'", M.options.hotkey), CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
vim.notify(string.format("JQuote: Initialized with hotkey '%s'", M.options.hotkey), vim.log.levels.INFO)
|
||||
log_message(string.format("Initialized with hotkey '%s'", M.options.hotkey), CONSTANTS.LOG_LEVELS.INFO)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Gets the plugin version from git tags at runtime
|
||||
--- @return string version The current plugin version or "unknown" if unavailable
|
||||
function M.get_version()
|
||||
local success, handle = pcall(io.popen, "git describe --tags --abbrev=0 2>/dev/null")
|
||||
if not success or not handle then
|
||||
return CONSTANTS.VERSION_FALLBACK
|
||||
end
|
||||
|
||||
local version = handle:read("*a")
|
||||
local close_success = pcall(handle.close, handle)
|
||||
|
||||
if not close_success then
|
||||
log_message("Warning: Failed to close git command handle", CONSTANTS.LOG_LEVELS.WARN)
|
||||
end
|
||||
|
||||
if version and type(version) == "string" and #version > 0 then
|
||||
-- Remove trailing whitespace and newlines
|
||||
version = version:gsub("%s+$", "")
|
||||
-- Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
|
||||
if version:match("^v%d") then
|
||||
version = version:sub(2)
|
||||
end
|
||||
-- Additional validation for semantic versioning format
|
||||
if version:match("^%d+%.%d+%.%d+") then
|
||||
return version
|
||||
end
|
||||
end
|
||||
|
||||
return CONSTANTS.VERSION_FALLBACK
|
||||
end
|
||||
|
||||
--- Health check function for plugin diagnostics
|
||||
--- @return table health_info Plugin health information
|
||||
function M.health()
|
||||
|
@ -249,6 +425,8 @@ function M.health()
|
|||
options = M.options,
|
||||
quote_chars_count = M.options.quote_chars and #M.options.quote_chars or 0,
|
||||
hotkey_configured = M.options.hotkey ~= nil,
|
||||
jump_behavior_valid = M.options.jump_behavior == CONSTANTS.JUMP_BEHAVIORS.AUTO or M.options.jump_behavior == CONSTANTS.JUMP_BEHAVIORS.MANUAL,
|
||||
plugin_version = M.get_version(),
|
||||
}
|
||||
|
||||
return health_info
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue