From c0f28342163694772041330f73bde18604831569 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:12:40 +0100 Subject: [PATCH 1/9] minor: reduce ak/sk snippet priority --- lua/typstar/snippets/math.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/typstar/snippets/math.lua b/lua/typstar/snippets/math.lua index ec01aac..c9f74c8 100644 --- a/lua/typstar/snippets/math.lua +++ b/lua/typstar/snippets/math.lua @@ -31,8 +31,8 @@ return { snip('ge', '>>= ', {}, math), -- operators - snip('ak([^k])', '+<>', { cap(1) }, math, 500, false), - snip('sk([^k])', '-<>', { cap(1) }, math, 500, false), + snip('ak([^k])', '+<>', { cap(1) }, math, 100, false), + snip('sk([^k])', '-<>', { cap(1) }, math, 100, false), snip('oak', 'plus.circle ', {}, math, 1100), snip('bak', 'plus.square ', {}, math, 1100), snip('mak', 'plus.minus ', {}, math, 1100), From 6c23cba4bc30377424765c19fed09cb8bbdcaaf0 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:59:40 +0100 Subject: [PATCH 2/9] feat(snip): markup wrapping snippets --- lua/typstar/autosnippets.lua | 20 +++++++----- lua/typstar/config.lua | 2 +- lua/typstar/snippets/document.lua | 36 --------------------- lua/typstar/snippets/markup.lua | 52 +++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 44 deletions(-) delete mode 100644 lua/typstar/snippets/document.lua create mode 100644 lua/typstar/snippets/markup.lua diff --git a/lua/typstar/autosnippets.lua b/lua/typstar/autosnippets.lua index ef9b780..44ca4e2 100644 --- a/lua/typstar/autosnippets.lua +++ b/lua/typstar/autosnippets.lua @@ -31,12 +31,18 @@ function M.cap(i) return luasnip.function_node(function(_, snip) return snip.captures[i] end) end -function M.get_visual(args, parent) - if (#parent.snippet.env.LS_SELECT_RAW > 0) then - return luasnip.snippet_node(nil, luasnip.insert_node(1, parent.snippet.env.LS_SELECT_RAW)) - else -- If LS_SELECT_RAW is empty, return a blank insert node - return luasnip.snippet_node(nil, luasnip.insert_node(1)) - end +function M.visual(idx, default) + 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)) + else -- If LS_SELECT_RAW is empty, return an insert node + return luasnip.snippet_node(nil, luasnip.insert_node(1, default)) + end + end + ) end function M.ri(insert_node_id) @@ -62,7 +68,7 @@ function M.snip(trigger, expand, insert, condition, priority, wordTrig) end function M.start_snip(trigger, expand, insert, condition, priority) - return M.snip('^\\s*' .. trigger, expand, insert, condition, priority) + return M.snip('^(\\s*)' .. trigger, '<>' .. expand, { M.cap(1), unpack(insert) }, condition, priority) end function M.engine(trigger, opts) diff --git a/lua/typstar/config.lua b/lua/typstar/config.lua index c8530b2..7ee2401 100644 --- a/lua/typstar/config.lua +++ b/lua/typstar/config.lua @@ -19,10 +19,10 @@ local default_config = { snippets = { enable = true, modules = { -- enable modules from ./snippets - 'document', 'letters', 'math', 'matrix', + 'markup', 'visual', } }, diff --git a/lua/typstar/snippets/document.lua b/lua/typstar/snippets/document.lua deleted file mode 100644 index 63dbd3c..0000000 --- a/lua/typstar/snippets/document.lua +++ /dev/null @@ -1,36 +0,0 @@ -local ls = require('luasnip') -local i = ls.insert_node -local d = ls.dynamic_node - -local helper = require('typstar.autosnippets') -local snip = helper.snip -local start = helper.start_snip -local markup = helper.in_markup - - -local ctheorems = { - { 'tem', 'theorem', markup }, - { 'pro', 'proof', markup }, - { 'axi', 'axiom', markup }, - { 'cor', 'corollary', markup }, - { 'lem', 'lemma', markup }, - { 'def', 'definition', markup }, - { 'exa', 'example', markup }, - { 'rem', 'remark', markup }, -} - -local ctheoremsstr = '#%s[\n\t<>\n]' -local document_snippets = {} - -for _, val in pairs(ctheorems) do - local snippet = start(val[1], string.format(ctheoremsstr, val[2]), { i(1) }, val[3]) - table.insert(document_snippets, snippet) -end - -return { - start('dm', '$\n\t<>\n$', { i(1) }, markup), - snip('ll', ' $<>$', { i(1, '1+1') }, markup), - start('fla', '#flashcard(0)[<>][\n\t<>\n]', { i(1, "flashcard"), i(2) }, markup), - start('flA', '#flashcard(0, "<>")[\n\t<>\n]', { i(1, "flashcard"), i(2) }, markup), - unpack(document_snippets), -} diff --git a/lua/typstar/snippets/markup.lua b/lua/typstar/snippets/markup.lua new file mode 100644 index 0000000..2daeb1e --- /dev/null +++ b/lua/typstar/snippets/markup.lua @@ -0,0 +1,52 @@ +local ls = require('luasnip') +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 ctheorems = { + { 'tem', 'theorem' }, + { 'pro', 'proof' }, + { 'axi', 'axiom' }, + { 'cor', 'corollary' }, + { 'lem', 'lemma' }, + { 'def', 'definition' }, + { 'exa', 'example' }, + { 'rem', 'remark' }, +} + +local wrappings = { + { 'll', '$', '$', '1+1' }, + { 'BLD', '*', '*', 'abc' }, + { 'ITL', '_', '_', 'abc' }, + { 'HIG', '#highlight[', ']', 'abc' }, + { 'UND', '#underline[', ']', 'abc' }, +} + +local document_snippets = {} +local ctheoremsstr = '#%s[\n<>\t<>\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) + 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) + table.insert(document_snippets, snippet) +end + +return { + start('dm', '$\n<>\t<>\n<>$', { cap(1), visual(1), cap(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), + snip('IMP', '$=>>$ ', {}, markup), + snip('IFF', '$<<=>>$ ', {}, markup), + unpack(document_snippets), +} From 0b84b6c53d3e3e84efdc4cffa6cbf98b2b4d9736 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:09:07 +0100 Subject: [PATCH 3/9] docs: markup snippets --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3ec428c..1243274 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,20 @@ Neovim plugin for efficient note taking in Typst Use `:TypstarToggleSnippets` to toggle all snippets at any time. Available snippets can mostly be intuitively derived from [here](././lua/typstar/snippets), they include: +Markup snippets: +- Begin inline math with `ll` and multiline math with `dm` +- [Markup shorthands](./lua/typstar/snippets/markup.lua) (e.g. `HIG` → `#highlight[]`, `IMP` → `$=>$ `) +- [ctheorems shorthands](./lua/typstar/snippets/markup.lua) (e.g. `tem` → empty theorem, `exa` → empty example) +- [Flashcards](#anki): `fla` and `flA` +- All above snippets support visual mode via the [selection key](#installation) + +Math snippets: +- [Many shorthands](./lua/typstar/snippets/math.lua) for mathematical expressions - Alphanumeric characters: `:` → `$$ ` in markup (e.g. `:X` → `$X$ `, `:5` → `$5$ `) - Greek letters: `;` → `` in math and `$$ ` in markup (e.g. `;a` → `alpha`/`$alpha$ `) - Common indices (numbers and letters `i-n`): `` → `_` in math and `$$ ` → `$_$ ` in markup (e.g `A314` → `A_314`, `$alpha$n ` → `$alpha_n$ `) - Wrapping of any mathematical expression (see [operations](./lua/typstar/snippets/visual.lua), works nested, multiline and in visual mode via the [selection key](#installation)): `` → `()` (e.g. `(a^2+b^2)rt` → `sqrt(a^2+b^2)`, `lambdatd` → `tilde(lambda)`, `(1+1)sQ` → `[1+1]`, `(1+1)sq` → `[(1+1)]`) - Matrices: `ma` and `lma` (e.g. `23ma` → 2x3 matrix) -- [ctheorems shorthands](./lua/typstar/snippets/document.lua) (e.g. `tem` → empty theorem, `exa` → empty example) -- [Many shorthands](./lua/typstar/snippets/math.lua) for mathematical expressions Note that you can enable and disable collections of snippets in the [config](#configuration). @@ -74,7 +81,7 @@ require('typstar').setup({ 1. Install [LuaSnip](https://github.com/L3MON4D3/LuaSnip/), set `enable_autosnippets = true` and set a visual mode selection key (e.g. `store_selection_keys = ''`) in the configuration 2. Install [jsregexp](https://github.com/kmarius/jsregexp) as described [here](https://github.com/L3MON4D3/LuaSnip/blob/master/DOC.md#transformations) (running `:lua require('jsregexp')` in nvim should not result in an error) 3. Install [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) and run `:TSInstall typst` -4. Optional: Setup [ctheorems](https://typst.app/universe/package/ctheorems/) with names like [here](./lua/typstar/snippets/document.lua) +4. Optional: Setup [ctheorems](https://typst.app/universe/package/ctheorems/) with names like [here](./lua/typstar/snippets/markup.lua) ### Excalidraw 1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory From 434dc5c6c40072321051f2abbe426672af7fb93d Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:44:16 +0100 Subject: [PATCH 4/9] feat(snip): warning if jsregexp is not installed --- README.md | 2 +- lua/typstar/autosnippets.lua | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1243274..cee2061 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ require('typstar').setup({ ### Snippets 1. Install [LuaSnip](https://github.com/L3MON4D3/LuaSnip/), set `enable_autosnippets = true` and set a visual mode selection key (e.g. `store_selection_keys = ''`) in the configuration -2. Install [jsregexp](https://github.com/kmarius/jsregexp) as described [here](https://github.com/L3MON4D3/LuaSnip/blob/master/DOC.md#transformations) (running `:lua require('jsregexp')` in nvim should not result in an error) +2. Install [jsregexp](https://github.com/kmarius/jsregexp) as described [here](https://github.com/L3MON4D3/LuaSnip/blob/master/DOC.md#transformations) (You will see a warning on startup if jsregexp isn't installed properly) 3. Install [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) and run `:TSInstall typst` 4. Optional: Setup [ctheorems](https://typst.app/universe/package/ctheorems/) with names like [here](./lua/typstar/snippets/markup.lua) diff --git a/lua/typstar/autosnippets.lua b/lua/typstar/autosnippets.lua index 44ca4e2..9fe5c8b 100644 --- a/lua/typstar/autosnippets.lua +++ b/lua/typstar/autosnippets.lua @@ -105,6 +105,13 @@ function M.setup() ) end luasnip.add_snippets('typst', autosnippets) + local jsregexp_ok, _ = pcall(require, "luasnip-jsregexp") + if not jsregexp_ok then + jsregexp_ok, _ = pcall(require, "jsregexp") + end + if not jsregexp_ok then + vim.notify("WARNING: Most snippets won't work as jsregexp is not installed", vim.log.levels.WARN) + end end end From e9824803b5d2460a275595863847fd734ff16d64 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:48:18 +0100 Subject: [PATCH 5/9] style: format lua using stylua --- .stylua.toml | 10 +++++ lua/typstar/anki.lua | 25 +++++------- lua/typstar/autosnippets.lua | 42 +++++++------------ lua/typstar/config.lua | 6 +-- lua/typstar/excalidraw.lua | 14 +++---- lua/typstar/snippets/letters.lua | 34 ++++++++-------- lua/typstar/snippets/markup.lua | 11 +++-- lua/typstar/snippets/matrix.lua | 4 +- lua/typstar/snippets/visual.lua | 69 ++++++++++++++------------------ lua/typstar/utils.lua | 42 ++++++------------- 10 files changed, 114 insertions(+), 143 deletions(-) create mode 100644 .stylua.toml diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..36ced5e --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,10 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 4 +quote_style = "AutoPreferSingle" +call_parentheses = "Always" +collapse_simple_statement = "Always" + +[sort_requires] +enabled = true diff --git a/lua/typstar/anki.lua b/lua/typstar/anki.lua index 01c2a43..d0d3490 100644 --- a/lua/typstar/anki.lua +++ b/lua/typstar/anki.lua @@ -4,29 +4,26 @@ local utils = require('typstar.utils') local cfg = config.config.anki - local function run_typstar_anki(args) local cwd = vim.fn.getcwd() local anki_key = '' - if cfg.ankiKey ~= nil then - anki_key = ' --anki-key ' .. cfg.ankiKey - end + if cfg.ankiKey ~= nil then anki_key = ' --anki-key ' .. cfg.ankiKey end local cmd = string.format( '%s --root-dir %s --typst-cmd %s --anki-url %s %s %s', - cfg.typstarAnkiCmd, cwd, cfg.typstCmd, cfg.ankiUrl, anki_key, args) + cfg.typstarAnkiCmd, + cwd, + cfg.typstCmd, + cfg.ankiUrl, + anki_key, + args + ) utils.run_shell_command(cmd, true) end -function M.scan() - run_typstar_anki('') -end +function M.scan() run_typstar_anki('') end -function M.scan_force() - run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) -end +function M.scan_force() run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) end -function M.scan_force_current() - run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p')) -end +function M.scan_force_current() run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p')) end return M diff --git a/lua/typstar/autosnippets.lua b/lua/typstar/autosnippets.lua index 9fe5c8b..45d5230 100644 --- a/lua/typstar/autosnippets.lua +++ b/lua/typstar/autosnippets.lua @@ -1,16 +1,14 @@ local M = {} local cfg = require('typstar.config').config.snippets -local utils = require('typstar.utils') 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 last_keystroke_time = nil vim.api.nvim_create_autocmd('TextChangedI', { - callback = function() - last_keystroke_time = vim.loop.now() - end, + callback = function() last_keystroke_time = vim.loop.now() end, }) local lexical_result_cache = {} local ts_markup_query = ts.query.parse('typst', '(text) @markup') @@ -33,16 +31,13 @@ end function M.visual(idx, default) 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)) - else -- If LS_SELECT_RAW is empty, return an insert node - return luasnip.snippet_node(nil, luasnip.insert_node(1, default)) - end + 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)) + else -- If LS_SELECT_RAW is empty, return an insert node + return luasnip.snippet_node(nil, luasnip.insert_node(1, default)) end - ) + end) end function M.ri(insert_node_id) @@ -58,11 +53,11 @@ function M.snip(trigger, expand, insert, condition, priority, wordTrig) trigEngineOpts = { condition = condition }, wordTrig = wordTrig, priority = priority, - snippetType = 'autosnippet' + snippetType = 'autosnippet', }, fmta(expand, { unpack(insert) }), { - condition = function() return M.snippets_toggle end + condition = function() return M.snippets_toggle end, } ) end @@ -75,17 +70,13 @@ function M.engine(trigger, opts) local base_engine = lsengines.ecma(trigger, opts) local condition = function() local cached = lexical_result_cache[opts.condition] - if cached ~= nil and cached[1] == last_keystroke_time then - return cached[2] - end + 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 return function(line, trig) - if not M.snippets_toggle or not condition() then - return nil - end + if not M.snippets_toggle or not condition() then return nil end return base_engine(line, trig) end end @@ -99,15 +90,12 @@ function M.setup() if cfg.enable then local autosnippets = {} for _, file in ipairs(cfg.modules) do - vim.list_extend( - autosnippets, - require(('typstar.snippets.%s'):format(file)) - ) + vim.list_extend(autosnippets, require(('typstar.snippets.%s'):format(file))) end luasnip.add_snippets('typst', autosnippets) - local jsregexp_ok, _ = pcall(require, "luasnip-jsregexp") + local jsregexp_ok, _ = pcall(require, 'luasnip-jsregexp') if not jsregexp_ok then - jsregexp_ok, _ = pcall(require, "jsregexp") + jsregexp_ok, _ = pcall(require, 'jsregexp') end if not jsregexp_ok then vim.notify("WARNING: Most snippets won't work as jsregexp is not installed", vim.log.levels.WARN) diff --git a/lua/typstar/config.lua b/lua/typstar/config.lua index 7ee2401..9491850 100644 --- a/lua/typstar/config.lua +++ b/lua/typstar/config.lua @@ -24,14 +24,14 @@ local default_config = { 'matrix', 'markup', 'visual', - } + }, }, } function M.merge_config(args) M.config = vim.tbl_deep_extend('force', default_config, args or {}) - M.config.excalidraw.templatePath = M.config.excalidraw.templatePath or - { + M.config.excalidraw.templatePath = M.config.excalidraw.templatePath + or { ['%.excalidraw%.md$'] = M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md', } end diff --git a/lua/typstar/excalidraw.lua b/lua/typstar/excalidraw.lua index f714d9d..fb6a8ab 100644 --- a/lua/typstar/excalidraw.lua +++ b/lua/typstar/excalidraw.lua @@ -12,7 +12,8 @@ local affix = [[ local function launch_obsidian(path) print(string.format('Opening %s in Excalidraw', path)) utils.run_shell_command( - string.format('%s "obsidian://open?path=%s"', cfg.uriOpenCommand, utils.urlencode(path)), false + string.format('%s "obsidian://open?path=%s"', cfg.uriOpenCommand, utils.urlencode(path)), + false ) end @@ -22,9 +23,7 @@ function M.insert_drawing() local path = assets_dir .. '/' .. filename .. cfg.fileExtension local path_inserted = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted - if vim.fn.isdirectory(assets_dir) == 0 then - vim.fn.mkdir(assets_dir, 'p') - end + if vim.fn.isdirectory(assets_dir) == 0 then vim.fn.mkdir(assets_dir, 'p') end local found_match = false for pattern, template_path in pairs(cfg.templatePath) do if string.match(path, pattern) then @@ -44,9 +43,10 @@ end function M.open_drawing() local line = vim.api.nvim_get_current_line() - local path = vim.fn.expand('%:p:h') .. - '/' .. string.match(line, '"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.')) .. - '.excalidraw.md' + local path = vim.fn.expand('%:p:h') + .. '/' + .. string.match(line, '"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.')) + .. '.excalidraw.md' launch_obsidian(path) end diff --git a/lua/typstar/snippets/letters.lua b/lua/typstar/snippets/letters.lua index dba0129..35908bf 100644 --- a/lua/typstar/snippets/letters.lua +++ b/lua/typstar/snippets/letters.lua @@ -8,7 +8,6 @@ local cap = helper.cap local math = helper.in_math local markup = helper.in_markup - local letter_snippets = {} local greek_letters_map = { ['a'] = 'alpha', @@ -42,9 +41,7 @@ local trigger_greek = '' local trigger_index_pre = '' local trigger_index_post = '' -local upper_first = function(str) - return str:sub(1, 1):upper() .. str:sub(2, -1) -end +local upper_first = function(str) return str:sub(1, 1):upper() .. str:sub(2, -1) end local greek_full = {} for latin, greek in pairs(greek_letters_map) do @@ -65,28 +62,33 @@ trigger_greek = table.concat(greek_keys, '|') trigger_index_pre = '[A-Za-z]' .. '|' .. table.concat(greek_letters, '|') trigger_index_post = table.concat(common_indices, '|') -local get_greek = function(_, snippet) - return s(nil, t(greek_letters_map[snippet.captures[1]])) -end +local get_greek = function(_, snippet) return s(nil, t(greek_letters_map[snippet.captures[1]])) end local get_index = function(_, snippet) local letter, index = snippet.captures[1], snippet.captures[2] local trigger = letter .. index - if index_conflicts_set[trigger] then - return s(nil, t(trigger)) - end + if index_conflicts_set[trigger] then return s(nil, t(trigger)) end return s(nil, t(letter .. '_' .. index)) end table.insert(letter_snippets, snip(':([A-Za-z0-9])', '$<>$ ', { cap(1) }, markup)) table.insert(letter_snippets, snip(';(' .. trigger_greek .. ')', '$<>$ ', { d(1, get_greek) }, markup)) table.insert(letter_snippets, snip(';(' .. trigger_greek .. ')', '<>', { d(1, get_greek) }, math)) -table.insert(letter_snippets, - snip('\\$(' .. trigger_index_pre .. ')\\$' .. '(' .. trigger_index_post .. ') ', - '$<>$ ', { d(1, get_index) }, markup, 500)) -table.insert(letter_snippets, - snip('(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ') ', '<> ', { d(1, get_index) }, math, 200)) +table.insert( + letter_snippets, + snip( + '\\$(' .. trigger_index_pre .. ')\\$' .. '(' .. trigger_index_post .. ') ', + '$<>$ ', + { d(1, get_index) }, + markup, + 500 + ) +) +table.insert( + letter_snippets, + snip('(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ') ', '<> ', { d(1, get_index) }, math, 200) +) return { - unpack(letter_snippets) + unpack(letter_snippets), } diff --git a/lua/typstar/snippets/markup.lua b/lua/typstar/snippets/markup.lua index 2daeb1e..79739cc 100644 --- a/lua/typstar/snippets/markup.lua +++ b/lua/typstar/snippets/markup.lua @@ -8,7 +8,6 @@ local visual = helper.visual local snip = helper.snip local start = helper.start_snip - local ctheorems = { { 'tem', 'theorem' }, { 'pro', 'proof' }, @@ -21,9 +20,9 @@ local ctheorems = { } local wrappings = { - { 'll', '$', '$', '1+1' }, - { 'BLD', '*', '*', 'abc' }, - { 'ITL', '_', '_', 'abc' }, + { 'll', '$', '$', '1+1' }, + { 'BLD', '*', '*', 'abc' }, + { 'ITL', '_', '_', 'abc' }, { 'HIG', '#highlight[', ']', 'abc' }, { 'UND', '#underline[', ']', 'abc' }, } @@ -44,8 +43,8 @@ end return { start('dm', '$\n<>\t<>\n<>$', { cap(1), visual(1), cap(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('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), snip('IMP', '$=>>$ ', {}, markup), snip('IFF', '$<<=>>$ ', {}, markup), unpack(document_snippets), diff --git a/lua/typstar/snippets/matrix.lua b/lua/typstar/snippets/matrix.lua index 8c76aac..3724ccd 100644 --- a/lua/typstar/snippets/matrix.lua +++ b/lua/typstar/snippets/matrix.lua @@ -61,9 +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, ')) 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 d821bcf..b588189 100644 --- a/lua/typstar/snippets/visual.lua +++ b/lua/typstar/snippets/visual.lua @@ -5,35 +5,35 @@ local i = ls.insert_node local s = ls.snippet_node local t = ls.text_node -local utils = require('typstar.utils') local helper = require('typstar.autosnippets') +local utils = require('typstar.utils') local math = helper.in_math local snip = helper.snip local snippets = {} -local operations = { -- first boolean: existing brackets should be kept; second boolean: brackets should be added - { 'vi', '1/', '', true, false }, - { 'bb', '(', ')', true, false }, -- add round brackets - { 'sq', '[', ']', true, false }, -- add square brackets - { 'bB', '(', ')', false, false }, -- replace with round brackets - { 'sQ', '[', ']', false, false }, -- replace with square brackets - { 'BB', '', '', false, false }, -- remove brackets - { 'ss', '"', '"', false, false }, - { 'abs', 'abs', '', true, true }, - { 'ul', 'underline', '', true, true }, - { 'ol', 'overline', '', true, true }, - { 'ub', 'underbrace', '', true, true }, - { 'ob', 'overbrace', '', true, true }, - { 'ht', 'hat', '', true, true }, - { 'br', 'macron', '', true, true }, - { 'dt', 'dot', '', true, true }, - { 'ci', 'circle', '', true, true }, - { 'td', 'tilde', '', true, true }, - { 'nr', 'norm', '', true, true }, - { 'vv', 'vec', '', true, true }, - { 'rt', 'sqrt', '', true, true }, - { 'flo', 'floor', '', true, true }, - { 'cei', 'ceil', '', true, true }, +local operations = { -- first boolean: existing brackets should be kept; second boolean: brackets should be added + { 'vi', '1/', '', true, false }, + { 'bb', '(', ')', true, false }, -- add round brackets + { 'sq', '[', ']', true, false }, -- add square brackets + { 'bB', '(', ')', false, false }, -- replace with round brackets + { 'sQ', '[', ']', false, false }, -- replace with square brackets + { 'BB', '', '', false, false }, -- remove brackets + { 'ss', '"', '"', false, false }, + { 'abs', 'abs', '', true, true }, + { 'ul', 'underline', '', true, true }, + { 'ol', 'overline', '', true, true }, + { 'ub', 'underbrace', '', true, true }, + { 'ob', 'overbrace', '', true, true }, + { 'ht', 'hat', '', true, true }, + { 'br', 'macron', '', true, true }, + { 'dt', 'dot', '', true, true }, + { 'ci', 'circle', '', true, true }, + { 'td', 'tilde', '', true, true }, + { 'nr', 'norm', '', true, true }, + { 'vv', 'vec', '', true, true }, + { 'rt', 'sqrt', '', true, true }, + { 'flo', 'floor', '', true, true }, + { 'cei', 'ceil', '', true, true }, } local ts_wrap_query = ts.query.parse('typst', '[(call) (ident) (letter) (number)] @wrap') @@ -46,16 +46,11 @@ local process_ts_query = function(bufnr, cursor, query, root, insert1, insert2, if end_row == cursor[1] and end_col == cursor[2] then vim.schedule(function() -- to not interfere with luasnip local cursor_offset = 0 - local old_len1, new_len1 = utils.insert_text( - bufnr, start_row, start_col, insert1, 0, cut_offset) - if start_row == cursor[1] then - cursor_offset = cursor_offset + (new_len1 - old_len1) - end - local old_len2, new_len2 = utils.insert_text( - bufnr, end_row, cursor[2] + cursor_offset, insert2, cut_offset, 0) - if end_row == cursor[1] then - cursor_offset = cursor_offset + (new_len2 - old_len2) - end + local old_len1, new_len1 = utils.insert_text(bufnr, start_row, start_col, insert1, 0, cut_offset) + if start_row == cursor[1] then cursor_offset = cursor_offset + (new_len1 - old_len1) end + local old_len2, new_len2 = + utils.insert_text(bufnr, end_row, cursor[2] + cursor_offset, insert2, cut_offset, 0) + if end_row == cursor[1] then cursor_offset = cursor_offset + (new_len2 - old_len2) end vim.api.nvim_win_set_cursor(0, { cursor[1] + 1, cursor[2] + cursor_offset }) end) return true @@ -76,9 +71,7 @@ local smart_wrap = function(args, parent, old_state, expand) local expand1 = expand[5] and expand[2] .. '(' or expand[2] local expand2 = expand[5] and expand[3] .. ')' or expand[3] - if process_ts_query(bufnr, cursor, ts_wrap_query, root, expand1, expand2) then - return s(nil, t()) - end + if process_ts_query(bufnr, cursor, ts_wrap_query, root, expand1, expand2) then return s(nil, t()) end if #parent.env.LS_SELECT_RAW > 0 then return s(nil, t(expand1 .. table.concat(parent.env.LS_SELECT_RAW) .. expand2)) end @@ -90,5 +83,5 @@ for _, val in pairs(operations) do end return { - unpack(snippets) + unpack(snippets), } diff --git a/lua/typstar/utils.lua b/lua/typstar/utils.lua index 6093ca3..4118d7c 100644 --- a/lua/typstar/utils.lua +++ b/lua/typstar/utils.lua @@ -20,7 +20,7 @@ end function M.insert_text_block(snip) local line_num = M.get_cursor_pos()[1] + 1 local lines = {} - for line in snip:gmatch '[^\r\n]+' do + for line in snip:gmatch('[^\r\n]+') do table.insert(lines, line) end vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), line_num, line_num, false, lines) @@ -35,45 +35,32 @@ function M.run_shell_command(cmd, show_output) end end if show_output then - vim.fn.jobstart( - cmd, - { - on_stdout = function(_, data, _) - handle_output(data, false) - end, - on_stderr = function(_, data, _) - handle_output(data, true) - end, - stdout_buffered = false, - stderr_buffered = true, - } - ) + vim.fn.jobstart(cmd, { + on_stdout = function(_, data, _) handle_output(data, false) end, + on_stderr = function(_, data, _) handle_output(data, true) end, + stdout_buffered = false, + stderr_buffered = true, + }) else vim.fn.jobstart(cmd) end end -function M.char_to_hex(c) - return string.format("%%%02X", string.byte(c)) -end +function M.char_to_hex(c) return string.format('%%%02X', string.byte(c)) end function M.urlencode(url) - if url == nil then - return '' - end + if url == nil then return '' end url = string.gsub(url, '\n', '\r\n') url = string.gsub(url, '([^%w _%%%-%.~])', M.char_to_hex) url = string.gsub(url, ' ', '%%20') return url end -function M.get_treesitter_root(bufnr) - return ts.get_parser(bufnr):parse()[1]:root() -end +function M.get_treesitter_root(bufnr) return ts.get_parser(bufnr):parse()[1]:root() end function M.treesitter_match_start_end(match) local start_row, start_col, _, _ = match[1]:range() - local _, _, end_row, end_col = match[#match]:range() + local _, _, end_row, end_col = match[#match]:range() return start_row, start_col, end_row, end_col end @@ -83,11 +70,8 @@ function M.cursor_within_treesitter_query(query, match_tolerance, cursor) 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 matched = M.cursor_within_coords(cursor, start_row, end_row, start_col, end_col, - match_tolerance) - if matched then - return true - end + local matched = M.cursor_within_coords(cursor, start_row, end_row, start_col, end_col, match_tolerance) + if matched then return true end end end return false From 4f9cf683fc7f2e16b2c395dbdb13a93089e4b3bd Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:16:01 +0100 Subject: [PATCH 6/9] feat(snip): improve greek letters --- lua/typstar/snippets/letters.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/typstar/snippets/letters.lua b/lua/typstar/snippets/letters.lua index 35908bf..6cc6257 100644 --- a/lua/typstar/snippets/letters.lua +++ b/lua/typstar/snippets/letters.lua @@ -24,16 +24,19 @@ local greek_letters_map = { ['m'] = 'mu', ['n'] = 'nu', ['o'] = 'omega', - ['p'] = 'pi', + ['p'] = 'psi', ['q'] = 'eta', ['r'] = 'rho', ['s'] = 'sigma', ['t'] = 'tau', + ['v'] = 'nu', + ['w'] = 'omega', ['x'] = 'xi', + ['y'] = 'upsilon', ['z'] = 'zeta', } -local greek_letters = {} local greek_keys = {} +local greek_letters_set = {} local common_indices = { '\\d+', '[i-n]' } local index_conflicts = { 'in', 'ln', 'pi', 'xi' } local index_conflicts_set = {} @@ -47,8 +50,10 @@ local greek_full = {} for latin, greek in pairs(greek_letters_map) do greek_full[latin] = greek greek_full[latin:upper()] = upper_first(greek) - table.insert(greek_letters, greek) - table.insert(greek_letters, upper_first(greek)) + if not greek_letters_set[greek] then + table.insert(greek_letters_set, greek) + table.insert(greek_letters_set, upper_first(greek)) + end table.insert(greek_keys, latin) table.insert(greek_keys, latin:upper()) end @@ -59,7 +64,7 @@ end greek_letters_map = greek_full trigger_greek = table.concat(greek_keys, '|') -trigger_index_pre = '[A-Za-z]' .. '|' .. table.concat(greek_letters, '|') +trigger_index_pre = '[A-Za-z]' .. '|' .. table.concat(greek_letters_set, '|') trigger_index_post = table.concat(common_indices, '|') local get_greek = function(_, snippet) return s(nil, t(greek_letters_map[snippet.captures[1]])) end From a1f2276614d6db22e4c3be7cc8a7f90d9f9b10e6 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:19:39 +0100 Subject: [PATCH 7/9] minor(snip): update math snippets --- lua/typstar/snippets/letters.lua | 2 +- lua/typstar/snippets/math.lua | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/typstar/snippets/letters.lua b/lua/typstar/snippets/letters.lua index 6cc6257..9b0513b 100644 --- a/lua/typstar/snippets/letters.lua +++ b/lua/typstar/snippets/letters.lua @@ -38,7 +38,7 @@ local greek_letters_map = { local greek_keys = {} local greek_letters_set = {} local common_indices = { '\\d+', '[i-n]' } -local index_conflicts = { 'in', 'ln', 'pi', 'xi' } +local index_conflicts = { 'in', 'ln', 'pi', 'xi', 'ak', 'sk' } local index_conflicts_set = {} local trigger_greek = '' local trigger_index_pre = '' diff --git a/lua/typstar/snippets/math.lua b/lua/typstar/snippets/math.lua index c9f74c8..2e2a7f7 100644 --- a/lua/typstar/snippets/math.lua +++ b/lua/typstar/snippets/math.lua @@ -20,6 +20,7 @@ return { -- boolean logic snip('no', 'not ', {}, math), + snip('iff', '<<=>> ', {}, math), -- relations snip('el', '= ', {}, math), @@ -60,10 +61,11 @@ return { snip('Oo', 'compose ', {}, math), snip('iso', 'tilde.equiv ', {}, math), snip('ep', 'exp(<>) ', { i(1, '1') }, math), - snip('rrn', 'RR^n ', {}, math), snip('cc', 'cases(\n\t<>\n)\\', { i(1, '1') }, math), + snip('(K|M|N|Q|R|S|Z)([\\dn]) ', '<><>^<> ', { cap(1), cap(1), cap(2) }, math), snip('(.*)iv', '<>^(-1)', { cap(1) }, math), snip('(.*)sr', '<>^2', { cap(1) }, math), + snip('(.*)cb', '<>^3', { cap(1) }, math), snip('(.*)jj', '<>_(<>)', { cap(1), i(1, 'n') }, math), snip('(.*)kk', '<>^(<>)', { cap(1), i(1, 'n') }, math), From 1de9ecee923ab036dd04f5c4c70d5d3076bed05e Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:38:34 +0100 Subject: [PATCH 8/9] feat(anki): default deck per directory --- README.md | 1 + pyproject.toml | 2 +- src/anki/config_parser.py | 35 +++++++++++++++++++++++++++++++++++ src/anki/parser.py | 30 ++++++++++-------------------- uv.lock | 2 +- 5 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 src/anki/config_parser.py diff --git a/README.md b/README.md index cee2061..8029b02 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ To render the flashcard in your document as well add some code like this ``` - Add a comment like `// ANKI: MY::DECK` to your document to set a deck used for all flashcards after this comment (You can use multiple decks per file) +- Add a file named `.anki` containing a deck name to define a default deck on a directory base - Add a file named `.anki.typ` to define a preamble on a directory base. You can find the default preamble [here](./src/anki/typst_compiler.py). - Tip: Despite the use of SVGs you can still search your flashcards in Anki as the typst source is added into an invisible html paragraph diff --git a/pyproject.toml b/pyproject.toml index 150a000..ddae3f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "pdm.backend" [project] name = "typstar" -version = "1.1.1" +version = "1.2.0" description = "Neovim plugin for efficient note taking in Typst" authors = [ { name = "arne314" } diff --git a/src/anki/config_parser.py b/src/anki/config_parser.py new file mode 100644 index 0000000..a821d13 --- /dev/null +++ b/src/anki/config_parser.py @@ -0,0 +1,35 @@ +from collections import defaultdict +from functools import cache +from glob import glob +from pathlib import Path + + +class RecursiveConfigParser: + dir: Path + targets: set[str] + results: dict[str, dict[Path, str]] + + def __init__(self, dir, targets): + self.dir = dir + self.targets = set(targets) + self.results = defaultdict(dict) + self._parse_recursive() + + def _parse_recursive(self): + files = [] + for target in self.targets: + files.extend(glob(f"{self.dir}/**/{target}", include_hidden=target.startswith("."), recursive=True)) + for file in files: + file = Path(file) + if file.name in self.targets: + self.results[file.name][file.parent] = file.read_text(encoding="utf-8") + + @cache + def get_config(self, path: Path, target) -> str | None: + root_parent = self.dir.parent.resolve() + path = Path(path.resolve()) + target_results = self.results[target] + while path != root_parent: + if result := target_results.get(path): + return result + path = path.parent diff --git a/src/anki/parser.py b/src/anki/parser.py index dd33158..ba49abf 100644 --- a/src/anki/parser.py +++ b/src/anki/parser.py @@ -1,7 +1,6 @@ -import glob import json import re -from functools import cache +from glob import glob from pathlib import Path from typing import List, Tuple @@ -9,6 +8,7 @@ import appdirs import tree_sitter from tree_sitter_typst import language as get_typst_language +from .config_parser import RecursiveConfigParser from .file_handler import FileHandler from .flashcard import Flashcard @@ -38,6 +38,8 @@ ts_deck_query = """ deck_regex = re.compile(r"\W+ANKI:\s*([\S ]*)") + + class FlashcardParser: typst_language: tree_sitter.Language typst_parser: tree_sitter.Parser @@ -56,7 +58,7 @@ class FlashcardParser: self.file_handlers = [] self._load_file_hashes() - def _parse_file(self, file: FileHandler, preamble: str | None) -> List[Flashcard]: + def _parse_file(self, file: FileHandler, preamble: str | None, default_deck: str | None) -> List[Flashcard]: cards = [] tree = self.typst_parser.parse(file.get_bytes(), encoding="utf8") card_captures = self.flashcard_query.captures(tree.root_node) @@ -73,7 +75,7 @@ class FlashcardParser: deck_refs: List[Tuple[int, str | None]] = [] deck_refs_idx = -1 - current_deck = None + current_deck = default_deck if deck_captures: deck_captures["deck"].sort(key=row_compare) for comment in deck_captures["deck"]: @@ -108,6 +110,7 @@ class FlashcardParser: return cards def parse_directory(self, root_dir: Path, force_scan: Path | None = None): + flashcards = [] single_file = None is_force_scan = force_scan is not None if is_force_scan: @@ -123,22 +126,9 @@ class FlashcardParser: f"Parsing flashcards in {scan_dir if single_file is None else single_file} ...", flush=True, ) - preambles = {} - flashcards = [] + configs = RecursiveConfigParser(root_dir, {".anki", ".anki.typ"}) - @cache - def get_preamble(path: Path) -> str | None: - while path != root_dir.parent: - if preamble := preambles.get(path): - return preamble - path = path.parent - - for file in glob.glob(f"{root_dir}/**/.anki.typ", include_hidden=True, recursive=True): - file = Path(file) - if file.name == ".anki.typ": - preambles[file.parent] = file.read_text(encoding="utf-8") - - for file in glob.glob(f"{scan_dir}/**/**.typ", recursive=True): + for file in glob(f"{scan_dir}/**/**.typ", recursive=True): file = Path(file) if single_file is not None and file != single_file: continue @@ -146,7 +136,7 @@ class FlashcardParser: fh = FileHandler(file) file_changed = self._hash_changed(fh) if is_force_scan or file_changed: - cards = self._parse_file(fh, get_preamble(file.parent)) + cards = self._parse_file(fh, configs.get_config(file, ".anki.typ"), configs.get_config(file, ".anki")) self.file_handlers.append((fh, cards)) flashcards.extend(cards) return flashcards diff --git a/uv.lock b/uv.lock index c2e72a0..8aed491 100644 --- a/uv.lock +++ b/uv.lock @@ -437,7 +437,7 @@ wheels = [ [[package]] name = "typstar" -version = "1.1.0" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From d03a5e4062b5d0e248fb788a1de3fb2d825009b8 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:49:41 +0100 Subject: [PATCH 9/9] style: remove space --- lua/typstar/snippets/visual.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/typstar/snippets/visual.lua b/lua/typstar/snippets/visual.lua index 9e36a08..74b2f5d 100644 --- a/lua/typstar/snippets/visual.lua +++ b/lua/typstar/snippets/visual.lua @@ -16,7 +16,7 @@ local operations = { -- first boolean: existing brackets should be kept; second { 'vi', '1/', '', true, false }, { 'bb', '(', ')', true, false }, -- add round brackets { 'sq', '[', ']', true, false }, -- add square brackets - { 'st', '{', '}', true, false }, -- add curly brackets + { 'st', '{', '}', true, false }, -- add curly brackets { 'bB', '(', ')', false, false }, -- replace with round brackets { 'sQ', '[', ']', false, false }, -- replace with square brackets { 'BB', '', '', false, false }, -- remove brackets