## 経緯 [[obsidian.nvim]]では、`ObsidianRename`コマンドを実行したときデフォルトの入力は空になっている。しかし、実際にリネームしたいときは現在のファイル名の一部を変更することが多いので不便。この挙動を変更したい。 ## 現行実装 `commands/rename.lua`の27行目より。 ```lua arg = util.input("Enter new note ID/name/path: ", { completion = "file" }) ``` `util.input`の実装。 ```lua --- Prompt user for an input. Returns nil if canceled, otherwise a string (possibly empty). --- ---@param prompt string ---@param opts { completion: string|?, default: string|? }|? --- ---@return string|? util.input = function(prompt, opts) opts = opts or {} if not vim.endswith(prompt, " ") then prompt = prompt .. " " end local input = util.strip_whitespace( vim.fn.input { prompt = prompt, completion = opts.completion, default = opts.default, cancelreturn = INPUT_CANCELLED } ) if input ~= INPUT_CANCELLED then return input else return nil end end ``` `opts.default` がデフォルト値として入るので、それを指定してあげればよさそう。実際に動くことも確認した。 ## 改修の注意点 `ObsidianRename`には2通りの挙動があることを念頭に置く必要がある。 1. カーソル配下のリンクに対するRename 2. カレントバッファのファイルに対するRename バックエンドではこれらのデータがbindされているが、フロントエンドには出てきていない... というのであれば大きな問題はない。対象ファイルをデフォルト値にぶち込めばOK。 変更処理で参照するファイルが、今回デフォルトで挿入する名称を持っている可能性が高いのでそちらを調べる。 ## 処理のコード 恐らくここ。 ```lua -- Resolve the note to rename. ---@type boolean local is_current_buf ---@type integer|? local cur_note_bufnr ---@type obsidian.Path local cur_note_path ---@type obsidian.Note local cur_note local cur_note_id = util.parse_cursor_link() if cur_note_id == nil then is_current_buf = true cur_note_bufnr = assert(vim.fn.bufnr()) cur_note_path = Path.buffer(cur_note_bufnr) cur_note = Note.from_file(cur_note_path) cur_note_id = tostring(cur_note.id) else local notes = { client:resolve_note(cur_note_id) } if #notes == 0 then log.err("Failed to resolve '%s' to a note", cur_note_id) return elseif #notes > 1 then log.err("Failed to resolve '%s' to a single note, found %d matches", cur_note_id, #notes) return else cur_note = notes[1] end is_current_buf = false cur_note_id = tostring(cur_note.id) cur_note_path = cur_note.path for bufnr, bufpath in util.get_named_buffers() do if bufpath == cur_note_path then cur_note_bufnr = bufnr break end end end ``` if trueが先ほど話した1のケース、if falseが2のケースとなる。いずれにしろ怪しいのは `cur_note_id` 。 なお、この処理ではそれまでに定義されたlocal変数を利用していない。そのため、この処理を先に持ってくることにより、`cur_note_id` の情報をプロンプトのデフォルト値に入れ込むことができるはずだ。 ## やってみる 先ほどのif-else文の下にある以下2行までまとめて入れ替える。 ```lua assert(cur_note_path) local dirname = assert(cur_note_path:parent(), string.format("failed to resolve parent of '%s'", cur_note_path)) ``` そして先ほどのコードにdefault値を追加。 ```diff - arg = util.input("Enter new note ID/name/path: ", { completion = "file" }) + arg = util.input("Enter new note ID/name/path: ", { completion = "file", default = cur_note_id }) ``` 基本的にうまくいったが問題もある。 ## フロントマターが追加される カーソル配下のリネームをしたときだけ、リネーム後のファイルに[[フロントマター]]が挿入されてしまう。 ```lua --- id: tagayasu-180 aliases: [] tags: [] --- tagayasu ``` この動きには見覚えがある。以前に対応した **新規ファイルでヘッダが追加される問題** に似ている。もしかしたら対応漏れ or 同じような方法でいけるかもしれない。 <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <p class="link-card-title">Comment out new file header · tadashi-aikawa/obsidian.nvim@180d537</p> <p class="link-card-description">Obsidian 🤝 Neovim. Contribute to tadashi-aikawa/obsidian.nvim development by creating an account on GitHub.</p> </div> <img src="https://opengraph.githubassets.com/3601b7cc66aa91866d83aa23332e5e75bcdbc589b864188b760168e9f86b998d/tadashi-aikawa/obsidian.nvim/commit/180d537fbe90488c45991a4945b2ccec0b6b9720" class="link-card-image" /> </div> <a href=" https://github.com/tadashi-aikawa/obsidian.nvim/commit/180d537fbe90488c45991a4945b2ccec0b6b9720"></a> </div> 以前にコメントアウトしたのは2つ。 - `note.lua` > `save_to_buffer()` の 766行目 - `note.lua` > `save()` の 715行目 今回は[[フロントマター]]なので... 怪しい箇所 ```lua lua/obsidian/note.lua ┃436┃table.insert(frontmatter_lines, line) lua/obsidian/note.lua ┃710┃ table.insert(existing_frontmatter, line) ``` 710行目が `save()` なのでこっちの方が怪しいかも...。 ```lua if not in_frontmatter and not at_boundary then table.insert(content, line) else table.insert(existing_frontmatter, line) end ``` ### 逆方向を見る 再び `rename.lua` の方に戻る。リネーム処理の肝は以下。コメントで補足。 ```lua -- ****************************** -- 現在のバッファがバッファリストにある -- ****************************** if cur_note_bufnr ~= nil then -- 現在のバッファに対するリネーム (カーソル配下ではない) -- >> 現在バッファのリネームは問題がないのでここはSKIP if is_current_buf then -- If we're renaming the note of a current buffer, save as the new path. if not dry_run then -- ★ここから処理! quietly(vim.cmd.saveas, tostring(new_note_path)) for bufnr, bufname in util.get_named_buffers() do if bufname == cur_note_path then quietly(vim.cmd.bdelete, bufnr) end end vim.fn.delete(tostring(cur_note_path)) else log.info("Dry run: saving current buffer as '" .. tostring(new_note_path) .. "' and removing old file") end -- カーソル配下のリンク先に対するリネーム else -- For the non-current buffer the best we can do is delete the buffer (we've already saved it above) -- and then make a file-system call to rename the file. if not dry_run then -- ★ここから処理! quietly(vim.cmd.bdelete, cur_note_bufnr) -- ★★ この先は一体...?? cur_note_path:rename(new_note_path) else log.info( "Dry run: removing buffer '" .. tostring(cur_note_path) .. "' and renaming file to '" .. tostring(new_note_path) .. "'" ) end end -- ****************************** -- 現在のバッファがバッファリストにない -- ****************************** else -- When the note is not loaded into a buffer we just need to rename the file. if not dry_run then -- ★★ この先は一体...?? cur_note_path:rename(new_note_path) else log.info("Dry run: renaming file '" .. tostring(cur_note_path) .. "' to '" .. tostring(new_note_path) .. "'") end end ``` ### cur_note_path:renameの先を見る ここからが一番怪しいのでしっかり見ていく。 ```lua --- Rename this file or directory to the given target. --- ---@param target obsidian.Path|string --- ---@return obsidian.Path Path.rename = function(self, target) local resolved = self:resolve { strict = false } target = Path.new(target) local ok, err_name, err_msg = vim.loop.fs_rename(resolved.filename, target.filename) if not ok then error(err_name .. ": " .. err_msg) end return target end ``` ただ、このコードを見る限り、[[フロントマター]]には関係していなそうに見える...。ここじゃないか? ### その先に... `rename` 呼び出している箇所のすぐ先にあった。。 ```lua if not is_current_buf then -- When the note to rename is not the current buffer we need to update its frontmatter -- to account for the rename. cur_note.id = new_note_id cur_note.path = Path.new(new_note_path) if not dry_run then cur_note:save() else log.info("Dry run: updating frontmatter of '" .. tostring(new_note_path) .. "'") end end ``` やはり `cur_note:save()` である。 ### 再びsave 先ほどの箇所。 > 710行目が `save()` なのでこっちの方が怪しいかも...。 > > ```lua > if not in_frontmatter and not at_boundary then > table.insert(content, line) > else > table.insert(existing_frontmatter, line) > end > ``` この続きを調べる。今回の呼び出しでは引数 `opts` は空である。 ```lua --- Save the note to a file. --- In general this only updates the frontmatter and header, leaving the rest of the contents unchanged --- unless you use the `update_content()` callback. --- ---@param opts { path: string|obsidian.Path|?, insert_frontmatter: boolean|?, frontmatter: table|?, update_content: (fun(lines: string[]): string[])|? }|? Options. --- --- Options: --- - `path`: Specify a path to save to. Defaults to `self.path`. --- - `insert_frontmatter`: Whether to insert/update frontmatter. Defaults to `true`. --- - `frontmatter`: Override the frontmatter. Defaults to the result of `self:frontmatter()`. --- - `update_content`: A function to update the contents of the note. This takes a list of lines --- representing the text to be written excluding frontmatter, and returns the lines that will --- actually be written (again excluding frontmatter). Note.save = function(self, opts) opts = opts or {} if self.path == nil and opts.path == nil then error "a path is required" end local save_path = Path.new(assert(opts.path or self.path)):resolve() assert(save_path:parent()):mkdir { parents = true, exist_ok = true } -- Read contents from existing file or buffer, if there is one. -- TODO: check for open buffer? ---@type string[] local content = {} ---@type string[] local existing_frontmatter = {} if self.path ~= nil and self.path:is_file() then with(open(tostring(self.path)), function(reader) local in_frontmatter, at_boundary = false, false -- luacheck: ignore (false positive) for idx, line in enumerate(reader:lines()) do if idx == 1 and Note._is_frontmatter_boundary(line) then at_boundary = true in_frontmatter = true elseif in_frontmatter and Note._is_frontmatter_boundary(line) then at_boundary = true in_frontmatter = false else at_boundary = false end if not in_frontmatter and not at_boundary then table.insert(content, line) else table.insert(existing_frontmatter, line) end end end) elseif self.title ~= nil then -- Add a header. -- 不要なのでコメントアウト -- table.insert(content, "# " .. self.title) end -- Pass content through callback. if opts.update_content then content = opts.update_content(content) end ---@type string[] local new_lines if opts.insert_frontmatter ~= false then -- Replace frontmatter. new_lines = compat.flatten { self:frontmatter_lines(false, opts.frontmatter), content } else -- Use existing frontmatter. new_lines = compat.flatten { existing_frontmatter, content } end -- Write new lines. with(open(tostring(save_path), "w"), function(writer) for _, line in ipairs(new_lines) do writer:write(line .. "\n") end end) end ``` コメントを補填しつつ理解する。 ```lua ---@type string[] local content = {} -- フロントマターでない部分のlineテーブル ---@type string[] local existing_frontmatter = {} -- フロントマター部分のlineテーブル if self.path ~= nil and self.path:is_file() then -- pathのファイルを読み込みreaderを渡す with(open(tostring(self.path)), function(reader) local in_frontmatter, at_boundary = false, false -- luacheck: ignore (false positive) -- 1行ずつイテレート for idx, line in enumerate(reader:lines()) do -- 現在行が『フロントマター』か? 『境界』か? をチェック if idx == 1 and Note._is_frontmatter_boundary(line) then at_boundary = true in_frontmatter = true elseif in_frontmatter and Note._is_frontmatter_boundary(line) then at_boundary = true in_frontmatter = false else at_boundary = false end if not in_frontmatter and not at_boundary then -- フロントマターでもないし境界でもない -> 本文 -> contentに追加 table.insert(content, line) else -- フロントマターの一部 -> existing_frontmatter に追加 table.insert(existing_frontmatter, line) end end end) ``` あとは `existing_frontmatter` を挿入しないようにすれば平気そう... だが、既にこのファイルにフロントマターがある場合は大丈夫かが気になる。消えたりしない? 恐らくこの部分で追加してそう。 ```lua ---@type string[] local new_lines -- ★ opts = {} なので opts.insert_frontmatter ~= false は nil ~= false となり trueか? if opts.insert_frontmatter ~= false then -- Replace frontmatter. -- ★ ということはコッチにくる... new_lines = compat.flatten { self:frontmatter_lines(false, opts.frontmatter), content } else -- Use existing frontmatter. new_lines = compat.flatten { existing_frontmatter, content } end ``` `self:frontmatter_lines(...)` がフロントマターを作成しているのでシンプルに `content` を渡してあげればよい。 ```lua if opts.insert_frontmatter ~= false then new_lines = content else -- Use existing frontmatter. new_lines = compat.flatten { existing_frontmatter, content } end ``` しかし、これだと**リネームしたファイルにフロントマターが含まれていると、フロントマターが消えてしまう**。一方、以前のコードに戻しても、`id` `aliases` `tags` は必ず割当たるようになっている。 ### よく見てみると... 冷静に見返してみる... と `insert_frontmatter` が `false` ならelseの方に処理がいく。 ```lua if opts.insert_frontmatter ~= false then new_lines = compat.flatten { self:frontmatter_lines(false, opts.frontmatter), content } else -- ★こっち new_lines = compat.flatten { existing_frontmatter, content } end ``` その場合は `existing_frontmatter` が挿入されるが、**既存のファイル内にフロントマターがあれば、それがそのまま入るし、なければ何も入らない**。なので、期待する動作になりそう。 ゆえに、呼び出し元を変える。 > `rename` 呼び出している箇所のすぐ先にあった。。 > > ```lua > if not is_current_buf then > -- When the note to rename is not the current buffer we need to update its frontmatter > -- to account for the rename. > cur_note.id = new_note_id > cur_note.path = Path.new(new_note_path) > if not dry_run then > cur_note:save() > else > log.info("Dry run: updating frontmatter of '" .. tostring(new_note_path) .. "'") > end > end > ``` ここの `cur_note:save()` を変更する。 ```diff if not dry_run then - cur_note:save() + cur_note:save { insert_frontmatter = false } else ``` これで期待通り動いた! ## 変更した内容 まとめ 変更したファイルは `commands/renam.lua` だけ。 - `ObsidianRename` コマンドの入力欄に現在ノートのタイトルをデフォルト値として入れる - `cur_note_id` などの生成処理ブロックをfunctionの先頭に移動 - 先に生成した `cur_note_id` を `uti.input` のオプション `default` に指定 - `ObsidianRename` コマンドでカーソル下のリンク先のノートタイトルを変更しても、変更したノートの先頭に[[フロントマター]]を挿入しない - `cur_note:save` のオプションに `insert_frontmatter = false` を指定 <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <p class="link-card-title">feat: Set the current note title as the default value in the input fi… · tadashi-aikawa/obsidian.nvim@2ac9cbb</p> <p class="link-card-description">…eld for the ObsidianRename ... </p> </div> <img src="https://opengraph.githubassets.com/6182e43556051d4a05553a7075c5c841827da0463e818ccf7828c3952ebace1c/tadashi-aikawa/obsidian.nvim/commit/2ac9cbbeb13fdaa5fb73aa02a4a9c8604998623f" class="link-card-image" /> </div> <a href="https://github.com/tadashi-aikawa/obsidian.nvim/commit/2ac9cbbeb13fdaa5fb73aa02a4a9c8604998623f"></a> </div> <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <p class="link-card-title">feat!: When renaming the note linked under the cursor with the Obsidi… · tadashi-aikawa/obsidian.nvim@d5220bf</p> <p class="link-card-description">…anRename command, do not in ... </p> </div> <img src="https://opengraph.githubassets.com/43d76b4d0b55ab44293a36b83c476ea0c0809cf2c8f8f0df253a0d35b8731533/tadashi-aikawa/obsidian.nvim/commit/d5220bf6765f0a01411141b2bb06798e60a5947e" class="link-card-image" /> </div> <a href="https://github.com/tadashi-aikawa/obsidian.nvim/commit/d5220bf6765f0a01411141b2bb06798e60a5947e"></a> </div>