aboutsummaryrefslogtreecommitdiffstats
path: root/plugin/50-notes.lua
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/50-notes.lua')
-rw-r--r--plugin/50-notes.lua179
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()