1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-11-29 16:38:24 +01:00

Compare commits

..

46 Commits

Author SHA1 Message Date
Alex Plãte
226f5e27b1 Merge branch 'master' into macro-code-completion 2025-10-24 15:23:22 +03:00
claude[bot]
cc3c132ccb Maintenance: Java tests - Improve null safety in fold region handling
Replace unsafe non-null assertions (!!) with explicit null checks that provide
meaningful error messages when fold regions are not found. This makes test
failures easier to debug by clearly indicating what was expected.

Changes:
- ChangeActionJavaTest: Fixed 4 instances in testInsertAfterToggleFold and testInsertBeforeFold
- VisualInsertActionJavaTest: Fixed 1 instance in test block insert after folds

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:39:39 +03:00
claude[bot]
13fb2d53f1 Maintenance: LenFunctionTest - Address review feedback
- Remove duplicate empty dictionary test (already covered at line 28)
- Rename 'test len with zero' to 'test len with zero number' for clarity
- Rename 'test len with negative number' to 'test len with negative numbers' (plural)

Co-authored-by: Alex Plãte <AlexPl292@users.noreply.github.com>
2025-10-24 14:38:49 +03:00
claude[bot]
c803a1cf24 Maintenance: LenFunctionTest - Add comprehensive edge case coverage
What was inspected:
- LenFunctionTest.kt and its corresponding implementation LenFunctionHandler.kt

Issues found:
- Test coverage was limited to basic happy path scenarios
- Missing tests for important edge cases: empty strings, zero, negative numbers,
  large numbers, empty collections, and special character handling
- No tests distinguishing between single-quoted and double-quoted string behavior
  in Vimscript

