📝 A simplified Obsidian for Neovim
vim neovim obsidian pkb notes
2

Configure Feed

Select the types of activity you want to include in your feed.

Lua 96.8%
Shell 3.2%
23 1 5

Clone this repository

https://tangled.org/jamie.schembri.me/obsidivim https://tangled.org/did:plc:xx4jujt2xye6t2jjoi4jbzal
git@tangled.org:jamie.schembri.me/obsidivim git@tangled.org:did:plc:xx4jujt2xye6t2jjoi4jbzal

For self-hosted knots, clone URLs may differ based on your setup.



README.md

obsidivim#

Obsidivim is a greatly simplified "implementation" of Obsidian for Neovim. I made it because I found that I don't actually use many features of Obsidian, and I didn't need to be tied to a (proprietary) application for that.

It basically provides some niceties for working with a collection of interlinked Markdown files.

Code primarily written by an LLM.

Screenshots#

Click any image to view full size.

Info panel & link highlighting Wiki-link completion Fuzzy note picker
Info panel showing linked and unlinked mentions with a wiki-link hover preview blink.cmp completion for [[ with a note preview Fuzzy note picker with a live preview

Features#

  • Wiki links: [[note]], [[note|alias]], [[note#heading]] — follow, hover, create on miss; resolved by title, path, filename, or alias.
  • Rename: renames the file and heading, updating every inbound link.
  • Frontmatter: reads title, aliases, and tags.
  • Highlighting: links, #tags, external-link markers, Obsidian-style concealing.
  • Pickers: notes, dailies, grep, backlinks, tags, orphans — via snacks.nvim, telescope.nvim, or fzf-lua.
  • Info panel: linked and unlinked mentions of the current note.
  • Snippets: markdown templates with placeholders, applied to new notes and dailies or inserted at the cursor.
  • Daily notes: today/yesterday with configurable paths.
  • Completion: blink.cmp source for [[ with note preview.

Requirements#

Installation#

With lazy.nvim. This is a complete, batteries-included setup: picker (snacks.nvim), completion (blink.cmp), and concealing. The plugin defines no keymaps of its own — the ones below are suggestions.

{
  {
    url = "https://tangled.org/@jamie.schembri.me/obsidivim",
    version = "*",
    -- Or telescope.nvim / fzf-lua — see "Picker backends" below
    dependencies = { "folke/snacks.nvim" },
    event = "BufReadPre",
    init = function()
      -- Obsidian-style concealing of [[brackets]] and alias| targets
      -- (alternatively, set these in after/ftplugin/markdown.lua)
      vim.api.nvim_create_autocmd("FileType", {
        pattern = "markdown",
        callback = function()
          vim.opt_local.conceallevel = 2
          vim.opt_local.concealcursor = ""
        end,
      })
    end,
    keys = {
      -- find
      { "<leader>nf", "<cmd>ObsidivimFind<cr>", desc = "Find note" },
      { "<leader>n/", "<cmd>ObsidivimSearch<cr>", desc = "Search notes" },
      { "<leader>nb", "<cmd>ObsidivimBacklinks<cr>", desc = "Backlinks" },
      { "<leader>n#", "<cmd>ObsidivimTags<cr>", desc = "Tags" },
      -- write
      { "<leader>nn", "<cmd>ObsidivimNew<cr>", desc = "New note" },
      { "<leader>nR", "<cmd>ObsidivimRename<cr>", desc = "Rename note" },
      { "<leader>ns", "<cmd>ObsidivimSnippet<cr>", desc = "Insert snippet" },
      -- dailies
      { "<leader>nd", "<cmd>ObsidivimFindDaily<cr>", desc = "Find daily notes" },
      { "<leader>nt", "<cmd>ObsidivimToday<cr>", desc = "Daily note (today)" },
      { "<leader>ny", "<cmd>ObsidivimYesterday<cr>", desc = "Daily note (yesterday)" },
      -- ui
      { "<leader>np", "<cmd>ObsidivimPalette<cr>", desc = "Command palette" },
      { "<leader>nP", "<cmd>ObsidivimInfoPanel<cr>", desc = "Toggle info panel" },
    },
    opts = {
      dir = "~/notes",                      -- required
      daily_dir = "~/notes/daily",          -- required
      snippets_dir = "~/notes/.snippets",   -- optional, see "Snippets"
      -- Snippets for new notes: the defaults shown here, or snippet
      -- file paths, e.g. "~/notes/.snippets/daily.md"
      default_snippet = function(title)
        return { "# " .. title, "" }
      end,
      daily_snippet = function(title)
        return { "# " .. title, "" }
      end,
      on_attach = function(buf)
        local ov = require("obsidivim")
        local map = function(mode, lhs, rhs, desc)
          vim.keymap.set(mode, lhs, rhs, { buffer = buf, desc = desc })
        end
        map("n", "K", ov.hover, "Preview wiki link")
        map("n", "gf", ov.follow_link, "Follow wiki link")
        map("n", "<C-]>", ov.follow_link, "Follow wiki link")
      end,
    },
  },

  -- Completion for [[wiki links]]. If you already configure blink.cmp
  -- elsewhere, just add the "obsidivim" source and provider there.
  {
    "saghen/blink.cmp",
    version = "1.*",
    opts_extend = { "sources.default" },
    opts = {
      sources = {
        default = { "lsp", "path", "snippets", "buffer", "obsidivim" },
        providers = {
          obsidivim = { module = "obsidivim.blink", name = "Notes" },
        },
      },
    },
  },
}

Configuration#

Defaults shown; dir and daily_dir are required.

opts = {
  dir = nil,       -- notes root
  daily_dir = nil, -- daily notes root

  -- os.date() format for daily note paths relative to daily_dir,
  -- e.g. "%Y/%m/%Y-%m-%d" -> daily_dir/2026/07/2026-07-03.md
  daily_path_format = "%Y-%m-%d",

  -- os.date() format for the title of new daily notes.
  -- nil uses the file name, e.g. "# 2026-07-03".
  daily_title_format = nil, -- e.g. "%A, %B %d %Y" -> "# Friday, July 03 2026"

  -- File name (without .md) for notes created from a title or wiki link:
  --   "verbatim" keeps the title, minus filename-unsafe characters:
  --     "Meeting: Notes!" -> Meeting Notes!.md
  --   "slugify" lowercases and dashes: "Meeting: Notes!" -> meeting-notes.md
  --   a fun(title): string returns the stem; a "/" in it creates the note
  --   in a subdirectory (missing directories are created)
  note_filename = "verbatim",

  -- Snippets applied to new notes / daily notes: a snippet file path
  -- (e.g. "~/notes/.snippets/note.md", see "Snippets") or a
  -- fun(title): string[] returning buffer lines.
  default_snippet = function(title)
    return { "# " .. title, "" }
  end,
  daily_snippet = function(title)
    return { "# " .. title, "" }
  end,

  -- Directory of snippet files, picked from by :ObsidivimSnippet.
  snippets_dir = nil, -- e.g. "~/notes/.snippets"

  -- Write newly created notes to disk immediately. When false they are
  -- left as unsaved buffers.
  auto_save = false,

  picker = "auto", -- "auto" | "snacks" | "telescope" | "fzf-lua" | "select" | backend table

  -- Read-only info split (linked + unlinked mentions) at the bottom of note
  -- windows.
  info_panel = {
    enabled = false, -- auto-open/refresh for note buffers
    height = 10,
  },

  highlight = {
    enabled = true,
    conceal = true,       -- conceal [[ ]] and the target| of aliased links
    tags = true,          -- highlight #tags
    external_icon = "↗",  -- marker on external [text](url) links
                          -- (e.g. a nerd-font glyph); "" disables
  },

  on_attach = nil, -- fun(buf) called for markdown buffers in the notes dir
}

Snippets#

Snippets are plain markdown files. These placeholders are expanded (anything else is left alone):

Placeholder Expands to
{{title}} note title (when inserting into an existing note: its first heading / file name)
{{date}}, {{time}} 2026-07-05, 14:30
{{date:<fmt>}}, {{time:<fmt>}} any os.date() format
{{cursor}} nothing — the cursor lands here (default: end of snippet)

default_snippet and daily_snippet are applied when a note / daily note is created. Each is either a snippet file path or a fun(title): string[] returning the buffer lines.

snippets_dir enables :ObsidivimSnippet: pick a snippet and insert it below the cursor (or as the whole buffer when empty). A snippets_dir inside your notes directory is fine: hidden directories are excluded from note scans.

Frontmatter#

Notes may start with a YAML frontmatter block; three fields are read (anything else is ignored):

---
title: Cascading Style Sheets
aliases: [CSS, styles]
tags: [webdev, reference]
---
  • title overrides the first # heading as the note's title.
  • aliases are extra names the note answers to: [[CSS]] resolves here and completes after [[.
  • tags count in :ObsidivimTags, alongside inline #tags.

Values can be quoted, inline arrays ([a, b]), block lists (- a), or bare scalars.

Renaming#

:ObsidivimRename [new title] (prompting with the current title when called bare) renames the current note: the file (named via note_filename), its # heading, and every inbound [[link]]. Links keep their addressing style (title / path / filename / alias) and their #anchors and |aliases; notes with unsaved changes elsewhere are skipped with a warning.

Daily notes#

ObsidivimToday/ObsidivimYesterday open (creating if needed) a note at daily_dir + daily_path_format + .md. For example:

opts = {
  dir = "~/notes",
  daily_dir = "~/notes/daily",
  daily_path_format = "%Y/%m/%Y-%m-%d",
  daily_title_format = "%A %d %B %Y",
  daily_snippet = "~/notes/.snippets/daily.md",
}

with ~/notes/.snippets/daily.md containing:

# {{title}}

## Tasks

## Log

creates ~/notes/daily/2026/07/2026-07-03.md containing:

# Friday 03 July 2026

## Tasks

## Log

LSP mappings#

follow_link and hover fall back to vim.lsp.buf.definition() and vim.lsp.buf.hover() off-link, so they can safely shadow LSP keys in note buffers. If an LSP (e.g. marksman) maps the same keys on LspAttach, its mappings run later and win — register those with unique = true so the buffer-local ones are kept.

Info panel#

With info_panel.enabled = true, a read-only split opens at the bottom of note windows, refreshed as you switch notes or save:

  • Linked mentions — notes containing a [[link]] that resolves here.
  • Unlinked mentions — notes using this note's title as plain text.

The panel follows the focused note window and renders [[wiki links]] like a note. <CR>, gf, or <C-]> jump to the mention under the cursor — in the note window you came from, never the panel — and q closes it.

:ObsidivimInfoPanel toggles the panel and focuses it. It works with enabled off too, so you can skip auto-open and drive it from a key.

Picker backends#

picker = "auto" uses the first of snacks.nvim, telescope.nvim, or fzf-lua that is installed, and vim.ui.select otherwise. Set it explicitly ("snacks", "telescope", "fzf-lua", "select") to skip detection.

In all three native backends, <CR> opens the selection; in the find picker <CR> with no match (or <C-x> at any point) creates a note from the typed query, and <C-r> rescans the notes directory.

A backend just needs its plugin installed, e.g. swap the dependencies line of the installation example for one of:

-- snacks.nvim
dependencies = { "folke/snacks.nvim" },

-- telescope.nvim
dependencies = {
  { "nvim-telescope/telescope.nvim", dependencies = { "nvim-lua/plenary.nvim" } },
},

-- fzf-lua (also needs the fzf binary, e.g. `brew install fzf`)
dependencies = { "ibhagwan/fzf-lua" },

To integrate another picker natively, pass a table with two functions:

picker = {
  --- Show a list of items.
  --- items(refresh?) returns { label, hint?, file, pos? }[]; call it with
  --- refresh = true to rescan notes. Confirming an item should call
  --- opts.confirm(item) when set, otherwise :edit its file and jump to pos
  --- ({lnum, col}) when set. on_create (only set for the find picker) may
  --- be called with a free-text query to create a note; support it if your
  --- picker exposes the typed query.
  pick = function(opts) end, -- opts = { title, items, on_create?, confirm? }

  --- Live-grep the notes directory.
  grep = function(opts) end, -- opts = { title, dir }
}

Commands#

Command Action
ObsidivimFind Pick a note by title (create on no match)
ObsidivimSearch Grep note contents
ObsidivimBacklinks Notes linking to the current note
ObsidivimTags Pick a #tag, then jump to an occurrence
ObsidivimOrphans Notes with no inbound links
ObsidivimNew Create a note (arg or prompt for title)
ObsidivimRename Rename current note, updating inbound links
ObsidivimSnippet Pick a snippet and insert it at the cursor
ObsidivimFindDaily Pick a daily note
ObsidivimToday Open today's daily note
ObsidivimYesterday Open yesterday's daily note
ObsidivimPalette Pick any Obsidivim command and run it
ObsidivimInfoPanel Toggle the info panel

API#

require("obsidivim"): find(), search(), backlinks(), tags(), orphans(), create(title), rename(title), insert_snippet(), find_daily(), today(), yesterday(), palette(), toggle_info_panel(), follow_link(), hover()

Highlights#

Override with vim.api.nvim_set_hl (defaults shown):

Group Default
ObsidivimLink underline, fg from @markup.link.label (fg or sp)
ObsidivimLinkMissing links to DiagnosticUnderlineHint
ObsidivimLinkExternal links to ObsidivimLink (external links + icon)
ObsidivimTag links to Special

Versioning#

Releases are SemVer git tags (vMAJOR.MINOR.PATCH); see CHANGELOG.md. While pre-1.0, minor bumps may break opts.

Pin to the latest tagged release in lazy.nvim, or omit version to follow the default branch:

{ url = "https://tangled.org/@jamie.schembri.me/obsidivim", version = "*" }