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/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..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 @@ -162,7 +161,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) @@ -211,8 +211,7 @@ function M.rename_note(new_name) 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 @@ -256,18 +255,65 @@ 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?" + -- 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 + + -- 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 rename, Esc to cancel > ", + cwd = vault_path, + previewer = "builtin", + actions = { + ["default"] = function() + -- 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 + ) + end, + }, + }) else - message = message .. "?" + -- 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 - local confirm = vim.fn.confirm(message, "&Yes\n&No", 2) - if confirm ~= 1 then - return - 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) + -- 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 @@ -293,7 +339,22 @@ function M.rename_note(new_name) -- 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 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)