Vim Motions supports Lua configuration files using a sandboxed Lua 5.3 runtime. Enable it in Settings → Vim Motions → Vimrc & key bindings → Configuration mode → Lua only (or Lua + Vimrc). The key value-add over vimrc is conditional logic and function-based keymaps.

File location

The plugin searches the vault root for the first matching file in this order:

  1. init.lua
  2. .init.lua
  3. obsidian.init.lua
  4. .obsidian.init.lua
  5. obsidian.lua

The first file found is used. Override this with a custom path in Settings → Vim Motions → Vimrc & key bindings → Custom init.lua path. The settings UI shows which file is currently active.

Shared config across vaults (desktop only)

On desktop, the custom path can be an absolute filesystem path — useful for sharing one init.lua across multiple vaults:

  • ~/.config/obsidian/init.lua (Linux)
  • ~/Library/Application Support/obsidian/init.lua (macOS)
  • C:\Users\<you>\.config\obsidian\init.lua (Windows)

Any absolute path (starting with /, ~, or a drive letter) is read directly from the filesystem instead of through the vault. This is not available on mobile.

Obsidian Sync

Obsidian Sync skips dotfiles. Use a non-dotfile name like init.lua (the first candidate in the fallback chain) to ensure your Lua config syncs across devices.

Multi-file configs with require()

Split your configuration across multiple files by placing Lua modules in a lua/ directory at the vault root:

<vault>/
  lua/
    keymaps.lua
    utils/
      strings.lua
  init.lua
-- init.lua
local keymaps = require("keymaps")      -- loads lua/keymaps.lua
local strings = require("utils.strings") -- loads lua/utils/strings.lua
 
keymaps.setup()
-- lua/keymaps.lua
local M = {}
 
function M.setup()
    vim.g.mapleader = " "
    vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save" })
end
 
return M

Modules are cached in package.loaded — calling require("keymaps") twice returns the same table. load(chunk) is available for dynamic string compilation. dofile and loadfile remain disabled.

Security: module names containing .., absolute paths (/, \), or null bytes are rejected.

Example init.lua

vim.g.mapleader = " "
 
vim.opt.scrolloff = 8
vim.opt.textobjects = true
vim.opt.clipboard = "unnamedplus"
 
-- Conditional config based on vault
if vim.vault_name() == "work" then
    vim.opt.clipboard = "unnamedplus"
else
    vim.opt.clipboard = ""
end
 
-- Keymaps with string RHS
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })
vim.keymap.set("i", "jk", "<Esc>", { desc = "Exit insert mode" })
 
-- Keymap with function callback
vim.keymap.set("n", "<leader>t", function()
    vim.cmd("obcommand daily-notes:open-today")
end, { desc = "Open daily note" })
 
-- Remove a mapping
vim.keymap.del("n", "Q")
 
-- Ex commands
vim.cmd("set nohlsearch")
 
print("init.lua loaded for vault:", vim.vault_name())

Supported APIs

APIDescriptionExample
vim.opt.<name> = valueSet a plugin option (string options accept tables)vim.opt.scrolloff = 8
vim.o.<name> = valueAlias for vim.optvim.o.scrolloff = 8
vim.g.mapleaderSet the leader keyvim.g.mapleader = " "
vim.g.<name> = valueSet a user variablevim.g.my_var = true
vim.cmd(string)Execute an ex commandvim.cmd("set nohlsearch")
vim.vault_name()Returns the current vault nameif vim.vault_name() == "work" then
vim.fn.has(feature)Platform/feature detectionvim.fn.has("mac")
vim.fn.expand(expr)Active file path (vault-relative)vim.fn.expand("%:t")
vim.fn.fnamemodify(path, mods)Path manipulationvim.fn.fnamemodify(path, ":t:r")
vim.fn.exists(expr)Check variable/option existencevim.fn.exists("g:my_var")
vim.fn.localtime()Unix timestampvim.fn.localtime()
vim.fn.strftime(fmt)Format date/timevim.fn.strftime("%Y-%m-%d")
vim.fn.filereadable(path)Check vault file existsvim.fn.filereadable("config.md")
vim.fn.isdirectory(path)Check vault directory existsvim.fn.isdirectory("templates")
vim.fn.glob(pattern)Find matching vault filesvim.fn.glob("*.md")
vim.fn.undotree()Returns undo tree dictionarylocal tree = vim.fn.undotree()
vim.fn.mode()Current vim modevim.fn.mode()
vim.fn.line(expr)Cursor line (1-based, callbacks)vim.fn.line(".")
vim.fn.col(expr)Cursor column (1-based, callbacks)vim.fn.col(".")
vim.notify(msg)Show Obsidian notificationvim.notify("Saved!")
vim.api.nvim_create_user_command(name, cmd, opts)Define custom ex commandsee below
vim.api.nvim_create_autocmd(event, opts)Register autocommandsee Autocommands section
vim.api.nvim_create_augroup(name, opts)Create/get autocommand groupsee Autocommands section
vim.keymap.set(mode, lhs, rhs, opts?)Create a key mappingsee example above
vim.keymap.del(mode, lhs)Remove a key mappingvim.keymap.del("n", "Q")
vim.obsidian.keymap.set(lhs, rhs, opts?)Create a global (non-editor) keymapsee Obsidian namespace
vim.obsidian.keymap.del(lhs)Remove a global keymapsee Obsidian namespace
vim.obsidian.pick(source, opts?)Open the unified pickervim.obsidian.pick("files")
vim.obsidian.whichkey.set_group(key, label, opts?)Name a which-key groupsee Obsidian namespace
vim.obsidian.whichkey.set_label(key, label, opts?)Label a which-key bindingsee Obsidian namespace
vim.obsidian.whichkey.add(entries)Batch-add group and command labelssee Obsidian namespace
vim.obsidian.oil.parent()Oil: navigate to parent directorysee Obsidian namespace
vim.obsidian.oil.open_entry()Oil: open file/directory under cursorsee Obsidian namespace
vim.obsidian.pick_keymap(table)Configure picker keyboard shortcutssee Obsidian namespace
vim.obsidian.im.get()Get current IM identifier (desktop only)see Obsidian namespace
vim.obsidian.im.set(id)Switch to specific IM (desktop only)see Obsidian namespace
vim.obsidian.im.save()Save current IM for active editor viewsee Obsidian namespace
vim.obsidian.im.restore()Restore saved IM for active editor viewsee Obsidian namespace
print(...)Print to developer consoleprint("loaded")

vim.textobject

Define custom text objects from Lua configuration.

vim.gen_spec.pair(open, close, opts?)

Creates a text object spec for delimiter pairs.

  • open (string) — Opening delimiter (e.g., "((", "**", "<")
  • close (string) — Closing delimiter (e.g., "))", "**", ">")
  • opts.multiline (boolean, default true) — Search across multiple lines

vim.textobject.add(keys, spec)

Register a custom text object.

  • keys (string) — Keybinding, must start with i (inner) or a (around), e.g., "iX", "a<"
  • spec — A spec table from vim.gen_spec.*

vim.textobject.del(keys)

Remove a previously registered text object.

Examples

-- Custom angle bracket text object
vim.textobject.add('i<', vim.gen_spec.pair('<', '>'))
vim.textobject.add('a<', vim.gen_spec.pair('<', '>'))
 
-- Custom double-asterisk text object
vim.textobject.add('iB', vim.gen_spec.pair('**', '**'))
vim.textobject.add('aB', vim.gen_spec.pair('**', '**'))
 
-- Single-line only
vim.textobject.add('iP', vim.gen_spec.pair('(', ')', { multiline = false }))

Leader key

Set the leader key with vim.g.mapleader. Common choices:

vim.g.mapleader = " "   -- Space (recommended, matches most Neovim configs)
vim.g.mapleader = ","   -- Comma
vim.g.mapleader = "\\"  -- Backslash (default)

Set mapleader before keymaps

Always set vim.g.mapleader before any vim.keymap.set or vim.obsidian.leader.add calls.
The leader key is substituted at registration time — changing it later won’t update existing mappings.

vim.v — Predefined variables

Neovim-compatible read-only predefined variables. Available in keymap callbacks and autocmds.

Core variables (available in keymap callbacks)

VariableTypeDescriptionDefault
vim.v.countintegerCount given for the last Normal mode command. 0 when no count typed.0
vim.v.count1integerLike count but defaults to 1 when no count given.1
vim.v.registerstringRegister in effect for the current command. '"' (unnamed) when none specified.'"'
vim.v.operatorstringPending operator (e.g., 'd', 'y', 'c'). Empty string when none.''

Search & mode variables

