diff --git a/flake.lock b/flake.lock index 52fb77f..80cfab8 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1736143030, - "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "lastModified": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1737525964, - "narHash": "sha256-3wFonKmNRWKq1himW9N3TllbeGIHFACI5vmLpk6moF8=", + "lastModified": 1746518791, + "narHash": "sha256-MiJ11L7w18S2G5ftcoYtcrrS0JFqBaj9d5rwJFpC5Wk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5757bbb8bd7c0630a0cc4bb19c47e588db30b97c", + "rev": "1cb1c02a6b1b7cf67e3d7731cbbf327a53da9679", "type": "github" }, "original": { @@ -36,14 +36,17 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1735774519, - "narHash": "sha256-CewEm1o2eVAnoqb6Ml+Qi9Gg/EfNAxbRx1lANGVyoLI=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz" + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz" + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" } }, "root": { diff --git a/flake.nix b/flake.nix index 700cd7f..1248f50 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,10 @@ pkgs.vimPlugins.luasnip pkgs.vimPlugins.nvim-treesitter-parsers.typst ]; + # TODO: make this check pass instead of skipping + neovimRequireCheckHook = '' + echo "Skipping neovimRequireCheckHook" + ''; }; in { packages = { @@ -43,7 +47,8 @@ } require('luasnip').config.set_config({ - enable_autosnippets = true, + enable_autosnippets = true, + store_selection_keys = "", }) require('typstar').setup() diff --git a/lua/typstar/autosnippets.lua b/lua/typstar/autosnippets.lua index 98064b3..8e73b96 100644 --- a/lua/typstar/autosnippets.lua +++ b/lua/typstar/autosnippets.lua @@ -32,23 +32,34 @@ function M.cap(i) return luasnip.function_node(function(_, snip) return snip.captures[i] end) end -function M.leading_white_spaces(i) - -- isolate whitespaces of captured group - return luasnip.function_node(function(_, snip) - local capture = snip.captures[i] or '' -- Return capture or empty string if nil - -- Extract only leading whitespace using pattern matching - local whitespace = capture:match('^%s*') or '' - return whitespace - end) +local compute_leading_white_spaces = function(snip, i) + local capture = snip.captures[i] or '' + return capture:match('^%s*') or '' end -function M.visual(idx, default) +function M.leading_white_spaces(i) + return luasnip.function_node(function(_, snip) return compute_leading_white_spaces(snip, i) end) +end + +function M.visual(idx, default, line_prefix, indent_capture_idx) default = default or '' - return luasnip.dynamic_node(idx, function(args, parent) - if #parent.snippet.env.LS_SELECT_RAW > 0 then - return luasnip.snippet_node(nil, luasnip.text_node(parent.snippet.env.LS_SELECT_RAW)) + line_prefix = line_prefix or '' + return luasnip.dynamic_node(idx, function(_, snip) + local select_raw = snip.snippet.env.LS_SELECT_RAW + if #select_raw > 0 then + if line_prefix ~= '' then -- e.g. indentation + for i, s in ipairs(select_raw) do + select_raw[i] = line_prefix .. s + end + end + return luasnip.snippet_node(nil, luasnip.text_node(select_raw)) else -- If LS_SELECT_RAW is empty, return an insert node - return luasnip.snippet_node(nil, luasnip.insert_node(1, default)) + local leading = '' + if indent_capture_idx ~= nil then leading = compute_leading_white_spaces(snip, indent_capture_idx) end + return luasnip.snippet_node(nil, { + luasnip.text_node(leading .. line_prefix), + luasnip.insert_node(1, default), + }) end end) end @@ -57,14 +68,18 @@ 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, wordTrig, maxTrigLength) +function M.snip(trigger, expand, insert, condition, priority, trigOptions) priority = priority or 1000 - if wordTrig == nil then wordTrig = true end + trigOptions = vim.tbl_deep_extend('force', { + maxTrigLength = nil, + wordTrig = true, + blacklist = {}, + }, trigOptions or {}) return luasnip.snippet( { trig = trigger, trigEngine = M.engine, - trigEngineOpts = { condition = condition, wordTrig = wordTrig, maxTrigLength = maxTrigLength }, + trigEngineOpts = vim.tbl_deep_extend('keep', { condition = condition }, trigOptions), wordTrig = false, priority = priority, snippetType = 'autosnippet', @@ -76,18 +91,18 @@ function M.snip(trigger, expand, insert, condition, priority, wordTrig, maxTrigL ) end -function M.start_snip(trigger, expand, insert, condition, priority) - return M.snip('^(\\s*)' .. trigger, '<>' .. expand, { M.cap(1), unpack(insert) }, condition, priority) +function M.start_snip(trigger, expand, insert, condition, priority, trigOptions) + return M.snip('^(\\s*)' .. trigger, '<>' .. expand, { M.cap(1), unpack(insert) }, condition, priority, trigOptions) end -function M.start_snip_in_newl(trigger, expand, insert, condition, priority) +function M.start_snip_in_newl(trigger, expand, insert, condition, priority, trigOptions) return M.snip( '([^\\s]\\s+)' .. trigger, '<>\n<>' .. expand, { M.cap(1), M.leading_white_spaces(1), unpack(insert) }, condition, priority, - false + vim.tbl_deep_extend('keep', { wordTrig = false }, trigOptions or {}) ) end @@ -99,7 +114,7 @@ function M.engine(trigger, opts) -- determine possibly max/fixed length of trigger local max_length = opts.maxTrigLength local is_fixed_length = false - if alts_regex ~= '' and not trigger:match('[%+%*]') then + if max_length == nil and alts_regex ~= '' and not trigger:match('[%+%*]') then max_length = #trigger - utils.count_string(trigger, '\\') - utils.count_string(trigger, '%(') @@ -137,10 +152,11 @@ function M.engine(trigger, opts) end -- matching - return function(line, trig) + 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 - local first_idx = #line - max_length -- include additional char for wordtrig + first_idx = #line_full - max_length -- include additional char for wordtrig if first_idx < 0 then if is_fixed_length then return nil @@ -149,10 +165,10 @@ function M.engine(trigger, opts) end end if first_idx > 0 then - if string.byte(line, first_idx) > 127 then return nil end + if string.byte(line_full, first_idx) > 127 then return nil end end - line = line:sub(first_idx) end + local line = line_full:sub(first_idx) local whole, captures = base_engine(line, trig) if whole == nil then return nil end @@ -161,6 +177,11 @@ function M.engine(trigger, opts) 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 @@ -177,7 +198,7 @@ function M.setup() jsregexp_ok, jsregexp = pcall(require, 'jsregexp') end if jsregexp_ok then - alts_regex = jsregexp.compile_safe(alts_regex) + 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) diff --git a/lua/typstar/snippets/letters.lua b/lua/typstar/snippets/letters.lua index e079b8b..18c1cd0 100644 --- a/lua/typstar/snippets/letters.lua +++ b/lua/typstar/snippets/letters.lua @@ -116,8 +116,7 @@ return { { d(1, get_index, {}, { user_args = { 1, 2, false } }), d(2, prepend_space, {}, { user_args = { 3 } }) }, markup, 500, - true, - 13 + { maxTrigLength = 13 } ), snip( '(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ')([^\\w])', @@ -125,14 +124,13 @@ return { { d(1, get_index, {}, { user_args = { 1, 2, true } }), d(2, prepend_space, {}, { user_args = { 3 } }) }, math, 200, - true, - 10 -- epsilon123 + { maxTrigLength = 10 } -- epsilon123 ), -- series of numbered letters snip('(' .. trigger_index_pre .. ') ot ', '<>_1, <>_2, ... ', { cap(1), cap(1) }, math), -- a_1, a_2, ... - snip('(' .. trigger_index_pre .. ') ot(\\w+) ', '<> ', { d(1, get_series) }, math, 1000, true, 13), -- a_1, a_2, ... a_j or a_1, a_2, a_2, a_3, a_4, a_5 + snip('(' .. trigger_index_pre .. ') ot(\\w+) ', '<> ', { d(1, get_series) }, math, 1000, { maxTrigLength = 13 }), -- a_1, a_2, ... a_j or a_1, a_2, a_2, a_3, a_4, a_5 -- misc - snip('(' .. trigger_index_pre .. ')bl', 'B_<> (<>)', { cap(1), i(1, 'x_0') }, math), + snip('(' .. trigger_index_pre .. ')bl', 'B_<> (<>) ', { cap(1), i(1, 'x_0') }, math, 100), } diff --git a/lua/typstar/snippets/markup.lua b/lua/typstar/snippets/markup.lua index 93af10d..0ca394c 100644 --- a/lua/typstar/snippets/markup.lua +++ b/lua/typstar/snippets/markup.lua @@ -4,13 +4,15 @@ local i = ls.insert_node local helper = require('typstar.autosnippets') local cap = helper.cap local markup = helper.in_markup -local visual = helper.visual local snip = helper.snip local start = helper.start_snip +local indent_visual = function(idx, default) return helper.visual(idx, default or '', '\t', 1) end + local ctheorems = { { 'tem', 'theorem' }, { 'pro', 'proof' }, + { 'prp', 'proposition' }, { 'axi', 'axiom' }, { 'cor', 'corollary' }, { 'lem', 'lemma' }, @@ -28,29 +30,24 @@ local wrappings = { } local document_snippets = {} -local ctheoremsstr = '#%s[\n<>\t<>\n<>]' +local ctheoremsstr = '#%s[\n<>\n<>]' local wrappingsstr = '%s<>%s' for _, val in pairs(ctheorems) do - local snippet = start(val[1], string.format(ctheoremsstr, val[2]), { cap(1), visual(1), cap(1) }, markup) + local snippet = start(val[1], string.format(ctheoremsstr, val[2]), { indent_visual(1), cap(1) }, markup) table.insert(document_snippets, snippet) end for _, val in pairs(wrappings) do - local snippet = snip(val[1], string.format(wrappingsstr, val[2], val[3]), { visual(1, val[4]) }, markup) + local snippet = snip(val[1], string.format(wrappingsstr, val[2], val[3]), { helper.visual(1, val[4]) }, markup) table.insert(document_snippets, snippet) end return { - start('dm', '$\n<>\t<>\n<>$', { cap(1), visual(1), cap(1) }, markup), - helper.start_snip_in_newl( - 'dm', - '$\n<>\t<>\n<>$', - { helper.leading_white_spaces(1), visual(1), helper.leading_white_spaces(1) }, - markup - ), - start('fla', '#flashcard(0)[<>][\n<>\t<>\n<>]', { i(1, 'flashcard'), cap(1), visual(2), cap(1) }, markup), - start('flA', '#flashcard(0, "<>")[\n<>\t<>\n<>]', { i(1, 'flashcard'), cap(1), visual(2), cap(1) }, markup), + start('dm', '$\n<>\n<>$', { indent_visual(1), cap(1) }, markup), + helper.start_snip_in_newl('dm', '$\n<>\n<>$', { indent_visual(1), helper.leading_white_spaces(1) }, markup), + start('fla', '#flashcard(0)[<>][\n<>\n<>]', { i(1, 'flashcard'), indent_visual(2), cap(1) }, markup), + start('flA', '#flashcard(0, "<>")[\n<>\n<>]', { i(1, 'flashcard'), indent_visual(2), cap(1) }, markup), snip('IMP', '$==>>$ ', {}, markup), snip('IFF', '$<<==>>$ ', {}, markup), unpack(document_snippets), diff --git a/lua/typstar/snippets/math.lua b/lua/typstar/snippets/math.lua index f3c578d..bb5f24d 100644 --- a/lua/typstar/snippets/math.lua +++ b/lua/typstar/snippets/math.lua @@ -19,7 +19,7 @@ return { snip('een', 'exists epsilon>>0 ', {}, math), -- boolean logic - snip('no', 'not ', {}, math), + snip('not', 'not ', {}, math), snip('ip', '==>> ', {}, math), snip('ib', '<<== ', {}, math), snip('iff', '<<==>> ', {}, math), @@ -34,8 +34,8 @@ return { snip('ge', '>>= ', {}, math), -- operators - snip('ak([^k ])', '+ <>', { cap(1) }, math, 100, false), - snip('sk([^k ])', '- <>', { cap(1) }, math, 100, false), + snip('ak([^k ])', '+ <>', { cap(1) }, math, 100, { wordTrig = false }), + snip('sk([^k ])', '- <>', { cap(1) }, math, 100, { wordTrig = false }), snip('oak', 'plus.circle ', {}, math), snip('bak', 'plus.square ', {}, math), snip('mak', 'plus.minus ', {}, math), @@ -45,11 +45,11 @@ return { snip('ff', '(<>) / (<>) <>', { i(1, 'a'), i(2, 'b'), i(3) }, math), -- exponents - snip('iv', '^(-1) ', {}, math, 500, false), - snip('sr', '^2 ', {}, math, 500, false), - snip('cb', '^3 ', {}, math, 500, false), - snip('jj', '_(<>) ', { i(1, 'n') }, math, 500, false), - snip('kk', '^(<>) ', { i(1, 'n') }, math, 500, false), + snip('iv', '^(-1) ', {}, math, 500, { wordTrig = false, blacklist = { 'equiv' } }), + snip('sr', '^2 ', {}, math, 500, { wordTrig = false }), + snip('cb', '^3 ', {}, math, 500, { wordTrig = false }), + snip('jj', '_(<>) ', { i(1, 'n') }, math, 500, { wordTrig = false }), + snip('kk', '^(<>) ', { i(1, 'n') }, math, 500, { wordTrig = false }), snip('ep', 'exp(<>) ', { i(1, '1') }, math), -- sets @@ -73,6 +73,10 @@ return { snip('Oo', 'compose ', {}, math), snip('iso', 'tilde.equiv ', {}, math), snip('cc', 'cases(\n\t<>\n)\\', { i(1, '1') }, math), + snip('([A-Za-z])o([A-Za-z0-9])', '<>(<>) ', { cap(1), cap(2) }, math, 100, { + maxTrigLength = 3, + blacklist = { 'bot', 'cos', 'col', 'com', 'con', 'dol', 'dot', 'loz', 'mod', 'top', 'won', 'xor' }, + }), snip('(K|M|N|Q|R|S|Z)([\\dn]) ', '<><>^<> ', { cap(1), cap(1), cap(2) }, math), snip('dx', 'dif / (dif <>) ', { i(1, 'x') }, math, 900), @@ -90,5 +94,12 @@ return { snip('lm', 'lim ', {}, math), snip('lim', 'lim_(<> ->> <>) ', { i(1, 'n'), i(2, 'oo') }, math), snip('lim (sup|inf)', 'lim<> ', { cap(1) }, math), - snip('lim(_\\(\\s?\\w+\\s?->\\s?\\w+\\s?\\)) (sup|inf)', 'lim<><> ', { cap(2), cap(1) }, math, 1000, true, 25), + snip( + 'lim(_\\(\\s?\\w+\\s?->\\s?\\w+\\s?\\)) (sup|inf)', + 'lim<><> ', + { cap(2), cap(1) }, + math, + 1000, + { maxTrigLength = 25 } + ), } diff --git a/lua/typstar/snippets/matrix.lua b/lua/typstar/snippets/matrix.lua index 3724ccd..17ce75c 100644 --- a/lua/typstar/snippets/matrix.lua +++ b/lua/typstar/snippets/matrix.lua @@ -61,7 +61,7 @@ local lmat = function(_, sp) ins_indx = ins_indx + 1 for k = 2, cols do table.insert(nodes, t(', ')) - if k == cols then table.insert(nodes, t('dots, ')) end + if k == cols then table.insert(nodes, t('dots.c, ')) end if j == k then table.insert(nodes, r(ins_indx, tostring(j) .. 'x' .. tostring(k), i(1, '1'))) else diff --git a/lua/typstar/snippets/visual.lua b/lua/typstar/snippets/visual.lua index d74d3b4..228d9d4 100644 --- a/lua/typstar/snippets/visual.lua +++ b/lua/typstar/snippets/visual.lua @@ -21,6 +21,7 @@ local operations = { -- first boolean: existing brackets should be kept; second { 'sQ', '[', ']', false, false }, -- replace with square brackets { 'BB', '', '', false, false }, -- remove brackets { 'ss', '"', '"', false, false }, + { 'agl', 'lr(angle.l ', ' angle.r)', false, false }, { 'abs', 'abs', '', true, true }, { 'ul', 'underline', '', true, true }, { 'ol', 'overline', '', true, true }, @@ -42,9 +43,9 @@ local ts_wrap_query = ts.query.parse('typst', '[(call) (ident) (letter) (number) local ts_wrapnobrackets_query = ts.query.parse('typst', '(group) @wrapnobrackets') local process_ts_query = function(bufnr, cursor, query, root, insert1, insert2, cut_offset) - for _, match, _ in query:iter_matches(root, bufnr, cursor[1], cursor[1] + 1) do - if match then - local start_row, start_col, end_row, end_col = utils.treesitter_match_start_end(match) + for _, match in ipairs(utils.treesitter_iter_matches(root, query, bufnr, cursor[1], cursor[1] + 1)) do + for _, nodes in pairs(match) do + local start_row, start_col, end_row, end_col = utils.treesitter_match_start_end(nodes) if end_row == cursor[1] and end_col == cursor[2] then vim.schedule(function() -- to not interfere with luasnip local cursor_offset = 0 @@ -81,7 +82,10 @@ local smart_wrap = function(args, parent, old_state, expand) end for _, val in pairs(operations) do - table.insert(snippets, snip(val[1], '<>', { d(1, smart_wrap, {}, { user_args = { val } }) }, math, 1500, false)) + table.insert( + snippets, + snip(val[1], '<>', { d(1, smart_wrap, {}, { user_args = { val } }) }, math, 1500, { wordTrig = false }) + ) end return { diff --git a/lua/typstar/utils.lua b/lua/typstar/utils.lua index 6dccdae..3a9c9a8 100644 --- a/lua/typstar/utils.lua +++ b/lua/typstar/utils.lua @@ -71,6 +71,21 @@ end function M.get_treesitter_root(bufnr) return ts.get_parser(bufnr):parse()[1]:root() end +function M.treesitter_iter_matches(root, query, bufnr, start, stop) + local result = {} + local idx = 1 + for _, matches, _ in query:iter_matches(root, bufnr, start, stop) do + if #matches then + if type(matches[1]) == 'userdata' then -- nvim version < 0.11 + matches = { matches } + end + result[idx] = matches + idx = idx + 1 + end + end + return result +end + function M.treesitter_match_start_end(match) local start_row, start_col, _, _ = match[1]:range() local _, _, end_row, end_col = match[#match]:range() @@ -80,9 +95,10 @@ end function M.cursor_within_treesitter_query(query, match_tolerance, cursor) cursor = cursor or M.get_cursor_pos() local bufnr = vim.api.nvim_get_current_buf() - for _, match, _ in query:iter_matches(M.get_treesitter_root(bufnr), bufnr, cursor[1], cursor[1] + 1) do - if match then - local start_row, start_col, end_row, end_col = M.treesitter_match_start_end(match) + local root = M.get_treesitter_root(bufnr) + for _, match in ipairs(M.treesitter_iter_matches(root, query, bufnr, cursor[1], cursor[1] + 1)) do + for _, nodes in pairs(match) do + local start_row, start_col, end_row, end_col = M.treesitter_match_start_end(nodes) local matched = M.cursor_within_coords(cursor, start_row, end_row, start_col, end_col, match_tolerance) if matched then return true end end diff --git a/pyproject.toml b/pyproject.toml index ba51394..53d6a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "pdm.backend" [project] name = "typstar" -version = "1.3.1" +version = "1.3.2" description = "Neovim plugin for efficient note taking in Typst" authors = [ { name = "arne314" } diff --git a/src/anki/flashcard.py b/src/anki/flashcard.py index 5ee2125..26b3029 100644 --- a/src/anki/flashcard.py +++ b/src/anki/flashcard.py @@ -1,3 +1,5 @@ +import html + import tree_sitter from .file_handler import FileHandler @@ -48,7 +50,9 @@ class Flashcard: return f"#flashcard({self.note_id})[{self.front if front else ''}][{self.back if not front else ''}]" def as_html(self, front: bool) -> str: - prefix = f"" # indexable via anki search + safe_front = html.escape(self.front) + safe_back = html.escape(self.back) + prefix = f"" # indexable via anki search image = f'' return prefix + image diff --git a/uv.lock b/uv.lock index 78a1ca7..bbb3141 100644 --- a/uv.lock +++ b/uv.lock @@ -437,7 +437,7 @@ wheels = [ [[package]] name = "typstar" -version = "1.3.1" +version = "1.3.2" source = { editable = "." } dependencies = [ { name = "aiohttp" },