## 経緯
[[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>