VariableTypeR/WDescription
vim.v.searchforwardintegerR/WSearch direction: 1 forward, 0 backward
vim.v.insertmodestringRInsert mode type: 'i' insert, 'r' replace, 'v' virtual replace
vim.v.hlsearchintegerRWhether search highlighting is active

Constants

VariableTypeValueDescription
vim.v.numbermaxinteger9007199254740991Maximum integer (53-bit)
vim.v.numbermininteger-9007199254740991Minimum integer (53-bit)
vim.v.numbersizeinteger53Number of bits in an integer
vim.v.truebooleantrueBoolean true
vim.v.falsebooleanfalseBoolean false
vim.v.nullnilnilNull value

Context-dependent variables

These are only meaningful within specific evaluation contexts (fold expressions, statuscolumn, autocmds):

VariableTypeR/WContext
vim.v.foldstartintegerRFold text evaluation
vim.v.foldendintegerRFold text evaluation
vim.v.foldlevelintegerRFold text evaluation
vim.v.folddashesstringRFold text evaluation
vim.v.lnumintegerRstatuscolumn evaluation
vim.v.relnumintegerRstatuscolumn evaluation
vim.v.virtnumintegerRstatuscolumn evaluation
vim.v.charstringR/WInsertCharPre autocmd
vim.v.eventtable/nilRAutocmd event data

Example: expr mapping with count

-- Use gj/gk for screen-line movement, j/k for counted movement
vim.keymap.set('n', 'j', function()
    if vim.v.count == 0 then
        return 'gj'
    else
        return vim.v.count1 .. 'j'
    end
end, { expr = true, silent = true })
 
vim.keymap.set('n', 'k', function()
    if vim.v.count == 0 then
        return 'gk'
    else
        return vim.v.count1 .. 'k'
    end
end, { expr = true, silent = true })

Count is not auto-forwarded to expr mapping results

Count is not auto-forwarded to expr mapping results. If you type 3j and your expr callback returns 'j', it executes once. Concatenate the count yourself: return vim.v.count1 .. 'j'.

Supported vim.opt options

All plugin options are available via vim.opt. vim.o is an alias.

