mirror of
https://github.com/windwp/nvim-ts-autotag.git
synced 2025-01-01 12:49:15 -06:00
feat: Add close tag after inputting / (#116)
* Add close tag on slash * Add tests for close tag on slash * Simplify "find_start_tag" function with reusing existing "find_child_match"
This commit is contained in:
parent
40615e9607
commit
e254b306fb
@ -1,8 +1,8 @@
|
||||
local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils')
|
||||
local configs = require('nvim-treesitter.configs')
|
||||
local parsers = require('nvim-treesitter.parsers')
|
||||
local log = require('nvim-ts-autotag._log')
|
||||
local utils = require('nvim-ts-autotag.utils')
|
||||
local _, ts_utils = pcall(require, "nvim-treesitter.ts_utils")
|
||||
local configs = require("nvim-treesitter.configs")
|
||||
local parsers = require("nvim-treesitter.parsers")
|
||||
local log = require("nvim-ts-autotag._log")
|
||||
local utils = require("nvim-ts-autotag.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
@ -23,7 +23,7 @@ M.tbl_skipTag = {
|
||||
'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr', 'menuitem'
|
||||
}
|
||||
|
||||
local ERROR_TAG = 'ERROR'
|
||||
local ERROR_TAG = "ERROR"
|
||||
|
||||
-- stylua: ignore
|
||||
local HTML_TAG = {
|
||||
@ -99,6 +99,7 @@ local all_tag = {
|
||||
}
|
||||
M.enable_rename = true
|
||||
M.enable_close = true
|
||||
M.enable_close_on_slash = true
|
||||
|
||||
M.setup = function(opts)
|
||||
opts = opts or {}
|
||||
@ -148,10 +149,10 @@ local function is_in_template_tag()
|
||||
|
||||
local current_node = cursor_node
|
||||
while not (has_element and has_template_string) and current_node do
|
||||
if not has_element and current_node:type() == 'element' then
|
||||
if not has_element and current_node:type() == "element" then
|
||||
has_element = true
|
||||
end
|
||||
if not has_template_string and current_node:type() == 'template_string' then
|
||||
if not has_template_string and current_node:type() == "template_string" then
|
||||
has_template_string = true
|
||||
end
|
||||
current_node = current_node:parent()
|
||||
@ -175,14 +176,11 @@ local function find_child_match(opts)
|
||||
if target == nil or pattern == nil then
|
||||
return nil
|
||||
end
|
||||
local tbl_pattern = vim.split(pattern, '|')
|
||||
local tbl_pattern = vim.split(pattern, "|")
|
||||
for _, ptn in pairs(tbl_pattern) do
|
||||
for node in target:iter_children() do
|
||||
local node_type = node:type()
|
||||
if node_type ~= nil
|
||||
and node_type == ptn
|
||||
and not is_in_table(skip_tag_pattern, node_type)
|
||||
then
|
||||
if node_type ~= nil and node_type == ptn and not is_in_table(skip_tag_pattern, node_type) then
|
||||
return node
|
||||
end
|
||||
end
|
||||
@ -197,7 +195,7 @@ local function find_parent_match(opts)
|
||||
if target == nil or pattern == nil then
|
||||
return nil
|
||||
end
|
||||
local tbl_pattern = vim.split(pattern, '|')
|
||||
local tbl_pattern = vim.split(pattern, "|")
|
||||
for _, ptn in pairs(tbl_pattern) do
|
||||
local cur_node = target
|
||||
local cur_depth = 0
|
||||
@ -224,7 +222,7 @@ local function get_tag_name(node)
|
||||
if node ~= nil then
|
||||
tag_name = utils.get_node_text(node)[1]
|
||||
if tag_name and #tag_name > 3 then
|
||||
tag_name = tag_name:gsub('</', ''):gsub('>', ''):gsub('<', '')
|
||||
tag_name = tag_name:gsub("</", ""):gsub(">", ""):gsub("<", "")
|
||||
end
|
||||
end
|
||||
return tag_name
|
||||
@ -255,8 +253,8 @@ local function find_tag_node(opt)
|
||||
end
|
||||
local name_node = node
|
||||
local tbl_name_pattern = {}
|
||||
if string.match(name_tag_pattern, '%|') then
|
||||
tbl_name_pattern = vim.split(name_tag_pattern, '|')
|
||||
if string.match(name_tag_pattern, "%|") then
|
||||
tbl_name_pattern = vim.split(name_tag_pattern, "|")
|
||||
for _, pattern in pairs(tbl_name_pattern) do
|
||||
name_node = find_child_match({
|
||||
target = node,
|
||||
@ -268,7 +266,7 @@ local function find_tag_node(opt)
|
||||
end
|
||||
end
|
||||
|
||||
tbl_name_pattern = vim.split(name_tag_pattern, '>')
|
||||
tbl_name_pattern = vim.split(name_tag_pattern, ">")
|
||||
for _, pattern in pairs(tbl_name_pattern) do
|
||||
name_node = find_child_match({
|
||||
target = name_node,
|
||||
@ -288,12 +286,52 @@ local function find_child_tag_node(opt)
|
||||
return find_tag_node(opt)
|
||||
end
|
||||
|
||||
local function check_close_tag()
|
||||
local function find_start_tag(current)
|
||||
local ts_tag = get_ts_tag()
|
||||
if not ts_tag then
|
||||
return nil
|
||||
end
|
||||
|
||||
if current:type() ~= "ERROR" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local target = nil
|
||||
|
||||
target = find_child_match({
|
||||
target = current:parent(),
|
||||
pattern = ts_tag.start_tag_pattern,
|
||||
})
|
||||
|
||||
if target ~= nil then
|
||||
return target
|
||||
end
|
||||
|
||||
target = find_child_match({
|
||||
target = current,
|
||||
pattern = ts_tag.start_tag_pattern,
|
||||
})
|
||||
|
||||
return target
|
||||
end
|
||||
|
||||
local function check_close_tag(close_slash_tag)
|
||||
local ts_tag = get_ts_tag()
|
||||
if not ts_tag then
|
||||
return false
|
||||
end
|
||||
|
||||
local target = nil
|
||||
|
||||
if close_slash_tag then
|
||||
-- Find start node from non closed tag
|
||||
local current = ts_utils.get_node_at_cursor()
|
||||
|
||||
target = find_start_tag(current)
|
||||
end
|
||||
|
||||
local tag_node = find_tag_node({
|
||||
target = target,
|
||||
tag_pattern = ts_tag.start_tag_pattern,
|
||||
name_tag_pattern = ts_tag.start_name_tag_pattern,
|
||||
skip_tag_pattern = ts_tag.skip_tag_pattern,
|
||||
@ -318,9 +356,7 @@ local function check_close_tag()
|
||||
if close_tag_node ~= nil then
|
||||
local start_row = tag_node:range()
|
||||
local close_start_row = close_tag_node:range()
|
||||
if start_row == close_start_row
|
||||
and tag_name == get_tag_name(close_tag_node)
|
||||
then
|
||||
if start_row == close_start_row and tag_name == get_tag_name(close_tag_node) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
@ -343,7 +379,18 @@ M.close_tag = function()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
M.close_slash_tag = function()
|
||||
local buf_parser = parsers.get_parser()
|
||||
if not buf_parser then
|
||||
return
|
||||
end
|
||||
buf_parser:parse()
|
||||
local result, tag_name = check_close_tag(true)
|
||||
if result == true and tag_name ~= nil then
|
||||
vim.api.nvim_put({ string.format("%s>", tag_name) }, "", true, true)
|
||||
vim.cmd([[normal! F>]])
|
||||
end
|
||||
end
|
||||
|
||||
local function replace_text_node(node, tag_name)
|
||||
if node == nil then
|
||||
@ -352,9 +399,7 @@ local function replace_text_node(node, tag_name)
|
||||
local start_row, start_col, end_row, end_col = node:range()
|
||||
if start_row == end_row then
|
||||
local line = vim.fn.getline(start_row + 1)
|
||||
local newline = line:sub(0, start_col)
|
||||
.. tag_name
|
||||
.. line:sub(end_col + 1, string.len(line))
|
||||
local newline = line:sub(0, start_col) .. tag_name .. line:sub(end_col + 1, string.len(line))
|
||||
vim.fn.setline(start_row + 1, { newline })
|
||||
end
|
||||
end
|
||||
@ -364,9 +409,7 @@ local function validate_tag_regex(node, start_regex, end_regex)
|
||||
return false
|
||||
end
|
||||
local texts = utils.get_node_text(node)
|
||||
if string.match(texts[1], start_regex)
|
||||
and string.match(texts[#texts], end_regex)
|
||||
then
|
||||
if string.match(texts[1], start_regex) and string.match(texts[#texts], end_regex) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@ -377,11 +420,11 @@ end
|
||||
-- end
|
||||
|
||||
local function validate_start_tag(node)
|
||||
return validate_tag_regex(node, '^%<%w', '%>$')
|
||||
return validate_tag_regex(node, "^%<%w", "%>$")
|
||||
end
|
||||
|
||||
local function validate_close_tag(node)
|
||||
return validate_tag_regex(node, '^%<%/%w', '%>$')
|
||||
return validate_tag_regex(node, "^%<%/%w", "%>$")
|
||||
end
|
||||
|
||||
local function rename_start_tag()
|
||||
@ -392,7 +435,7 @@ local function rename_start_tag()
|
||||
local tag_node = find_tag_node({
|
||||
tag_pattern = ts_tag.start_tag_pattern,
|
||||
name_tag_pattern = ts_tag.start_name_tag_pattern,
|
||||
skip_tag_pattern = ts_tag.skip_tag_pattern
|
||||
skip_tag_pattern = ts_tag.skip_tag_pattern,
|
||||
})
|
||||
|
||||
if tag_node == nil then
|
||||
@ -407,7 +450,7 @@ local function rename_start_tag()
|
||||
|
||||
tag_node = find_parent_match({
|
||||
target = parent_node,
|
||||
pattern = ts_tag.element_tag .. '|' .. ERROR_TAG,
|
||||
pattern = ts_tag.element_tag .. "|" .. ERROR_TAG,
|
||||
max_depth = 2,
|
||||
})
|
||||
|
||||
@ -435,7 +478,7 @@ local function rename_start_tag()
|
||||
pattern = ERROR_TAG,
|
||||
})
|
||||
if error_node == nil then
|
||||
log.debug('do replace')
|
||||
log.debug("do replace")
|
||||
local close_tag_name = get_tag_name(close_tag_node)
|
||||
log.debug(close_tag_name)
|
||||
|
||||
@ -443,10 +486,8 @@ local function rename_start_tag()
|
||||
if close_tag_node ~= nil and tag_node ~= nil then
|
||||
local tag_parent = get_tag_name(tag_node:parent())
|
||||
-- log.debug(utils.dump_node(tag_node:parent()))
|
||||
if tag_parent == close_tag_name
|
||||
and not utils.verify_node(tag_node:parent(), close_tag_name)
|
||||
then
|
||||
log.debug('skip it have same')
|
||||
if tag_parent == close_tag_name and not utils.verify_node(tag_node:parent(), close_tag_name) then
|
||||
log.debug("skip it have same")
|
||||
return
|
||||
end
|
||||
end
|
||||
@ -457,12 +498,12 @@ local function rename_start_tag()
|
||||
else
|
||||
local error_tag = get_tag_name(error_node)
|
||||
-- tsx node is empty
|
||||
if error_tag == '</>' then
|
||||
replace_text_node(error_node, '</' .. tag_name .. '>')
|
||||
if error_tag == "</>" then
|
||||
replace_text_node(error_node, "</" .. tag_name .. ">")
|
||||
end
|
||||
-- have both parent node and child node is error
|
||||
if close_tag_node:type() == ERROR_TAG then
|
||||
replace_text_node(error_node, '</' .. tag_name .. '>')
|
||||
replace_text_node(error_node, "</" .. tag_name .. ">")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -483,8 +524,7 @@ local function rename_end_tag()
|
||||
end
|
||||
|
||||
-- we check if that node text match </>
|
||||
if not (validate_close_tag(tag_node:parent()) or validate_close_tag(tag_node))
|
||||
then
|
||||
if not (validate_close_tag(tag_node:parent()) or validate_close_tag(tag_node)) then
|
||||
return
|
||||
end
|
||||
|
||||
@ -518,7 +558,7 @@ local function validate_rename()
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local char = line:sub(cursor[2] + 1, cursor[2] + 1)
|
||||
-- only rename when last character is a word
|
||||
if string.match(char, '%w') then
|
||||
if string.match(char, "%w") then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@ -534,27 +574,40 @@ end
|
||||
|
||||
M.attach = function(bufnr, lang)
|
||||
M.lang = lang
|
||||
local config = configs.get_module('autotag')
|
||||
local config = configs.get_module("autotag")
|
||||
M.setup(config)
|
||||
if is_in_table(M.tbl_filetypes, vim.bo.filetype) then
|
||||
setup_ts_tag()
|
||||
if M.enable_close == true then
|
||||
vim.api.nvim_buf_set_keymap(bufnr or 0, 'i', ">", ">", {
|
||||
vim.api.nvim_buf_set_keymap(bufnr or 0, "i", ">", ">", {
|
||||
noremap = true,
|
||||
silent = true,
|
||||
callback = function()
|
||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
vim.api.nvim_buf_set_text(bufnr or 0, row - 1, col, row - 1, col, { '>' })
|
||||
vim.api.nvim_buf_set_text(bufnr or 0, row - 1, col, row - 1, col, { ">" })
|
||||
M.close_tag()
|
||||
vim.api.nvim_win_set_cursor(0, { row, col + 1 })
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
if M.enable_close_on_slash == true then
|
||||
vim.api.nvim_buf_set_keymap(bufnr or 0, "i", "/", "/", {
|
||||
noremap = true,
|
||||
silent = true,
|
||||
callback = function()
|
||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
vim.api.nvim_buf_set_text(bufnr or 0, row - 1, col, row - 1, col, { "/" })
|
||||
M.close_slash_tag()
|
||||
local new_row, new_col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
vim.api.nvim_win_set_cursor(0, { new_row, new_col + 1 })
|
||||
end,
|
||||
})
|
||||
end
|
||||
if M.enable_rename == true then
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_create_autocmd('InsertLeave', {
|
||||
vim.api.nvim_create_autocmd("InsertLeave", {
|
||||
buffer = bufnr,
|
||||
callback = M.rename_tag
|
||||
callback = M.rename_tag,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
198
tests/close_slash_tag_spec.lua
Normal file
198
tests/close_slash_tag_spec.lua
Normal file
@ -0,0 +1,198 @@
|
||||
if not _G.test_close then
|
||||
return
|
||||
end
|
||||
|
||||
local ts = require('nvim-treesitter.configs')
|
||||
local helpers = {}
|
||||
ts.setup({
|
||||
ensure_installed = _G.ts_filetypes,
|
||||
highlight = { enable = true },
|
||||
})
|
||||
local eq = assert.are.same
|
||||
|
||||
function helpers.feed(text, feed_opts)
|
||||
feed_opts = feed_opts or 'n'
|
||||
local to_feed = vim.api.nvim_replace_termcodes(text, true, false, true)
|
||||
vim.api.nvim_feedkeys(to_feed, feed_opts, true)
|
||||
end
|
||||
|
||||
function helpers.insert(text)
|
||||
helpers.feed('a' .. text, 'x')
|
||||
end
|
||||
|
||||
local data = {
|
||||
{
|
||||
name = '1 html close tag after inputting /',
|
||||
filepath = './sample/index.html',
|
||||
filetype = 'html',
|
||||
linenr = 10,
|
||||
key = [[/]],
|
||||
before = [[<div><| ]],
|
||||
after = [[<div></div>|]],
|
||||
},
|
||||
{
|
||||
name = '2 html close tag after inputting /',
|
||||
filepath = './sample/index.html',
|
||||
filetype = 'html',
|
||||
linenr = 10,
|
||||
key = [[/]],
|
||||
before = [[<div clas="laa"><| ]],
|
||||
after = [[<div clas="laa"></div>|]],
|
||||
},
|
||||
{
|
||||
name = '3 html don\'t close tag when no opening tag is found',
|
||||
filepath = './sample/index.html',
|
||||
filetype = 'html',
|
||||
linenr = 10,
|
||||
key = [[/>]],
|
||||
before = [[<div><|</div> ]],
|
||||
after = [[<div></>|</div>]],
|
||||
},
|
||||
{
|
||||
name = '4 html not close inside quote',
|
||||
filepath = './sample/index.html',
|
||||
filetype = 'html',
|
||||
linenr = 10,
|
||||
key = [[/]],
|
||||
before = [[<div class="aa|"> </div> ]],
|
||||
after = [[<div class="aa/|"> </div> ]],
|
||||
},
|
||||
{
|
||||
name = '5 typescriptreact close tag after inputting /',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 12,
|
||||
key = [[/]],
|
||||
before = [[<Img><| ]],
|
||||
after = [[<Img></Img>| ]],
|
||||
},
|
||||
{
|
||||
name = '6 typescriptreact close after inputting /',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 12,
|
||||
key = [[/]],
|
||||
before = [[<div class="abc"><| ]],
|
||||
after = [[<div class="abc"></div>| ]],
|
||||
},
|
||||
{
|
||||
name = '7 typescriptreact close on inline script after inputting /',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 9,
|
||||
key = [[/]],
|
||||
before = [[const a = () => <div><| ]],
|
||||
after = [[const a = () => <div></div>| ]],
|
||||
},
|
||||
{
|
||||
name = '8 typescriptreact not close on close tag',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 12,
|
||||
key = [[/]],
|
||||
before = [[<button className="btn " onClick={()}> <| ]],
|
||||
after = [[<button className="btn " onClick={()}> </button>| ]],
|
||||
},
|
||||
{
|
||||
name = '9 typescriptreact not close on expresion',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 12,
|
||||
key = [[/]],
|
||||
before = [[<button className="btn " onClick={(|)}> </button> ]],
|
||||
after = [[<button className="btn " onClick={(/|)}> </button> ]],
|
||||
},
|
||||
{
|
||||
name = '10 typescriptreact not close on typescript',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 6,
|
||||
key = [[/]],
|
||||
before = [[const data:Array<string| ]],
|
||||
after = [[const data:Array<string/| ]],
|
||||
},
|
||||
{
|
||||
name = '11 typescriptreact not close on script',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 6,
|
||||
key = [[/]],
|
||||
before = [[{(card.data | 0) && <div></div>}]],
|
||||
after = [[{(card.data /| 0) && <div></div>}]],
|
||||
},
|
||||
{
|
||||
name = '12 vue close tag after inputting /',
|
||||
filepath = './sample/index.vue',
|
||||
filetype = 'vue',
|
||||
linenr = 4,
|
||||
key = [[/]],
|
||||
before = [[<Img><| ]],
|
||||
after = [[<Img></Img>|]],
|
||||
},
|
||||
{
|
||||
name = '13 vue not close on script',
|
||||
filepath = './sample/index.vue',
|
||||
filetype = 'vue',
|
||||
linenr = 12,
|
||||
key = [[/]],
|
||||
before = [[const data:Array<string| ]],
|
||||
after = [[const data:Array<string/| ]],
|
||||
},
|
||||
{
|
||||
name = '14 typescriptreact nested indentifer close after inputting /',
|
||||
filepath = './sample/index.tsx',
|
||||
filetype = 'typescriptreact',
|
||||
linenr = 12,
|
||||
key = [[/]],
|
||||
before = [[<Opt.Input><| ]],
|
||||
after = [[<Opt.Input></Opt.Input>| ]],
|
||||
},
|
||||
{
|
||||
name = '15 php close tag after inputting /',
|
||||
filepath = './sample/index.php',
|
||||
filetype = 'php',
|
||||
linenr = 25,
|
||||
key = [[/]],
|
||||
before = [[<div><| ]],
|
||||
after = [[<div></div>| ]],
|
||||
},
|
||||
{
|
||||
name = '16 lit template div close after inputting /',
|
||||
filepath = './sample/index.ts',
|
||||
filetype = 'typescript',
|
||||
linenr = 3,
|
||||
key = [[/]],
|
||||
before = [[<div><| ]],
|
||||
after = [[<div></div>| ]],
|
||||
},
|
||||
{
|
||||
name = '17 eruby template div close after inputting /',
|
||||
filepath = './sample/index.html.erb',
|
||||
filetype = 'eruby',
|
||||
linenr = 10,
|
||||
key = [[/]],
|
||||
before = [[<div><| ]],
|
||||
after = [[<div></div>| ]],
|
||||
},
|
||||
{
|
||||
name = '18 eruby template ruby string write raw /',
|
||||
filepath = './sample/index.html.erb',
|
||||
filetype = 'eruby',
|
||||
linenr = 10,
|
||||
key = [[/]],
|
||||
before = [[<%= <div| %>]],
|
||||
after = [[<%= <div/| %> ]],
|
||||
},
|
||||
}
|
||||
|
||||
local autotag = require('nvim-ts-autotag')
|
||||
autotag.test = true
|
||||
local run_data = _G.Test_filter(data)
|
||||
|
||||
describe('[close slash tag]', function()
|
||||
_G.Test_withfile(run_data, {
|
||||
mode = 'i',
|
||||
cursor_add = 0,
|
||||
before_each = function(value) end,
|
||||
})
|
||||
end)
|
Loading…
Reference in New Issue
Block a user