[[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>