diff --git a/README.md b/README.md index b663c5d3..1bf5bce9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ require('nvim-autopairs').setup() local disable_filetype = { "TelescopePrompt" } local ignored_next_char = string.gsub([[ [%w%%%'%[%"%.] ]],"%s+", "") +local check_ts = false, ``` @@ -155,18 +156,51 @@ npairs.add_rules({ -- you can do anything with regex +special key -- example press tab will upper text -- press b1234s => B1234S1234S -Rule("b%d%d%d%d%w$", "", "vim") - :use_regex(true,"") - :replace_endpair(function(opts) - return - opts.prev_char:sub(#opts.prev_char - 4,#opts.prev_char) - .."viwU" - end) +npairs.add_rules({ + Rule("b%d%d%d%d%w$", "", "vim") + :use_regex(true,"") + :replace_endpair(function(opts) + return + opts.prev_char:sub(#opts.prev_char - 4,#opts.prev_char) + .."viwU" + end) +}) --- check ./lua/nvim-autopairs/rules/basic.lua ``` +### Treesitter +You can use treesitter to check + +```lua +local npairs = require("nvim-autopairs") + +npairs.setup({ + check_ts = true, + ts_config = { + lua = {'string', 'comment'}-- it will not add pair on that treesitter node + javascript = {'template_string', 'comment'} + java = false,-- don't check treesitter on java + } +}) + +require('nvim-treesitter.configs').setup { + autopairs = {enable = true} +} + +local ts_conds = require('nvim-autopairs.ts-conds') + + +-- press % => %% is only inside comment or string +npairs.add_rules({ + Rule("%", "%", "lua") + :with_pair(ts_conds.is_ts_node({'string','comment'})), + Rule("$", "$", "lua") + :with_pair(ts_conds.is_not_ts_node({'function'})) +}) +``` + ### Don't add pairs if it already have a close pairs in same line if **next character** is a close pairs and it doesn't have an open pairs in same line then it will not add a close pairs diff --git a/doc/endwise.md b/doc/endwise.md index 201439f6..1812df6f 100644 --- a/doc/endwise.md +++ b/doc/endwise.md @@ -1,6 +1,6 @@ #Endwise (experiment) -** warning** you endwise base on treesitter is not always correct. +** warning** use endwise base on treesitter is not always correct. treesitter group all error node with parent node so we can't find a perfect solution for this. diff --git a/lua/nvim-autopairs.lua b/lua/nvim-autopairs.lua index f14596eb..ec0cec16 100644 --- a/lua/nvim-autopairs.lua +++ b/lua/nvim-autopairs.lua @@ -5,16 +5,47 @@ local api = vim.api local M={} -local state = {} +M.state = { + disabled=false, + rules = {}, + buf_ts = {} +} local default = { disable_filetype = {"TelescopePrompt", "spectre_panel"}, - ignored_next_char = string.gsub([[ [%w%%%'%[%"%.] ]],"%s+", "") + ignored_next_char = string.gsub([[ [%w%%%'%[%"%.] ]],"%s+", ""), + check_ts = false, + ts_config = { + lua = {'string', 'comment'} + } } +M.init = function() + require "nvim-treesitter".define_modules { + autopairs = { + module_path = 'nvim-autopairs.internal', + is_supported = function() + return true + end + } + } + +end + M.setup = function(opt) M.config = vim.tbl_extend('force', default, opt or {}) M.config.rules = basic_rule.setup(M.config) + + if M.config.check_ts then + local ok, ts_rule = pcall(require, 'nvim-autopairs.rules.ts_basic') + if ok then + M.config.rules = ts_rule.setup(M.config) + else + print("you need to install treesitter") + end + + end + api.nvim_exec ([[ augroup autopairs_buf autocmd! @@ -48,15 +79,15 @@ M.clear_rules = function() end M.disable=function() - state.disabled = true + M.state.disabled = true end M.enable = function() - state.disabled = false + M.state.disabled = false end M.on_attach = function(bufnr) - if state.disabled then return end + if M.state.disabled then return end bufnr = bufnr or api.nvim_get_current_buf() if not utils.check_disable_ft(M.config.disable_filetype, vim.bo.filetype) then return end local rules = {}; @@ -70,10 +101,20 @@ M.on_attach = function(bufnr) return (#a.start_pair or 0) > (#b.start_pair or 0) end) - state.rules = rules + M.state.rules = rules + + if M.state.buf_ts[bufnr] == true then + M.state.ts_node = M.config.ts_config[vim.bo.filetype] + if M.state.ts_node == nil then + M.state.ts_node = {'string', 'comment'} + end + else + M.state.ts_node = nil + end + if utils.is_attached(bufnr) then return end local enable_insert_auto = false - for _, rule in pairs(state.rules) do + for _, rule in pairs(M.state.rules) do if rule.is_regex == false then if rule.key_map == "" then rule.key_map = rule.start_pair:sub((#rule.start_pair)) @@ -114,10 +155,10 @@ M.on_attach = function(bufnr) end M.autopairs_bs = function(bufnr) - if state.disabled then return end + if M.state.disabled then return end local line = utils.text_get_current_line(bufnr) local _, col = utils.get_cursor() - for _, rule in pairs(state.rules) do + for _, rule in pairs(M.state.rules) do if rule.start_pair then local prev_char, next_char = utils.text_cusor_line( @@ -131,6 +172,7 @@ M.autopairs_bs = function(bufnr) utils.is_equal(rule.start_pair, prev_char, rule.is_regex) and rule.end_pair == next_char and rule:can_del({ + ts_node = M.state.ts_node, bufnr = bufnr, prev_char = prev_char, next_char = next_char, @@ -156,19 +198,19 @@ local skip_next = false M.autopairs_map = function(bufnr, char) - if state.disabled then return end + if M.state.disabled then return end if skip_next then skip_next = false return end local line = utils.text_get_current_line(bufnr) local _, col = utils.get_cursor() local new_text = line:sub(1, col) .. char .. line:sub(col + 1,#line) local add_char = 1 - for _, rule in pairs(state.rules) do + for _, rule in pairs(M.state.rules) do if rule.start_pair then if rule.is_regex and rule.key_map ~= "" then new_text = line:sub(1, col) .. line:sub(col + 1,#line) add_char = 0 end - log.debug("new_text:[" .. new_text .. "]") + -- log.debug("new_text:[" .. new_text .. "]") local prev_char, next_char = utils.text_cusor_line( new_text, col+ add_char, @@ -176,6 +218,7 @@ M.autopairs_map = function(bufnr, char) #rule.end_pair, rule.is_regex ) local cond_opt = { + ts_node = M.state.ts_node, text = new_text, rule = rule, bufnr = bufnr, @@ -208,12 +251,12 @@ M.autopairs_map = function(bufnr, char) return char end M.autopairs_insert = function(bufnr, char) - if state.disabled then return end + if M.state.disabled then return end if skip_next then skip_next = false return end local line = utils.text_get_current_line(bufnr) local _, col = utils.get_cursor() local new_text = line:sub(1, col) .. char .. line:sub(col + 1,#line) - for _, rule in pairs(state.rules) do + for _, rule in pairs(M.state.rules) do if rule.start_pair and rule.is_regex and rule.key_map == "" then local prev_char, next_char = utils.text_cusor_line( new_text, @@ -222,6 +265,7 @@ M.autopairs_insert = function(bufnr, char) #rule.end_pair, rule.is_regex ) local cond_opt = { + ts_node = M.state.ts_node, text = new_text, rule = rule, bufnr = bufnr, @@ -262,12 +306,12 @@ M.autopairs_insert = function(bufnr, char) end M.autopairs_cr = function(bufnr) - if state.disabled then return end + if M.state.disabled then return end bufnr = bufnr or api.nvim_get_current_buf() local line = utils.text_get_current_line(bufnr) local _, col = utils.get_cursor() -- log.debug("on_cr") - for _, rule in pairs(state.rules) do + for _, rule in pairs(M.state.rules) do if rule.start_pair then local prev_char, next_char = utils.text_cusor_line( line, @@ -282,7 +326,8 @@ M.autopairs_cr = function(bufnr) rule.is_endwise and utils.is_equal(rule.start_pair, prev_char, rule.is_regex) and rule:can_cr({ - check_ts = true, + ts_node = M.state.ts_node, + check_endwise_ts = true, bufnr = bufnr, rule = rule, prev_char = prev_char, @@ -301,7 +346,8 @@ M.autopairs_cr = function(bufnr) utils.is_equal(rule.start_pair, prev_char, rule.is_regex) and rule.end_pair == next_char and rule:can_cr({ - check_ts = false, + ts_node = M.state.ts_node, + check_endwise_ts = false, bufnr = bufnr, rule = rule, prev_char = prev_char, diff --git a/lua/nvim-autopairs/internal.lua b/lua/nvim-autopairs/internal.lua new file mode 100644 index 00000000..9948ab12 --- /dev/null +++ b/lua/nvim-autopairs/internal.lua @@ -0,0 +1,15 @@ +local log = require('nvim-autopairs._log') + +local M = {} + +M.attach = function (bufnr) + log.debug('treesitter.attach') + MPairs.state.buf_ts[bufnr] = true +end + +M.detach = function (bufnr ) + MPairs.state.buf_ts[bufnr] =nil +end + +-- _G.AUTO = M +return M diff --git a/lua/nvim-autopairs/rules/ts_basic.lua b/lua/nvim-autopairs/rules/ts_basic.lua new file mode 100644 index 00000000..4cfb92f5 --- /dev/null +++ b/lua/nvim-autopairs/rules/ts_basic.lua @@ -0,0 +1,24 @@ +local basic = require('nvim-autopairs.rules.basic') +local utils = require('nvim-autopairs.utils') +local ts_conds = require('nvim-autopairs.ts-conds') +local log = require('nvim-autopairs._log') +local ts_extend = { + "'", + '"', + '(', + '[', + '{', + '`', +} +return { + setup = function (config) + local rules=basic.setup(config) + for _, rule in pairs(rules) do + if utils.is_in_table(ts_extend, rule.start_pair) then + log.debug(rule.start_pair) + rule:with_pair(ts_conds.is_not_ts_node_comment()) + end + end + return rules + end +} diff --git a/lua/nvim-autopairs/ts-conds.lua b/lua/nvim-autopairs/ts-conds.lua index afbc63b3..37a1cbbf 100644 --- a/lua/nvim-autopairs/ts-conds.lua +++ b/lua/nvim-autopairs/ts-conds.lua @@ -1,29 +1,37 @@ -local conds = {} local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') + local log = require('nvim-autopairs._log') local parsers = require'nvim-treesitter.parsers' +local utils = require('nvim-autopairs.utils') + +local conds = {} + +conds.is_endwise_node = function(nodes) + if type(nodes) == 'string' then nodes = {nodes} end + assert(nodes ~= nil, "ts nodes should be string or table") -conds.is_ts_node = function(nodename) return function (opts) - if not opts.check_ts then return true end - if nodename == "" then return true end + log.debug('is_endwise_node') + if not opts.check_endwise_ts then return true end + if #nodes == 0 then return true end + parsers.get_parser():parse() local target = ts_utils.get_node_at_cursor() - if target ~= nil and target:type() == nodename then - local text = ts_utils.get_node_text(target) + if target ~= nil and utils.is_in_table(nodes, target:type()) then + local text = ts_utils.get_node_text(target) or {""} local last = text[#text]:match(opts.rule.end_pair) - log.debug('last:' .. last) -- check last character is match with end_pair if last == nil then return true end + log.debug('last:' .. last) -- if match then we need tocheck parent node local _,_, linenr_target = target:range() local _,_, linenr_parent = target:parent():range() - log.debug(target:range()) - log.debug(ts_utils.get_node_text(target)) - log.debug(target:parent():range()) - log.debug(ts_utils.get_node_text(target:parent())) + -- log.debug(target:range()) + -- log.debug(ts_utils.get_node_text(target)) + -- log.debug(target:parent():range()) + -- log.debug(ts_utils.get_node_text(target:parent())) if linenr_parent - linenr_target == 1 then return true end @@ -33,4 +41,50 @@ conds.is_ts_node = function(nodename) end end +conds.is_ts_node = function(nodes) + if type(nodes) == 'string' then nodes = {nodes} end + assert(nodes ~= nil, "ts nodes should be string or table") + return function (opts) + log.debug('is_ts_node') + if not opts.ts_node then return end + if #nodes == 0 then return end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + if target ~= nil and utils.is_in_table(nodes, target:type()) then + return true + end + end +end + +conds.is_not_ts_node = function(nodes) + if type(nodes) == 'string' then nodes = {nodes} end + assert(nodes ~= nil, "ts nodes should be string or table") + return function (opts) + log.debug('is_not_ts_node') + if not opts.ts_node then return end + if #nodes == 0 then return end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + if target ~= nil and utils.is_in_table(nodes, target:type()) then + return false + end + end +end + +conds.is_not_ts_node_comment = function() + return function(opts) + log.debug('not_in_ts_node_comment') + if not opts.ts_node then return end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + log.debug(target:type()) + if target ~= nil and utils.is_in_table(opts.ts_node, target:type()) then + return false + end + end +end + return conds diff --git a/lua/nvim-autopairs/ts-rule.lua b/lua/nvim-autopairs/ts-rule.lua index e1a73452..58986f61 100644 --- a/lua/nvim-autopairs/ts-rule.lua +++ b/lua/nvim-autopairs/ts-rule.lua @@ -5,14 +5,15 @@ local ts_conds = require('nvim-autopairs.ts-conds') return { endwise = function (...) local params = {...} - assert(type(params[4]) == 'string', 'treesitter name is string or"" ') + assert(type(params[4]) == 'string', 'treesitter name is string or "" ') return Rule(...) :with_pair(cond.none()) :with_move(cond.none()) :with_del(cond.none()) - :with_cr(ts_conds.is_ts_node(params[4])) + :with_cr(ts_conds.is_endwise_node(params[4])) :use_regex(true) :end_wise() end + } diff --git a/plugin.vim b/plugin.vim new file mode 100644 index 00000000..e69de29b diff --git a/plugin/nvim-autopairs.vim b/plugin/nvim-autopairs.vim new file mode 100644 index 00000000..ce194ec4 --- /dev/null +++ b/plugin/nvim-autopairs.vim @@ -0,0 +1 @@ +lua require "nvim-autopairs".init() diff --git a/tests/endwise/javascript.js b/tests/endwise/javascript.js new file mode 100644 index 00000000..64b12393 --- /dev/null +++ b/tests/endwise/javascript.js @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/endwise/sample.lua b/tests/endwise/sample.lua new file mode 100644 index 00000000..1bfe111b --- /dev/null +++ b/tests/endwise/sample.lua @@ -0,0 +1,18 @@ +local M={} + +M.autopairs_bs = function(rules) + for _, rule in pairs(rules) do + + if rule.start_pair then +end + + + if rule.start_pair then + + end + + + + end +end +return M diff --git a/tests/endwise_spec.lua b/tests/endwise_spec.lua index a7ce6765..d3310bae 100644 --- a/tests/endwise_spec.lua +++ b/tests/endwise_spec.lua @@ -1,32 +1,14 @@ local npairs = require('nvim-autopairs') local ts = require 'nvim-treesitter.configs' -local Rule = require('nvim-autopairs.ts-rule') local log = require('nvim-autopairs._log') -local helpers = {} - ts.setup { - ensure_installed = 'maintained', - highlight = {enable = true}, + ensure_installed = 'maintained', + highlight = {enable = true}, } - _G.npairs = npairs; -local eq=_G.eq - vim.api.nvim_set_keymap('i' , '','v:lua.npairs.check_break_line_char()', {expr = true , noremap = true}) -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('i' .. text, 'x') -end -ts.setup { - ensure_installed = 'maintained', - highlight = {enable = true}, -} local data = { { @@ -62,66 +44,17 @@ local data = { }, } -local run_data = {} -for _, value in pairs(data) do - if value.only == true then - table.insert(run_data, value) - break - end -end - -if #run_data == 0 then run_data = data end +local run_data = _G.Test_filter(data) local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') _G.TU = ts_utils -local function Test(test_data) - log.debug("aaa") - for _, value in pairs(test_data) do - it("test "..value.name, function() - local text_before = {} - local pos_before = { - linenr = value.linenr, - colnr = 0 - } - if not vim.tbl_islist(value.before) then - value.before = {value.before} - end - local numlnr = 0 - for _, text in pairs(value.before) do - local txt = string.gsub(text, '%|' , "") - table.insert(text_before, txt ) - if string.match( text, "%|") then - pos_before.colnr = string.find(text, '%|') - pos_before.linenr = pos_before.linenr + numlnr - end - numlnr = numlnr + 1 - end - local after = string.gsub(value.after, '%|' , "") - vim.bo.filetype = value.filetype - if vim.fn.filereadable(vim.fn.expand(value.filepath)) == 1 then - npairs.clear_rules() - npairs.add_rules(require('nvim-autopairs.rules.endwise-'..value.filetype)) - vim.cmd(":bd!") - vim.cmd(":e " .. value.filepath) - vim.bo.filetype = value.filetype - vim.api.nvim_buf_set_lines(0, pos_before.linenr -1, pos_before.linenr +#text_before, false, text_before) - vim.fn.cursor(pos_before.linenr, pos_before.colnr) - log.debug("insert:"..value.key) - helpers.insert(value.key) - vim.wait(10) - helpers.feed("") - local result = vim.fn.getline(pos_before.linenr + 2) - local pos = vim.fn.getpos('.') - eq(pos_before.linenr + 1, pos[2], '\n\n breakline error:' .. value.name .. "\n") - eq(after, result , "\n\n text error: " .. value.name .. "\n") - else - eq(false, true, "\n\n file not exist " .. value.filepath .. "\n") - end - end) - end -end describe('[endwise tag]', function() - Test(run_data) + _G.Test_withfile(run_data,{ + before = function(value) + npairs.clear_rules() + npairs.add_rules(require('nvim-autopairs.rules.endwise-'..value.filetype)) + end + }) end) diff --git a/tests/minimal.vim b/tests/minimal.vim index b6b16b8a..878532fa 100644 --- a/tests/minimal.vim +++ b/tests/minimal.vim @@ -3,9 +3,11 @@ set rtp +=../plenary.nvim/ set rtp +=../nvim-treesitter set rtp +=../playground/ +lua _G.__is_log = true runtime! plugin/plenary.vim runtime! plugin/nvim-treesitter.vim runtime! plugin/playground.vim +runtime! plugin/nvim-autopairs.vim set noswapfile set nobackup @@ -18,7 +20,6 @@ set nosmartindent set indentexpr= lua << EOF -_G.__is_log = true require("plenary/busted") vim.cmd[[luafile ./tests/test_utils.lua]] require("nvim-autopairs").setup() diff --git a/tests/test_utils.lua b/tests/test_utils.lua index 985b4115..f2d6f1dd 100644 --- a/tests/test_utils.lua +++ b/tests/test_utils.lua @@ -2,25 +2,99 @@ local utils = require('nvim-autopairs.utils') local log = require('nvim-autopairs._log') local api = vim.api +local helpers = {} +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('i' .. text, 'x') +end utils.insert_char = function(text) api.nvim_put({text}, "c", true, true) end utils.feed = function(text,num) - -- if num > 0 then - -- num = num + 1 - -- else - -- num = 1 - -- end - local result = '' for _ = 1, num, 1 do result = result .. text end - -- log.debug("result" .. result) api.nvim_feedkeys (api.nvim_replace_termcodes( result, true, false, true), "x", true) end _G.eq = assert.are.same + +_G.Test_filter = function (data) + local run_data = {} + for _, value in pairs(data) do + if value.only == true then + table.insert(run_data, value) + break + end + end + if #run_data == 0 then run_data = data end + return run_data +end + + + +_G.Test_withfile = function(test_data, cb) + for _, value in pairs(test_data) do + it("test "..value.name, function() + local text_before = {} + local pos_before = { + linenr = value.linenr, + colnr = 0 + } + if not vim.tbl_islist(value.before) then + value.before = {value.before} + end + local numlnr = 0 + for _, text in pairs(value.before) do + local txt = string.gsub(text, '%|' , "") + table.insert(text_before, txt ) + if string.match( text, "%|") then + pos_before.colnr = string.find(text, '%|') + pos_before.linenr = pos_before.linenr + numlnr + end + numlnr = numlnr + 1 + end + local after = string.gsub(value.after, '%|' , "") + local p_after = string.find(value.after , '%|') + vim.bo.filetype = value.filetype + if vim.fn.filereadable(vim.fn.expand(value.filepath)) == 1 then + vim.cmd(":bd!") + if cb.before then cb.before(value) end + vim.cmd(":e " .. value.filepath) + if value.filetype then + vim.bo.filetype = value.filetype + vim.cmd(":e") + end + vim.api.nvim_buf_set_lines(0, pos_before.linenr -1, pos_before.linenr +#text_before, false, text_before) + vim.fn.cursor(pos_before.linenr, pos_before.colnr) + log.debug("insert:"..value.key) + helpers.insert(value.key) + vim.wait(10) + helpers.feed("") + if value.key == '' then + local result = vim.fn.getline(pos_before.linenr + 2) + local pos = vim.fn.getpos('.') + eq(pos_before.linenr + 1, pos[2], '\n\n breakline error:' .. value.name .. "\n") + eq(after, result , "\n\n text error: " .. value.name .. "\n") + else + local result = vim.fn.getline(pos_before.linenr) + local pos = vim.fn.getpos('.') + eq(after, result , "\n\n text error: " .. value.name .. "\n") + eq(p_after, pos[3] + 1, "\n\n pos error: " .. value.name .. "\n") + end + if cb.after then cb.after(value) end + else + eq(false, true, "\n\n file not exist " .. value.filepath .. "\n") + end + end) + end +end diff --git a/tests/treesitter_spec.lua b/tests/treesitter_spec.lua new file mode 100644 index 00000000..d4f3e2f6 --- /dev/null +++ b/tests/treesitter_spec.lua @@ -0,0 +1,81 @@ + +local npairs = require('nvim-autopairs') +local ts = require 'nvim-treesitter.configs' +local log = require('nvim-autopairs._log') +local Rule=require('nvim-autopairs.rule') +local ts_conds=require('nvim-autopairs.ts-conds') + +_G.npairs = npairs; +npairs.setup({ + check_ts = true, + ts_config={ + javascript = {'template_string', 'comment'} + } +}) + +npairs.add_rules({ + Rule("%", "%", "lua") + :with_pair(ts_conds.is_ts_node({'string','comment'})) +}) +vim.api.nvim_set_keymap('i' , '','v:lua.npairs.check_break_line_char()', {expr = true , noremap = true}) + +ts.setup { + ensure_installed = 'maintained', + highlight = {enable = true}, + autopairs = {enable = true} +} + +local data = { + { + name = "treesitter lua quote" , + filepath = './tests/endwise/init.lua', + filetype = "lua", + linenr = 5, + key = [["]], + before = { + [[ [[ aaa| ]], + [[ ]], + "]]" + }, + after = [[ [[ aaa"| ]] + }, + + { + name = "treesitter javascript quote" , + filepath = './tests/endwise/javascript.js', + filetype = "javascript", + linenr = 5, + key = [[(]], + before = { + [[ const data= `aaa | ]], + [[ ]], + "`" + }, + after = [[ const data= `aaa (| ]] + }, + { + name = "ts_conds is_ts_node quote" , + filepath = './tests/endwise/init.lua', + filetype = "lua", + linenr = 5, + key = [[%]], + before = { + [[ [[ abcde | ]], + [[ ]], + "]]" + }, + after = [[ [[ abcde %|% ]] + }, +} + +local run_data = _G.Test_filter(data) + +local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') +_G.TU = ts_utils + + +describe('[treesitter check]', function() + _G.Test_withfile(run_data,{ + before = function() end + }) +end)