Merge pull request #8 from arne314/dev: V1.2.0

Anki and snippet improvements
This commit is contained in:
arne314
2025-01-10 16:51:31 +01:00
committed by GitHub
17 changed files with 247 additions and 206 deletions

10
.stylua.toml Normal file
View File

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

View File

@@ -12,13 +12,20 @@ Neovim plugin for efficient note taking in Typst
Use `:TypstarToggleSnippets` to toggle all snippets at any time. Use `:TypstarToggleSnippets` to toggle all snippets at any time.
Available snippets can mostly be intuitively derived from [here](././lua/typstar/snippets), they include: 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` &#8594; `#highlight[<cursor>]`, `IMP` &#8594; `$=>$ `)
- [ctheorems shorthands](./lua/typstar/snippets/markup.lua) (e.g. `tem` &#8594; empty theorem, `exa` &#8594; 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: `:<char>` &#8594; `$<char>$ ` in markup (e.g. `:X` &#8594; `$X$ `, `:5` &#8594; `$5$ `) - Alphanumeric characters: `:<char>` &#8594; `$<char>$ ` in markup (e.g. `:X` &#8594; `$X$ `, `:5` &#8594; `$5$ `)
- Greek letters: `;<latin>` &#8594; `<greek>` in math and `$<greek>$ ` in markup (e.g. `;a` &#8594; `alpha`/`$alpha$ `) - Greek letters: `;<latin>` &#8594; `<greek>` in math and `$<greek>$ ` in markup (e.g. `;a` &#8594; `alpha`/`$alpha$ `)
- Common indices (numbers and letters `i-n`): `<letter><index>` &#8594; `<letter>_<index>` in math and `$<letter>$<index> ` &#8594; `$<letter>_<index>$ ` in markup (e.g `A314` &#8594; `A_314`, `$alpha$n ` &#8594; `$alpha_n$ `) - Common indices (numbers and letters `i-n`): `<letter><index>` &#8594; `<letter>_<index>` in math and `$<letter>$<index> ` &#8594; `$<letter>_<index>$ ` in markup (e.g `A314` &#8594; `A_314`, `$alpha$n ` &#8594; `$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)): `<expression><operation>` &#8594; `<operation>(<expression>)` (e.g. `(a^2+b^2)rt` &#8594; `sqrt(a^2+b^2)`, `lambdatd` &#8594; `tilde(lambda)`, `(1+1)sQ` &#8594; `[1+1]`, `(1+1)sq` &#8594; `[(1+1)]`) - Wrapping of any mathematical expression (see [operations](./lua/typstar/snippets/visual.lua), works nested, multiline and in visual mode via the [selection key](#installation)): `<expression><operation>` &#8594; `<operation>(<expression>)` (e.g. `(a^2+b^2)rt` &#8594; `sqrt(a^2+b^2)`, `lambdatd` &#8594; `tilde(lambda)`, `(1+1)sQ` &#8594; `[1+1]`, `(1+1)sq` &#8594; `[(1+1)]`)
- Matrices: `<size>ma` and `<size>lma` (e.g. `23ma` &#8594; 2x3 matrix) - Matrices: `<size>ma` and `<size>lma` (e.g. `23ma` &#8594; 2x3 matrix)
- [ctheorems shorthands](./lua/typstar/snippets/document.lua) (e.g. `tem` &#8594; empty theorem, `exa` &#8594; 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). Note that you can enable and disable collections of snippets in the [config](#configuration).
@@ -50,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 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). - 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 - 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
@@ -72,9 +80,9 @@ require('typstar').setup({
### Snippets ### 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 = '<Tab>'`) in the configuration 1. Install [LuaSnip](https://github.com/L3MON4D3/LuaSnip/), set `enable_autosnippets = true` and set a visual mode selection key (e.g. `store_selection_keys = '<Tab>'`) 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` 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 ### Excalidraw
1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory 1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory

View File

@@ -4,29 +4,26 @@ local utils = require('typstar.utils')
local cfg = config.config.anki local cfg = config.config.anki
local function run_typstar_anki(args) local function run_typstar_anki(args)
local cwd = vim.fn.getcwd() local cwd = vim.fn.getcwd()
local anki_key = '' local anki_key = ''
if cfg.ankiKey ~= nil then if cfg.ankiKey ~= nil then anki_key = ' --anki-key ' .. cfg.ankiKey end
anki_key = ' --anki-key ' .. cfg.ankiKey
end
local cmd = string.format( local cmd = string.format(
'%s --root-dir %s --typst-cmd %s --anki-url %s %s %s', '%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) utils.run_shell_command(cmd, true)
end end
function M.scan() function M.scan() run_typstar_anki('') end
run_typstar_anki('')
end
function M.scan_force() function M.scan_force() run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) end
run_typstar_anki('--force-scan ' .. vim.fn.getcwd())
end
function M.scan_force_current() function M.scan_force_current() run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p')) end
run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p'))
end
return M return M

View File

@@ -1,16 +1,14 @@
local M = {} local M = {}
local cfg = require('typstar.config').config.snippets local cfg = require('typstar.config').config.snippets
local utils = require('typstar.utils')
local luasnip = require('luasnip') local luasnip = require('luasnip')
local utils = require('typstar.utils')
local fmta = require('luasnip.extras.fmt').fmta local fmta = require('luasnip.extras.fmt').fmta
local lsengines = require('luasnip.nodes.util.trig_engines') local lsengines = require('luasnip.nodes.util.trig_engines')
local ts = vim.treesitter local ts = vim.treesitter
local last_keystroke_time = nil local last_keystroke_time = nil
vim.api.nvim_create_autocmd('TextChangedI', { vim.api.nvim_create_autocmd('TextChangedI', {
callback = function() callback = function() last_keystroke_time = vim.loop.now() end,
last_keystroke_time = vim.loop.now()
end,
}) })
local lexical_result_cache = {} local lexical_result_cache = {}
local ts_markup_query = ts.query.parse('typst', '(text) @markup') local ts_markup_query = ts.query.parse('typst', '(text) @markup')
@@ -31,12 +29,15 @@ function M.cap(i)
return luasnip.function_node(function(_, snip) return snip.captures[i] end) return luasnip.function_node(function(_, snip) return snip.captures[i] end)
end end
function M.get_visual(args, parent) function M.visual(idx, default)
if (#parent.snippet.env.LS_SELECT_RAW > 0) then default = default or ''
return luasnip.snippet_node(nil, luasnip.insert_node(1, parent.snippet.env.LS_SELECT_RAW)) return luasnip.dynamic_node(idx, function(args, parent)
else -- If LS_SELECT_RAW is empty, return a blank insert node if #parent.snippet.env.LS_SELECT_RAW > 0 then
return luasnip.snippet_node(nil, luasnip.insert_node(1)) 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)
end end
function M.ri(insert_node_id) function M.ri(insert_node_id)
@@ -52,34 +53,30 @@ function M.snip(trigger, expand, insert, condition, priority, wordTrig)
trigEngineOpts = { condition = condition }, trigEngineOpts = { condition = condition },
wordTrig = wordTrig, wordTrig = wordTrig,
priority = priority, priority = priority,
snippetType = 'autosnippet' snippetType = 'autosnippet',
}, },
fmta(expand, { unpack(insert) }), fmta(expand, { unpack(insert) }),
{ {
condition = function() return M.snippets_toggle end condition = function() return M.snippets_toggle end,
} }
) )
end end
function M.start_snip(trigger, expand, insert, condition, priority) 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 end
function M.engine(trigger, opts) function M.engine(trigger, opts)
local base_engine = lsengines.ecma(trigger, opts) local base_engine = lsengines.ecma(trigger, opts)
local condition = function() local condition = function()
local cached = lexical_result_cache[opts.condition] local cached = lexical_result_cache[opts.condition]
if cached ~= nil and cached[1] == last_keystroke_time then if cached ~= nil and cached[1] == last_keystroke_time then return cached[2] end
return cached[2]
end
local result = opts.condition() local result = opts.condition()
lexical_result_cache[opts.condition] = { last_keystroke_time, result } lexical_result_cache[opts.condition] = { last_keystroke_time, result }
return result return result
end end
return function(line, trig) return function(line, trig)
if not M.snippets_toggle or not condition() then if not M.snippets_toggle or not condition() then return nil end
return nil
end
return base_engine(line, trig) return base_engine(line, trig)
end end
end end
@@ -93,12 +90,16 @@ function M.setup()
if cfg.enable then if cfg.enable then
local autosnippets = {} local autosnippets = {}
for _, file in ipairs(cfg.modules) do for _, file in ipairs(cfg.modules) do
vim.list_extend( vim.list_extend(autosnippets, require(('typstar.snippets.%s'):format(file)))
autosnippets,
require(('typstar.snippets.%s'):format(file))
)
end end
luasnip.add_snippets('typst', autosnippets) 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
end end

View File

@@ -19,19 +19,19 @@ local default_config = {
snippets = { snippets = {
enable = true, enable = true,
modules = { -- enable modules from ./snippets modules = { -- enable modules from ./snippets
'document',
'letters', 'letters',
'math', 'math',
'matrix', 'matrix',
'markup',
'visual', 'visual',
} },
}, },
} }
function M.merge_config(args) function M.merge_config(args)
M.config = vim.tbl_deep_extend('force', default_config, args or {}) 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', ['%.excalidraw%.md$'] = M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md',
} }
end end

