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.lua and 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

APIDescriptionExample
vim.opt.<name> = valueSet a plugin optionvim.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.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")
print(...)Print to developer consoleprint("loaded")

Supported vim.opt options

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

OptionTypeDefaultValid range / valuesExample
textobjectsbooleantruevim.opt.textobjects = true
navigationbooleantruevim.opt.navigation = true
hardwrapbooleantruevim.opt.hardwrap = true
listcontinuationbooleantruevim.opt.listcontinuation = true
tablenavbooleantruevim.opt.tablenav = true
workspacenavbooleantruevim.opt.workspacenav = 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
scrolloffnumber50–9999vim.opt.scrolloff = 8
scanlimitnumber205–200vim.opt.scanlimit = 20
labelfontsizenumber1410–20vim.opt.labelfontsize = 14
tabstopnumber4vim.opt.tabstop = 4
shiftwidthnumber4vim.opt.shiftwidth = 4
textwidthnumber80vim.opt.textwidth = 80
insertmodeescapetimeoutnumber1000100–5000 msvim.opt.insertmodeescapetimeout = 1000
updatetimenumber4000ms (CursorHold delay)vim.opt.updatetime = 4000
clipboardstring"""", "unnamed", "unnamedplus"vim.opt.clipboard = "unnamedplus"
insertmodeescapestring""vim.opt.insertmodeescape = "jk"
easymotionlabelsstring"asdghklqwertyuiopzxcvbnmfj"vim.opt.easymotionlabels = "asdf"
hintlabelsstring"asdfghjkl"vim.opt.hintlabels = "asdf"
tablewidgetstring"cursor""off", "cursor", "always"vim.opt.tablewidget = "cursor"
formattingmarkmodestring"cursor""off", "cursor"vim.opt.formattingmarkmode = "cursor"
whichkeystring"off""off", "leader", "all"vim.opt.whichkey = "leader"
whichkeygroupingstring"grouped""flat", "grouped"vim.opt.whichkeygrouping = "grouped"
whichkeydelaynumber5000–2000 msvim.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.

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

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

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")

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.

Supported events

EventWhen it firesPattern support
InsertEnterEntering insert or replace modeNo
InsertLeaveLeaving insert or replace modeNo
CursorMovedAfter cursor moves in normal modeNo
CursorHoldAfter cursor is idle for updatetime ms (default 4000)No
ModeChangedAny mode transition"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
FocusGainedObsidian window gains focusNo
FocusLostObsidian window loses focusNo
TextYankPostAfter yank, delete, or change operationNo

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

OptionTypeDefaultDescription
descstring(none)Description shown in which-key popup
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.
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.

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.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()

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.

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: require(), 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. Global functions load, dofile, loadfile, require, rawget, rawset, and rawequal are disabled.

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 / require()Stripped from fork (no module system)
load, dofile, loadfileDisabled (no code 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

  • 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.