e8fb987

dotfiles.git > main > e8fb987 > files

Markdown treesitter highlighting and wikilink navigation

Grahp, 2026-11-09

Commit: e8fb98750bbf2dcc7cb5d6de0ceb340d0a45039f
Parent: dee5107

4 changed files:

nvim/after/queries/markdown_inline/highlights.scm added (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 modified (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 modified (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 added (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}