View File

@@ -12,7 +12,8 @@ local affix = [[
local function launch_obsidian(path) local function launch_obsidian(path)
print(string.format('Opening %s in Excalidraw', path)) print(string.format('Opening %s in Excalidraw', path))
utils.run_shell_command( 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 end
@@ -22,9 +23,7 @@ function M.insert_drawing()
local path = assets_dir .. '/' .. filename .. cfg.fileExtension local path = assets_dir .. '/' .. filename .. cfg.fileExtension
local path_inserted = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted local path_inserted = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted
if vim.fn.isdirectory(assets_dir) == 0 then if vim.fn.isdirectory(assets_dir) == 0 then vim.fn.mkdir(assets_dir, 'p') end
vim.fn.mkdir(assets_dir, 'p')
end
local found_match = false local found_match = false
for pattern, template_path in pairs(cfg.templatePath) do for pattern, template_path in pairs(cfg.templatePath) do
if string.match(path, pattern) then if string.match(path, pattern) then
@@ -44,9 +43,10 @@ end
function M.open_drawing() function M.open_drawing()
local line = vim.api.nvim_get_current_line() local line = vim.api.nvim_get_current_line()
local path = vim.fn.expand('%:p:h') .. local path = vim.fn.expand('%:p:h')
'/' .. string.match(line, '"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.')) .. .. '/'
'.excalidraw.md' .. string.match(line, '"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.'))
.. '.excalidraw.md'
launch_obsidian(path) launch_obsidian(path)
end end

View File

@@ -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),
}

