Added
vim.ob.*API expansion (47 new functions across 4 sub-namespaces) — thevim.obsidian/vim.obLua namespace grows from 21 to 68 functions- Leaf introspection (Tier 1):
vim.ob.get_leaf_type()returns the active view type string,vim.ob.get_active_leaf()returns{id, type, pinned, file_path}table,vim.ob.list_leaves()returns all open tabs,vim.ob.is_markdown_view()returns boolean - Command wrappers (Tier 2):
vim.ob.follow_link(),vim.ob.backlinks(),vim.ob.daily(),vim.ob.search(),vim.ob.tags(),vim.ob.new_note(),vim.ob.rename(),vim.ob.toggle_checkbox(),vim.ob.template()— thin wrappers around Obsidian commands, silent no-op if required core plugin is disabled - Leaf management (Tier 3):
vim.ob.focus(direction)navigates panes ("left","right","top","bottom"),vim.ob.close_leaf()closes active tab,vim.ob.split(direction)splits vertically/horizontally,vim.ob.get_leaf_for_file(path)finds which leaf has a file open vim.ob.meta.*sub-namespace (9 metadata query functions) — read-only access to note metadata via Obsidian'sMetadataCachevim.ob.meta.frontmatter(path?)— returns YAML frontmatter as a Lua table, or nilvim.ob.meta.tags(path?)— returns combined body + frontmatter tags asstring[]vim.ob.meta.links(path?)— returns outgoing links as{link, display, original}[]vim.ob.meta.backlinks(path?)— returns source file paths linking to this file asstring[]vim.ob.meta.headings(path?)— returns headings as{heading, level}[]vim.ob.meta.embeds(path?)— returns embedded content as{link, display}[]vim.ob.meta.aliases(path?)— returns YAML aliases asstring[]vim.ob.meta.tasks(path?)— returns checklist items as{text, status, line}[]vim.ob.meta.lists(path?)— returns all list items as{text, line, indent}[]- All functions default to the current file when
pathis omitted - Plugin:
src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts
vim.ob.fs.*sub-namespace (11 vault filesystem functions) — read and write vault files with config-dir guards- Read:
vim.ob.fs.files(pattern?),vim.ob.fs.all_files(),vim.ob.fs.folders(),vim.ob.fs.exists(path),vim.ob.fs.stat(path?) - Write:
vim.ob.fs.create(path, content?),vim.ob.fs.write(content)orvim.ob.fs.write(path, content),vim.ob.fs.append(content)orvim.ob.fs.append(path, content) - Management:
vim.ob.fs.rename(new_path)orvim.ob.fs.rename(path, new_path),vim.ob.fs.move(dest)orvim.ob.fs.move(path, dest)(detects folder dest and appends filename),vim.ob.fs.trash(path?) - Write/rename/move/trash operations silently reject paths inside the vault config directory (
app.vault.configDir) renameusesfileManager.renameFile()which updates backlinks;trashusesfileManager.trashFile()which respects the user's trash preference- Write operations are fire-and-forget (async internally, Lua returns immediately)
- All write operations default to the current file when path is omitted
- Plugin:
src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts
- Read:
vim.ob.ui.*sub-namespace (4 UI control functions) — control Obsidian UI from Luavim.ob.ui.sidebar(side, state?)— toggle/open/close sidebar ("left"/"right", optional"open"/"close"/"toggle")vim.ob.ui.command_palette()— open command palettevim.ob.ui.quickswitch()— open quick switchervim.ob.ui.notice(msg)— alias forvim.notify(convenience for staying invim.obnamespace)- Plugin:
src/lua/obsidian-api.ts
vim.obeditor state and convenience functions — cursor, selection, mode, and notification accessvim.ob.get_cursor()— returns{line, col}(1-indexed, Lua/Neovim convention)vim.ob.set_cursor(line, col)— sets cursor position (1-indexed)vim.ob.get_selection()— returns visual selection text or nilvim.ob.mode()— alias forvim.fn.mode()(convenience)vim.ob.notice(msg)— alias forvim.notify(convenience)- Plugin:
src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts
- Leaf introspection (Tier 1):
- 3 new autocmd events —
LeafEnter,LeafLeave,FileType(total: 15 events)LeafEnter— fires when a new leaf gains focus (debounced 50ms), event data includes{type, leaf_id}inev.dataLeafLeave— fires when a leaf loses focus (immediate, beforeLeafEnter)FileType— fires afterBufEnterwithev.matchset to detected filetype from file extension (.md→"markdown",.ts→"typescript", etc.)- Enables Neovim-style per-filetype keymaps:
vim.api.nvim_create_autocmd("FileType", { pattern = "markdown", callback = function() ... end }) - Plugin:
src/lua/autocmd.ts(fireFileType,onActiveLeafChangeextension),src/main.ts(leaf info passthrough)
workspaceNavViewTypessetting — comma-separated list of view types where scroll and count keys are intercepted. Defaults tomarkdown,graph,pdf,canvas,empty,image. Plugin views not in this list receive their own keystrokes. Configurable via Settings → Vim Motions → Workspace navigation view types, vimrc (set workspacenavviewtypes=...), or Lua (vim.opt.workspacenavviewtypes = "..."orvim.opt.workspacenavviewtypes = {"markdown", "graph", "pdf"})- Plugin:
src/settings.ts,src/vim/options.ts,src/vimrc/loader.ts
- Plugin:
vim.opttable (array) support for string options — string-type options can now be set using Lua tables:vim.opt.workspacenavviewtypes = {"markdown", "graph", "pdf"}is equivalent tovim.opt.workspacenavviewtypes = "markdown,graph,pdf". Elements are joined with commas. Applies to all string-type options.- Plugin:
src/lua/api.ts(vim.opt.__newindextable handling)
- Plugin:
Fixed
- Surround opening bracket semantics (
ds(/ds[/ds{/cs({) —findSurroundingBracketsreceived swapped parameters when the target was an opening bracket ((,[,{,<), causing the backward search to look for the wrong bracket character.ds(on( hello world )was a no-op because the search looked for)going backward. Fixed by detecting opening bracket targets and swapping parameters so the closing bracket is always passed as the forward-search character. Also fixescs({,ds(on nested/multiline content, andds<. (#41)- Fork:
src/vim.js—findSurroundingPairbracket parameter ordering
- Fork:
- Surround cursor position after
ys/yss/visualS—addSurroundToRangeplaced the cursor atfrom.ch + pair.open.length(after the opening delimiter). nvim-surround places it atfrom.ch(on the opening delimiter). Fixed by removing+ pair.open.length. The_surroundSelOffset.chDeltaused for dot-repeat now addspair.open.lengthat recording time to compensate, preserving correct visual surround replay ranges. (#41)- Fork:
src/vim.js—addSurroundToRangecursor,surroundVisualoffset recording
- Fork:
- Visual-block cursor displaced rightward at end-of-line — in visual-block mode (
<C-v>), selecting to the end of a line (via$orlto EoL) caused the block cursor to render one position past the last visible character. ThemeasureCursor()function in the fork'sblock-cursor.tshad a guard (!vim.visualBlock) that prevented the EOL step-back for visual-block mode. This guard was originally correct whenmakeCmSelectionproducedtoCh + 1without clamping, but after the per-line clamping fix (issue #38), block selection heads legitimately land on newline positions and need the step-back. Fixed by removing the!vim.visualBlockexclusion. (#41)- Fork:
src/block-cursor.ts—measureCursor()EOL adjustment guard
- Fork:
- Visual-block
Askips short lines instead of padding — in visual-block mode,A(append) on a block spanning lines shorter than the block column skipped those lines entirely. Neovim pads short lines with spaces to reach the block's right edge before appending. Fixed by adding apadShortLinesparameter toselectForInsertin the fork —Apads,Istill skips (matching Neovim). (#41)- Fork:
src/vim.js—selectForInsert()padding,enterInsertModepasses flag forendOfSelectedArea
- Fork:
- Visual charwise
rreplaces one fewer character across line boundary — thereplaceaction in the fork usedcurEnd = selEnd(the inclusive head position) for charwise visual mode, butcm.getRange()treats the end as exclusive. This causedr <Space>across a line boundary to replace one fewer character than the visual selection covered. Fixed by usingselEnd.ch + 1for the exclusive end. (#41)- Fork:
src/vim.js—actions.replacecharwise visual branch
- Fork:
set insertmodeescape=jkleavesjin buffer after escaping insert mode — theInsertEscapeHandlersentvim.handleKey(adapter, '<BS>')to delete typed characters before sending<Esc>, but codemirror-vim does not handle<BS>in insert mode (returns false, expecting the browser default action). SincehandleKeyis called programmatically with no DOM event, the backspace had no effect and the first character(s) of the escape sequence remained in the buffer. Fixed by replacing thehandleKey('<BS>')loop with a directadapter.replaceRange()call that deletes exactlyescapeSeq.length - 1characters before the cursor (the last key in the sequence is already intercepted bypreventDefaultand never enters the document). The nativeimap jk <Esc>mapping (via vimrc orvim.map()) was unaffected — codemirror-vim'schangeQueuecleanup handles that path correctly.- Plugin:
src/vim/insert-escape.ts(onKeyDownmethod)
- Plugin:
scrolloffvalues above ~30 pin view at bottom of document — highscrolloffvalues (e.g.,set scrolloff=999to center the cursor) caused the viewport to pin at the top or bottom instead of centering. The scroll margin was passed to CodeMirror'sEditorView.scrollMarginsunclamped, producing a target rect taller than the viewport. CM6'sscrollRectIntoViewresolved the conflicting top/bottom constraints by favoring one side based on cursor direction. Fixed by clamping the margin to half the viewport height, mirroring Vim's silent cap ofscrolloffto(window_height - 1) / 2. (#48)- Plugin:
src/vim/scrolloff.ts—createScrolloffExtension()viewport-relative clamp
- Plugin:
- Workspace navigation intercepting keystrokes in plugin leaves — when workspace navigation was enabled, the global key handler consumed keystrokes (
1,2,3,0,j,k, etc.) in non-editor plugin views (Spaced Repetition, Excalidraw, etc.) before the plugin could process them. Fixed with a three-gate interception system: structural keys (<C-w>*,gt/gT,<C-o>/<C-i>,:) always work in non-editor views, content keys (scroll, digits, tab shortcuts) only intercept in whitelisted view types, and plugin views receive their own keystrokes. (#47)- Plugin:
src/workspace/global-mapping-registry.ts(GlobalMapGate→'standard' | 'hint' | 'structural'),src/workspace/global-defaults.ts(gate assignments),src/workspace/global-key-handler.ts(three-gateonKeydownrewrite,GLOBAL_NAV_VIEW_TYPESwhitelist,shouldInterceptContent/shouldInterceptStructuralmethods)
- Plugin:
Changed
minAppVersionbumped from 1.4.10 to 1.6.6 — required forVault.getAllFolders()used byvim.ob.fs.folders()vim.obsidian.*namespace extracted to dedicated module — the Obsidian-specific Lua API is now insrc/lua/obsidian-api.ts(extracted fromapi.ts), following the pattern offn.ts,stdlib.ts,timers.ts,highlight.ts. No behavioral change.api.tsshrinks by ~504 lines.
Documentation
docs/configuration/lua-config.md: addedvim.ob.meta.*(9 functions),vim.ob.fs.*(11 functions),vim.ob.ui.*(4 functions), editor state functions (5 functions) with API tables and examplesKNOWN_LIMITATIONS.md: added "Workspace navigation in plugin views" section documenting three-gate interception and thegg-in-plugin-leaf trade-off; marked #47 as fixed; added "Neovim golden test coverage gaps" section documenting non-verifiable areas (scroll/viewport, fold, jumplist, cursor rendering); updated visual mode EOL cursor section with visual-blockApadding and visualroff-by-one fixes- Neovim golden test coverage expansion — 106 new golden comparison test cases across 6 suites, recorded against Neovim 0.12.2:
surround(74 cases): comprehensive nvim-surround parity —ds/cs/ys/yss/visualSwith all delimiter types, count-prefixed operations (2dsb,2csbB), dot-repeat (ysiwb..,dsb..,csba..), tag surround (dst,cst), function surround (dsf),ysa(around surround), empty content, whitespace cascade (ds{strips /ds}preserves), motion-based (ys$,ysjb), newline variants (ySS,VSB), angle brackets, arbitrary delimiters (|,^), multiline, nesting, and cursor positioning. Ground truth shifted from tpope/vim-surround to nvim-surround — better maintained, comprehensive test suite, Lua-native, superset of tpope behavior. 54 pass, 20 tracked deviations.dot-repeat(17 cases):.after2dw,dd,3i,3o,cw,R,2dl,d2w,g~2w,V>,3J,3I, visual block~,oselect-mode-extended(6 cases):gh/gHenter select, type replaces,<BS>deletes,<Esc>exits,<C-g>toggles visual↔selectex-sort(6 cases)::sort,:sort!,:sort i,:sort u,:sort n,:2,3sortex-global(3 cases)::g/pattern/d,:v/pattern/d,:g/a/s/a/x/upstream-gaps(7 cases):dipparagraph, backward blockA, blockAshort-line padding, visualrcross-line, block↔char/line mode switch, macro replay- Test infrastructure:
SuiteDefinition.nvimSetupfield for per-suite Neovim commands (loads nvim-surround for surround suite),NeovimClient.executeCommand()method - Total golden test coverage: 276 → 382 cases across 28 suites
docs/configuration/settings.md: addedWorkspace navigation view typesto Vim features tabledocs/configuration/vimrc.md: addedworkspacenavviewtypes(wnvtalias) to string options tabledocs/configuration/lua-config.md: added 17 newvim.ob.*functions, 3 new autocmd events (LeafEnter,LeafLeave,FileType),workspacenavviewtypesoptiondocs/features/workspace-navigation.md: added "Plugin view compatibility" section with key passthrough table and whitelist customizationdocs/guides/ecosystem-compatibility.md: added "Plugin leaf key passthrough" sectiondocs/configuration/lua-config.md: added table (array) syntax tip for string options with example
Full Changelog: 0.38.0...0.39.0