The sticky lines panel's height depends on the scroll position, so an exact middle doesn't always exist, and the old tolerance range let the previous position count as centred after every other k; instead, try the middle for each panel height we find and keep the closest position, staying put on a tie unless the caret moved to another line, so moves always scroll but typing never scrolls back and forth.
Add possibility to disable pushing to primary register on selction change. Needed to disable platform primary publishing so mouse selection won't publish too.
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>
For large files running search count blocked edt which caused IDE freezes.
Now we first try to quickly show coutn and if we are not able in given threshold move it to other thread and later on update UI accordingly.
Additionally added tests for large files
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>
`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>