Vim's `:help hist-names` lists "?" alongside "/" as an alias for the
search history, but getTypeByString only recognised "/". As a result,
histadd('?', ...), histdel('?'), histget('?') and histnr('?') failed
instead of operating on the search history.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a 2.46.1 section for the two fixes that went into the release (VIM-4310, VIM-4309), and documents the % register (VIM-1266) and the two g8 fixes under 2.47.0, the release they shipped in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
findMatchingPairOnCurrentLine read the character at the caret offset
without checking that the current line has any characters. On an empty
document this indexed past the end of the text. An empty line has no
pair characters to match, so return no match there, as Vim does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
determineSelectionType classified every range spanning more than one line as
line-wise. replaceText then built a line-wise VimSelection from it, which
expands to whole lines, so the text before startOffset on the first line and
after endOffset on the last one was silently discarded.
// "one two three\nfour five six\nseven eight"
replaceText(8, 18, "XXX") // "three\nfour"
// was: "XXX\nseven eight"
// expected: "one two XXX five six\nseven eight"
A multi-line range is now line-wise only when it actually covers those lines
completely, which is what ReplaceWithRegister's `2grr` needs; anything that
starts or ends mid-line stays character-wise and replaces exactly the
requested range.
This also drops the `isLine` check that the old code computed but could never
observe as true: it required the range to end with a line break while start
and end sat on the same line, and it was only consulted on the
startLine == endLine branch, where that cannot hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CaretTransactionImpl declared its own getLineStartOffset, getLineEndOffset
and getLine, duplicating the implementations it already inherits through
`EditorAccessor by EditorAccessorImpl(projectId)`. The copies had drifted:
getLineEndOffset dropped the allowEnd argument and always called the
single-argument VimEditor.getLineEndOffset, so it behaved as allowEnd = true
regardless of what the caller asked for.
The visible effect is that the same EditorAccessor function disagrees with
itself depending on how deeply it is nested:
change {
getLineEndOffset(line, allowEnd = false) // last character
withPrimaryCaret { getLineEndOffset(line, allowEnd = false) } // line break
}
ReadScopeImpl and TransactionImpl both rely on the delegate, so dropping the
three redundant overrides fixes the argument handling and removes the
duplication that let them drift apart in the first place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`isCommandHistoryWindow` used `Editor.getVirtualFile()`, which is not necessarily
populated while the editor is still being created - and that is exactly when it is
called, from `isAllowedFileEditor` via `vimDisabled`, to decide whether an editor
should be initialised. The command-line window opened by `q:`, `q/` and `q?` was
therefore classified as a non-Vim editor and `EditorListeners.add` never ran, so its
buffer never got its local option values.
Search count and search highlights are computed for the selected editor and for every
open editor, so searching for a pattern containing `\<` asked the cmdwin buffer for its
local 'iskeyword' and threw "Unexpected uninitialised buffer local value: iskeyword".
Resolve the file from the document instead, as every other check in `EditorHelper`
already does.
Found by the RandomActionsPropertyTest property tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`SliceFunctionTest` covered negative and out-of-range end indices, but
not an end index that resolves to zero, which is the boundary where the
exclusive end index of `slice()` and the inclusive end index of a sublist
expression disagree. Verified against the previous implementation: the
`end index of zero` cases fail (returning the whole collection), and the
negative cases that resolve to zero or below already passed.
Also pin the sublist side of that boundary: an explicit `-1` end index,
`-length` (first item only), and `-length - 1` (empty). These passed
before, and guard the shared `VimList.slice`/`VimString.substring`
against a regression in the other direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`VimList.slice` and `VimString.substring` treated an exclusive end index
of zero as "count from the end", i.e. as the size of the collection. That
convention exists only because `SublistExpression` has to convert Vim's
*inclusive* end index (which defaults to -1, the last item) into an
exclusive one, so `endInclusive + 1` legitimately produces zero for the
common `list[1:]` and `list[1:-1]` cases.
`slice()`, however, takes an end index that is already exclusive, so
`slice([1, 2, 3, 4, 5], 0, 0)` must be an empty list, and
`slice([1, 2, 3, 4, 5], 0, -5)` likewise resolves to an exclusive end of
zero. Both returned the entire collection instead. (See Vim's
`list_slice_or_index` / `eval_index_inner`, which decrement the end index
when it is exclusive and produce an empty result once it drops below
zero.)
Make the end index of `slice`/`substring` an unambiguous absolute index
and move the resolution of Vim's negative indices into the two call
sites, where the inclusive/exclusive distinction is known.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tag stack entries keep the line number they were recorded with and are not
adjusted on edits. When that line no longer exists, :tags called
getLineText() with an out-of-range line and failed with
"Incorrect line: N, out of M". Show empty text for such entries instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A search range consisting of a single delimiter made SearchAddress call
substring(1, 0), throwing StringIndexOutOfBoundsException. An empty
pattern now uses the last search pattern, as in Vim, and reports E35
when there is none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In ForLoop (List iterable) and ForLoopWithList, the Continue branch used
Kotlin's `continue`, which jumped back to the `while` condition and
skipped `index += 1`. As a result `for x in [1, 2] | continue | endfor`
never terminated. Fall through to the increment instead.
Add regression tests for continue/break when iterating over lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`y<C-V>{motion}` (`:help o_CTRL-V`) makes `getMotionRange` return a
rectangular block, i.e. a multi-range `TextRange` with one range per row
of the block. `YankGroupBase.yankMotion` assumed a single range and
tripped `assert(motionRange.size() == 1)`; with assertions disabled it
silently yanked only the first row of the block, characterwise.
Handle a blockwise-forced motion the way the delete path already does:
store the whole multi-range as a blockwise yank and skip the
characterwise-to-linewise promotion, which does not apply to a block.
Found by the YankDeletePropertyTest property test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`c_CTRL-R_CTRL-F` uses the *exclusive* end offset of the current incsearch
match as the document offset to look for a filename at. When the match ends
at the very end of the document, that offset equals the text length, and
`findFilenameAtOrFollowingCursor` indexed the text with it directly.
Guard against an offset that isn't a valid index into the text, and return
null, which reports E446 "No file name under cursor". This is the same guard
that `findWordAtOrFollowingCursor` already has, and it matches Vim, which
finds no identifier or filename when the position is past the end of the line.
Found by the property based tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`insertText` declared a local `offset` that shadowed its own `offset`
parameter, leaving the parameter unused and the body reaching for
`commandLine.caret.offset` instead. The two are the same value (that is what
the base class passes), so this is not a behaviour change — but the shadowing
hides the parameter and makes the function harder to read.
Name the local after what it holds, and use the parameter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`findPreviousWordOne` always steps back one character before it starts
skipping, to avoid getting stuck on the start of a word. When the caller
passes `allowMoveFromWordStart = false`, the character at the new position is
read to decide whether to skip at all — but if the search started at offset 0,
that position is -1 and the read throws.
The only caller that passes `allowMoveFromWordStart = false` is
`findWordObject` when expanding a right-to-left visual selection, so `viw`,
`vaw`, `viW` and `vaW` crashed whenever the selection reached the very start of
the file, e.g. `v h iw` with the caret on the second character.
Return 0 as soon as we step past the start of the text. Every path through the
rest of the function already returns 0 in that case, so this only affects the
out of bounds read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`g8` is documented as printing "the hex values of the bytes used in the
character under the cursor, assuming it is in UTF-8 encoding". IdeaVim
printed the hex value of a single UTF-16 code unit instead, so anything
outside ASCII was wrong: `é` reported "e9" rather than "c3 a9", and a
character outside the BMP reported half of its surrogate pair ("d83d")
rather than its four UTF-8 bytes.
Encode the full code point at the caret to UTF-8 and format each byte as
two lowercase hex digits, space separated. ASCII output is unchanged.
Vim also appends the bytes of trailing composing characters, separated
by `+`. That is still not implemented, and is noted in the KDoc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`g8` read `editor.text()[caret.offset]` unguarded. On an empty file, or
with the caret on an empty last line, the offset is the end of the text
and the read threw IndexOutOfBoundsException.
Vim's cursor never sits on the line break - at the end of a line it sits
on the line's terminating NUL - so there is no character under it and
Vim reports "NUL". Do the same, which covers both the crashing offsets
and an empty line in the middle of the file (which previously reported
the hex value of the line feed, "a").
Adds FileGetHexActionTest, which had no coverage at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`gx` opens the URL under the caret with an external program and never moves
the caret - the existing `test gx opens the URL without moving the caret`
asserts exactly that. Saving a jump location was therefore pointless and
actively harmful:
- `addJump(reset = true)` pushed the *unchanged* caret position onto the jump
list and reset the jump spot, so a `<C-O>` right after `gx` landed on the
caret's own line instead of returning to the previous jump.
- `saveJumpLocation` also overwrites the `'` mark, breaking `''`.
- The action is a `ForEachCaret` handler, so both happened once per caret.
Vim does not list `gx` under `:help jump-motions`, and netrw's `gx`
implementation leaves the jump list alone.
Also drop the `FLAG_SAVE_JUMP` flag: it is only honoured by
`MotionActionHandler`, so on a `VimActionHandler` it was inert and misleading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tbr page was still a byte-for-byte copy of the 2.45.0 release notes,
so its content is replaced with the features and fixes currently under
To Be Released: the YankRing extension, modeless selection, the
current-search-match highlight, :stopinsert, command-line sethandler and
the macOS key repeat setting, plus the polish & fixes list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Document VIM-4287 and VIM-4301 under 2.45.0 (both commits are contained
in the 2.45.0 tag) and list PRs #1946 and #1949 under Merged PRs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Document the VIM-330 spelling dictionary commands in the changelog and
bring whatsnew-tbr.html up to date with the changelog entries that were
not yet represented on the page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SelectLastFileCommand passed 999 to selectFile(), but both implementations
only treat 99 as the "select last file" sentinel. As a result `:last` returned
an error and never switched buffers for any realistic number of open files —
a regression introduced when the original Java handler (which correctly passed
99) was rewritten in Kotlin.
Use the shared VimFile.LAST_FILE_SENTINEL constant so the command and the
implementations agree by construction, and add a behavior test that opens
several files and asserts `:last` selects the last one. The test fails against
the old 999 value and passes with the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `selectFile(count)` API uses the magic number 99 as a sentinel meaning
"select the last open file". This value was duplicated as a bare literal in
both implementations (IjFileGroup and FileRemoteApiImpl), with nothing tying
it to the callers that must produce it. Name it once on the VimFile companion
and reference it from both implementations so the value can no longer drift.
No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was no count-prefixed `yy` test in the normal-mode suite, leaving the
`count - 1` / `min(..., fileSize)` line-range logic in YankGroupBase.yankLine
uncovered. Add two tests:
- `2yy` yanks the expected multiple lines.
- `5yy` near end-of-file clamps to the last line instead of running past the
end of the buffer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`end` is computed as `min(moveCaretToRelativeLineEnd(...) + 1, fileSize)`.
`moveCaretToRelativeLineEnd` (the only implementation, in VimMotionGroupBase)
always returns a normalized, non-negative offset, so after the `+ 1` and
`min(..., fileSize)` the result is always >= 0. The `if (end == -1) continue`
check can therefore never fire and is dead code.
The equivalent line-range computation in VimChangeGroupBase.deleteLine
correctly has no such guard, confirming this was a stale leftover. This is a
behavior-preserving cleanup. Also drops an accidental double blank line in
yankMotion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the changelog and the What's New page for the next release.
Changelog (CHANGES.md), added to [To Be Released]:
- Feature: VIM-1850 'keymap' option, 'iminsert', <C-^>, :loadkeymap and the
:lmap/:lnoremap/:lunmap/:lmapclear language-mapping commands
- Fix: VIM-4281 output panel and command line now refresh colors on theme change
- Merged PRs: 1929, 1928, 1925
What's New (whatsnew-tbr.html), incremental update of the existing page:
- New section for the 'keymap' language-mappings feature
- Mini cards for gx and :w with a file argument
- Fixes list entries for theme refresh, :s///g on tab-indented lines, method
navigation crash, and caret after delete-to-end from <C-O>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the placeholder comments in the textobj-user section with a full
datetime spec so the config format is clear.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`val func= arguments[0]` was missing a space before `=`. This is the only
such occurrence in the entire vim-engine and IntelliJ main source, so it is
a clear unintended slip in the recently-added `call()` function rather than
a deliberate style choice. Align it with Kotlin conventions and the rest of
the codebase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline fully-qualified references to
com.maddyhome.idea.vim.state.mode.CtrlXCompletionMode with a proper
import, matching the sibling completion actions
(InsertFilePathCompletionAction, InsertXCompletionAction) in the same
package. Behavior is unchanged; this only improves readability and
import consistency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add missing changelog entries for the mouse option and indentwise rename,
and rebuild the What's New page from the current [To Be Released] section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the changelog and the What's New page for the upcoming release.
CHANGES.md ([To Be Released]):
- Features: 'langmap'/'langremap' support (VIM-2283)
- Fixes: <C-O> caret at end of line (VIM-315)
- Merged PRs: #1861 (langmap), #1809 (VIM-315)
whatsnew-tbr.html: incrementally added the new langmap feature section
and the new fixes to "polish & fixes", preserving the existing content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
count(list, expr, ic, start) handled the start index incorrectly:
- Negative start indices (which Vim counts from the end of the list)
were ignored, falling back to counting the whole list.
- An out-of-range start index silently counted the whole list instead
of raising an error.
Vim resolves the start index via list_find(), which supports negative
indices and reports E684 (list index out of range) when the index is
invalid. Match that behavior: normalize negative indices and throw E684
for out-of-range values. The start argument is only applied when
explicitly provided, so count(list, expr) on an empty list still
returns 0 rather than erroring.
Adds regression tests for zero, negative, and out-of-range start indices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vim's get(list, idx [, default]) supports negative indices, which count
from the end of the list (e.g. -1 is the last item). IdeaVim used
List.getOrElse(idx) directly, which treats any negative index as
out-of-bounds and returns the default value instead.
Normalize negative indices before lookup, matching the pattern already
used in remove(). Adds regression tests for negative and out-of-range
negative indices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document VIM-2883 ('inccommand' substitute preview) and VIM-3049
(<C-Y>/<C-E> completion popup keys) under [To Be Released].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `injector.globalOptions().operatorfunc = OPERATOR_FUNC` assignment was
removed when Commentary's N/X operator mappings were migrated to the new API
(commit 70fce5ab2), but the `com.maddyhome.idea.vim.api.globalOptions` import
was left behind. It is now unused.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove dead if/else in visitRange: both branches returned the same
Multi.RangeMulti value, making the condition a no-op
- Simplify `?: run { 0 }` to `?: 0` in limited lookbehind visitors;
a run block with a single constant expression adds no value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>