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:
init.lua
.init.lua
obsidian.init.lua
.obsidian.init.lua
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:
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:
-- lua/keymaps.lualocal M = {}function M.setup() vim.g.mapleader = " " vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save" })endreturn 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 = 8vim.opt.textobjects = truevim.opt.clipboard = "unnamedplus"-- Conditional config based on vaultif vim.vault_name() == "work" then vim.opt.clipboard = "unnamedplus"else vim.opt.clipboard = ""end-- Keymaps with string RHSvim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })vim.keymap.set("i", "jk", "<Esc>", { desc = "Exit insert mode" })-- Keymap with function callbackvim.keymap.set("n", "<leader>t", function() vim.cmd("obcommand daily-notes:open-today")end, { desc = "Open daily note" })-- Remove a mappingvim.keymap.del("n", "Q")-- Ex commandsvim.cmd("set nohlsearch")print("init.lua loaded for vault:", vim.vault_name())
Supported APIs
API
Description
Example
vim.opt.<name> = value
Set a plugin option (string options accept tables)
Set the leader key with vim.g.mapleader. Common choices:
vim.g.mapleader = " " -- Space (recommended, matches most Neovim configs)vim.g.mapleader = "," -- Commavim.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)
Variable
Type
Description
Default
vim.v.count
integer
Count given for the last Normal mode command. 0 when no count typed.
0
vim.v.count1
integer
Like count but defaults to 1 when no count given.
1
vim.v.register
string
Register in effect for the current command. '"' (unnamed) when none specified.
'"'
vim.v.operator
string
Pending operator (e.g., 'd', 'y', 'c'). Empty string when none.
These are only meaningful within specific evaluation contexts (fold expressions, statuscolumn, autocmds):
Variable
Type
R/W
Context
vim.v.foldstart
integer
R
Fold text evaluation
vim.v.foldend
integer
R
Fold text evaluation
vim.v.foldlevel
integer
R
Fold text evaluation
vim.v.folddashes
string
R
Fold text evaluation
vim.v.lnum
integer
R
statuscolumn evaluation
vim.v.relnum
integer
R
statuscolumn evaluation
vim.v.virtnum
integer
R
statuscolumn evaluation
vim.v.char
string
R/W
InsertCharPre autocmd
vim.v.event
table/nil
R
Autocmd event data
Example: expr mapping with count
-- Use gj/gk for screen-line movement, j/k for counted movementvim.keymap.set('n', 'j', function() if vim.v.count == 0 then return 'gj' else return vim.v.count1 .. 'j' endend, { expr = true, silent = true })vim.keymap.set('n', 'k', function() if vim.v.count == 0 then return 'gk' else return vim.v.count1 .. 'k' endend, { 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.
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.
Function
Returns
Example
vim.fn.has(feature)
1 or 0
if vim.fn.has("mac") == 1 then
vim.fn.expand("%")
Vault-relative file path
vim.fn.expand("%") → "folder/note.md"
vim.fn.expand("%:t")
Filename only
vim.fn.expand("%:t") → "note.md"
vim.fn.expand("%:e")
Extension only
vim.fn.expand("%:e") → "md"
vim.fn.expand("%:r")
Path without extension
vim.fn.expand("%:r") → "folder/note"
vim.fn.fnamemodify(path, mods)
Modified path
vim.fn.fnamemodify("a/b.md", ":t:r") → "b"
vim.fn.exists(expr)
1 if exists, 0 otherwise
vim.fn.exists("g:my_var")
vim.fn.localtime()
Unix timestamp (seconds)
vim.fn.localtime()
vim.fn.strftime(fmt)
Formatted date string
vim.fn.strftime("%Y-%m-%d")
vim.fn.filereadable(path)
1 if vault file exists
vim.fn.filereadable("config.md")
vim.fn.isdirectory(path)
1 if vault directory exists
vim.fn.isdirectory("templates")
vim.fn.glob(pattern)
Newline-separated file list
vim.fn.glob("*.md")
vim.fn.mode()
Current mode string
vim.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 string
vim.fn.getline(".") (callbacks only)
vim.fn.tolower(s)
Lowercase string
vim.fn.tolower("Hello") → "hello"
vim.fn.toupper(s)
Uppercase string
vim.fn.toupper("Hello") → "HELLO"
vim.fn.trim(s)
Trimmed string
vim.fn.trim(" hi ") → "hi"
vim.fn.strlen(s)
String length
vim.fn.strlen("hello") → 5
vim.fn.strwidth(s)
Display width
vim.fn.strwidth("hello") → 5
vim.fn.stridx(s, needle)
First index of needle
vim.fn.stridx("hello", "ll") → 2
vim.fn.strridx(s, needle)
Last index of needle
vim.fn.strridx("abab", "ab") → 2
vim.fn.strpart(s, start, len?)
Substring
vim.fn.strpart("hello", 1, 3) → "ell"
vim.fn.substitute(s, pat, sub, flags)
Regex replace
vim.fn.substitute("hi", "h", "H", "") → "Hi"
vim.fn.nr2char(n)
Character from code point
vim.fn.nr2char(65) → "A"
vim.fn.char2nr(c)
Code point from character
vim.fn.char2nr("A") → 65
vim.fn.split(s, sep?)
List (table) of parts
vim.fn.split("a,b", ",")
vim.fn.join(list, sep?)
Joined string
vim.fn.join({"a","b"}, "-") → "a-b"
vim.fn.has() features
Feature
Returns 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
Expression
Checks
"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
Modifier
Result
Example with "folder/note.md"
:t
Filename with extension
"note.md"
:r
Remove last extension
"folder/note"
:e
Extension only
"md"
:h
Directory part
"folder"
:t:r
Filename without extension
"note" (chained)
:p
Vault-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") endend, { desc = "Smart go-to-top" })
Conditional config examples
-- Per-platform settingsif vim.fn.has("mobile") == 1 then vim.opt.easymotion = false vim.opt.hintmode = falseend-- Check if a templates directory existsif vim.fn.isdirectory("templates") == 1 then vim.g.has_templates = trueend-- 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") endend, { desc = "Toggle preview" })-- User feedback via vim.notifyvim.keymap.set("n", "<leader>r", function() vim.cmd("obcommand app:reload") vim.notify("Reloaded!")end, { desc = "Reload" })-- Check if a config file existsif vim.fn.filereadable("vim-motions-config.md") == 1 then vim.opt.scrolloff = 10end
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.
Function
Description
Example
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 keys
vim.tbl_keys({a=1, b=2})
vim.tbl_values(t)
Returns list of all values
vim.tbl_values({a=1, b=2})
vim.tbl_map(fn, t)
Map function over table values
vim.tbl_map(function(v) return v*2 end, {1,2,3})
vim.tbl_filter(fn, t)
Filter table by predicate
vim.tbl_filter(function(v) return v > 1 end, {1,2,3})
vim.tbl_count(t)
Count entries in table
vim.tbl_count({a=1, b=2}) → 2
vim.tbl_isempty(t)
Check if table is empty
vim.tbl_isempty({}) → true
vim.tbl_get(t, ...)
Safe nested access
vim.tbl_get({a={b=42}}, "a", "b") → 42
vim.list_extend(dst, src)
Append elements from src to dst
vim.list_extend({1,2}, {3,4})
vim.deepcopy(t)
Deep copy a table
local 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 ends
vim.trim(" hi ") → "hi"
vim.startswith(s, prefix)
Check if string starts with prefix
vim.startswith("hello", "hel") → true
vim.endswith(s, suffix)
Check if string ends with suffix
vim.endswith("hello", "lo") → true
vim.pesc(s)
Escape Lua pattern special characters
vim.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
Function
Description
Example
vim.json.encode(value)
Encode Lua value to JSON string
vim.json.encode({a=1}) → '{"a":1}'
vim.json.decode(str)
Decode JSON string to Lua value
vim.json.decode('{"x":42}').x → 42
Regular expressions
vim.regex(pattern, flags?) creates a regex object wrapping JavaScript’s RegExp. This uses ECMAScript regex syntax, not Vim regex syntax.
Method
Description
Example
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 nil
Replace match(es). Use "g" flag for global replace
vim.regex("o", "g"):replace("foo", "0") → "f00"
re:test(str)
Returns true if pattern matches, false otherwise
vim.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
Function
Description
Example
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 message
vim.notify_once("Migration complete")
vim.log.levels
Level
Value
Behavior
vim.log.levels.TRACE
0
Console only
vim.log.levels.DEBUG
1
Console only
vim.log.levels.INFO
2
Obsidian Notice + console
vim.log.levels.WARN
3
Obsidian Notice + console.warn
vim.log.levels.ERROR
4
Obsidian Notice + console.error
vim.log.levels.OFF
5
No output
Snippets
Define snippets using a LuaSnip-inspired DSL. Static snippets compile to VS Code JSON at load time.
Function
Description
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
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:
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:
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 aliasvim.api.nvim_create_user_command("W", "w", {})vim.api.nvim_create_user_command("Q", "q", {})-- Command calling a Lua functionvim.api.nvim_create_user_command("Today", function() vim.cmd("obcommand daily-notes:open-today") vim.notify("Opened today's note")end, {})-- Command with argumentsvim.api.nvim_create_user_command("Open", function(opts) vim.cmd("obcommand switcher:open " .. opts.args)end, {})-- Toggle commandvim.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") endend, {})
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
Event
When it fires
Pattern support
InsertEnter
Entering insert or replace mode (per-view)
No
InsertLeave
Leaving insert or replace mode (per-view)
No
CursorMoved
After cursor moves in normal mode (per-view)
No
CursorHold
After cursor is idle for updatetime ms (per-view)
No
ModeChanged
Any mode transition (per-view)
"old:new" with * wildcard
BufEnter
A file becomes the active note
Vault-relative path globs ("*.md", "projects/**")
BufLeave
A file is deactivated (switching away)
Vault-relative path globs
BufWritePre
Before saving a file
Vault-relative path globs
BufWritePost
After saving a file
Vault-relative path globs
LeafEnter
A leaf (tab/pane) gains focus (debounced 50ms)
No
LeafLeave
A leaf (tab/pane) loses focus
No
FileType
After BufEnter when filetype is detected
No
FocusGained
Obsidian window gains focus
No
FocusLost
Obsidian window loses focus
No
TextYankPost
After yank, delete, or change operation (per-view)
No
OilEnter
An oil explorer buffer becomes active
No
OilLeave
Leaving an oil explorer buffer
No
CmdlineEnter
Opening :, /, or ? prompt (per-view, active only)
No (data.cmdtype = ":", "/", or "?")
CmdlineLeave
Closing 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. md → markdown, ts → typescript, py → python). 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 modevim.api.nvim_create_autocmd("InsertEnter", { group = g, callback = function() vim.notify("Insert mode") end,})-- Per-folder settingsvim.api.nvim_create_autocmd("BufEnter", { group = g, pattern = "projects/**", callback = function(ev) vim.opt.shiftwidth = 4 end,})-- React to mode changesvim.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 lostvim.api.nvim_create_autocmd("FocusLost", { group = g, callback = function() vim.cmd("w") end,})-- Track yank operationsvim.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
Option
Type
Default
Description
desc
string
(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.
noremap
boolean
true
Non-recursive mapping
remap
boolean
false
Recursive mapping (inverse of noremap)
silent
boolean
(none)
Accepted but no effect in Obsidian
nowait
boolean
(none)
Accepted but no effect in Obsidian
buffer
number/boolean
(none)
Buffer-local keymap (0 or true = current file). See Buffer-local keymaps above. Non-zero numbers error.
expr
boolean
false
If 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.
Function
Returns
Example
vim.obsidian.vault_name()
Vault name
vim.obsidian.vault_name()
vim.obsidian.app_version()
Obsidian version string
vim.obsidian.app_version()
vim.obsidian.plugin_version()
Plugin version string
vim.obsidian.plugin_version()
vim.obsidian.run_command(id)
Execute Obsidian command by ID
vim.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 file
vim.obsidian.open_file("notes/todo.md")
vim.obsidian.pick(source, opts?)
Open a picker source
vim.obsidian.pick("files")
vim.obsidian.current_file()
Table {path, name, extension, basename} or nil
vim.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 pickervim.obsidian.pick('files')-- Open grep with pre-filled queryvim.obsidian.pick('grep', { query = 'todo' })-- Resume last sessionvim.obsidian.pick('resume')
Workspace and leaf management
Function
Returns
Example
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 tables
for _, leaf in ipairs(vim.ob.list_leaves())
vim.ob.is_markdown_view()
Boolean
if vim.ob.is_markdown_view() then
vim.ob.get_leaf_for_file(path)
Leaf info table or nil
vim.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
Function
Description
Example
vim.ob.follow_link()
Follow link under cursor
vim.ob.follow_link()
vim.ob.backlinks()
Open backlinks for current note
vim.ob.backlinks()
vim.ob.daily()
Open today’s daily note
vim.ob.daily()
vim.ob.search()
Open global search
vim.ob.search()
vim.ob.tags()
Open tags view
vim.ob.tags()
vim.ob.new_note()
Create new note
vim.ob.new_note()
vim.ob.rename()
Rename current note
vim.ob.rename()
vim.ob.toggle_checkbox()
Toggle checkbox on current line
vim.ob.toggle_checkbox()
vim.ob.template()
Open template picker
vim.ob.template()
vim.ob.meta — Note metadata
Function
Returns
Description
vim.ob.meta.frontmatter(path?)
table or nil
YAML 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
Function
Description
Async
vim.ob.fs.read(path)
Read file content as string
Yes
vim.ob.fs.readlines(path)
Read file content as table of lines
Yes
vim.ob.fs.files(pattern?)
Markdown files matching optional glob
No
vim.ob.fs.all_files()
All files in vault
No
vim.ob.fs.folders()
All folders
No
vim.ob.fs.exists(path)
Check if file exists
No
vim.ob.fs.stat(path?)
File stats {ctime, mtime, size}
No
vim.ob.fs.create(path, content?)
Create new file
No
vim.ob.fs.write(content) or write(path, content)
Overwrite file content
No
vim.ob.fs.append(content) or append(path, content)
Append to file
No
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 path
No
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
Function
Description
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).
Function
Description
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:
Define key bindings for non-editor contexts (graph view, canvas, PDF viewer, file explorer, reading mode). These bindings work when no editor is focused.
Function
Description
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>:
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.
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.whichkeywk.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).
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.
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).
Function
Description
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.
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/Property
Returns
Description
vim.obsidian.im.get()
string|nil
Current 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.enabled
boolean
Read/write: master toggle for IM switching
vim.obsidian.im.auto
boolean
Read/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:
These map directly to plugin UI elements via CSS custom properties:
Group
Controls
EasyMotionTarget
EasyMotion jump labels
EasyMotionShade
EasyMotion dimmed text
HintTarget
Hint mode labels
StatusLineNormal
Normal mode status bar
StatusLineInsert
Insert mode status bar
StatusLineVisual
Visual mode status bar
StatusLineReplace
Replace mode status bar
StatusLineVLine
V-Line mode status bar
StatusLineVBlock
V-Block mode status bar
StatusLineCommand
Command mode status bar
StatusLineSearch
Search mode status bar
StatusLineSelect
Select mode status bar
StatusLineVReplace
V-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:
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:
Settings UI values (base)
Vimrc values override Settings UI
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:
Mode
Behavior
"collect"
Drains the __gc finalizer queue for unreachable userdata
"count"
Returns 0, 0 (no memory tracking)
"isrunning"
Returns false
Other modes
No-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 string
Context
Description
'n'
Normal
Normal mode mappings
'i'
Insert
Insert mode mappings
'v'
Visual
Visual mode (same as 'x')
'x'
Visual
Visual mode (alias for 'v')
's'
Select
Select mode only
'o'
Operator-pending
Maps 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:
Field
Type
Description
event
string
Event name (e.g., "BufEnter")
file
string
Vault-relative file path
match
string
Pattern match string
buf
number
Buffer number (always 0)
id
number
Autocmd ID
group
number or nil
Augroup ID (nil if no group)
data
table or nil
Event-specific data (see below)
Per-event data fields
Most events set data = nil. Only these events provide event-specific data:
TextYankPost:
Field
Type
Description
operator
string
Operator used ("y", "d", "c")
regcontents
table
Table of yanked lines
regtype
string
"V" (linewise), "v" (charwise)
regname
string
Register name (e.g., "a", "" for default)
visual
boolean
Whether the yank was from visual mode
ModeChanged:
Field
Type
Description
old_mode
string
Mode before transition
new_mode
string
Mode 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
Group
CSS variable
Controls
EasyMotionTarget
--vim-motions-em
EasyMotion jump labels
EasyMotionShade
--vim-motions-em-shade
EasyMotion dimmed text
HintTarget
--vim-motions-hint
Hint mode labels
StatusLineNormal
--vim-pl-normal
Normal mode status bar
StatusLineInsert
--vim-pl-insert
Insert mode status bar
StatusLineVisual
--vim-pl-visual
Visual mode status bar
StatusLineReplace
--vim-pl-replace
Replace mode status bar
StatusLineVLine
--vim-pl-v-line
V-Line mode status bar
StatusLineVBlock
--vim-pl-v-block
V-Block mode status bar
StatusLineCommand
--vim-pl-command
Command mode status bar
StatusLineSearch
--vim-pl-search
Search mode status bar
StatusLineSelect
--vim-pl-select
Select mode status bar
StatusLineVReplace
--vim-pl-vreplace
V-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:
Math functions (floor, ceil, abs, max, min, random, sqrt, sin, cos, pi, huge, etc.)
coroutine
Coroutine support (create, resume, yield, wrap, status)
utf8
UTF-8 support (char, codepoint, codes, len, offset, charpattern)
Not available
Library/function
Reason
io
Stripped from fork (file system access)
os
Not loaded by plugin (security)
debug
Not loaded by plugin (security)
package (native)
Stripped from fork; plugin provides package.loaded/package.path and a custom require()
dofile, loadfile
Disabled (no direct file loading)
rawget, rawset, rawequal
Disabled (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.