View File

@@ -8,7 +8,6 @@ local cap = helper.cap
local math = helper.in_math local math = helper.in_math
local markup = helper.in_markup local markup = helper.in_markup
local letter_snippets = {} local letter_snippets = {}
local greek_letters_map = { local greek_letters_map = {
['a'] = 'alpha', ['a'] = 'alpha',
@@ -25,33 +24,36 @@ local greek_letters_map = {
['m'] = 'mu', ['m'] = 'mu',
['n'] = 'nu', ['n'] = 'nu',
['o'] = 'omega', ['o'] = 'omega',
['p'] = 'pi', ['p'] = 'psi',
['q'] = 'eta', ['q'] = 'eta',
['r'] = 'rho', ['r'] = 'rho',
['s'] = 'sigma', ['s'] = 'sigma',
['t'] = 'tau', ['t'] = 'tau',
['v'] = 'nu',
['w'] = 'omega',
['x'] = 'xi', ['x'] = 'xi',
['y'] = 'upsilon',
['z'] = 'zeta', ['z'] = 'zeta',
} }
local greek_letters = {}
local greek_keys = {} local greek_keys = {}
local greek_letters_set = {}
local common_indices = { '\\d+', '[i-n]' } 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 index_conflicts_set = {}
local trigger_greek = '' local trigger_greek = ''
local trigger_index_pre = '' local trigger_index_pre = ''
local trigger_index_post = '' local trigger_index_post = ''
local upper_first = function(str) local upper_first = function(str) return str:sub(1, 1):upper() .. str:sub(2, -1) end
return str:sub(1, 1):upper() .. str:sub(2, -1)
end
local greek_full = {} local greek_full = {}
for latin, greek in pairs(greek_letters_map) do for latin, greek in pairs(greek_letters_map) do
greek_full[latin] = greek greek_full[latin] = greek
greek_full[latin:upper()] = upper_first(greek) greek_full[latin:upper()] = upper_first(greek)
table.insert(greek_letters, greek) if not greek_letters_set[greek] then
table.insert(greek_letters, upper_first(greek)) 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)
table.insert(greek_keys, latin:upper()) table.insert(greek_keys, latin:upper())
end end
@@ -62,31 +64,36 @@ end
greek_letters_map = greek_full greek_letters_map = greek_full
trigger_greek = table.concat(greek_keys, '|') 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, '|') trigger_index_post = table.concat(common_indices, '|')
local get_greek = function(_, snippet) local get_greek = function(_, snippet) return s(nil, t(greek_letters_map[snippet.captures[1]])) end
return s(nil, t(greek_letters_map[snippet.captures[1]]))
end
local get_index = function(_, snippet) local get_index = function(_, snippet)
local letter, index = snippet.captures[1], snippet.captures[2] local letter, index = snippet.captures[1], snippet.captures[2]
local trigger = letter .. index local trigger = letter .. index
if index_conflicts_set[trigger] then if index_conflicts_set[trigger] then return s(nil, t(trigger)) end
return s(nil, t(trigger))
end
return s(nil, t(letter .. '_' .. index)) return s(nil, t(letter .. '_' .. index))
end end
table.insert(letter_snippets, snip(':([A-Za-z0-9])', '$<>$ ', { cap(1) }, markup)) 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) }, markup))
table.insert(letter_snippets, snip(';(' .. trigger_greek .. ')', '<>', { d(1, get_greek) }, math)) table.insert(letter_snippets, snip(';(' .. trigger_greek .. ')', '<>', { d(1, get_greek) }, math))
table.insert(letter_snippets, table.insert(
snip('\\$(' .. trigger_index_pre .. ')\\$' .. '(' .. trigger_index_post .. ') ', letter_snippets,
'$<>$ ', { d(1, get_index) }, markup, 500)) snip(
table.insert(letter_snippets, '\\$(' .. trigger_index_pre .. ')\\$' .. '(' .. trigger_index_post .. ') ',
snip('(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ') ', '<> ', { d(1, get_index) }, math, 200)) '$<>$ ',
{ d(1, get_index) },
markup,
500
)
)
table.insert(
letter_snippets,
snip('(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ') ', '<> ', { d(1, get_index) }, math, 200)
)
return { return {
unpack(letter_snippets) unpack(letter_snippets),
} }

View File

@@ -0,0 +1,51 @@
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),
}

