Skip to content

Commit

Permalink
Replace loclist with picker for backlinks and tags (#415)
Browse files Browse the repository at this point in the history
* Open picker for backlinks/tags location

Instead of a "location list" buffer. Closes #374.

* Improve filter field ("ordinal")

* Consolidate code to open a buffer

* improve the `open_buffer` command

* Standardize how we resolve paths

* Fix test

* Revert change to `:vault_relative_path()`

* choose picker on the fly

* fix typo

* Fix another bug with `Executor:map()`

* Add icons and highlights

* Make icons more robust
  • Loading branch information
epwalsh authored Feb 20, 2024
1 parent 23e987d commit 7bee190
Show file tree
Hide file tree
Showing 21 changed files with 275 additions and 455 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added client methods `Client:find_backlinks()` and `Client:find_backlinks_async()`.

### Changed

- `:ObsidianBacklinks` and `:ObsidianTags` now open your preferred picker instead of a separate buffer.

### Fixed

- Fixed `:ObsidianExtractNote` when usual visual line selection ("V").
Expand Down
20 changes: 2 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ _Keep in mind this plugin is not meant to replace Obsidian, but to complement it

- `:ObsidianFollowLink [vsplit|hsplit]` to follow a note reference under the cursor, optionally opening it in a vertical or horizontal split.

- `:ObsidianBacklinks` for getting a location list of references to the current buffer.
- `:ObsidianBacklinks` for getting a picker list of references to the current buffer.

- `:ObsidianTags [TAG ...]` for getting a location list of all occurrences of the given tags.
- `:ObsidianTags [TAG ...]` for getting a picker list of all occurrences of the given tags.

- `:ObsidianToday [OFFSET]` to open/create a new daily note. This command also takes an optional offset in days, e.g. use `:ObsidianToday -1` to go to yesterday's note. Unlike `:ObsidianYesterday` and `:ObsidianTomorrow` this command does not differentiate between weekdays and weekends.

Expand Down Expand Up @@ -367,22 +367,6 @@ This is a complete list of all of the options that can be passed to `require("ob
substitutions = {},
},

-- Optional, customize the backlinks interface.
backlinks = {
-- The default height of the backlinks location list.
height = 10,
-- Whether or not to wrap lines.
wrap = true,
},

-- Optional, customize the tags interface.
tags = {
-- The default height of the tags location list.
height = 10,
-- Whether or not to wrap lines.
wrap = true,
},

-- Optional, by default when you use `:ObsidianFollowLink` on a link to an external
-- URL it will be ignored but you can customize this behavior here.
---@param url string
Expand Down
10 changes: 7 additions & 3 deletions lua/obsidian/async.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,21 @@ Executor.map = function(self, fn, task_args, callback)
if type(task_args) == "table" and util.tbl_is_array(task_args) then
num_tasks = #task_args
for i, args in ipairs(task_args) do
if i == #task_args then
all_submitted = true
end
self:submit(fn, get_task_done_fn(i), unpack(args))
end
all_submitted = true
elseif type(task_args) == "table" then
num_tasks = vim.tbl_count(task_args)
local i = 0
for k, v in pairs(task_args) do
i = i + 1
num_tasks = num_tasks + 1
if i == #task_args then
all_submitted = true
end
self:submit(fn, get_task_done_fn(i), k, v)
end
all_submitted = true
elseif type(task_args) == "function" then
local i = 0
local args = { task_args() }
Expand Down
31 changes: 18 additions & 13 deletions lua/obsidian/client.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
--- *obidian-api*
--- *obsidian-api*
---
--- The Obsidian.nvim Lua API.
---
Expand Down Expand Up @@ -178,7 +178,7 @@ end
---
---@return boolean
Client.path_is_note = function(self, path, workspace)
path = vim.fs.normalize(tostring(path))
path = util.resolve_path(path)

-- Notes have to be markdown file.
if not vim.endswith(path, ".md") then
Expand Down Expand Up @@ -223,6 +223,9 @@ end
---
---@return string|?
Client.vault_relative_path = function(self, path)
-- NOTE: we don't use `util.resolve_path()` here because that would make the path absolute,
-- which may result in the wrong relative path if the current working directory is not within
-- the vault.
local normalized_path = vim.fs.normalize(tostring(path))
local relative_path = Path:new(normalized_path):make_relative(tostring(self:vault_root()))
if relative_path == normalized_path then
Expand Down Expand Up @@ -378,7 +381,7 @@ Client._search_iter_async = function(self, term, search_opts, find_opts)

---@param content_match MatchData
local function on_search_match(content_match)
local path = vim.fs.normalize(content_match.path.text)
local path = util.resolve_path(content_match.path.text)
if not found[path] then
found[path] = true
tx.send(path)
Expand All @@ -387,7 +390,7 @@ Client._search_iter_async = function(self, term, search_opts, find_opts)

---@param path_match string
local function on_find_match(path_match)
local path = vim.fs.normalize(path_match)
local path = util.resolve_path(path_match)
if not found[path] then
found[path] = true
tx.send(path)
Expand Down Expand Up @@ -508,7 +511,7 @@ Client.find_files_async = function(self, term, opts, callback)
local matches = {}
local tx, rx = channel.oneshot()
local on_find_match = function(path_match)
matches[#matches + 1] = Path:new(vim.fs.normalize(path_match))
matches[#matches + 1] = Path:new(util.resolve_path(path_match))
end

local on_exit = function(_)
Expand Down Expand Up @@ -702,7 +705,7 @@ Client.follow_link_async = function(self, link, opts)
-- Go to resolved note.
local path = assert(res.path)
return vim.schedule(function()
vim.api.nvim_command(open_cmd .. tostring(path))
util.open_buffer(path, { cmd = open_cmd })
end)
end

Expand All @@ -724,7 +727,7 @@ Client.follow_link_async = function(self, link, opts)
end

local note = self:new_note(res.name, id, nil, aliases)
vim.api.nvim_command(open_cmd .. tostring(note.path))
util.open_buffer(note.path, { cmd = open_cmd })
else
log.warn "Aborting"
end
Expand Down Expand Up @@ -829,7 +832,7 @@ Client.find_tags_async = function(self, term, opts, callback)

---@param match_data MatchData
local on_match = function(match_data)
local path = vim.fs.normalize(match_data.path.text)
local path = util.resolve_path(match_data.path.text)

if path_order[path] == nil then
num_paths = num_paths + 1
Expand Down Expand Up @@ -1016,7 +1019,7 @@ Client.find_backlinks_async = function(self, note, opts, callback)
end

local function on_match(match)
local path = vim.fs.normalize(match.path.text)
local path = util.resolve_path(match.path.text)

if path_order[path] == nil then
num_paths = num_paths + 1
Expand Down Expand Up @@ -1160,7 +1163,7 @@ Client.apply_async_raw = function(self, on_path, on_done, timeout)

local executor = AsyncExecutor.new()

scan.scan_dir(vim.fs.normalize(tostring(self.dir)), {
scan.scan_dir(util.resolve_path(self.dir), {
hidden = false,
add_dirs = false,
respect_gitignore = true,
Expand Down Expand Up @@ -1487,11 +1490,13 @@ Client.format_link = function(self, note, opts)
end
end

--- Get the default Picker.
--- Get the Picker.
---
---@param picker_name obsidian.config.Picker|?
---
---@return obsidian.Picker|?
Client.picker = function(self)
return require("obsidian.pickers").get(self)
Client.picker = function(self, picker_name)
return require("obsidian.pickers").get(self, picker_name)
end

return Client
108 changes: 32 additions & 76 deletions lua/obsidian/commands/backlinks.lua
Original file line number Diff line number Diff line change
@@ -1,85 +1,16 @@
local util = require "obsidian.util"
local log = require "obsidian.log"
local search = require "obsidian.search"
local RefTypes = require("obsidian.search").RefTypes
local LocationList = require "obsidian.location_list"
local Note = require "obsidian.note"
local iter = require("obsidian.itertools").iter

local NAMESPACE = "ObsidianBacklinks"

---@param client obsidian.Client
---@param note obsidian.Note
local function gather_backlinks_location_list(client, note)
client:find_backlinks_async(note, true, function(backlink_matches)
if vim.tbl_isempty(backlink_matches) then
log.warn("No backlinks to '%s'", note.id)
return
end

local loclist = LocationList.new(client, vim.fn.bufnr(), vim.fn.winnr(), NAMESPACE, client.opts.backlinks)

local view_lines = {}
local highlights = {}
local folds = {}

for match in iter(backlink_matches) do
-- Header for note.
view_lines[#view_lines + 1] = (" %s"):format(match.note:display_name())
highlights[#highlights + 1] = { group = "CursorLineNr", line = #view_lines - 1, col_start = 0, col_end = 1 }
highlights[#highlights + 1] = { group = "Directory", line = #view_lines - 1, col_start = 2, col_end = -1 }

local display_path = assert(client:vault_relative_path(match.note.path))

-- Line for backlink within note.
for line_match in iter(match.matches) do
local text, ref_indices, ref_strs = search.find_and_replace_refs(line_match.text)
local text_start = 4 + display_path:len() + tostring(line_match.line):len()
view_lines[#view_lines + 1] = (" %s:%s:%s"):format(display_path, line_match.line, text)

-- Add highlights for all refs in the text.
for i, ref_idx in ipairs(ref_indices) do
local ref_str = ref_strs[i]
if string.find(ref_str, tostring(note.id), 1, true) ~= nil then
highlights[#highlights + 1] = {
group = "Search",
line = #view_lines - 1,
col_start = text_start + ref_idx[1] - 1,
col_end = text_start + ref_idx[2],
}
end
end

-- Add highlight for path and line number
highlights[#highlights + 1] = {
group = "Comment",
line = #view_lines - 1,
col_start = 2,
col_end = text_start,
}
end

folds[#folds + 1] = { range = { #view_lines - #match.matches, #view_lines } }
view_lines[#view_lines + 1] = ""
end

-- Remove last blank line.
view_lines[#view_lines] = nil

loclist:render(view_lines, folds, highlights)

log.info(
"Showing backlinks to '%s'.\n\n"
.. "TIPS:\n\n"
.. "- Hit ENTER on a match to follow the backlink\n"
.. "- Hit ENTER on a group header to toggle the fold, or use normal fold mappings",
note.id
)
end)
end

---@param client obsidian.Client
return function(client, _)
local picker = assert(client:picker())
if not picker then
log.err "No picker configured"
return
end

---@type obsidian.Note|?
local note
local cursor_link, _, ref_type = util.parse_cursor_link()
Expand All @@ -95,5 +26,30 @@ return function(client, _)

assert(note)

return gather_backlinks_location_list(client, note)
client:find_backlinks_async(note, true, function(backlinks)
if vim.tbl_isempty(backlinks) then
log.info "No backlinks found"
return
end

local entries = {}
for _, matches in ipairs(backlinks) do
for _, match in ipairs(matches.matches) do
entries[#entries + 1] = {
value = { path = matches.path, line = match.line },
filename = matches.path,
lnum = match.line,
}
end
end

vim.schedule(function()
picker:pick(entries, {
prompt_title = "Backlinks",
callback = function(value)
util.open_buffer(value.path, { line = value.line })
end,
})
end)
end)
end
11 changes: 9 additions & 2 deletions lua/obsidian/commands/links.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local AsyncExecutor = require("obsidian.async").AsyncExecutor
local log = require "obsidian.log"
local search = require "obsidian.search"
local iter = require("obsidian.itertools").iter
local util = require "obsidian.util"
local channel = require("plenary.async.control").channel

---@param client obsidian.Client
Expand All @@ -15,7 +16,7 @@ return function(client)
---@type (string[])[]
local links = {}
for line in iter(vim.api.nvim_buf_get_lines(0, 0, -1, true)) do
for match in iter(search.find_refs(line, { include_naked_urls = true })) do
for match in iter(search.find_refs(line, { include_naked_urls = true, include_file_urls = true })) do
local m_start, m_end = unpack(match)
local link = string.sub(line, m_start, m_end)
links[#links + 1] = { link }
Expand All @@ -30,12 +31,18 @@ return function(client)
local entry

client:resolve_link_async(link, function(res)
local icon, icon_hl
if res.url ~= nil then
icon, icon_hl = util.get_icon(res.url)
end

if res ~= nil then
entry = {
value = link,
display = res.name,
ordinal = res.name,
filename = res.path and tostring(res.path) or nil,
icon = icon,
icon_hl = icon_hl,
}
else
entry = {
Expand Down
6 changes: 3 additions & 3 deletions lua/obsidian/commands/rename.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ return function(client, data)
if cur_note_id == nil then
is_current_buf = true
cur_note_bufnr = assert(vim.fn.bufnr())
cur_note_path = vim.fs.normalize(vim.api.nvim_buf_get_name(cur_note_bufnr))
cur_note_path = util.resolve_path(vim.api.nvim_buf_get_name(cur_note_bufnr))
cur_note = Note.from_file(cur_note_path)
cur_note_id = tostring(cur_note.id)
dirname = vim.fs.dirname(cur_note_path)
Expand All @@ -55,7 +55,7 @@ return function(client, data)
return
end
cur_note_id = tostring(cur_note.id)
cur_note_path = vim.fs.normalize(tostring(cur_note.path:absolute()))
cur_note_path = util.resolve_path(cur_note.path)
dirname = vim.fs.dirname(cur_note_path)
for bufnr, bufpath in util.get_named_buffers() do
if bufpath == cur_note_path then
Expand Down Expand Up @@ -252,7 +252,7 @@ return function(client, data)
end

local function on_search_match(match)
local path = vim.fs.normalize(match.path.text)
local path = util.resolve_path(match.path.text)
file_count = file_count + 1
executor:submit(replace_refs, function(count)
replacement_count = replacement_count + count
Expand Down
Loading

0 comments on commit 7bee190

Please sign in to comment.