diff --git a/README.md b/README.md index 8029b02..e166c66 100644 --- a/README.md +++ b/README.md @@ -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` → `#highlight[]`, `IMP` → `$=>$ `) +- [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) @@ -23,11 +23,12 @@ 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$ `) +- Common indices (numbers and letters `i-n`): ` ` → `_ ` in math and `$$ ` → `$_$ ` in markup (e.g `A314 ` → `A_314 `, `$alpha$n ` → `$alpha_n$ `) +- Series of numbered letters: ` ot ` → `_1, _2, ... ` (e.g. `a ot ` → `a_1, a_2, ... `, `a ot4 ` → `a_1, a_2, a_3, a_4 `, `alpha otk ` → `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)): `` → `()` (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) -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), +} +``` + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..52fb77f --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..700cd7f --- /dev/null +++ b/flake.nix @@ -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; + }; + }; + }; +} diff --git a/lua/typstar/anki.lua b/lua/typstar/anki.lua index d0d3490..9dfa6e6 100644 --- a/lua/typstar/anki.lua +++ b/lua/typstar/anki.lua @@ -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 diff --git a/lua/typstar/autosnippets.lua b/lua/typstar/autosnippets.lua index 45d5230..1dd4052 100644 --- a/lua/typstar/autosnippets.lua +++ b/lua/typstar/autosnippets.lua @@ -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 diff --git a/lua/typstar/config.lua b/lua/typstar/config.lua index 9491850..b7b4b19 100644 --- a/lua/typstar/config.lua +++ b/lua/typstar/config.lua @@ -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', diff --git a/lua/typstar/init.lua b/lua/typstar/init.lua index 6089e82..e0c49eb 100644 --- a/lua/typstar/init.lua +++ b/lua/typstar/init.lua @@ -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 diff --git a/lua/typstar/snippets/letters.lua b/lua/typstar/snippets/letters.lua index 6cc6257..7ffbc5a 100644 --- a/lua/typstar/snippets/letters.lua +++ b/lua/typstar/snippets/letters.lua @@ -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), } diff --git a/lua/typstar/snippets/math.lua b/lua/typstar/snippets/math.lua index 69fdb81..ae28c01 100644 --- a/lua/typstar/snippets/math.lua +++ b/lua/typstar/snippets/math.lua @@ -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('int', 'integral_(<>)^(<>)', { i(1, 'a'), i(2, 'b') }, math), - snip('oit', 'integral_Omega', {}, math), - snip('dit', 'integral_(<>)', { i(1, 'Omega') }, 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('sum', 'sum_(<>)^(<>)', { i(1, 'i=0'), i(2, 'oo') }, math), - snip('osm', 'sum_Omega', {}, math), - snip('dsm', 'sum_(<>)', { i(1, 'I') }, 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), } diff --git a/lua/typstar/snippets/visual.lua b/lua/typstar/snippets/visual.lua index 74b2f5d..d74d3b4 100644 --- a/lua/typstar/snippets/visual.lua +++ b/lua/typstar/snippets/visual.lua @@ -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 { diff --git a/lua/typstar/utils.lua b/lua/typstar/utils.lua index 4118d7c..85bc145 100644 --- a/lua/typstar/utils.lua +++ b/lua/typstar/utils.lua @@ -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) diff --git a/pyproject.toml b/pyproject.toml index ddae3f2..b79757c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/src/anki/anki_api.py b/src/anki/anki_api.py index b211d12..7844484 100644 --- a/src/anki/anki_api.py +++ b/src/anki/anki_api.py @@ -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 diff --git a/src/anki/flashcard.py b/src/anki/flashcard.py index ec8e23a..5ee2125 100644 --- a/src/anki/flashcard.py +++ b/src/anki/flashcard.py @@ -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 diff --git a/src/anki/main.py b/src/anki/main.py index 648eb85..08f0e6b 100644 --- a/src/anki/main.py +++ b/src/anki/main.py @@ -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():