Merge pull request #11 from arne314/dev

V1.3.0
This commit is contained in:
arne314
2025-02-07 12:09:33 +01:00
committed by GitHub
15 changed files with 453 additions and 88 deletions

View File

@@ -14,7 +14,7 @@ Available snippets can mostly be intuitively derived from [here](././lua/typstar
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; `$=>$ `)
- [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)
@@ -24,10 +24,11 @@ Math snippets:
- 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$ `)
- 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$ `)
- Series of numbered letters: `<letter> ot<optional last index> ` &#8594; `<letter>_1, <letter>_2, ... ` (e.g. `a ot ` &#8594; `a_1, a_2, ... `, `a ot4 ` &#8594; `a_1, a_2, a_3, a_4 `, `alpha otk ` &#8594; `alpha_1, alpha_2, ..., alpha_k `)
- 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)
Note that you can enable and disable collections of snippets in the [config](#configuration).
Note that you can [customize](#custom-snippets) (enable, disable and modify) every snippet.
### Excalidraw
- Use `:TypstarInsertExcalidraw` to create a new drawing using the configured template, insert a figure displaying it and open it in Obsidian.
@@ -65,16 +66,19 @@ To render the flashcard in your document as well add some code like this
- Use `:TypstarAnkiScan` to scan the current nvim working directory and compile all flashcards in its context, unchanged files will be ignored
- Use `:TypstarAnkiForce` to force compilation of all flashcards in the current working directory even if the files haven't changed since the last scan (e.g. on preamble change)
- Use `:TypstarAnkiForceCurrent` to force compilation of all flashcards in the file currently edited
- Use `:TypstarAnkiReimport` to also add flashcards that have already been asigned an id but are not currently
present in Anki
- Use `:TypstarAnkiForceReimport` and `:TypstarAnkiForceCurrentReimport` to combine features accordingly
#### Standalone
- Run `typstar-anki --help` to show the available options
## Installation
Install the plugin in Neovim and set the `typstarRoot` config or alternatively clone typstar into `~/typstar`.
Install the plugin in Neovim (see [Nix instructions](#in-a-nix-flake-optional)) and run the plugin setup.
```lua
require('typstar').setup({
typstarRoot = '/path/to/typstar/repo' -- depending on your nvim plugin system
require('typstar').setup({ -- depending on your neovim plugin system
-- your typstar config goes here
})
```
@@ -88,6 +92,7 @@ require('typstar').setup({
1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory
2. Install the [obsidian-excalidraw-plugin](https://github.com/zsviczian/obsidian-excalidraw-plugin) and enable `Auto-export SVG` (in plugin settings at `Embedding Excalidraw into your Notes and Exporting > Export Settings > Auto-export Settings`)
3. Have the `xdg-open` command working or set a different command at `uriOpenCommand` in the [config](#configuration)
4. If you encounter issues try cloning the repo into `~/typstar` or setting the `typstarRoot` config accordingly, feel free to open an issue
### Anki
0. Typst version `0.12.0` or higher is required
@@ -96,6 +101,70 @@ require('typstar').setup({
3. Install the typstar python package (I recommend using [pipx](https://github.com/pypa/pipx) via `pipx install git+https://github.com/arne314/typstar`, you will need to have python build tools and clang installed) \[Note: this may take a while\]
4. Make sure the `typstar-anki` command is available in your `PATH` or modify the `typstarAnkiCmd` option in the [config](#configuration)
### In a Nix Flake (optional)
You can add typstar to your `nix-flake` like so
```nix
# `flake.nix`
inputs = {
# ... other inputs
typstar = {
url = "github:arne314/typstar";
flake = false;
};
}
```
Now you can use `typstar` in any package-set
```nix
with pkgs; [
# ... other packges
(pkgs.vimUtils.buildVimPlugin {
name = "typstar";
src = inputs.typstar;
buildInputs = [
vimPlugins.luasnip
vimPlugins.nvim-treesitter-parsers.typst
];
})
]
```
## Configuration
Configuration options can be intuitively derived from the table [here](./lua/typstar/config.lua).
### Custom snippets
The [config](#configuration) allows you to
- disable all snippets via `snippets.enable = false`
- only include specific modules from the snippets folder via e.g. `snippets.modules = { 'letters' }`
- exclude specific triggers via e.g. `snippets.exclude = { 'dx', 'ddx' }`
For further customization you can make use of the provided wrappers from within your [LuaSnip](https://github.com/L3MON4D3/LuaSnip/) config.
Let's say you prefer the short `=>` arrow over the long `==>` one and would like to change the `ip` trigger to `imp`.
Your `typstar` config could look like
```lua
require('typstar').setup({
snippets = {
exclude = { 'ip' },
},
})
```
while your LuaSnip `typst.lua` could look like this (`<` and `>` require escaping as `<>` [introduces a new node](https://github.com/L3MON4D3/LuaSnip/blob/master/DOC.md#fmt))
```lua
local tp = require('typstar.autosnippets')
local snip = tp.snip
local math = tp.in_math
local markup = tp.in_markup
return {
-- add a new snippet (the old one is excluded via the config)
snip('imp', '=>> ', {}, math),
-- override existing triggers by setting a high priority
snip('ib', '<<= ', {}, math, 2000),
snip('iff', '<<=>> ', {}, math, 2000),
-- setup markup snippets accordingly
snip('IMP', '$=>>$ ', {}, markup, 2000),
snip('IFF', '$<<=>>$ ', {}, markup, 2000),
}
```

58
flake.lock generated Normal file
View File

@@ -0,0 +1,58 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1736143030,
"narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1737525964,
"narHash": "sha256-3wFonKmNRWKq1himW9N3TllbeGIHFACI5vmLpk6moF8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5757bbb8bd7c0630a0cc4bb19c47e588db30b97c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1735774519,
"narHash": "sha256-CewEm1o2eVAnoqb6Ml+Qi9Gg/EfNAxbRx1lANGVyoLI=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

64
flake.nix Normal file
View File

@@ -0,0 +1,64 @@
{
description = "typstar nix flake for development";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs @ {
self,
nixpkgs,
flake-parts,
...
}:
flake-parts.lib.mkFlake {inherit inputs;} {
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem = {system, ...}: let
pkgs = import nixpkgs {inherit system;};
typstar = pkgs.vimUtils.buildVimPlugin {
name = "typstar";
src = self;
buildInputs = [
pkgs.vimPlugins.luasnip
pkgs.vimPlugins.nvim-treesitter-parsers.typst
];
};
in {
packages = {
default = typstar;
nvim = let
config = pkgs.neovimUtils.makeNeovimConfig {
customRC = ''
lua << EOF
print("Welcome to Typstar! This is just a demo.")
require('nvim-treesitter.configs').setup {
highlight = { enable = true },
}
require('luasnip').config.set_config({
enable_autosnippets = true,
})
require('typstar').setup()
EOF
'';
plugins = [
typstar
pkgs.vimPlugins.luasnip
pkgs.vimPlugins.nvim-treesitter
pkgs.vimPlugins.nvim-treesitter-parsers.typst
];
};
in
pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped config;
};
};
};
}

View File

@@ -22,8 +22,14 @@ end
function M.scan() run_typstar_anki('') end
function M.scan_reimport() run_typstar_anki('--reimport') end
function M.scan_force() run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) end
function M.scan_force_reimport() run_typstar_anki('--reimport --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_reimport() run_typstar_anki('--reimport --force-scan ' .. vim.fn.expand('%:p')) end
return M

View File

@@ -6,15 +6,18 @@ local fmta = require('luasnip.extras.fmt').fmta
local lsengines = require('luasnip.nodes.util.trig_engines')
local ts = vim.treesitter
local exclude_triggers_set = {}
local last_keystroke_time = nil
vim.api.nvim_create_autocmd('TextChangedI', {
callback = function() last_keystroke_time = vim.loop.now() end,
})
local lexical_result_cache = {}
local ts_markup_query = ts.query.parse('typst', '(text) @markup')
local ts_math_query = ts.query.parse('typst', '(math) @math')
local ts_string_query = ts.query.parse('typst', '(string) @string')
utils.generate_bool_set(cfg.exclude, exclude_triggers_set)
vim.api.nvim_create_autocmd('TextChangedI', {
callback = function() last_keystroke_time = vim.loop.now() end,
})
M.in_math = function()
local cursor = utils.get_cursor_pos()
return utils.cursor_within_treesitter_query(ts_math_query, 0, cursor)
@@ -44,14 +47,15 @@ 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)
function M.snip(trigger, expand, insert, condition, priority, wordTrig, maxTrigLength)
priority = priority or 1000
if wordTrig == nil then wordTrig = true end
return luasnip.snippet(
{
trig = trigger,
trigEngine = M.engine,
trigEngineOpts = { condition = condition },
wordTrig = wordTrig,
trigEngineOpts = { condition = condition, wordTrig = wordTrig, maxTrigLength = maxTrigLength },
wordTrig = false,
priority = priority,
snippetType = 'autosnippet',
},
@@ -66,8 +70,43 @@ function M.start_snip(trigger, expand, insert, condition, priority)
return M.snip('^(\\s*)' .. trigger, '<>' .. expand, { M.cap(1), unpack(insert) }, condition, priority)
end
local alts_regex = '[\\[\\(](.*|.*)[\\)\\]]'
function M.engine(trigger, opts)
local base_engine = lsengines.ecma(trigger, opts)
-- determine possibly max/fixed length of trigger
local max_length = opts.maxTrigLength
local is_fixed_length = false
if alts_regex ~= '' and not trigger:match('[%+%*]') then
max_length = #trigger
- utils.count_string(trigger, '\\')
- utils.count_string(trigger, '%(')
- utils.count_string(trigger, '%)')
- utils.count_string(trigger, '%?')
is_fixed_length = not trigger:match('[%+%*%?%[%]|]')
local alts_match = alts_regex:match(trigger) -- find longest trigger in [...|...]
if alts_match then
for _, alts in ipairs(alts_match) do
local max_alt_length = 1
for alt in alts:gmatch('([^|]+)') do
local len
if alt:match('%[.*-.*%]') then -- [A-Za-z0-9] and similar
len = 2
else
len = #alt
end
max_alt_length = math.max(max_alt_length, len)
end
max_length = max_length - (#alts - max_alt_length)
end
else -- [^...] and similar
max_length = max_length - utils.count_string(trigger, '%[') - utils.count_string(trigger, '%]')
end
end
-- cache preanalysis results
local condition = function()
local cached = lexical_result_cache[opts.condition]
if cached ~= nil and cached[1] == last_keystroke_time then return cached[2] end
@@ -75,9 +114,33 @@ function M.engine(trigger, opts)
lexical_result_cache[opts.condition] = { last_keystroke_time, result }
return result
end
-- matching
return function(line, trig)
if not M.snippets_toggle or not condition() then return nil end
return base_engine(line, trig)
if max_length ~= nil then
local first_idx = #line - max_length -- include additional char for wordtrig
if first_idx < 0 then
if is_fixed_length then
return nil
else
first_idx = 1
end
end
if first_idx > 0 then
if string.byte(line, first_idx) > 127 then return nil end
end
line = line:sub(first_idx)
end
local whole, captures = base_engine(line, trig)
if whole == nil then return nil end
-- custom word trig
local from = #line - #whole + 1
if opts.wordTrig and from ~= 1 and string.match(string.sub(line, from - 1, from - 1), '[%w.]') ~= nil then
return nil
end
return whole, captures
end
end
@@ -88,18 +151,30 @@ end
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)))
end
luasnip.add_snippets('typst', autosnippets)
local jsregexp_ok, _ = pcall(require, 'luasnip-jsregexp')
local jsregexp_ok, jsregexp = pcall(require, 'luasnip-jsregexp')
if not jsregexp_ok then
jsregexp_ok, _ = pcall(require, 'jsregexp')
jsregexp_ok, jsregexp = pcall(require, 'jsregexp')
end
if not jsregexp_ok then
if jsregexp_ok then
alts_regex = jsregexp.compile_safe(alts_regex)
else
alts_regex = ''
vim.notify("WARNING: Most snippets won't work as jsregexp is not installed", vim.log.levels.WARN)
end
local autosnippets = {}
for _, file in ipairs(cfg.modules) do
for _, sn in ipairs(require(('typstar.snippets.%s'):format(file))) do
local exclude
local is_start = sn.trigger:match('^%^%(\\s%*%)')
if is_start then
exclude = exclude_triggers_set[sn.trigger:sub(7)]
else
exclude = exclude_triggers_set[sn.trigger]
end
if not exclude then table.insert(autosnippets, sn) end
end
end
luasnip.add_snippets('typst', autosnippets)
end
end

View File

@@ -1,7 +1,7 @@
local M = {}
local default_config = {
typstarRoot = '~/typstar',
typstarRoot = nil,
anki = {
typstarAnkiCmd = 'typstar-anki',
typstCmd = 'typst',
@@ -25,11 +25,15 @@ local default_config = {
'markup',
'visual',
},
exclude = {}, -- list of triggers to exclude
},
}
function M.merge_config(args)
M.config = vim.tbl_deep_extend('force', default_config, args or {})
M.config.typstarRoot = M.config.typstarRoot
or debug.getinfo(1).source:match('^@(.*)/lua/typstar/config%.lua$')
or '~/typstar'
M.config.excalidraw.templatePath = M.config.excalidraw.templatePath
or {
['%.excalidraw%.md$'] = M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md',

View File

@@ -14,8 +14,11 @@ M.setup = function(args)
vim.api.nvim_create_user_command('TypstarOpenExcalidraw', excalidraw.open_drawing, {})
vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {})
vim.api.nvim_create_user_command('TypstarAnkiReimport', anki.scan_reimport, {})
vim.api.nvim_create_user_command('TypstarAnkiForce', anki.scan_force, {})
vim.api.nvim_create_user_command('TypstarAnkiForceReimport', anki.scan_force_reimport, {})
vim.api.nvim_create_user_command('TypstarAnkiForceCurrent', anki.scan_force_current, {})
vim.api.nvim_create_user_command('TypstarAnkiForceCurrentReimport', anki.scan_force_current_reimport, {})
autosnippets.setup()
end

View File

@@ -1,23 +1,25 @@
local ls = require('luasnip')
local d = ls.dynamic_node
local i = ls.insert_node
local s = ls.snippet_node
local t = ls.text_node
local helper = require('typstar.autosnippets')
local utils = require('typstar.utils')
local snip = helper.snip
local cap = helper.cap
local math = helper.in_math
local markup = helper.in_markup
local letter_snippets = {}
local greek_letters_map = {
['a'] = 'alpha',
['b'] = 'beta',
['c'] = 'chi',
['d'] = 'delta',
['e'] = 'epsilon',
['f'] = 'phi',
['g'] = 'gamma',
['h'] = 'phi',
['i'] = 'iotta',
['h'] = 'eta',
['i'] = 'iota',
['j'] = 'theta',
['k'] = 'kappa',
['l'] = 'lambda',
@@ -38,8 +40,11 @@ local greek_letters_map = {
local greek_keys = {}
local greek_letters_set = {}
local common_indices = { '\\d+', '[i-n]' }
local index_conflicts = { 'in', 'ln', 'pi', 'xi' }
-- buitins and caligraphic letters from github.com/lentilus/readable-typst
local index_conflicts = { 'in', 'ln', 'pi', 'xi', 'Ii', 'Jj', 'Kk', 'Ll', 'Mm', 'Nn' }
local index_conflicts_set = {}
local punctuation_prepend_space = { ',', ';' }
local punctuation_prepend_space_set = {}
local trigger_greek = ''
local trigger_index_pre = ''
local trigger_index_post = ''
@@ -58,9 +63,8 @@ for latin, greek in pairs(greek_letters_map) do
table.insert(greek_keys, latin:upper())
end
for _, conflict in ipairs(index_conflicts) do
index_conflicts_set[conflict] = true
end
utils.generate_bool_set(index_conflicts, index_conflicts_set)
utils.generate_bool_set(punctuation_prepend_space, punctuation_prepend_space_set)
greek_letters_map = greek_full
trigger_greek = table.concat(greek_keys, '|')
@@ -69,31 +73,66 @@ 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_index = function(_, snippet)
local letter, index = snippet.captures[1], snippet.captures[2]
local get_index = function(_, snippet, _, idx1, idx2, check_conflict)
local letter, index = snippet.captures[idx1], snippet.captures[idx2]
local trigger = letter .. index
if index_conflicts_set[trigger] then return s(nil, t(trigger)) end
if check_conflict and 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)
)
local get_series = function(_, snippet)
local letter, target = snippet.captures[1], snippet.captures[2]
local target_num = tonumber(target)
local result
if target_num then
local res = {}
for n = 1, target_num do
table.insert(res, string.format('%s_%d', letter, n))
if n ~= target_num then table.insert(res, ', ') end
end
result = table.concat(res, '')
else
result = string.format('%s_1, %s_2, ..., %s_%s', letter, letter, letter, target)
end
return s(nil, t(result))
end
local prepend_space = function(_, snippet, _, idx)
local punc = snippet.captures[idx]
if punctuation_prepend_space_set[punc] then punc = punc .. ' ' end
return s(nil, t(punc))
end
return {
unpack(letter_snippets),
-- latin/greek
snip(':([A-Za-z0-9])', '$<>$ ', { cap(1) }, markup),
snip(';(' .. trigger_greek .. ')', '$<>$ ', { d(1, get_greek) }, markup),
snip(';(' .. trigger_greek .. ')', '<>', { d(1, get_greek) }, math),
-- indices
snip(
'\\$(' .. trigger_index_pre .. ')\\$' .. ' (' .. trigger_index_post .. ')([^\\w])',
'$<>$<>',
{ d(1, get_index, {}, { user_args = { 1, 2, false } }), d(2, prepend_space, {}, { user_args = { 3 } }) },
markup,
500,
true,
13
),
snip(
'(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ')([^\\w])',
'<><>',
{ d(1, get_index, {}, { user_args = { 1, 2, true } }), d(2, prepend_space, {}, { user_args = { 3 } }) },
math,
200,
true,
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
-- misc
snip('(' .. trigger_index_pre .. ')bl', 'B_<> (<>)', { cap(1), i(1, 'x_0') }, math),
}

View File

@@ -36,26 +36,35 @@ return {
-- operators
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),
snip('xx', 'times ', {}, math),
snip('oak', 'plus.circle ', {}, math),
snip('bak', 'plus.square ', {}, math),
snip('mak', 'plus.minus ', {}, math),
snip('xx', 'times ', {}, math, 900),
snip('oxx', 'times.circle ', {}, math),
snip('bxx', 'times.square ', {}, math),
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('ep', 'exp(<>) ', { i(1, '1') }, math),
-- sets
-- 'st' to '{<>} in ./visual.lua
snip('set', '{<> | <>}', { i(1), i(2) }, math),
snip('es', 'emptyset ', {}, math),
snip('es', 'emptyset ', {}, math, 900),
snip('ses', '{emptyset} ', {}, math),
snip('sp', 'supset ', {}, math),
snip('sb', 'subset ', {}, math),
snip('sep', 'supset.eq ', {}, math),
snip('seb', 'subset.eq ', {}, math),
snip('nn', 'sect ', {}, math),
snip('uu', 'union ', {}, math),
snip('bnn', 'sect.big ', {}, math, 1100),
snip('buu', 'union.big ', {}, math, 1100),
snip('nn', 'sect ', {}, math, 900),
snip('uu', 'union ', {}, math, 900),
snip('bnn', 'sect.big ', {}, math),
snip('buu', 'union.big ', {}, math),
snip('swo', 'without ', {}, math),
-- misc
@@ -63,28 +72,23 @@ return {
snip('mt', '|->> ', {}, math),
snip('Oo', 'compose ', {}, math),
snip('iso', 'tilde.equiv ', {}, math),
snip('ep', 'exp(<>) ', { 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('(.*)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),
snip('ddx', '(d <>)(d <>)', { i(1, 'f'), i(2, 'x') }, math),
snip('it', 'integral', {}, math),
snip('dx', 'd / (d <>) ', { i(1, 'x') }, math, 900),
snip('ddx', '(d <>) / (d <>) ', { i(1, 'f'), i(2, 'x') }, math),
snip('it', 'integral ', {}, math, 900),
snip('int', 'integral_(<>)^(<>) ', { i(1, 'a'), i(2, 'b') }, math),
snip('oit', 'integral_Omega ', {}, math),
snip('dit', 'integral_(<>) ', { i(1, 'Omega') }, math),
snip('sm', 'sum ', {}, math),
snip('sm', 'sum ', {}, math, 900),
snip('sum', 'sum_(<>)^(<>) ', { i(1, 'i=0'), i(2, 'oo') }, math),
snip('osm', 'sum_Omega ', {}, math),
snip('dsm', 'sum_(<>) ', { i(1, 'I') }, math),
snip('lm', 'lim <>', { i(1, 'a_n') }, math),
snip('lim', 'lim_(<> ->> <>) <>', { i(1, 'n'), i(2, 'oo'), i(3, 'a_n') }, math),
snip('lim (sup|inf)', 'lim<> <>', { cap(1), i(1, 'a_n') }, math),
snip('lim(_.*-.*) (sup|inf)', 'lim<><> <>', { cap(2), cap(1), i(1, 'a_n') }, math),
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),
}

View File

@@ -81,7 +81,7 @@ 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, 1000, false))
table.insert(snippets, snip(val[1], '<>', { d(1, smart_wrap, {}, { user_args = { val } }) }, math, 1500, false))
end
return {

View File

@@ -46,6 +46,11 @@ function M.run_shell_command(cmd, show_output)
end
end
function M.count_string(str, tocount)
local _, count = str:gsub(tocount, '')
return count
end
function M.char_to_hex(c) return string.format('%%%02X', string.byte(c)) end
function M.urlencode(url)
@@ -56,6 +61,12 @@ function M.urlencode(url)
return url
end
function M.generate_bool_set(arr, target)
for _, val in ipairs(arr) do
target[val] = true
end
end
function M.get_treesitter_root(bufnr) return ts.get_parser(bufnr):parse()[1]:root() end
function M.treesitter_match_start_end(match)

View File

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

View File

@@ -8,7 +8,7 @@ import aiohttp
from .flashcard import Flashcard
async def _gather_exceptions(coroutines):
async def gather_exceptions(coroutines):
for result in await asyncio.gather(*coroutines, return_exceptions=True):
if isinstance(result, Exception):
raise result
@@ -28,7 +28,7 @@ class AnkiConnectApi:
self.api_key = api_key
self.semaphore = asyncio.Semaphore(2) # increase in case Anki implements multithreading
async def push_flashcards(self, cards: Iterable[Flashcard]):
async def push_flashcards(self, cards: Iterable[Flashcard], reimport: bool):
add: dict[str, List[Flashcard]] = defaultdict(list)
update: dict[str, List[Flashcard]] = defaultdict(list)
n_add: int = 0
@@ -41,6 +41,14 @@ class AnkiConnectApi:
else:
update[card.deck].append(card)
n_update += 1
if reimport:
reimport_cards = await self._check_reimport(update)
print(f"Found {len(reimport_cards)} flashcards to reimport")
for card in reimport_cards:
update[card.deck].remove(card)
add[card.deck].append(card)
n_update -= 1
n_add += 1
print(
f"Pushing {n_add} new flashcards and {n_update} updated flashcards to Anki...",
@@ -48,7 +56,7 @@ class AnkiConnectApi:
)
await self._create_required_decks({*add.keys(), *update.keys()})
await self._add_new_cards(add)
await _gather_exceptions(
await gather_exceptions(
[
*self._update_cards_requests(add),
*self._update_cards_requests(update, True),
@@ -115,7 +123,18 @@ class AnkiConnectApi:
for deck in required:
if deck not in existing:
requests.append(self._request_api("createDeck", deck=deck))
await _gather_exceptions(requests)
await gather_exceptions(requests)
async def _check_reimport(self, cards_map: dict[str, List[Flashcard]]) -> List[Flashcard]:
cards = []
for cs in cards_map.values():
cards.extend(cs)
if not cards:
return []
existing = await self._request_api(
"findNotes", query=f"nid:{','.join([str(c.note_id) for c in cards])}"
)
return [c for c in cards if c.note_id not in existing]
def _update_cards_requests(
self, cards_map: dict[str, List[Flashcard]], update_deck: bool = True

View File

@@ -78,7 +78,7 @@ class Flashcard:
self.back_node = back
self.note_id_node = note_id
def update_id(self, value):
def update_id(self, value: int):
if self.note_id != value:
self.note_id = value
self.id_updated = True

View File

@@ -12,7 +12,9 @@ from anki.typst_compiler import TypstCompiler
cli = typer.Typer(name="typstar-anki")
async def export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_url, anki_key):
async def export_flashcards(
root_dir, force_scan, clear_cache, reimport, typst_cmd, anki_url, anki_key
):
parser = FlashcardParser()
compiler = TypstCompiler(root_dir, typst_cmd)
api = AnkiConnectApi(anki_url, anki_key)
@@ -27,7 +29,7 @@ async def export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_u
try:
# async anki push
await api.push_flashcards(flashcards)
await api.push_flashcards(flashcards, reimport)
finally:
# write id updates to files
parser.update_ids_in_source()
@@ -57,13 +59,24 @@ def cmd(
"as it clears hashes regardless of their path)"
),
] = False,
reimport: Annotated[
bool,
typer.Option(
help="Instead of throwing an error also add flashcards that have already been asigned an id "
"but are not present in Anki. The asigned id will be updated."
),
] = False,
typst_cmd: Annotated[
str, typer.Option(help="Typst command used for flashcard compilation")
] = "typst",
anki_url: Annotated[str, typer.Option(help="Url for Anki-Connect")] = "http://127.0.0.1:8765",
anki_key: Annotated[str | None, typer.Option(help="Api key for Anki-Connect")] = None,
):
asyncio.run(export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_url, anki_key))
asyncio.run(
export_flashcards(
root_dir, force_scan, clear_cache, reimport, typst_cmd, anki_url, anki_key
)
)
def main():