github saberzero1/motions 1.7.0

one day ago

Added

  • set markdownindent: = re-indents by Markdown's rules. Off by default. On, a list keeps its nesting, normalized to one shiftwidth a level, a line continuing a list item goes to the item's text, other lines get no indent, and fenced code blocks and frontmatter keep theirs. Off, = is Neovim's, which flattens a nested list.
    • Plugin: src/vim/markdown-indent.ts, src/vim/registration.ts (defineOperatorOverride), src/settings.ts, src/vim/options.ts, src/vimrc/loader.ts
  • set hoverfocus: K moves into the preview it opens (#234). Off by default, as Neovim's hover window does not take the cursor. On, K on a link or footnote waits for Obsidian to show the preview, about 300 ms after the key, and moves into it as a second K does; it gives up if the cursor moves first or another K replaces the preview. K on plain text or an external link is unchanged, so character info shows once and a URL opens once, which a mapping pressing K twice could not manage. A quickly typed KK could not focus the preview either: measured on 1.13.7, the second K arrived before the preview existed. Both settings implementations, set hoverfocus and vim.opt.hoverfocus.
  • set tabindent: normal-mode <Tab> indents the line and <S-Tab> outdents it (#237). Off by default: <Tab> is <C-i>, as in Vim and Neovim, and has walked forward through the jump list since 1.5.0. On, <Tab> is >> and <S-Tab> is <<, so a count indents that many lines and . repeats it. <C-i> keeps walking the jump list. In Obsidian's native table cell editor both keys still move between cells, which a <Tab> mapping in a config cannot keep: measured, vim.keymap.set("n", "<Tab>", ">>") and the same mapping to editor:indent-list both left the cursor in its cell and wrote a tab into the cell text. With the option off, <S-Tab> is still Obsidian's outdent, now handed to the editor's keymap rather than falling through. >> was chosen over Obsidian's editor:indent-list after measuring both, with insert-mode Tab, on list items, a parent with a child, ordered and task items, a paragraph and a table row: all three gave the same text in every case, and only >> takes a count and repeats with .. Also Settings → Vim Motions → General → Tab indents in normal mode, in both settings implementations, and vim.opt.tabindent. Bundled engine only; with the Neovim backend, <Tab> is Neovim's.
    • Plugin: src/vim/normal-tab.ts, src/main.ts, src/settings.ts, src/vim/options.ts, src/vimrc/loader.ts
  • {count}% goes to that percentage of the file, as in Vim. 50% goes to the line halfway down, (count × lines + 99) / 100, at the startofline column. As an operator target it is charwise exclusive, which is what Neovim 0.12.6 does, although its help says linewise. A count over 100 fails. % with a count used to match brackets.
    • Fork: codemirror-vim 6.4.17
  • :> and :< shift lines, as Vim's do. :[range]> shifts the range one shiftwidth, each further > once more (:>>>), and a count shifts that many lines from the range's last line. The cursor ends on the last line's first non-blank.
    • Fork: codemirror-vim 6.4.18

Changed

  • An expr mapping's returned keys now get the count and register you typed, as in Neovim. Before, the plugin used them up before calling the expression, so 3K with K returning 'j' moved one line, and the docs told you to add the count yourself with vim.v.count1 .. 'j'. Now 3K moves three lines, and "a2K with K returning 'yy' yanks two lines into a. If the expression returns nothing, the count waits for the next key. vim.v.count and vim.v.register still read what you typed. Each case was measured against Neovim 0.12.6.
    • If your config adds the count itself, it now gets it twice. That includes the display-line navigation recipe from the docs: return vim.v.count1 .. "j" turns 5j into 55j, as the same line does in Neovim. Return the key alone instead, return vim.v.count == 0 and "gj" or "j", which also works in Neovim. The recipe and the Lua configuration page are updated.
    • Plugin: src/lua/expr-prefix.ts (pendingPrefix), src/lua/api.ts
  • startofline is now off by default, as in Neovim. It was on, Vim's default. With it off, G, gg, H, M and L keep the cursor's screen column instead of going to the first non-blank, and land on the last character after $. They also read the screen column now, not the character index, so on lines indented with different tabs they land under the cursor rather than on another character: with tabstop=4, gg from the d of \t\tend line now lands on the r of \tfoo bar baz, as Neovim does, where it landed on the space before bar. set sol or vim.opt.startofline = true brings back the old behaviour.
    • Fork: codemirror-vim 6.4.15 (startOfLineColumn, curswant)

Fixed

  • With the Neovim backend, a <C-o> that went nowhere could undo the keys typed after it. A bridged <C-o> or Harpoon jump waits up to 1.5 s for its jump to land, and stops holding the cursor after 300 ms if nothing has moved. A key typed after that, still within the wait, moved the cursor, which the wait took for the jump landing: it put the cursor back on the jump list's entry, in the note and in Neovim, undoing the key. A move within the note no longer counts once the hold is released; only another note does. A wait still running when Neovim disconnected likewise wrote into the next connection's editor and then showed "Obsidian action failed: RPC client closed"; an action now stops at the next step once its connection has gone.
    • Plugin: src/rpc/obsidian-feature-bridge.ts
  • = changed nothing in a note. The bundled engine's = takes the indent CodeMirror's language gives a line, and Obsidian's Markdown gives each line the indent it already has. = now does what Neovim's does in a Markdown file, which has no indent rules: each line takes the indent of the nearest non-blank line above it, as re-indented, and a blank line is emptied. With tabstop=4, == on bar below \tfoo gives \tbar, as in Neovim 0.12.6.
    • Plugin: src/vim/markdown-indent.ts (autoindentIndents)
  • <C-^> opened the alternate note at its first line. It now returns to the cursor the note was left with, as Neovim's returns to a buffer's " mark: the same column without startofline, the line's first non-blank with it. <C-w><C-^> does the same in its split, and now opens the alternate note there at all: opened before the new pane had finished loading, it was replaced by the note the pane was split with.
    • Plugin: src/workspace/navigation.ts (openAlternateFile), src/workspace/note-switch-jumps.ts (getLastNoteCursor)
  • vim.fn.strdisplaywidth counted an emoji as one cell and a combining mark as one of its own. Neovim counts most emoji as two cells, and a combining mark, a variation selector, a zero-width joiner or a skin-tone modifier as none, since each is drawn with the character before. The Lua coordinate functions that count display cells share the same table.
    • Plugin: src/lua/coordinates.ts (isWideCodePoint, isZeroWidthCodePoint), src/lua/fn.ts
  • A noremap rhs reaches the plugin's own keys (#234). vim.keymap.set("n", "X", "K"), nnoremap X ]h, nvim_feedkeys("]h", "n", false) and :normal! ]h did nothing, because a noremap expansion searched only the engine's defaults and the plugin registers its keys as mappings. The plugin now registers them as built-ins (codemirror-vim fork 6.4.14, mapCommand(..., { builtin: true })), which a noremap rhs reaches as it reaches j; your own mappings, Lua function mappings and Lua text objects included, are still skipped, as in Vim. They are not defaults in any other sense: :mapclear, :unmap and reloads treat them as before.
  • With a space leader, a counted j through an expr mapping on j typed j into the note (#236). This hit the docs recipe for display-line navigation with counted jumps, which maps j to an expression that returns 3j for 3j. Making Space the leader removes the default <Space> motion. The fork found the defaults by counting from the end of its keymap, so with one default fewer, the user's own j mapping fell inside the range a noremap rhs searches. 3j then re-entered the expression until the stack overflowed, about 700 errors, and every j and k after it was typed into the note. A noremap rhs now skips user mappings by their flag rather than their position, so removing a default no longer matters. A space leader set in vimrc did the same.
    • Fork: codemirror-vim 6.4.12 (commandMatches)
  • A Lua mapping on a key the plugin also maps, such as Q, stacked another copy of the plugin's mapping on every settings reload. On a reload the plugin removes its own key mappings and adds them again, and it removed them by key alone. That takes the newest mapping on the key, which was the user's: with a Lua function on Q, each reload removed the Lua mapping instead of the plugin's Q → @@. The Lua mapping was put back, and another @@ was stacked under it, four after three reloads. The plugin now removes exactly its own mapping, by keys, result and noremap.
    • Fork: codemirror-vim 6.4.13 (an exact toKeys is normalized the way the mapping stored it, so a result with a space matches)
    • Plugin: src/vim/registration.ts
  • A Lua function mapping was in the keymap twice until the next settings reload. The Lua loader maps a function as soon as vim.keymap.set runs, so it works straight away. Loading the config then mapped it again under a second action name. It now keeps the loader's name, and puts the mapping back on top by lifting only that action's earlier entry, so a vimrc mapping on the same keys stays.
    • Fork: codemirror-vim 6.4.13 (unmap(lhs, ctx, { action }))
    • Plugin: src/main.ts (liftLuaAction), src/lua/loader.ts
  • _, +, - and <CR> after an operator acted on part of a line, not whole lines. Vim documents all four as linewise, but the engine took them charwise. On " a\nb\nc\nd\ne", d3_ left an empty line and c|d|e, where Neovim 0.12.6 leaves d|e. d_ deleted only the indent, c2_ lost it, and d<CR> did nothing. Upstream codemirror-vim has the same definitions. <CR> now also moves down in visual mode, as + does.
    • Fork: codemirror-vim 6.4.13
  • g@ marked one line too many after a linewise motion, and one character too many after a charwise one, so mini.comment's gcj commented three lines and 3gcc two. '[ and '] were the edges of the range handed to the operator, which ends past the text. They now mark where the text starts and ends, as in Neovim 0.12.6: g@j gives lines 0 to 1, g@_ stops at the first non-blank, and g@w ends on the character before the next word. Blockwise marks are unchanged.
    • Fork: codemirror-vim 6.4.13 (operatorfuncMarks)
  • A visual-block selection inside a code block showed only the cursor (#249). The editor draws a <C-v> selection on a layer behind the text, and Obsidian paints each code block line with an opaque background, which covered it. The selection is now painted on the text itself, as the browser paints v's, in Live Preview and source mode, and the layer is hidden while it is so no line is painted twice. v and V were not affected.
    • Plugin: src/vim/visual-block-highlight.ts, styles.css
  • << and >> put the cursor on column 0, which neither Vim nor Neovim does. They now follow startofline, as Neovim's do: off, the cursor keeps its screen column while the text moves under it, and after $ it stays on the last character; on, it goes to the first non-blank. This applies to >{motion}, <{motion}, counts, . and visual mode as well.
    • Fork: codemirror-vim 6.4.15 (operators.indent, operatorArgs.curswant)
  • j and k keep a screen column, as Vim's curswant is, not a character index. On lines indented with different tabs they landed on another character: with tabstop=4, j from the first tab of \tfoo bar baz went to column 0 of abcdefghijklmn instead of under the tab's last cell, column 3, as in Neovim. The column is kept across shorter lines and through $, and +, -, gj, gk, a click and {count}| set it, {count}| now counting screen columns, so a tab is as wide as it shows. G, gg, H, M, L, > and < take it after a vertical motion, so jjG lands where Neovim's does.
    • Fork: codemirror-vim 6.4.16 (moveByLines, moveToColumn, curswant)
  • Linewise d, =, <C-d>, <C-u>, <C-f> and <C-b> follow startofline, as they do in Neovim. Off, the cursor keeps the screen column on the line it ends on; on, it goes to the first non-blank. Linewise d kept the character index, = always went to the first non-blank, and the scroll and page keys kept the cursor's pixel position.
    • Fork: codemirror-vim 6.4.16 (operators.delete, operators.indentAuto, moveByScroll, moveByPage)
  • Operators across lines follow Vim's range rules. An exclusive motion that ends in column 0 of a later line ends on the line before, and the operation is linewise if it starts in the indent: d} from the start of a paragraph deletes whole lines. A charwise d across lines with only blanks around it is linewise too. w stops at the end of its last word's line only for word motions, where it was applied to every forward motion. Text objects are exempt from both rules, as in Vim. After a linewise d, < or >, the cursor keeps its screen column only when the end was not moved back.
    • Fork: codemirror-vim 6.4.17
  • A yank leaves the cursor at the start of what it yanked. yb, yk, ygg and yiw move the cursor back to where the yanked text starts, and yj and yw leave it where it is, as in Neovim. It always stayed put. A visual-line yank keeps the head's column only when the head is above the anchor.
    • Fork: codemirror-vim 6.4.18
  • ; and , after t or T stop where Vim's do. Whether they included the character came from whether the last search was a t, where it comes from the direction they search, as f/t and F/T do.
    • Fork: codemirror-vim 6.4.18
  • Screen columns count a wide character or an emoji as two cells, and a combining mark as none, as Neovim does. j, k, G, gg, H, M, L, |, > and < counted every UTF-16 unit as one cell, so on a line with Chinese, Japanese or emoji they landed a character away from where Neovim's do.
    • Fork: codemirror-vim 6.4.18
  • :j leaves the cursor on the joined line's first non-blank, not column 0, as in Neovim.
    • Fork: codemirror-vim 6.4.18
  • :m and :t follow startofline, as Neovim's do. Off, the cursor keeps the screen column it had before the command; on, it goes to the first non-blank. They went to the first non-blank either way.
    • Fork: codemirror-vim 6.4.18 (Vim.curswant, Vim.startOfLineColumn)
    • Plugin: src/workspace/commands.ts
  • With the Neovim backend, ]h and [h after an operator, and a yank of a Markdown text object, end where Neovim's own motions and text objects do. The companion imitated the bundled engine's old handling. It ran ]h as {n}G, so d]h from mid-line deleted whole lines, and it ran c]h as d]h, so the empty line Vim's linewise c leaves was gone. After yi* and the other Markdown text objects, it put the cursor back where it was, where Neovim's y leaves it at the start of the yanked text. The companion now moves the cursor and lets Neovim apply its own rules, with the bundled engine's . repeating them the same way.
    • Fork: codemirror-vim 6.4.20 (. works the range rules out again where it repeats)
    • Plugin: src/rpc/companion.lua

Tests

  • markdown-indent.test.ts: 10 Neovim = cases and 9 Markdown-aware cases, the Neovim ones measured in 0.12.6 on a Markdown buffer. markdown-indent.e2e.ts: = in a note with markdownindent off and on, a nested list, an item two levels deep, a code block and a paragraph. Without the operator registered, 4 of its 5 tests fail; with the fork's =, a nested list stays nested where Neovim flattens it.
  • Four specs read the files and settings they write before rebooting Obsidian. Each wrote a vimrc, an init.lua, plugin settings or harpoon pins and then rebooted with reloadObsidian({ vault }), which opens a fresh copy of the vault without them, and their assertions held with nothing loaded. They now reboot in place and check that the setup took effect first. With the fresh copy put back, 7 tests fail:
    • lua-config.e2e.ts: the four "Lua overrides the vimrc" tests compared Lua against no vimrc; the vimrc's value now has to read back first (scrolloff 6 where the vault already holds 5, textwidth 100, clipboard unnamed). "should not load init.lua when configMode is settings" now checks both survived the reboot.
    • config-management.e2e.ts: the reload test now checks the vimrc the plugin booted with, L mapped and H not, before replacing it.
    • harpoon.e2e.ts: "should persist pins across reload" was skipped as "reloadObsidian may reset plugin data"; it runs and expects exactly one pin.
  • rpc-obsidian-bridge.e2e.ts: a key typed after a <C-o> that went nowhere stays, and a waiting <C-o> is dropped when Neovim disconnects. The first types j 600 ms after the <C-o>, past the hold but inside the wait; without the fix the cursor went back to line 1 in the note and in Neovim, which is how "lets the cursor follow Neovim again when <C-o> has nowhere to go" failed in CI on a slow runner. The second disconnects inside the wait; without the fix the cursor moved to the jump list's entry, (1,4) for (1,1), and "Obsidian action failed: RPC client closed" showed.
  • alternate-file-cursor.e2e.ts: <C-^> and <C-w><C-^> return to the alternate note's last cursor, each position measured in Neovim 0.12.6, with startofline off and on. Without the fix, all three land on line 1.
  • fn.test.ts: strdisplaywidth on 14 strings, each width measured in Neovim 0.12.6: CJK, fullwidth, emoji wide and narrow, a combining accent, a skin-tone sequence and a flag. Before the fix, 6 fail, the emoji reading 1 for 2 and e with a combining accent 2 for 1.
  • keyword-preview-focus.e2e.ts: K with hoverfocus off and on, on a link, a footnote, plain text and an external link, q from the focused preview, a cursor moved before the preview shows, and vim.opt.hoverfocus from Lua. With the focus call removed, the link, footnote and Lua tests fail with focusInside false, and the q test fails too; with the cursor check removed, the moved-cursor test fails (focusInside true).
  • noremap-plugin-keys.e2e.ts: noremap rhs reaching the plugin's ]h from a Lua string mapping, a Lua nvim_feedkeys(..., "n"), :normal! and a vimrc nnoremap, past a user mapping on ]h, with controls that the user mapping wins when ]h is pressed and that a noremap rhs still skips a user function mapping. With the fork at 6.4.13, and again with the fixed fork but the plugin not marking its keys, the noremap tests stay on line 0 where they should reach line 2; with Lua function mappings marked as built-ins too, the noremap rhs lands on the user's ]h (line 0) and runs the user's function, changing the note.
  • e2e-headless runs the e2e specs on a private display. A nix develop command that starts Xvfb with herbstluftwm on a free display from :90 to :99, clears WAYLAND_DISPLAY so Electron cannot open on the desktop's compositor, runs the given command (npm run test:e2e by default) and tears the display down from a trap. Run directly, every spec's Obsidian window opened on the desktop and took its focus. A stale /tmp/.X90-lock with no server behind it is removed and the display reused. The display runs at 96 DPI: at Xvfb's default resolution Chromium reports a device pixel ratio of 1.046875, which put visual-line-editor-selection.e2e.ts's cursor a column off.
  • lua-expr-mapping.e2e.ts: the #236 recipe with a space leader, pressing j, 3j, k and 2k. With the fork at 6.4.11 the cursor stays on line 1 for all four, where it should reach lines 1, 4, 3 and 1, and the note's second line becomes jkkl1. That was measured with the original recipe; the test now uses the updated one (see Changed).
  • lua-expr-mapping.e2e.ts: the register and count stay pending for an expr mapping's keys, with real keys and each expected value measured in Neovim 0.12.6: 3Q with Q returning j reaches line 3, 3R with R returning vim.v.count1 .. "j" reaches line 33, "a2Y with Y returning yy yanks l0 and l1 into a, and "b3X with X returning nothing sees 3/b, then j reaches line 3. With the prefix removed, these reach lines 1 and 3, register a is empty, and the two existing count tests reach lines 2 and 1 instead of 3. Six unit tests in test/unit/lua/vim-v.test.ts drive the real expr callback and check what it feeds; with the prefix removed, the four that involve a count or register fail.
  • lua-plugin-mini-comment.e2e.ts: 3gcc and gcj with a line to spare, each against Neovim 0.12.6 with the same mini.comment. With codemirror-vim 6.4.12, 3gcc comments two lines and gcj comments all three. The existing gcj test has only two lines, so it could not see the extra one.
  • Fork: 8 linewise motion tests and 16 g@ mark tests, each expected value measured in Neovim 0.12.6. Before the fix, all 8 motion tests fail. With the mark fix disabled, 8 of the 16 mark tests fail: j, 2j, _, jjk, w, e, $ and vj each land one past Neovim's position.
  • Fork: noremap_skips_user_mappings_after_default_removed and noremap_feedKeys_from_action_after_default_removed, each with the default <Space> removed. Before the fix the first stays at [0,0] instead of [2,0], and the second throws RangeError: Maximum call stack size exceeded.
  • lua-fn-keymap-single-copy.e2e.ts: Lua function mappings on j and Q are one entry each, after the config loads, after three settings reloads and after four note switches, and both keys still work. With the exact removal reverted, Q → @@ is there four times after three reloads. With the Lua lift reverted, Q and j each have a second Lua action straight after the config loads.
  • Fork: unmap_exact_action_leaves_other_mappings and unmap_exact_toKeys_with_a_space. Without the action option, the first removes the wrong Q entry. Without the normalization, the second removes nothing.
  • lua-recipe-markdown-insert.e2e.ts: the Markdown-aware I recipe, read from the docs page, on 35 lines in normal and insert mode: the issue's list and task examples, empty items, quoted lists, callout titles, checkboxes holding ! or ✓, and lines that are not list items. Neovim 0.12 gives the same 35 results with the same file. With no recipe loaded, all 70 tests fail.
  • normal-tab-indent.e2e.ts: normal-mode <Tab> and <S-Tab> with tabindent off and on, with real keys: the jump-list walk and Obsidian's outdent when off; >>, 2<S-Tab> as << on two lines, 2<Tab> then ., <C-i> and vim.opt.tabindent when on; and cell moves in a native table with either. Obsidian's Shift-Tab gives the same text as << on one line, so the count is what tells them apart. With the source changes reverted, the four tabindent tests fail: <Tab> leaves - b unindented, 2<S-Tab> outdents only - b and leaves \t- c, and vim.opt.tabindent leaves plain. With <S-Tab> not handed back, the off test leaves \t- b. With the cell check skipped, <Tab> stays in cell {1, 1} instead of moving to {2, 0}.
  • visual-block-codeblock.e2e.ts: <C-v> on the #249 note in Live Preview and source mode, reading the background painted under each selected character. Without the fix, the four code block lines read the code block's rgb(35, 35, 35) instead of the selection colour. Also checks a column outside the block keeps the code background, <Esc>, and switching between v and <C-v> without moving.
  • visual-line-editor-selection.e2e.ts reads the drawn cursor a quarter of the way into its glyph, not at the middle. The middle is equally far from both of the glyph's edges, so posAtCoords could name either column: at a device pixel ratio of 1.046875 the Vk test read the cursor on d in third as column 5, 0.01 px past the midpoint, though it was drawn on column 4. With the cursor drawn one column right, the V, Vj and Vk tests still fail, reading 7 for 6 and 5 for 4, at both device pixel ratios.
  • Fork: 18 >/< cursor tests and 8 G/gg startofline tests, each expected position measured in Neovim 0.12.6 with tabstop=4 shiftwidth=4 noexpandtab, and HML split into startofline on and off. With the shift returning column 0 again, 19 tests fail. With G and gg keeping the character index, 4 fail. With startofline on by default again, 17 fail.
  • vimrc-custom-path.e2e.ts reads the vimrc it writes. Each scenario wrote a vimrc and then rebooted Obsidian with reloadObsidian({ vault }), which opens a fresh copy of the vault without that file or the vimrc path just saved, so no scenario ever loaded a vimrc. Its mappings could not show that either: j to gj and k to gk move like j and k on lines that do not wrap, and H to ^ landed where H did with startofline on. It now reboots on the same vault, and the mappings land where unmapped keys cannot: 2j and 2k, and ^ from the second line, where H goes to the first. With the fresh copy put back, 5 of its 6 tests fail, j reading line 1 for 2.
  • Fork: 22 j/k screen-column tests, 6 {count}| tests, 28 linewise d and = tests and 14 scroll and page tests, each expected position measured in Neovim 0.12.6 with tabstop=4. Against the previous code, 16 of the j/k tests, 4 of the | tests and 26 of the others fail. The scroll tests with startofline off pass either way, as the test editor's monospace font puts the pixel position and the screen column on the same character.
  • ex-move-copy-normal.e2e.ts: the cursor after :m, :t and :co, with startofline off and on, each expected position measured in Neovim 0.12.6 with tabstop=4. With the cursor sent to the first non-blank again, the four startofline-off tests fail.
  • Fork: {count}%, the range rules, yank cursor, ;/,, wide-character and :>/:< tests, each expected value measured in Neovim 0.12.6.
  • rpc-structural-nav.e2e.ts: d]h from mid-line, d]h j ., and d]h with no heading ahead, each compared with the bundled engine. With the companion's old {n}G operator motion, c]h loses its empty line and d]h from column 2 deletes first\nsecond\n whole where the fork leaves fi. With codemirror-vim 6.4.19, the fork's . deletes two and # C where Neovim deletes two alone, and three fork tests measured in Neovim 0.12.6 cover the same repeat; two of them fail on 6.4.19.

Documentation

  • Lua recipes: a Markdown-aware I (#241). A <leader>i that inserts where a line's text starts, after its indentation, any blockquote or callout > markers, and a callout's [!type], a list marker or a task checkbox; <C-q> does the same in insert mode, a Control chord because on macOS Option and a letter type a character instead of reaching the mapping. I itself is unchanged. The same code works in a Neovim config for the Neovim backend.

Full Changelog: 1.6.0...1.7.0

Don't miss a new motions release

NewReleases is sending notifications on new releases.