View File

@@ -22,6 +22,7 @@ return {
snip('no', 'not ', {}, math), snip('no', 'not ', {}, math),
snip('ip', '==>> ', {}, math), snip('ip', '==>> ', {}, math),
snip('ib', '<<== ', {}, math), snip('ib', '<<== ', {}, math),
snip('iff', '<<=>> ', {}, math),
-- relations -- relations
snip('el', '= ', {}, math), snip('el', '= ', {}, math),
@@ -33,8 +34,8 @@ return {
snip('ge', '>>= ', {}, math), snip('ge', '>>= ', {}, math),
-- operators -- operators
snip('ak([^k])', '+<>', { cap(1) }, math, 500, false), snip('ak([^k])', '+<>', { cap(1) }, math, 100, false),
snip('sk([^k])', '-<>', { cap(1) }, math, 500, false), snip('sk([^k])', '-<>', { cap(1) }, math, 100, false),
snip('oak', 'plus.circle ', {}, math, 1100), snip('oak', 'plus.circle ', {}, math, 1100),
snip('bak', 'plus.square ', {}, math, 1100), snip('bak', 'plus.square ', {}, math, 1100),
snip('mak', 'plus.minus ', {}, math, 1100), snip('mak', 'plus.minus ', {}, math, 1100),
@@ -63,10 +64,11 @@ return {
snip('Oo', 'compose ', {}, math), snip('Oo', 'compose ', {}, math),
snip('iso', 'tilde.equiv ', {}, math), snip('iso', 'tilde.equiv ', {}, math),
snip('ep', 'exp(<>) ', { i(1, '1') }, math), snip('ep', 'exp(<>) ', { i(1, '1') }, math),
snip('rrn', 'RR^n ', {}, math),
snip('cc', 'cases(\n\t<>\n)\\', { i(1, '1') }, 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('(.*)iv', '<>^(-1)', { cap(1) }, math),
snip('(.*)sr', '<>^2', { cap(1) }, math), snip('(.*)sr', '<>^2', { cap(1) }, math),
snip('(.*)cb', '<>^3', { cap(1) }, math),
snip('(.*)jj', '<>_(<>)', { cap(1), i(1, 'n') }, math), snip('(.*)jj', '<>_(<>)', { cap(1), i(1, 'n') }, math),
snip('(.*)kk', '<>^(<>)', { cap(1), i(1, 'n') }, math), snip('(.*)kk', '<>^(<>)', { cap(1), i(1, 'n') }, math),

View File

@@ -61,9 +61,7 @@ local lmat = function(_, sp)
ins_indx = ins_indx + 1 ins_indx = ins_indx + 1
for k = 2, cols do for k = 2, cols do
table.insert(nodes, t(', ')) table.insert(nodes, t(', '))
if k == cols then if k == cols then table.insert(nodes, t('dots, ')) end
table.insert(nodes, t('dots, '))
end
if j == k then if j == k then
table.insert(nodes, r(ins_indx, tostring(j) .. 'x' .. tostring(k), i(1, '1'))) table.insert(nodes, r(ins_indx, tostring(j) .. 'x' .. tostring(k), i(1, '1')))
else else

View File

@@ -5,12 +5,13 @@ local i = ls.insert_node
local s = ls.snippet_node local s = ls.snippet_node
local t = ls.text_node local t = ls.text_node
local utils = require('typstar.utils')
local helper = require('typstar.autosnippets') local helper = require('typstar.autosnippets')
local utils = require('typstar.utils')
local math = helper.in_math local math = helper.in_math
local snip = helper.snip local snip = helper.snip
local snippets = {} local snippets = {}
local operations = { -- first boolean: existing brackets should be kept; second boolean: brackets should be added local operations = { -- first boolean: existing brackets should be kept; second boolean: brackets should be added
{ 'vi', '1/', '', true, false }, { 'vi', '1/', '', true, false },
{ 'bb', '(', ')', true, false }, -- add round brackets { 'bb', '(', ')', true, false }, -- add round brackets
@@ -47,16 +48,11 @@ local process_ts_query = function(bufnr, cursor, query, root, insert1, insert2,
if end_row == cursor[1] and end_col == cursor[2] then if end_row == cursor[1] and end_col == cursor[2] then
vim.schedule(function() -- to not interfere with luasnip vim.schedule(function() -- to not interfere with luasnip
local cursor_offset = 0 local cursor_offset = 0
local old_len1, new_len1 = utils.insert_text( local old_len1, new_len1 = utils.insert_text(bufnr, start_row, start_col, insert1, 0, cut_offset)
bufnr, start_row, start_col, insert1, 0, cut_offset) if start_row == cursor[1] then cursor_offset = cursor_offset + (new_len1 - old_len1) end
if start_row == cursor[1] then local old_len2, new_len2 =
cursor_offset = cursor_offset + (new_len1 - old_len1) utils.insert_text(bufnr, end_row, cursor[2] + cursor_offset, insert2, cut_offset, 0)
end if end_row == cursor[1] then cursor_offset = cursor_offset + (new_len2 - old_len2) 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 }) vim.api.nvim_win_set_cursor(0, { cursor[1] + 1, cursor[2] + cursor_offset })
end) end)
return true return true
@@ -77,9 +73,7 @@ local smart_wrap = function(args, parent, old_state, expand)
local expand1 = expand[5] and expand[2] .. '(' or expand[2] local expand1 = expand[5] and expand[2] .. '(' or expand[2]
local expand2 = expand[5] and expand[3] .. ')' or expand[3] local expand2 = expand[5] and expand[3] .. ')' or expand[3]
if process_ts_query(bufnr, cursor, ts_wrap_query, root, expand1, expand2) then if process_ts_query(bufnr, cursor, ts_wrap_query, root, expand1, expand2) then return s(nil, t()) end
return s(nil, t())
end
if #parent.env.LS_SELECT_RAW > 0 then if #parent.env.LS_SELECT_RAW > 0 then
return s(nil, t(expand1 .. table.concat(parent.env.LS_SELECT_RAW) .. expand2)) return s(nil, t(expand1 .. table.concat(parent.env.LS_SELECT_RAW) .. expand2))
end end
@@ -91,5 +85,5 @@ for _, val in pairs(operations) do
end end
return { return {
unpack(snippets) unpack(snippets),
} }

View File

@@ -20,7 +20,7 @@ end
function M.insert_text_block(snip) function M.insert_text_block(snip)
local line_num = M.get_cursor_pos()[1] + 1 local line_num = M.get_cursor_pos()[1] + 1
local lines = {} local lines = {}
for line in snip:gmatch '[^\r\n]+' do for line in snip:gmatch('[^\r\n]+') do
table.insert(lines, line) table.insert(lines, line)
end end
vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), line_num, line_num, false, lines) vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), line_num, line_num, false, lines)
@@ -35,41 +35,28 @@ function M.run_shell_command(cmd, show_output)
end end
end end
if show_output then if show_output then
vim.fn.jobstart( vim.fn.jobstart(cmd, {
cmd, on_stdout = function(_, data, _) handle_output(data, false) end,
{ on_stderr = function(_, data, _) handle_output(data, true) end,
on_stdout = function(_, data, _)
handle_output(data, false)
end,
on_stderr = function(_, data, _)
handle_output(data, true)
end,
stdout_buffered = false, stdout_buffered = false,
stderr_buffered = true, stderr_buffered = true,
} })
)
else else
vim.fn.jobstart(cmd) vim.fn.jobstart(cmd)
end end
end end
function M.char_to_hex(c) function M.char_to_hex(c) return string.format('%%%02X', string.byte(c)) end
return string.format("%%%02X", string.byte(c))
end
function M.urlencode(url) function M.urlencode(url)
if url == nil then if url == nil then return '' end
return ''
end
url = string.gsub(url, '\n', '\r\n') url = string.gsub(url, '\n', '\r\n')
url = string.gsub(url, '([^%w _%%%-%.~])', M.char_to_hex) url = string.gsub(url, '([^%w _%%%-%.~])', M.char_to_hex)
url = string.gsub(url, ' ', '%%20') url = string.gsub(url, ' ', '%%20')
return url return url
end end
function M.get_treesitter_root(bufnr) function M.get_treesitter_root(bufnr) return ts.get_parser(bufnr):parse()[1]:root() end
return ts.get_parser(bufnr):parse()[1]:root()
end
function M.treesitter_match_start_end(match) function M.treesitter_match_start_end(match)
local start_row, start_col, _, _ = match[1]:range() local start_row, start_col, _, _ = match[1]:range()
@@ -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 for _, match, _ in query:iter_matches(M.get_treesitter_root(bufnr), bufnr, cursor[1], cursor[1] + 1) do
if match then if match then
local start_row, start_col, end_row, end_col = M.treesitter_match_start_end(match) 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, local matched = M.cursor_within_coords(cursor, start_row, end_row, start_col, end_col, match_tolerance)
match_tolerance) if matched then return true end
if matched then
return true
end
end end
end end
return false return false

