Vim Motions supports .obsidian.init.lua 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
By default, place an .obsidian.init.lua file in your vault root. For Obsidian Sync users (which skips dotfiles), configure a custom path in Settings → Vim Motions → Vimrc & key bindings → Custom Lua config path, for example, init.lua or config/my.lua.
Obsidian Sync
Dotfiles are not synced by Obsidian Sync. Use a non-dotfile path like
init.luaand configure it in settings to ensure your Lua config syncs across devices.
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
| API | Description | Example |
|---|---|---|
vim.opt.<name> = value | Set a plugin option | vim.opt.scrolloff = 8 |
vim.o.<name> = value | Alias for vim.opt | vim.o.scrolloff = 8 |
vim.g.mapleader | Set the leader key | vim.g.mapleader = " " |
vim.g.<name> = value | Set a user variable | vim.g.my_var = true |
vim.cmd(string) | Execute an ex command | vim.cmd("set nohlsearch") |
vim.vault_name() | Returns the current vault name | if vim.vault_name() == "work" then |
vim.fn.has(feature) | Platform/feature detection | vim.fn.has("mac") |
vim.fn.expand(expr) | Active file path (vault-relative) | vim.fn.expand("%:t") |
vim.fn.fnamemodify(path, mods) | Path manipulation | vim.fn.fnamemodify(path, ":t:r") |
vim.fn.exists(expr) | Check variable/option existence | vim.fn.exists("g:my_var") |
vim.fn.localtime() | Unix timestamp | vim.fn.localtime() |
vim.fn.strftime(fmt) | Format date/time | vim.fn.strftime("%Y-%m-%d") |
vim.fn.filereadable(path) | Check vault file exists | vim.fn.filereadable("config.md") |
vim.fn.isdirectory(path) | Check vault directory exists | vim.fn.isdirectory("templates") |
vim.fn.glob(pattern) | Find matching vault files | vim.fn.glob("*.md") |
vim.fn.mode() | Current vim mode | vim.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 notification | vim.notify("Saved!") |
vim.api.nvim_create_user_command(name, cmd, opts) | Define custom ex command | see below |
vim.api.nvim_create_autocmd(event, opts) | Register autocommand | see Autocommands section |
vim.api.nvim_create_augroup(name, opts) | Create/get autocommand group | see Autocommands section |
vim.keymap.set(mode, lhs, rhs, opts?) | Create a key mapping | see example above |
vim.keymap.del(mode, lhs) | Remove a key mapping | vim.keymap.del("n", "Q") |
print(...) | Print to developer console | print("loaded") |
Supported vim.opt options
All plugin options are available via vim.opt. vim.o is an alias.
| Option | Type | Default | Valid range / values | Example |
|---|---|---|---|---|
textobjects | boolean | true | vim.opt.textobjects = true | |
navigation | boolean | true | vim.opt.navigation = true | |
hardwrap | boolean | true | vim.opt.hardwrap = true | |
listcontinuation | boolean | true | vim.opt.listcontinuation = true | |
tablenav | boolean | true | vim.opt.tablenav = true | |
workspacenav | boolean | true | vim.opt.workspacenav = true | |
easymotion | boolean | true | vim.opt.easymotion = true | |
easymotiondimming | boolean | true | vim.opt.easymotiondimming = true | |
hintmode | boolean | true | vim.opt.hintmode = true | |
statusbar | boolean | true | vim.opt.statusbar = true | |
chorddisplay | boolean | true | vim.opt.chorddisplay = true | |
powerline | boolean | false | vim.opt.powerline = true | |
expandtab | boolean | true | vim.opt.expandtab = true | |
scrolloff | number | 5 | 0–9999 | vim.opt.scrolloff = 8 |
scanlimit | number | 20 | 5–200 | vim.opt.scanlimit = 20 |
labelfontsize | number | 14 | 10–20 | vim.opt.labelfontsize = 14 |
tabstop | number | 4 | vim.opt.tabstop = 4 | |
shiftwidth | number | 4 | vim.opt.shiftwidth = 4 | |
textwidth | number | 80 | vim.opt.textwidth = 80 | |
insertmodeescapetimeout | number | 1000 | 100–5000 ms | vim.opt.insertmodeescapetimeout = 1000 |
updatetime | number | 4000 | ms (CursorHold delay) | vim.opt.updatetime = 4000 |
clipboard | string | "" | "", "unnamed", "unnamedplus" | vim.opt.clipboard = "unnamedplus" |
insertmodeescape | string | "" | vim.opt.insertmodeescape = "jk" | |
easymotionlabels | string | "asdghklqwertyuiopzxcvbnmfj" | vim.opt.easymotionlabels = "asdf" | |
hintlabels | string | "asdfghjkl" | vim.opt.hintlabels = "asdf" | |
tablewidget | string | "cursor" | "off", "cursor", "always" | vim.opt.tablewidget = "cursor" |
formattingmarkmode | string | "cursor" | "off", "cursor" | vim.opt.formattingmarkmode = "cursor" |
whichkey | string | "off" | "off", "leader", "all" | vim.opt.whichkey = "leader" |
whichkeygrouping | string | "grouped" | "flat", "grouped" | vim.opt.whichkeygrouping = "grouped" |
whichkeydelay | number | 500 | 0–2000 ms | vim.opt.whichkeydelay = 300 |
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")
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
endFile paths are vault-relative
vim.fn.expand("%"),vim.fn.filereadable(),vim.fn.isdirectory(), andvim.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 |
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 |
Async and timers
| Function | Description | Example |
|---|---|---|
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.
| Function | Description |
|---|---|
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")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
BufEnterautocmd, always usenvim_create_augroupwith{ 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:
| Function | Description | Example |
|---|---|---|
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 path | vim.api.nvim_buf_get_name(0) |
vim.api.nvim_buf_line_count(0) | Total line count | vim.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.
Supported events
| Event | When it fires | Pattern support |
|---|---|---|
InsertEnter | Entering insert or replace mode | No |
InsertLeave | Leaving insert or replace mode | No |
CursorMoved | After cursor moves in normal mode | No |
CursorHold | After cursor is idle for updatetime ms (default 4000) | No |
ModeChanged | Any mode transition | "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 |
FocusGained | Obsidian window gains focus | No |
FocusLost | Obsidian window loses focus | No |
TextYankPost | After yank, delete, or change operation | No |
CursorHold timing
Configure the idle timeout with
vim.opt.updatetime = 1000(milliseconds). Default is 4000ms, matching Neovim.
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)
}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 |
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 | (none) | (none) | Not supported (throws error) |
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.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() |
Environment variables
vim.env provides a sandboxed environment variable proxy:
| Key | Value |
|---|---|
vim.env.HOME | Vault absolute path (desktop) |
vim.env.VIMRUNTIME | "obsidian" |
vim.env.VIM | "motions" |
vim.env.TERM | "obsidian" |
vim.env.OBSIDIAN_VERSION | Obsidian version string |
vim.env.MYVIMRC | "init.lua" |
Custom variables can be set: vim.env.MY_VAR = "value". Unknown keys return nil.
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:
| 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, noteasymotiontarget). 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
| Attribute | Type | CSS mapping |
|---|---|---|
fg / foreground | string | color |
bg / background | string | background-color |
sp / special | string | text-decoration-color |
bold | boolean | font-weight: bold |
italic | boolean | font-style: italic |
underline | boolean | text-decoration-line: underline |
undercurl | boolean | text-decoration: underline wavy |
underdouble | boolean | text-decoration: underline double |
underdotted | boolean | text-decoration: underline dotted |
underdashed | boolean | text-decoration: underline dashed |
strikethrough | boolean | text-decoration-line: line-through |
reverse | boolean | Swaps fg/bg |
blend | number (0-100) | opacity |
link | string | Inherit from another group |
default | boolean | Only apply if group not already defined |
update | boolean | Merge with existing (don’t replace) |
Namespace
Only
ns_id = 0(global namespace) is supported.vim.api.nvim_create_namespace()always returns0.
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.luaor.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:
require(),vim.lsp,vim.treesitter,vim.ui,vim.diagnostic. Attempting to use them produces a clear error message.vim.apiis 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, andnvim_buf_del_keymapwork, other functions error with a helpful message).vim.fnis partially supported (see above). The Lua runtime is sandboxed: only 6 standard libraries are loaded (_G,string,table,math,coroutine,utf8). Theio,os,debug, andpackagelibraries are not available. Global functionsload,dofile,loadfile,require,rawget,rawset, andrawequalare disabled.
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:
vim.api.nvim_set_hl(0, "MyHighlight", { fg = "#00ff00", bold = true })
-- Generates: .vim-hl-MyHighlight { color: #00ff00; font-weight: bold }Attribute → CSS property mapping
| Attribute | CSS property |
|---|---|
fg | color |
bg | background-color |
sp | text-decoration-color |
bold | font-weight: bold |
italic | font-style: italic |
underline | text-decoration-line: underline |
undercurl | text-decoration: underline wavy |
underdouble | text-decoration: underline double |
underdotted | text-decoration: underline dotted |
underdashed | text-decoration: underline dashed |
strikethrough | text-decoration-line: line-through |
reverse | Swaps fg/bg values |
blend | opacity (0–100 → 0.0–1.0) |
link | Inherit from another group |
default | Only apply if group not defined |
update | Merge 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:
| Library | Description |
|---|---|
_G (base) | Core functions (type, tostring, tonumber, pcall, xpcall, error, select, pairs, ipairs, next, unpack, assert) |
string | String manipulation (format, find, gsub, sub, rep, byte, char, len, lower, upper, match, gmatch, reverse) |
table | Table manipulation (insert, remove, sort, concat, move, pack, unpack) |
math | 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 / require() | Stripped from fork (no module system) |
load, dofile, loadfile | Disabled (no code loading) |
rawget, rawset, rawequal | Disabled (sandbox integrity) |
Fork vs plugin
The fengari fork retains browser-safe
osfunctions (os.date,os.time, etc.) and thedebuglibrary 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
- Instruction limit: 1,000,000 Lua VM instructions per execution. Scripts exceeding this limit are terminated with a timeout error.
- Error handling: Syntax errors and runtime errors 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.