[[obsidian.nvim]]の`ObsidianBacklinks`コマンドを実行して表示された候補一覧に対し、分割した別[[ウィンドウ (Vim)|ウィンドウ]]で開きたい。
## 調査
`commands/backlinks.lua` の57行目関数の実装を確認する。
```lua
---@param client obsidian.Client
return function(client)
```
`callect_backlinks` に入るはず。
```lua
local location, _, ref_type = util.parse_cursor_link { include_block_ids = true }
if
location ~= nil and ...
then
...
else
---@type { anchor: string|?, block: string|? }
local opts = {}
---@type obsidian.note.LoadOpts
local load_opts = {}
if ref_type == RefTypes.BlockID then
opts.block = location
else
load_opts.collect_anchor_links = true
end
local note = client:current_note(0, load_opts)
-- Check if cursor is on a header, if so, use that anchor.
local header_match = util.parse_header(vim.api.nvim_get_current_line())
if header_match then
opts.anchor = header_match.anchor
end
if note == nil then
log.err "Current buffer does not appear to be a note inside the vault"
else
-- ★ここが呼ばれるはず
collect_backlinks(client, picker, note, opts)
end
end
```
候補作成の処理は省略。最終的にpickerが呼び出される。
```lua
---@param client obsidian.Client
---@param picker obsidian.Picker
---@param note obsidian.Note
---@param opts { anchor: string|?, block: string|? }|?
local function collect_backlinks(client, picker, note, opts)
...
vim.schedule(function()
-- ★候補を添えてpickerが呼ばれる
picker:pick(entries, {
prompt_title = prompt_title,
callback = function(value)
-- 決定した候補の値がvalueとして流れてくる
util.open_buffer(value.path, { line = value.line })
end,
})
end)
```
`commands/quick_switch_smart.lua` の場合は `pickers/picker.lua` の `find_notes` を呼び出していたが...
```lua
Picker.find_notes = function(self, opts)
self.calling_bufnr = vim.api.nvim_get_current_buf()
opts = opts or {}
local query_mappings
local selection_mappings
if not opts.no_default_mappings then
query_mappings = self:_note_query_mappings()
selection_mappings = self:_note_selection_mappings()
end
return self:find_files {
prompt_title = opts.prompt_title or "Notes",
dir = self.client.dir,
callback = opts.callback,
no_default_mappings = opts.no_default_mappings,
query_mappings = query_mappings,
selection_mappings = selection_mappings,
recent = opts.recent,
smart = opts.smart,
}
end
```
`commands/backlinks.lua` の場合は `pickers/picker.lua` の `pick` を呼び出しているので実装は[[snacks.picker]]に委ねられているということになる。
```lua
--- Pick from a list of items.
---
---@param values string[]|obsidian.PickerEntry[] Items to pick from.
---@param opts obsidian.PickerPickOpts|? Options.
---
--- Options:
--- `prompt_title`: Title for the prompt window.
--- `callback`: Callback to run with the selected item(s).
--- `allow_multiple`: Allow multiple selections to pass to the callback.
--- `query_mappings`: Mappings that run with the query prompt.
--- `selection_mappings`: Mappings that run with the current selection.
---
---@diagnostic disable-next-line: unused-local
Picker.pick = function(self, values, opts)
error "not implemented"
end
```
`pickers/_snacks.lua` の現実装。
```lua
---@param values string[]|obsidian.PickerEntry[]
---@param opts obsidian.PickerPickOpts|? Options.
---@diagnostic disable-next-line: unused-local
SnacksPicker.pick = function(self, values, opts)
self.calling_bufnr = vim.api.nvim_get_current_buf()
opts = opts or {}
local entries = {}
for _, value in ipairs(values) do
if type(value) == "string" then
table.insert(entries, {
text = value,
value = value,
})
elseif value.valid ~= false then
local name = self:_make_display(value)
table.insert(entries, {
text = name,
file = value.filename,
pos = { value.lnum, value.col or 0 },
value = value.value,
})
end
end
local map = vim.tbl_deep_extend("force", {}, selection_mappings(opts.selection_mappings))
local pick_opts = vim.tbl_extend("force", map or {}, {
title = opts.prompt_title,
items = entries,
layout = {
preview = true,
},
format = "text",
confirm = function(picker, item, action)
picker:close()
if item then
if opts.callback then
opts.callback(item.value)
else
snacks_picker.actions.jump(picker, item, action)
end
end
end,
})
snacks_picker.pick(pick_opts)
end
```
`picker:pick` の呼び出し場所。
```lua
lua/obsidian/commands/dailies.lua
picker:pick(dailies, {
lua/obsidian/commands/backlinks.lua
picker:pick(entries, {
picker:pick_note(notes, {
lua/obsidian/commands/toc.lua
picker:pick(picker_entries, { prompt_title = "Table of Contents" })
lua/obsidian/commands/links.lua
picker:pick(entries, {
lua/obsidian/commands/tags.lua
picker:pick(entries, {
picker:pick_tag(all_tags, {
lua/obsidian/commands/workspace.lua
picker:pick(options, {
```
キーマップを追加すると影響は大きそうだが、実際に利用している機能はbacklinkしかないので支障はなさそう。なので、`commands/backlinks` の45行目で `selection_mappings` を指定するのが一番よさそう。
```lua
vim.schedule(function()
picker:pick(entries, {
prompt_title = prompt_title,
callback = function(value)
util.open_buffer(value.path, { line = value.line })
end,
})
end)
```
## 修正
直前に以下のようなコードを追加。
```lua
local _selection_mappings = {
["<C-CR>"] = {
desc = "edit_vsplit",
callback = function(paths)
for _, path in ipairs(paths) do
vim.cmd [[ vsplit ]]
util.open_buffer(path, { line = 1 })
end
end,
},
}
```
これで `Cmd+Enter` によって垂直分割で新しい[[ウィンドウ (Vim)|ウィンドウ]]に結果が開かれる... が、この実装には問題がある。
### lineが正しくない
カーソル行が正しくない。
`selection_mappings` の型定義をちゃんと確認する。
`pickers/picker.lua`
```lua
---@class obsidian.PickerMappingOpts
---
---@field desc string
---@field callback fun(...)
---@field fallback_to_query boolean|?
---@field keep_open boolean|?
---@field allow_multiple boolean|?
---@alias obsidian.PickerMappingTable table<string, obsidian.PickerMappingOpts>
.
.
---@field selection_mappings obsidian.PickerMappingTable|?
```
```lua
{
["C-CR"] = ...
}
```
の右辺に指定できるのは以下の[[テーブル (Lua)|テーブル]]。
```lua
---@field desc string
---@field callback fun(...)
---@field fallback_to_query boolean|?
---@field keep_open boolean|?
---@field allow_multiple boolean|?
```
今回は `callback` の中身が知りたい。`callback` は関数ということしかここでは分からないため、中身の実装は `_snacks.lua` に委ねられている。つまり
```lua
v.callback(vim.tbl_map(function(i)
return i._path or i.value
end, items))
```
なので **すべての選択されたitemからpathのみを抽出** している。
```lua
local paths = vim.tbl_map(function(i)
return i._path or i.value
end, items)
v.callback(paths)
```
### callbackに隠し第2引数としてitemを渡す
他が **pathであることを前提に** 実装されているため、後ろに引数を追加するほうが安全そう。
```lua
local function selection_mappings(mapping)
if type(mapping) == "table" then
local opts = { win = { input = { keys = {} } }, actions = {} }
for k, v in pairs(mapping) do
local name = string.gsub(v.desc, " ", "_")
opts.win.input.keys = {
[k] = { name, mode = { "n", "i" }, desc = v.desc },
}
opts.actions[name] = function(picker, item)
picker:close()
local items = picker:selected { fallback = true }
vim.schedule(function()
local paths = vim.tbl_map(function(i)
return i._path or i.value
end, items)
-- ★itemsも引数に追加
v.callback(paths, items)
end)
end
end
return opts
end
return {}
end
```
`items` の値は `pickers/_snacks.lua` の `PickerEntry` の要素。
```lua
---@class obsidian.PickerEntry
---
---@field value any
---@field ordinal string|?
---@field display string|?
---@field filename string|?
---@field valid boolean|?
---@field lnum integer|?
---@field col integer|?
---@field icon string|?
---@field icon_hl string|?
```
`value` が `any` となっているが、これが肝。`value.path` と `value.line` が必要な情報となる。以下のように `selection_mappings` を直接ハードコーディングする。
`commands/backlinks.lua`
```lua
-- ★追加
-- 設定に外だしが不要なのでハードコーディング(必要なら切り分けが必要)
local _selection_mappings = {
["<C-CR>"] = {
desc = "edit_vsplit",
callback = function(_, items)
for _, item in ipairs(items) do
vim.cmd [[ vsplit ]]
util.open_buffer(item.value.path, { line = item.value.line })
end
end,
},
["<C-s>"] = {
desc = "edit_split",
callback = function(_, items)
for _, item in ipairs(items) do
vim.cmd [[ split ]]
util.open_buffer(item.value.path, { line = item.value.line })
end
end,
},
}
vim.schedule(function()
picker:pick(entries, {
prompt_title = prompt_title,
selection_mappings = _selection_mappings,
callback = function(value)
util.open_buffer(value.path, { line = value.line })
end,
})
end)
```
設定できるようにしたい場合は `pickers/picker.lua` に対応が必要だが、forkしたリポジトリを使っているうちはその必要はなさそう。
## 関連コミット
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" />
<span class="link-card-v2-site-name">GitHub</span>
</div>
<div class="link-card-v2-title">
feat: Add keymaps for opening in split and vsplit in ObsidianBacklink… · tadashi-aikawa/obsidian.nvim@d989540
</div>
<div class="link-card-v2-content">
…s (non-customizable)
</div>
<img class="link-card-v2-image" src="https://opengraph.githubassets.com/4ccb9ac376ef3a03e821885f856e1fd36993756e26e7eafbd649a2639db25b45/tadashi-aikawa/obsidian.nvim/commit/d9895400b3ab0c367d8517f9682df4076289a370" />
<a href="https://github.com/tadashi-aikawa/obsidian.nvim/commit/d9895400b3ab0c367d8517f9682df4076289a370"></a>
</div>