diff options
Diffstat (limited to 'plugin/50-notes.lua')
| -rw-r--r-- | plugin/50-notes.lua | 179 |
1 files changed, 97 insertions, 82 deletions
diff --git a/plugin/50-notes.lua b/plugin/50-notes.lua index ab90c8e..04fc824 100644 --- a/plugin/50-notes.lua +++ b/plugin/50-notes.lua @@ -1,31 +1,28 @@ --- --- 50-notes.lua --- --- * Turns the directory in `$NOTES_DIR` into a wiki of markdown notes linked with `[[wiki-links]]`. --- * Generates ctags for the notes and regenerates them whenever a note is written. --- * Provides 'omnifunc' wiki-link completion: note names inside `[[`, headings after `#`; --- auto-triggered as soon as `[[` or `#` is typed in a notes buffer. --- * Provides fzf-lua pickers to find notes by tag/name/section, by file name, or by content. --- * Provides rename commands that move the note (or rewrite a heading) and update every --- `[[wiki-link]]` pointing to it across all notes, editing loaded buffers in place and the rest --- on disk. --- --- User commands: --- `NotesRename`: rename the current note and update wiki-links --- `NotesRenameSection`: rename the heading at the cursor and update wiki-links --- `NotesFindTags`: fuzzy-find notes (tag, name, section) --- `NotesFindFile`: fuzzy-find notes (file name) --- `NotesFindGrep`: fuzzy-find notes (content) --- `NotesOpen`: move to the notes directory and open index --- --- Keymaps (global): --- `<Leader>N`: open notes in current window --- Keymaps (notes buffers only; shadow the matching `<Leader>f…` fzf defaults from 50-fzf.lua): --- `<Leader>ft`: fuzzy-find notes by tags --- `<Leader>ff`: fuzzy-find notes by file name --- `<Leader>fg`: fuzzy-find notes by content --- `<Tab>` / `<S-Tab>`: go to next/previous followable entity --- +--[[ 50-notes.lua — turn `$NOTES_DIR` into a wiki of markdown notes linked by wiki-link. + +Generates ctags for the notes and regenerates them whenever a note is written. Provides +'omnifunc' wiki-link completion: note names inside `[[`, headings after `#`; auto-triggered as +soon as `[[` or `#` is typed in a notes buffer. Provides fzf-lua pickers to find notes by +tag/name/section, by file name, or by content. Provides rename commands that move the note (or +rewrite a heading) and update every wiki-link pointing to it across all notes, editing loaded +buffers in place and the rest on disk. + +User commands: + `NotesRename`: rename the current note and update wiki-links + `NotesRenameSection`: rename the heading at the cursor and update wiki-links + `NotesFindTags`: fuzzy-find notes (tag, name, section) + `NotesFindFile`: fuzzy-find notes (file name) + `NotesFindGrep`: fuzzy-find notes (content) + `NotesOpen`: move to the notes directory and open index + +Keymaps (global): + `<Leader>N`: open notes in current window +Keymaps (notes buffers only; shadow the matching `<Leader>f…` fzf defaults from 50-fzf.lua): + `<Leader>ft`: fuzzy-find notes by tags + `<Leader>ff`: fuzzy-find notes by file name + `<Leader>fg`: fuzzy-find notes by content + `<Tab>` / `<S-Tab>`: go to next/previous followable entity +]] local notes_dir = require("dotfiles.notes").dir if notes_dir == nil then @@ -69,8 +66,9 @@ local function note_candidates() return out end --- Headings of a note, read from the ctags tagfile so `#` comments inside fenced code blocks aren't --- mistaken for headings (hashtag `h` / footnote `n` kinds excluded). +--[[ Headings of a note, read from the ctags tagfile (hashtag `h` / footnote `n` kinds excluded). +Reading tags rather than the file keeps `#` comments inside fenced code blocks from being mistaken +for headings. ]] local function heading_candidates(note) local out = {} local target = notes_dir .. "/" .. note .. ".md" @@ -90,8 +88,7 @@ local function heading_candidates(note) return out end --- 'omnifunc' for wiki-links: inside `[[`, complete note names; after `#`, complete the target --- note's headings. +-- 'omnifunc' for wiki-links: note names inside `[[`, the target note's headings after `#`. function _G.dotfiles_wikilink_source(findstart, base) local line = vim.api.nvim_get_current_line() local col = vim.api.nvim_win_get_cursor(0)[2] @@ -128,11 +125,12 @@ end -- Link rewriting ---------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- --- Generic engine: apply a `rewriter(text) -> new_text, n` to notes, editing loaded buffers in place --- and the rest on disk. Shared by note rename and section rename below. +--[[ Generic engine: apply a `rewriter(text) -> new_text, n` to notes, loaded or not. +Edits loaded buffers in place and the rest on disk. +Shared by note rename and section rename below. ]] --- Rewrite loaded buffer `buf` with `rewriter` --- Returns the number of elements rewritten +--[[ Rewrite loaded buffer `buf` with `rewriter`. +Returns the number of elements rewritten. ]] local function rewrite_loaded_buf(buf, rewriter) local was_modified = vim.bo[buf].modified local text = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n") @@ -171,8 +169,8 @@ local function rewrite_unloaded_file(file, rewriter) return n end --- Apply `rewriter(text) -> new_text, n` to every note, editing loaded buffers in place (preserving --- unsaved changes) and others on disk. Returns total edited links and files. +--[[ Apply `rewriter(text) -> new_text, n` to every note, returning total edited links and files. +Loaded buffers are edited in place (preserving unsaved changes), the others on disk. ]] local function rewrite_all_notes(rewriter) local loaded = {} for _, buf in ipairs(vim.api.nvim_list_bufs()) do @@ -211,10 +209,10 @@ end -- Rename note ------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- --- Rewrite `[[oldname]]` / `[[oldname#heading]]` targets to `newname`, matching the target exactly --- so `[[oldname_extra]]` is left alone. The gsub replacement is a function, so its return value is --- used literally (no `%` escaping needed). --- Returns the new text and the number of links rewritten. +--[=[ Rewrite `[[oldname]]` / `[[oldname#heading]]` targets to `newname`. +Matches the target exactly so `[[oldname_extra]]` is left alone. The gsub replacement is a +function, so its return value is used literally (no `%` escaping needed). +Returns the new text and the number of links rewritten. ]=] local function rewrite_links(text, oldname, newname) local count = 0 local out = text:gsub("%[%[(.-)%]%]", function(inner) @@ -259,10 +257,10 @@ local function rename_buffer(buf, new_path) end end --- Rename note `old_path` → `new_path` and update every wiki-link pointing to it across all notes. --- If the note is open in a buffer, that buffer is repointed at `new_path` *before* links are --- rewritten, so the rewrite reaches the buffer (incl. its own self-links) instead of being --- clobbered by a later write. Regenerates the notes tags after renaming. +--[[ Rename note `old_path` → `new_path` and update every wiki-link pointing to it across all notes. +If the note is open in a buffer, that buffer is repointed at `new_path` *before* links are +rewritten, so the rewrite reaches the buffer (incl. its own self-links) instead of being clobbered +by a later write. Regenerates the notes tags after renaming. ]] local function rename_note(old_path, new_path) if not vim.startswith(old_path, notes_dir) or not vim.startswith(new_path, notes_dir) then vim.notify("old_path and new_path must both be inside " .. notes_dir, vim.log.levels.ERROR) @@ -299,7 +297,7 @@ local function rename_note(old_path, new_path) link_count, file_count ) - vim.notify(notification) + vim.notify(notification, vim.log.levels.INFO) end -- Rename the current note to `new_rel` (relative to the notes dir, `.md` optional). @@ -310,8 +308,8 @@ local function rename_current_note(new_rel) rename_note(old_path, vim.fs.joinpath(notes_dir, new_rel)) end --- `:NotesRename` implementation: take the new note name from the command argument, or prompt for it --- (defaulting to the current note's name), then rename via `rename_current_note`. +--[[ `:NotesRename` implementation: take the new note name from the argument, or prompt for it. +The prompt defaults to the current note's name; the rename goes via `rename_current_note`. ]] local function rename_note_command(opts) if opts.args ~= "" then rename_current_note(opts.args) @@ -331,8 +329,8 @@ end -- Rename section ---------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- --- Rewrites `[[note#oldhead]]` heading anchors to `newhead` in `text`. --- Returns the edited text and the number of substitutions performed. +--[=[ Rewrites `[[note#oldhead]]` heading anchors to `newhead` in `text`. +Returns the edited text and the number of substitutions performed. ]=] local function rewrite_heading_links(text, note, oldhead, newhead) local count = 0 local out = text:gsub("%[%[(.-)%]%]", function(inner) @@ -345,8 +343,8 @@ local function rewrite_heading_links(text, note, oldhead, newhead) return out, count end --- Returns the markdown heading at or above the cursor as `{ row, level, text }` (row is 1-based), --- or nil if not found. +--[[ Returns the markdown heading at or above the cursor, or nil if not found. +The heading is a `{ row, level, text }` table with a 1-based `row`. ]] local function heading_at_or_above_cursor(buf) local row = vim.api.nvim_win_get_cursor(0)[1] local lines = vim.api.nvim_buf_get_lines(buf, 0, row, false) @@ -359,8 +357,9 @@ local function heading_at_or_above_cursor(buf) return nil end --- Rename section `old_head` → `new_head` in note `note` (rel path) and update every wiki-link --- pointing to it across all notes. Regenerates the notes tags after renaming. +--[[ Rename section `old_head` → `new_head` in note `note` (rel path), updating its wiki-links. +Every wiki-link pointing to the section across all notes is rewritten. +Regenerates the notes tags after renaming. ]] local function rename_section(note, old_head, new_head) local link_count, file_count = rewrite_all_notes(function(text) return rewrite_heading_links(text, note, old_head, new_head) @@ -373,7 +372,7 @@ local function rename_section(note, old_head, new_head) link_count, file_count ) - vim.notify(notification) + vim.notify(notification, vim.log.levels.INFO) end -- Rename the heading at the cursor to `new_head` and update wiki-links to it. @@ -401,8 +400,8 @@ local function rename_current_section(new_head) rename_section(note, heading.text, new_head) end --- `:NotesRenameSection` implementation: take the new heading from the command argument, or prompt --- for it (defaulting to the heading at the cursor), then rename via `rename_current_section`. +--[[ `:NotesRenameSection` implementation: the new heading comes from the argument, or a prompt. +The prompt defaults to the heading at the cursor; the rename goes via `rename_current_section`. ]] local function rename_section_command(opts) if opts.args ~= "" then rename_current_section(opts.args) @@ -423,23 +422,34 @@ end -- Buffer init & pickers --------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- --- Initializes a notes buffer --- * Include generated notes tags in 'tags' option --- * Enable wikilink completion --- * Map <Tab>/<S-Tab> to jump between followable entities --- * Shadow fzf's `<Leader>ft`/`<Leader>ff`/`<Leader>fg` with the notes pickers, buffer-locally so --- the global fzf defaults (50-fzf.lua) stay untouched everywhere else. +local function notes_follow_next() + follow.next(false) +end + +local function notes_follow_prev() + follow.next(true) +end + +--[[ Initialize a notes buffer: + * include generated notes tags in the 'tags' option, + * enable wiki-link completion, + * map <Tab>/<S-Tab> to jump between followable entities, + * shadow fzf's `<Leader>ft`/`<Leader>ff`/`<Leader>fg` with the notes pickers, buffer-locally, + so the global fzf defaults (50-fzf.lua) stay untouched everywhere else. +]] local function init_notes_buffer() if not vim.tbl_contains(vim.opt_local.tags:get(), tagfile) then vim.opt_local.tags:append(tagfile) end vim.bo.omnifunc = "v:lua.dotfiles_wikilink_source" - vim.keymap.set("n", "<Tab>", function() - follow.next(false) - end, { buffer = true, desc = "Go to next followable entity" }) - vim.keymap.set("n", "<S-Tab>", function() - follow.next(true) - end, { buffer = true, desc = "Go to previous followable entity" }) + vim.keymap.set("n", "<Tab>", notes_follow_next, { + buffer = true, + desc = "Go to next followable entity", + }) + vim.keymap.set("n", "<S-Tab>", notes_follow_prev, { + buffer = true, + desc = "Go to previous followable entity", + }) vim.keymap.set("n", "<Leader>ft", vim.cmd.NotesFindTags, { buffer = true, desc = "Fuzzy-find notes", @@ -473,10 +483,15 @@ end -- Enters the notes dir and opens its index, creating it with a `# Index` header on first use. local function open_notes() + vim.cmd.new() vim.cmd.lcd(notes_dir) local index = notes_dir .. "/index.md" if not vim.uv.fs_stat(index) then - local f = assert(io.open(index, "w")) + local f = io.open(index, "w") + if not f then + vim.notify("Could not open file " .. index, vim.log.levels.ERROR) + return + end f:write("# Index\n") f:close() end @@ -490,7 +505,7 @@ end -- Initializes note buffer vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, { desc = "Set up notes buffer (tagfile, wiki-link completion)", - group = vim.g.dotfiles.augroup, + group = vim.g.dotfiles_augroup, pattern = notes_dir .. "/*", callback = init_notes_buffer, }) @@ -498,19 +513,19 @@ vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, { -- Regenerate tags everytime a note (i.e. file inside `notes_dir`) is written vim.api.nvim_create_autocmd("BufWritePost", { desc = "Regenerate notes ctags", - group = vim.g.dotfiles.augroup, + group = vim.g.dotfiles_augroup, pattern = notes_dir .. "/*", callback = generate_tags, }) --- Pop wiki-link completion as soon as `[[` (note name) or `#` (heading) is typed. 'autocomplete' --- only triggers on keyword characters, so it would otherwise wait for the first note-name char. --- This watches the typed text instead of mapping `[`, so nvim-autopairs keeps its `[` -> `[]` rule; --- the omnifunc silently cancels (returns -3) when the cursor is not inside an open `[[`, so the `#` --- triggers on plain headings are harmless. +--[[ Pop wiki-link completion as soon as `[[` (note name) or `#` (heading) is typed. +'autocomplete' only triggers on keyword characters, so it would otherwise wait for the first +note-name char. This watches the typed text instead of mapping `[`, so nvim-autopairs keeps its +`[` -> `[]` rule; the omnifunc silently cancels (returns -3) when the cursor is not inside an open +`[[`, so the `#` triggers on plain headings are harmless. ]] vim.api.nvim_create_autocmd("TextChangedI", { desc = "Trigger wiki-link completion after `[[` or `#`", - group = vim.g.dotfiles.augroup, + group = vim.g.dotfiles_augroup, callback = function() if vim.bo.omnifunc ~= "v:lua.dotfiles_wikilink_source" or vim.fn.pumvisible() == 1 then return @@ -550,8 +565,8 @@ vim.api.nvim_create_user_command( ) vim.api.nvim_create_user_command("NotesOpen", open_notes, { desc = "Open notes" }) --- Only the notes-open map is global; the find pickers (`ft`/`ff`/`fg`) are set buffer-locally in --- `init_notes_buffer` so they shadow the fzf defaults only inside notes buffers. -vim.keymap.set("n", "<Leader>N", vim.cmd.NotesOpen, { desc = "Open notes in current window" }) +--[[ Only the notes-open map is global; the find pickers (`ft`/`ff`/`fg`) are buffer-local. +`init_notes_buffer` sets them so they shadow the fzf defaults only inside notes buffers. ]] +vim.keymap.set("n", "<Leader>N", vim.cmd.NotesOpen, { desc = "Open notes in new window" }) generate_tags() |