OptionTypeDefaultValid range / valuesExample
textobjectsbooleantruevim.opt.textobjects = true
replacewithregisterbooleantruevim.opt.replacewithregister = true
navigationbooleantruevim.opt.navigation = true
hardwrapbooleantruevim.opt.hardwrap = true
listcontinuationbooleantruevim.opt.listcontinuation = true
tablenavbooleantruevim.opt.tablenav = true
workspacenavbooleantruevim.opt.workspacenav = true
numberbooleanfalsevim.opt.number = true
relativenumberbooleanfalsevim.opt.relativenumber = true
flashbooleantruevim.opt.flash = true
flashmultilinebooleantruevim.opt.flashmultiline = true
flashjumpbooleanfalsevim.opt.flashjump = true
flashcleverfbooleanfalsevim.opt.flashcleverf = true
flashsearchbooleantruevim.opt.flashsearch = true
labelmatchfontsizebooleanfalsevim.opt.labelmatchfontsize = true
easymotionbooleantruevim.opt.easymotion = true
easymotiondimmingbooleantruevim.opt.easymotiondimming = true
hintmodebooleantruevim.opt.hintmode = true
statusbarbooleantruevim.opt.statusbar = true
chorddisplaybooleantruevim.opt.chorddisplay = true
powerlinebooleanfalsevim.opt.powerline = true
expandtabbooleantruevim.opt.expandtab = true
cursorlinebooleantruevim.opt.cursorline = true
foldcolumnbooleanfalsevim.opt.foldcolumn = true
undotreebooleantruevim.opt.undotree = true
undofilebooleanfalsevim.opt.undofile = true
vimtextareasbooleanfalsevim.opt.vimtextareas = true
yankringbooleantruevim.opt.yankring = true
harpoonbooleantruevim.opt.harpoon = true
dialbooleanfalsevim.opt.dial = true
jumplistbooleantruevim.opt.jumplist = true
foldawarenavigationbooleantruevim.opt.foldawarenavigation = true
foldpersistencebooleanfalsevim.opt.foldpersistence = true
subwordbooleanfalsevim.opt.subword = true
pickerbooleantruevim.opt.picker = true
pickerleadermappingsbooleantruevim.opt.pickerleadermappings = true
pickeromnisearchbooleanfalsevim.opt.pickeromnisearch = true
pickertasksbooleanfalsevim.opt.pickertasks = true
pickerdataviewbooleanfalsevim.opt.pickerdataview = true
ripgrepbooleanfalsevim.opt.ripgrep = true
oilbooleanfalsevim.opt.oil = true
oilhiddenfilesbooleanfalsevim.opt.oilhiddenfiles = true
undotreeautoopenbooleanfalsevim.opt.undotreeautoopen = true
imswitchingbooleanfalsevim.opt.imswitching = true
pcrebooleantruevim.opt.pcre = false
smoothcursorbooleanfalsevim.opt.smoothcursor = true
smoothcursorglidebooleantruevim.opt.smoothcursorglide = true
smoothcursorsmearbooleantruevim.opt.smoothcursorsmear = true
scrolloffnumber50–9999vim.opt.scrolloff = 8
scanlimitnumber205–200vim.opt.scanlimit = 20
undotreemaxnodesnumber1000100–5000vim.opt.undotreemaxnodes = 500
jumplistsizenumber200> 0vim.opt.jumplistsize = 100
yankhighlightdurationnumber2000–5000 msvim.opt.yankhighlightduration = 300
labelfontsizenumber1410–20vim.opt.labelfontsize = 14
tabstopnumber4vim.opt.tabstop = 4
shiftwidthnumber4vim.opt.shiftwidth = 4
textwidthnumber80vim.opt.textwidth = 80
insertmodeescapetimeoutnumber1000100–5000 msvim.opt.insertmodeescapetimeout = 1000
operatorshadowtimeoutnumber10000–5000 ms (0 = disabled)vim.opt.operatorshadowtimeout = 1000
numberwidthnumber21–20vim.opt.numberwidth = 2
smoothcursorsmoothnessnumber0.50–1vim.opt.smoothcursorsmoothness = 0.3
smoothcursorstiffnessnumber0.60.1–1vim.opt.smoothcursorstiffness = 0.6
smoothcursortrailstiffnessnumber0.30.1–1vim.opt.smoothcursortrailstiffness = 0.3
smoothcursordampingnumber0.850.1–0.99vim.opt.smoothcursordamping = 0.85
smoothcursormaxlengthnumber40050–800 pxvim.opt.smoothcursormaxlength = 400
clipboardstring"""", "unnamed", "unnamedplus"vim.opt.clipboard = "unnamedplus"
insertmodeescapestring""vim.opt.insertmodeescape = "jk"
flashjumpkeystring"s"vim.opt.flashjumpkey = "s"
flashminpatternlengthnumber10–10vim.opt.flashminpatternlength = 2
easymotionlabelsstring"asdghklqwertyuiopzxcvbnmfj"vim.opt.easymotionlabels = "asdf"
hintlabelsstring"asdfghjkl"vim.opt.hintlabels = "asdf"
yankhighlightmodestring"solid""off", "solid", "fade"vim.opt.yankhighlightmode = "fade"
tablewidgetstring"cursor""off", "cursor", "always", "embedded"vim.opt.tablewidget = "cursor"
whichkeystring"off""off", "leader", "all"vim.opt.whichkey = "leader"
whichkeygroupingstring"grouped""flat", "grouped"vim.opt.whichkeygrouping = "grouped"
whichkeysortstring"which-key""which-key", "groups-first"vim.opt.whichkeysort = "which-key"
whichkeyiconsbooleantruevim.opt.whichkeyicons = true
whichkeydelaynumber5000–2000 msvim.opt.whichkeydelay = 300
workspacenavviewtypesstring""Comma-separated view types (defaults: markdown, graph, pdf, canvas, empty, image, bases)vim.opt.workspacenavviewtypes = "markdown,graph"
guicursorstring"n:block,i:bar,v:block,r:underline,o:underline"see Cursor shapesvim.opt.guicursor = "n:bar,i:block"
cursorlineoptstring"number""number", "line", "both"vim.opt.cursorlineopt = "both"
signcolumnstring"auto""auto[:N]", "yes[:N]", "no"vim.opt.signcolumn = "auto:3"
linenumbermodestring"hybrid""hybrid", "dual", "dual-rel-abs"vim.opt.linenumbermode = "dual"
statuscolumnstring""format string (%l, %r, %s, %C, %=)vim.opt.statuscolumn = "%s %l %r %C"
oilconfirmdeletethresholdnumber50–100vim.opt.oilconfirmdeletethreshold = 10
updatetimenumber4000ms (CursorHold delay)vim.opt.updatetime = 4000
pickermatcherstring"ufuzzy""ufuzzy", "obsidian"vim.opt.pickermatcher = "obsidian"
ripgreppathstring""vim.opt.ripgreppath = "/usr/bin/rg"
ripgrepargsstring""vim.opt.ripgrepargs = "--hidden"
grepmodestring"ripgrep""ripgrep", "grep"vim.opt.grepmode = "grep"
oilsortstring"name""name", "mtime", "size"vim.opt.oilsort = "mtime"
hinthotkeystring""vim.opt.hinthotkey = "f"
undotreepositionstring"right""left", "right"vim.opt.undotreeposition = "left"
impresetstring"custom""custom", "macism", "im-select", "fcitx5-remote", "ibus"vim.opt.impreset = "fcitx5-remote"
imbinarypathstring""vim.opt.imbinarypath = "/usr/bin/fcitx5-remote"
imobtainargsstring""vim.opt.imobtainargs = ""
imswitchargsstring"{im}"vim.opt.imswitchargs = "-t {im}"
imdefaultnormalstring""vim.opt.imdefaultnormal = "1"
imrestorebehaviorstring"restore""restore", "default"vim.opt.imrestorebehavior = "default"
imdefaultinsertstring""vim.opt.imdefaultinsert = "2"

Hybrid line numbers

Enabling both vim.opt.number = true and vim.opt.relativenumber = true activates hybrid mode: the current line shows its absolute number, while all other lines show their relative distance from the cursor.

Example with cursor on line 8:

 a   3   ## Introduction
     2   Some text here.
     1   More context.
     8   ← cursor line (shows absolute number)
     1   Additional notes.
 b   2   Another paragraph.
     3   Final thoughts.

The sign column (a, b) appears to the left of line numbers. The fold column (if enabled) appears to the right. This layout matches Neovim’s default gutter arrangement: sign column → line numbers → fold column → content.

Table syntax for string options

String options that accept comma-separated values can also be set using Lua tables. The elements are joined with commas automatically.

-- These are equivalent:
vim.opt.workspacenavviewtypes = "markdown,graph,pdf,canvas"
vim.opt.workspacenavviewtypes = {"markdown", "graph", "pdf", "canvas"}

See settings for the full list of options and their descriptions.

Supported vim.fn functions

A subset of Neovim’s vim.fn.* functions is available for conditional configuration and platform detection.

FunctionReturnsExample
vim.fn.has(feature)1 or 0if vim.fn.has("mac") == 1 then
vim.fn.expand("%")Vault-relative file pathvim.fn.expand("%")"folder/note.md"
vim.fn.expand("%:t")Filename onlyvim.fn.expand("%:t")"note.md"
vim.fn.expand("%:e")Extension onlyvim.fn.expand("%:e")"md"
vim.fn.expand("%:r")Path without extensionvim.fn.expand("%:r")"folder/note"
vim.fn.fnamemodify(path, mods)Modified pathvim.fn.fnamemodify("a/b.md", ":t:r")"b"
vim.fn.exists(expr)1 if exists, 0 otherwisevim.fn.exists("g:my_var")
vim.fn.localtime()Unix timestamp (seconds)vim.fn.localtime()
vim.fn.strftime(fmt)Formatted date stringvim.fn.strftime("%Y-%m-%d")
vim.fn.filereadable(path)1 if vault file existsvim.fn.filereadable("config.md")
vim.fn.isdirectory(path)1 if vault directory existsvim.fn.isdirectory("templates")
vim.fn.glob(pattern)Newline-separated file listvim.fn.glob("*.md")
vim.fn.mode()Current mode stringvim.fn.mode()"n", "i", "v"
vim.fn.line(expr)Cursor line (1-based)vim.fn.line(".") (callbacks only)
vim.fn.col(expr)Cursor column (1-based)vim.fn.col(".") (callbacks only)
vim.fn.getline(expr)Line content stringvim.fn.getline(".") (callbacks only)
vim.fn.tolower(s)Lowercase stringvim.fn.tolower("Hello")"hello"
vim.fn.toupper(s)Uppercase stringvim.fn.toupper("Hello")"HELLO"
vim.fn.trim(s)Trimmed stringvim.fn.trim(" hi ")"hi"
vim.fn.strlen(s)String lengthvim.fn.strlen("hello")5
vim.fn.strwidth(s)Display widthvim.fn.strwidth("hello")5
vim.fn.stridx(s, needle)First index of needlevim.fn.stridx("hello", "ll")2
vim.fn.strridx(s, needle)Last index of needlevim.fn.strridx("abab", "ab")2
vim.fn.strpart(s, start, len?)Substringvim.fn.strpart("hello", 1, 3)"ell"
vim.fn.substitute(s, pat, sub, flags)Regex replacevim.fn.substitute("hi", "h", "H", "")"Hi"
vim.fn.nr2char(n)Character from code pointvim.fn.nr2char(65)"A"
vim.fn.char2nr(c)Code point from charactervim.fn.char2nr("A")65
vim.fn.split(s, sep?)List (table) of partsvim.fn.split("a,b", ",")
vim.fn.join(list, sep?)Joined stringvim.fn.join({"a","b"}, "-")"a-b"

vim.fn.has() features

FeatureReturns 1 when
"mac" / "macunix"macOS
"linux"Linux desktop
"win32" / "win64"Windows
"unix"macOS or Linux
"mobile"Mobile device (iOS or Android)
"desktop"Desktop device
"ios"iOS
"android"Android
"obsidian"Always (running in Obsidian)
"obsidian-X.Y"Obsidian version >= X.Y
"nvim"Never (not Neovim)
"vim"Never (not Vim)

All other feature strings return 0. Use vim.fn.has("obsidian-1.7") to check for a minimum Obsidian version.

vim.fn.exists() expressions

ExpressionChecks
"g:varname"Whether vim.g.varname has been set
"&option"Whether a vim.opt option exists
"*funcname"Whether a vim.fn function is implemented

vim.fn.fnamemodify() modifiers

ModifierResultExample with "folder/note.md"
:tFilename with extension"note.md"
:rRemove last extension"folder/note"
:eExtension only"md"
:hDirectory part"folder"
:t:rFilename without extension"note" (chained)
:pVault-relative path (identity)"folder/note.md"

vim.fn.line() and vim.fn.col()

These functions return cursor position (1-based) and are only meaningful inside function callbacks. At config-load time they return 0 because no editor is active.

vim.keymap.set("n", "<leader>h", function()
    if vim.fn.line(".") == 1 then
        vim.notify("Already at top!")
    else
        vim.cmd("normal! gg")
    end
end, { desc = "Smart go-to-top" })

Conditional config examples

-- Per-platform settings
if vim.fn.has("mobile") == 1 then
    vim.opt.easymotion = false
    vim.opt.hintmode = false
end
 
-- Check if a templates directory exists
if vim.fn.isdirectory("templates") == 1 then
    vim.g.has_templates = true
end
 
-- Per-filetype keymaps (inside function callbacks)
vim.keymap.set("n", "<leader>p", function()
    if vim.fn.expand("%:e") == "md" then
        vim.cmd("obcommand markdown:toggle-preview")
    end
end, { desc = "Toggle preview" })
 
-- User feedback via vim.notify
vim.keymap.set("n", "<leader>r", function()
    vim.cmd("obcommand app:reload")
    vim.notify("Reloaded!")
end, { desc = "Reload" })
 
-- Check if a config file exists
if vim.fn.filereadable("vim-motions-config.md") == 1 then
    vim.opt.scrolloff = 10
end

File paths are vault-relative

vim.fn.expand("%"), vim.fn.filereadable(), vim.fn.isdirectory(), and vim.fn.glob() use vault-relative paths. Absolute filesystem paths and .. path traversal are not supported for security.

Table and string utilities

A subset of Neovim’s vim.* utility functions is available for table manipulation, string operations, and debugging.

FunctionDescriptionExample
vim.tbl_deep_extend(behavior, ...)Recursive table merge. "force" = rightmost wins, "keep" = leftmost wins, "error" = throw on conflict. Lists are atomic (replaced, not merged).vim.tbl_deep_extend("force", {a=1}, {a=2, b=3})
vim.tbl_extend(behavior, ...)Shallow table merge (same behaviors as above)vim.tbl_extend("force", defaults, opts)
vim.tbl_contains(t, value, opts?)Check if table contains value. With {predicate=true}, value is called as a function.vim.tbl_contains({1,2,3}, 2)
vim.tbl_keys(t)Returns list of all keysvim.tbl_keys({a=1, b=2})
vim.tbl_values(t)Returns list of all valuesvim.tbl_values({a=1, b=2})
vim.tbl_map(fn, t)Map function over table valuesvim.tbl_map(function(v) return v*2 end, {1,2,3})
vim.tbl_filter(fn, t)Filter table by predicatevim.tbl_filter(function(v) return v > 1 end, {1,2,3})
vim.tbl_count(t)Count entries in tablevim.tbl_count({a=1, b=2})2
vim.tbl_isempty(t)Check if table is emptyvim.tbl_isempty({})true
vim.tbl_get(t, ...)Safe nested accessvim.tbl_get({a={b=42}}, "a", "b")42
vim.list_extend(dst, src)Append elements from src to dstvim.list_extend({1,2}, {3,4})
vim.deepcopy(t)Deep copy a tablelocal copy = vim.deepcopy(original)
vim.split(s, sep, opts?)Split string. {plain=true} for literal sep, {trimempty=true} to trim empty parts.vim.split("a,b,c", ",")
vim.trim(s)Strip whitespace from both endsvim.trim(" hi ")"hi"
vim.startswith(s, prefix)Check if string starts with prefixvim.startswith("hello", "hel")true
vim.endswith(s, suffix)Check if string ends with suffixvim.endswith("hello", "lo")true
vim.pesc(s)Escape Lua pattern special charactersvim.pesc("a.b")"a%.b"
vim.inspect(value)Human-readable string representation of any value. Useful for debugging.print(vim.inspect({1,2,{nested=true}}))
vim.stricmp(a, b)Case-insensitive string comparison. Returns -1 (a < b), 0 (equal), or 1 (a > b).vim.stricmp("Hello", "hello")0

JSON

FunctionDescriptionExample
vim.json.encode(value)Encode Lua value to JSON stringvim.json.encode({a=1})'{"a":1}'
vim.json.decode(str)Decode JSON string to Lua valuevim.json.decode('{"x":42}').x42

Regular expressions

vim.regex(pattern, flags?) creates a regex object wrapping JavaScript’s RegExp. This uses ECMAScript regex syntax, not Vim regex syntax.

MethodDescriptionExample
vim.regex(pattern, flags?)Create a regex object. flags is optional (e.g. "g", "i", "gi")local re = vim.regex("\\d+")
re:match_str(str)Returns 0-based start, end byte offsets of first match, or nilvim.regex("hello"):match_str("hello world")0, 5
re:match_line(str)Alias for match_strvim.regex("world"):match_line("hello world")6, 11
re:match_pos(str, start?)Match starting from byte offset (default 0)vim.regex("o"):match_pos("hello world", 5)7, 8
re:replace(str, replacement)Replace match(es). Use "g" flag for global replacevim.regex("o", "g"):replace("foo", "0")"f00"
re:test(str)Returns true if pattern matches, false otherwisevim.regex("^hello"):test("hello world")true

Invalid patterns raise a Lua error catchable with pcall:

local ok, err = pcall(vim.regex, "[invalid")
-- ok = false, err contains "invalid regular expression"

ECMAScript regex, not Vim regex

vim.regex() uses JavaScript’s RegExp engine (ECMAScript syntax), not Vim’s regex syntax. This means patterns like \d, \w, [A-Z], and lookahead/lookbehind work as in JavaScript. Vim-specific atoms like \v, \m, \zs are not supported.

Return value convention

All match methods return 0-based byte offsets, matching Neovim’s vim.regex():match_str() convention. This differs from Lua’s string.find() which returns 1-based indices.

Notifications

FunctionDescriptionExample
vim.notify(msg, level?)Show notification. Level from vim.log.levels (default: INFO). ERROR/WARN show Notice + console.vim.notify("Saved!", vim.log.levels.INFO)
vim.notify_once(msg, level?)Same as vim.notify but only shows once per messagevim.notify_once("Migration complete")

vim.log.levels

LevelValueBehavior
vim.log.levels.TRACE0Console only
vim.log.levels.DEBUG1Console only
vim.log.levels.INFO2Obsidian Notice + console
vim.log.levels.WARN3Obsidian Notice + console.warn
vim.log.levels.ERROR4Obsidian Notice + console.error
vim.log.levels.OFF5No output

Snippets

Define snippets using a LuaSnip-inspired DSL. Static snippets compile to VS Code JSON at load time.

FunctionDescription
vim.snippet.s(name, nodes, opts?)Create a snippet definition
vim.snippet.t(text)Static text node
vim.snippet.i(index, default?)Editable tabstop (index 0 = final position)
vim.snippet.c(index, choices)Choice node (list of t() nodes)
vim.snippet.rep(index)Mirror/repeat another tabstop
vim.snippet.fmt(str, nodes, opts?)Format string — {} replaced by nodes in order
vim.snippet.f(fn, deps)Function node — computes text from dependency fields
vim.snippet.d(index, fn, deps)Dynamic node — generates sub-snippet from field values
vim.snippet.sn(index, nodes, opts?)Snippet node — return value for d() callbacks
vim.snippet.r(index, type_name?)Restore node — preserves edits across d() regeneration
vim.snippet.add(trigger, snippet)Register a snippet with a trigger prefix
vim.snippet.add_all(table)Register multiple snippets ({trigger = snippet, ...})
local s = vim.snippet.s
local t = vim.snippet.t
local i = vim.snippet.i
local fmt = vim.snippet.fmt
 
vim.snippet.add("meta", s("Frontmatter", fmt([[
---
title: {}
date: {}
tags: [{}]
---
{}
]], { i(1, "Title"), i(2, "2026-01-01"), i(3, "tag"), i(0) })))

Options for vim.snippet.s():

  • context — restrict snippet to "prose", "code:*", "code:js", or "frontmatter"
  • description — shown in the snippet picker

See snippets for the complete snippet reference.

Async and timers

FunctionDescriptionExample
vim.schedule(fn)Defer function to next event loop iteration. Useful for breaking recursive autocmd loops.vim.schedule(function() vim.g.x = true end)
vim.schedule_wrap(fn)Returns a function that wraps fn with vim.schedule, passing all arguments through.timer:start(100, 0, vim.schedule_wrap(callback))
vim.defer_fn(fn, timeout)Defer function by timeout ms. Returns a handle with stop(), close(), is_closing().vim.defer_fn(function() vim.notify("Done") end, 1000)

vim.uv (timers)

A subset of Neovim’s vim.uv (libuv bindings) is available for timer operations. vim.loop is an alias.

FunctionDescription
vim.uv.new_timer()Create a timer with start(delay, repeat, callback), stop(), close(), is_closing(), is_active()
vim.uv.hrtime()High-resolution time in nanoseconds
vim.uv.now()Current time in milliseconds
-- Debounced autosave
local timer = vim.uv.new_timer()
vim.api.nvim_create_autocmd("FocusLost", {
    callback = function()
        timer:stop()
        timer:start(500, 0, vim.schedule_wrap(function()
            vim.cmd("w")
        end))
    end,
})

Mapping examples

-- Normal mode mapping
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })
 
-- Insert mode escape
vim.keymap.set("i", "jk", "<Esc>", { desc = "Exit insert mode" })
 
-- Multiple modes
vim.keymap.set({"n", "v"}, "<leader>y", '"+y', { desc = "Yank to clipboard" })
 
-- Function callback
vim.keymap.set("n", "<leader>e", function()
    vim.cmd("obcommand file-explorer:reveal-active-file")
end, { desc = "Reveal in explorer" })
 
-- Remove default mapping
vim.keymap.del("n", "Q")

Choosing between vim.cmd() and vim.obsidian.leader.add()

For leader-prefixed commands that execute Obsidian commands, vim.obsidian.leader.add() is the simplest approach — it automatically registers which-key labels. vim.keymap.set with function callbacks gives you more flexibility (conditional logic, vim.fn checks, vim.notify) but requires an explicit desc option for which-key labels.

Which-key auto-resolution with function callbacks

String RHS mappings like vim.keymap.set("n", "<leader>r", ":ob app:go-back<CR>") auto-resolve the Obsidian command name in the which-key popup. Function callbacks wrapping vim.cmd("ob ...") do not — Lua functions are opaque and cannot be introspected. Always provide a desc option when using function callbacks, or use a string RHS for automatic resolution.

Buffer-local keymaps

Keymaps can be scoped to specific files using the buffer option:

vim.api.nvim_create_autocmd("BufEnter", {
    pattern = "*.md",
    callback = function()
        vim.keymap.set("n", "gd", function()
            vim.cmd("obcommand editor:follow-link")
        end, { buffer = 0, desc = "Follow link" })
    end,
})

Use buffer = 0 for the current file. Buffer-local keymaps are automatically swapped when switching between files.

Buffer numbers

Obsidian does not use Neovim-style buffer numbers. Only buffer = 0 (current file) is supported. Positive buffer numbers produce an error.

Keymap accumulation

When setting buffer-local keymaps inside a BufEnter autocmd, always use nvim_create_augroup with { clear = true } (as shown above). Without an augroup, each file switch adds another copy of the keymap.

Buffer content

Read and modify editor content from Lua callbacks:

FunctionDescriptionExample
vim.api.nvim_buf_get_lines(0, start, end, strict)Get lines (0-based, end-exclusive, -1 = EOF)vim.api.nvim_buf_get_lines(0, 0, -1, true)
vim.api.nvim_buf_set_lines(0, start, end, strict, lines)Set lines (empty table = delete)vim.api.nvim_buf_set_lines(0, 0, 0, true, {"new"})
vim.api.nvim_get_current_buf()Returns 0 (current buffer)local buf = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_get_name(0)Vault-relative file pathvim.api.nvim_buf_get_name(0)
vim.api.nvim_buf_line_count(0)Total line countvim.api.nvim_buf_line_count(0)

Buffer argument

Only buffer = 0 (current buffer) is supported. These functions operate on the active editor.

Custom ex commands

Define custom commands that are usable from the : ex command line.

-- Simple alias
vim.api.nvim_create_user_command("W", "w", {})
vim.api.nvim_create_user_command("Q", "q", {})
 
-- Command calling a Lua function
vim.api.nvim_create_user_command("Today", function()
    vim.cmd("obcommand daily-notes:open-today")
    vim.notify("Opened today's note")
end, {})
 
-- Command with arguments
vim.api.nvim_create_user_command("Open", function(opts)
    vim.cmd("obcommand switcher:open " .. opts.args)
end, {})
 
-- Toggle command
vim.api.nvim_create_user_command("SpellToggle", function()
    -- Toggle a user variable and notify
    if vim.g.spell_enabled then
        vim.g.spell_enabled = false
        vim.notify("Spell check disabled")
    else
        vim.g.spell_enabled = true
        vim.notify("Spell check enabled")
    end
end, {})

Registered commands are immediately available via :CommandName in the ex command line. The function callback receives an opts table with an args field containing the argument string.

Autocommands

Vim Motions supports a Neovim-compatible autocommand system for reacting to editor events.

Per-view mode events

Mode events (InsertEnter, InsertLeave, ModeChanged) and cursor/yank/cmdline events (CursorMoved, CursorHold, TextYankPost, CmdlineEnter, CmdlineLeave) fire per-view across all editors — split panes, popover editors, and canvas card text inputs — when using the bundled vim fork (recommended setup). This means autocmd callbacks work in every editor, not just the active leaf.

Supported events

EventWhen it firesPattern support
InsertEnterEntering insert or replace mode (per-view)No
InsertLeaveLeaving insert or replace mode (per-view)No
CursorMovedAfter cursor moves in normal mode (per-view)No
CursorHoldAfter cursor is idle for updatetime ms (per-view)No
ModeChangedAny mode transition (per-view)"old:new" with * wildcard
BufEnterA file becomes the active noteVault-relative path globs ("*.md", "projects/**")
BufLeaveA file is deactivated (switching away)Vault-relative path globs
BufWritePreBefore saving a fileVault-relative path globs
BufWritePostAfter saving a fileVault-relative path globs
LeafEnterA leaf (tab/pane) gains focus (debounced 50ms)No
LeafLeaveA leaf (tab/pane) loses focusNo
FileTypeAfter BufEnter when filetype is detectedNo
FocusGainedObsidian window gains focusNo
FocusLostObsidian window loses focusNo
TextYankPostAfter yank, delete, or change operation (per-view)No
OilEnterAn oil explorer buffer becomes activeNo
OilLeaveLeaving an oil explorer bufferNo
CmdlineEnterOpening :, /, or ? prompt (per-view, active only)No (data.cmdtype = ":", "/", or "?")
CmdlineLeaveClosing a command-line prompt (per-view, active only)No (data.cmdtype = ":", "/", or "?")

CursorHold timing

Configure the idle timeout with vim.opt.updatetime = 1000 (milliseconds). Default is 4000ms, matching Neovim.

FileType detection

FileType detection is based on file extension (e.g. mdmarkdown, tstypescript, pypython). If a filetype is unknown, the FileType event does not fire.

Usage examples

-- Augroup with clear (recommended for config reloads)
local g = vim.api.nvim_create_augroup("my-config", { clear = true })
 
-- Notify on insert mode
vim.api.nvim_create_autocmd("InsertEnter", {
    group = g,
    callback = function()
        vim.notify("Insert mode")
    end,
})
 
-- Per-folder settings
vim.api.nvim_create_autocmd("BufEnter", {
    group = g,
    pattern = "projects/**",
    callback = function(ev)
        vim.opt.shiftwidth = 4
    end,
})
 
-- React to mode changes
vim.api.nvim_create_autocmd("ModeChanged", {
    group = g,
    pattern = "*:i",
    callback = function(ev)
        -- ev.data.old_mode and ev.data.new_mode available
    end,
})
 
-- Auto-save on focus lost
vim.api.nvim_create_autocmd("FocusLost", {
    group = g,
    callback = function()
        vim.cmd("w")
    end,
})
 
-- Track yank operations
vim.api.nvim_create_autocmd("TextYankPost", {
    group = g,
    callback = function(ev)
        -- ev.data.operator ("y", "d", "c")
        -- ev.data.regcontents (table of lines)
        -- ev.data.regtype ("V" linewise, "v" charwise)
        -- ev.data.regname (register name, e.g. "a", "" for default)
        -- ev.data.visual (boolean)
    end,
})

Callback event data

The callback receives a table with the following fields:

{
    event = "BufEnter",
    file = "projects/todo.md",  -- vault-relative
    match = "projects/todo.md",
    buf = 0,                    -- always 0
    id = 42,                    -- autocmd ID
    group = 1,                  -- group ID or nil
    data = nil,                 -- event-specific (TextYankPost, ModeChanged)
}

For LeafEnter and LeafLeave, data includes { type = "markdown", leaf_id = "..." } and match is set to the leaf type. For FileType, match is the detected filetype (for example, markdown).

Augroup management

local g = vim.api.nvim_create_augroup("name", { clear = true })
vim.api.nvim_del_autocmd(id)
vim.api.nvim_del_augroup_by_name("name")
vim.api.nvim_clear_autocmds({ group = g, event = "InsertEnter" })

ModeChanged pattern format

  • "n:i": normal to insert
  • "*:i": any mode to insert
  • "i:*": insert to any mode
  • "*:*": any transition

vim.keymap.set options

OptionTypeDefaultDescription
descstring(none)Description shown in which-key popup. Required for function callbacks — string RHS with :ob/:obcommand auto-resolves the command name, but function callbacks are opaque and need an explicit description.
noremapbooleantrueNon-recursive mapping
remapbooleanfalseRecursive mapping (inverse of noremap)
silentboolean(none)Accepted but no effect in Obsidian
nowaitboolean(none)Accepted but no effect in Obsidian
buffernumber/boolean(none)Buffer-local keymap (0 or true = current file). See Buffer-local keymaps above. Non-zero numbers error.
exprbooleanfalseIf true, the callback must return a string that is fed as keystrokes. Sync only — async APIs cannot be used in expr callbacks. String rhs not supported for expr.

Obsidian namespace

Obsidian-specific APIs that don’t exist in Neovim. Available as vim.obsidian or vim.ob.

FunctionReturnsExample
vim.obsidian.vault_name()Vault namevim.obsidian.vault_name()
vim.obsidian.app_version()Obsidian version stringvim.obsidian.app_version()
vim.obsidian.plugin_version()Plugin version stringvim.obsidian.plugin_version()
vim.obsidian.run_command(id)Execute Obsidian command by IDvim.obsidian.run_command("app:reload")
vim.obsidian.list_commands()Table of {id, name}vim.obsidian.list_commands()
vim.obsidian.open_file(path)Open a vault filevim.obsidian.open_file("notes/todo.md")
vim.obsidian.pick(source, opts?)Open a picker sourcevim.obsidian.pick("files")
vim.obsidian.current_file()Table {path, name, extension, basename} or nilvim.obsidian.current_file().path
vim.obsidian.vault_path()Vault absolute path (desktop only)vim.obsidian.vault_path()

Picker sources include files, buffers, commands, headings, outline, backlinks, tags, recent, marks, registers, grep, and resume (reopen the last picker session).

-- Open files picker
vim.obsidian.pick('files')
 
-- Open grep with pre-filled query
vim.obsidian.pick('grep', { query = 'todo' })
 
-- Resume last session
vim.obsidian.pick('resume')

Workspace and leaf management

FunctionReturnsExample
vim.ob.get_active_leaf()Table {id, type, pinned}local leaf = vim.ob.get_active_leaf()
vim.ob.get_leaf_type()View type string (e.g., "markdown")if vim.ob.get_leaf_type() == "pdf" then
vim.ob.list_leaves()Table of leaf info tablesfor _, leaf in ipairs(vim.ob.list_leaves())
vim.ob.is_markdown_view()Booleanif vim.ob.is_markdown_view() then
vim.ob.get_leaf_for_file(path)Leaf info table or nilvim.ob.get_leaf_for_file("note.md")
vim.ob.focus(direction)Boolean (success)vim.ob.focus("right")
vim.ob.split(direction)Boolean (success)vim.ob.split("vertical")
vim.ob.close_leaf()Boolean (success)vim.ob.close_leaf()

Note operations

FunctionDescriptionExample
vim.ob.follow_link()Follow link under cursorvim.ob.follow_link()
vim.ob.backlinks()Open backlinks for current notevim.ob.backlinks()
vim.ob.daily()Open today’s daily notevim.ob.daily()
vim.ob.search()Open global searchvim.ob.search()
vim.ob.tags()Open tags viewvim.ob.tags()
vim.ob.new_note()Create new notevim.ob.new_note()
vim.ob.rename()Rename current notevim.ob.rename()
vim.ob.toggle_checkbox()Toggle checkbox on current linevim.ob.toggle_checkbox()
vim.ob.template()Open template pickervim.ob.template()

vim.ob.meta — Note metadata

FunctionReturnsDescription
vim.ob.meta.frontmatter(path?)table or nilYAML frontmatter as key-value pairs
vim.ob.meta.tags(path?)string[]Combined body + frontmatter tags
vim.ob.meta.links(path?){link, display, original}[]Outgoing wikilinks and markdown links
vim.ob.meta.backlinks(path?)string[]Source file paths linking to this note
vim.ob.meta.headings(path?){heading, level}[]Headings with H1-H6 level
vim.ob.meta.embeds(path?){link, display}[]Embedded content (![[...]])
vim.ob.meta.aliases(path?)string[]YAML aliases
vim.ob.meta.tasks(path?){text, status, line}[]Checklist items with status char
vim.ob.meta.lists(path?){text, line, indent}[]All list items

All meta.* functions default to the current file when path is omitted.

vim.ob.fs — Vault filesystem

FunctionDescriptionAsync
vim.ob.fs.read(path)Read file content as stringYes
vim.ob.fs.readlines(path)Read file content as table of linesYes
vim.ob.fs.files(pattern?)Markdown files matching optional globNo
vim.ob.fs.all_files()All files in vaultNo
vim.ob.fs.folders()All foldersNo
vim.ob.fs.exists(path)Check if file existsNo
vim.ob.fs.stat(path?)File stats {ctime, mtime, size}No
vim.ob.fs.create(path, content?)Create new fileNo
vim.ob.fs.write(content) or write(path, content)Overwrite file contentNo
vim.ob.fs.append(content) or append(path, content)Append to fileNo
vim.ob.fs.rename(new_path) or rename(path, new_path)Rename (updates backlinks)No
vim.ob.fs.move(dest) or move(path, dest)Move to folder or new pathNo
vim.ob.fs.trash(path?)Move to trash (user preference)No

Async functions yield the Lua coroutine internally and resume when the operation completes. They work in keymap callbacks, autocmd handlers, timer callbacks, user commands, and at the top level of init.lua. They are blocked in snippet f()/d() nodes. Errors from async functions are catchable with pcall.

local content = vim.ob.fs.read("notes/todo.md")
vim.notify("File has " .. #content .. " chars")
 
local lines = vim.ob.fs.readlines("notes/todo.md")
vim.notify("File has " .. #lines .. " lines")
 
local ok, err = pcall(vim.ob.fs.read, "nonexistent.md")
if not ok then vim.notify("Error: " .. err) end

Write operations silently reject paths inside .obsidian/. Write/append/rename/move/trash default to the current file when path is omitted.

vim.ob.ui — UI control

FunctionDescription
vim.ob.ui.sidebar(side, state?)Toggle sidebar ("left", "right")
vim.ob.ui.command_palette()Open command palette
vim.ob.ui.quickswitch()Open quick switcher
vim.ob.ui.notice(msg)Show notification (alias for vim.notify)

vim.obsidian.oil — Oil explorer

Functions for controlling the oil explorer. All functions are also available as ex commands (e.g., :oilparent).

FunctionDescription
vim.obsidian.oil.open(path)Open oil for a directory
vim.obsidian.oil.close()Close oil buffer
vim.obsidian.oil.parent()Navigate to parent directory
vim.obsidian.oil.root()Navigate to vault root
vim.obsidian.oil.refresh()Refresh current listing
vim.obsidian.oil.toggle_hidden()Toggle dotfile visibility
vim.obsidian.oil.cycle_sort()Cycle sort order
vim.obsidian.oil.yank_path()Copy file path to clipboard
vim.obsidian.oil.reveal()Reveal in Obsidian file explorer
vim.obsidian.oil.open_entry()Open file/directory under cursor

Use OilEnter / OilLeave autocmd events to set buffer-local keymaps:

vim.api.nvim_create_autocmd('OilEnter', {
    callback = function()
        vim.keymap.set('n', 'l', function()
            vim.obsidian.oil.open_entry()
        end, { buffer = 0 })
    end
})

Editor state

FunctionDescription
vim.ob.get_cursor()Cursor position {line, col} (1-indexed)
vim.ob.set_cursor(line, col)Set cursor (1-indexed)
vim.ob.get_selection()Visual selection text or nil
vim.ob.mode()Current vim mode (alias for vim.fn.mode())
vim.ob.notice(msg)Show notification (alias for vim.notify)

Global keymaps (vim.obsidian.keymap)

Define key bindings for non-editor contexts (graph view, canvas, PDF viewer, file explorer, reading mode). These bindings work when no editor is focused.

FunctionDescription
vim.obsidian.keymap.set(lhs, rhs, opts?)Create a global key mapping
vim.obsidian.keymap.del(lhs)Remove a global key mapping

The rhs must be either :obcommand <command-id> or :<ex-command>:

vim.obsidian.keymap.set("<leader>f", ":obcommand switcher:open", { desc = "Open file" })
vim.obsidian.keymap.set("<leader>e", ":obcommand file-explorer:reveal-active-file", { desc = "Reveal in explorer" })
vim.obsidian.keymap.set("<leader>s", ":sidebar left", { desc = "Toggle sidebar" })
 
vim.obsidian.keymap.del("<leader>f")

The desc option automatically creates a label in the global which-key popup.

String-only RHS

Only string commands are supported as RHS (:obcommand ... or :ex-command). Lua function callbacks are not supported for global keymaps. Use vim.api.nvim_create_user_command to define a named command, then reference it.

No mode parameter

Global keymaps are mode-agnostic — they don’t use vim modes. The noremap option is accepted for compatibility but has no effect.

Which-key labels (vim.obsidian.whichkey)

Set group and command labels for the which-key popup. Labels from vim.keymap.set’s desc option are applied automatically for editor keymaps, but this API adds group labels, labels for keys you didn’t create, and global context labels.

FunctionDescription
vim.obsidian.whichkey.set_group(key, label, opts?)Name a which-key group by prefix
vim.obsidian.whichkey.set_label(key, label, opts?)Label an individual which-key binding
vim.obsidian.whichkey.add(entries)Batch-add group and command labels
vim.obsidian.whichkey.set_group("<leader>t", "Table")
vim.obsidian.whichkey.set_group("<leader>g", "Git")
vim.obsidian.whichkey.set_label("<leader>w", "Save file")
 
-- For global (non-editor) which-key:
vim.obsidian.whichkey.set_group("<leader>", "+leader", { context = "global" })
vim.obsidian.whichkey.set_label("<leader>f", "Open file", { context = "global" })

The context option defaults to "editor". Use { context = "global" } for labels in the non-editor which-key overlay.

The add() function accepts a table of entries for batch configuration, similar to which-key.nvim’s wk.add():

local wk = vim.obsidian.whichkey
wk.add({
    { "<leader>f", group = "Find" },
    { "<leader>g", group = "Git" },
    { "<leader>t", group = "Table" },
    { "<leader>w", desc = "Save file" },
    { "<leader>q", desc = "Close tab" },
})

Each entry uses group for prefix labels or desc for individual binding labels. The context and mode fields are supported per entry (mode is reserved for future use).

See which-key > Batch labels (`add()`) for details.

Cursor shapes (vim.obsidian.cursor)

Set cursor shapes for each vim mode using a structured table instead of the guicursor format string.

FunctionDescription
vim.obsidian.cursor.set(table)Set cursor shapes (partial tables allowed)
vim.obsidian.cursor.set({
    normal = "block",
    insert = "bar",
    visual = "block",
    replace = "underline",
    operator_pending = "underline",
})

Valid shapes: "block", "bar", "underline", "hollow". Modes not specified keep their current value. This is equivalent to vim.opt.guicursor but uses a table instead of Neovim’s format string.

See cursor-shapes for the full list of modes and shapes.

Mode prompts (vim.obsidian.modeprompt)

Set the status bar mode text for multiple modes in a single call.

FunctionDescription
vim.obsidian.modeprompt.set(table)Set mode prompts (partial tables allowed)
vim.obsidian.modeprompt.set({
    normal = "NOR",
    insert = "INS",
    visual = "VIS",
    visual_line = "V-LN",
    visual_block = "V-BLK",
})

Valid mode keys: normal, insert, visual, replace, visual_line, visual_block, select, vreplace, command, search, insert_normal. This is equivalent to setting individual vim.g.mode_prompt_* variables but allows batch configuration.

See status-bar for details on status bar customization.

Custom surround pairs (vim.obsidian.surround)

Define custom character-to-delimiter mappings for surround operations (ys, ds, cs).

FunctionDescription
vim.obsidian.surround.set(trigger, opts)Register a custom surround pair
vim.obsidian.surround.del(trigger)Remove a custom surround pair
vim.obsidian.surround.add(entries)Batch-register custom surround pairs
vim.obsidian.surround.set("l", { left = "[[", right = "]]" })
vim.obsidian.surround.set("m", { left = "$$", right = "$$" })
 
vim.obsidian.surround.add({
    { "l", left = "[[", right = "]]" },
    { "m", left = "$$", right = "$$" },
    { "e", left = "\\begin{equation}", right = "\\end{equation}" },
})

After registration, ysiw l wraps a word in [[word]], ds l removes surrounding [[...]], and cs l m changes [[...]] to $$...$$.

The trigger must be a single character. Built-in surround characters ((, ), [, ], {, }, <, >, b, B, r, a, t, T, f, F, ", ', `) are reserved and cannot be overridden.

Fork mode required

Custom surround pairs require the plugin’s bundled fork mode. Disable Obsidian’s built-in Vim mode in Settings → Editor → Vim key bindings for full support.

Leader bindings (vim.obsidian.leader)

Convenience API for binding leader key sequences to Obsidian commands. Automatically prepends the leader key, adds the :ob command prefix, and registers a which-key label from the desc option.

FunctionDescription
vim.obsidian.leader.set(key, commandId, opts?)Bind leader+key to a command
vim.obsidian.leader.del(key)Remove a leader binding
vim.obsidian.leader.add(entries)Batch-register leader bindings
vim.g.mapleader = " "
 
vim.obsidian.leader.set("e", "file-explorer:reveal-active-file", { desc = "Reveal in explorer" })
vim.obsidian.leader.set("p", "command-palette:open", { desc = "Command palette" })
 
vim.obsidian.leader.add({
    { "ff", "switcher:open", desc = "Find file" },
    { "fg", "global-search:open", desc = "Grep" },
    { "t", "daily-notes:open-today", desc = "Today" },
})

The second argument is an Obsidian command ID (the same IDs shown by :ob with no arguments). For general-purpose keymaps or Lua function callbacks, use vim.keymap.set instead.

Input method switching (vim.obsidian.im)

Programmatic control over input method (IM) switching for CJK users (per-view across all editors). Requires an external IM switching binary and configuration in Settings → Vim Motions → Input method. Desktop only.

Function/PropertyReturnsDescription
vim.obsidian.im.get()string|nilCurrent IM identifier (cached), or nil if unavailable
vim.obsidian.im.set(id)Switch to specific IM identifier
vim.obsidian.im.save()Save current IM for the active editor view
vim.obsidian.im.restore()Restore saved IM for the active editor view
vim.obsidian.im.enabledbooleanRead/write: master toggle for IM switching
vim.obsidian.im.autobooleanRead/write: auto-wire to InsertEnter/InsertLeave

When vim.obsidian.im.auto is true (default), the plugin automatically saves/restores IM on mode changes across all editor views. Set it to false for manual control:

vim.obsidian.im.auto = false
 
vim.api.nvim_create_autocmd('InsertLeave', {
    callback = function()
        vim.obsidian.im.save()
        vim.obsidian.im.set('com.apple.keylayout.ABC')
    end
})
 
vim.api.nvim_create_autocmd('InsertEnter', {
    callback = function()
        vim.obsidian.im.restore()
    end
})

Desktop only

vim.obsidian.im.get() returns nil on mobile. All other functions are silent no-ops.

Environment variables

vim.env provides a sandboxed environment variable proxy:

KeyValue
vim.env.HOMEVault absolute path (desktop)
vim.env.VIMRUNTIME"obsidian"
vim.env.VIM"motions"
vim.env.TERM"obsidian"
vim.env.OBSIDIAN_VERSIONObsidian version string
vim.env.MYVIMRC"init.lua"

Custom variables can be set: vim.env.MY_VAR = "value". Unknown keys return nil.

Mode prompt customization

Customize the text shown in the status bar for each vim mode using vim.g.mode_prompt_*:

VariableModeDefault
vim.g.mode_prompt_normalNormalNORMAL
vim.g.mode_prompt_insertInsertINSERT
vim.g.mode_prompt_visualVisualVISUAL
vim.g.mode_prompt_replaceReplaceREPLACE
vim.g.mode_prompt_visual_lineVisual LineV-LINE
vim.g.mode_prompt_visual_blockVisual BlockV-BLOCK
vim.g.mode_prompt_selectSelectSELECT
vim.g.mode_prompt_vreplaceVirtual ReplaceV-REPLACE
vim.g.mode_prompt_commandCommandCOMMAND
vim.g.mode_prompt_searchSearchSEARCH
vim.g.mode_prompt_insert_normalInsert-NormalNORMAL
vim.g.mode_prompt_normal = "N"
vim.g.mode_prompt_insert = "I"
vim.g.mode_prompt_visual = "V"
vim.g.mode_prompt_replace = "R"

Highlight groups

Customize plugin styling from Lua using Neovim’s nvim_set_hl API:

-- Change EasyMotion label colors
vim.api.nvim_set_hl(0, "EasyMotionTarget", { fg = "#ff5555", bg = "#282a36", bold = true })
 
-- Change status bar mode colors
vim.api.nvim_set_hl(0, "StatusLineNormal", { bg = "#282a36", fg = "#f8f8f2" })
vim.api.nvim_set_hl(0, "StatusLineInsert", { bg = "#50fa7b", fg = "#282a36" })

Plugin-defined highlight groups

These map directly to plugin UI elements via CSS custom properties:

GroupControls
EasyMotionTargetEasyMotion jump labels
EasyMotionShadeEasyMotion dimmed text
HintTargetHint mode labels
StatusLineNormalNormal mode status bar
StatusLineInsertInsert mode status bar
StatusLineVisualVisual mode status bar
StatusLineReplaceReplace mode status bar
StatusLineVLineV-Line mode status bar
StatusLineVBlockV-Block mode status bar
StatusLineCommandCommand mode status bar
StatusLineSearchSearch mode status bar
StatusLineSelectSelect mode status bar
StatusLineVReplaceV-Replace mode status bar

Case-sensitive group names

Highlight group names are case-sensitive. Use the exact casing shown in the table above (e.g., EasyMotionTarget, not easymotiontarget). This differs from Neovim, where highlight group names are case-insensitive.

User-defined highlight groups

Custom groups generate CSS classes (.vim-hl-GroupName) that can be used in CSS snippets:

vim.api.nvim_set_hl(0, "MyHighlight", { fg = "#00ff00", bold = true })

Supported attributes

AttributeTypeCSS mapping
fg / foregroundstringcolor
bg / backgroundstringbackground-color
sp / specialstringtext-decoration-color
boldbooleanfont-weight: bold
italicbooleanfont-style: italic
underlinebooleantext-decoration-line: underline
undercurlbooleantext-decoration: underline wavy
underdoublebooleantext-decoration: underline double
underdottedbooleantext-decoration: underline dotted
underdashedbooleantext-decoration: underline dashed
strikethroughbooleantext-decoration-line: line-through
reversebooleanSwaps fg/bg
blendnumber (0-100)opacity
linkstringInherit from another group
defaultbooleanOnly apply if group not already defined
updatebooleanMerge with existing (don’t replace)

Namespace

Only ns_id = 0 (global namespace) is supported. vim.api.nvim_create_namespace() always returns 0.

Underline styles

Only one underline style can be active per highlight group. If multiple underline attributes (undercurl, underdouble, underdotted, underdashed) are set, only the first one takes effect.

When to use Lua vs Vimrc

  • Use init.lua (recommended) when you need conditional logic (per-vault config), function-based keymaps, or prefer Neovim-style Lua syntax
  • Use vimrc for simple key mappings and option settings if you prefer traditional Vimscript syntax
  • Both can be used together: init.lua loads after vimrc, and Lua values override vimrc values on conflict

Loading order

The plugin follows a specific override hierarchy:

  1. Settings UI values (base)
  2. Vimrc values override Settings UI
  3. init.lua values override both

Override hierarchy

This hierarchy differs from Neovim, which typically uses either init.lua or .vimrc, but not both simultaneously. In Vim Motions, they are additive.

Unsupported Neovim APIs

Obsidian is not Neovim. Many Neovim-specific APIs are not available in this sandboxed environment.

Obsidian is not Neovim

The following Neovim APIs are not available: vim.lsp, vim.treesitter, vim.ui, vim.diagnostic. Attempting to use them produces a clear error message. vim.api is partially supported (nvim_create_user_command, nvim_create_autocmd, nvim_create_augroup, nvim_del_autocmd, nvim_del_augroup_by_name, nvim_clear_autocmds, nvim_set_hl, nvim_get_hl, nvim_create_namespace, nvim_buf_get_lines, nvim_buf_set_lines, nvim_get_current_buf, nvim_buf_get_name, nvim_buf_line_count, nvim_buf_set_keymap, and nvim_buf_del_keymap work, other functions error with a helpful message). vim.fn is partially supported (see above). The Lua runtime is sandboxed: only 6 standard libraries are loaded (_G, string, table, math, coroutine, utf8). The io, os, debug, and package libraries are not available (but package.loaded and package.path are provided by the plugin’s require() implementation). require() loads modules from lua/ in the vault root. load(chunk) compiles string chunks. dofile, loadfile, rawget, rawset, and rawequal are disabled.

collectgarbage behavior

collectgarbage() is available with all standard modes. Since fengari has no garbage collector, behavior differs from PUC-Rio Lua:

ModeBehavior
"collect"Drains the __gc finalizer queue for unreachable userdata
"count"Returns 0, 0 (no memory tracking)
"isrunning"Returns false
Other modesNo-op, returns 0

__gc metamethods on userdata are supported via FinalizationRegistry. Finalizers fire when userdata becomes unreachable from JavaScript and the queue is drained (at collectgarbage("collect"), outermost pcall return, or plugin unload). Finalization timing is non-deterministic and ordering is unspecified. __gc on tables is not supported. Errors in __gc are silently swallowed.

Keymapping mode reference

Mode stringContextDescription
'n'NormalNormal mode mappings
'i'InsertInsert mode mappings
'v'VisualVisual mode (same as 'x')
'x'VisualVisual mode (alias for 'v')
's'SelectSelect mode only
'o'Operator-pendingMaps to normal mode internally

Difference from Neovim

In Neovim, 'v' maps to both visual and select mode. In Vim Motions, 'v' maps to visual mode only. Use {"v", "s"} to map in both visual and select modes.

Unsupported modes

Command-line ('c'), terminal ('t'), and insert+command ('!') modes are not supported.

Multiple modes can be specified as a table: vim.keymap.set({"n", "v"}, ...).

Autocmd event data reference

Every autocmd callback receives an event table with these common fields:

FieldTypeDescription
eventstringEvent name (e.g., "BufEnter")
filestringVault-relative file path
matchstringPattern match string
bufnumberBuffer number (always 0)
idnumberAutocmd ID
groupnumber or nilAugroup ID (nil if no group)
datatable or nilEvent-specific data (see below)

Per-event data fields

Most events set data = nil. Only these events provide event-specific data:

TextYankPost:

FieldTypeDescription
operatorstringOperator used ("y", "d", "c")
regcontentstableTable of yanked lines
regtypestring"V" (linewise), "v" (charwise)
regnamestringRegister name (e.g., "a", "" for default)
visualbooleanWhether the yank was from visual mode

ModeChanged:

FieldTypeDescription
old_modestringMode before transition
new_modestringMode after transition

All other events (InsertEnter, InsertLeave, CursorMoved, CursorHold, BufEnter, BufLeave, BufWritePre, BufWritePost, FocusGained, FocusLost): data = nil.

Highlight group CSS reference

Plugin-defined highlight groups map to CSS custom properties. User-defined groups generate CSS classes.

Plugin groups → CSS variables

GroupCSS variableControls
EasyMotionTarget--vim-motions-emEasyMotion jump labels
EasyMotionShade--vim-motions-em-shadeEasyMotion dimmed text
HintTarget--vim-motions-hintHint mode labels
StatusLineNormal--vim-pl-normalNormal mode status bar
StatusLineInsert--vim-pl-insertInsert mode status bar
StatusLineVisual--vim-pl-visualVisual mode status bar
StatusLineReplace--vim-pl-replaceReplace mode status bar
StatusLineVLine--vim-pl-v-lineV-Line mode status bar
StatusLineVBlock--vim-pl-v-blockV-Block mode status bar
StatusLineCommand--vim-pl-commandCommand mode status bar
StatusLineSearch--vim-pl-searchSearch mode status bar
StatusLineSelect--vim-pl-selectSelect mode status bar
StatusLineVReplace--vim-pl-vreplaceV-Replace mode status bar

Plugin groups update CSS custom properties on the document root (:root). For example, setting fg on StatusLineNormal updates --vim-pl-normal-fg.

User-defined groups

Custom highlight groups generate a CSS class .vim-hl-{GroupName}. Use these in CSS snippets to style custom elements:

vim.api.nvim_set_hl(0, "MyHighlight", { fg = "#00ff00", bold = true })
-- Generates: .vim-hl-MyHighlight { color: #00ff00; font-weight: bold }

Attribute → CSS property mapping

AttributeCSS property
fgcolor
bgbackground-color
sptext-decoration-color
boldfont-weight: bold
italicfont-style: italic
underlinetext-decoration-line: underline
undercurltext-decoration: underline wavy
underdoubletext-decoration: underline double
underdottedtext-decoration: underline dotted
underdashedtext-decoration: underline dashed
strikethroughtext-decoration-line: line-through
reverseSwaps fg/bg values
blendopacity (0–100 → 0.0–1.0)
linkInherit from another group
defaultOnly apply if group not defined
updateMerge with existing (don’t replace)

Lua sandbox reference

The Lua runtime runs in a sandboxed Lua 5.3 environment (fengari).

Available standard libraries

Only 6 standard libraries are loaded:

LibraryDescription
_G (base)Core functions (type, tostring, tonumber, pcall, xpcall, error, select, pairs, ipairs, next, unpack, assert)
stringString manipulation (format, find, gsub, sub, rep, byte, char, len, lower, upper, match, gmatch, reverse)
tableTable manipulation (insert, remove, sort, concat, move, pack, unpack)
mathMath functions (floor, ceil, abs, max, min, random, sqrt, sin, cos, pi, huge, etc.)
coroutineCoroutine support (create, resume, yield, wrap, status)
utf8UTF-8 support (char, codepoint, codes, len, offset, charpattern)

Not available

Library/functionReason
ioStripped from fork (file system access)
osNot loaded by plugin (security)
debugNot loaded by plugin (security)
package (native)Stripped from fork; plugin provides package.loaded/package.path and a custom require()
dofile, loadfileDisabled (no direct file loading)
rawget, rawset, rawequalDisabled (sandbox integrity)

Fork vs plugin

The fengari fork retains browser-safe os functions (os.date, os.time, etc.) and the debug library in its compiled VM. However, the plugin’s sandbox deliberately does not load these libraries. Only the 6 libraries listed above are available to Lua scripts.

Execution limits

  • Config load instruction limit: 1,000,000 Lua VM instructions per execution. Scripts exceeding this limit are terminated with a timeout error.
  • Runtime callback instruction limit: 500,000 instructions for function keymaps, user commands, autocmd handlers, and timer callbacks. 100,000 instructions for snippet dynamic nodes (f()/d()). An infinite loop in a callback shows a throttled error Notice (5-second cooldown) and Obsidian remains responsive.
  • Error handling: Syntax errors, runtime errors, and instruction limit timeouts are caught and displayed as an Obsidian Notice. The plugin continues to load normally.

Error handling

Syntax errors and runtime errors show an Obsidian Notice with the error message. The plugin continues to load normally. Check the developer console for details.