mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-11-29 16:38:24 +01:00
Compare commits
46 Commits
5aaa84744f
...
226f5e27b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
226f5e27b1 | ||
|
|
cc3c132ccb | ||
|
|
13fb2d53f1 | ||
|
|
c803a1cf24 | ||
|
|
1d60e47bb8 | ||
|
|
664c433ee9 | ||
|
|
67a44b4d25 | ||
|
|
b4915d08cd | ||
|
|
6b7b18d947 | ||
|
|
54cc89f641 | ||
|
|
1048d06586 | ||
|
|
7f0ab93ea7 | ||
|
|
a0f923512a | ||
|
|
d60af9afa1 | ||
|
|
1ef919f73a | ||
|
|
f6947d73f6 | ||
|
|
54c12470f3 | ||
|
|
8aa8725a8d | ||
|
|
1e1bbbac2a | ||
|
|
0bb3524118 | ||
|
|
12596540e9 | ||
|
|
3eadff6401 | ||
|
|
52f4c24b1e | ||
|
|
7de2ea0e0b | ||
|
|
a9fc2c58a6 | ||
|
|
1aa586484f | ||
|
|
aa24d53b18 | ||
|
|
c396d98022 | ||
|
|
db767f534f | ||
|
|
d955b1b85c | ||
|
|
0fdfc04068 | ||
|
|
244d13a3cc | ||
|
|
4a2600738f | ||
|
|
82ed26a701 | ||
|
|
cf5dce3ce7 | ||
|
|
c68e216c87 | ||
|
|
c12f4a75ac | ||
|
|
9a069e5ef8 | ||
|
|
27f2e56635 | ||
|
|
7a0bdb8f3d | ||
|
|
1d9bb6ec70 | ||
|
|
bb62dcdc15 | ||
|
|
fdcb954e31 | ||
|
|
747e4053ba | ||
|
|
448c19af47 | ||
|
|
f0edc797dc |
206
.claude/maintenance-instructions.md
Normal file
206
.claude/maintenance-instructions.md
Normal 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.
|
||||||
23
.github/workflows/claude-code-review.yml
vendored
23
.github/workflows/claude-code-review.yml
vendored
@@ -3,26 +3,13 @@ name: Claude Code Review
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
# Optional: Only run on specific file changes
|
|
||||||
# paths:
|
|
||||||
# - "src/**/*.ts"
|
|
||||||
# - "src/**/*.tsx"
|
|
||||||
# - "src/**/*.js"
|
|
||||||
# - "src/**/*.jsx"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
claude-review:
|
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
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
issues: read
|
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -37,6 +24,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
prompt: |
|
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:
|
Please review this pull request and provide feedback on:
|
||||||
- Code quality and best practices
|
- Code quality and best practices
|
||||||
- Potential bugs or issues
|
- Potential bugs or issues
|
||||||
@@ -44,11 +35,13 @@ jobs:
|
|||||||
- Security concerns
|
- Security concerns
|
||||||
- Test coverage
|
- 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 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.
|
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
|
# 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
|
# 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:*)"'
|
||||||
|
|
||||||
|
|||||||
55
.github/workflows/codebaseMaintenance.yml
vendored
Normal file
55
.github/workflows/codebaseMaintenance.yml
vendored
Normal 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:*)"'
|
||||||
5
.teamcity/_Self/Project.kt
vendored
5
.teamcity/_Self/Project.kt
vendored
@@ -5,6 +5,7 @@ import _Self.buildTypes.LongRunning
|
|||||||
import _Self.buildTypes.Nvim
|
import _Self.buildTypes.Nvim
|
||||||
import _Self.buildTypes.PluginVerifier
|
import _Self.buildTypes.PluginVerifier
|
||||||
import _Self.buildTypes.PropertyBased
|
import _Self.buildTypes.PropertyBased
|
||||||
|
import _Self.buildTypes.RandomOrderTests
|
||||||
import _Self.buildTypes.TestingBuildType
|
import _Self.buildTypes.TestingBuildType
|
||||||
import _Self.subprojects.GitHub
|
import _Self.subprojects.GitHub
|
||||||
import _Self.subprojects.Releases
|
import _Self.subprojects.Releases
|
||||||
@@ -16,7 +17,8 @@ import jetbrains.buildServer.configs.kotlin.v2019_2.Project
|
|||||||
object Project : Project({
|
object Project : Project({
|
||||||
description = "Vim engine for JetBrains IDEs"
|
description = "Vim engine for JetBrains IDEs"
|
||||||
|
|
||||||
subProjects(Releases, GitHub)
|
subProject(Releases)
|
||||||
|
subProject(GitHub)
|
||||||
|
|
||||||
// VCS roots
|
// VCS roots
|
||||||
vcsRoot(GitHubPullRequest)
|
vcsRoot(GitHubPullRequest)
|
||||||
@@ -30,6 +32,7 @@ object Project : Project({
|
|||||||
|
|
||||||
buildType(PropertyBased)
|
buildType(PropertyBased)
|
||||||
buildType(LongRunning)
|
buildType(LongRunning)
|
||||||
|
buildType(RandomOrderTests)
|
||||||
|
|
||||||
buildType(Nvim)
|
buildType(Nvim)
|
||||||
buildType(PluginVerifier)
|
buildType(PluginVerifier)
|
||||||
|
|||||||
44
.teamcity/_Self/buildTypes/RandomOrderTests.kt
vendored
Normal file
44
.teamcity/_Self/buildTypes/RandomOrderTests.kt
vendored
Normal 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>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
16
AUTHORS.md
16
AUTHORS.md
@@ -638,6 +638,22 @@ Contributors:
|
|||||||
[![icon][github]](https://github.com/magidc)
|
[![icon][github]](https://github.com/magidc)
|
||||||
|
|
||||||
magidc
|
magidc
|
||||||
|
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||||
|
[![icon][github]](https://github.com/apps/github-actions)
|
||||||
|
|
||||||
|
github-actions[bot]
|
||||||
|
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||||
|
[![icon][github]](https://github.com/apps/github-actions)
|
||||||
|
|
||||||
|
github-actions[bot]
|
||||||
|
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||||
|
[![icon][github]](https://github.com/apps/github-actions)
|
||||||
|
|
||||||
|
github-actions[bot]
|
||||||
|
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||||
|
[![icon][github]](https://github.com/apps/github-actions)
|
||||||
|
|
||||||
|
github-actions[bot]
|
||||||
|
|
||||||
Previous contributors:
|
Previous contributors:
|
||||||
|
|
||||||
|
|||||||
@@ -130,8 +130,13 @@ Sed in orci mauris.
|
|||||||
Cras id tellus in ex imperdiet egestas.
|
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
|
3. Don't forget to test your functionality with various corner cases:
|
||||||
carets, dollar motion, etc.
|
- **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
|
##### Neovim
|
||||||
IdeaVim has an integration with neovim in tests. Tests that are performed with `doTest` also executed in
|
IdeaVim has an integration with neovim in tests. Tests that are performed with `doTest` also executed in
|
||||||
|
|||||||
@@ -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
|
value is off. The equivalent processing for paste is controlled by the
|
||||||
"ideaput" value to the 'clipboard' option.
|
"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)
|
'ideajoin' boolean (default off)
|
||||||
global or local to buffer
|
global or local to buffer
|
||||||
When enabled, join commands will be handled by the IDE's "smart join"
|
When enabled, join commands will be handled by the IDE's "smart join"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ package com.maddyhome.idea.vim.extension.hints
|
|||||||
import com.intellij.ui.treeStructure.Tree
|
import com.intellij.ui.treeStructure.Tree
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Point
|
import java.awt.Point
|
||||||
import java.util.*
|
import java.util.WeakHashMap
|
||||||
import javax.accessibility.Accessible
|
import javax.accessibility.Accessible
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@ internal sealed class HintGenerator {
|
|||||||
val hintIterator = alphabet.permutations(length).map { it.joinToString("") }.iterator()
|
val hintIterator = alphabet.permutations(length).map { it.joinToString("") }.iterator()
|
||||||
targets.forEach { target ->
|
targets.forEach { target ->
|
||||||
target.hint = if (preserve) {
|
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
|
// 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
|
} ?: return generate(targets, false) // do not preserve previous hints if failed
|
||||||
} else {
|
} else {
|
||||||
hintIterator.next()
|
hintIterator.next()
|
||||||
|
|||||||
@@ -81,8 +81,12 @@ internal object IdeaSelectionControl {
|
|||||||
return@singleTask
|
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)) {
|
if (dontChangeMode(editor)) {
|
||||||
IdeaRefactorModeHelper.correctSelection(editor)
|
IdeaRefactorModeHelper.correctEditorSelection(editor)
|
||||||
logger.trace { "Selection corrected for refactoring" }
|
logger.trace { "Selection corrected for refactoring" }
|
||||||
return@singleTask
|
return@singleTask
|
||||||
}
|
}
|
||||||
@@ -146,8 +150,13 @@ internal object IdeaSelectionControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun chooseNonSelectionMode(editor: Editor): Mode {
|
private fun chooseNonSelectionMode(editor: Editor): Mode {
|
||||||
val templateActive = editor.isTemplateActive()
|
// If we're in an active template and the editor has just removed a selection without adding a new one, we're in a
|
||||||
if (templateActive && editor.vim.mode.inNormalMode || editor.inInsertMode) {
|
// 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.INSERT
|
||||||
}
|
}
|
||||||
return Mode.NORMAL()
|
return Mode.NORMAL()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
|
|||||||
import com.intellij.codeInsight.template.Template
|
import com.intellij.codeInsight.template.Template
|
||||||
import com.intellij.codeInsight.template.TemplateEditingAdapter
|
import com.intellij.codeInsight.template.TemplateEditingAdapter
|
||||||
import com.intellij.codeInsight.template.TemplateManagerListener
|
import com.intellij.codeInsight.template.TemplateManagerListener
|
||||||
|
import com.intellij.codeInsight.template.impl.TemplateImpl
|
||||||
import com.intellij.codeInsight.template.impl.TemplateState
|
import com.intellij.codeInsight.template.impl.TemplateState
|
||||||
import com.intellij.find.FindModelListener
|
import com.intellij.find.FindModelListener
|
||||||
import com.intellij.ide.actions.ApplyIntentionAction
|
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.KeyHandler
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.action.VimShortcutKeyAction
|
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.injector
|
||||||
|
import com.maddyhome.idea.vim.api.options
|
||||||
import com.maddyhome.idea.vim.group.NotificationService
|
import com.maddyhome.idea.vim.group.NotificationService
|
||||||
import com.maddyhome.idea.vim.group.RegisterGroup
|
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.helper.isIdeaVimDisabledHere
|
||||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||||
import com.maddyhome.idea.vim.newapi.initInjector
|
import com.maddyhome.idea.vim.newapi.initInjector
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
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.undo.VimTimestampBasedUndoService
|
||||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper
|
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.isIdeaRefactorModeKeep
|
||||||
|
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
|
||||||
import org.jetbrains.annotations.NonNls
|
import org.jetbrains.annotations.NonNls
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import javax.swing.KeyStroke
|
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 {
|
class VimTemplateManagerListener : TemplateManagerListener {
|
||||||
override fun templateStarted(state: TemplateState) {
|
override fun templateStarted(state: TemplateState) {
|
||||||
if (VimPlugin.isNotEnabled()) return
|
if (VimPlugin.isNotEnabled()) return
|
||||||
@@ -223,27 +267,65 @@ internal object IdeaSpecifics {
|
|||||||
oldIndex: Int,
|
oldIndex: Int,
|
||||||
newIndex: Int,
|
newIndex: Int,
|
||||||
) {
|
) {
|
||||||
if (templateState.editor.vim.isIdeaRefactorModeKeep) {
|
fun VimEditor.exitMode() = when (this.mode) {
|
||||||
IdeaRefactorModeHelper.correctSelection(templateState.editor)
|
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
|
//endregion
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import com.maddyhome.idea.vim.api.VimEditorBase
|
|||||||
import com.maddyhome.idea.vim.api.VimFoldRegion
|
import com.maddyhome.idea.vim.api.VimFoldRegion
|
||||||
import com.maddyhome.idea.vim.api.VimIndentConfig
|
import com.maddyhome.idea.vim.api.VimIndentConfig
|
||||||
import com.maddyhome.idea.vim.api.VimScrollingModel
|
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.VimVirtualFile
|
||||||
import com.maddyhome.idea.vim.api.VimVisualPosition
|
import com.maddyhome.idea.vim.api.VimVisualPosition
|
||||||
import com.maddyhome.idea.vim.api.injector
|
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)
|
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 {
|
override fun getScrollingModel(): VimScrollingModel {
|
||||||
return object : VimScrollingModel {
|
return object : VimScrollingModel {
|
||||||
private val sm = editor.scrollingModel as ScrollingModelEx
|
private val sm = editor.scrollingModel as ScrollingModelEx
|
||||||
|
|||||||
@@ -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
|
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
|
||||||
// things happen.
|
// things happen.
|
||||||
override fun clearStatusBarMessage() {
|
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
|
// Don't clear the status bar message if we've only just set it
|
||||||
if (!allowClearStatusBarMessage) return
|
if (!allowClearStatusBarMessage) return
|
||||||
@@ -71,7 +72,7 @@ internal class IjVimMessages : VimMessagesBase() {
|
|||||||
ProjectManager.getInstance().openProjects.forEach { project ->
|
ProjectManager.getInstance().openProjects.forEach { project ->
|
||||||
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
|
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
|
||||||
// Only clear the status bar if it's showing our last message
|
// 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 = ""
|
statusBar.info = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* 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.Component
|
||||||
import java.awt.Insets
|
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 {
|
override fun getBorderInsets(component: Component?): Insets {
|
||||||
return JBInsets(getThickness() + 2, 0, 2, 2)
|
return JBInsets(getThickness() + 2, 0, 2, 2)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import com.maddyhome.idea.vim.api.VimEditor
|
|||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
import com.maddyhome.idea.vim.group.IjOptionConstants
|
||||||
import com.maddyhome.idea.vim.helper.VimLockLabel
|
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.helper.hasVisualSelection
|
||||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
||||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||||
@@ -87,36 +86,50 @@ internal object IdeaRefactorModeHelper {
|
|||||||
|
|
||||||
@VimLockLabel.RequiresReadLock
|
@VimLockLabel.RequiresReadLock
|
||||||
@RequiresReadLock
|
@RequiresReadLock
|
||||||
fun calculateCorrections(editor: Editor): List<Action> {
|
fun calculateCorrectionsToSyncEditorToMode(editor: Editor): List<Action> {
|
||||||
val corrections = mutableListOf<Action>()
|
val corrections = mutableListOf<Action>()
|
||||||
val mode = editor.vim.mode
|
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()) {
|
if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
|
||||||
corrections.add(Action.RemoveSelection)
|
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()) {
|
if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
|
||||||
val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim)
|
val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim)
|
||||||
if (mode.selectionType != selectionType) {
|
if (mode.selectionType != selectionType) {
|
||||||
val newMode = when (mode) {
|
val newMode = when (mode) {
|
||||||
is Mode.SELECT -> mode.copy(selectionType)
|
is Mode.SELECT -> mode.copy(selectionType = selectionType)
|
||||||
is Mode.VISUAL -> mode.copy(selectionType)
|
is Mode.VISUAL -> mode.copy(selectionType = selectionType)
|
||||||
else -> error("IdeaVim should be either in visual or select modes")
|
else -> error("IdeaVim should be either in visual or select modes")
|
||||||
}
|
}
|
||||||
corrections.add(Action.SetMode(newMode))
|
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 ->
|
TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange ->
|
||||||
if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) {
|
if (!segmentRange.isEmpty && segmentRange.startOffset != editor.caretModel.offset) {
|
||||||
corrections.add(Action.MoveToOffset(editor.caretModel.offset - 1))
|
corrections.add(Action.MoveToOffset(segmentRange.startOffset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return corrections
|
return corrections
|
||||||
}
|
}
|
||||||
|
|
||||||
fun correctSelection(editor: Editor) {
|
/**
|
||||||
val corrections = injector.application.runReadAction { calculateCorrections(editor) }
|
* Correct the editor's selection to match the current Vim mode
|
||||||
applyCorrections(corrections, editor)
|
*/
|
||||||
|
fun correctEditorSelection(editor: Editor) {
|
||||||
|
injector.application.runReadAction {
|
||||||
|
val corrections = calculateCorrectionsToSyncEditorToMode(editor)
|
||||||
|
applyCorrections(corrections, editor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ nowrap
|
|||||||
nowrapscan
|
nowrapscan
|
||||||
|
|
||||||
ideacopypreprocess
|
ideacopypreprocess
|
||||||
ideaglobalmode
|
|
||||||
ideajoin
|
ideajoin
|
||||||
ideamarks
|
ideamarks
|
||||||
idearefactormode
|
idearefactormode
|
||||||
|
|||||||
@@ -50,4 +50,10 @@ class DeletePreviousWordActionTest : VimExTestCase() {
|
|||||||
typeText(":set keymodel=continueselect,stopselect<C-W>")
|
typeText(":set keymodel=continueselect,stopselect<C-W>")
|
||||||
assertExText("set ")
|
assertExText("set ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test delete at beginning does nothing`() {
|
||||||
|
typeText(":<C-W>")
|
||||||
|
assertExText("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -721,6 +721,17 @@ class MapCommandTest : VimTestCase() {
|
|||||||
assertState("zzz\n")
|
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")
|
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "bad replace term codes")
|
||||||
@Test
|
@Test
|
||||||
fun testAmbiguousMapping() {
|
fun testAmbiguousMapping() {
|
||||||
|
|||||||
@@ -103,4 +103,22 @@ class PrintLineNumberTest : VimTestCase() {
|
|||||||
enterCommand("2=p#")
|
enterCommand("2=p#")
|
||||||
assertStatusLineMessageContains("2 \t\t\tconsectetur adipiscing elit")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,15 @@ class IndexedExpressionTest : VimTestCase("\n") {
|
|||||||
assertPluginError(false)
|
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
|
@Test
|
||||||
fun `test indexed String expression with negative index returns empty String`() {
|
fun `test indexed String expression with negative index returns empty String`() {
|
||||||
// Surprisingly not the same as List
|
// Surprisingly not the same as List
|
||||||
|
|||||||
@@ -30,6 +30,51 @@ class LenFunctionTest : VimTestCase() {
|
|||||||
assertCommandOutput("echo len(12 . 4)", "3")
|
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
|
@Test
|
||||||
fun `test len with float causes errors`() {
|
fun `test len with float causes errors`() {
|
||||||
enterCommand("echo len(4.2)")
|
enterCommand("echo len(4.2)")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package org.jetbrains.plugins.ideavim.ex.implementation.functions.listFunctions
|
package org.jetbrains.plugins.ideavim.ex.implementation.functions.listFunctions
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
|
import kotlin.test.assertTrue
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -59,17 +60,18 @@ class RangeFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test range with zero stride throws error`() {
|
fun `test range with zero stride throws error`() {
|
||||||
enterCommand("echo range(1, 5, 0)")
|
enterCommand("echo range(1, 5, 0)")
|
||||||
kotlin.test.assertTrue(injector.messages.isError())
|
assertTrue(injector.messages.isError())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test range with start past end throws error`() {
|
fun `test range with start past end throws error`() {
|
||||||
enterCommand("echo range(2, 0)")
|
enterCommand("echo range(2, 0)")
|
||||||
kotlin.test.assertTrue(injector.messages.isError())
|
assertTrue(injector.messages.isError())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test range negative with start past end throws error`() {
|
fun `test range negative with start past end throws error`() {
|
||||||
enterCommand("echo range(-2, 0, -1)")
|
enterCommand("echo range(-2, 0, -1)")
|
||||||
|
assertTrue(injector.messages.isError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,6 @@ class TrimFunctionTest : VimTestCase() {
|
|||||||
assertCommandOutput("echo trim('\t\nhello\n\t')", "hello")
|
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
|
@Test
|
||||||
fun `test trim with custom mask does not affect middle`() {
|
fun `test trim with custom mask does not affect middle`() {
|
||||||
assertCommandOutput("echo trim('rm<Xrm<>X>rrm', 'rm<>')", "Xrm<>X")
|
assertCommandOutput("echo trim('rm<Xrm<>X>rrm', 'rm<>')", "Xrm<>X")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.state.mode.selectionType
|
|||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
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.rangeOf
|
||||||
import org.jetbrains.plugins.ideavim.waitAndAssert
|
import org.jetbrains.plugins.ideavim.waitAndAssert
|
||||||
import org.jetbrains.plugins.ideavim.waitAndAssertMode
|
import org.jetbrains.plugins.ideavim.waitAndAssertMode
|
||||||
@@ -89,7 +89,7 @@ class NonVimVisualChangeTest : VimTestCase() {
|
|||||||
fixture.editor.selectionModel.removeSelection()
|
fixture.editor.selectionModel.removeSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertDoesntChange { fixture.editor.vim.mode == Mode.INSERT }
|
assertModeDoesNotChange(fixture.editor, Mode.INSERT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ package org.jetbrains.plugins.ideavim
|
|||||||
|
|
||||||
import com.intellij.ide.IdeEventQueue
|
import com.intellij.ide.IdeEventQueue
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
||||||
import com.intellij.util.containers.toArray
|
import com.intellij.util.containers.toArray
|
||||||
import com.maddyhome.idea.vim.api.injector
|
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.newapi.vim
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
import com.maddyhome.idea.vim.state.mode.Mode
|
||||||
import org.junit.jupiter.params.provider.Arguments
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,17 +59,9 @@ annotation class VimBehaviorDiffers(
|
|||||||
val shouldBeFixed: Boolean = true,
|
val shouldBeFixed: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
inline fun waitAndAssert(timeInMillis: Int = 1000, crossinline condition: () -> Boolean) {
|
// The selection is updated after 'visualdelay' milliseconds. Add an adjustment when we wait for it to be completed.
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
// Since we wait for it on the main thread (to avoid reading in-progress state), we can get away with a short adjustment
|
||||||
val end = System.currentTimeMillis() + timeInMillis
|
private const val visualDelayAdjustment = 200
|
||||||
while (end > System.currentTimeMillis()) {
|
|
||||||
Thread.sleep(10)
|
|
||||||
IdeEventQueue.getInstance().flushQueue()
|
|
||||||
if (condition()) return@invokeAndWait
|
|
||||||
}
|
|
||||||
fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) {
|
fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) {
|
||||||
assertDoesntChange(timeInMillis - precision) { !condition() }
|
assertDoesntChange(timeInMillis - precision) { !condition() }
|
||||||
@@ -111,15 +106,58 @@ fun <T> product(vararg elements: List<T>): List<List<T>> {
|
|||||||
return res
|
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(
|
fun waitAndAssertMode(
|
||||||
fixture: CodeInsightTestFixture,
|
fixture: CodeInsightTestFixture,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
timeInMillis: Int? = null,
|
timeInMillis: Int? = null,
|
||||||
) {
|
) {
|
||||||
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + 1000)
|
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment)
|
||||||
waitAndAssert(timeout) { fixture.editor.vim.mode == mode }
|
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 {
|
fun waitUntil(timeout: Int = 10_000, condition: () -> Boolean): Boolean {
|
||||||
val timeEnd = System.currentTimeMillis() + timeout
|
val timeEnd = System.currentTimeMillis() + timeout
|
||||||
while (System.currentTimeMillis() < timeEnd) {
|
while (System.currentTimeMillis() < timeEnd) {
|
||||||
|
|||||||
@@ -139,19 +139,25 @@ and some text after""",
|
|||||||
ApplicationManager.getApplication().invokeAndWait {
|
ApplicationManager.getApplication().invokeAndWait {
|
||||||
ApplicationManager.getApplication().runWriteAction {
|
ApplicationManager.getApplication().runWriteAction {
|
||||||
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
|
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"))
|
typeText(injector.parser.parseKeys("za"))
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
ApplicationManager.getApplication().invokeAndWait {
|
||||||
ApplicationManager.getApplication().runWriteAction {
|
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"))
|
typeText(injector.parser.parseKeys("za"))
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
ApplicationManager.getApplication().invokeAndWait {
|
||||||
ApplicationManager.getApplication().runWriteAction {
|
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 {
|
ApplicationManager.getApplication().invokeAndWait {
|
||||||
fixture.editor.foldingModel.runBatchFoldingOperation {
|
fixture.editor.foldingModel.runBatchFoldingOperation {
|
||||||
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ bar
|
|||||||
ApplicationManager.getApplication().invokeAndWait {
|
ApplicationManager.getApplication().invokeAndWait {
|
||||||
fixture.editor.foldingModel.runBatchFoldingOperation {
|
fixture.editor.foldingModel.runBatchFoldingOperation {
|
||||||
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
|||||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
|
|||||||
@@ -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.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
@CommandOrMotion(keys = ["s"], modes = [Mode.NORMAL])
|
@CommandOrMotion(keys = ["s"], modes = [Mode.NORMAL])
|
||||||
class ChangeCharactersAction : ChangeInInsertSequenceAction() {
|
class ChangeCharactersAction : ChangeInInsertSequenceAction() {
|
||||||
|
|||||||
@@ -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.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
@CommandOrMotion(keys = ["C"], modes = [Mode.NORMAL])
|
@CommandOrMotion(keys = ["C"], modes = [Mode.NORMAL])
|
||||||
class ChangeEndOfLineAction : ChangeInInsertSequenceAction() {
|
class ChangeEndOfLineAction : ChangeInInsertSequenceAction() {
|
||||||
|
|||||||
@@ -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.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
@CommandOrMotion(keys = ["S"], modes = [Mode.NORMAL])
|
@CommandOrMotion(keys = ["S"], modes = [Mode.NORMAL])
|
||||||
class ChangeLineAction : ChangeInInsertSequenceAction() {
|
class ChangeLineAction : ChangeInInsertSequenceAction() {
|
||||||
|
|||||||
@@ -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.DuplicableOperatorAction
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
@CommandOrMotion(keys = ["c"], modes = [Mode.NORMAL])
|
@CommandOrMotion(keys = ["c"], modes = [Mode.NORMAL])
|
||||||
class ChangeMotionAction : ChangeInInsertSequenceAction(), DuplicableOperatorAction {
|
class ChangeMotionAction : ChangeInInsertSequenceAction(), DuplicableOperatorAction {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
|
|||||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
|
|||||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
|||||||
import com.maddyhome.idea.vim.handler.VimActionHandler
|
import com.maddyhome.idea.vim.handler.VimActionHandler
|
||||||
import com.maddyhome.idea.vim.helper.endOffsetInclusive
|
import com.maddyhome.idea.vim.helper.endOffsetInclusive
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
@CommandOrMotion(keys = ["!"], modes = [Mode.VISUAL])
|
@CommandOrMotion(keys = ["!"], modes = [Mode.VISUAL])
|
||||||
class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterCommand {
|
class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterCommand {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
|||||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
|
|||||||
@@ -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
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
|
|||||||
@@ -442,9 +442,11 @@ abstract class VimChangeGroupBase : VimChangeGroup {
|
|||||||
*/
|
*/
|
||||||
override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) {
|
override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) {
|
||||||
val state = injector.vimState
|
val state = injector.vimState
|
||||||
for (caret in editor.nativeCarets()) {
|
injector.application.runReadAction {
|
||||||
caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset)
|
for (caret in editor.nativeCarets()) {
|
||||||
injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
|
caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset)
|
||||||
|
injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val cmd = state.executingCommand
|
val cmd = state.executingCommand
|
||||||
if (cmd != null && state.isDotRepeatInProgress) {
|
if (cmd != null && state.isDotRepeatInProgress) {
|
||||||
@@ -470,7 +472,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
|
|||||||
val myChangeListener = VimChangesListener()
|
val myChangeListener = VimChangesListener()
|
||||||
vimDocumentListener = myChangeListener
|
vimDocumentListener = myChangeListener
|
||||||
vimDocument!!.addChangeListener(myChangeListener)
|
vimDocument!!.addChangeListener(myChangeListener)
|
||||||
oldOffset = editor.currentCaret().offset
|
injector.application.runReadAction {
|
||||||
|
oldOffset = editor.currentCaret().offset
|
||||||
|
}
|
||||||
editor.insertMode = mode == Mode.INSERT
|
editor.insertMode = mode == Mode.INSERT
|
||||||
editor.mode = mode
|
editor.mode = mode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ interface VimEditor {
|
|||||||
return getText(start, end)
|
return getText(start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSelectionModel(): VimSelectionModel
|
|
||||||
fun getScrollingModel(): VimScrollingModel
|
fun getScrollingModel(): VimScrollingModel
|
||||||
|
|
||||||
fun removeCaret(caret: VimCaret)
|
fun removeCaret(caret: VimCaret)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -27,26 +27,30 @@ abstract class VimStringParserBase : VimStringParser {
|
|||||||
override fun toPrintableString(keys: List<KeyStroke>): String {
|
override fun toPrintableString(keys: List<KeyStroke>): String {
|
||||||
val builder = StringBuilder()
|
val builder = StringBuilder()
|
||||||
for (key in keys) {
|
for (key in keys) {
|
||||||
val keyAsChar = keyStrokeToChar(key)
|
val keyAsString = keyStrokeToString(key)
|
||||||
builder.append(keyAsChar)
|
builder.append(keyAsString)
|
||||||
}
|
}
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun keyStrokeToChar(key: KeyStroke): Char {
|
private fun keyStrokeToString(key: KeyStroke): String {
|
||||||
if (key.keyChar != KeyEvent.CHAR_UNDEFINED) {
|
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) {
|
} else if (key.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
|
||||||
return if (key.keyCode == 'J'.code) {
|
return if (isControlCharacterKeyCode(key.keyCode)) {
|
||||||
// 'J' is a special case, keycode 10 is \n char
|
if (key.keyCode == 'J'.code) {
|
||||||
0.toChar()
|
// 'J' is a special case, keycode 10 is \n char
|
||||||
|
0.toChar().toString()
|
||||||
|
} else {
|
||||||
|
(key.keyCode - 'A'.code + 1).toChar().toString()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
(key.keyCode - 'A'.code + 1).toChar()
|
"^" + key.keyCode.toChar()
|
||||||
}
|
}
|
||||||
} else if (key.keyChar == KeyEvent.CHAR_UNDEFINED && key.keyCode == KeyEvent.VK_ENTER) {
|
} 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 {
|
override fun toKeyNotation(keyStrokes: List<KeyStroke>): String {
|
||||||
@@ -178,7 +182,18 @@ abstract class VimStringParserBase : VimStringParser {
|
|||||||
private fun getMapLeader(): List<KeyStroke> {
|
private fun getMapLeader(): List<KeyStroke> {
|
||||||
val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader")
|
val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader")
|
||||||
return if (mapLeader is VimString) {
|
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 {
|
} else {
|
||||||
stringToKeys("\\")
|
stringToKeys("\\")
|
||||||
}
|
}
|
||||||
@@ -207,6 +222,11 @@ abstract class VimStringParserBase : VimStringParser {
|
|||||||
return c < '\u0020'
|
return c < '\u0020'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isControlCharacterKeyCode(code: Int): Boolean {
|
||||||
|
// Ctrl-(A..Z [\]^_) are ASCII control characters
|
||||||
|
return code >= 'A'.code && code <= '_'.code
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("SpellCheckingInspection")
|
@Suppress("SpellCheckingInspection")
|
||||||
private fun getVimKeyValue(c: Int): @NonNls String? {
|
private fun getVimKeyValue(c: Int): @NonNls String? {
|
||||||
return when (c) {
|
return when (c) {
|
||||||
@@ -430,17 +450,25 @@ abstract class VimStringParserBase : VimStringParser {
|
|||||||
val specialKey = parseSpecialKey(specialKeyBuilder.toString(), 0)
|
val specialKey = parseSpecialKey(specialKeyBuilder.toString(), 0)
|
||||||
if (specialKey != null) {
|
if (specialKey != null) {
|
||||||
var keyCode = specialKey.keyCode
|
var keyCode = specialKey.keyCode
|
||||||
|
var useKeyCode = true
|
||||||
if (specialKey.keyCode == 0) {
|
if (specialKey.keyCode == 0) {
|
||||||
keyCode = specialKey.keyChar.code
|
keyCode = specialKey.keyChar.code
|
||||||
} else if (specialKey.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
|
} else if (specialKey.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
|
||||||
keyCode = if (specialKey.keyCode == 'J'.code) {
|
if (isControlCharacterKeyCode(specialKey.keyCode)) {
|
||||||
// 'J' is a special case, keycode 10 is \n char
|
keyCode = if (specialKey.keyCode == 'J'.code) {
|
||||||
0
|
// 'J' is a special case, keycode 10 is \n char
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
specialKey.keyCode - 'A'.code + 1
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
specialKey.keyCode - 'A'.code + 1
|
useKeyCode = false
|
||||||
|
result.append("\\<${specialKeyBuilder}>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.append(keyCode.toChar())
|
if (useKeyCode) {
|
||||||
|
result.append(keyCode.toChar())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.append("<").append(specialKeyBuilder).append(">")
|
result.append("<").append(specialKeyBuilder).append(">")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ class VimEditorReplaceMask {
|
|||||||
fun recordChangeAtCaret(editor: VimEditor) {
|
fun recordChangeAtCaret(editor: VimEditor) {
|
||||||
for (caret in editor.carets()) {
|
for (caret in editor.carets()) {
|
||||||
val offset = caret.offset
|
val offset = caret.offset
|
||||||
val marker = editor.createLiveMarker(offset, offset)
|
if (offset < editor.fileSize()) {
|
||||||
changedChars[marker] = editor.charAt(offset)
|
val marker = editor.createLiveMarker(offset, offset)
|
||||||
|
changedChars[marker] = editor.charAt(offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ object EngineStringHelper {
|
|||||||
if (c == KeyEvent.CHAR_UNDEFINED && key.modifiers == 0) {
|
if (c == KeyEvent.CHAR_UNDEFINED && key.modifiers == 0) {
|
||||||
c = key.keyCode.toChar()
|
c = key.keyCode.toChar()
|
||||||
} else if (c == KeyEvent.CHAR_UNDEFINED && key.modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
|
} 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)
|
return toPrintableCharacter(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import com.maddyhome.idea.vim.api.VimEditorBase
|
|||||||
import com.maddyhome.idea.vim.api.VimFoldRegion
|
import com.maddyhome.idea.vim.api.VimFoldRegion
|
||||||
import com.maddyhome.idea.vim.api.VimIndentConfig
|
import com.maddyhome.idea.vim.api.VimIndentConfig
|
||||||
import com.maddyhome.idea.vim.api.VimScrollingModel
|
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.VimVirtualFile
|
||||||
|
import com.maddyhome.idea.vim.api.VimVisualPosition
|
||||||
import com.maddyhome.idea.vim.common.LiveRange
|
import com.maddyhome.idea.vim.common.LiveRange
|
||||||
import com.maddyhome.idea.vim.common.TextRange
|
import com.maddyhome.idea.vim.common.TextRange
|
||||||
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
|
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
|
||||||
@@ -696,10 +695,6 @@ class VimRegex(pattern: String) {
|
|||||||
|
|
||||||
override fun deleteString(range: TextRange) {}
|
override fun deleteString(range: TextRange) {}
|
||||||
|
|
||||||
override fun getSelectionModel(): VimSelectionModel {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getScrollingModel(): VimScrollingModel {
|
override fun getScrollingModel(): VimScrollingModel {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ internal class EndOfLineMatcher : Matcher {
|
|||||||
isCaseInsensitive: Boolean,
|
isCaseInsensitive: Boolean,
|
||||||
possibleCursors: MutableList<VimCaret>,
|
possibleCursors: MutableList<VimCaret>,
|
||||||
): MatcherResult {
|
): 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
|
else MatcherResult.Failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ internal class StartOfLineMatcher : Matcher {
|
|||||||
isCaseInsensitive: Boolean,
|
isCaseInsensitive: Boolean,
|
||||||
possibleCursors: MutableList<VimCaret>,
|
possibleCursors: MutableList<VimCaret>,
|
||||||
): MatcherResult {
|
): MatcherResult {
|
||||||
|
if (index < 0 || index > editor.text().length) return MatcherResult.Failure
|
||||||
return if (index == 0 || editor.text()[index - 1] == '\n') MatcherResult.Success(0)
|
return if (index == 0 || editor.text()[index - 1] == '\n') MatcherResult.Success(0)
|
||||||
else MatcherResult.Failure
|
else MatcherResult.Failure
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ data class PrintLineNumberCommand(val range: Range, val modifier: CommandModifie
|
|||||||
throw exExceptionMessage("E488", argument)
|
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 `$`
|
// `l` means output the line like `:list` - show unprintable chars, and include `^` and `$`
|
||||||
// `#` means output the line with the line number
|
// `#` means output the line with the line number
|
||||||
|
|||||||
@@ -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 we don't have a range, we either have "sort", a selection, or a block
|
||||||
if (range.size == 1) {
|
if (range.size == 1) {
|
||||||
// If we have a selection.
|
// If we have a selection.
|
||||||
val selectionModel = editor.getSelectionModel()
|
return if (caret.hasSelection()) {
|
||||||
return if (selectionModel.hasSelection()) {
|
val start = caret.selectionStart
|
||||||
val start = selectionModel.selectionStart
|
val end = caret.selectionEnd
|
||||||
val end = selectionModel.selectionEnd
|
|
||||||
|
|
||||||
val startLine = editor.offsetToBufferPosition(start).line
|
val startLine = editor.offsetToBufferPosition(start).line
|
||||||
val endLine = editor.offsetToBufferPosition(end).line
|
val endLine = editor.offsetToBufferPosition(end).line
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ data class IndexedExpression(val index: Expression, val expression: Expression)
|
|||||||
else -> {
|
else -> {
|
||||||
// Try to convert the expression to String, then index it
|
// Try to convert the expression to String, then index it
|
||||||
val text = expressionValue.toVimString().value
|
val text = expressionValue.toVimString().value
|
||||||
val idx = index.evaluate(editor, context, vimContext).toVimNumber().value
|
val idx = indexValue.toVimNumber().value
|
||||||
if (idx < 0 || idx > text.length) {
|
if (idx < 0 || idx >= text.length) {
|
||||||
return VimString.EMPTY
|
return VimString.EMPTY
|
||||||
}
|
}
|
||||||
return VimString(text[idx].toString())
|
return VimString(text[idx].toString())
|
||||||
@@ -121,10 +121,10 @@ data class IndexedExpression(val index: Expression, val expression: Expression)
|
|||||||
vimContext: VimLContext,
|
vimContext: VimLContext,
|
||||||
assignmentTextForErrors: String
|
assignmentTextForErrors: String
|
||||||
) {
|
) {
|
||||||
val index = index.evaluate(editor, context, vimContext).toVimNumber().value
|
val indexNum = index.evaluate(editor, context, vimContext).toVimNumber().value
|
||||||
val idx = if (index < 0) index + list.values.size else index
|
val idx = if (indexNum < 0) indexNum + list.values.size else indexNum
|
||||||
if (idx < 0 || idx >= list.values.size) {
|
if (idx < 0 || idx >= list.values.size) {
|
||||||
throw exExceptionMessage("E684", index)
|
throw exExceptionMessage("E684", indexNum)
|
||||||
}
|
}
|
||||||
if (list.values[idx].isLocked) {
|
if (list.values[idx].isLocked) {
|
||||||
throw exExceptionMessage("E741", assignmentTextForErrors)
|
throw exExceptionMessage("E741", assignmentTextForErrors)
|
||||||
|
|||||||
Reference in New Issue
Block a user