initial support for #36 : pickers can now show the number of links and

backlinks if opts.show_link_counts is set. Which it is for the 'link
search' following the tag selection.
This commit is contained in:
Rene Schallner
2021-12-13 07:38:51 +01:00
parent 0803d54f62
commit 89191a1c84
3 changed files with 468 additions and 40 deletions

135
lua/taglinks/linkutils.lua Normal file
View File

@@ -0,0 +1,135 @@
-- local async = require("plenary.async")
local scan = require("plenary.scandir")
local M = {}
local function file_exists(fn, file_list)
return file_list[fn] ~= nil
end
local function resolve_link(title, file_list, subdir_list, opts)
local fexists = false
local filename = title .. opts.extension
filename = filename:gsub("^%./", "") -- strip potential leading ./
if
opts.weeklies
and file_exists(opts.weeklies .. "/" .. filename, file_list)
then
filename = opts.weeklies .. "/" .. filename
fexists = true
end
if
opts.dailies and file_exists(opts.dailies .. "/" .. filename, file_list)
then
filename = opts.dailies .. "/" .. filename
fexists = true
end
if file_exists(opts.home .. "/" .. filename, file_list) then
filename = opts.home .. "/" .. filename
fexists = true
end
if fexists == false then
-- now search for it in all subdirs
local tempfn
for _, folder in pairs(subdir_list) do
tempfn = folder .. "/" .. filename
-- [[testnote]]
if file_exists(tempfn, file_list) then
filename = tempfn
fexists = true
-- print("Found: " .. filename)
break
end
end
end
if fexists == false then
-- default fn for creation
filename = opts.home .. "/" .. filename
end
return fexists, filename
end
-- TODO: cache mtimes and only update if changed
-- The reason we go over all notes in one go, is: backlinks
-- We generate 2 maps: one containing the number of links within a note
-- and a second one containing the number of backlinks to a note
-- Since we're parsing all notes anyway, we can mark linked notes as backlinked from the currently parsed note
M.generate_backlink_map = function(opts)
assert(opts ~= nil, "opts must not be nil")
-- TODO: check for code blocks
-- local in_fenced_code_block = false
-- also watch out for \t tabbed code blocks or ones with leading spaces that don't end up in a - or * list
-- first, find all notes
assert(opts.extension ~= nil, "Error: need extension in opts!")
assert(opts.home ~= nil, "Error: need home dir in opts!")
-- async seems to have lost await and we don't want to enter callback hell, hence we go sync here
local subdir_list = scan.scan_dir(opts.home, { only_dirs = true })
local file_list = {}
-- transform the file list
local _x = scan.scan_dir(opts.home, {
search_pattern = function(entry)
return entry:sub(-#opts.extension) == opts.extension
end,
})
for _, v in pairs(_x) do
file_list[v] = true
end
-- now process all the notes
local link_counts = {}
local backlink_counts = {}
for note_fn, _ in pairs(file_list) do
-- print("processing " .. note_fn .. "...")
-- go over file line by line
for line in io.lines(note_fn) do
for linktitle in line:gmatch("%[%[(.-)%]%]") do
-- strip # from title
linktitle = linktitle:gsub("#.*$", "")
-- now: inc our link count
link_counts[note_fn] = link_counts[note_fn] or 0
link_counts[note_fn] = link_counts[note_fn] + 1
-- and: inc the backlinks of the linked note
local fexists, backlinked_file = resolve_link(
linktitle,
file_list,
subdir_list,
opts
)
-- print(
-- "note for link `"
-- .. linktitle
-- .. "` = "
-- .. backlinked_file
-- .. " (exists: "
-- .. tostring(fexists)
-- .. ')'
-- )
if fexists and (note_fn ~= backlinked_file) then
backlink_counts[backlinked_file] = backlink_counts[backlinked_file]
or 0
backlink_counts[backlinked_file] = backlink_counts[backlinked_file]
+ 1
end
end
end
-- check if in comments block
-- find all links in the note and count them
-- add 1 (this note) as back-link to linked note
end
local ret = {
link_counts = link_counts,
backlink_counts = backlink_counts,
}
return ret
end
return M

View File

@@ -21,6 +21,7 @@ local function command_find_all_tags(opts)
return "rg", { "--vimgrep", "-o", re, "--", opts.cwd }
end
-- strips away leading ' or " , then trims whitespace
local function trim(s)
if s:sub(1, 1) == '"' or s:sub(1, 1) == "'" then
s = s:sub(2)

View File

@@ -9,11 +9,15 @@ local scan = require("plenary.scandir")
local utils = require("telescope.utils")
local previewers = require("telescope.previewers")
local make_entry = require("telescope.make_entry")
local entry_display = require("telescope.pickers.entry_display")
local sorters = require("telescope.sorters")
local themes = require("telescope.themes")
local debug_utils = require("plenary.debug_utils")
local filetype = require("plenary.filetype")
local taglinks = require("taglinks.taglinks")
local tagutils = require("taglinks.tagutils")
local linkutils = require("taglinks.linkutils")
local Path = require("plenary.path")
-- declare locals for the nvim api stuff to avoid more lsp warnings
local vim = vim
@@ -377,6 +381,11 @@ local function find_files_sorted(opts)
file_list = filter_filetypes(file_list, filter_extensions)
table.sort(file_list, order_numeric)
local counts = nil
if opts.show_link_counts then
counts = linkutils.generate_backlink_map(M.Cfg)
end
-- display with devicons
local function iconic_display(display_entry)
local display_opts = {
@@ -407,11 +416,46 @@ local function find_files_sorted(opts)
return popup_opts.preview
end
-- local width = config.width
-- or config.layout_config.width
-- or config.layout_config[config.layout_strategy].width
-- or vim.o.columns
-- local telescope_win_width
-- if width > 1 then
-- telescope_win_width = width
-- else
-- telescope_win_width = math.floor(vim.o.columns * width)
-- end
local displayer = entry_display.create({
separator = "",
items = {
{ width = 4 },
{ width = 4 },
{ remaining = true },
},
})
local function make_display(entry)
local fn = entry.value
local nlinks = counts.link_counts[fn] or 0
local nbacks = counts.backlink_counts[fn] or 0
return displayer({
{ "L" .. tostring(nlinks), "nLinks" },
{ "B" .. tostring(nbacks), "nBacks" },
{ iconic_display(entry), "thePath" },
})
end
local function entry_maker(entry)
local iconic_entry = {}
iconic_entry.value = entry
iconic_entry.ordinal = entry
if opts.show_link_counts then
iconic_entry.display = make_display
else
iconic_entry.display = iconic_display
end
return iconic_entry
end
@@ -421,7 +465,7 @@ local function find_files_sorted(opts)
end
opts.attach_mappings = opts.attach_mappings
or function(prompt_bufnr, _)
or function(_, _)
actions.select_default:replace(picker_actions.select_default)
end
@@ -870,8 +914,246 @@ local function FollowLink(opts)
local cwd = M.Cfg.home
opts.cwd = cwd
local counts = nil
if opts.show_link_counts then
counts = linkutils.generate_backlink_map(M.Cfg)
end
local live_grepper = finders.new_job(function(prompt)
-- display with devicons
local function iconic_display(display_entry)
local display_opts = {
path_display = function(_, e)
return e:gsub(opts.cwd .. "/", "")
end,
}
local hl_group
local display = utils.transform_path(
display_opts,
display_entry.value
)
display, hl_group = utils.transform_devicons(
display_entry.value,
display,
false
)
if hl_group then
return display, { { { 1, 30 }, hl_group } }
else
return display
end
end
-- for media_files
local popup_opts = {}
opts.get_preview_window = function()
return popup_opts.preview
end
-- local width = config.width
-- or config.layout_config.width
-- or config.layout_config[config.layout_strategy].width
-- or vim.o.columns
-- local telescope_win_width
-- if width > 1 then
-- telescope_win_width = width
-- else
-- telescope_win_width = math.floor(vim.o.columns * width)
-- end
local displayer = entry_display.create({
separator = "",
items = {
{ width = 4 },
{ width = 4 },
{ remaining = true },
},
})
local function make_display(entry)
local fn = entry.value
local nlinks = counts.link_counts[fn] or 0
local nbacks = counts.backlink_counts[fn] or 0
if opts.show_link_counts then
return displayer({
{ "L" .. tostring(nlinks), "nLinks" },
{ "B" .. tostring(nbacks), "nBacks" },
{ iconic_display(entry), "thePath" },
})
else
return iconic_display(entry)
end
end
local lookup_keys = {
value = 1,
ordinal = 1,
}
local find = (function()
if Path.path.sep == "\\" then
return function(t)
local start, _, filn, lnum, col, text = string.find(
t,
[[([^:]+):(%d+):(%d+):(.*)]]
)
-- Handle Windows drive letter (e.g. "C:") at the beginning (if present)
if start == 3 then
filn = string.sub(t.value, 1, 3) .. filn
end
return filn, lnum, col, text
end
else
return function(t)
local _, _, filn, lnum, col, text = string.find(
t,
[[([^:]+):(%d+):(%d+):(.*)]]
)
return filn, lnum, col, text
end
end
end)()
local parse = function(t)
print("t: ", t)
local filn, lnum, col, text = find(t.value)
local ok
ok, lnum = pcall(tonumber, lnum)
if not ok then
lnum = nil
end
ok, col = pcall(tonumber, col)
if not ok then
col = nil
end
t.filn = filn
t.lnum = lnum
t.col = col
t.text = text
return { filn, lnum, col, text }
end
local function entry_maker(_)
local mt_vimgrep_entry
opts = opts or {}
local disable_devicons = opts.disable_devicons
local disable_coordinates = opts.disable_coordinates or true
local only_sort_text = opts.only_sort_text
local execute_keys = {
path = function(t)
if Path:new(t.filename):is_absolute() then
return t.filename, false
else
return Path:new({ t.cwd, t.filename }):absolute(), false
end
end,
filename = function(t)
return parse(t)[1], true
end,
lnum = function(t)
return parse(t)[2], true
end,
col = function(t)
return parse(t)[3], true
end,
text = function(t)
return parse(t)[4], true
end,
}
-- For text search only, the ordinal value is actually the text.
if only_sort_text then
execute_keys.ordinal = function(t)
return t.text
end
end
local display_string = "%s:%s%s"
mt_vimgrep_entry = {
cwd = vim.fn.expand(opts.cwd or vim.loop.cwd()),
display = function(entry)
local display_filename = utils.transform_path(
opts,
entry.filename
)
local coordinates = ""
if not disable_coordinates then
coordinates = string.format(
"%s:%s:",
entry.lnum,
entry.col
)
end
local display, hl_group = utils.transform_devicons(
entry.filename,
string.format(
display_string,
display_filename,
coordinates,
entry.text
),
disable_devicons
)
if hl_group then
return display, { { { 1, 3 }, hl_group } }
else
return display
end
end,
__index = function(t, k)
local raw = rawget(mt_vimgrep_entry, k)
if raw then
return raw
end
local executor = rawget(execute_keys, k)
if executor then
local val, save = executor(t)
if save then
rawset(t, k, val)
end
return val
end
return rawget(t, rawget(lookup_keys, k))
end,
}
if opts.show_link_counts then
mt_vimgrep_entry.display = make_display
else
mt_vimgrep_entry.display = iconic_display
end
return function(line)
return setmetatable({ line }, mt_vimgrep_entry)
end
end
opts.entry_maker = entry_maker(opts)
local live_grepper = finders.new_job(
function(prompt)
if not prompt or prompt == "" then
return nil
end
@@ -911,9 +1193,14 @@ local function FollowLink(opts)
local ret = vim.tbl_flatten({ search_command })
return ret
end, make_entry.gen_from_vimgrep(opts), opts.max_results, opts.cwd)
end,
opts.entry_maker or make_entry.gen_from_vimgrep(opts),
opts.max_results,
opts.cwd
)
builtin.live_grep({
-- builtin.live_grep({
pickers.new({
cwd = cwd,
prompt_title = "Notes referencing `" .. title .. "`",
default_text = search_pattern,
@@ -923,6 +1210,8 @@ local function FollowLink(opts)
-- link to heading in specific file (a daily file): [[The cool note#^xAcSh-xxr]]
-- link to paragraph globally [[#^xAcSh-xxr]]
finder = live_grepper,
previewer = conf.grep_previewer(opts),
sorter = sorters.highlighter_only(opts),
attach_mappings = function(_, map)
actions.select_default:replace(picker_actions.select_default)
map("i", "<c-y>", picker_actions.yank_link(opts))
@@ -933,7 +1222,7 @@ local function FollowLink(opts)
map("n", "<c-cr>", picker_actions.paste_link(opts))
return true
end,
})
}):find()
end
end
@@ -1634,7 +1923,10 @@ local function FindAllTags(opts)
-- TODO actions for insert tag, default action: search for tag
local selection = action_state.get_selected_entry().value.tag
local follow_opts = { follow_tag = selection }
local follow_opts = {
follow_tag = selection,
show_link_counts = true,
}
FollowLink(follow_opts)
end)
map("i", "<c-y>", picker_actions.yank_tag(opts))