Changes made:
- Added 9 new test methods covering:
  * Empty strings (both single and double quoted)
  * Empty lists and dictionaries
  * Zero and negative numbers
  * Large numbers
  * Multi-element collections
  * Strings with escape sequences (documenting Vim's single vs double quote behavior)

Why this improves the code:
- Ensures len() function handles all data type edge cases correctly
- Documents expected behavior for Vimscript string quoting conventions
- Provides regression protection against future changes
- Aligns with testing best practices by covering boundary conditions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:38:49 +03:00
Alex Plate
1d60e47bb8 Remove the PR pre-fetch 2025-10-24 14:38:37 +03:00
Alex Plate
664c433ee9 Add test for Visual mode inclusive caret positioning in templates
Adds a test to verify that when using 'idearefactormode=visual' with
inclusive selection (default), the caret is correctly positioned at the
inclusive end of the selection (on the last character) rather than the
exclusive end (after the last character).

This test covers the caret adjustment logic in IdeaSpecifics.kt that
moves the caret from IntelliJ's exclusive end position to Vim's
inclusive end position for Visual mode when 'selection' doesn't contain
"exclusive".
2025-10-24 14:32:03 +03:00
Alex Plate
67a44b4d25 Fix boundary check and null safety in mapleader parsing
Adds length validation before accessing string indices and replaces unsafe
!! operator with proper null handling to prevent crashes on invalid mapleader values.
2025-10-24 14:31:23 +03:00
Matt Ellis
b4915d08cd Remove redundant code
The template listener is called immediately for the first variable, which will do the same thing.
2025-10-24 14:29:58 +03:00
Matt Ellis
6b7b18d947 Switch to Normal when accepting an inline rename
The 'idearefactormode' default of Select would leave an inline rename in Insert mode, and hitting Escape to return to Normal frequently cancelled the rename.

Also fixes various edge cases when moving between template variables. Often, this works due to a change in selection, but if there's an empty template variable and no current selection, there's no change in selection, and the mode isn't updated.
2025-10-24 14:29:58 +03:00
Matt Ellis
54cc89f641 Fix missing read action exceptions 2025-10-24 14:29:58 +03:00
Matt Ellis
1048d06586 Remove no longer used 'ideaglobalmode' text 2025-10-24 14:29:58 +03:00
azjf
7f0ab93ea7 Return \<${specialKeyBuilder} for VimStringParserBase#parseVimScriptString(string: String) 2025-10-24 13:44:57 +03:00
azjf
a0f923512a Add support for non-control-character mapleader such as \<C-Space> 2025-10-24 13:44:57 +03:00
Alex Plate
d60af9afa1 Remove the checkout from the manual command 2025-10-24 13:14:32 +03:00
Alex Plate
1ef919f73a Add checkout step 2025-10-24 13:05:03 +03:00
Alex Plate
f6947d73f6 Trying to disable oidc for forkes 2025-10-24 11:22:14 +03:00
Alex Plate
54c12470f3 Claude Code now has an ability to create inline comments 2025-10-24 11:10:51 +03:00
Alex Plate
8aa8725a8d Update Claude Code workflow
I'm mostly trying to force Claude Code to work with the forks
2025-10-24 11:08:35 +03:00
IdeaVim Bot
1e1bbbac2a Add github-actions[bot] to contributors list 2025-10-20 09:01:57 +00:00
claude[bot]
0bb3524118 Maintenance: IjVimMessages - Fix redundant toString() call and improve null safety
In clearStatusBarMessage(), the mutable 'message' property was being
accessed multiple times without capturing it in a local variable. This
could theoretically cause issues if the property changed between the
null check and the usage. Additionally, calling toString() on a String
is redundant and creates an unnecessary allocation.

Fixed by capturing the message in a local variable at the start of the
method, which allows Kotlin's smart cast to work properly and removes
the need for the redundant toString() call.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 08:45:45 +03:00
claude[bot]
12596540e9 Maintenance: Line matchers - Add defensive bounds checking
Added bounds checking to EndOfLineMatcher and StartOfLineMatcher to
prevent potential IndexOutOfBoundsException when index is outside the
valid text range. This follows the defensive programming pattern used
in other matchers like CharacterMatcher, DotMatcher, StartOfWordMatcher,
and EndOfWordMatcher.

Changes:
- EndOfLineMatcher: Added check for index > length before array access
- StartOfLineMatcher: Added check for index < 0 or index > length

While the NFA engine should prevent invalid indices, these defensive
checks improve code safety and consistency across all matcher
implementations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 08:44:51 +03:00
IdeaVim Bot
3eadff6401 Add github-actions[bot] to contributors list 2025-10-18 09:01:56 +00:00
claude[bot]
52f4c24b1e Docs: Add regression testing guideline to maintenance instructions
Added a new bullet point under the Testing section that instructs
maintainers to write regression tests when fixing bugs. These tests
should:
- Fail with the buggy implementation
- Pass with the fix
- Document what bug they're testing
- Test the specific boundary condition that exposed the bug

This ensures bugs don't resurface during future refactorings and
provides clear documentation of the expected behavior.

Co-authored-by: Alex Plãte <AlexPl292@users.noreply.github.com>
2025-10-17 21:15:47 +03:00
claude[bot]
7de2ea0e0b Add regression test for string indexing boundary condition
This test validates the off-by-one error fix in IndexedExpression.kt:56.
When accessing a string at index equal to its length, the code should
return an empty string rather than throwing IndexOutOfBoundsException.

The old condition (idx > text.length) would allow idx == text.length to
pass through, causing an exception. The new condition (idx >= text.length)
correctly treats this as out of bounds.

Test case: 'hello'[5] should return '' since 'hello'.length == 5

Co-authored-by: Alex Plãte <AlexPl292@users.noreply.github.com>
2025-10-17 21:15:47 +03:00
claude[bot]
a9fc2c58a6 Maintenance: IndexedExpression - Fix off-by-one error and variable shadowing
Fixed three issues in IndexedExpression.kt:
1. Off-by-one error: Changed condition from `idx > text.length` to `idx >= text.length` to properly handle boundary cases when indexing strings
2. Redundant evaluation: Reused `indexValue` instead of re-evaluating `index.evaluate()` in the string indexing path
3. Variable shadowing: Renamed local variable from `index` to `indexNum` in `assignToListItem` to avoid shadowing the property

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 21:15:47 +03:00
claude[bot]
1aa586484f Maintenance: Change actions - Remove wildcard imports
Replace wildcard import `java.util.*` with explicit import `java.util.EnumSet`
in 9 action classes in the change/change package. This improves code clarity
and follows the project's convention of avoiding wildcard imports.

Files updated:
- AutoIndentLinesVisualAction
- ChangeCharactersAction
- ChangeEndOfLineAction
- ChangeLineAction
- ChangeMotionAction
- ChangeVisualLinesAction
- ChangeVisualLinesEndAction
- FilterMotionAction
- ReformatCodeVisualAction

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 12:41:03 +03:00
Alex Plate
aa24d53b18 Get rid of the deprecated method in the configuration 2025-10-17 12:39:18 +03:00
Alex Plate
c396d98022 Add RandomOrderTests configuration to TeamCity project
Introduce a new build type `RandomOrderTests` for detecting order-dependent bugs. Update project to include the new build type.
2025-10-17 12:38:55 +03:00
Alex Plate
db767f534f Remove an unclear test 2025-10-17 12:30:29 +03:00
Alex Plate
d955b1b85c Fix an exception that we try to record wrong characters 2025-10-17 12:21:57 +03:00
IdeaVim Bot
0fdfc04068 Add github-actions[bot] to contributors list 2025-10-14 09:01:55 +00:00
claude[bot]
244d13a3cc Maintenance: HintGenerator - Fix variable shadowing and remove wildcard import
Fixed a variable shadowing bug in the hint generation logic where the lambda
parameter name 'it' was being shadowed in nested lambda, causing incorrect
logic when checking for hint conflicts. The code now uses explicit parameter
names 'candidateHint' and 'existingHint' to make the logic clear and correct.

Also replaced wildcard import (java.util.*) with explicit import of
WeakHashMap, following project conventions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 10:23:36 +03:00
claude[bot]
4a2600738f Maintenance: RangeFunctionTest - Fix missing assertion and improve imports
Fixed a missing assertion in the test case "test range negative with start
past end throws error" that was not verifying the error condition actually
occurred. Also cleaned up imports by adding a proper import for assertTrue
instead of using the fully qualified kotlin.test.assertTrue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 18:01:47 +03:00
claude[bot]
82ed26a701 Maintenance: PrintLineNumberCommand - Fix out-of-bounds range handling
Add bounds checking to clamp line numbers to the last line of the document,
preventing potential exceptions when accessing line text with out-of-bounds
ranges like `:$+100=`. This matches the behavior of other commands like
GoToLineCommand.

Also add tests to verify the edge case behavior with and without flags.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 18:00:49 +03:00
Alex Plate
cf5dce3ce7 Remove "getSelectionModel" from VimRegex.kt 2025-10-13 17:37:23 +03:00
Alex Plate
c68e216c87 Add copyright year policy to maintenance instructions
Clarify that copyright years should only be updated when making
substantive changes to files, and should not be mentioned in commits.
2025-10-13 16:22:16 +03:00
IdeaVim Bot
c12f4a75ac Add github-actions[bot] to contributors list 2025-10-10 09:01:54 +00:00
claude[bot]
9a069e5ef8 Maintenance: ExPanelBorder - Update copyright and remove redundant modifier
- Update copyright year from 2023 to 2025
- Remove redundant 'internal constructor()' modifier (already implied by internal class)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:51:32 +03:00
Alex Plate
27f2e56635 Update codebase maintenance workflow to run daily
Change schedule from weekly (Monday) to daily execution at 6 AM UTC to enable more frequent code quality checks.
2025-10-09 14:26:32 +02:00
Alex Plate
7a0bdb8f3d Add reference to CONTRIBUTING.md in maintenance instructions
Expand the architecture reference to include more details about what contributors can find in CONTRIBUTING.md: architecture overview, testing guidelines, patterns, and awards program information.
2025-10-09 14:16:36 +02:00
Alex Plate
1d9bb6ec70 Expand testing corner cases in contribution guide
Add comprehensive corner case categories for testing Vim commands including position-based, content-based, selection-based, motion-based, buffer state, and boundary conditions to help contributors write more thorough tests.
2025-10-09 14:16:36 +02:00
claude[bot]
bb62dcdc15 Maintenance: DeletePreviousWordAction - Update copyright and add test
Update copyright year to 2025 and add test coverage for edge case when
caret is at the beginning of the command line.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:15:59 +03:00
Alex Plate
fdcb954e31 Remove the VimSelectionModel as it's not used anywhere 2025-10-09 13:14:49 +02:00
Alex Plate
747e4053ba Update codebase maintenance workflow to skip PR creation when no changes are made
Previously the workflow instructed Claude to always create a PR even when no
issues were found. This resulted in unnecessary PRs documenting inspections
where no changes were needed.

Now the workflow only creates PRs when actual changes are made to the codebase.
2025-10-09 13:10:41 +02:00
Alex Plate
448c19af47 Fix SortCommand to use caret selection instead of editor selection model
The SortCommand was incorrectly using editor.getSelectionModel() which
returns the editor's global selection model. This is problematic in
multi-caret scenarios where each caret has its own independent selection.

Changed to use caret.hasSelection(), caret.selectionStart, and
caret.selectionEnd directly, which correctly handles per-caret selections.

This also removes the only meaningful usage of VimEditor.getSelectionModel()
in vim-engine, making that interface a candidate for removal in future
cleanup.
2025-10-09 13:09:38 +02:00
Alex Plate
f0edc797dc Add codebase maintenance workflow
This workflow enables automated codebase maintenance by randomly selecting
and inspecting parts of the codebase for quality issues, bugs, and
improvements.

Key features:
- Random file/area selection for inspection
- Comprehensive code review guidelines (style, quality, bugs, architecture)
- Focus on genuine improvements, not pedantic changes
- Always creates PR to document inspection results
- Runs weekly on Mondays, can be triggered manually

The maintenance instructions cover:
- What to check: code style, null safety, test coverage, IdeaVim-specific issues
- When to make changes vs document issues
- Commit best practices (split complex changes into multiple commits)
- IdeaVim-specific considerations (enablement checks, Vim compatibility)
2025-10-09 13:04:55 +02:00
50 changed files with 1951 additions and 622 deletions

View File

@@ -0,0 +1,206 @@
# Codebase Maintenance Instructions
## Goal
Perform routine maintenance on random parts of the IdeaVim codebase to ensure code quality, consistency, and catch potential issues early. This is not about being overly pedantic or making changes for the sake of changes - it's about keeping an eye on the codebase and identifying genuine issues.
## Approach
### 1. Select Random Area
Choose a random part of the codebase to inspect. Use one of these strategies:
```bash
# Get a random Kotlin file
find . -name "*.kt" -not -path "*/build/*" -not -path "*/.gradle/*" | shuf -n 1
# Get a random package/directory
find . -type d -name "*.kt" -not -path "*/build/*" | shuf -n 1 | xargs dirname
# Pick from core areas randomly
# - vim-engine/src/main/kotlin/com/maddyhome/idea/vim/
# - src/main/java/com/maddyhome/idea/vim/
# - tests/
```
**Important**: You're not limited to the file you randomly selected. If investigating reveals related files that need attention, follow the trail. The random selection is just a starting point.
## 2. What to Check
### Code Style & Formatting
- **Kotlin conventions**: Proper use of data classes, sealed classes, when expressions
- **Naming consistency**: Follow existing patterns in the codebase
- **Import organization**: Remove unused imports, prefer explicit imports over wildcards (wildcard imports are generally not welcome)
- **Code structure**: Proper indentation, spacing, line breaks
- **Documentation**: KDoc comments where needed (public APIs, complex logic)
- **Copyright years**: Do NOT update copyright years unless you're making substantive changes to the file. It's perfectly fine for copyright to show an older year. Don't mention copyright year updates in commit messages or change summaries
### Code Quality Issues
- **Null safety**: Proper use of nullable types, safe calls, Elvis operator
- **Error handling**: Appropriate exception handling, meaningful error messages
- **Code duplication**: Identify repeated code that could be extracted
- **Dead code**: Unused functions, parameters, variables
- **TODOs/FIXMEs**: Check if old TODOs are still relevant or can be addressed
- **Magic numbers/strings**: Should be named constants
- **Complex conditionals**: Can they be simplified or extracted?
### Potential Bugs
- **Off-by-one errors**: Especially in loops and range operations
- **Edge cases**: Empty collections, null values, boundary conditions
- **Type safety**: Unnecessary casts, unchecked casts
- **Resource handling**: Proper cleanup, try-with-resources
- **Concurrency issues**: Thread safety if applicable
- **State management**: Proper initialization, mutation patterns
- **IdeaVim enablement checks**: Verify that `injector.enabler.isEnabled()` or `Editor.isIdeaVimDisabledHere` are not missed in places where they should be checked. These functions determine if IdeaVim is active and should be called before performing Vim-specific operations
### Architecture & Design
- **Separation of concerns**: Does the code have a single responsibility?
- **Dependency direction**: Are dependencies pointing the right way?
- **Abstraction level**: Consistent level of abstraction within methods
- **Vim architecture alignment**: Does it match Vim's design philosophy?
- **IntelliJ Platform conventions**: Proper use of platform APIs
### Testing
- **Test coverage**: Are there tests for the code you're reviewing?
- If checking a specific command or function, verify that tests exist for it
- If tests exist, check if they cover the needed cases (edge cases, error conditions, typical usage)
- If tests don't exist or coverage is incomplete, consider creating comprehensive test coverage
- **Test quality**: Do tests cover edge cases?
- **Test naming**: Clear, descriptive test names
- **Flaky tests**: Any potentially unstable tests?
- **Regression tests for bug fixes**: When fixing a bug, always write a test that:
- Would fail with the old (buggy) implementation
- Passes with the fixed implementation
- Clearly documents what bug it's testing (include comments explaining the issue)
- Tests the specific boundary condition or edge case that exposed the bug
- This ensures the bug doesn't resurface in future refactorings
## 3. Investigation Strategy
Don't just look at surface-level issues. Dig deeper:
1. **Read the code**: Understand what it does before suggesting changes
2. **Check related files**: Look at callers, implementations, tests
3. **Look at git history**: `git log --oneline <file>` to understand context
4. **Find related issues**: Search for TODOs, FIXMEs, or commented code
5. **Run tests**: If you make changes, ensure tests pass
6. **Check YouTrack**: Look for related issues if you find bugs
## 4. When to Make Changes
**DO fix**:
- Clear bugs or logic errors
- Obvious code quality issues (unused imports, etc.)
- Misleading or incorrect documentation
- Code that violates established patterns
- Security vulnerabilities
- Performance issues with measurable impact
**DON'T fix**:
- Stylistic preferences if existing code is consistent
- Working code just to use "newer" patterns
- Minor formatting if it's consistent with surrounding code
- Things that are subjective or arguable
- Massive refactorings without clear benefit
**When in doubt**: Document the issue in your report but don't make changes.
## 5. Making Changes
If you decide to make changes:
1. **Make focused commits**: One logical change per commit
- If the change affects many files or is complicated, split it into multiple step-by-step commits
- This makes it easier for reviewers to understand the changes
- Example: First commit renames a function, second commit updates callers, third commit adds new functionality
2. **Write clear commit messages**: Explain why, not just what
3. **Run tests**: `./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test`
## 6. Examples
### Good Maintenance Examples
**Example 1: Found and fixed null safety issue**
```
Inspected: vim-engine/.../motion/VimMotionHandler.kt
Issues found:
- Several nullable properties accessed without safe checks
- Could cause NPE in edge cases with cursor at document end
Changes:
- Added null checks with Elvis operator
- Added early returns for invalid state
- Added KDoc explaining preconditions
```
**Example 2: No changes needed**
```
Inspected: src/.../action/change/ChangeLineAction.kt
Checked:
- Code style and formatting ✓
- Null safety ✓
- Error handling ✓
- Tests present and comprehensive ✓
Observations:
- Code is well-structured and follows conventions
- Good test coverage including edge cases
- Documentation is clear
- No issues found
```
**Example 3: Found issues but didn't fix**
```
Inspected: tests/.../motion/MotionTests.kt
Issues noted:
- Some test names could be more descriptive
- Potential for extracting common setup code
- Tests are comprehensive but could add edge case for empty file
Recommendation: These are minor quality-of-life improvements.
Not critical, but could be addressed in future cleanup.
```
## IdeaVim-Specific Considerations
- **Vim compatibility**: Changes should maintain compatibility with Vim behavior
- **IntelliJ Platform**: Follow IntelliJ platform conventions and APIs
- **Property tests**: Can be flaky - verify if test failures relate to your changes
- **Action syntax**: Use `<Action>` in mappings, not `:action`
- **Architecture & Guidelines**: Refer to [CONTRIBUTING.md](../CONTRIBUTING.md) for:
- Architecture overview and where to find specific code
- Testing guidelines and corner cases to consider
- Common patterns and conventions
- Information about awards for quality contributions
## Commands Reference
```bash
# Run tests (standard suite)
./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test
# Run specific test class
./gradlew test --tests "ClassName"
# Check code style
./gradlew ktlintCheck
# Format code
./gradlew ktlintFormat
# Run IdeaVim in dev instance
./gradlew runIde
```
## Final Notes
- **Be thorough but practical**: Don't waste time on nitpicks
- **Context matters**: Understand why code is the way it is before changing
- **Quality over quantity**: One good fix is better than ten trivial changes
- **Document your process**: Help future maintainers understand your thinking
- **Learn from the code**: Use this as an opportunity to understand the codebase better
Remember: The goal is to keep the codebase healthy, not to achieve perfection. Focus on genuine improvements that make the code safer, clearer, or more maintainable.

View File

@@ -3,26 +3,13 @@ name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
pull-requests: write
id-token: write
steps:
@@ -37,6 +24,10 @@ jobs:
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
CONTRIBUTOR: ${{ github.event.pull_request.user.login }}
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
@@ -44,11 +35,13 @@ jobs:
- Security concerns
- Test coverage
Provide detailed feedback using inline comments for specific issues.
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
claude_args: '--allowed-tools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"'

View File

@@ -0,0 +1,55 @@
name: Codebase Maintenance with Claude
on:
schedule:
# Run every day at 6 AM UTC
- cron: '0 6 * * *'
workflow_dispatch: # Allow manual trigger
jobs:
maintain-codebase:
runs-on: ubuntu-latest
if: github.repository == 'JetBrains/ideavim'
permissions:
contents: write
pull-requests: write
id-token: write
issues: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need history for context
- name: Run Claude Code for Codebase Maintenance
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
## Task: Perform Codebase Maintenance
Your goal is to inspect a random part of the IdeaVim codebase and perform maintenance checks.
Please follow the detailed maintenance instructions in `.claude/maintenance-instructions.md`.
## Creating Pull Requests
**Only create a pull request if you made changes to the codebase.**
If you made changes, create a PR with:
- **Title**: "Maintenance: <area> - <brief description>"
- Example: "Maintenance: VimMotionHandler - Fix null safety issues"
- **Body** including:
- What area you inspected
- Issues you found
- Changes you made
- Why the changes improve the code
If no changes are needed, do not create a pull request.
# Allow Claude to use necessary tools for code inspection and maintenance
claude_args: '--allowed-tools "Read,Edit,Write,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(./gradlew:*),Bash(find:*),Bash(shuf:*)"'

View File

@@ -5,6 +5,7 @@ import _Self.buildTypes.LongRunning
import _Self.buildTypes.Nvim
import _Self.buildTypes.PluginVerifier
import _Self.buildTypes.PropertyBased
import _Self.buildTypes.RandomOrderTests
import _Self.buildTypes.TestingBuildType
import _Self.subprojects.GitHub
import _Self.subprojects.Releases
@@ -16,7 +17,8 @@ import jetbrains.buildServer.configs.kotlin.v2019_2.Project
object Project : Project({
description = "Vim engine for JetBrains IDEs"
subProjects(Releases, GitHub)
subProject(Releases)
subProject(GitHub)
// VCS roots
vcsRoot(GitHubPullRequest)
@@ -30,6 +32,7 @@ object Project : Project({
buildType(PropertyBased)
buildType(LongRunning)
buildType(RandomOrderTests)
buildType(Nvim)
buildType(PluginVerifier)

View File

@@ -0,0 +1,44 @@
package _Self.buildTypes
import _Self.IdeaVimBuildType
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
object RandomOrderTests : IdeaVimBuildType({
name = "Random order tests"
description = "Running tests with random order on each run. This way we can catch order-dependent bugs."
params {
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
}
vcs {
root(DslContext.settingsRoot)
branchFilter = "+:<default>"
checkoutMode = CheckoutMode.AUTO
}
steps {
gradle {
tasks = """
clean test
-x :tests:property-tests:test
-x :tests:long-running-tests:test
-Djunit.jupiter.execution.order.random.seed=default
-Djunit.jupiter.testmethod.order.default=random
""".trimIndent().replace("\n", " ")
buildFile = ""
enableStacktrace = true
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
}
}
triggers {
vcs {
branchFilter = "+:<default>"
}
}
})

View File

@@ -638,6 +638,22 @@ Contributors:
[![icon][github]](https://github.com/magidc)
&nbsp;
magidc
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
Previous contributors:

View File

@@ -130,8 +130,13 @@ Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
```
3. Don't forget to test your functionality with line start, line end, file start, file end, empty line, multiple
carets, dollar motion, etc.
3. Don't forget to test your functionality with various corner cases:
- **Position-based**: line start, line end, file start, file end, empty line, single character line
- **Content-based**: whitespace-only lines, lines with trailing spaces, mixed tabs and spaces, Unicode characters, multi-byte characters (e.g., emoji, CJK)
- **Selection-based**: multiple carets, visual mode (character/line/block), empty selection
- **Motion-based**: dollar motion, count with motion (e.g., `3w`, `5j`), zero-width motions
- **Buffer state**: empty file, single line file, very long lines, read-only files
- **Boundaries**: word boundaries with punctuation, sentence/paragraph boundaries, matching brackets at extremes
##### Neovim
IdeaVim has an integration with neovim in tests. Tests that are performed with `doTest` also executed in

View File

@@ -180,12 +180,6 @@ Unless otherwise stated, these options do not have abbreviations.
value is off. The equivalent processing for paste is controlled by the
"ideaput" value to the 'clipboard' option.
'ideaglobalmode' boolean (default off)
global
This option will cause IdeaVim to share a single mode across all open
windows. In other words, entering Insert mode in one window will
enable Insert mode in all windows.
'ideajoin' boolean (default off)
global or local to buffer
When enabled, join commands will be handled by the IDE's "smart join"

View File

@@ -11,7 +11,7 @@ package com.maddyhome.idea.vim.extension.hints
import com.intellij.ui.treeStructure.Tree
import java.awt.Component
import java.awt.Point
import java.util.*
import java.util.WeakHashMap
import javax.accessibility.Accessible
import javax.swing.SwingUtilities
@@ -44,9 +44,9 @@ internal sealed class HintGenerator {
val hintIterator = alphabet.permutations(length).map { it.joinToString("") }.iterator()
targets.forEach { target ->
target.hint = if (preserve) {
previousHints[target.component] ?: hintIterator.firstOrNull {
previousHints[target.component] ?: hintIterator.firstOrNull { candidateHint ->
// Check if the hint is not already used by previous targets
!previousHints.values.any { hint -> hint.startsWith(it) || it.startsWith(hint) }
!previousHints.values.any { existingHint -> existingHint.startsWith(candidateHint) || candidateHint.startsWith(existingHint) }
} ?: return generate(targets, false) // do not preserve previous hints if failed
} else {
hintIterator.next()

View File

@@ -81,8 +81,12 @@ internal object IdeaSelectionControl {
return@singleTask
}
// The editor changed the selection, but we want to keep the current Vim mode. This is the case if we're editing
// a template and either 'idearefactormode' is set to `keep`, or the current Vim mode already has a selection.
// (i.e., if the user explicitly enters Visual or Select and then moves to the next template variable, don't
// switch to Select or Visual - keep the current Vim selection mode)
if (dontChangeMode(editor)) {
IdeaRefactorModeHelper.correctSelection(editor)
IdeaRefactorModeHelper.correctEditorSelection(editor)
logger.trace { "Selection corrected for refactoring" }
return@singleTask
}
@@ -146,8 +150,13 @@ internal object IdeaSelectionControl {
}
private fun chooseNonSelectionMode(editor: Editor): Mode {
val templateActive = editor.isTemplateActive()
if (templateActive && editor.vim.mode.inNormalMode || editor.inInsertMode) {
// If we're in an active template and the editor has just removed a selection without adding a new one, we're in a
// variable with nothing to select. When 'idearefactormode' is "select", enter Insert mode. Otherwise, stay in
// Normal.
// Note that when 'idearefactormode' is "visual", we enter Normal mode for an empty variable. While it might seem
// natural to want to insert text here, we could also paste a register, dot-repeat an insertion or other Normal
// commands. Normal is Visual without the selection.
if ((editor.isTemplateActive() && editor.vim.mode.inNormalMode && editor.vim.isIdeaRefactorModeSelect) || editor.inInsertMode) {
return Mode.INSERT
}
return Mode.NORMAL()

View File

@@ -16,6 +16,7 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction
@@ -37,18 +38,24 @@ import com.intellij.openapi.util.TextRange
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.action.VimShortcutKeyAction
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.group.NotificationService
import com.maddyhome.idea.vim.group.RegisterGroup
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
import com.maddyhome.idea.vim.helper.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.hasVisualSelection
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.initInjector
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inNormalMode
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@@ -210,7 +217,44 @@ internal object IdeaSpecifics {
}
}
//region Enter insert mode for surround templates without selection
//region Handle mode and selection for Live Templates and refactorings
/**
* Listen to template notifications to provide additional handling of templates, selection and Vim mode
*
* Most of the handling of templates is done by [IdeaSelectionControl]. When moving between editable segments of a
* template, the editor will remove the current selection and add a selection for the text of the new segment, if any.
* [IdeaSelectionControl] will be notified and handle Vim mode based on the `'idearefactormode'` option. This will
* switch to Select mode for each variable by default (or Insert if there's no text) or Visual/Normal mode when the
* option is set to "visual".
*
* Select mode makes IdeaVim behaviour a little more like a traditional editor. Inserting a Live Template is typically
* done in Insert mode, so moving to the next editable segment always switches to Select mode, ready to continue
* typing. At the end of the template, the caret is still in Insert mode, still ready to continue typing.
*
* The exception is for an "inline" template. This is an editable segment placed on top of existing text and is
* typically used to rename a symbol. This is usually a refactoring started with an explicit action, and switching to
* Select mode means the user is ready to start typing. However, accepting the change should switch back to Normal, as
* the editing/refactoring is complete. This also helps when the refactoring shows a progress dialog, since Escape to
* switch back to Normal can cancel the refactoring.
*
* Again, this is like a traditional editor, and still only changes Vim mode based on an explicit action.
*
* This class handles the following edge cases that [IdeaSelectionControl] cannot:
* * When `'idearefactormode'` is "keep" it will maintain the current Vim mode, by removing the current selection
* (so [IdeaSelectionControl] does nothing). It also ensures that the current Vim selection mode matches the actual
* selection (i.e., character wise vs line wise).
* * When the editor is moving to the next segment but there is no current selection and the next segment is empty,
* there will be no change in selection and [IdeaSelectionControl] will not be notified. This class will switch to
* Insert mode when `'idearefactormode'` is set to "select" and Normal when it's "visual".
* * A special case of the above scenario is moving to the end of the template, which always has no selection. If
* there is no current selection [IdeaSelectionControl] is not called and the mode is not updated, so we stay in
* whatever mode the user had last - Insert, Normal, whatever. When there is a selection, [IdeaSelectionControl]
* will be called, but since there is no template active anymore, it would set the mode to Normal. This class will
* switch to Insert when `'idearefactormode'` is "select" and Normal for "visual". It does nothing for "keep".
* * If the template is an "inline" template, it is typically a rename refactoring on existing text.
* When ending the template and `'idearefactormode'` is "select", the above would leave is in Insert mode. This
* class will switch to Normal for inline templates, for both "select" and "visual". It does nothing for "keep".
*/
class VimTemplateManagerListener : TemplateManagerListener {
override fun templateStarted(state: TemplateState) {
if (VimPlugin.isNotEnabled()) return
@@ -223,27 +267,65 @@ internal object IdeaSpecifics {
oldIndex: Int,
newIndex: Int,
) {
if (templateState.editor.vim.isIdeaRefactorModeKeep) {
IdeaRefactorModeHelper.correctSelection(templateState.editor)
fun VimEditor.exitMode() = when (this.mode) {
is Mode.SELECT -> this.exitSelectMode(adjustCaretPosition = false)
is Mode.VISUAL -> this.exitVisualMode()
is Mode.INSERT -> this.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(this))
else -> Unit
}
fun Template?.myIsInline() = this is TemplateImpl && this.isInline
val vimEditor = editor.vim
// This function is called when moving between variables. It is called with oldIndex == -1 when moving to the
// first variable, and newIndex == -1 just before ending, when moving to the end of the template text, or to
// $END$ (which is treated as a segment, but not a variable). If there are no variables, it is called with
// oldIndex == newIndex == -1.
if (vimEditor.isIdeaRefactorModeKeep) {
IdeaRefactorModeHelper.correctEditorSelection(templateState.editor)
}
else {
// The editor places the caret at the exclusive end of the variable. For Visual, unless we've enabled
// exclusive selection, move it to the inclusive end.
// Note that "keep" does this as part of IdeaRefactorModeHelper
if (editor.selectionModel.hasSelection()
&& !editor.vim.isIdeaRefactorModeSelect
&& templateState.currentVariableRange?.endOffset == editor.caretModel.offset
&& !injector.options(vimEditor).selection.contains("exclusive")
) {
vimEditor.primaryCaret()
.moveToInlayAwareOffset((editor.selectionModel.selectionEnd - 1).coerceAtLeast(editor.selectionModel.selectionStart))
}
if (newIndex == -1 && template.myIsInline() && vimEditor.isIdeaRefactorModeSelect) {
// Rename refactoring has just completed with 'idearefactormode' in "select". Return to Normal instead of
// our default behaviour of switching to Insert
if (vimEditor.mode !is Mode.NORMAL) {
vimEditor.exitMode()
vimEditor.mode = Mode.NORMAL()
}
} else {
// IdeaSelectionControl will not be called if we're moving to a new variable with no change in selection.
// And if we're moving to the end of the template, the change in selection will reset us to Normal because
// IdeaSelectionControl will be called when the template is no longer active.
if ((!editor.selectionModel.hasSelection() && !vimEditor.mode.hasVisualSelection) || newIndex == -1) {
if (vimEditor.isIdeaRefactorModeSelect) {
if (vimEditor.mode !is Mode.INSERT) {
vimEditor.exitMode()
injector.application.runReadAction {
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
VimPlugin.getChange().insertBeforeCaret(editor.vim, context)
}
}
} else {
vimEditor.mode = Mode.NORMAL()
}
}
}
}
}
})
if (state.editor.vim.isIdeaRefactorModeKeep) {
IdeaRefactorModeHelper.correctSelection(editor)
} else {
if (!editor.selectionModel.hasSelection()) {
// Enable insert mode if there is no selection in template
// Template with selection is handled by [com.maddyhome.idea.vim.group.visual.VisualMotionGroup.controlNonVimSelectionChange]
if (editor.vim.inNormalMode) {
VimPlugin.getChange().insertBeforeCaret(
editor.vim,
injector.executionContextManager.getEditorExecutionContext(editor.vim),
)
KeyHandler.getInstance().reset(editor.vim)
}
}
}
}
}
//endregion

View File

@@ -34,7 +34,6 @@ import com.maddyhome.idea.vim.api.VimEditorBase
import com.maddyhome.idea.vim.api.VimFoldRegion
import com.maddyhome.idea.vim.api.VimIndentConfig
import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVirtualFile
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.injector
@@ -298,18 +297,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
editor.document.deleteString(range.startOffset, range.endOffset)
}
override fun getSelectionModel(): VimSelectionModel {
return object : VimSelectionModel {
private val sm = editor.selectionModel
override val selectionStart = sm.selectionStart
override val selectionEnd = sm.selectionEnd
override fun hasSelection(): Boolean {
return sm.hasSelection()
}
}
}
override fun getScrollingModel(): VimScrollingModel {
return object : VimScrollingModel {
private val sm = editor.scrollingModel as ScrollingModelEx

View File

@@ -63,7 +63,8 @@ internal class IjVimMessages : VimMessagesBase() {
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
// things happen.
override fun clearStatusBarMessage() {
if (message.isNullOrEmpty()) return
val currentMessage = message
if (currentMessage.isNullOrEmpty()) return
// Don't clear the status bar message if we've only just set it
if (!allowClearStatusBarMessage) return
@@ -71,7 +72,7 @@ internal class IjVimMessages : VimMessagesBase() {
ProjectManager.getInstance().openProjects.forEach { project ->
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
// Only clear the status bar if it's showing our last message
if (statusBar.info?.contains(message.toString()) == true) {
if (statusBar.info?.contains(currentMessage) == true) {
statusBar.info = ""
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2023 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -14,7 +14,7 @@ import com.intellij.util.ui.JBInsets
import java.awt.Component
import java.awt.Insets
internal class ExPanelBorder internal constructor() : SideBorder(JBColor.border(), TOP) {
internal class ExPanelBorder : SideBorder(JBColor.border(), TOP) {
override fun getBorderInsets(component: Component?): Insets {
return JBInsets(getThickness() + 2, 0, 2, 2)

View File

@@ -20,7 +20,6 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.helper.VimLockLabel
import com.maddyhome.idea.vim.helper.hasBlockOrUnderscoreCaret
import com.maddyhome.idea.vim.helper.hasVisualSelection
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
import com.maddyhome.idea.vim.newapi.ijOptions
@@ -87,36 +86,50 @@ internal object IdeaRefactorModeHelper {
@VimLockLabel.RequiresReadLock
@RequiresReadLock
fun calculateCorrections(editor: Editor): List<Action> {
fun calculateCorrectionsToSyncEditorToMode(editor: Editor): List<Action> {
val corrections = mutableListOf<Action>()
val mode = editor.vim.mode
// If the current Vim mode doesn't have a selection, remove the editor's selection
if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
corrections.add(Action.RemoveSelection)
}
// If the current mode does have a selection, make sure it matches the selection type of the editor
if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim)
if (mode.selectionType != selectionType) {
val newMode = when (mode) {
is Mode.SELECT -> mode.copy(selectionType)
is Mode.VISUAL -> mode.copy(selectionType)
is Mode.SELECT -> mode.copy(selectionType = selectionType)
is Mode.VISUAL -> mode.copy(selectionType = selectionType)
else -> error("IdeaVim should be either in visual or select modes")
}
corrections.add(Action.SetMode(newMode))
}
}
if (editor.hasBlockOrUnderscoreCaret()) {
// IntelliJ places the caret on the exclusive end of the current variable. I.e. *after* the end of the variable.
// This makes sense when selecting the current variable, and when the editor is using a bar caret - the selection is
// naturally exclusive (and IdeaVim treats Select mode as having exclusive selection).
// But we don't have a selection, so it's weird to be placed at the end of a selection that no longer exists. Move
// the caret to the start of the (missing) selection instead.
if (editor.vim.isIdeaRefactorModeKeep) {
TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange ->
if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) {
corrections.add(Action.MoveToOffset(editor.caretModel.offset - 1))
if (!segmentRange.isEmpty && segmentRange.startOffset != editor.caretModel.offset) {
corrections.add(Action.MoveToOffset(segmentRange.startOffset))
}
}
}
return corrections
}
fun correctSelection(editor: Editor) {
val corrections = injector.application.runReadAction { calculateCorrections(editor) }
applyCorrections(corrections, editor)
/**
* Correct the editor's selection to match the current Vim mode
*/
fun correctEditorSelection(editor: Editor) {
injector.application.runReadAction {
val corrections = calculateCorrectionsToSyncEditorToMode(editor)
applyCorrections(corrections, editor)
}
}
}

View File

@@ -72,7 +72,6 @@ nowrap
nowrapscan
ideacopypreprocess
ideaglobalmode
ideajoin
ideamarks
idearefactormode

View File

@@ -50,4 +50,10 @@ class DeletePreviousWordActionTest : VimExTestCase() {
typeText(":set keymodel=continueselect,stopselect<C-W>")
assertExText("set ")
}
@Test
fun `test delete at beginning does nothing`() {
typeText(":<C-W>")
assertExText("")
}
}

View File

@@ -721,6 +721,17 @@ class MapCommandTest : VimTestCase() {
assertState("zzz\n")
}
// VIM-650 |mapleader|
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "Bad replace of term codes")
@Test
fun testMapLeaderToCtrlSpace() {
configureByText("\n")
enterCommand("let mapleader = \"\\<C-SPACE>\"")
enterCommand("nmap <Leader>z izzz<Esc>")
typeText("<C-SPACE>z")
assertState("zzz\n")
}
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "bad replace term codes")
@Test
fun testAmbiguousMapping() {

View File

@@ -103,4 +103,22 @@ class PrintLineNumberTest : VimTestCase() {
enterCommand("2=p#")
assertStatusLineMessageContains("2 \t\t\tconsectetur adipiscing elit")
}
@Test
fun `test out of bounds range is clamped to last line`() {
configureByLines(10, "Lorem ipsum dolor sit amet")
enterCommand("\$+100=")
assertStatusLineMessageContains("10")
}
@Test
fun `test out of bounds range with flags is clamped to last line`() {
configureByText("""
|Lorem ipsum dolor sit amet
|consectetur adipiscing elit
|Maecenas efficitur nec odio vel malesuada
""".trimMargin())
enterCommand("\$+100=p")
assertStatusLineMessageContains("3 Maecenas efficitur nec odio vel malesuada")
}
}

View File

@@ -144,6 +144,15 @@ class IndexedExpressionTest : VimTestCase("\n") {
assertPluginError(false)
}
@Test
fun `test indexed String expression at exactly length boundary returns empty String`() {
// This test validates the off-by-one fix in IndexedExpression.kt line 56
// With old code (idx > text.length), accessing index == length would cause IndexOutOfBoundsException
// With new code (idx >= text.length), it correctly returns empty string
assertCommandOutput("echo string('hello'[5])", "''")
assertPluginError(false)
}
@Test
fun `test indexed String expression with negative index returns empty String`() {
// Surprisingly not the same as List

View File

@@ -30,6 +30,51 @@ class LenFunctionTest : VimTestCase() {
assertCommandOutput("echo len(12 . 4)", "3")
}
@Test
fun `test len with empty string`() {
assertCommandOutput("echo len('')", "0")
assertCommandOutput("echo len(\"\")", "0")
}
@Test
fun `test len with empty list`() {
assertCommandOutput("echo len([])", "0")
}
@Test
fun `test len with zero number`() {
assertCommandOutput("echo len(0)", "1")
}
@Test
fun `test len with negative numbers`() {
assertCommandOutput("echo len(-123)", "4")
assertCommandOutput("echo len(-1)", "2")
}
@Test
fun `test len with large number`() {
assertCommandOutput("echo len(9999999)", "7")
}
@Test
fun `test len with multi-element list`() {
assertCommandOutput("echo len([1, 2, 3, 4, 5])", "5")
}
@Test
fun `test len with multi-element dictionary`() {
assertCommandOutput("echo len(#{a: 1, b: 2, c: 3})", "3")
}
@Test
fun `test len with string containing special characters`() {
// Single-quoted strings in Vim don't interpret escape sequences (except '')
assertCommandOutput("echo len('hello\\nworld')", "12")
// Double-quoted strings do interpret escape sequences
assertCommandOutput("echo len(\"hello\\nworld\")", "11")
}
@Test
fun `test len with float causes errors`() {
enterCommand("echo len(4.2)")

View File

@@ -9,6 +9,7 @@
package org.jetbrains.plugins.ideavim.ex.implementation.functions.listFunctions
import com.maddyhome.idea.vim.api.injector
import kotlin.test.assertTrue
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -59,17 +60,18 @@ class RangeFunctionTest : VimTestCase() {
@Test
fun `test range with zero stride throws error`() {
enterCommand("echo range(1, 5, 0)")
kotlin.test.assertTrue(injector.messages.isError())
assertTrue(injector.messages.isError())
}
@Test
fun `test range with start past end throws error`() {
enterCommand("echo range(2, 0)")
kotlin.test.assertTrue(injector.messages.isError())
assertTrue(injector.messages.isError())
}
@Test
fun `test range negative with start past end throws error`() {
enterCommand("echo range(-2, 0, -1)")
assertTrue(injector.messages.isError())
}
}

View File

@@ -65,11 +65,6 @@ class TrimFunctionTest : VimTestCase() {
assertCommandOutput("echo trim('\t\nhello\n\t')", "hello")
}
@Test
fun `test trim with carriage return and vertical tab`() {
assertCommandOutput("echo trim('\r\t\t\r RESERVE \t\n\u000B\u00A0') .. '_TAIL'", "RESERVE_TAIL")
}
@Test
fun `test trim with custom mask does not affect middle`() {
assertCommandOutput("echo trim('rm<Xrm<>X>rrm', 'rm<>')", "Xrm<>X")

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.state.mode.selectionType
import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase
import org.jetbrains.plugins.ideavim.assertDoesntChange
import org.jetbrains.plugins.ideavim.assertModeDoesNotChange
import org.jetbrains.plugins.ideavim.rangeOf
import org.jetbrains.plugins.ideavim.waitAndAssert
import org.jetbrains.plugins.ideavim.waitAndAssertMode
@@ -89,7 +89,7 @@ class NonVimVisualChangeTest : VimTestCase() {
fixture.editor.selectionModel.removeSelection()
}
assertDoesntChange { fixture.editor.vim.mode == Mode.INSERT }
assertModeDoesNotChange(fixture.editor, Mode.INSERT)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)

View File

@@ -10,6 +10,7 @@ package org.jetbrains.plugins.ideavim
import com.intellij.ide.IdeEventQueue
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.util.containers.toArray
import com.maddyhome.idea.vim.api.injector
@@ -17,6 +18,8 @@ import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import org.junit.jupiter.params.provider.Arguments
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
/**
@@ -56,17 +59,9 @@ annotation class VimBehaviorDiffers(
val shouldBeFixed: Boolean = true,
)
inline fun waitAndAssert(timeInMillis: Int = 1000, crossinline condition: () -> Boolean) {
ApplicationManager.getApplication().invokeAndWait {
val end = System.currentTimeMillis() + timeInMillis
while (end > System.currentTimeMillis()) {
Thread.sleep(10)
IdeEventQueue.getInstance().flushQueue()
if (condition()) return@invokeAndWait
}
fail()
}
}
// The selection is updated after 'visualdelay' milliseconds. Add an adjustment when we wait for it to be completed.
// Since we wait for it on the main thread (to avoid reading in-progress state), we can get away with a short adjustment
private const val visualDelayAdjustment = 200
fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) {
assertDoesntChange(timeInMillis - precision) { !condition() }
@@ -111,15 +106,58 @@ fun <T> product(vararg elements: List<T>): List<List<T>> {
return res
}
private inline fun invokeAndWaitUntil(timeout: Int = 1_000, crossinline condition: () -> Boolean): Boolean {
// Run the check on the main thread to serialise access for our condition
var result = false
ApplicationManager.getApplication().invokeAndWait {
val end = System.currentTimeMillis() + timeout
while (end > System.currentTimeMillis()) {
if (condition()) {
result = true
return@invokeAndWait
}
Thread.sleep(10)
IdeEventQueue.getInstance().flushQueue()
}
}
return result
}
fun waitAndAssert(timeInMillis: Int = 1000, condition: () -> Boolean) {
assertTrue(invokeAndWaitUntil(timeInMillis, condition), "Condition not met within timeout")
}
fun waitAndAssertMode(
fixture: CodeInsightTestFixture,
mode: Mode,
timeInMillis: Int? = null,
) {
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + 1000)
waitAndAssert(timeout) { fixture.editor.vim.mode == mode }
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment)
val currentMode = fixture.editor.vim.mode
waitAndAssert(timeout) {
if (fixture.editor.vim.mode == currentMode && fixture.editor.vim.mode != mode) return@waitAndAssert false
assertEquals(mode, fixture.editor.vim.mode)
return@waitAndAssert true
}
}
fun assertModeDoesNotChange(editor: Editor, expectedMode: Mode, timeInMillis: Int? = null) {
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment)
val currentMode = editor.vim.mode
assertEquals(expectedMode, currentMode, "Initial mode is not as expected")
invokeAndWaitUntil(timeout) { editor.vim.mode != currentMode }
assertEquals(currentMode, editor.vim.mode, "Mode should not change")
}
// Note that the selection might not update, but we wait long enough to give it chance to change
fun waitUntilSelectionUpdated(editor: Editor) {
val timeout = injector.globalIjOptions().visualdelay + visualDelayAdjustment
val currentMode = editor.vim.mode
invokeAndWaitUntil(timeout) { editor.vim.mode != currentMode }
}
// This waits on the current thread, which in tests isn't the main thread. If we're waiting on a callback on the main
// thread, we might see in-progress changes rather than the final state.
fun waitUntil(timeout: Int = 10_000, condition: () -> Boolean): Boolean {
val timeEnd = System.currentTimeMillis() + timeout
while (System.currentTimeMillis() < timeEnd) {

View File

@@ -139,19 +139,25 @@ and some text after""",
ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().runWriteAction {
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, true)
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
?: error("Expected fold region at line 0 for Javadoc comment")
assertEquals(foldRegion.isExpanded, true)
}
}
typeText(injector.parser.parseKeys("za"))
ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().runWriteAction {
assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, false)
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
?: error("Expected fold region at line 0 for Javadoc comment")
assertEquals(foldRegion.isExpanded, false)
}
}
typeText(injector.parser.parseKeys("za"))
ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().runWriteAction {
assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, true)
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
?: error("Expected fold region at line 0 for Javadoc comment")
assertEquals(foldRegion.isExpanded, true)
}
}
}
@@ -174,7 +180,9 @@ and some text after""",
ApplicationManager.getApplication().invokeAndWait {
fixture.editor.foldingModel.runBatchFoldingOperation {
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded = false
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
?: error("Expected fold region at line 0 for Javadoc comment")
foldRegion.isExpanded = false
}
}

View File

@@ -34,7 +34,9 @@ bar
ApplicationManager.getApplication().invokeAndWait {
fixture.editor.foldingModel.runBatchFoldingOperation {
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded = false
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
?: error("Expected fold region at line 0 for Javadoc comment")
foldRegion.isExpanded = false
}
}

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
/**
* @author vlan

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
@CommandOrMotion(keys = ["s"], modes = [Mode.NORMAL])
class ChangeCharactersAction : ChangeInInsertSequenceAction() {

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
@CommandOrMotion(keys = ["C"], modes = [Mode.NORMAL])
class ChangeEndOfLineAction : ChangeInInsertSequenceAction() {

View File

@@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
@CommandOrMotion(keys = ["S"], modes = [Mode.NORMAL])
class ChangeLineAction : ChangeInInsertSequenceAction() {

View File

@@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.DuplicableOperatorAction
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
@CommandOrMotion(keys = ["c"], modes = [Mode.NORMAL])
class ChangeMotionAction : ChangeInInsertSequenceAction(), DuplicableOperatorAction {

View File

@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.mode.SelectionType
import java.util.*
import java.util.EnumSet
/**
* @author vlan

View File

@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.mode.SelectionType
import java.util.*
import java.util.EnumSet
/**
* @author vlan

View File

@@ -21,7 +21,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.endOffsetInclusive
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
@CommandOrMotion(keys = ["!"], modes = [Mode.VISUAL])
class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterCommand {

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
import java.util.EnumSet
/**
* @author vlan

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2024 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at

View File

@@ -442,9 +442,11 @@ abstract class VimChangeGroupBase : VimChangeGroup {
*/
override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) {
val state = injector.vimState
for (caret in editor.nativeCarets()) {
caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset)
injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
injector.application.runReadAction {
for (caret in editor.nativeCarets()) {
caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset)
injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
}
}
val cmd = state.executingCommand
if (cmd != null && state.isDotRepeatInProgress) {
@@ -470,7 +472,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val myChangeListener = VimChangesListener()
vimDocumentListener = myChangeListener
vimDocument!!.addChangeListener(myChangeListener)
oldOffset = editor.currentCaret().offset
injector.application.runReadAction {
oldOffset = editor.currentCaret().offset
}
editor.insertMode = mode == Mode.INSERT
editor.mode = mode
}

View File

@@ -162,7 +162,6 @@ interface VimEditor {
return getText(start, end)
}
fun getSelectionModel(): VimSelectionModel
fun getScrollingModel(): VimScrollingModel
fun removeCaret(caret: VimCaret)

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.api
interface VimSelectionModel {
val selectionStart: Int
val selectionEnd: Int
fun hasSelection(): Boolean
}

View File

@@ -27,26 +27,30 @@ abstract class VimStringParserBase : VimStringParser {
override fun toPrintableString(keys: List<KeyStroke>): String {
val builder = StringBuilder()
for (key in keys) {
val keyAsChar = keyStrokeToChar(key)
builder.append(keyAsChar)
val keyAsString = keyStrokeToString(key)
builder.append(keyAsString)
}
return builder.toString()
}
private fun keyStrokeToChar(key: KeyStroke): Char {
private fun keyStrokeToString(key: KeyStroke): String {
if (key.keyChar != KeyEvent.CHAR_UNDEFINED) {
return key.keyChar
return key.keyChar.toString()
} else if (key.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
return if (key.keyCode == 'J'.code) {
// 'J' is a special case, keycode 10 is \n char
0.toChar()
return if (isControlCharacterKeyCode(key.keyCode)) {
if (key.keyCode == 'J'.code) {
// 'J' is a special case, keycode 10 is \n char
0.toChar().toString()
} else {
(key.keyCode - 'A'.code + 1).toChar().toString()
}
} else {
(key.keyCode - 'A'.code + 1).toChar()
"^" + key.keyCode.toChar()
}
} else if (key.keyChar == KeyEvent.CHAR_UNDEFINED && key.keyCode == KeyEvent.VK_ENTER) {
return '\u000D'
return "\u000D"
}
return key.keyCode.toChar()
return key.keyCode.toChar().toString()
}
override fun toKeyNotation(keyStrokes: List<KeyStroke>): String {
@@ -178,7 +182,18 @@ abstract class VimStringParserBase : VimStringParser {
private fun getMapLeader(): List<KeyStroke> {
val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader")
return if (mapLeader is VimString) {
stringToKeys(mapLeader.value)
val v: String = mapLeader.value
// Minimum length is 4 for the shortest special key format: \<X> (e.g., "\<a>")
if (v.startsWith("\\<") && v.length >= 4 && v[v.length - 1] == '>') {
val specialKey = parseSpecialKey(v.substring(2, v.length - 1), 0)
if (specialKey != null) {
listOf(specialKey)
} else {
stringToKeys(mapLeader.value)
}
} else {
stringToKeys(mapLeader.value)
}
} else {
stringToKeys("\\")
}
@@ -207,6 +222,11 @@ abstract class VimStringParserBase : VimStringParser {
return c < '\u0020'
}
private fun isControlCharacterKeyCode(code: Int): Boolean {
// Ctrl-(A..Z [\]^_) are ASCII control characters
return code >= 'A'.code && code <= '_'.code
}
@Suppress("SpellCheckingInspection")
private fun getVimKeyValue(c: Int): @NonNls String? {
return when (c) {
@@ -430,17 +450,25 @@ abstract class VimStringParserBase : VimStringParser {
val specialKey = parseSpecialKey(specialKeyBuilder.toString(), 0)
if (specialKey != null) {
var keyCode = specialKey.keyCode
var useKeyCode = true
if (specialKey.keyCode == 0) {
keyCode = specialKey.keyChar.code
} else if (specialKey.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
keyCode = if (specialKey.keyCode == 'J'.code) {
// 'J' is a special case, keycode 10 is \n char
0
if (isControlCharacterKeyCode(specialKey.keyCode)) {
keyCode = if (specialKey.keyCode == 'J'.code) {
// 'J' is a special case, keycode 10 is \n char
0
} else {
specialKey.keyCode - 'A'.code + 1
}
} else {
specialKey.keyCode - 'A'.code + 1
useKeyCode = false
result.append("\\<${specialKeyBuilder}>")
}
}
result.append(keyCode.toChar())
if (useKeyCode) {
result.append(keyCode.toChar())
}
} else {
result.append("<").append(specialKeyBuilder).append(">")
}

View File

@@ -17,8 +17,10 @@ class VimEditorReplaceMask {
fun recordChangeAtCaret(editor: VimEditor) {
for (caret in editor.carets()) {
val offset = caret.offset
val marker = editor.createLiveMarker(offset, offset)
changedChars[marker] = editor.charAt(offset)
if (offset < editor.fileSize()) {
val marker = editor.createLiveMarker(offset, offset)
changedChars[marker] = editor.charAt(offset)
}
}
}

View File

@@ -40,7 +40,13 @@ object EngineStringHelper {
if (c == KeyEvent.CHAR_UNDEFINED && key.modifiers == 0) {
c = key.keyCode.toChar()
} else if (c == KeyEvent.CHAR_UNDEFINED && key.modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
c = (key.keyCode - 'A'.code + 1).toChar()
val code = key.keyCode
if (code >= 'A'.code && code <= '_'.code) {
// Ctrl-(A..Z [\]^_) are ASCII control characters
c = (code - 'A'.code + 1).toChar()
} else {
return "^" + code.toChar()
}
}
return toPrintableCharacter(c)
}

View File

@@ -20,9 +20,8 @@ import com.maddyhome.idea.vim.api.VimEditorBase
import com.maddyhome.idea.vim.api.VimFoldRegion
import com.maddyhome.idea.vim.api.VimIndentConfig
import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.VimVirtualFile
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
@@ -696,10 +695,6 @@ class VimRegex(pattern: String) {
override fun deleteString(range: TextRange) {}
override fun getSelectionModel(): VimSelectionModel {
TODO("Not yet implemented")
}
override fun getScrollingModel(): VimScrollingModel {
TODO("Not yet implemented")
}

View File

@@ -23,7 +23,9 @@ internal class EndOfLineMatcher : Matcher {
isCaseInsensitive: Boolean,
possibleCursors: MutableList<VimCaret>,
): MatcherResult {
return if (index == editor.text().length || editor.text()[index] == '\n') MatcherResult.Success(0)
val length = editor.text().length
if (index > length) return MatcherResult.Failure
return if (index == length || editor.text()[index] == '\n') MatcherResult.Success(0)
else MatcherResult.Failure
}

View File

@@ -23,6 +23,7 @@ internal class StartOfLineMatcher : Matcher {
isCaseInsensitive: Boolean,
possibleCursors: MutableList<VimCaret>,
): MatcherResult {
if (index < 0 || index > editor.text().length) return MatcherResult.Failure
return if (index == 0 || editor.text()[index - 1] == '\n') MatcherResult.Success(0)
else MatcherResult.Failure
}

View File

@@ -38,7 +38,7 @@ data class PrintLineNumberCommand(val range: Range, val modifier: CommandModifie
throw exExceptionMessage("E488", argument)
}
val line1 = range.getLineRange(editor, editor.currentCaret()).endLine1
val line1 = range.getLineRange(editor, editor.currentCaret()).endLine1.coerceAtMost(editor.lineCount())
// `l` means output the line like `:list` - show unprintable chars, and include `^` and `$`
// `#` means output the line with the line number

View File

@@ -58,10 +58,9 @@ data class SortCommand(val range: Range, val modifier: CommandModifier, val argu
// If we don't have a range, we either have "sort", a selection, or a block
if (range.size == 1) {
// If we have a selection.
val selectionModel = editor.getSelectionModel()
return if (selectionModel.hasSelection()) {
val start = selectionModel.selectionStart
val end = selectionModel.selectionEnd
return if (caret.hasSelection()) {
val start = caret.selectionStart
val end = caret.selectionEnd
val startLine = editor.offsetToBufferPosition(start).line
val endLine = editor.offsetToBufferPosition(end).line

View File

@@ -52,8 +52,8 @@ data class IndexedExpression(val index: Expression, val expression: Expression)
else -> {
// Try to convert the expression to String, then index it
val text = expressionValue.toVimString().value
val idx = index.evaluate(editor, context, vimContext).toVimNumber().value
if (idx < 0 || idx > text.length) {
val idx = indexValue.toVimNumber().value
if (idx < 0 || idx >= text.length) {
return VimString.EMPTY
}
return VimString(text[idx].toString())
@@ -121,10 +121,10 @@ data class IndexedExpression(val index: Expression, val expression: Expression)
vimContext: VimLContext,
assignmentTextForErrors: String
) {
val index = index.evaluate(editor, context, vimContext).toVimNumber().value
val idx = if (index < 0) index + list.values.size else index
val indexNum = index.evaluate(editor, context, vimContext).toVimNumber().value
val idx = if (indexNum < 0) indexNum + list.values.size else indexNum
if (idx < 0 || idx >= list.values.size) {
throw exExceptionMessage("E684", index)
throw exExceptionMessage("E684", indexNum)
}
if (list.values[idx].isLocked) {
throw exExceptionMessage("E741", assignmentTextForErrors)