Merge pull request #19 from arne314/dev

V1.4.0
This commit is contained in:
Arne
2025-08-04 17:55:03 +02:00
committed by GitHub
19 changed files with 209 additions and 135 deletions

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.9 (typstar)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (typstar)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/typstar.iml" filepath="$PROJECT_DIR$/.idea/typstar.iml" />
</modules>
</component>
</project>

10
.idea/typstar.iml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9 (typstar)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,9 +1,9 @@
# Typstar
Neovim plugin for efficient note taking in Typst
Neovim plugin for efficient (mathematical) note taking in Typst
## Features
- Powerful autosnippets using [LuaSnip](https://github.com/L3MON4D3/LuaSnip/) and [Tree-sitter](https://tree-sitter.github.io/) (inspired by [fastex.nvim](https://github.com/lentilus/fastex.nvim))
- Easy insertion of drawings using [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin)
- Easy insertion of drawings using [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) or [Rnote](https://github.com/flxzt/rnote)
- Export of [Anki](https://apps.ankiweb.net/) flashcards \[No Neovim required\]
## Usage
@@ -14,6 +14,14 @@ To efficiently navigate insert nodes and avoid overlapping ones,
use `:TypstarSmartJump` and `:TypstarSmartJumpBack`.
Available snippets can mostly be intuitively derived from [here](././lua/typstar/snippets), they include:
Universal 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$ `)
You can find a complete map of latin to greek letters including reasons for the less intuitive ones [here](./lua/typstar/snippets/letters.lua).
Note that some greek letters have multiple latin ones mapped to them.
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; `$==>$ `)
@@ -23,18 +31,20 @@ Markup snippets:
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$ `)
- 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)]`)
- Simple functions: `fo<value> ` &#8594; `f(<value>) ` (e.g. `fox ` &#8594; `f(x) `, `ao5 ` &#8594; `a(5) `)
- Matrices: `<size>ma` and `<size>lma` (e.g. `23ma` &#8594; 2x3 matrix)
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.
- To open an inserted drawing in Obsidian, simply run `:TypstarOpenExcalidraw` while your cursor is on a line referencing the drawing.
### Excalidraw/Rnote
- Use `:TypstarInsertExcalidraw`/`:TypstarInsertRnote` to
create a new drawing using the [configured](#configuration) template,
insert a figure displaying it and open it in Obsidian/Rnote.
- To open an inserted drawing in Obsidian/Rnote,
simply run `:TypstarOpenDrawing` (or `:TypstarOpenExcalidraw`/`:TypstarOpenRnote` if you are using the same file extension for both)
while your cursor is on a line referencing the drawing.
### Anki
Use the `flA` snippet to create a new flashcard
@@ -156,7 +166,13 @@ require('typstar').setup({ -- depending on your neovim plugin system
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
4. If you encounter issues with the file creation of drawings, try cloning the repo into `~/typstar` or setting the `typstarRoot` config accordingly; feel free to open an issue
### Rnote
1. Install [Rnote](https://github.com/flxzt/rnote?tab=readme-ov-file#installation); I recommend not using flatpak as that might cause issues with file permissions.
2. Make sure `rnote-cli` is available in your `PATH` or set a different command at `exportCommand` in the [config](#configuration)
3. Have the `xdg-open` command working with Rnote files or set a different command at `uriOpenCommand` in the [config](#configuration)
4. See comment 4 above at Excalidraw
### Anki
0. Typst version `0.12.0` or higher is required
@@ -195,6 +211,16 @@ with pkgs; [
## Configuration
Configuration options can be intuitively derived from the table [here](./lua/typstar/config.lua).
### Excalidraw/Rnote templates
The `templatePath` option expects a table that maps file patterns to template locations.
To for example have a specific template for lectures, you could configure it like this
```Lua
templatePath = {
{ 'lectures/.*%.excalidraw%.md$', '~/Templates/lecture_excalidraw.excalidraw.md' }, -- path contains "lectures"
{ '%.excalidraw%.md$', '~/Templates/default_excalidraw.excalidraw.md' }, -- fallback
},
```
### Custom snippets
The [config](#configuration) allows you to
- disable all snippets via `snippets.enable = false`

View File

@@ -42,6 +42,8 @@
lua << EOF
print("Welcome to Typstar! This is just a demo.")
vim.g.mapleader = " "
require('nvim-treesitter.configs').setup {
highlight = { enable = true },
}
@@ -58,6 +60,12 @@
vim.keymap.set({'n', 'i'}, '<M-t>', '<Cmd>TypstarToggleSnippets<CR>', { silent = true, noremap = true })
vim.keymap.set({'s', 'i'}, '<M-j>', '<Cmd>TypstarSmartJump<CR>', { silent = true, noremap = true })
vim.keymap.set({'s', 'i'}, '<M-k>', '<Cmd>TypstarSmartJumpBack<CR>', { silent = true, noremap = true })
vim.keymap.set('n', '<leader>e', '<Cmd>TypstarInsertExcalidraw<CR>', { silent = true, noremap = true })
vim.keymap.set('n', '<leader>r', '<Cmd>TypstarInsertRnote<CR>', { silent = true, noremap = true })
vim.keymap.set('n', '<leader>o', '<Cmd>TypstarOpenDrawing<CR>', { silent = true, noremap = true })
vim.keymap.set('n', '<leader>a', '<Cmd>TypstarAnkiScan<CR>', { silent = true, noremap = true })
EOF
'';
plugins = [

View File

@@ -1,7 +1,7 @@
local M = {}
local default_config = {
typstarRoot = nil,
typstarRoot = nil, -- typstar installation location required to use default drawing templates (usually determined automatically)
anki = {
typstarAnkiCmd = 'typstar-anki',
typstCmd = 'typst',
@@ -13,8 +13,18 @@ local default_config = {
filename = 'drawing-%Y-%m-%d-%H-%M-%S',
fileExtension = '.excalidraw.md',
fileExtensionInserted = '.excalidraw.svg',
uriOpenCommand = 'xdg-open', -- set depending on OS
templatePath = nil,
uriOpenCommand = 'xdg-open', -- set depending on OS; try setting it to "obsidian" directly if you encounter problems and have it in your PATH
templatePath = {},
},
rnote = {
assetsDir = 'assets',
-- can be modified to e.g. export full pages; default is to try to export strokes only and otherwise export the entire document
exportCommand = 'rnote-cli export selection --no-background --no-pattern --on-conflict overwrite --output-file %s all %s || rnote-cli export doc --no-background --no-pattern --on-conflict overwrite --output-file %s %s',
filename = 'drawing-%Y-%m-%d-%H-%M-%S',
fileExtension = '.rnote',
fileExtensionInserted = '.rnote.svg', -- valid rnote export type
uriOpenCommand = 'xdg-open', -- see comment above for excalidraw
templatePath = {},
},
snippets = {
enable = true,
@@ -38,10 +48,12 @@ function M.merge_config(args)
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',
}
vim.list_extend(M.config.excalidraw.templatePath, {
{ '%.excalidraw%.md$', M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md' },
})
vim.list_extend(M.config.rnote.templatePath, {
{ '%.rnote$', M.config.typstarRoot .. '/res/rnote_template.rnote' },
})
end
M.merge_config(nil)

113
lua/typstar/drawings.lua Normal file
View File

@@ -0,0 +1,113 @@
local M = {}
local config = require('typstar.config')
local utils = require('typstar.utils')
local affix = [[
#figure(
image("%s"),
)
]]
local config_excalidraw = config.config.excalidraw
local config_rnote = config.config.rnote
local function launch_excalidraw(path, path_export)
print(string.format('Opening %s in Obsidian Excalidraw', path))
utils.run_shell_command(
string.format('%s "obsidian://open?path=%s"', config_excalidraw.uriOpenCommand, utils.urlencode(path)),
false
)
end
local rnote_watched = {}
local function auto_export_rnote(path, path_export)
if rnote_watched[path] then return end
rnote_watched[path] = true
local job_id = -1
local last_export = 0
local run_export = function(err, filename)
local time = vim.uv.now()
if err ~= nil or time - last_export < 800 then return end
if job_id == -1 then
last_export = time
local cmd = string.format(config_rnote.exportCommand, path_export, path, path_export, path)
job_id = utils.run_shell_command(cmd, false, nil, { on_exit = function() job_id = -1 end })
end
end
local watcher = vim.uv.new_fs_event()
watcher:start(path, {}, vim.schedule_wrap(run_export))
end
local function launch_rnote(path, path_export)
print(string.format('Opening %s in Rnote', path))
utils.run_shell_command(string.format('%s %s', config_rnote.uriOpenCommand, path), false)
auto_export_rnote(path, path_export)
end
local function insert_drawing(provider)
local cfg = provider[1]
local assets_dir = vim.fn.expand('%:p:h') .. '/' .. cfg.assetsDir
local filename = os.date(cfg.filename)
local path = assets_dir .. '/' .. filename .. cfg.fileExtension
local path_export = assets_dir .. '/' .. filename .. cfg.fileExtensionInserted
local path_insert = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted -- local relative path
if vim.fn.isdirectory(assets_dir) == 0 then vim.fn.mkdir(assets_dir, 'p') end
local found_match = false
for _, template_config in ipairs(cfg.templatePath) do
local pattern = template_config[1]
local template_path = template_config[2]
if string.match(path, pattern) then
found_match = true
-- use cat as we don't want to copy file metadata
utils.run_shell_command(string.format('cat %s > %s', template_path, path), false, nil, {
on_exit = function()
-- insert text and launch program
utils.insert_text_block(string.format(provider[2], path_insert))
provider[3](path, path_export)
end,
})
break
end
end
if not found_match then
print('No matching template found for path: ' .. path)
return
end
end
local excalidraw = {
config_excalidraw,
affix,
launch_excalidraw,
}
local rnote = {
config_rnote,
affix,
launch_rnote,
}
local providers = { excalidraw, rnote }
local open_drawing = function(prov)
for _, provider in ipairs(prov) do
local cfg = provider[1]
local line = vim.api.nvim_get_current_line()
local filename = line:match('"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.'))
if filename ~= nil and filename:match('^%s*$') == nil then
local path = vim.fn.expand('%:p:h') .. '/' .. filename .. cfg.fileExtension
local path_export = vim.fn.expand('%:p:h') .. '/' .. filename .. cfg.fileExtensionInserted
provider[3](path, path_export) -- launch program
break
end
end
end
function M.insert_obsidian_excalidraw() insert_drawing(excalidraw) end
function M.insert_rnote() insert_drawing(rnote) end
function M.open_obsidian_excalidraw() open_drawing({ excalidraw }) end
function M.open_rnote() open_drawing({ rnote }) end
function M.open_drawing() open_drawing(providers) end
return M

View File

@@ -15,7 +15,7 @@ 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,
callback = function() last_keystroke_time = vim.uv.now() end,
})
M.in_math = function()

View File

@@ -1,53 +0,0 @@
local M = {}
local config = require('typstar.config')
local utils = require('typstar.utils')
local cfg = config.config.excalidraw
local affix = [[
#figure(
image("%s"),
)
]]
local function launch_obsidian(path)
print(string.format('Opening %s in Excalidraw', path))
utils.run_shell_command(
string.format('%s "obsidian://open?path=%s"', cfg.uriOpenCommand, utils.urlencode(path)),
false
)
end
function M.insert_drawing()
local assets_dir = vim.fn.expand('%:p:h') .. '/' .. cfg.assetsDir
local filename = os.date(cfg.filename)
local path = assets_dir .. '/' .. filename .. cfg.fileExtension
local path_inserted = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted
if vim.fn.isdirectory(assets_dir) == 0 then vim.fn.mkdir(assets_dir, 'p') end
local found_match = false
for pattern, template_path in pairs(cfg.templatePath) do
if string.match(path, pattern) then
found_match = true
utils.run_shell_command(string.format('cat %s > %s', template_path, path), false) -- don't copy file metadata
break
end
end
if not found_match then
print('No matching template found for the path: ' .. path)
return
end
utils.insert_text_block(string.format(affix, path_inserted))
launch_obsidian(path)
end
function M.open_drawing()
local line = vim.api.nvim_get_current_line()
local path = vim.fn.expand('%:p:h')
.. '/'
.. string.match(line, '"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.'))
.. '.excalidraw.md'
launch_obsidian(path)
end
return M

View File

@@ -6,15 +6,18 @@ local luasnip = nil
M.setup = function(args)
config.merge_config(args)
local autosnippets = require('typstar.autosnippets')
local excalidraw = require('typstar.excalidraw')
local drawings = require('typstar.drawings')
local anki = require('typstar.anki')
vim.api.nvim_create_user_command('TypstarToggleSnippets', autosnippets.toggle_autosnippets, {})
vim.api.nvim_create_user_command('TypstarSmartJump', function() M.smart_jump(1) end, {})
vim.api.nvim_create_user_command('TypstarSmartJumpBack', function() M.smart_jump(-1) end, {})
vim.api.nvim_create_user_command('TypstarInsertExcalidraw', excalidraw.insert_drawing, {})
vim.api.nvim_create_user_command('TypstarOpenExcalidraw', excalidraw.open_drawing, {})
vim.api.nvim_create_user_command('TypstarInsertExcalidraw', drawings.insert_obsidian_excalidraw, {})
vim.api.nvim_create_user_command('TypstarInsertRnote', drawings.insert_rnote, {})
vim.api.nvim_create_user_command('TypstarOpenExcalidraw', drawings.open_obsidian_excalidraw, {})
vim.api.nvim_create_user_command('TypstarOpenRnote', drawings.open_rnote, {})
vim.api.nvim_create_user_command('TypstarOpenDrawing', drawings.open_drawing, {})
vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {})
vim.api.nvim_create_user_command('TypstarAnkiReimport', anki.scan_reimport, {})

View File

@@ -16,31 +16,32 @@ local greek_letters_map = {
['c'] = 'chi',
['d'] = 'delta',
['e'] = 'epsilon',
['f'] = 'phi',
['f'] = 'phi', -- sound
['g'] = 'gamma',
['h'] = 'eta',
['h'] = 'eta', -- look
['i'] = 'iota',
['j'] = 'theta',
['k'] = 'kappa',
['l'] = 'lambda',
['m'] = 'mu',
['n'] = 'nu',
['o'] = 'omega',
['o'] = 'omikron',
['p'] = 'psi',
['q'] = 'eta',
['q'] = 'theta', -- look?
['r'] = 'rho',
['s'] = 'sigma',
['t'] = 'tau',
['v'] = 'nu',
['w'] = 'omega',
['u'] = 'upsilon',
['v'] = 'nu', -- look
['w'] = 'omega', -- look
['x'] = 'xi',
['y'] = 'upsilon',
['y'] = 'upsilon', -- look
['z'] = 'zeta',
}
local greek_keys = {}
local greek_letters_set = {}
local common_indices = { '\\d+', '[i-n]' }
-- buitins and caligraphic letters from github.com/lentilus/readable-typst
-- builtins and calligraphic letters from github.com/lentilus/typst-scribe
local index_conflicts = { 'Im', 'in', 'ln', 'Pi', 'pi', 'Xi', 'xi', 'Ii', 'Jj', 'Kk', 'Ll', 'Mm', 'Nn' }
local index_conflicts_set = {}
local punctuation_prepend_space = { ',', ';' }

View File

@@ -73,9 +73,9 @@ return {
snip('Oo', 'compose ', {}, math),
snip('iso', 'tilde.equiv ', {}, math),
snip('cc', 'cases(\n\t<>\n)\\', { i(1, '1') }, math),
snip('([A-Za-z])o([A-Za-z0-9])', '<>(<>) ', { cap(1), cap(2) }, math, 100, {
maxTrigLength = 3,
blacklist = { 'bot', 'col', 'com', 'con', 'cos', 'cot', 'dol', 'dot', 'log', 'loz', 'mod', 'roo', 'top', 'won', 'xor' },
snip('([A-Za-z])o([A-Za-z0-9]) ', '<>(<>) ', { cap(1), cap(2) }, math, 100, {
maxTrigLength = 4,
blacklist = { 'bot ', 'cos ', 'cot ', 'dot ', 'log ', 'mod ', 'top ', 'won ', 'xor ' },
}),
snip('(K|M|N|Q|R|S|Z)([\\dn]) ', '<><>^<> ', { cap(1), cap(1), cap(2) }, math),

View File

@@ -26,8 +26,9 @@ function M.insert_text_block(snip)
vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), line_num, line_num, false, lines)
end
function M.run_shell_command(cmd, show_output, extra_handler)
function M.run_shell_command(cmd, show_output, extra_handler, opts)
extra_handler = extra_handler or function(msg) end
opts = opts or { on_exit = function() end }
local handle_output = function(data, err)
local msg = table.concat(data, '\n')
if not string.match(msg, '^%s*$') then
@@ -37,14 +38,17 @@ function M.run_shell_command(cmd, show_output, extra_handler)
end
end
if show_output then
vim.fn.jobstart(cmd, {
return vim.fn.jobstart(
cmd,
vim.tbl_deep_extend('force', {
on_stdout = function(_, data, _) handle_output(data, false) end,
on_stderr = function(_, data, _) handle_output(data, true) end,
stdout_buffered = false,
stderr_buffered = true,
})
}, opts)
)
else
vim.fn.jobstart(cmd)
return vim.fn.jobstart(cmd, opts)
end
end

View File

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

BIN
res/rnote_template.rnote Normal file
View File

Binary file not shown.

2
uv.lock generated
View File

@@ -438,7 +438,7 @@ wheels = [
[[package]]
name = "typstar"
version = "1.3.4"
version = "1.4.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },