From 0fdcfcc9c072e80d82cce9ff93d629b3e46bfac0 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Thu, 15 May 2025 00:33:36 +0200 Subject: [PATCH] refactor(snip): separate engine and helpers --- lua/typstar/autosnippets.lua | 171 +--------------------------------- lua/typstar/engine.lua | 172 +++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 170 deletions(-) create mode 100644 lua/typstar/engine.lua diff --git a/lua/typstar/autosnippets.lua b/lua/typstar/autosnippets.lua index c36a239..611a775 100644 --- a/lua/typstar/autosnippets.lua +++ b/lua/typstar/autosnippets.lua @@ -1,32 +1,5 @@ -local M = {} -local cfg = require('typstar.config').config.snippets +local M = require('typstar.engine') -- inherit all functions local luasnip = require('luasnip') -local utils = require('typstar.utils') -local fmta = require('luasnip.extras.fmt').fmta -local lsengines = require('luasnip.nodes.util.trig_engines') -local ts = vim.treesitter - -local exclude_triggers_set = {} -local last_keystroke_time = nil -local lexical_result_cache = {} -local ts_markup_query = ts.query.parse('typst', '(text) @markup') -local ts_math_query = ts.query.parse('typst', '(math) @math') -local ts_string_query = ts.query.parse('typst', '(string) @string') - -utils.generate_bool_set(cfg.exclude, exclude_triggers_set) -vim.api.nvim_create_autocmd('TextChangedI', { - callback = function() last_keystroke_time = vim.loop.now() end, -}) - -M.in_math = function() - local cursor = utils.get_cursor_pos() - return utils.cursor_within_treesitter_query(ts_math_query, 0, cursor) - and not utils.cursor_within_treesitter_query(ts_string_query, 0, cursor) -end -M.in_markup = function() return utils.cursor_within_treesitter_query(ts_markup_query, 2) end -M.not_in_math = function() return not M.in_math() end -M.not_in_markup = function() return not M.in_markup() end -M.snippets_toggle = true function M.cap(i) return luasnip.function_node(function(_, snip) return snip.captures[i] end) @@ -68,33 +41,6 @@ function M.ri(insert_node_id) return luasnip.function_node(function(args) return args[1][1] end, insert_node_id) end -function M.snip(trigger, expand, insert, condition, priority, options) - priority = priority or 1000 - options = vim.tbl_deep_extend('force', { - maxTrigLength = nil, - wordTrig = true, - blacklist = {}, - prepend = nil, - }, options or {}) - if options.prepend ~= nil then - expand, insert = M.blocktransform(expand, insert, options.prepend, true) - end - return luasnip.snippet( - { - trig = trigger, - trigEngine = M.engine, - trigEngineOpts = vim.tbl_deep_extend('keep', { condition = condition }, options), - wordTrig = false, - priority = priority, - snippetType = 'autosnippet', - }, - fmta(expand, { unpack(insert) }), - { - condition = function() return M.snippets_toggle end, - } - ) -end - function M.start_snip(trigger, expand, insert, condition, priority, options) return M.snip('^(\\s*)' .. trigger, '<>' .. expand, { M.cap(1), unpack(insert) }, condition, priority, options) end @@ -176,119 +122,4 @@ function M.bulletpoint_snip(trigger, expand, insert, condition, priority, option options ) end - -local alts_regex = '[\\[\\(](.*|.*)[\\)\\]]' - -function M.engine(trigger, opts) - local base_engine = lsengines.ecma(trigger, opts) - - -- determine possibly max/fixed length of trigger - local max_length = opts.maxTrigLength - local is_fixed_length = false - if max_length == nil and alts_regex ~= '' and not trigger:match('[%+%*]') then - max_length = #trigger - - utils.count_string(trigger, '\\') - - utils.count_string(trigger, '%(') - - utils.count_string(trigger, '%)') - - utils.count_string(trigger, '%?') - is_fixed_length = not trigger:match('[%+%*%?%[%]|]') - - local alts_match = alts_regex:match(trigger) -- find longest trigger in [...|...] - if alts_match then - for _, alts in ipairs(alts_match) do - local max_alt_length = 1 - for alt in alts:gmatch('([^|]+)') do - local len - if alt:match('%[.*-.*%]') then -- [A-Za-z0-9] and similar - len = 2 - else - len = #alt - end - max_alt_length = math.max(max_alt_length, len) - end - max_length = max_length - (#alts - max_alt_length) - end - else -- [^...] and similar - max_length = max_length - utils.count_string(trigger, '%[') - utils.count_string(trigger, '%]') - end - end - - -- cache preanalysis results - local condition = function() - local cached = lexical_result_cache[opts.condition] - if cached ~= nil and cached[1] == last_keystroke_time then return cached[2] end - local result = opts.condition() - lexical_result_cache[opts.condition] = { last_keystroke_time, result } - return result - end - - -- matching - return function(line_full, trig) - if not M.snippets_toggle or not condition() then return nil end - local first_idx = 1 - if max_length ~= nil then - first_idx = #line_full - max_length -- include additional char for wordtrig - if first_idx < 0 then - if is_fixed_length then - return nil - else - first_idx = 1 - end - end - if first_idx > 0 then - if string.byte(line_full, first_idx) > 127 then return nil end - end - end - local line = line_full:sub(first_idx) - local whole, captures = base_engine(line, trig) - if whole == nil then return nil end - - -- custom word trig - local from = #line - #whole + 1 - if opts.wordTrig and from ~= 1 and string.match(string.sub(line, from - 1, from - 1), '[%w.]') ~= nil then - return nil - end - - -- blacklist - for _, w in ipairs(opts.blacklist) do - if line_full:sub(-#w) == w then return nil end - end - return whole, captures - end -end - -function M.toggle_autosnippets() - M.snippets_toggle = not M.snippets_toggle - print(string.format('%sabled typstar autosnippets', M.snippets_toggle and 'En' or 'Dis')) -end - -function M.setup() - if cfg.enable then - local jsregexp_ok, jsregexp = pcall(require, 'luasnip-jsregexp') - if not jsregexp_ok then - jsregexp_ok, jsregexp = pcall(require, 'jsregexp') - end - if jsregexp_ok then - if type(alts_regex) == 'string' then alts_regex = jsregexp.compile_safe(alts_regex) end - else - alts_regex = '' - vim.notify("WARNING: Most snippets won't work as jsregexp is not installed", vim.log.levels.WARN) - end - local autosnippets = {} - for _, file in ipairs(cfg.modules) do - for _, sn in ipairs(require(('typstar.snippets.%s'):format(file))) do - local exclude - local is_start = sn.trigger:match('^%^%(\\s%*%)') - if is_start then - exclude = exclude_triggers_set[sn.trigger:sub(7)] - else - exclude = exclude_triggers_set[sn.trigger] - end - if not exclude then table.insert(autosnippets, sn) end - end - end - luasnip.add_snippets('typst', autosnippets) - end -end - return M diff --git a/lua/typstar/engine.lua b/lua/typstar/engine.lua new file mode 100644 index 0000000..19885ba --- /dev/null +++ b/lua/typstar/engine.lua @@ -0,0 +1,172 @@ +local M = {} +local cfg = require('typstar.config').config.snippets +local luasnip = require('luasnip') +local utils = require('typstar.utils') +local fmta = require('luasnip.extras.fmt').fmta +local lsengines = require('luasnip.nodes.util.trig_engines') +local ts = vim.treesitter + +local exclude_triggers_set = {} +local last_keystroke_time = nil +local lexical_result_cache = {} +local ts_markup_query = ts.query.parse('typst', '(text) @markup') +local ts_math_query = ts.query.parse('typst', '(math) @math') +local ts_string_query = ts.query.parse('typst', '(string) @string') + +utils.generate_bool_set(cfg.exclude, exclude_triggers_set) +vim.api.nvim_create_autocmd('TextChangedI', { + callback = function() last_keystroke_time = vim.loop.now() end, +}) + +M.in_math = function() + local cursor = utils.get_cursor_pos() + return utils.cursor_within_treesitter_query(ts_math_query, 0, cursor) + and not utils.cursor_within_treesitter_query(ts_string_query, 0, cursor) +end +M.in_markup = function() return utils.cursor_within_treesitter_query(ts_markup_query, 2) end +M.not_in_math = function() return not M.in_math() end +M.not_in_markup = function() return not M.in_markup() end +M.snippets_toggle = true + +function M.snip(trigger, expand, insert, condition, priority, options) + priority = priority or 1000 + options = vim.tbl_deep_extend('force', { + maxTrigLength = nil, + wordTrig = true, + blacklist = {}, + prepend = nil, + }, options or {}) + if options.prepend ~= nil then + expand, insert = M.blocktransform(expand, insert, options.prepend, true) + end + return luasnip.snippet( + { + trig = trigger, + trigEngine = M.engine, + trigEngineOpts = vim.tbl_deep_extend('keep', { condition = condition }, options), + wordTrig = false, + priority = priority, + snippetType = 'autosnippet', + }, + fmta(expand, { unpack(insert) }), + { + condition = function() return M.snippets_toggle end, + } + ) +end + +local alts_regex = '[\\[\\(](.*|.*)[\\)\\]]' + +function M.engine(trigger, opts) + local base_engine = lsengines.ecma(trigger, opts) + + -- determine possibly max/fixed length of trigger + local max_length = opts.maxTrigLength + local is_fixed_length = false + if max_length == nil and alts_regex ~= '' and not trigger:match('[%+%*]') then + max_length = #trigger + - utils.count_string(trigger, '\\') + - utils.count_string(trigger, '%(') + - utils.count_string(trigger, '%)') + - utils.count_string(trigger, '%?') + is_fixed_length = not trigger:match('[%+%*%?%[%]|]') + + local alts_match = alts_regex:match(trigger) -- find longest trigger in [...|...] + if alts_match then + for _, alts in ipairs(alts_match) do + local max_alt_length = 1 + for alt in alts:gmatch('([^|]+)') do + local len + if alt:match('%[.*-.*%]') then -- [A-Za-z0-9] and similar + len = 2 + else + len = #alt + end + max_alt_length = math.max(max_alt_length, len) + end + max_length = max_length - (#alts - max_alt_length) + end + else -- [^...] and similar + max_length = max_length - utils.count_string(trigger, '%[') - utils.count_string(trigger, '%]') + end + end + + -- cache preanalysis results + local condition = function() + local cached = lexical_result_cache[opts.condition] + if cached ~= nil and cached[1] == last_keystroke_time then return cached[2] end + local result = opts.condition() + lexical_result_cache[opts.condition] = { last_keystroke_time, result } + return result + end + + -- matching + return function(line_full, trig) + if not M.snippets_toggle or not condition() then return nil end + local first_idx = 1 + if max_length ~= nil then + first_idx = #line_full - max_length -- include additional char for wordtrig + if first_idx < 0 then + if is_fixed_length then + return nil + else + first_idx = 1 + end + end + if first_idx > 0 then + if string.byte(line_full, first_idx) > 127 then return nil end + end + end + local line = line_full:sub(first_idx) + local whole, captures = base_engine(line, trig) + if whole == nil then return nil end + + -- custom word trig + local from = #line - #whole + 1 + if opts.wordTrig and from ~= 1 and string.match(string.sub(line, from - 1, from - 1), '[%w.]') ~= nil then + return nil + end + + -- blacklist + for _, w in ipairs(opts.blacklist) do + if line_full:sub(-#w) == w then return nil end + end + return whole, captures + end +end + +function M.toggle_autosnippets() + M.snippets_toggle = not M.snippets_toggle + print(string.format('%sabled typstar autosnippets', M.snippets_toggle and 'En' or 'Dis')) +end + +function M.setup() + if cfg.enable then + local jsregexp_ok, jsregexp = pcall(require, 'luasnip-jsregexp') + if not jsregexp_ok then + jsregexp_ok, jsregexp = pcall(require, 'jsregexp') + end + if jsregexp_ok then + if type(alts_regex) == 'string' then alts_regex = jsregexp.compile_safe(alts_regex) end + else + alts_regex = '' + vim.notify("WARNING: Most snippets won't work as jsregexp is not installed", vim.log.levels.WARN) + end + local autosnippets = {} + for _, file in ipairs(cfg.modules) do + for _, sn in ipairs(require(('typstar.snippets.%s'):format(file))) do + local exclude + local is_start = sn.trigger:match('^%^%(\\s%*%)') + if is_start then + exclude = exclude_triggers_set[sn.trigger:sub(7)] + else + exclude = exclude_triggers_set[sn.trigger] + end + if not exclude then table.insert(autosnippets, sn) end + end + end + luasnip.add_snippets('typst', autosnippets) + end +end + +return M