diff --git a/README.md b/README.md index fe0f1a4..ba3d620 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,8 @@ Install using your favorite package manager for Neovim. For example, using [lazy event = "BufReadPost", dependencies = { "nvim-lua/plenary.nvim", - }, - config = function() - require("template-string").setup() - end, + "nvim-treesitter/nvim-treesitter", + } } ``` @@ -53,19 +51,6 @@ const props = { ; ``` -## Configuration - -The `setup` function accepts an optional configuration object with the following options: - -- **jsx_brackets** `boolean | nil`: Enable/disable wrapping template literals with `{``}` inside JSX/TSX components. Defaults to `true` if not provided. - -```lua --- Default configuration -require('template-string').setup({ - jsx_brackets = true, -- Wrap template literals with {``} inside JSX/TSX components -}) -``` - ## License This plugin is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/lua/template-string.lua b/lua/template-string.lua index b174974..ed44eee 100644 --- a/lua/template-string.lua +++ b/lua/template-string.lua @@ -1,108 +1,18 @@ ----@class TemplateStringConfig ----@field jsx_brackets boolean | nil - local M = {} -local allowed_filetypes = { - javascript = true, - typescript = true, - javascriptreact = true, - typescriptreact = true, -} - -local function is_allowed_filetype() - local filetype = vim.bo.filetype - return allowed_filetypes[filetype] or false -end - --- Function to wrap with {``} if inside a JSX/TSX component -local function wrap_with_brackets_if_necessary(content) - if content:find("%${") then - return "={`" .. content .. "`}" - else - return '="' .. content .. '"' - end -end - --- Function to replace quotes in the current line -local function replace_quotes_in_line() - if not is_allowed_filetype() then - return - end - - local row = vim.api.nvim_win_get_cursor(0)[1] - local line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1] - - if not line then - return - end - - local new_line = line - - -- Replace quotes with backticks when ${ is found - new_line = new_line:gsub("(['\"])(.-)%${(.-)}(.-)%1", function(quote, before, inside, after) - return "`" .. before .. "${" .. inside .. "}" .. after .. "`" - end) - - if M.jsx_brackets then - -- Wrap with {``} if inside a JSX/TSX component - new_line = new_line:gsub("=%s*`([^`]*)`", wrap_with_brackets_if_necessary) - - -- Revert backticks to original quotes if ${ is not found - new_line = new_line:gsub("={[`{]+([^`]*)[`}]+}", function(content) - if not content:find("%${") then - -- Determine the original type of quotes, double or single - local original_quote = line:match("=[\"']") and '"' or line:match("=['\"]") and "'" or '"' - return "=" .. original_quote .. content .. original_quote - end - return "={" .. "`" .. content .. "`" .. "}" - end) - end - - -- Also handle reverting solitary backticks on normal lines - new_line = new_line:gsub("`([^`]*)`", function(content) - if not content:find("%${") then - -- Determine the original type of quotes, double or single - local original_quote = line:match("[\"']") or '"' - return original_quote .. content .. original_quote - end - return "`" .. content .. "`" - end) - - if new_line ~= line then - vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line }) - end -end - --- Function to execute update with debounce -local function debounce(fn, ms) - local timer = vim.loop.new_timer() - return function(...) - timer:stop() - local argv = { ... } - timer:start( - ms, - 0, - vim.schedule_wrap(function() - fn(unpack(argv)) - end) - ) - end -end +local debounce = require("template-string.debounce") +local quotes = require("template-string.replace_quotes") ---- Configures the plugin behavior. ----@param opts TemplateStringConfig | nil Optional plugin configuration. -function M.setup(opts) - opts = opts or {} - -- Enable brackets for JSX/TSX - local jsx_brackets = opts.jsx_brackets == nil or opts.jsx_brackets - M.jsx_brackets = jsx_brackets +--- Configures the behavior of the plugin. +function M.setup() + -- Create a debounced version of the replace_quotes_in_line function + local debounced_replace = debounce.debounce(quotes.replace_quotes_in_line, 100) - -- Enable debounce - local debounced_replace = debounce(replace_quotes_in_line, 100) + -- Set up an autocmd to trigger the debounced function on TextChanged and TextChangedI events vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { callback = debounced_replace, }) end +--- @export return M diff --git a/lua/template-string/allowed_filetypes.lua b/lua/template-string/allowed_filetypes.lua new file mode 100644 index 0000000..82162f9 --- /dev/null +++ b/lua/template-string/allowed_filetypes.lua @@ -0,0 +1,20 @@ +local vim = vim + +local M = {} + +-- List of allowed file types for syntax checking +local allowed_filetypes = { + javascript = true, + typescript = true, + javascriptreact = true, + typescriptreact = true, +} + +--- Checks if the current buffer's file type is allowed for syntax checking. +-- @return boolean indicating if the file type is allowed. +function M.is_allowed_filetype() + local filetype = vim.bo.filetype + return allowed_filetypes[filetype] or false +end + +return M diff --git a/lua/template-string/debounce.lua b/lua/template-string/debounce.lua new file mode 100644 index 0000000..c4af56f --- /dev/null +++ b/lua/template-string/debounce.lua @@ -0,0 +1,26 @@ +local vim = vim + +local M = {} + +--- Creates a debounced version of a function. +-- @param fn The function to debounce. +-- @param ms The debounce time in milliseconds. +-- @return The debounced function. +function M.debounce(fn, ms) + local timer = vim.loop.new_timer() -- Create a new timer from Vim's event loop + + return function(...) + timer:stop() -- Stop the timer to reset debounce time + local argv = { ... } -- Capture arguments passed to the debounced function + + timer:start( + ms, + 0, + vim.schedule_wrap(function() + fn(unpack(argv)) -- Call the original function after debounce time + end) + ) + end +end + +return M diff --git a/lua/template-string/react_utils.lua b/lua/template-string/react_utils.lua new file mode 100644 index 0000000..75d1258 --- /dev/null +++ b/lua/template-string/react_utils.lua @@ -0,0 +1,31 @@ +local ts = require("nvim-treesitter.ts_utils") + +local M = {} + +--- Checks if the cursor is inside a React prop or JSX/TSX opening element. +-- @return boolean indicating if the cursor is inside a React prop or JSX/TSX opening element. +function M.is_inside_react_opening_element() + local node = ts.get_node_at_cursor() + + if not node then + return false + end + + -- Check if the node is a JSX/TSX string or template string + if node:type() == "string" or node:type() == "template_string" then + local prev_node = ts.get_previous_node(node) + + if not prev_node then + return false + end + + -- Check if the previous node is a property identifier + if prev_node:type() == "property_identifier" then + return true + end + end + + return false +end + +return M diff --git a/lua/template-string/replace_quotes.lua b/lua/template-string/replace_quotes.lua new file mode 100644 index 0000000..bded6cd --- /dev/null +++ b/lua/template-string/replace_quotes.lua @@ -0,0 +1,56 @@ +local filetypes = require("template-string.allowed_filetypes") +local u = require("template-string.react_utils") +local M = {} + +--- Replaces quotes in a line based on the presence of template literals and context. +-- @param line The line of text to process. +-- @param use_brackets A boolean indicating if brackets should be used. +-- @return The processed line with appropriate quote replacements. +local function replace_quotes(line, use_brackets) + -- Replace quotes with backticks and brackets if ${} is found and use_brackets is true + line = line:gsub("(['\"])(.-)(%${.-})(.-)%1", function(quote, before, inside, after) + if use_brackets then + return "{`" .. before .. inside .. after .. "`}" + else + return "`" .. before .. inside .. after .. "`" + end + end) + + -- Revert backticks and brackets to original quotes if ${} is not found in the content + line = line:gsub("{`([^`]*)`}", function(content) + if not content:find("%${.-}") then + local original_quote = line:match("[\"']") or '"' + return original_quote .. content .. original_quote + end + return "{`" .. content .. "`}" + end) + + -- Revert solitary backticks to original quotes if ${} is not found in the content + line = line:gsub("`([^`]*)`", function(content) + if not content:find("%${.-}") then + local original_quote = line:match("[\"']") or '"' + return original_quote .. content .. original_quote + end + return "`" .. content .. "`" + end) + + return line +end + +--- Replaces quotes in the current line of the buffer if the filetype is allowed. +function M.replace_quotes_in_line() + if not filetypes.is_allowed_filetype() then + return + end + + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1] + local new_line = replace_quotes(line, u.is_inside_react_opening_element()) + + -- Update the buffer if there were changes + if new_line ~= line then + vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line }) + end +end + +return M diff --git a/plugin/template-string.vim b/plugin/template-string.vim new file mode 100644 index 0000000..19cb8b1 --- /dev/null +++ b/plugin/template-string.vim @@ -0,0 +1,7 @@ +if exists('g:loaded_template_string') + finish +endif + +lua require'template-string'.setup() + +let g:loaded_template_string = 1