Title-based markdon picker in Neovim
Introduction
Switching Regular filenames for UUIDs leaves folder in a state that is hard to wrangle for everyday use. The point being to replace and learn from Emacs, not to rebuild all of its problems in Obsidian.
So let's see how can read this mess easily in my go-to editor, Neovim, using Lua.
The goal is to replace the tool I use as a file picker, Telescope
to display the title
values instead of the filenames.
Format
Implementation
First create a new lua file that will be sourced by neovim, this depends a lot on your setup but make sure it does so only after loading the Telescope plugin.
Let's break it down in blocks and comment them.
-- Function to get a list of all markdown files
-- markdown_dir needs to be set first somewhere else
local function get_markdown_files()
local files = {}
-- Somewhat suprisingly there doesn't seem to be a fs module in neovim
-- Relying on "find" here, not sure how this will fare on every platform.
local handle = io.popen("find " .. vim.fn.expand(markdown_dir) .. " -name '*.md'")
if handle then
-- Iterate through each item returned by the shell sub-process
for file in handle:lines() do
table.insert(files, file)
end
handle:close()
end
return files
end
-- Extract the title from the frontmatter of the markdown file
local function get_title_from_frontmatter(file_path)
local file = io.open(file_path, "r")
if not file then return "" end
-- Read the entire content of the file
local content = file:read("*all")
file:close()
-- Use pattern matching to extract the title from the frontmatter
-- It could be re;evan to use the yaml module but for this single
-- it doesn't make the script any shorter or simpler
local title = content:match("title:%s*(.-)%s*\n")
return title or ""
end
-- Function to prepare each pair of items (title and path) for the picker
local function prepare_items_for_picker()
local files = get_markdown_files()
local items = {}
for _, file in ipairs(files) do
local title = get_title_from_frontmatter(file)
table.insert(items, {title = title, path = file})
end
return items
end
-- Defines a previewer
local function define_file_previewer(self, entry, status)
local lines = {}
for line in io.lines(entry.path) do
table.insert(lines, line)
end
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, lines)
-- This will allow us to keep treesitter's syntax highlighting in the previewer
-- This even includes code blocks such as this one
vim.api.nvim_buf_set_option(self.state.bufnr, 'filetype', 'markdown')
vim.api.nvim_buf_call(self.state.bufnr, function()
vim.cmd('syntax enable')
end)
end
-- Function to create and configure the Telescope picker
local function create_telescope_picker(items)
pickers.new({}, {
prompt_title = "Markdown Files",
finder = finders.new_table {
results = items,
entry_maker = function(entry)
return {
value = entry,
display = entry.title ~= "" and entry.title or vim.fn.fnamemodify(entry.path, ":t"),
ordinal = entry.title .. " " .. entry.path,
path = entry.path,
}
end,
},
sorter = conf.generic_sorter({}),
previewer = previewers.new_buffer_previewer({
title = "File Preview",
define_preview = define_file_previewer
}),
attach_mappings = function(prompt_bufnr, map)
actions.select_default:replace(function()
actions.close(prompt_bufnr)
local selection = require("telescope.actions.state").get_selected_entry()
vim.cmd("edit " .. selection.path)
end)
return true
end,
}):find()
end
-- Main function to orchestrate the markdown picker
local function markdown_picker()
local items = prepare_items_for_picker()
create_telescope_picker(items)
end
-- Adds a Neovim user command to launch the markdown picker
vim.api.nvim_create_user_command("MarkdownPicker", markdown_picker, {})
-- Map the Ctrl+O keybinding in normal mode to launch the markdown picker
-- This is similar to Obsidian, so the behavior is the same across editors
vim.keymap.set("n", "<C-o>", ":MarkdownPicker<CR>", { noremap = true, silent = true })
Most of the code here is meant to setup the Telescope picker instance, it takes very little effort to copy the behavior added to Obsidian in the previous article to Neovim in such a seemless manner.
Going further
Before I commit this as part of a plugin, beside gathering more requirements, we could also:
- improve performance on larger vaults by caching the results asynchronously
- starting with the old results and updating with the new ones as they become available
- or filter files before listing them, like look for a certain field/value pair or a tag
- Improve the cosmetic of it, add icons based on certain value in the frontmatter
- or let a running instance of obsidian to the heavy lifting
- But more on this here