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:
Gungun974 2023-06-14 10:59:43 +04:00 committed by GitHub
parent 40615e9607
commit e254b306fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 301 additions and 50 deletions

View File

@ -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

View 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)