e8fb987
dotfiles.git > main > e8fb987 > files
Markdown treesitter highlighting and wikilink navigation
Commit: e8fb98750bbf2dcc7cb5d6de0ceb340d0a45039f
Parent: dee5107
4 changed files:
nvim/after/queries/markdown_inline/highlights.scm view file
@@ -0,0 +1,13 @@+;; extends++; [[wikilinks]] parse as "[" (shortcut_link) "]", hide the brackets like Obsidian does+(inline+ "[" @markup.link.wiki.bracket+ .+ (shortcut_link+ "[" @markup.link.wiki.bracket+ (link_text) @markup.link.wiki+ "]" @markup.link.wiki.bracket)+ .+ "]" @markup.link.wiki.bracket+ (#set! @markup.link.wiki.bracket conceal ""))nvim/ftplugin/markdown.lua view file
@@ -3,3 +3,9 @@ ol.linebreak = true ol.spell = true ol.columns = 80++vim.treesitter.start()++local follow = function(fallback) return function() require("wikilink").follow(fallback) end end+vim.keymap.set("n", "gd", follow("gd"), {buffer = true})+vim.keymap.set("n", "gf", follow("gf"), {buffer = true})nvim/lua/colors.lua view file
@@ -142,6 +142,27 @@ hl(0, "@keyword.return.lua", {link = "Blue"}) hl(0, "@string.escape.lua", {link = "String"}) +hl(0, "@markup.heading.1", {fg = red, bold = true})+hl(0, "@markup.heading.2", {fg = orange, bold = true})+hl(0, "@markup.heading.3", {fg = pink, bold = true})+hl(0, "@markup.heading.4", {fg = blue, bold = true})+hl(0, "@markup.heading.5", {fg = blue, bold = true})+hl(0, "@markup.heading.6", {fg = blue, bold = true})+hl(0, "@markup.strong", {bold = true})+hl(0, "@markup.italic", {italic = true})+hl(0, "@markup.strikethrough", {fg = gray, strikethrough = true})+hl(0, "@markup.raw", {link = "Green"})+hl(0, "@markup.raw.block", {link = "Green"})+hl(0, "@markup.quote", {fg = yellow, italic = true})+hl(0, "@markup.list", {link = "Punctuation"})+hl(0, "@markup.list.checked", {link = "Green"})+hl(0, "@markup.list.unchecked", {link = "Punctuation"})+hl(0, "@markup.link", {link = "Punctuation"})+hl(0, "@markup.link.label", {fg = blue, underline = true})+hl(0, "@markup.link.url", {link = "Gray"})+hl(0, "@markup.link.wiki", {fg = blue, underline = true})+hl(0, "@markup.link.wiki.bracket", {link = "Punctuation"})+ hl(0, "OilDir", {link = "Blue"}) hl(0, "OilDirHidden", {link = "Gray"}) hl(0, "OilFileHidden", {link = "Gray"})nvim/lua/wikilink.lua view file
@@ -0,0 +1,75 @@+--- Follow [[wikilinks]] and [text](local/links.md) like Obsidian does+local fn = vim.fn++--- Returns the note name and anchor (heading or ^block) of the link under the cursor, or nil+local function link_under_cursor()+ local line = vim.api.nvim_get_current_line()+ local col = vim.api.nvim_win_get_cursor(0)[2] + 1++ for s, target, e in line:gmatch("()%[%[(.-)%]%]()") do+ if col >= s and col < e then+ return target:gsub("|.*", ""):match("^([^#]*)#?(.*)$")+ end+ end+ for s, target, e in line:gmatch("()%[[^%]]*%]%(([^)]+)%)()") do+ if col >= s and col < e and not target:match("^%a+:") then+ local name, anchor = vim.uri_decode(target):match("^([^#]*)#?(.*)$")+ --- Markdown links use #some-heading slugs, turn them back into words+ return name, (anchor:gsub("-", " "))+ end+ end+end++--- The vault is the nearest dir with .obsidian (or .git), falling back to the file's own dir+local function vault_root()+ return vim.fs.root(0, {".obsidian", ".git"}) or fn.expand("%:p:h")+end++--- Obsidian matches by note name anywhere in the vault, or by path if one is given+local function resolve(name)+ if not name:match("%.%w+$") then name = name .. ".md" end+ local here = fn.expand("%:p:h")+ local root = vault_root()++ for _, dir in ipairs({here, root}) do+ local path = vim.fs.joinpath(dir, name)+ if fn.filereadable(path) == 1 then return path end+ end++ local matches = vim.fs.find(function(f, dir)+ return vim.fs.joinpath(dir, f):sub(-#name - 1) == "/" .. name+ end, {path = root, type = "file", limit = math.huge})+ --- Shortest path wins, same as Obsidian+ table.sort(matches, function(a, b) return #a < #b end)+ if matches[1] then return matches[1] end++ --- Doesn't exist yet, so make it next to the current note+ return vim.fs.joinpath(here, name)+end++--- Jumps to the "# Heading" or "^block-id" in the current buffer+local function jump_to_anchor(anchor)+ local pat = anchor:sub(1, 1) == "^"+ and "\\V " .. fn.escape(anchor, "\\") .. "\\$"+ or "\\c\\V\\^#\\+\\s\\+" .. fn.escape(anchor, "\\") .. "\\s\\*\\$"+ if fn.search(pat, "cw") == 0 then+ vim.notify("No anchor " .. anchor, vim.log.levels.WARN)+ end+end++--- Follows the link under the cursor, otherwise runs `fallback` as a normal mode command+local function follow(fallback)+ local name, anchor = link_under_cursor()+ if not name then+ return vim.cmd("normal! " .. fallback)+ end++ if name ~= "" then+ vim.cmd.edit(fn.fnameescape(resolve(name)))+ else+ vim.cmd("normal! m'")+ end+ if anchor ~= "" then jump_to_anchor(anchor) end+end++return {follow = follow}