View File

@@ -4,7 +4,7 @@ build-backend = "pdm.backend"
[project] [project]
name = "typstar" name = "typstar"
version = "1.1.1" version = "1.2.0"
description = "Neovim plugin for efficient note taking in Typst" description = "Neovim plugin for efficient note taking in Typst"
authors = [ authors = [
{ name = "arne314" } { name = "arne314" }

35
src/anki/config_parser.py Normal file
View File

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

View File

@@ -1,7 +1,6 @@
import glob
import json import json
import re import re
from functools import cache from glob import glob
from pathlib import Path from pathlib import Path
from typing import List, Tuple from typing import List, Tuple
@@ -9,6 +8,7 @@ import appdirs
import tree_sitter import tree_sitter
from tree_sitter_typst import language as get_typst_language from tree_sitter_typst import language as get_typst_language
from .config_parser import RecursiveConfigParser
from .file_handler import FileHandler from .file_handler import FileHandler
from .flashcard import Flashcard from .flashcard import Flashcard
@@ -38,6 +38,8 @@ ts_deck_query = """
deck_regex = re.compile(r"\W+ANKI:\s*([\S ]*)") deck_regex = re.compile(r"\W+ANKI:\s*([\S ]*)")
class FlashcardParser: class FlashcardParser:
typst_language: tree_sitter.Language typst_language: tree_sitter.Language
typst_parser: tree_sitter.Parser typst_parser: tree_sitter.Parser
@@ -56,7 +58,7 @@ class FlashcardParser:
self.file_handlers = [] self.file_handlers = []
self._load_file_hashes() 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 = [] cards = []
tree = self.typst_parser.parse(file.get_bytes(), encoding="utf8") tree = self.typst_parser.parse(file.get_bytes(), encoding="utf8")
card_captures = self.flashcard_query.captures(tree.root_node) card_captures = self.flashcard_query.captures(tree.root_node)
@@ -73,7 +75,7 @@ class FlashcardParser:
deck_refs: List[Tuple[int, str | None]] = [] deck_refs: List[Tuple[int, str | None]] = []
deck_refs_idx = -1 deck_refs_idx = -1
current_deck = None current_deck = default_deck
if deck_captures: if deck_captures:
deck_captures["deck"].sort(key=row_compare) deck_captures["deck"].sort(key=row_compare)
for comment in deck_captures["deck"]: for comment in deck_captures["deck"]:
@@ -108,6 +110,7 @@ class FlashcardParser:
return cards return cards
def parse_directory(self, root_dir: Path, force_scan: Path | None = None): def parse_directory(self, root_dir: Path, force_scan: Path | None = None):
flashcards = []
single_file = None single_file = None
is_force_scan = force_scan is not None is_force_scan = force_scan is not None
if is_force_scan: if is_force_scan:
@@ -123,22 +126,9 @@ class FlashcardParser:
f"Parsing flashcards in {scan_dir if single_file is None else single_file} ...", f"Parsing flashcards in {scan_dir if single_file is None else single_file} ...",
flush=True, flush=True,
) )
preambles = {} configs = RecursiveConfigParser(root_dir, {".anki", ".anki.typ"})
flashcards = []
@cache for file in glob(f"{scan_dir}/**/**.typ", recursive=True):
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):
file = Path(file) file = Path(file)
if single_file is not None and file != single_file: if single_file is not None and file != single_file:
continue continue
@@ -146,7 +136,7 @@ class FlashcardParser:
fh = FileHandler(file) fh = FileHandler(file)
file_changed = self._hash_changed(fh) file_changed = self._hash_changed(fh)
if is_force_scan or file_changed: 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)) self.file_handlers.append((fh, cards))
flashcards.extend(cards) flashcards.extend(cards)
return flashcards return flashcards

2
uv.lock generated
View File

@@ -437,7 +437,7 @@ wheels = [
[[package]] [[package]]
name = "typstar" name = "typstar"
version = "1.1.0" version = "1.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },