From 4598ccda8bf7082c9b67a5a9e355830f4906edad Mon Sep 17 00:00:00 2001 From: Jason Paris Date: Mon, 7 Jul 2025 09:38:53 -0400 Subject: [PATCH 1/3] feat: enhance rename function with fzf-lua file preview - Add fzf-lua preview interface showing files that will be updated during rename - New ui.show_rename_preview config option (default: true) to control behavior - Maintain backward compatibility with skip_ui option for tests - Update all documentation (README, help files) with new functionality - Add comprehensive test coverage for config option - Clean up debug statements and fix linting issues Users can now see exactly which files will be affected before confirming a rename operation, with the ability to preview file contents and disable the feature if desired. --- README.md | 17 +++++- doc/markdown-notes.txt | 31 ++++++++--- doc/tags | 1 + lua/markdown-notes/config.lua | 6 +++ lua/markdown-notes/links.lua | 80 +++++++++++++++++++++++++---- tests/markdown-notes/links_spec.lua | 75 +++++++++++++++++++-------- tests/markdown-notes/notes_spec.lua | 4 +- 7 files changed, 174 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index ec3934d..bdce8c2 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ That's it! You're ready to start building your knowledge base. - **📅 Daily Notes** - Quick creation and navigation with automatic templating - **📝 Template System** - Flexible templates with variable substitution (`{{date}}`, `{{time}}`, `{{title}}`, etc.) - **🔗 Wiki-style Links** - Create and follow `[[note-name]]` links between notes -- **🔄 Smart Renaming** - Rename notes and automatically update all references +- **🔄 Smart Renaming** - Rename notes and automatically update all references with file preview - **🔍 Powerful Search** - Find notes by filename or content with syntax highlighting - **â†Šī¸ Backlinks** - Discover which notes reference the current note @@ -136,7 +136,7 @@ All keybindings use `n` as the prefix for easy discovery: | `np` | Insert template | Insert template at cursor | | `ng` | Search tags | Find notes by frontmatter tags | | `nb` | Show backlinks | Show notes linking to current note | -| `nr` | Rename note | Rename note and update all references | +| `nr` | Rename note | Rename note and update all references with preview | | `nw` | Pick workspace | Switch between workspaces | | `gf` | Follow link | Follow link under cursor | @@ -185,6 +185,14 @@ gf → Follow the link under cursor nr → Rename current note and update all references ``` +**Smart Renaming**: When you rename a note that has links pointing to it, markdown-notes.nvim will: +1. Show you a preview of all files that will be updated +2. Let you browse through them with fzf-lua +3. Update all `[[note-name]]` and `[[note-name|display text]]` references automatically +4. Handle files in subdirectories correctly + +> **💡 Tip:** You can disable the preview and use a simple confirmation dialog by setting `ui.show_rename_preview = false` in your configuration. + ### Using Templates Templates make your notes consistent and save time: @@ -239,6 +247,11 @@ require("markdown-notes").setup({ -- Template settings default_template = "basic", -- Auto-apply this template to new notes + -- UI behavior + ui = { + show_rename_preview = true, -- Show file preview when renaming notes with links + }, + -- Custom template variables template_vars = { date = function() return os.date("%Y-%m-%d") end, diff --git a/doc/markdown-notes.txt b/doc/markdown-notes.txt index 3e7a9a3..85c2274 100644 --- a/doc/markdown-notes.txt +++ b/doc/markdown-notes.txt @@ -33,7 +33,7 @@ Core Features~ - Daily Notes - Quick creation and navigation with automatic templating - Template System - Flexible templates with variable substitution - Wiki-style Links - Create and follow [[note-name]] links between notes -- Smart Renaming - Rename notes and automatically update all references +- Smart Renaming - Rename notes and automatically update all references with file preview - Powerful Search - Find notes by filename or content with syntax highlighting - Backlinks - Discover which notes reference the current note @@ -158,9 +158,20 @@ Creating Links Between Notes: > Following and Managing Links: > gf → Follow the link under cursor nb → Show all notes that link to current note (backlinks) - nr → Rename current note and update all references + nr → Rename current note and update all references with preview < +Smart Renaming~ + *markdown-notes-usage-smart-renaming* +When you rename a note that has links pointing to it, markdown-notes.nvim will: +1. Show you a preview of all files that will be updated +2. Let you browse through them with fzf-lua and file preview +3. Update all `[[note-name]]` and `[[note-name|display text]]` references +4. Handle files in subdirectories correctly + +You can disable the preview and use a simple confirmation dialog by setting +`ui.show_rename_preview = false` in your configuration. + Using Templates~ *markdown-notes-usage-templates* Templates make your notes consistent and save time: > @@ -206,7 +217,7 @@ Note Management~ Links and Navigation~ `nl` Search for note and insert wiki-link `nb` Show notes linking to current note -`nr` Rename note and update all references +`nr` Rename note and update all references with preview `gf` Follow link under cursor Templates and Tags~ @@ -235,6 +246,11 @@ Custom Configuration Options~ -- Template settings default_template = "basic", -- Auto-apply this template to new notes + -- UI behavior + ui = { + show_rename_preview = true, -- Show file preview when renaming notes with links + }, + -- Custom template variables template_vars = { date = function() return os.date("%Y-%m-%d") end, @@ -416,7 +432,8 @@ Ex Commands~ *markdown-notes-ex-commands* :MarkdownNotesRename [{name}] *:MarkdownNotesRename* Rename current note and update all references. If {name} is not provided, - prompts for the new name. Shows confirmation dialog with file count. + prompts for the new name. Shows file preview with affected files or + confirmation dialog depending on configuration. Function API~ *markdown-notes-function-commands* @@ -452,10 +469,12 @@ require("markdown-notes.links").follow_link() *markdown-notes.follow_link* require("markdown-notes.links").show_backlinks() *markdown-notes.show_backlinks* Show backlinks to the current note. -require("markdown-notes.links").rename_note(new_name) *markdown-notes.rename_note* +require("markdown-notes.links").rename_note(new_name, opts) *markdown-notes.rename_note* Rename the current note and automatically update all wiki-style link references across the vault. Supports both `[[note]]` and `[[note|display]]` - formats. Shows confirmation dialog with file count before proceeding. + formats. Shows file preview with affected files by default, or simple + confirmation if ui.show_rename_preview is false. Optional {opts} table + accepts skip_ui boolean for programmatic usage. require("markdown-notes.templates").pick_template() *markdown-notes.pick_template* Pick and insert a template at cursor position. diff --git a/doc/tags b/doc/tags index 0559b3d..f4dcbed 100644 --- a/doc/tags +++ b/doc/tags @@ -29,6 +29,7 @@ markdown-notes-usage-daily-notes markdown-notes.txt /*markdown-notes-usage-daily markdown-notes-usage-guide markdown-notes.txt /*markdown-notes-usage-guide* markdown-notes-usage-links markdown-notes.txt /*markdown-notes-usage-links* markdown-notes-usage-note-management markdown-notes.txt /*markdown-notes-usage-note-management* +markdown-notes-usage-smart-renaming markdown-notes.txt /*markdown-notes-usage-smart-renaming* markdown-notes-usage-templates markdown-notes.txt /*markdown-notes-usage-templates* markdown-notes-workspace-commands markdown-notes.txt /*markdown-notes-workspace-commands* markdown-notes-workspace-setup markdown-notes.txt /*markdown-notes-workspace-setup* diff --git a/lua/markdown-notes/config.lua b/lua/markdown-notes/config.lua index 817122f..824f199 100644 --- a/lua/markdown-notes/config.lua +++ b/lua/markdown-notes/config.lua @@ -33,6 +33,12 @@ M.defaults = { -- Default workspace (optional) default_workspace = nil, + -- UI behavior + ui = { + -- Show file preview when renaming notes that have links + show_rename_preview = true, + }, + -- Key mappings mappings = { daily_note_today = "nd", diff --git a/lua/markdown-notes/links.lua b/lua/markdown-notes/links.lua index 458f2e9..d72d306 100644 --- a/lua/markdown-notes/links.lua +++ b/lua/markdown-notes/links.lua @@ -162,7 +162,8 @@ function M.show_backlinks() }) end -function M.rename_note(new_name) +function M.rename_note(new_name, opts) + opts = opts or {} local current_path = vim.fn.expand("%:p") local options = config.get_current_config() local vault_path = vim.fn.expand(options.vault_path) @@ -256,19 +257,76 @@ function M.rename_note(new_name) new_relative_path = current_dir_relative .. "/" .. new_name end - -- Ask for confirmation - local message = "Rename '" .. relative_path .. "' to '" .. new_name .. "'" - if #files_to_update > 0 then - message = message .. " and update " .. #files_to_update .. " files with links?" - else - message = message .. "?" - end + -- Show affected files and ask for confirmation + if #files_to_update > 0 and not opts.skip_ui and options.ui.show_rename_preview then + local ok, fzf = pcall(require, "fzf-lua") + if not ok then + vim.notify("fzf-lua not available", vim.log.levels.ERROR) + return + end - local confirm = vim.fn.confirm(message, "&Yes\n&No", 2) - if confirm ~= 1 then - return + -- Show files that will be updated + local file_list = {} + for _, file_info in ipairs(files_to_update) do + table.insert(file_list, file_info.file) + end + + fzf.fzf_exec(file_list, { + prompt = "Files to update (" .. #files_to_update .. ") - Enter to proceed > ", + cwd = vault_path, + previewer = "builtin", + actions = { + ["default"] = function() + -- Proceed with rename + local confirm = vim.fn.confirm( + "Proceed with rename and update " .. #files_to_update .. " files?", + "&Yes\n&No", + 2 + ) + if confirm == 1 then + M._perform_rename( + current_path, + new_file_path, + files_to_update, + relative_path, + new_relative_path, + update_count + ) + end + end, + }, + }) + else + -- Skip UI for tests or when no files to update + local message = "Rename '" .. relative_path .. "' to '" .. new_name .. "'" + if #files_to_update > 0 then + message = message .. " and update " .. #files_to_update .. " files?" + else + message = message .. "?" + end + local confirm = opts.skip_ui and 1 or vim.fn.confirm(message, "&Yes\n&No", 2) + if confirm == 1 then + M._perform_rename( + current_path, + new_file_path, + files_to_update, + relative_path, + new_relative_path, + update_count + ) + end end +end +-- Helper function to perform the actual rename operation +function M._perform_rename( + current_path, + new_file_path, + files_to_update, + relative_path, + new_relative_path, + update_count +) -- Update all linking files for _, file_info in ipairs(files_to_update) do local file_content = vim.fn.readfile(file_info.path) diff --git a/tests/markdown-notes/links_spec.lua b/tests/markdown-notes/links_spec.lua index 51f8272..de1f9b7 100644 --- a/tests/markdown-notes/links_spec.lua +++ b/tests/markdown-notes/links_spec.lua @@ -48,7 +48,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Rename the note - links.rename_note("renamed-note") + links.rename_note("renamed-note", { skip_ui = true }) -- Check that the file was renamed assert.is_true(vim.fn.filereadable(vault_path .. "/renamed-note.md") == 1) @@ -75,7 +75,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Rename the note - links.rename_note("new-name") + links.rename_note("new-name", { skip_ui = true }) -- Check that the file was renamed assert.is_true(vim.fn.filereadable(vault_path .. "/new-name.md") == 1) @@ -105,7 +105,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Rename the note - links.rename_note("tech-guide") + links.rename_note("tech-guide", { skip_ui = true }) -- Check that the file was renamed assert.is_true(vim.fn.filereadable(vault_path .. "/tech-guide.md") == 1) @@ -136,7 +136,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Rename the note - links.rename_note("primary-topic") + links.rename_note("primary-topic", { skip_ui = true }) -- Check file was renamed assert.is_true(vim.fn.filereadable(vault_path .. "/primary-topic.md") == 1) @@ -169,7 +169,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Try to rename to existing file name - links.rename_note("existing") + links.rename_note("existing", { skip_ui = true }) -- Check that original file was not renamed assert.is_true(vim.fn.filereadable(note_path) == 1) @@ -201,7 +201,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Rename the note - links.rename_note("alpha-project") + links.rename_note("alpha-project", { skip_ui = true }) -- Check file was renamed in same directory assert.is_true(vim.fn.filereadable(subdir .. "/alpha-project.md") == 1) @@ -222,7 +222,7 @@ describe("links", function() vim.cmd("edit " .. note_path) -- Rename with .md extension - links.rename_note("new-name.md") + links.rename_note("new-name.md", { skip_ui = true }) -- Check that file was renamed without double extension assert.is_true(vim.fn.filereadable(vault_path .. "/new-name.md") == 1) @@ -250,7 +250,7 @@ describe("links", function() -- Open and rename the "project" note vim.cmd("edit " .. note_path) - links.rename_note("main-project") + links.rename_note("main-project", { skip_ui = true }) -- Check that files were renamed correctly assert.is_true(vim.fn.filereadable(vault_path .. "/main-project.md") == 1) @@ -271,8 +271,6 @@ describe("links", function() -- Verify the similar link wasn't corrupted assert.is_nil(final_content:find("[[main-project-archive]]", 1, true)) - - print("Final content:", final_content) end) it("handles very similar note names correctly", function() @@ -287,7 +285,7 @@ describe("links", function() -- Open and rename vim.cmd("edit " .. note_path) - links.rename_note("new-api") + links.rename_note("new-api", { skip_ui = true }) -- Check final content local final_content = table.concat(vim.fn.readfile(linking_note_path), "\n") @@ -300,8 +298,6 @@ describe("links", function() -- The old exact link should be gone assert.is_nil(final_content:find("[[api]]", 1, true)) - - print("Very similar test result:", final_content) end) it("handles files with spaces in names", function() @@ -314,7 +310,7 @@ describe("links", function() -- Open and rename vim.cmd("edit " .. vim.fn.fnameescape(note_path)) - links.rename_note("new project name") + links.rename_note("new project name", { skip_ui = true }) -- Check results assert.is_true(vim.fn.filereadable(vault_path .. "/new project name.md") == 1) @@ -334,11 +330,11 @@ describe("links", function() _G.notifications = {} -- Test empty string - links.rename_note("") + links.rename_note("", { skip_ui = true }) assert.is_true(vim.fn.filereadable(note_path) == 1) -- Should still exist -- Test whitespace only - links.rename_note(" ") + links.rename_note(" ", { skip_ui = true }) assert.is_true(vim.fn.filereadable(note_path) == 1) -- Should still exist -- Should have error notifications @@ -361,9 +357,9 @@ describe("links", function() _G.notifications = {} -- Test invalid characters - links.rename_note("test/invalid") - links.rename_note("test\\invalid") - links.rename_note("test:invalid") + links.rename_note("test/invalid", { skip_ui = true }) + links.rename_note("test\\invalid", { skip_ui = true }) + links.rename_note("test:invalid", { skip_ui = true }) -- File should still exist since rename should fail assert.is_true(vim.fn.filereadable(note_path) == 1) @@ -384,7 +380,7 @@ describe("links", function() vim.cmd("enew") -- Should handle gracefully without crashing - links.rename_note("test") + links.rename_note("test", { skip_ui = true }) -- Check for appropriate notification assert.is_true(#_G.notifications > 0) @@ -397,5 +393,44 @@ describe("links", function() end assert.is_true(found_warning) end) + + it("respects show_rename_preview config option", function() + -- Create a note with links + local note_path = vault_path .. "/original-note.md" + local linking_note_path = vault_path .. "/linking-note.md" + + vim.fn.writefile({ "# Original Note" }, note_path) + vim.fn.writefile({ "Link to [[original-note]]" }, linking_note_path) + + -- Test with show_rename_preview = false + local original_config = config.get_current_config() + local test_config = vim.deepcopy(original_config) + test_config.ui = test_config.ui or {} + test_config.ui.show_rename_preview = false + + -- Mock the config to return our test config + local original_get_config = config.get_current_config + config.get_current_config = function() + return test_config + end + + vim.cmd("edit " .. note_path) + + -- This should use simple confirmation, not fzf-lua preview + -- Since we're using skip_ui=true, it should still work + links.rename_note("renamed-note", { skip_ui = true }) + + -- Verify the rename worked + assert.is_true(vim.fn.filereadable(vault_path .. "/renamed-note.md") == 1) + assert.is_true(vim.fn.filereadable(note_path) == 0) + + -- Verify the link was updated + local updated_content = table.concat(vim.fn.readfile(linking_note_path), "\n") + assert.is_not_nil(updated_content:find("[[renamed-note]]", 1, true)) + assert.is_nil(updated_content:find("[[original-note]]", 1, true)) + + -- Restore original config + config.get_current_config = original_get_config + end) end) end) diff --git a/tests/markdown-notes/notes_spec.lua b/tests/markdown-notes/notes_spec.lua index 6f3bc09..738b5f0 100644 --- a/tests/markdown-notes/notes_spec.lua +++ b/tests/markdown-notes/notes_spec.lua @@ -22,7 +22,9 @@ describe("notes", function() local original_os = _G.os local test_timestamp = 1720094400 _G.os = setmetatable({ - time = function() return test_timestamp end + time = function() + return test_timestamp + end, }, { __index = original_os }) -- Mock vim.fn.input to return empty string (no title) From e9dcad5a90993bcd9424d2d509a6bea25a166433 Mon Sep 17 00:00:00 2001 From: Jason Paris Date: Mon, 7 Jul 2025 10:35:03 -0400 Subject: [PATCH 2/3] fix: remove redundant confirmation dialog in rename preview - Remove confusing second confirmation after preview selection - Update prompt to clearly indicate Enter executes rename, Esc cancels - Preview interface now serves as the confirmation step - Improves user experience by reducing friction --- lua/markdown-notes/links.lua | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/lua/markdown-notes/links.lua b/lua/markdown-notes/links.lua index d72d306..af3f27b 100644 --- a/lua/markdown-notes/links.lua +++ b/lua/markdown-notes/links.lua @@ -272,27 +272,20 @@ function M.rename_note(new_name, opts) end fzf.fzf_exec(file_list, { - prompt = "Files to update (" .. #files_to_update .. ") - Enter to proceed > ", + prompt = "Files to update (" .. #files_to_update .. ") - Enter to rename, Esc to cancel > ", cwd = vault_path, previewer = "builtin", actions = { ["default"] = function() - -- Proceed with rename - local confirm = vim.fn.confirm( - "Proceed with rename and update " .. #files_to_update .. " files?", - "&Yes\n&No", - 2 + -- Proceed with rename directly (preview was the confirmation) + M._perform_rename( + current_path, + new_file_path, + files_to_update, + relative_path, + new_relative_path, + update_count ) - if confirm == 1 then - M._perform_rename( - current_path, - new_file_path, - files_to_update, - relative_path, - new_relative_path, - update_count - ) - end end, }, }) From f7cd0113783b3ed17b71e61062d0bbbfbb014df6 Mon Sep 17 00:00:00 2001 From: Jason Paris Date: Mon, 7 Jul 2025 11:02:34 -0400 Subject: [PATCH 3/3] fix: resolve buffer management issue in rename operation Previously when renaming the currently open file, both old and new file buffers would remain open, causing confusion. Now properly handles the buffer transition by updating the current buffer's filename and reloading it, while gracefully handling any buffer name conflicts. Also includes luacheck configuration improvements for better code formatting and increased line length limits for improved readability. --- .luacheckrc | 48 +++++++++++++++++++++--------------- lua/markdown-notes/links.lua | 36 +++++++++++++++++---------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 5ce58f4..7cdc317 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -5,41 +5,49 @@ std = "luajit" -- Add Neovim globals globals = { - "vim", + "vim", } -- Additional read-only globals for testing read_globals = { - -- Busted testing framework - "describe", "it", "before_each", "after_each", "setup", "teardown", - "assert", "spy", "stub", "mock", - -- Plenary testing - "require", + -- Busted testing framework + "describe", + "it", + "before_each", + "after_each", + "setup", + "teardown", + "assert", + "spy", + "stub", + "mock", + -- Plenary testing + "require", } -- Files/directories to exclude exclude_files = { - ".luarocks/", - "doc/tags", + ".luarocks/", + "doc/tags", } -- Ignore specific warnings ignore = { - "212", -- Unused argument (common in Neovim plugins for callback functions) - "213", -- Unused loop variable (common in ipairs/pairs loops) + "212", -- Unused argument (common in Neovim plugins for callback functions) + "213", -- Unused loop variable (common in ipairs/pairs loops) } -- Maximum line length -max_line_length = 100 +max_line_length = 120 -- Files and their specific configurations files = { - ["tests/"] = { - -- Allow longer lines in tests for readability - max_line_length = 120, - -- Additional testing globals - globals = { - "vim", -- vim is mocked in tests - } - } -} \ No newline at end of file + ["tests/"] = { + -- Allow longer lines in tests for readability + max_line_length = 120, + -- Additional testing globals + globals = { + "vim", -- vim is mocked in tests + }, + }, +} diff --git a/lua/markdown-notes/links.lua b/lua/markdown-notes/links.lua index af3f27b..64fbb78 100644 --- a/lua/markdown-notes/links.lua +++ b/lua/markdown-notes/links.lua @@ -110,8 +110,7 @@ function M.show_backlinks() local search_text = "[[" .. relative_path .. "]]" -- Get all markdown files first, then check each one - local all_files_cmd = "cd " .. vim.fn.shellescape(vault_path) .. - " && find . -name '*.md' -type f -not -path '*/.*'" + local all_files_cmd = "cd " .. vim.fn.shellescape(vault_path) .. " && find . -name '*.md' -type f -not -path '*/.*'" local all_files = vim.fn.systemlist(all_files_cmd) -- Remove ./ prefix from paths @@ -212,8 +211,7 @@ function M.rename_note(new_name, opts) local link_with_display = "%[%[" .. vim.pesc(relative_path) .. "|" -- Get all markdown files - local all_files_cmd = "cd " .. vim.fn.shellescape(vault_path) .. - " && find . -name '*.md' -type f -not -path '*/.*'" + local all_files_cmd = "cd " .. vim.fn.shellescape(vault_path) .. " && find . -name '*.md' -type f -not -path '*/.*'" local all_files = vim.fn.systemlist(all_files_cmd) -- Remove ./ prefix from paths @@ -312,14 +310,11 @@ function M.rename_note(new_name, opts) end -- Helper function to perform the actual rename operation -function M._perform_rename( - current_path, - new_file_path, - files_to_update, - relative_path, - new_relative_path, - update_count -) +function M._perform_rename(current_path, new_file_path, files_to_update, relative_path, new_relative_path, update_count) + -- Check if we're renaming the current buffer's file (before renaming) + local current_bufnr = vim.fn.bufnr("%") + local is_current_buffer = current_bufnr ~= -1 and vim.fn.expand("%:p") == current_path + -- Update all linking files for _, file_info in ipairs(files_to_update) do local file_content = vim.fn.readfile(file_info.path) @@ -344,7 +339,22 @@ function M._perform_rename( -- Rename the actual file local success = vim.fn.rename(current_path, new_file_path) if success == 0 then - vim.cmd("edit " .. vim.fn.fnameescape(new_file_path)) + -- Handle buffer management if we renamed the current buffer's file + if is_current_buffer then + -- Update the buffer's filename to the new path + -- Use pcall to handle E95 error if buffer name already exists + local success_rename = pcall(vim.api.nvim_buf_set_name, current_bufnr, new_file_path) + if not success_rename then + -- If buffer name collision, force close conflicting buffer and retry + local conflicting_bufnr = vim.fn.bufnr(new_file_path) + if conflicting_bufnr ~= -1 then + vim.cmd("bwipeout! " .. conflicting_bufnr) + vim.api.nvim_buf_set_name(current_bufnr, new_file_path) + end + end + -- Reload the buffer to reflect the new filename + vim.cmd("edit!") + end if update_count > 0 then vim.notify("Renamed note and updated " .. update_count .. " files", vim.log.levels.INFO) else