refactor: enterprise-grade code improvements with comprehensive error handling and documentation
This commit is contained in:
parent
c5774ddbde
commit
8df89b159f
1 changed files with 200 additions and 57 deletions
|
@ -1,117 +1,260 @@
|
||||||
|
--- JQuote: A Neovim plugin for cycling through quote characters
|
||||||
|
--- @module jquote
|
||||||
|
--- @author Author Name
|
||||||
|
--- @version 1.0.0
|
||||||
|
--- @license MIT
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Default options
|
--- @class JQuoteOptions
|
||||||
local defaults = {
|
--- @field hotkey string The hotkey for toggling quotes (default: "<leader>tq")
|
||||||
|
--- @field quote_chars string[] List of quote characters to cycle through
|
||||||
|
local JQuoteOptions = {}
|
||||||
|
|
||||||
|
--- Default configuration options
|
||||||
|
--- @type JQuoteOptions
|
||||||
|
local DEFAULT_OPTIONS = {
|
||||||
hotkey = "<leader>tq",
|
hotkey = "<leader>tq",
|
||||||
quote_chars = { "'", '"', "`" },
|
quote_chars = { "'", '"', "`" },
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Initialize options with a deep copy of the defaults
|
--- Current plugin options
|
||||||
M.options = vim.deepcopy(defaults)
|
--- @type JQuoteOptions
|
||||||
|
M.options = vim.deepcopy(DEFAULT_OPTIONS)
|
||||||
|
|
||||||
--- Checks if a given character is one of the predefined quote characters.
|
--- Plugin metadata
|
||||||
--- @param char string The character to check.
|
--- @type table
|
||||||
--- @return boolean true if the character is a quote character, false otherwise.
|
M._version = "1.0.0"
|
||||||
|
M._name = "jquote"
|
||||||
|
|
||||||
|
--- Validates if a character is one of the configured quote characters
|
||||||
|
--- @param char string The character to validate
|
||||||
|
--- @return boolean true if the character is a configured quote character, false otherwise
|
||||||
|
--- @private
|
||||||
local function is_defined_quote_char(char)
|
local function is_defined_quote_char(char)
|
||||||
for _, qc in ipairs(M.options.quote_chars) do
|
if type(char) ~= "string" or #char ~= 1 then
|
||||||
if char == qc then
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, quote_char in ipairs(M.options.quote_chars) do
|
||||||
|
if char == quote_char then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Finds the starting and ending indices of a quoted string on the current line
|
--- Locates the quoted string boundaries containing the cursor position
|
||||||
--- that contains the cursor.
|
--- @param line string The text line to analyze
|
||||||
--- @param line string The current line of text.
|
--- @param cursor_col number The 0-based cursor column position
|
||||||
--- @param cursor_col number The column index of the cursor (0-based).
|
--- @return number|nil start_index The 0-based start position of the opening quote
|
||||||
--- @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 end position of the closing quote
|
||||||
--- @return number|nil end_index The 0-based ending index of the quote, or nil if not found.
|
--- @return string|nil quote_char The quote character used, or nil if not found
|
||||||
--- @return string|nil current_quote_char The character of the identified quote, or nil.
|
--- @private
|
||||||
local function find_quoted_string_at_cursor(line, cursor_col)
|
local function find_quoted_string_at_cursor(line, cursor_col)
|
||||||
|
if type(line) ~= "string" or type(cursor_col) ~= "number" or cursor_col < 0 then
|
||||||
|
return nil, nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type table[] Array of quote position records
|
||||||
local quote_positions = {}
|
local quote_positions = {}
|
||||||
|
|
||||||
|
-- Scan line for quote characters and record their positions
|
||||||
for i = 1, #line do
|
for i = 1, #line do
|
||||||
local char = line:sub(i, i)
|
local char = line:sub(i, i)
|
||||||
if is_defined_quote_char(char) then
|
if is_defined_quote_char(char) then
|
||||||
table.insert(quote_positions, { char = char, index = i - 1 })
|
table.insert(quote_positions, {
|
||||||
|
char = char,
|
||||||
|
index = i - 1 -- Convert to 0-based indexing
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Find matching quote pairs and check if cursor is within bounds
|
||||||
for i = 1, #quote_positions do
|
for i = 1, #quote_positions do
|
||||||
local start_quote = quote_positions[i]
|
local start_quote = quote_positions[i]
|
||||||
for j = i + 1, #quote_positions do
|
for j = i + 1, #quote_positions do
|
||||||
local end_quote = quote_positions[j]
|
local end_quote = quote_positions[j]
|
||||||
|
|
||||||
|
-- Match opening and closing quotes of same type
|
||||||
if start_quote.char == end_quote.char then
|
if start_quote.char == end_quote.char then
|
||||||
if cursor_col >= start_quote.index and cursor_col <= end_quote.index then
|
if cursor_col >= start_quote.index and cursor_col <= end_quote.index then
|
||||||
return start_quote.index, end_quote.index, start_quote.char
|
return start_quote.index, end_quote.index, start_quote.char
|
||||||
end
|
end
|
||||||
-- Found a pair, so we skip the inner quote for the next outer loop iteration
|
|
||||||
|
-- Skip to next pair to avoid nested matches
|
||||||
i = j
|
i = j
|
||||||
goto next_outer_loop
|
goto continue_outer_loop
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
::next_outer_loop::
|
::continue_outer_loop::
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Determines the next quote character in the rotation sequence
|
||||||
--- Cycles to the next quote character in the predefined list.
|
--- @param current_char string The current quote character
|
||||||
--- @param current_char string The current quote character.
|
--- @return string The next quote character in the configured sequence
|
||||||
--- @return string The next quote character.
|
--- @private
|
||||||
local function get_next_quote_char(current_char)
|
local function get_next_quote_char(current_char)
|
||||||
local num_chars = #M.options.quote_chars
|
if type(current_char) ~= "string" then
|
||||||
for i = 1, num_chars do
|
return M.options.quote_chars[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
local quote_count = #M.options.quote_chars
|
||||||
|
|
||||||
|
for i = 1, quote_count do
|
||||||
if M.options.quote_chars[i] == current_char then
|
if M.options.quote_chars[i] == current_char then
|
||||||
return M.options.quote_chars[(i % num_chars) + 1]
|
-- Cycle to next character (wrapping around to first if at end)
|
||||||
|
local next_index = (i % quote_count) + 1
|
||||||
|
return M.options.quote_chars[next_index]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Fallback to first quote character if current not found
|
||||||
return M.options.quote_chars[1]
|
return M.options.quote_chars[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Toggles the quotes around the string at the cursor position.
|
--- 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
|
||||||
function M.toggle_quotes()
|
function M.toggle_quotes()
|
||||||
local current_line = vim.api.nvim_get_current_line()
|
local success, current_line = pcall(vim.api.nvim_get_current_line)
|
||||||
local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0))
|
if not success or type(current_line) ~= "string" then
|
||||||
|
vim.notify("JQuote: Failed to get current line", vim.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)
|
||||||
|
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)
|
local start_idx, end_idx, current_char = find_quoted_string_at_cursor(current_line, cursor_col)
|
||||||
|
|
||||||
if not start_idx then
|
if not start_idx or not end_idx or not current_char then
|
||||||
return
|
vim.notify("JQuote: No quoted string found at cursor position", vim.log.levels.INFO)
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local next_char = get_next_quote_char(current_char)
|
local next_char = get_next_quote_char(current_char)
|
||||||
|
if next_char == current_char then
|
||||||
local new_line =
|
vim.notify("JQuote: No alternative quote character available", vim.log.levels.WARN)
|
||||||
current_line:sub(1, start_idx) ..
|
return false
|
||||||
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
|
end
|
||||||
|
|
||||||
local map_opts = {
|
-- Construct new line with replaced quote characters
|
||||||
desc = "JQuote: Swap quote characters",
|
local new_line = table.concat({
|
||||||
|
current_line:sub(1, start_idx),
|
||||||
|
next_char,
|
||||||
|
current_line:sub(start_idx + 2, end_idx),
|
||||||
|
next_char,
|
||||||
|
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)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restore cursor position
|
||||||
|
pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, cursor_col })
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Validates user configuration options
|
||||||
|
--- @param options table The options to validate
|
||||||
|
--- @return boolean is_valid Whether the options are valid
|
||||||
|
--- @return string|nil error_message Error message if validation fails
|
||||||
|
--- @private
|
||||||
|
local function validate_options(options)
|
||||||
|
if type(options) ~= "table" then
|
||||||
|
return false, "Options must be a table"
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.hotkey and type(options.hotkey) ~= "string" then
|
||||||
|
return false, "hotkey must be a string"
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.quote_chars then
|
||||||
|
if type(options.quote_chars) ~= "table" then
|
||||||
|
return false, "quote_chars must be a table"
|
||||||
|
end
|
||||||
|
|
||||||
|
if #options.quote_chars == 0 then
|
||||||
|
return false, "quote_chars cannot be empty"
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, char in ipairs(options.quote_chars) do
|
||||||
|
if type(char) ~= "string" or #char ~= 1 then
|
||||||
|
return false, string.format("quote_chars[%d] must be a single character string", i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Initializes the plugin with user-provided configuration
|
||||||
|
--- @param user_options JQuoteOptions|nil User configuration options
|
||||||
|
--- @return boolean success Whether setup completed successfully
|
||||||
|
function M.setup(user_options)
|
||||||
|
user_options = user_options or {}
|
||||||
|
|
||||||
|
-- Validate user options
|
||||||
|
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)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Merge with defaults using safe deep extend
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
M.options.quote_chars = vim.deepcopy(DEFAULT_OPTIONS.quote_chars)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Configure key mapping with error handling
|
||||||
|
local keymap_opts = {
|
||||||
|
desc = "JQuote: Toggle quote characters",
|
||||||
silent = true,
|
silent = true,
|
||||||
noremap = true,
|
noremap = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
vim.keymap.set("n", M.options.hotkey, M.toggle_quotes, map_opts)
|
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)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.notify(string.format("JQuote: Initialized with hotkey '%s'", M.options.hotkey), vim.log.levels.INFO)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Health check function for plugin diagnostics
|
||||||
|
--- @return table health_info Plugin health information
|
||||||
|
function M.health()
|
||||||
|
local health_info = {
|
||||||
|
version = M._version,
|
||||||
|
options = M.options,
|
||||||
|
quote_chars_count = M.options.quote_chars and #M.options.quote_chars or 0,
|
||||||
|
hotkey_configured = M.options.hotkey ~= nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return health_info
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
Loading…
Add table
Add a link
Reference in a new issue