mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-10-16 20:33:57 +02:00
Compare commits
144
Commits
d9b8a4fb13
...
975bead6f8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
975bead6f8
|
||
|
|
fdcd20bc19
|
||
|
|
183a20c936
|
||
|
|
2f9a9a972a
|
||
|
|
c764f7455f
|
||
|
|
4a3c8b46a4
|
||
|
|
2ec581cdcf
|
||
|
|
0b4eecb284
|
||
|
|
3c5a8760f4
|
||
|
|
7658d83564
|
||
|
|
8d0b403c50
|
||
|
|
c4362ddc01
|
||
|
|
5eff0518f1
|
||
|
|
f2fb835f87
|
||
|
|
8c12c4a4fc
|
||
|
|
500270f1ec
|
||
|
|
08cf6701ed
|
||
|
|
a62604111c
|
||
|
|
38b26df573
|
||
|
|
7d414beaa0
|
||
|
|
0fae2bec79
|
||
|
|
f11750f4ea
|
||
|
|
273beba127
|
||
|
|
d57361ba0c
|
||
|
|
7437800180
|
||
|
|
ddd83eac56
|
||
|
|
54d4f03c95
|
||
|
|
7dbf4fe83a
|
||
|
|
ebafeac717
|
||
|
|
9b30fa67e2
|
||
|
|
e0f24cc6a7
|
||
|
|
a71c8a319c
|
||
|
|
b8f9ad7892 | ||
|
|
0efcd70c6f | ||
|
|
a00e382bdf | ||
|
|
713a61e362 | ||
|
|
bb18bb5356 | ||
|
|
2201617e81 | ||
|
|
fe78002de0 | ||
|
|
6cedadc455 | ||
|
|
d2ac6b9fd8 | ||
|
|
8914f1da2d | ||
|
|
ee77c6de90 | ||
|
|
3fdb747969 | ||
|
|
f4272fe350 | ||
|
|
8b97cd8bed | ||
|
|
518c5eab09 | ||
|
|
398b161704 | ||
|
|
2d72b1f083 | ||
|
|
36ef5574bd | ||
|
|
c3f7c1c216 | ||
|
|
1033626866 | ||
|
|
7cccaaa814 | ||
|
|
06778f5380 | ||
|
|
738b73417f | ||
|
|
81ad363113 | ||
|
|
bd0f361afa | ||
|
|
fbc9424f6f | ||
|
|
9e142d4401 | ||
|
|
19925409d5 | ||
|
|
55549efd72 | ||
|
|
2275c21729 | ||
|
|
90f715dee5 | ||
|
|
4114fc1122 | ||
|
|
644f9cf628 | ||
|
|
8e3aa39789 | ||
|
|
1e46befc77 | ||
|
|
3058a337d4 | ||
|
|
1d34cc3206 | ||
|
|
21ffd15bdb | ||
|
|
f90bc0bca6 | ||
|
|
f686ed5911 | ||
|
|
86e9130d87 | ||
|
|
1a2cc3c401 | ||
|
|
5e6c95c187 | ||
|
|
c78d2f326c | ||
|
|
c11a903c5b | ||
|
|
ac52d3a0e4 | ||
|
|
165b9b1fa5 | ||
|
|
2b5f5401ac | ||
|
|
6620891534 | ||
|
|
f51223ffbb | ||
|
|
5fd38b21b3 | ||
|
|
1e3c671c0c | ||
|
|
0e5e95d02d | ||
|
|
ca384caae4 | ||
|
|
f3003d92ef | ||
|
|
c898281cd9 | ||
|
|
e084c387fa | ||
|
|
52b33469ec | ||
|
|
4bc3e56898 | ||
|
|
38489c3177 | ||
|
|
807aa55dcc | ||
|
|
9062301f21 | ||
|
|
b6e2bc32a9 | ||
|
|
3b92308f9c | ||
|
|
2295e96509 | ||
|
|
e4bccc976e | ||
|
|
ae6198400b | ||
|
|
b3413f5924 | ||
|
|
5ea29e1a6e | ||
|
|
92af7a9b58 | ||
|
|
7b9cf20a19 | ||
|
|
9ed7a53709 | ||
|
|
fd310620e7 | ||
|
|
30c876dd59 | ||
|
|
d7b5df3ebc | ||
|
|
37aeff64aa | ||
|
|
723e45deb9 | ||
|
|
543d577bfa | ||
|
|
e0193e6f3b | ||
|
|
f6ee0c0f11 | ||
|
|
2282456ddd | ||
|
|
d092c00547 | ||
|
|
6f48fd9903 | ||
|
|
fe7bc72193 | ||
|
|
ccb0fb1677 | ||
|
|
c1d9ee7378 | ||
|
|
360d3d4b69 | ||
|
|
e597ca1069 | ||
|
|
f65c5e28f5 | ||
|
|
e27ffe0549 | ||
|
|
8ad1ab8bdf | ||
|
|
69d845f92f | ||
|
|
f94d5b05dc | ||
|
|
77d6a1d78f | ||
|
|
2ec828b529 | ||
|
|
b00370888a | ||
|
|
a1efa6f18a | ||
|
|
c06802d135 | ||
|
|
7bd8f1315c | ||
|
|
5bc0344547 | ||
|
|
76a26d51f2 | ||
|
|
d99f46937f | ||
|
|
6e47c141de | ||
|
|
5b2a1725ec | ||
|
|
60d17b0c83 | ||
|
|
2b55e12b15 | ||
|
|
f5fd057ba1 | ||
|
|
aa14a3036e | ||
|
|
fe53a09522 | ||
|
|
57aa1424ab | ||
|
|
7ec829344e | ||
|
|
c24adece73 |
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description: Code reviewer for IdeaVim - focuses on Vim compatibility, Kotlin/Java quality, IntelliJ Platform patterns, and test coverage.
|
||||
model: inherit
|
||||
color: pink
|
||||
---
|
||||
|
||||
You are a code reviewer for IdeaVim, an open-source Vim emulator plugin for JetBrains IDEs. Your focus is on Vim compatibility, code quality, and maintainability.
|
||||
|
||||
## Project Context
|
||||
|
||||
IdeaVim is:
|
||||
- Written primarily in Kotlin with some Java
|
||||
- An IntelliJ Platform plugin
|
||||
- Split into `vim-engine` (platform-independent) and `IdeaVim` (IntelliJ-specific) modules
|
||||
- Goal: Match Vim functionality and architecture as closely as possible
|
||||
|
||||
## When Reviewing Code
|
||||
|
||||
### Vim Compatibility
|
||||
- Does the change match Vim's behavior? Check against `:help` documentation
|
||||
- Is `@VimBehaviorDiffers` annotation used if behavior intentionally differs from Vim?
|
||||
- Are motions correctly typed (inclusive, exclusive, or linewise via `MotionType`)?
|
||||
- Do extensions use the same command names as original Vim plugins?
|
||||
|
||||
### Code Quality (Kotlin/Java)
|
||||
- Kotlin is preferred for new code; Java only where explicitly used
|
||||
- Check for null safety, proper use of Kotlin idioms
|
||||
- Resource management (especially with IntelliJ Platform disposables)
|
||||
- Error handling appropriate for plugin context
|
||||
|
||||
### IntelliJ Platform Patterns
|
||||
- Correct use of Application/Project services
|
||||
- Proper threading (read/write actions, EDT vs background)
|
||||
- Disposable lifecycle management
|
||||
- Action system usage (`<Action>` in mappings, not `:action`)
|
||||
|
||||
### Test Coverage
|
||||
Check that tests cover corner cases from CONTRIBUTING.md:
|
||||
- **Position-based**: line start/end, file start/end, empty line, single char line
|
||||
- **Content-based**: whitespace-only lines, trailing spaces, tabs/spaces, Unicode, multi-byte chars
|
||||
- **Selection-based**: multiple carets, visual modes (char/line/block), empty selection
|
||||
- **Motion-based**: dollar motion, count with motion (e.g., `3w`, `5j`)
|
||||
- **Buffer state**: empty file, single line file, long lines
|
||||
|
||||
Tests using `doTest` are automatically verified against neovim - this is good.
|
||||
|
||||
### Test Quality
|
||||
- Avoid senseless text like "dhjkwaldjwa" - use Lorem Ipsum or realistic code snippets
|
||||
- Check if `@TestWithoutNeovim` or `@VimBehaviorDiffers` annotations are appropriate
|
||||
- Property tests in `propertybased` package are flaky by nature - verify if failures relate to the change
|
||||
|
||||
## Review Priorities
|
||||
|
||||
1. **Correctness** - Does it work as Vim does?
|
||||
2. **Safety** - No crashes, proper null handling, thread safety
|
||||
3. **Tests** - Corner cases covered, meaningful test data
|
||||
4. **Maintainability** - Clear code, follows project patterns
|
||||
|
||||
## What NOT to Focus On
|
||||
|
||||
- Generic security issues (this is a local editor plugin, not a web service)
|
||||
- Database queries (there are none)
|
||||
- Network security (minimal network usage)
|
||||
- Arbitrary code metrics like "cyclomatic complexity < 10"
|
||||
|
||||
## Output Format
|
||||
|
||||
Provide concise, actionable feedback:
|
||||
- Link to specific lines when pointing out issues
|
||||
- Reference Vim documentation (`:help <topic>`) when relevant
|
||||
- Suggest specific fixes, not just problem descriptions
|
||||
- Acknowledge what's done well (briefly)
|
||||
@@ -1,206 +0,0 @@
|
||||
# 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 or has multiple logical changes, 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
|
||||
- This rule is important!
|
||||
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
|
||||
|
||||
## 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.
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"context7@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
---
|
||||
name: changelog
|
||||
description: Maintains the IdeaVim changelog (CHANGES.md). Use when updating the changelog, documenting releases, or reviewing commits/PRs for changelog entries.
|
||||
---
|
||||
|
||||
# Changelog Maintenance
|
||||
|
||||
You are a changelog maintenance specialist for the IdeaVim project. Your job is to keep the changelog (CHANGES.md) in sync with code changes.
|
||||
|
||||
`build.gradle.kts` `changeNotes` is auto-derived from CHANGES.md at build time via the `org.jetbrains.changelog` plugin — do not edit it by hand.
|
||||
|
||||
## Historical Context
|
||||
|
||||
- The changelog was actively maintained until version 2.9.0
|
||||
- There's a gap from 2.10.0 through 2.27.0 where changelog wasn't maintained
|
||||
- We're resuming changelog maintenance from version 2.28.0 onwards
|
||||
- Between 2.9.0 and 2.28.0, include this note: **"Changelog was not maintained for versions 2.10.0 through 2.27.0"**
|
||||
|
||||
## Changelog Structure
|
||||
|
||||
### To Be Released Section
|
||||
- All unreleased changes from master branch go here
|
||||
- When a release is made, this section becomes the new version section
|
||||
- Create a new empty `To Be Released` section after each release
|
||||
|
||||
> **Header form is load-bearing — do not add brackets.** The header MUST be
|
||||
> exactly `## To Be Released` (no `[...]`). This exact string is matched by
|
||||
> `unreleasedTerm` in `build.gradle.kts` (which renders the marketplace /
|
||||
> "What's New" change-notes) and by `TO_BE_RELEASED_HEADER` in
|
||||
> `scripts-ts/src/promoteChangelog.ts` (which promotes it to a versioned
|
||||
> section at release time). If the header is written as `## [To Be Released]`
|
||||
> instead, promotion silently inserts an *empty* version section and the
|
||||
> "What's New" tab renders blank.
|
||||
|
||||
### Version Entry Format
|
||||
```
|
||||
## 2.28.0, 2024-MM-DD
|
||||
|
||||
### Features:
|
||||
* Feature description without ticket number
|
||||
* `CommandName` action can be used... | [VIM-XXXX](https://youtrack.jetbrains.com/issue/VIM-XXXX)
|
||||
|
||||
### Fixes:
|
||||
* [VIM-XXXX](https://youtrack.jetbrains.com/issue/VIM-XXXX) Bug fix description
|
||||
|
||||
### Changes:
|
||||
* Other changes
|
||||
```
|
||||
|
||||
## How to Gather Information
|
||||
|
||||
### 1. Check Current State
|
||||
- Read CHANGES.md to find the last documented version
|
||||
- **Important**: Only read the top portion of CHANGES.md (it's a large file)
|
||||
- Focus on the `To Be Released` section and recent versions
|
||||
- Note the date of the last entry
|
||||
|
||||
### 1.5. Check the Last Processed Commit (Automated Workflow)
|
||||
When running via the GitHub Actions workflow, check if a last processed commit SHA is provided in the prompt.
|
||||
- If a commit SHA is provided, use `git log <SHA>..HEAD --oneline` to see only unprocessed commits
|
||||
- This is more accurate than date-based filtering
|
||||
- The last successful workflow run is tracked via GitHub Actions API
|
||||
|
||||
### 2. Find Releases
|
||||
- Use `git tag --list --sort=-version:refname` to see all version tags
|
||||
- Tags like `2.27.0`, `2.27.1` indicate releases
|
||||
- Note: Patch releases (x.x.1, x.x.2) might be on separate branches
|
||||
- Release dates available at: https://plugins.jetbrains.com/plugin/164-ideavim/versions
|
||||
|
||||
### 3. Review Changes
|
||||
```bash
|
||||
# Get commits since last documented version
|
||||
git log --oneline --since="YYYY-MM-DD" --first-parent master
|
||||
|
||||
# Get merged PRs
|
||||
gh pr list --state merged --limit 100 --json number,title,author,mergedAt
|
||||
|
||||
# Check specific release commits
|
||||
git log --oneline <previous-tag>..<new-tag>
|
||||
```
|
||||
|
||||
**Important**: Don't just read commit messages - examine the actual changes:
|
||||
- Use `git show <commit-hash>` to see the full commit content
|
||||
- Look at modified test files to find specific examples of fixed commands
|
||||
- Check the actual code changes to understand what was really fixed or added
|
||||
- Tests often contain the best examples for changelog entries (e.g., exact commands that now work)
|
||||
|
||||
### 4. What to Include
|
||||
- **Features**: New functionality with [VIM-XXXX] ticket numbers if available
|
||||
- **Bug Fixes**: Fixed issues with [VIM-XXXX] ticket references
|
||||
- **Breaking Changes**: Any backwards-incompatible changes
|
||||
- **Deprecations**: Features marked for future removal
|
||||
- **Merged PRs**: Reference significant PRs like "Implement vim-surround (#123)"
|
||||
- Note: PRs have their own inclusion rules - see "Merged PRs Special Rules" section below
|
||||
|
||||
### 5. What to Exclude
|
||||
- Dependabot PRs (author: dependabot[bot])
|
||||
- Claude-generated PRs (check PR author/title)
|
||||
- Internal refactoring with no user impact
|
||||
- Documentation-only changes (unless significant)
|
||||
- Test-only changes
|
||||
- **API module changes** (while in experimental status) - Do not log changes to the `api` module as it's currently experimental
|
||||
- Note: This exclusion should be removed once the API status is no longer experimental
|
||||
- **Vim Everywhere project** (including Hints toggle) - Do not log changes related to the Vim Everywhere project as it's not yet ready
|
||||
- **Internal code changes** - Do not log coding changes that users cannot see or experience
|
||||
- Refactoring, code cleanup, internal architecture changes
|
||||
- Performance optimizations (unless they fix a noticeable user issue)
|
||||
- Remember: The changelog is for users, not developers
|
||||
|
||||
## Writing Style
|
||||
|
||||
- **Be concise**: One line per change when possible
|
||||
- **User-focused**: Describe what changed from user's perspective
|
||||
- Write for end users, not developers
|
||||
- Focus on visible behavior changes, new commands, fixed issues users experience
|
||||
- Avoid technical implementation details
|
||||
- **Include examples** when helpful:
|
||||
- For fixes: Show the command/operation that now works correctly
|
||||
- For features: Demonstrate the new commands or functionality
|
||||
- Good example: "Fixed `ci"` command in empty strings" or "Added support for `gn` text object"
|
||||
- Bad examples (too vague, unclear what was broken):
|
||||
- "Fixed count validation in text objects"
|
||||
- "Fixed inlay offset calculations"
|
||||
- Better: Specify the actual case - "Fixed `3daw` deleting wrong number of words" or "Fixed cursor position with inlay hints in `f` motion"
|
||||
- **If you can't determine the specific case from tests/code, omit the entry rather than leave it unclear**
|
||||
- **Add helpful links** for context:
|
||||
- When mentioning IntelliJ features, search for official JetBrains documentation or blog posts
|
||||
- When referencing Vim commands, link to Vim documentation if helpful
|
||||
- Example: "Added support for [Next Edit Suggestion](https://blog.jetbrains.com/ai/2025/08/introducing-next-edit-suggestions-in-jetbrains-ai-assistant/)"
|
||||
- Use web search to find the most relevant official sources
|
||||
- **Include references**: Add [VIM-XXXX] for YouTrack tickets, (#XXX) for PRs
|
||||
- **Group logically**: Features, Fixes, Changes, Merged PRs
|
||||
- **No duplication**: Each change appears in exactly ONE subsection - don't repeat items across categories
|
||||
- **Use consistent tense**: Past tense for completed work
|
||||
|
||||
## Examples of Good Entries
|
||||
|
||||
```
|
||||
### Features:
|
||||
* Added support for `gn` text object - select next match with `gn`, change with `cgn`
|
||||
* Implemented `:tabmove` command - use `:tabmove +1` or `:tabmove -1` to reorder tabs
|
||||
* Support for `z=` to show spelling suggestions
|
||||
* Added integration with [Next Edit Suggestion](https://blog.jetbrains.com/ai/2025/08/introducing-next-edit-suggestions-in-jetbrains-ai-assistant/) feature
|
||||
* Support for [multiple cursors](https://www.jetbrains.com/help/idea/multicursor.html) in visual mode
|
||||
|
||||
### Fixes:
|
||||
* [VIM-3456](https://youtrack.jetbrains.com/issue/VIM-3456) Fixed cursor position after undo in visual mode
|
||||
* [VIM-3458](https://youtrack.jetbrains.com/issue/VIM-3458) Fixed `ci"` command now works correctly in empty strings
|
||||
* [VIM-3260](https://youtrack.jetbrains.com/issue/VIM-3260) Fixed `G` command at file end with count
|
||||
* [VIM-3180](https://youtrack.jetbrains.com/issue/VIM-3180) Fixed `vib` and `viB` selection in nested blocks
|
||||
|
||||
### Merged PRs:
|
||||
* [805](https://github.com/JetBrains/ideavim/pull/805) by [chylex](https://github.com/chylex): VIM-3238 Fix recording a macro that replays another macro
|
||||
```
|
||||
|
||||
## IMPORTANT Format Notes
|
||||
|
||||
### For Fixes:
|
||||
Always put the ticket link FIRST, then the description:
|
||||
```
|
||||
* [VIM-XXXX](https://youtrack.jetbrains.com/issue/VIM-XXXX) Description of what was fixed
|
||||
```
|
||||
|
||||
### For Features:
|
||||
- Without ticket: Just the description
|
||||
- With ticket: Can use either format:
|
||||
- Description with pipe: `* Feature description | [VIM-XXXX](https://youtrack.jetbrains.com/issue/VIM-XXXX)`
|
||||
- Link first (like fixes): `* [VIM-XXXX](https://youtrack.jetbrains.com/issue/VIM-XXXX) Feature description`
|
||||
|
||||
### Avoid Duplication:
|
||||
- **Each change should appear in only ONE subsection**
|
||||
- If a feature is listed in Features, don't repeat it in Fixes
|
||||
- If a bug fix is in Fixes, don't list it again elsewhere
|
||||
- Choose the most appropriate category for each change
|
||||
|
||||
### Merged PRs Special Rules:
|
||||
- **Different criteria than other sections**: The exclusion rules for Features/Fixes don't apply here
|
||||
- **Include PRs from external contributors** even if they're internal changes or refactoring
|
||||
- **List significant community contributions** regardless of whether they're user-visible
|
||||
- **Format**: PR number, author, and brief description
|
||||
- **Use PR title as-is**: Take the description directly from the PR title, don't regenerate or rewrite it
|
||||
- **Purpose**: Acknowledge community contributions and provide PR tracking
|
||||
- The "user-visible only" rule does NOT apply to this section
|
||||
|
||||
## Process
|
||||
|
||||
1. Read the current CHANGES.md (only the top portion - focus on `To Be Released` and recent versions)
|
||||
2. Check previous changelog PRs from GitHub:
|
||||
- Review the last few changelog update PRs (use `gh pr list --search "Update changelog" --state all --limit 5`)
|
||||
- **Read the PR comments**: Use `gh pr view <PR_NUMBER> --comments` to check for specific instructions
|
||||
- Look for any comments or instructions about what NOT to log this time
|
||||
- Previous PRs may contain specific exclusions or special handling instructions
|
||||
- Pay attention to review feedback that might indicate what to avoid in future updates
|
||||
3. Check git tags for any undocumented releases
|
||||
4. Review commits and PRs since last entry
|
||||
5. Group changes by release or under `To Be Released`
|
||||
6. Update CHANGES.md maintaining existing format
|
||||
7. Create a PR only if there are changes to document:
|
||||
- Title format: "Update changelog: <super short summary>"
|
||||
- Example: "Update changelog: Add gn text object, fix visual mode issues"
|
||||
- Body: Brief summary of what was added
|
||||
|
||||
`build.gradle.kts` `changeNotes` is auto-derived from CHANGES.md at build time via the `org.jetbrains.changelog` plugin — do not touch it.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Don't create a PR if changelog is already up to date**
|
||||
- **Preserve existing format and structure**
|
||||
- **Maintain chronological order (newest first)**
|
||||
- **Keep the historical gap note between 2.9.0 and 2.28.0**
|
||||
@@ -1,275 +0,0 @@
|
||||
---
|
||||
name: doc-sync
|
||||
description: Keeps IdeaVim documentation in sync with code changes. Use this skill when you need to verify documentation accuracy after code changes, or when checking if documentation (in doc/, README.md, CONTRIBUTING.md) matches the current codebase. The skill can work bidirectionally - from docs to code verification, or from code changes to documentation updates.
|
||||
---
|
||||
|
||||
# Doc Sync Skill
|
||||
|
||||
You are a documentation synchronization specialist for the IdeaVim project. Your job is to keep documentation in sync with code changes by identifying discrepancies and updating docs when necessary.
|
||||
|
||||
## Documentation Locations
|
||||
|
||||
The IdeaVim project has documentation in these locations:
|
||||
- `doc/` folder - Detailed documentation files
|
||||
- `README.md` - Main project README
|
||||
- `CONTRIBUTING.md` - Contribution guidelines
|
||||
|
||||
## Core Mindset
|
||||
|
||||
**CRITICAL:** After code changes, documentation is **GUILTY until proven innocent**.
|
||||
|
||||
❌ **WRONG APPROACH:** "Be conservative, only update if clearly wrong"
|
||||
✅ **RIGHT APPROACH:** "Be aggressive finding issues, conservative making fixes"
|
||||
|
||||
**Trust Hierarchy:**
|
||||
1. Working Implementation in codebase (highest truth)
|
||||
2. API Definition (interface/class)
|
||||
3. Documentation (assume outdated until verified)
|
||||
|
||||
## Phase 0: Pre-Analysis Search (DO THIS FIRST)
|
||||
|
||||
Before reading full files, run these quick searches to find red flags:
|
||||
|
||||
### 1. Find Working Examples (Ground Truth)
|
||||
```bash
|
||||
# Find real implementations
|
||||
grep -r '@VimPlugin\|@Plugin\|class.*Extension' --include="*.kt" | head -5
|
||||
|
||||
# Or search for known implementation patterns
|
||||
find . -name "*NewApi.kt" -o -name "*Example*.kt"
|
||||
```
|
||||
**Read at least ONE working implementation as ground truth.** This shows you what "correct" looks like.
|
||||
|
||||
### 2. Check Recent Breaking Changes
|
||||
```bash
|
||||
# Check recent commits to the changed files
|
||||
git log --oneline -10 -- '**/[ChangedFile]*'
|
||||
|
||||
# Look for removal commits
|
||||
git log --grep="remove\|deprecate\|incorrect" --oneline -10
|
||||
|
||||
# Check what was actually deleted (more important than additions!)
|
||||
git show [recent-commit] --stat
|
||||
```
|
||||
|
||||
### 3. Quick Pattern Search in Documentation
|
||||
```bash
|
||||
# Find all named parameters in code examples
|
||||
grep -E '\w+\s*=' doc/*.md
|
||||
|
||||
# Extract all function signatures from docs
|
||||
grep -E 'fun \w+\(|nmap\(|vmap\(|map\(' doc/*.md -B1 -A3
|
||||
```
|
||||
|
||||
Compare each signature/parameter against the actual API.
|
||||
|
||||
## Two Modes of Operation
|
||||
|
||||
### Mode A: Documentation → Code Verification
|
||||
Starting with documentation, verify that the code still matches what's documented.
|
||||
|
||||
**Steps:**
|
||||
0. **FIRST:** Find working implementation as ground truth (Phase 0)
|
||||
1. Read the specified documentation file(s)
|
||||
2. Extract ALL code examples and function signatures
|
||||
3. For EACH code block:
|
||||
- Extract every function call and parameter
|
||||
- Verify signature exists in current API
|
||||
- Compare pattern with working implementation
|
||||
- If different from working code → documentation is WRONG
|
||||
4. Update documentation if needed
|
||||
|
||||
### Mode B: Code Changes → Documentation Update
|
||||
Starting with code changes (e.g., from git diff), find related documentation and update if needed.
|
||||
|
||||
**Steps:**
|
||||
0. **FIRST:** Understand what was REMOVED (Phase 0 - check git show/diff)
|
||||
1. Read the changed files and git diff
|
||||
2. Understand what changed (especially deletions and breaking changes)
|
||||
3. Find working implementations that use the new API
|
||||
4. Search for documentation that references these files/features/APIs
|
||||
5. Extract all code examples from docs
|
||||
6. Compare each example against working implementation
|
||||
7. Update documentation to match the correct pattern
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
### When to Update
|
||||
✅ **DO update when:**
|
||||
- API signatures have changed (parameters added/removed/renamed)
|
||||
- Function/class/file names have been renamed
|
||||
- Behavior has fundamentally changed
|
||||
- Features have been removed or added
|
||||
- File paths in documentation are now incorrect
|
||||
- Code examples in docs no longer work
|
||||
|
||||
❌ **DON'T update when:**
|
||||
- Only internal implementation changed (not public API)
|
||||
- Wording could be slightly better but is still accurate
|
||||
- Minor formatting inconsistencies
|
||||
- Documentation uses slightly different terminology but conveys the same meaning
|
||||
- Changes are in test files that don't affect public API
|
||||
|
||||
### Update Strategy
|
||||
1. **Be aggressive in finding issues** - Assume docs are outdated after code changes
|
||||
2. **Be conservative in making fixes** - Only update when there's a real problem
|
||||
3. **Preserve style** - Match the existing documentation style
|
||||
4. **Be specific** - Don't make sweeping changes; target the specific issue
|
||||
5. **Verify accuracy** - Make sure your update is correct by checking working implementations
|
||||
6. **Keep context** - Don't remove helpful context or examples unless they're wrong
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
For EACH code block in documentation, verify:
|
||||
|
||||
- [ ] Extract the complete code example
|
||||
- [ ] Identify every function call with its parameters
|
||||
- [ ] For each function: Does this signature exist in current API?
|
||||
- [ ] For each parameter: Does this parameter name/type exist in API?
|
||||
- [ ] Does this pattern match the working implementation from codebase?
|
||||
- [ ] If different from working code → **Documentation is WRONG**
|
||||
- [ ] If parameters don't exist in API → **Documentation is WRONG**
|
||||
|
||||
## Workflow
|
||||
|
||||
When invoked, you should:
|
||||
|
||||
### Step 0: Establish Ground Truth (CRITICAL - DO FIRST)
|
||||
- **Find working implementations:** Search for @VimPlugin, real examples in codebase
|
||||
- **Check git history:** Run `git log -10` on changed files, look for "remove" commits
|
||||
- **Understand deletions:** Run `git show [commit]` to see what was removed
|
||||
- **Study working code:** Read at least 1-2 real implementations to understand correct patterns
|
||||
|
||||
### Step 1: Understand the Task
|
||||
- If given doc files: Mode A (verify docs match code)
|
||||
- If given code changes: Mode B (update docs to match code)
|
||||
- If given both: Check if the code changes affect the mentioned docs
|
||||
|
||||
### Step 2: Quick Pattern Search
|
||||
- Run grep searches from Phase 0 to find obvious red flags
|
||||
- Extract all function signatures from docs
|
||||
- Compare against API and working implementations
|
||||
|
||||
### Step 3: Detailed Verification
|
||||
- Read relevant documentation thoroughly
|
||||
- For EACH code example: Run through Verification Checklist
|
||||
- Compare every signature and parameter against actual API
|
||||
- Compare patterns against working implementations
|
||||
|
||||
### Step 4: Analyze Discrepancies
|
||||
- List what's different between docs and code
|
||||
- Assess severity (critical vs. minor)
|
||||
- Determine if update is needed
|
||||
- **Default to updating** when in doubt about code examples
|
||||
|
||||
### Step 5: Make Updates if Needed
|
||||
- Edit documentation files with precise changes
|
||||
- Explain what was changed and why
|
||||
- Verify the update matches working implementation
|
||||
|
||||
### Step 6: Report Findings
|
||||
- Summarize what was checked
|
||||
- List any discrepancies found
|
||||
- Describe what was updated (if anything)
|
||||
- Note anything that might need human review
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Example 1: Check specific documentation
|
||||
```
|
||||
User: "Check if doc/ideavim-mappings.md is in sync with the code"
|
||||
|
||||
You should:
|
||||
0. FIRST: Find working implementation (grep for @VimPlugin or similar)
|
||||
1. Read at least one working example to establish ground truth
|
||||
2. Read doc/ideavim-mappings.md
|
||||
3. Extract ALL code examples and function signatures
|
||||
4. For EACH signature: verify it exists in API and matches working code
|
||||
5. Compare patterns with working implementation
|
||||
6. Update docs if any discrepancies found
|
||||
```
|
||||
|
||||
### Example 2: Code changes → docs
|
||||
```
|
||||
User: "I changed MappingScope.kt, check if docs need updating"
|
||||
|
||||
You should:
|
||||
0. FIRST: Check git log and recent commits for MappingScope
|
||||
1. Run: git log --oneline -10 -- '**/MappingScope*'
|
||||
2. Check for removal commits: git log --grep="remove" --oneline -5
|
||||
3. If recent commits removed code: git show [commit] to see what was deleted
|
||||
4. Find working implementation that uses MappingScope correctly
|
||||
5. Read MappingScope.kt to understand current API
|
||||
6. Search docs for references to MappingScope, mapping functions, etc.
|
||||
7. Extract all code examples from docs
|
||||
8. Compare each example against working implementation
|
||||
9. Update docs to match the correct pattern
|
||||
```
|
||||
|
||||
### Example 3: Comprehensive check
|
||||
```
|
||||
User: "Check if all documentation in doc/ folder is up to date"
|
||||
|
||||
You should:
|
||||
0. FIRST: Find working implementations as ground truth
|
||||
1. Check recent git history for breaking changes
|
||||
2. List files in doc/ folder
|
||||
3. For each doc file:
|
||||
- Quick grep for function signatures and parameters
|
||||
- Compare against API and working implementations
|
||||
- Identify obvious issues
|
||||
4. For files with issues: run full Mode A verification
|
||||
5. Update any that need it
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Always provide a clear report:
|
||||
|
||||
```
|
||||
## Documentation Sync Report
|
||||
|
||||
### Files Checked
|
||||
- [doc file 1]
|
||||
- [doc file 2]
|
||||
- [code file 1]
|
||||
- [code file 2]
|
||||
|
||||
### Discrepancies Found
|
||||
1. **[Doc file]: [Issue description]**
|
||||
- Current docs say: [quote]
|
||||
- Actual code: [description]
|
||||
- Severity: [Critical/Minor]
|
||||
- Action: [Updated/No action needed]
|
||||
|
||||
### Updates Made
|
||||
- [File]: [Description of change]
|
||||
|
||||
### Notes
|
||||
- [Any observations or recommendations]
|
||||
```
|
||||
|
||||
## Tools Available
|
||||
|
||||
You have access to:
|
||||
- **Read**: Read any file in the project
|
||||
- **Edit**: Update documentation files
|
||||
- **Glob**: Find files by pattern
|
||||
- **Grep**: Search for text in files
|
||||
- **Bash**: Run git commands to see recent changes
|
||||
|
||||
## Key Lessons Learned
|
||||
|
||||
**Most Important Insights:**
|
||||
|
||||
1. **Start with working code, not documentation.** The working implementation is your ground truth. Documentation is assumed outdated until proven otherwise.
|
||||
|
||||
2. **Deletions matter more than additions.** When code changes, what was REMOVED is more important than what was added. Removed functions/parameters will break documentation examples.
|
||||
|
||||
3. **Verify every parameter name.** Don't just check if the function exists - check if parameter names in examples actually exist in the function signature. Named parameters in docs that don't exist in code are a critical bug.
|
||||
|
||||
4. **Compare patterns, not just signatures.** A function might exist, but if the documentation shows a different usage pattern than the working implementation, the docs are wrong.
|
||||
|
||||
5. **Git history tells the story.** Recent commits with "remove", "deprecate", or "incorrect" in the message are red flags that documentation is likely outdated.
|
||||
|
||||
Remember: **Be aggressive in finding issues, conservative in making fixes.** Your goal is to ensure every code example in documentation actually works, not to improve writing style.
|
||||
@@ -1,207 +0,0 @@
|
||||
---
|
||||
name: extensions-api-migration
|
||||
description: Migrates IdeaVim extensions from the old VimExtensionFacade API to the new @VimPlugin annotation-based API. Use when converting existing extensions to use the new API patterns.
|
||||
---
|
||||
|
||||
# Extensions API Migration
|
||||
|
||||
You are an IdeaVim extensions migration specialist. Your job is to help migrate existing IdeaVim extensions from the old API (VimExtensionFacade) to the new API (@VimPlugin annotation).
|
||||
|
||||
## Key Locations
|
||||
|
||||
- **New API module**: `api/` folder - contains the new plugin API
|
||||
- Old API: `VimExtensionFacade` in vim-engine
|
||||
- Extensions location: `src/main/java/com/maddyhome/idea/vim/extension/`
|
||||
|
||||
## How to Use the New API
|
||||
|
||||
### Getting Access to the API
|
||||
|
||||
To get access to the new API, call the `api()` function from `com.maddyhome.idea.vim.extension.api`:
|
||||
|
||||
```kotlin
|
||||
val api = api()
|
||||
```
|
||||
|
||||
Obtain the API at the start of the `init()` method - this is the entry point for all further work.
|
||||
|
||||
### Registering Text Objects
|
||||
|
||||
Use `api.textObjects { }` to register text objects:
|
||||
|
||||
```kotlin
|
||||
// From VimIndentObject.kt
|
||||
override fun init() {
|
||||
val api = api()
|
||||
api.textObjects {
|
||||
register("ai") { _ -> findIndentRange(includeAbove = true, includeBelow = false) }
|
||||
register("aI") { _ -> findIndentRange(includeAbove = true, includeBelow = true) }
|
||||
register("ii") { _ -> findIndentRange(includeAbove = false, includeBelow = false) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering Mappings
|
||||
|
||||
Use `api.mappings { }` to register mappings:
|
||||
|
||||
```kotlin
|
||||
// From ParagraphMotion.kt
|
||||
override fun init() {
|
||||
val api = api()
|
||||
|
||||
api.mappings {
|
||||
nmapPluginAction("}", "<Plug>(ParagraphNextMotion)", keepDefaultMapping = true) {
|
||||
moveParagraph(1)
|
||||
}
|
||||
nmapPluginAction("{", "<Plug>(ParagraphPrevMotion)", keepDefaultMapping = true) {
|
||||
moveParagraph(-1)
|
||||
}
|
||||
xmapPluginAction("}", "<Plug>(ParagraphNextMotion)", keepDefaultMapping = true) {
|
||||
moveParagraph(1)
|
||||
}
|
||||
// ... operator-pending mode mappings with omapPluginAction
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Defining Helper Functions
|
||||
|
||||
The lambdas in text object and mapping registrations typically call helper functions. Define these functions with `VimApi` as a receiver - this makes the API available inside:
|
||||
|
||||
```kotlin
|
||||
// From VimIndentObject.kt
|
||||
private fun VimApi.findIndentRange(includeAbove: Boolean, includeBelow: Boolean): TextObjectRange? {
|
||||
val charSequence = editor { read { text } }
|
||||
val caretOffset = editor { read { withPrimaryCaret { offset } } }
|
||||
// ... implementation using API
|
||||
}
|
||||
|
||||
// From ParagraphMotion.kt
|
||||
internal fun VimApi.moveParagraph(direction: Int) {
|
||||
val count = getVariable<Int>("v:count1") ?: 1
|
||||
editor {
|
||||
change {
|
||||
forEachCaret {
|
||||
val newOffset = getNextParagraphBoundOffset(actualCount, includeWhitespaceLines = true)
|
||||
if (newOffset != null) {
|
||||
updateCaret(offset = newOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Features
|
||||
|
||||
<!-- Fill in additional API features here -->
|
||||
|
||||
## How to Migrate Existing Extensions
|
||||
|
||||
### What Stays the Same
|
||||
|
||||
- The extension **still inherits VimExtensionFacade** - this does not change
|
||||
- The extension **still registers in the XML file** - this does not change
|
||||
|
||||
### Migration Steps
|
||||
|
||||
#### Step 1: Ensure Test Coverage
|
||||
|
||||
Before starting migration, make sure tests exist for the extension:
|
||||
- Tests should work and have good coverage
|
||||
- If there aren't enough tests, create more tests first
|
||||
- Verify tests pass on the existing version of the plugin
|
||||
|
||||
#### Step 2: Migrate in Small Steps
|
||||
|
||||
- Don't try to handle everything in one run
|
||||
- Run tests on the plugin (just the single test class to speed up things) after making smaller changes
|
||||
- This ensures consistency and makes it easier to identify issues
|
||||
- **Do a separate commit for each small sensible change or migration** unless explicitly told not to
|
||||
|
||||
#### Step 3: Migrate Handlers One by One
|
||||
|
||||
If the extension has multiple handlers, migrate them one at a time rather than all at once.
|
||||
|
||||
#### Step 4: Handler Migration Process
|
||||
|
||||
For each handler, follow this approach:
|
||||
|
||||
1. **Inject the API**: Add `val api = api()` as the first line inside the `execute` function
|
||||
|
||||
2. **Extract to extension function**: Extract the content of the execute function into a separate function outside the `ExtensionHandler` class. The new function should:
|
||||
- Have `VimApi` as a receiver
|
||||
- Use the api that was obtained before
|
||||
- Keep the extraction as-is (no changes to logic yet)
|
||||
|
||||
3. **Verify tests pass**: Run tests to ensure the extraction didn't break anything
|
||||
|
||||
4. **Migrate function content**: Now start migrating the content of the extracted function to use the new API
|
||||
|
||||
5. **Verify tests pass again**: Run tests after each significant change
|
||||
|
||||
6. **Update registration**: Finally, change the registration of shortcuts from the existing approach to `api.mappings { }` where you call the newly created function
|
||||
|
||||
#### Example Migration Flow
|
||||
|
||||
```kotlin
|
||||
// BEFORE: Old style handler
|
||||
class MyHandler : ExtensionHandler {
|
||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
||||
// ... implementation
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 1: Inject API
|
||||
class MyHandler : ExtensionHandler {
|
||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
||||
val api = api()
|
||||
// ... implementation
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 2: Extract to extension function (as-is)
|
||||
class MyHandler : ExtensionHandler {
|
||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
||||
val api = api()
|
||||
api.doMyAction(/* pass needed params */)
|
||||
}
|
||||
}
|
||||
|
||||
private fun VimApi.doMyAction(/* params */) {
|
||||
// ... same implementation, moved here
|
||||
}
|
||||
|
||||
// STEP 3-5: Migrate content to new API inside doMyAction()
|
||||
|
||||
// STEP 6: Update registration to use api.mappings { }
|
||||
override fun init() {
|
||||
val api = api()
|
||||
api.mappings {
|
||||
nmapPluginAction("key", "<Plug>(MyAction)") {
|
||||
doMyAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now MyHandler class can be removed
|
||||
```
|
||||
|
||||
#### Handling Complicated Plugins
|
||||
|
||||
For more complicated plugins, additional steps may be required.
|
||||
|
||||
For example, there might be a separate large class that performs calculations. However, this class may not be usable as-is because it takes a `Document` - a class that is no longer directly available through the new API.
|
||||
|
||||
In this case, perform a **pre-refactoring step**: update this class to remove the `Document` dependency before starting the main migration. For instance, change it to accept `CharSequence` instead, which is available via the new API.
|
||||
|
||||
#### Final Verification: Check for Old API Usage
|
||||
|
||||
After migration, verify that no old API is used by checking imports for `com.maddyhome`.
|
||||
|
||||
**Allowed imports** (these are still required):
|
||||
- `com.maddyhome.idea.vim.extension.VimExtension`
|
||||
- `com.maddyhome.idea.vim.extension.api`
|
||||
|
||||
Any other `com.maddyhome` imports indicate incomplete migration.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: git-workflow
|
||||
description: IdeaVim git workflow conventions covering commits, branches, PRs, and CI. Use when creating commits, managing branches, creating pull requests, reviewing git history, or any git-related activity in the IdeaVim project.
|
||||
---
|
||||
|
||||
# Git Workflow
|
||||
|
||||
## Branching
|
||||
|
||||
- **Master** is the trunk and MUST always be in a "ready to release" state
|
||||
- Use **feature branches** for development work
|
||||
- Naming: `VIM-XXXX/short-description` (e.g., `VIM-3948/editor`)
|
||||
- Rebase to master frequently to avoid large conflicts
|
||||
- Small, isolated changes (bug fixes, minor tweaks) MAY go directly to master
|
||||
- Unfinished changes MAY be committed to master only if they do NOT break functionality
|
||||
- Use **rebase** for integration, not merge commits (linear history)
|
||||
|
||||
## Commits
|
||||
|
||||
**Standard format:**
|
||||
```
|
||||
VIM-XXXX Description of the change
|
||||
```
|
||||
|
||||
- Start with the YouTrack ticket ID when the change relates to a ticket
|
||||
- Example: `VIM-3948 Traverse vertical panes in ConfigurableEditor`
|
||||
|
||||
**Auto-closing format** (moves YouTrack ticket to "Ready To Release"):
|
||||
```
|
||||
fix(VIM-XXXX): Description of the fix
|
||||
```
|
||||
|
||||
**Content rules:**
|
||||
- Each commit MUST contain a single, focused, meaningful change
|
||||
- MUST NOT include unrelated changes (formatting, unrelated refactoring)
|
||||
- Include appropriate tests with behavioral changes
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- PRs target `master`
|
||||
- CI runs standard tests automatically (`./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test`)
|
||||
- PRs from external contributors are listed in the changelog under "Merged PRs"
|
||||
|
||||
## Issue Tracking
|
||||
|
||||
- Use **YouTrack** (not GitHub Issues) - tickets are `VIM-XXXX`
|
||||
- URL: https://youtrack.jetbrains.com/issues/VIM
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
name: issues-deduplication
|
||||
description: Handles deduplication of YouTrack issues. Use when cleaning up duplicate issues, consolidating related bug reports, or organizing issue tracker.
|
||||
---
|
||||
|
||||
# Issues Deduplication
|
||||
|
||||
You are an issue tracker specialist for the IdeaVim project. Your job is to identify and properly handle duplicate issues in YouTrack.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Choosing Which Issue to Keep Open
|
||||
|
||||
**Default rule**: The older issue is typically kept open, and newer issues are marked as duplicates.
|
||||
|
||||
**Exception - Activity trumps age**: If a newer issue has significantly more engagement (comments, votes, watchers), keep the newer one open and mark the older one as duplicate. Consider:
|
||||
- Number of comments
|
||||
- Number of votes/thumbs-up
|
||||
- Number of watchers
|
||||
- Quality of discussion and information
|
||||
|
||||
### 2. Never Duplicate Issues with Customer Tags
|
||||
|
||||
**IMPORTANT**: Do not mark an issue as duplicate if it has a customer-related tag:
|
||||
- Tags like `Customer:XXX`
|
||||
- Company name tags like `Uber`, `Google`, `Meta`, etc.
|
||||
- Any tag indicating a specific customer reported or is affected by the issue
|
||||
|
||||
These issues need individual tracking for customer relationship purposes.
|
||||
|
||||
### 3. Closed Issue Warning
|
||||
|
||||
**CRITICAL**: Be very careful about duplicating into a closed issue!
|
||||
|
||||
Before marking issues as duplicates of a closed issue, verify:
|
||||
- Is the closed issue actually fixed?
|
||||
- Does the fix apply to all the duplicate reports?
|
||||
- Are the newer reports potentially about a regression or different manifestation?
|
||||
|
||||
**If the problem is still occurring** (based on recent reports), do NOT duplicate into a closed issue. Instead:
|
||||
- Reopen the closed issue, OR
|
||||
- Keep one of the open issues as the primary and duplicate into that
|
||||
|
||||
Duplicating active issues into a wrongly-closed issue will mark all related issues as "resolved" and lose track of an unresolved problem.
|
||||
|
||||
### 4. Consolidate to a Single Issue
|
||||
|
||||
When multiple issues are duplicates of each other (e.g., issues 1, 2, 3, 4, 5):
|
||||
- **DO**: Mark 2, 3, 4, 5 as duplicates of 1 (star topology)
|
||||
- **DON'T**: Create chains like 2→1, 3→2, 4→3, 5→4
|
||||
|
||||
This makes it easier to track all related reports from a single issue.
|
||||
|
||||
### 5. Preserve Unique Information
|
||||
|
||||
Before marking an issue as duplicate:
|
||||
1. Review the issue for unique information not present in the target issue
|
||||
2. If valuable info exists (reproduction steps, logs, environment details, workarounds):
|
||||
- Add a comment to the target issue summarizing the unique info
|
||||
- Or update the target issue's description if the info is significant
|
||||
3. Then mark as duplicate
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Gather Issue Details
|
||||
For each candidate issue, collect:
|
||||
- Issue ID and summary
|
||||
- Creation date
|
||||
- Number of comments
|
||||
- Number of votes
|
||||
- Tags (especially customer tags)
|
||||
- Current state (Open, Closed, etc.)
|
||||
- Key details from description
|
||||
|
||||
### Step 2: Group Duplicates
|
||||
Identify which issues are truly duplicates vs. related-but-different issues.
|
||||
|
||||
### Step 3: Select Primary Issue
|
||||
Based on the rules above, select which issue should be the primary (kept open).
|
||||
|
||||
### Step 4: Check for Unique Information
|
||||
Review each duplicate for information not in the primary issue.
|
||||
|
||||
### Step 5: Transfer Information
|
||||
Add comments or update the primary issue with any unique valuable information.
|
||||
|
||||
### Step 6: Mark Duplicates
|
||||
Use YouTrack to link issues as duplicates:
|
||||
- Add "duplicates" link from duplicate → primary
|
||||
- Update the issue state to "Duplicate"
|
||||
|
||||
### Step 7: Leave a Courteous Comment
|
||||
After marking an issue as duplicate, leave a comment on the duplicated issue to:
|
||||
- Inform the reporter about the merge
|
||||
- Direct them to the primary issue for updates
|
||||
- Thank them for their contribution
|
||||
|
||||
Example comment:
|
||||
> This issue has been merged into VIM-XXXX for easier tracking. Please follow that issue for updates. Thank you for your contribution!
|
||||
|
||||
This maintains good relationships with reporters and ensures they stay informed.
|
||||
|
||||
## YouTrack Operations
|
||||
|
||||
### Link as Duplicate
|
||||
Use `mcp__YouTrack__link_issues` with:
|
||||
- `issueId`: The duplicate issue
|
||||
- `targetIssueId`: The primary issue to duplicate into
|
||||
- `linkName`: "duplicates"
|
||||
|
||||
### Add Comment
|
||||
Use `mcp__YouTrack__add_issue_comment` to transfer unique information.
|
||||
|
||||
### Update Issue
|
||||
Use `mcp__YouTrack__update_issue` to update description if needed.
|
||||
|
||||
## Example Decision Matrix
|
||||
|
||||
| Scenario | Action |
|
||||
|----------------------------------------------------------------------------------|------------------------------------------------------------|
|
||||
| Old issue (2022), new issue (2024) with same problem, similar activity | Duplicate new → old |
|
||||
| Old issue (2022) with 2 comments, new issue (2024) with 15 comments and 10 votes | Duplicate old → new |
|
||||
| Issue has `Customer:Acme` tag | Never mark as duplicate |
|
||||
| Old issue closed as "Fixed", new reports say problem still exists | Keep new issue open, investigate if regression |
|
||||
| 5 issues about same bug | Pick best one as primary, duplicate all 4 others → primary |
|
||||
@@ -1,279 +0,0 @@
|
||||
---
|
||||
name: tests-maintenance
|
||||
description: Maintains IdeaVim test suite quality. Reviews disabled tests, ensures Neovim annotations are documented, and improves test readability. Use for periodic test maintenance.
|
||||
---
|
||||
|
||||
# Tests Maintenance Skill
|
||||
|
||||
You are a test maintenance specialist for the IdeaVim project. Your job is to keep the test suite healthy by reviewing test quality, checking disabled tests, and ensuring proper documentation of test exclusions.
|
||||
|
||||
## Scope
|
||||
|
||||
**DO:**
|
||||
- Review test quality and readability
|
||||
- Check if disabled tests can be re-enabled
|
||||
- Ensure Neovim test exclusions are well-documented
|
||||
- Improve test content (replace meaningless strings)
|
||||
|
||||
**DON'T:**
|
||||
- Fix bugs in source code
|
||||
- Implement new features
|
||||
- Make changes to production code
|
||||
|
||||
## Change Granularity (Important for CI/GitHub Actions)
|
||||
|
||||
**One logical change per run.** This ensures granular, reviewable Pull Requests.
|
||||
|
||||
**Rules:**
|
||||
1. **One test per run**: Focus on a single test file or test method
|
||||
2. **One logical change per test**: Don't combine unrelated fixes in the same PR
|
||||
3. **Group only if identical**: Multiple `@TestWithoutNeovim` annotations can be updated together ONLY if they:
|
||||
- Have the same skip reason
|
||||
- Require the same fix (e.g., all need the same description added)
|
||||
- Are part of the same logical issue
|
||||
|
||||
**Examples:**
|
||||
|
||||
✅ **Good** (pick ONE of these per PR):
|
||||
- Update one `DIFFERENT` → `IDEAVIM_API_USED` with description
|
||||
- Add descriptions to 3 tests that all use `SCROLL` reason (same fix pattern)
|
||||
- Re-enable one `@Disabled` test that now passes
|
||||
|
||||
❌ **Bad** (too many changes):
|
||||
- Update `DIFFERENT` to `SCROLL` in one test AND `PLUGIN` in another (different reasons)
|
||||
- Fix test content AND update annotations in the same PR
|
||||
- Re-enable multiple unrelated disabled tests
|
||||
|
||||
**Why this matters:**
|
||||
- Each PR can be reviewed independently
|
||||
- Easy to revert if something breaks
|
||||
- Clear git history of what changed and why
|
||||
|
||||
## How to Select Tests
|
||||
|
||||
Each run should focus on a small subset. Use one of these strategies:
|
||||
|
||||
```bash
|
||||
# Get a random test file
|
||||
find . -path "*/test/*" -name "*Test*.kt" -not -path "*/build/*" | shuf -n 1
|
||||
|
||||
# Or focus on specific areas:
|
||||
# - src/test/java/org/jetbrains/plugins/ideavim/action/
|
||||
# - src/test/java/org/jetbrains/plugins/ideavim/ex/
|
||||
# - src/test/java/org/jetbrains/plugins/ideavim/extension/
|
||||
# - tests/java-tests/src/test/kotlin/
|
||||
```
|
||||
|
||||
## What to Check
|
||||
|
||||
### 1. Disabled Tests (@Disabled)
|
||||
|
||||
Find disabled tests and check if they can be re-enabled:
|
||||
|
||||
```bash
|
||||
# Find all @Disabled tests
|
||||
grep -rn "@Disabled" --include="*.kt" src/test tests/
|
||||
```
|
||||
|
||||
For each disabled test:
|
||||
1. **Try running it**: `./gradlew test --tests "ClassName.testMethod"`
|
||||
2. **If it passes**: Investigate what changed, re-enable with explanation
|
||||
3. **If it fails**: Ensure reason is documented in @Disabled annotation
|
||||
4. **If obsolete**: Remove tests for features that no longer exist
|
||||
|
||||
### 2. Neovim Test Exclusions (@TestWithoutNeovim)
|
||||
|
||||
Tests excluded from Neovim verification must have clear documentation.
|
||||
|
||||
```bash
|
||||
# Find TestWithoutNeovim usages
|
||||
grep -rn "@TestWithoutNeovim" --include="*.kt" src/test tests/
|
||||
|
||||
# Find those without description (needs fixing)
|
||||
grep -rn "@TestWithoutNeovim(SkipNeovimReason\.[A-Z_]*)" --include="*.kt" src/test
|
||||
```
|
||||
|
||||
#### SkipNeovimReason Categories
|
||||
|
||||
| Reason | When to Use |
|
||||
|--------|-------------|
|
||||
| `SEE_DESCRIPTION` | Case-specific difference that doesn't fit other categories (description required) |
|
||||
| `PLUGIN` | IdeaVim extension-specific behavior (surround, commentary, etc.) |
|
||||
| `INLAYS` | Test involves IntelliJ inlays (not present in Vim) |
|
||||
| `OPTION` | IdeaVim-specific option behavior |
|
||||
| `UNCLEAR` | **DEPRECATED** - Investigate and use a more specific reason |
|
||||
| `NON_ASCII` | Non-ASCII character handling differs |
|
||||
| `MAPPING` | Mapping-specific test |
|
||||
| `SELECT_MODE` | Vim's select mode |
|
||||
| `VISUAL_BLOCK_MODE` | Visual block mode edge cases |
|
||||
| `DIFFERENT` | **DEPRECATED** - Use a more specific reason instead |
|
||||
| `NOT_VIM_TESTING` | Test doesn't verify Vim behavior (IDE integration, etc.) |
|
||||
| `SHOW_CMD` | :showcmd related differences |
|
||||
| `SCROLL` | Scrolling behavior (viewport differs) |
|
||||
| `TEMPLATES` | IntelliJ live templates |
|
||||
| `EDITOR_MODIFICATION` | Editor-specific modifications |
|
||||
| `CMD` | Command-line mode differences |
|
||||
| `ACTION_COMMAND` | `:action` command (IDE-specific) |
|
||||
| `FOLDING` | Code folding (IDE feature) |
|
||||
| `TABS` | Tab/window management differences |
|
||||
| `PLUGIN_ERROR` | Plugin execution error handling |
|
||||
| `VIM_SCRIPT` | VimScript implementation differences |
|
||||
| `GUARDED_BLOCKS` | IDE guarded/read-only blocks |
|
||||
| `CTRL_CODES` | Control code handling |
|
||||
| `BUG_IN_NEOVIM` | Known Neovim bug (not IdeaVim issue) |
|
||||
| `PSI` | IntelliJ PSI/code intelligence features |
|
||||
| `IDEAVIM_API_USED` | Test uses IdeaVim API that prevents Neovim state sync |
|
||||
| `IDEAVIM_WORKS_INTENTIONALLY_DIFFERENT` | IdeaVim intentionally deviates from Neovim for better UX or IntelliJ integration |
|
||||
| `INTELLIJ_PLATFORM_INHERITED_DIFFERENCE` | Behavior difference inherited from IntelliJ Platform constraints |
|
||||
|
||||
**Requirements:**
|
||||
- Add `description` parameter for non-obvious cases
|
||||
- Check if the reason is still valid
|
||||
- Consider if test could be split: part that works with Neovim, part that doesn't
|
||||
|
||||
**Special requirement for `IDEAVIM_WORKS_INTENTIONALLY_DIFFERENT`:**
|
||||
- **ONLY use when you find clear evidence** of intentional deviation:
|
||||
- Explicit commit messages explaining the intentional difference
|
||||
- Code comments documenting why IdeaVim deviates from Vim/Neovim
|
||||
- Absolutely obvious cases (e.g., IntelliJ-specific features not in Neovim)
|
||||
- **DO NOT use based on guesswork or assumptions**
|
||||
- If uncertain, use `DIFFERENT` or `UNCLEAR` instead and investigate git history/comments
|
||||
- The `description` parameter is **mandatory** and must explain what exactly differs and why
|
||||
|
||||
**Special requirement for `INTELLIJ_PLATFORM_INHERITED_DIFFERENCE`:**
|
||||
- Use when behavior difference is due to IntelliJ Platform's underlying implementation
|
||||
- Common cases include:
|
||||
- Empty buffer handling (Platform editors can be empty, Neovim buffers always have a newline)
|
||||
- Position/offset calculations for newline characters
|
||||
- Line/column indexing differences
|
||||
- The `description` parameter is **mandatory** and must explain:
|
||||
- What Platform behavior causes the difference
|
||||
- How it manifests in the test
|
||||
- Evidence can be found in Platform API documentation, IdeaVim code comments, or obvious Platform limitations
|
||||
|
||||
**Special requirement for `SEE_DESCRIPTION`:**
|
||||
- Use as a last resort when the difference doesn't fit any standard category
|
||||
- The `description` parameter is **mandatory** and must provide a clear, specific explanation
|
||||
- Use sparingly - if multiple tests share similar reasons, consider creating a new dedicated reason
|
||||
- Always check existing reasons first before using this catch-all
|
||||
|
||||
**Handling `DIFFERENT` and `UNCLEAR` (DEPRECATED):**
|
||||
|
||||
Both `DIFFERENT` and `UNCLEAR` reasons are deprecated because they're too vague. When you encounter a test with either of these reasons, follow this process:
|
||||
|
||||
1. **First, try removing the annotation and running with Neovim:**
|
||||
```bash
|
||||
# Comment out or remove @TestWithoutNeovim, then run:
|
||||
./gradlew test -Dnvim --tests "ClassName.testMethodName"
|
||||
```
|
||||
|
||||
**IMPORTANT:** Verify the output contains `NEOVIM TESTING ENABLED` to confirm Neovim testing is active.
|
||||
If this message is not present, the test ran without Neovim verification.
|
||||
|
||||
2. **If the test passes with Neovim:**
|
||||
- The annotation is outdated and should be removed
|
||||
- IdeaVim and Neovim now behave identically for this case
|
||||
|
||||
3. **If the test fails with Neovim:**
|
||||
- Analyze the failure to understand WHY the behavior differs
|
||||
- Replace `DIFFERENT` with a more specific reason:
|
||||
- `IDEAVIM_API_USED` - if test uses VimPlugin.* or injector.* APIs directly
|
||||
- `IDEAVIM_WORKS_INTENTIONALLY_DIFFERENT` - if IdeaVim intentionally deviates (need evidence)
|
||||
- `INTELLIJ_PLATFORM_INHERITED_DIFFERENCE` - if difference comes from Platform constraints
|
||||
- `SEE_DESCRIPTION` - for unique cases that don't fit other categories (description required)
|
||||
- Or another appropriate reason from the table above
|
||||
- Always add a `description` parameter explaining the specific difference
|
||||
|
||||
### 3. Test Quality & Readability
|
||||
|
||||
**Meaningful test content**: Avoid senseless text. Look for:
|
||||
```bash
|
||||
grep -rn "asdf\|qwerty\|xxxxx\|aaaaa\|dhjkw" --include="*.kt" src/test tests/
|
||||
```
|
||||
|
||||
Replace with:
|
||||
- Actual code snippets relevant to the test
|
||||
- Lorem Ipsum template from CONTRIBUTING.md
|
||||
- Realistic text demonstrating the feature
|
||||
|
||||
**Test naming**: Names should explain what's being tested.
|
||||
|
||||
### 4. @VimBehaviorDiffers Annotation
|
||||
|
||||
Tests marked with this document intentional differences from Vim:
|
||||
|
||||
```kotlin
|
||||
@VimBehaviorDiffers(
|
||||
originalVimAfter = "expected vim result",
|
||||
description = "why IdeaVim differs",
|
||||
shouldBeFixed = true/false
|
||||
)
|
||||
```
|
||||
|
||||
Check:
|
||||
- Is the difference still valid?
|
||||
- If `shouldBeFixed = true`, is there a YouTrack issue?
|
||||
- Can behavior now be aligned with Vim?
|
||||
|
||||
## Making Changes
|
||||
|
||||
### When to Change
|
||||
|
||||
**DO fix:**
|
||||
- Unclear or missing test descriptions
|
||||
- Senseless test content
|
||||
- Disabled tests that now pass
|
||||
- Incorrect `@TestWithoutNeovim` reasons
|
||||
- Missing `description` on annotations
|
||||
|
||||
**DON'T:**
|
||||
- Fix source code bugs
|
||||
- Implement missing features
|
||||
- Major refactoring without clear benefit
|
||||
|
||||
### Commit Messages
|
||||
|
||||
```
|
||||
tests: Re-enable DeleteMotionTest after fix in #1234
|
||||
|
||||
The test was disabled due to a caret positioning bug that was
|
||||
fixed in commit abc123. Verified the test passes consistently.
|
||||
```
|
||||
|
||||
```
|
||||
tests: Improve test content readability in ChangeActionTest
|
||||
|
||||
Replace meaningless "asdfgh" strings with realistic code snippets
|
||||
that better demonstrate the change operation behavior.
|
||||
```
|
||||
|
||||
```
|
||||
tests: Document @TestWithoutNeovim reasons in ScrollTest
|
||||
|
||||
Added description parameter to clarify why scroll tests
|
||||
are excluded from Neovim verification (viewport behavior differs).
|
||||
```
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Run specific test
|
||||
./gradlew test --tests "ClassName.testMethod"
|
||||
|
||||
# Run all tests in a class
|
||||
./gradlew test --tests "ClassName"
|
||||
|
||||
# Run tests with Neovim verification (look for "NEOVIM TESTING ENABLED" in output)
|
||||
./gradlew test -Dnvim --tests "ClassName"
|
||||
|
||||
# Standard test suite (excludes property and long-running)
|
||||
./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
When run via workflow, if changes are made, create a PR with:
|
||||
- **Title**: "Tests maintenance: <brief description>"
|
||||
- **Body**: What was checked, issues found, changes made
|
||||
|
||||
If no changes needed, report what was checked and that everything is fine.
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -1,5 +1,10 @@
|
||||
name: Prepare What's New with Claude
|
||||
|
||||
# The PR goes to master so that master keeps every page and the full changelog, but the page itself
|
||||
# describes the `release` branch: the release is published from that branch as is (only the EAP build
|
||||
# moves it to master), so changes that are only on master are not part of the upcoming release. The
|
||||
# TeamCity release build takes the merged whatsnew-tbr.html from master and promotes it.
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manually triggered by a maintainer
|
||||
|
||||
@@ -21,6 +26,22 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # Full history so the changelog can be analyzed and the PR branch pushed
|
||||
|
||||
- name: Fetch the release branch
|
||||
id: release
|
||||
run: |
|
||||
git fetch origin release:refs/remotes/origin/release
|
||||
echo "commit=$(git rev-parse origin/release)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
# Claude runs prepareReleaseChangelog.ts to see which changelog entries the release contains
|
||||
- name: Install script dependencies
|
||||
run: npm ci --no-fund --no-audit
|
||||
working-directory: scripts-ts
|
||||
|
||||
# Tells apart "the JWT expired" from "the quota is gone" BEFORE Claude runs -
|
||||
# claude-code-action reports both as a bare `result is_error:true`.
|
||||
- name: Check Claude credentials and quota
|
||||
@@ -63,23 +84,34 @@ jobs:
|
||||
gone, so USUALLY it does NOT exist when this workflow runs — only the version-specific
|
||||
pages for older releases (`whatsnew-<version>.html`) remain.
|
||||
|
||||
Read the now-updated `## [To Be Released]` section of `CHANGES.md` (everything between
|
||||
that heading and the next dated release heading, e.g. `## 2.39.0, ...`). Use only its
|
||||
Features and Fixes; you may skip Merged PRs.
|
||||
### Describe only what the release will contain
|
||||
The release is built from the `origin/release` branch, which may be BEHIND master, so not
|
||||
everything under `## [To Be Released]` in `CHANGES.md` is part of it. After Step 1, run
|
||||
exactly this command (do not work it out from `git log` yourself):
|
||||
|
||||
scripts-ts/node_modules/.bin/tsx scripts-ts/src/prepareReleaseChangelog.ts --print . origin/release
|
||||
|
||||
It prints the `## To Be Released` section the release will be published with - the same one
|
||||
the release build puts in the plugin. Below, "the release section" means this output. Use
|
||||
only its Features and Fixes; you may skip Merged PRs. The entries it leaves out stay in
|
||||
`CHANGES.md`; they will be on the page of the next release. Run it again if you change
|
||||
`CHANGES.md` afterwards.
|
||||
Leave the `<!-- whatsnew-release-commit: ... -->` line at the top of the page as it is
|
||||
(or do not add one): this workflow records there which release commit the page describes.
|
||||
|
||||
First decide what to do by checking the working tree:
|
||||
- List `src/main/resources/whatsnew-*.html`.
|
||||
- **If `whatsnew-tbr.html` does NOT exist (the normal case):** create it fresh,
|
||||
describing every Feature and Fix currently under `## [To Be Released]`.
|
||||
describing every Feature and Fix in the release section.
|
||||
- **If `whatsnew-tbr.html` ALREADY exists (it was generated earlier this cycle):**
|
||||
do NOT rebuild it from scratch. Diff it against the current `## [To Be Released]`
|
||||
section — work out which changelog entries are NOT yet represented on the page — and
|
||||
do NOT rebuild it from scratch. Diff it against the release section — work out which
|
||||
changelog entries are NOT yet represented on the page — and
|
||||
update the page to incorporate ONLY those new/changed items, preserving the existing
|
||||
sections, ordering and wording already present. This is an incremental update, not a
|
||||
rewrite. (If nothing in the changelog is new relative to the page, make no changes
|
||||
and do not open a PR.)
|
||||
|
||||
- If, after Step 1, `## [To Be Released]` still has no Features or Fixes, STOP:
|
||||
- If the release section has no Features or Fixes, STOP:
|
||||
do not create or change the HTML and do not open a PR. Explain why in your final message.
|
||||
|
||||
### Style — match the existing pages EXACTLY
|
||||
@@ -121,7 +153,48 @@ jobs:
|
||||
- Body: a short summary of the changelog entries added and the What's New highlights.
|
||||
|
||||
# Changelog needs the skill + git/gh/web; the page generation needs file read/write.
|
||||
claude_args: '--allowed-tools "Skill,Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*),WebSearch,WebFetch(domain:plugins.jetbrains.com),WebFetch(domain:youtrack.jetbrains.com),WebFetch(domain:github.com)"'
|
||||
claude_args: '--allowed-tools "Skill,Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(scripts-ts/node_modules/.bin/tsx scripts-ts/src/prepareReleaseChangelog.ts:*),WebSearch,WebFetch(domain:plugins.jetbrains.com),WebFetch(domain:youtrack.jetbrains.com),WebFetch(domain:github.com)"'
|
||||
|
||||
# The release build refuses a page written for another commit of the release branch: an EAP build or a
|
||||
# cherry-pick after this run changes what the release contains. So the commit is recorded even when
|
||||
# the page itself needed no changes - then in a PR of its own.
|
||||
- name: Record the release commit the page describes
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_COMMIT: ${{ steps.release.outputs.commit }}
|
||||
run: |
|
||||
set -e
|
||||
tbr="src/main/resources/whatsnew-tbr.html"
|
||||
marker="<!-- whatsnew-release-commit: $RELEASE_COMMIT -->"
|
||||
git config user.name "IdeaVim Bot"
|
||||
git config user.email "maintainers@ideavim.dev"
|
||||
git fetch origin master
|
||||
pr=$(gh pr list --head whatsnew-tbr --state open --json number --jq '.[0].number // empty')
|
||||
if [ -n "$pr" ]; then
|
||||
git fetch origin whatsnew-tbr
|
||||
git checkout -f -B whatsnew-tbr origin/whatsnew-tbr
|
||||
elif git cat-file -e "origin/master:$tbr" 2>/dev/null; then
|
||||
git checkout -f -B whatsnew-tbr origin/master
|
||||
else
|
||||
echo "No What's New page to record the release commit in"
|
||||
exit 0
|
||||
fi
|
||||
if grep -qxF "$marker" "$tbr"; then
|
||||
echo "The page already describes release commit $RELEASE_COMMIT"
|
||||
exit 0
|
||||
fi
|
||||
sed -i '/^<!-- whatsnew-release-commit: .* -->$/d' "$tbr"
|
||||
sed -i "1i $marker" "$tbr"
|
||||
git commit -qam "Record release commit $RELEASE_COMMIT for What's New"
|
||||
if [ -n "$pr" ]; then
|
||||
git push origin whatsnew-tbr
|
||||
echo "Recorded release commit $RELEASE_COMMIT in PR #$pr"
|
||||
else
|
||||
git push -f origin whatsnew-tbr
|
||||
gh pr create --base master --head whatsnew-tbr \
|
||||
--title "Prepare What's New for the upcoming release" \
|
||||
--body "The release branch moved to $RELEASE_COMMIT, and the What's New page needed no changes for it. This only records the new commit, so the release build accepts the page."
|
||||
fi
|
||||
|
||||
# Decodes the session log that the action writes but never prints.
|
||||
- name: Claude run post-mortem
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
name: Run UI Rider Tests macOS
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
jobs:
|
||||
build-for-ui-test-mac-os:
|
||||
if: github.repository == 'JetBrains/ideavim'
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 21
|
||||
- name: Setup FFmpeg
|
||||
run: brew install ffmpeg
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: false
|
||||
- name: Build Plugin
|
||||
run: gradle :buildPlugin
|
||||
- name: Run Idea
|
||||
run: |
|
||||
mkdir -p build/reports
|
||||
gradle :runIdeForUiTests -PideaType=RD > build/reports/idea.log &
|
||||
- name: List available capture devices
|
||||
run: ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true
|
||||
continue-on-error: true
|
||||
- name: Start screen recording
|
||||
run: |
|
||||
mkdir -p build/reports/ci-screen-recording
|
||||
ffmpeg -f avfoundation -capture_cursor 1 -i "0:none" -r 30 -vcodec libx264 -pix_fmt yuv420p build/reports/ci-screen-recording/screen-recording.mp4 &
|
||||
echo $! > /tmp/ffmpeg_pid.txt
|
||||
continue-on-error: true
|
||||
- name: Auto-click Allow button for screen recording permission
|
||||
run: |
|
||||
sleep 3
|
||||
brew install cliclick || true
|
||||
|
||||
for coords in "512:367" "960:540" "640:400" "800:450"; do
|
||||
x=$(echo $coords | cut -d: -f1)
|
||||
y=$(echo $coords | cut -d: -f2)
|
||||
echo "Trying coordinates: $x,$y"
|
||||
|
||||
cliclick c:$x,$y 2>/dev/null && echo "cliclick succeeded" && break
|
||||
sleep 0.5
|
||||
|
||||
osascript -e "tell application \"System Events\" to click at {$x, $y}" 2>/dev/null && echo "AppleScript succeeded" && break
|
||||
sleep 1
|
||||
done
|
||||
continue-on-error: true
|
||||
- name: Wait for Idea started
|
||||
uses: jtalk/url-health-check-action@v3
|
||||
with:
|
||||
url: http://127.0.0.1:8082
|
||||
max-attempts: 100
|
||||
retry-delay: 10s
|
||||
- name: Tests
|
||||
run: gradle :tests:ui-rd-tests:testUi
|
||||
env:
|
||||
RIDER_LICENSE: ${{ secrets.RIDER_LICENSE }}
|
||||
- name: Stop screen recording
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f /tmp/ffmpeg_pid.txt ]; then
|
||||
kill $(cat /tmp/ffmpeg_pid.txt) || true
|
||||
sleep 2
|
||||
fi
|
||||
continue-on-error: true
|
||||
- name: Move sandbox logs
|
||||
if: always()
|
||||
run: mv build/idea-sandbox/RD-*/log_runIdeForUiTests idea-sandbox-log
|
||||
- name: AI Analysis of Test Failures
|
||||
if: failure()
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: ${{ secrets.PROXY_URL }}
|
||||
ANTHROPIC_CUSTOM_HEADERS: |
|
||||
Grazie-Agent: {"name":"ideavim-claude-code-action","version":"github-actions"}
|
||||
Grazie-Authenticate-JWT: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: '--allowed-tools "Read,Write,Edit,Glob,Grep,Bash(ffmpeg:*),Bash(ffprobe:*),Bash(mkdir:*),Bash(touch:*),Bash(echo:*),Bash(ls:*),Bash(cat:*),Bash(grep:*),Bash(find:*),Bash(cd:*),Bash(rm:*),Bash(git:*),Bash(gh:*),Bash(gradle:*),Bash(./gradlew:*),Bash(java:*),Bash(which:*)"'
|
||||
prompt: |
|
||||
## Task: Analyze UI Test Failures
|
||||
|
||||
Please analyze the UI test failures in the current directory.
|
||||
|
||||
Key information:
|
||||
- Test reports are located in: build/reports and tests/ui-rd-tests/build/reports
|
||||
- There is a CI screen recording at build/reports/ci-screen-recording/screen-recording.mp4 that shows what happened during the entire test run - this video is usually very useful for understanding what went wrong visually
|
||||
- There is also a single screenshot at tests/ui-rd-tests/build/reports/ideaVimTest.png showing the state when the test failed
|
||||
- IDE sandbox logs are in the idea-sandbox-log directory
|
||||
- ffmpeg is already installed and available. Useful commands for video analysis:
|
||||
* Extract frame at specific time: `ffmpeg -i video.mp4 -ss 00:01:30 -vframes 1 output.png`
|
||||
* Extract multiple frames: `ffmpeg -i video.mp4 -vf fps=1/10 frame_%04d.png` (1 frame every 10 seconds)
|
||||
* Get video duration: `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video.mp4`
|
||||
* Extract last N seconds: `ffmpeg -sseof -10 -i video.mp4 -update 1 last_frame.png` (last 10 seconds)
|
||||
* Create thumbnail grid: `ffmpeg -i video.mp4 -vf "select='not(mod(n\,300))',scale=160:120,tile=5x5" -frames:v 1 grid.png`
|
||||
|
||||
Special troubleshooting for timeout failures:
|
||||
- If the test fails because it waited for something to appear, but according to the video and screenshot the element actually appeared on screen, check the UI hierarchy file at build/reports/hierarchy-ideaVimTest.html
|
||||
- This hierarchy file shows the actual structure of UI elements and their properties at the time of failure
|
||||
- The failure may be caused by a renamed property or changed class name in the UI element
|
||||
- If you find this is the case, suggest a new query or selector that matches the current element structure
|
||||
|
||||
Please provide:
|
||||
1. A detailed analysis of what went wrong
|
||||
2. The root cause of the failure
|
||||
3. Potential fixes or suggestions
|
||||
|
||||
Write your analysis to build/reports/ai-analysis.txt
|
||||
|
||||
## UI Test Best Practices
|
||||
|
||||
When fixing UI tests, follow these principles:
|
||||
|
||||
**Cause-Effect Over Timeouts**: UI tests should wait for specific conditions rather than arbitrary timeouts.
|
||||
- ✅ GOOD: Wait for a button to become visible or enabled before clicking it
|
||||
- ✅ GOOD: Wait for specific text to appear in a component
|
||||
- ✅ GOOD: Wait for a dialog to be present with a specific accessible name
|
||||
- ❌ BAD: Use Thread.sleep() or fixed delays
|
||||
- ❌ BAD: Wait for arbitrary timeouts without checking for specific conditions
|
||||
|
||||
Only use timeouts as a maximum wait duration with explicit condition checks (e.g., "wait up to 30 seconds for dialog to appear").
|
||||
Tests should be deterministic and based on observable state changes in the UI, not time-based assumptions.
|
||||
|
||||
IMPORTANT: If you have a concrete suggestion for fixing the test, ALWAYS proceed with creating a branch and PR. Never ask for permission - just do it.
|
||||
|
||||
If you have a concrete suggestion for fixing the test:
|
||||
1. Create a new branch with a descriptive name (e.g., fix/ui-test-accessible-name)
|
||||
2. Apply your suggested fix to the codebase
|
||||
3. CRITICAL: When staging changes, NEVER use `git add -A` or `git add .`. Always add modified files explicitly by path (e.g., `git add path/to/file.kt path/to/other.kt`). This prevents accidentally staging unrelated files.
|
||||
4. MANDATORY: Verify compilation succeeds with your changes: `gradle compileKotlin compileTestKotlin`
|
||||
5. MANDATORY: Run the specific failing test to verify the fix improves or resolves the issue
|
||||
- For Rider UI tests: `gradle :tests:ui-rd-tests:testUi --tests "YourTestClassName.yourTestMethod"`
|
||||
- To run all Rider UI tests: `gradle :tests:ui-rd-tests:testUi`
|
||||
- The test MUST either pass completely or show clear improvement (e.g., progressing further before failure)
|
||||
6. If the test passes or shows improvement with your fix, create a PR with:
|
||||
- Clear title describing the fix
|
||||
- Description explaining the root cause and solution
|
||||
- Test results showing the fix works
|
||||
- Reference to the failing CI run
|
||||
7. Use the base branch 'master' for the PR
|
||||
- name: Save report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-test-fails-report-mac
|
||||
path: |
|
||||
build/reports
|
||||
tests/ui-rd-tests/build/reports
|
||||
idea-sandbox-log
|
||||
@@ -1,153 +0,0 @@
|
||||
name: Run UI Rider Tests Linux
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
jobs:
|
||||
build-for-ui-test-linux:
|
||||
if: github.repository == 'JetBrains/ideavim'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo docker image prune --all --force
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 21
|
||||
- name: Setup FFmpeg
|
||||
run: sudo apt-get update && sudo apt-get install -y ffmpeg
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- name: Build Plugin
|
||||
run: gradle :buildPlugin --configuration-cache
|
||||
- name: Start Xvfb
|
||||
run: |
|
||||
export DISPLAY=:99.0
|
||||
Xvfb :99 -screen 0 1280x720x24 &
|
||||
echo "DISPLAY=:99.0" >> $GITHUB_ENV
|
||||
- name: Run Idea
|
||||
run: |
|
||||
mkdir -p build/reports
|
||||
gradle :runIdeForUiTests -PideaType=RD > build/reports/idea.log 2>&1 &
|
||||
- name: Start screen recording
|
||||
run: |
|
||||
mkdir -p build/reports/ci-screen-recording
|
||||
ffmpeg -f x11grab -video_size 1280x720 -i :99.0 -r 15 -vcodec libx264 -preset ultrafast -crf 28 -pix_fmt yuv420p build/reports/ci-screen-recording/screen-recording.mp4 &
|
||||
echo $! > /tmp/ffmpeg_pid.txt
|
||||
continue-on-error: true
|
||||
- name: Wait for Idea started
|
||||
uses: jtalk/url-health-check-action@v3
|
||||
with:
|
||||
url: http://127.0.0.1:8082
|
||||
max-attempts: 100
|
||||
retry-delay: 10s
|
||||
- name: Tests
|
||||
run: gradle :tests:ui-rd-tests:testUi
|
||||
env:
|
||||
RIDER_LICENSE: ${{ secrets.RIDER_LICENSE }}
|
||||
- name: Stop screen recording
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f /tmp/ffmpeg_pid.txt ]; then
|
||||
kill $(cat /tmp/ffmpeg_pid.txt) || true
|
||||
sleep 2
|
||||
fi
|
||||
continue-on-error: true
|
||||
- name: Move sandbox logs
|
||||
if: always()
|
||||
run: mv build/idea-sandbox/RD-*/log_runIdeForUiTests idea-sandbox-log
|
||||
- name: AI Analysis of Test Failures
|
||||
if: failure()
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: ${{ secrets.PROXY_URL }}
|
||||
ANTHROPIC_CUSTOM_HEADERS: |
|
||||
Grazie-Agent: {"name":"ideavim-claude-code-action","version":"github-actions"}
|
||||
Grazie-Authenticate-JWT: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: '--allowed-tools "Read,Write,Edit,Glob,Grep,Bash(ffmpeg:*),Bash(ffprobe:*),Bash(mkdir:*),Bash(touch:*),Bash(echo:*),Bash(ls:*),Bash(cat:*),Bash(grep:*),Bash(find:*),Bash(cd:*),Bash(rm:*),Bash(git:*),Bash(gh:*),Bash(gradle:*),Bash(./gradlew:*),Bash(java:*),Bash(which:*)"'
|
||||
prompt: |
|
||||
## Task: Analyze UI Test Failures
|
||||
|
||||
Please analyze the UI test failures in the current directory.
|
||||
|
||||
Key information:
|
||||
- Test reports are located in: build/reports and tests/ui-rd-tests/build/reports
|
||||
- There is a CI screen recording at build/reports/ci-screen-recording/screen-recording.mp4 that shows what happened during the entire test run - this video is usually very useful for understanding what went wrong visually
|
||||
- There is also a single screenshot at tests/ui-rd-tests/build/reports/ideaVimTest.png showing the state when the test failed
|
||||
- IDE sandbox logs are in the idea-sandbox-log directory
|
||||
- ffmpeg is already installed and available. Useful commands for video analysis:
|
||||
* Extract frame at specific time: `ffmpeg -i video.mp4 -ss 00:01:30 -vframes 1 output.png`
|
||||
* Extract multiple frames: `ffmpeg -i video.mp4 -vf fps=1/10 frame_%04d.png` (1 frame every 10 seconds)
|
||||
* Get video duration: `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video.mp4`
|
||||
* Extract last N seconds: `ffmpeg -sseof -10 -i video.mp4 -update 1 last_frame.png` (last 10 seconds)
|
||||
* Create thumbnail grid: `ffmpeg -i video.mp4 -vf "select='not(mod(n\,300))',scale=160:120,tile=5x5" -frames:v 1 grid.png`
|
||||
|
||||
Special troubleshooting for timeout failures:
|
||||
- If the test fails because it waited for something to appear, but according to the video and screenshot the element actually appeared on screen, check the UI hierarchy file at build/reports/hierarchy-ideaVimTest.html
|
||||
- This hierarchy file shows the actual structure of UI elements and their properties at the time of failure
|
||||
- The failure may be caused by a renamed property or changed class name in the UI element
|
||||
- If you find this is the case, suggest a new query or selector that matches the current element structure
|
||||
|
||||
Please provide:
|
||||
1. A detailed analysis of what went wrong
|
||||
2. The root cause of the failure
|
||||
3. Potential fixes or suggestions
|
||||
|
||||
Write your analysis to build/reports/ai-analysis.txt
|
||||
|
||||
## UI Test Best Practices
|
||||
|
||||
When fixing UI tests, follow these principles:
|
||||
|
||||
**Cause-Effect Over Timeouts**: UI tests should wait for specific conditions rather than arbitrary timeouts.
|
||||
- ✅ GOOD: Wait for a button to become visible or enabled before clicking it
|
||||
- ✅ GOOD: Wait for specific text to appear in a component
|
||||
- ✅ GOOD: Wait for a dialog to be present with a specific accessible name
|
||||
- ❌ BAD: Use Thread.sleep() or fixed delays
|
||||
- ❌ BAD: Wait for arbitrary timeouts without checking for specific conditions
|
||||
|
||||
Only use timeouts as a maximum wait duration with explicit condition checks (e.g., "wait up to 30 seconds for dialog to appear").
|
||||
Tests should be deterministic and based on observable state changes in the UI, not time-based assumptions.
|
||||
|
||||
IMPORTANT: If you have a concrete suggestion for fixing the test, ALWAYS proceed with creating a branch and PR. Never ask for permission - just do it.
|
||||
|
||||
If you have a concrete suggestion for fixing the test:
|
||||
1. Create a new branch with a descriptive name (e.g., fix/ui-test-accessible-name)
|
||||
2. Apply your suggested fix to the codebase
|
||||
3. CRITICAL: When staging changes, NEVER use `git add -A` or `git add .`. Always add modified files explicitly by path (e.g., `git add path/to/file.kt path/to/other.kt`). This prevents accidentally staging unrelated files.
|
||||
4. MANDATORY: Verify compilation succeeds with your changes: `gradle compileKotlin compileTestKotlin`
|
||||
5. MANDATORY: Run the specific failing test to verify the fix improves or resolves the issue
|
||||
- For Rider UI tests: `gradle :tests:ui-rd-tests:testUi --tests "YourTestClassName.yourTestMethod"`
|
||||
- To run all Rider UI tests: `gradle :tests:ui-rd-tests:testUi`
|
||||
- The test MUST either pass completely or show clear improvement (e.g., progressing further before failure)
|
||||
6. If the test passes or shows improvement with your fix, create a PR with:
|
||||
- Clear title describing the fix
|
||||
- Description explaining the root cause and solution
|
||||
- Test results showing the fix works
|
||||
- Reference to the failing CI run
|
||||
7. Use the base branch 'master' for the PR
|
||||
- name: Save report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-test-fails-report-linux
|
||||
path: |
|
||||
build/reports
|
||||
tests/ui-rd-tests/build/reports
|
||||
idea-sandbox-log
|
||||
@@ -0,0 +1,53 @@
|
||||
# Regenerates the help file that :help opens (src/main/resources/help/ideavim.txt) from the descriptions in the
|
||||
# @ExCommand and @CommandOrMotion annotations, and commits it when it changed.
|
||||
|
||||
name: Update Help
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master ]
|
||||
# The commit of this workflow changes only the help file, there is nothing to regenerate after it
|
||||
paths-ignore:
|
||||
- 'src/main/resources/help/**'
|
||||
|
||||
# A newer push regenerates the help from a newer state, so the older run can be dropped
|
||||
concurrency:
|
||||
group: update-help
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'JetBrains/ideavim'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# A key that is allowed to push to the protected master branch
|
||||
ssh-key: ${{ secrets.PUSH_TO_PROTECTED_BRANCH_SECRET }}
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'corretto'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Generate help
|
||||
run: ./gradlew generateHelp
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_downloadIdeaSources: false
|
||||
ORG_GRADLE_PROJECT_instrumentPluginCode: false
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Update help file
|
||||
commit_user_name: IdeaVim Bot
|
||||
commit_user_email: maintainers@ideavim.dev
|
||||
commit_author: IdeaVim Bot <maintainers@ideavim.dev>
|
||||
file_pattern: src/main/resources/help/ideavim.txt
|
||||
@@ -67,6 +67,7 @@ jobs:
|
||||
- Keep the entire `<head>`/`<style>` block, every CSS class, the
|
||||
`data-theme="__THEME__"` attribute and the `__VERSION__` token unchanged.
|
||||
- Never hard-code a version; leave `__VERSION__` exactly as-is.
|
||||
- Keep the `<!-- whatsnew-release-commit: ... -->` line at the top unchanged.
|
||||
- Escape HTML correctly inside `code`/`kbd`/`pre` (`<` `>` `&`).
|
||||
- Keep the friendly, user-facing tone; no `VIM-####` ids or raw issue URLs.
|
||||
|
||||
|
||||
Generated
+2
@@ -19,10 +19,12 @@
|
||||
<option value="$PROJECT_DIR$/ideavim-common" />
|
||||
<option value="$PROJECT_DIR$/ideavim-frontend" />
|
||||
<option value="$PROJECT_DIR$/ideavim-rider" />
|
||||
<option value="$PROJECT_DIR$/ideavim-rpc" />
|
||||
<option value="$PROJECT_DIR$/ideavim-terminal" />
|
||||
<option value="$PROJECT_DIR$/scripts" />
|
||||
<option value="$PROJECT_DIR$/tests" />
|
||||
<option value="$PROJECT_DIR$/tests/java-tests" />
|
||||
<option value="$PROJECT_DIR$/tests/large-file-tests" />
|
||||
<option value="$PROJECT_DIR$/tests/long-running-tests" />
|
||||
<option value="$PROJECT_DIR$/tests/property-tests" />
|
||||
<option value="$PROJECT_DIR$/tests/split-mode-tests" />
|
||||
|
||||
Generated
+2
-2
@@ -18,5 +18,5 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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 _Self
|
||||
|
||||
import _Self.buildTypes.LargeFileTests
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CustomChart
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CustomChart.Serie
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CustomChart.SeriesKey
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.ProjectFeatures
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.projectCustomChart
|
||||
|
||||
/**
|
||||
* Trend charts for the timings that the large file tests publish (see IdeaVimLargeFileTestBase.publishMetric), shown in
|
||||
* the project's Statistics tab
|
||||
*/
|
||||
fun ProjectFeatures.largeFileCharts() {
|
||||
largeFileChart("GotoDeclaration", "Large files: Go to Declaration with other large files visible") {
|
||||
listOf(0, 1, 3).map { serie("$it other large editors", "gotoDeclaration.otherLargeEditors$it") }
|
||||
}
|
||||
largeFileChart("SearchLarge", "Large files: search in a 200k-line file") {
|
||||
listOf(
|
||||
serie("/ in 1k lines (baseline)", "search.nohlsearch.log1k.search.caretMs"),
|
||||
serie("/", "search.nohlsearch.log200k.search.caretMs"),
|
||||
serie("n", "search.nohlsearch.log200k.next.caretMs"),
|
||||
serie("*", "search.nohlsearch.log200k.star.caretMs"),
|
||||
serie("/ count shown", "search.nohlsearch.log200k.search.countMs"),
|
||||
serie("n count shown", "search.nohlsearch.log200k.next.countMs"),
|
||||
serie("* count shown", "search.nohlsearch.log200k.star.countMs"),
|
||||
)
|
||||
}
|
||||
largeFileChart("SearchNextToLarge", "Large files: search in a small file next to a 200k-line one") {
|
||||
listOf(
|
||||
serie("/ next to 1k lines (baseline)", "search.nohlsearch.smallNextTo.log1k.search.caretMs"),
|
||||
serie("/", "search.nohlsearch.smallNextTo.log200k.search.caretMs"),
|
||||
serie("n", "search.nohlsearch.smallNextTo.log200k.next.caretMs"),
|
||||
serie("*", "search.nohlsearch.smallNextTo.log200k.star.caretMs"),
|
||||
)
|
||||
}
|
||||
largeFileChart("Freezes", "Large files: UI freezes", CustomChart.Format.INTEGER) {
|
||||
listOf(
|
||||
serie("Search tests", "SearchLargeFileTest.freezes"),
|
||||
serie("Go to Declaration tests", "GotoDeclarationVisibleEditorsTest.freezes"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ProjectFeatures.largeFileChart(
|
||||
id: String,
|
||||
title: String,
|
||||
format: CustomChart.Format = CustomChart.Format.DURATION,
|
||||
series: () -> List<Serie>,
|
||||
) {
|
||||
projectCustomChart {
|
||||
this.id = "IdeaVimLargeFileChart_$id"
|
||||
this.title = title
|
||||
this.seriesTitle = "Serie"
|
||||
this.format = format
|
||||
this.series = series()
|
||||
}
|
||||
}
|
||||
|
||||
private fun serie(title: String, metric: String) =
|
||||
Serie(title, SeriesKey("ideavim.largeFile.$metric"), sourceBuildTypeId = LargeFileTests.id?.value)
|
||||
Vendored
+8
@@ -9,11 +9,13 @@
|
||||
package _Self
|
||||
|
||||
import _Self.buildTypes.Compatibility
|
||||
import _Self.buildTypes.LargeFileTests
|
||||
import _Self.buildTypes.LongRunning
|
||||
import _Self.buildTypes.Nvim
|
||||
import _Self.buildTypes.PluginVerifier
|
||||
import _Self.buildTypes.PropertyBased
|
||||
import _Self.buildTypes.RandomOrderTests
|
||||
import _Self.buildTypes.RiderTests
|
||||
import _Self.buildTypes.SplitModeTests
|
||||
|
||||
import _Self.buildTypes.TestingBuildType
|
||||
@@ -43,10 +45,16 @@ object Project : Project({
|
||||
buildType(LongRunning)
|
||||
buildType(RandomOrderTests)
|
||||
buildType(SplitModeTests)
|
||||
buildType(LargeFileTests)
|
||||
buildType(RiderTests)
|
||||
|
||||
buildType(Nvim)
|
||||
buildType(PluginVerifier)
|
||||
buildType(Compatibility)
|
||||
|
||||
features {
|
||||
largeFileCharts()
|
||||
}
|
||||
})
|
||||
|
||||
// Agent size configurations (CPU count)
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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 _Self.buildTypes
|
||||
|
||||
import _Self.AgentSize
|
||||
import _Self.IdeaVimBuildType
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.BuildStep
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
|
||||
|
||||
object LargeFileTests : IdeaVimBuildType({
|
||||
id("IdeaVimLargeFileTests")
|
||||
name = "Large file tests"
|
||||
description = "Tests for IdeaVim in a full IDE with large files (search, search count)"
|
||||
|
||||
// The IDE logs include the thread dumps of UI freezes, which show where a slow operation spent its time
|
||||
artifactRules = """
|
||||
+:tests/large-file-tests/build/reports => large-file-tests-reports.zip
|
||||
+:out/ide-tests/tests/**/log => ide-tests-log.zip
|
||||
""".trimIndent()
|
||||
|
||||
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 {
|
||||
script {
|
||||
name = "Run large file tests"
|
||||
scriptContent = """
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-amazon-corretto
|
||||
export PATH="${'$'}JAVA_HOME/bin:${'$'}PATH"
|
||||
xvfb-run -a -s '-screen 0 1920x1080x24' ./gradlew :tests:large-file-tests:testLargeFiles --console=plain --build-cache --configuration-cache --stacktrace
|
||||
""".trimIndent()
|
||||
}
|
||||
// The tests write their timings as service messages. Printing them publishes them as build statistics, charted in
|
||||
// the project's Statistics tab (see LargeFileCharts). Also for failed runs, so the tests that passed still count
|
||||
script {
|
||||
name = "Publish metrics"
|
||||
executionMode = BuildStep.ExecutionMode.ALWAYS
|
||||
scriptContent = "cat tests/large-file-tests/build/metrics/*.txt || true"
|
||||
}
|
||||
}
|
||||
|
||||
triggers {
|
||||
vcs {
|
||||
branchFilter = "+:<default>"
|
||||
}
|
||||
}
|
||||
|
||||
requirements {
|
||||
// Use a larger agent, the same as for split-mode tests, so searching the large files doesn't hit the timeouts
|
||||
equals("teamcity.agent.hardware.cpuCount", AgentSize.XLARGE)
|
||||
equals("teamcity.agent.os.family", "Linux")
|
||||
}
|
||||
})
|
||||
+86
-12
@@ -81,6 +81,47 @@ sealed class ReleasePlugin(private val releaseType: String) : IdeaVimBuildType({
|
||||
set -e
|
||||
git checkout release
|
||||
echo Checked out release branch. It is published as is - only the EAP build moves it to master
|
||||
git fetch origin +master:refs/remotes/origin/master
|
||||
""".trimIndent()
|
||||
}
|
||||
script {
|
||||
name = "Take the changelog and what's new from master"
|
||||
scriptContent = """
|
||||
set -e
|
||||
# Both are maintained on master (the What's New PR is merged there), while master may be ahead
|
||||
# of the release branch. The changelog is taken from master without the To Be Released entries
|
||||
# for changes that are only on master; the What's New page was written for the release branch
|
||||
# by the Prepare What's New workflow, so it is taken as is. A patch release does not use it: it gets
|
||||
# the page of its minor, and the prepared page stays on master for the next minor or major release.
|
||||
cd scripts-ts
|
||||
npm ci --silent --no-fund --no-audit
|
||||
npx tsx src/prepareReleaseChangelog.ts .. origin/master
|
||||
cd ..
|
||||
if [ "%env.ORG_GRADLE_PROJECT_releaseType%" = "patch" ]; then
|
||||
echo "Patch release: What's New is the page of the minor it patches"
|
||||
exit 0
|
||||
fi
|
||||
tbr="src/main/resources/whatsnew-tbr.html"
|
||||
if ! git cat-file -e "origin/master:${'$'}tbr" 2>/dev/null; then
|
||||
echo "ERROR: master has no ${'$'}tbr. Run the Prepare What's New workflow and merge its PR first"
|
||||
exit 1
|
||||
fi
|
||||
git checkout origin/master -- "${'$'}tbr"
|
||||
# The page describes the release branch as it was when Prepare What's New ran. An EAP build or
|
||||
# a cherry-pick since then changed what the release contains, so the page may miss some of it.
|
||||
described=${'$'}(sed -n 's/^<!-- whatsnew-release-commit: \([0-9a-f]*\) -->${'$'}/\1/p' "${'$'}tbr")
|
||||
released=${'$'}(git rev-parse HEAD)
|
||||
if [ -z "${'$'}described" ]; then
|
||||
echo "ERROR: ${'$'}tbr does not say which release commit it describes. Run the Prepare What's New workflow and merge its PR first"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${'$'}described" != "${'$'}released" ]; then
|
||||
echo "ERROR: ${'$'}tbr describes release commit ${'$'}described, but the release branch is at ${'$'}released."
|
||||
echo "Run the Prepare What's New workflow again and merge its PR. Commits since the page was written:"
|
||||
git log --oneline "${'$'}described..${'$'}released" || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Took ${'$'}tbr from master, written for release commit ${'$'}released"
|
||||
""".trimIndent()
|
||||
}
|
||||
gradle {
|
||||
@@ -114,12 +155,28 @@ sealed class ReleasePlugin(private val releaseType: String) : IdeaVimBuildType({
|
||||
version=${'$'}(echo "%build.number%" | tr '[:upper:]' '[:lower:]')
|
||||
tbr="src/main/resources/whatsnew-tbr.html"
|
||||
target="src/main/resources/whatsnew-${'$'}version.html"
|
||||
if [ -f "${'$'}tbr" ]; then
|
||||
cp "${'$'}tbr" "${'$'}target"
|
||||
if [ "%env.ORG_GRADLE_PROJECT_releaseType%" = "patch" ]; then
|
||||
# Patches roll into their parent minor, so they get the newest page of that minor. This way the IDE
|
||||
# and the What's New site have a page for every released version.
|
||||
minor=${'$'}(echo "${'$'}version" | cut -d. -f1,2)
|
||||
previous=${'$'}(ls src/main/resources/whatsnew-${'$'}minor.*.html 2>/dev/null | sort -V | tail -n 1)
|
||||
if [ -f "${'$'}target" ]; then
|
||||
echo "${'$'}target already exists"
|
||||
elif [ -n "${'$'}previous" ]; then
|
||||
cp "${'$'}previous" "${'$'}target"
|
||||
git add "${'$'}target"
|
||||
echo "Copied ${'$'}previous to ${'$'}target"
|
||||
else
|
||||
echo "WARN: no whatsnew-${'$'}minor.*.html page found; skipping What's New"
|
||||
fi
|
||||
elif [ -f "${'$'}tbr" ]; then
|
||||
git mv "${'$'}tbr" "${'$'}target"
|
||||
sed -i '/^<!-- whatsnew-release-commit: .* -->${'$'}/d' "${'$'}target"
|
||||
git add "${'$'}target"
|
||||
echo "Promoted whatsnew-tbr.html to whatsnew-${'$'}version.html"
|
||||
else
|
||||
echo "WARN: ${'$'}tbr not found; skipping What's New promotion"
|
||||
echo "ERROR: ${'$'}tbr not found"
|
||||
exit 1
|
||||
fi
|
||||
""".trimIndent()
|
||||
}
|
||||
@@ -160,17 +217,29 @@ sealed class ReleasePlugin(private val releaseType: String) : IdeaVimBuildType({
|
||||
name = "Sync changelog and what's new to master"
|
||||
scriptContent = """
|
||||
set -e
|
||||
# Master may be ahead of the release branch: the EAP build is the only thing that moves the
|
||||
# release branch to master, and fixes keep landing on master afterwards. So nothing here is
|
||||
# promoted from master's own To Be Released content - the released changelog section and the
|
||||
# What's New page are taken from the release branch, and master keeps the rest as unreleased.
|
||||
git checkout master
|
||||
git show release:CHANGES.md > /tmp/release-CHANGES.md
|
||||
cd scripts-ts
|
||||
npx tsx src/promoteChangelog.ts "%build.number%" "%env.ORG_GRADLE_PROJECT_releaseType%" ..
|
||||
npx tsx src/syncChangelogToMaster.ts "%build.number%" ../CHANGES.md /tmp/release-CHANGES.md
|
||||
cd ..
|
||||
version=${'$'}(echo "%build.number%" | tr '[:upper:]' '[:lower:]')
|
||||
tbr="src/main/resources/whatsnew-tbr.html"
|
||||
target="src/main/resources/whatsnew-${'$'}version.html"
|
||||
if [ -f "${'$'}tbr" ] && [ ! -f "${'$'}target" ]; then
|
||||
cp "${'$'}tbr" "${'$'}target"
|
||||
git add "${'$'}target"
|
||||
echo "Promoted whatsnew-tbr.html to whatsnew-${'$'}version.html on master"
|
||||
if git cat-file -e "release:${'$'}target" 2>/dev/null; then
|
||||
git checkout release -- "${'$'}target"
|
||||
echo "Took whatsnew-${'$'}version.html from the release branch"
|
||||
# The page is published now; the next What's New run starts a fresh one for the next release.
|
||||
# A patch release did not use it, so it stays for the next minor or major release.
|
||||
if [ "%env.ORG_GRADLE_PROJECT_releaseType%" != "patch" ] && [ -f "${'$'}tbr" ]; then
|
||||
git rm -q "${'$'}tbr"
|
||||
echo "Removed whatsnew-tbr.html on master"
|
||||
fi
|
||||
else
|
||||
echo "WARN: the release branch has no ${'$'}target; master's What's New is left as is"
|
||||
fi
|
||||
git config user.name "IdeaVim Bot"
|
||||
git config user.email "maintainers@ideavim.dev"
|
||||
@@ -197,11 +266,16 @@ sealed class ReleasePlugin(private val releaseType: String) : IdeaVimBuildType({
|
||||
git push origin %build.number%
|
||||
""".trimIndent()
|
||||
}
|
||||
gradle {
|
||||
script {
|
||||
name = "Run Integrations"
|
||||
tasks = "releaseActions"
|
||||
gradleParams = "--build-cache --configuration-cache"
|
||||
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
|
||||
scriptContent = """
|
||||
set -e
|
||||
cd scripts-ts
|
||||
npm ci --silent --no-fund --no-audit
|
||||
: "${'$'}{ORG_GRADLE_PROJECT_youtrackToken:?ORG_GRADLE_PROJECT_youtrackToken is not set}"
|
||||
export YOUTRACK_TOKEN="${'$'}ORG_GRADLE_PROJECT_youtrackToken"
|
||||
npx tsx src/releaseActions.ts "%build.number%" "%env.ORG_GRADLE_PROJECT_releaseType%" .. origin/master release
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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 _Self.buildTypes
|
||||
|
||||
import _Self.AgentSize
|
||||
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.script
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
|
||||
|
||||
object RiderTests : IdeaVimBuildType({
|
||||
name = "Rider tests"
|
||||
description = "Tests for IdeaVim in Rider, where many editor features run on the ReSharper backend"
|
||||
|
||||
artifactRules = """
|
||||
+:tests/ui-rd-tests/build/reports => rider-tests-reports.zip
|
||||
+:out/ide-tests/tests/**/log => ide-tests-log.zip
|
||||
""".trimIndent()
|
||||
|
||||
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 {
|
||||
script {
|
||||
name = "Run Rider tests"
|
||||
// Without RIDER_LICENSE_KEY, Rider runs on a trial. It installs the .NET SDK for the test project itself
|
||||
scriptContent = """
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-amazon-corretto
|
||||
export PATH="${'$'}JAVA_HOME/bin:${'$'}PATH"
|
||||
xvfb-run -a -s '-screen 0 1920x1080x24' ./gradlew :tests:ui-rd-tests:testUi --console=plain --build-cache --configuration-cache --stacktrace
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
triggers {
|
||||
vcs {
|
||||
branchFilter = "+:<default>"
|
||||
}
|
||||
}
|
||||
|
||||
requirements {
|
||||
// Use a larger agent, the same as for split-mode tests: Rider runs a frontend and a ReSharper backend
|
||||
equals("teamcity.agent.hardware.cpuCount", AgentSize.XLARGE)
|
||||
equals("teamcity.agent.os.family", "Linux")
|
||||
}
|
||||
})
|
||||
+145
-1
@@ -25,8 +25,105 @@ usual beta standards.
|
||||
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-748](https://youtrack.jetbrains.com/issue/VIM-748) [`|`](https://vimhelp.org/cmdline.txt.html#%3Abar) now separates Ex commands that take a pattern or free-form argument, such as [`:s`](https://vimhelp.org/change.txt.html#%3As) and [`:sort`](https://vimhelp.org/change.txt.html#%3Asort), so `:%s/foo/bar/|%s/baz/qux/` runs both substitutions and `:%sort u|1d` sorts and then deletes the first line. A `\|` inside the pattern or replacement is still regex alternation or a literal `|`, and a `|` inside a `:sort` pattern such as `:%sort /a|b/` stays part of it. As in Vim, [`:global`](https://vimhelp.org/repeat.txt.html#%3Aglobal), [`:vglobal`](https://vimhelp.org/repeat.txt.html#%3Avglobal), [`:normal`](https://vimhelp.org/various.txt.html#%3Anormal) and `:!` take the `|` as part of their argument
|
||||
* [VIM-3963](https://youtrack.jetbrains.com/issue/VIM-3963) Vim can now be enabled in two more kinds of editors with the `'ideaeditor'` option: `main` for tabs in the main editor area that are not backed by a regular file, such as editors that third-party plugins open for in-memory or decrypted content, and `chat` for the prompt input of [JetBrains AI Assistant](https://www.jetbrains.com/help/ai-assistant/ai-chat.html) chats. Both are off by default — enable them with `:set ideaeditor+=main` or `:set ideaeditor+=chat`. The chat input starts in Insert mode, so `<Enter>` still sends the message
|
||||
* [VIM-1306](https://youtrack.jetbrains.com/issue/VIM-1306) New `keylog` extension writes every key you type to a file, like Vim's [`-w`](https://vimhelp.org/starting.txt.html#-w) command line argument. Enable it with `set keylog`. The keys go to `ideavim.log` next to your ideavimrc, or to the file set with `let g:keylog_file = "~/vimlogs/ideavim.log"`
|
||||
* [`:help`](https://vimhelp.org/helphelp.txt.html#%3Ahelp) now opens IdeaVim's own read-only help file inside the IDE instead of a web page. It lists every supported command with a description of what it does in IdeaVim, and `:help :s` or `:help i_CTRL-W` jumps straight to that command
|
||||
* Action IDs can now be completed: press `<Tab>`/`<S-Tab>` after `:action` or `:actionlist` in the command line to cycle through matching IDs, and in `.ideavimrc` code completion suggests action IDs, with their names and descriptions, inside `<Action(...)>` and after `action`
|
||||
* [VIM-1948](https://youtrack.jetbrains.com/issue/VIM-1948) New `'ideascrollcursor'` option makes the caret move with the text when scrolling with the mouse wheel or the scrollbar, keeping it [`'scrolloff'`](https://vimhelp.org/options.txt.html#%27scrolloff%27) lines from the edge of the window, as in Vim. It is off by default — enable it with `:set ideascrollcursor`
|
||||
* [VIM-720](https://youtrack.jetbrains.com/issue/VIM-720) `<CR>`, `<Esc>` and the arrow keys can now be handed over to the IDE with [`sethandler`](https://github.com/JetBrains/ideavim/blob/master/doc/sethandler.md) for that key, e.g. `sethandler <CR> i:ide` or `sethandler <Esc> n:ide`. Handlers set for all shortcuts at once, such as `sethandler a:ide`, still leave these keys to Vim
|
||||
* [VIM-2372](https://youtrack.jetbrains.com/issue/VIM-2372) The [NERDTree](https://github.com/JetBrains/ideavim/blob/master/doc/NERDTree-support.md) navigation keys `j`, `k`, `gg`, `G`, `<C-D>`, `<C-U>` and `/` can now be changed like the other NERDTree keys, e.g. `let g:NERDTreeMapSelectNext = '<C-N>'` or `let g:NERDTreeMapSpeedSearch = 'f'`. Changed `g:NERDTreeMap*` variables are applied the next time the Project tool window gets focus, without restarting the IDE
|
||||
* [VIM-2293](https://youtrack.jetbrains.com/issue/VIM-2293) New `visual-multi` extension emulates [vim-visual-multi](https://github.com/mg979/vim-visual-multi) using the IDE's native carets. Enable it with `Plug 'mg979/vim-visual-multi'`. Press `<C-n>` to select the word under the cursor and add its next occurrence, `\\A` to select all occurrences, or `<C-Down>`/`<C-Up>` to add cursors on the lines below/above. During a session, `n`/`N` find the next/previous occurrence, `q`/`Q` skip or remove a region, `<Tab>` switches between regions and cursors, and `\\z`, `\\@`, `\\C` and `\\a` run a `:normal` command, run a macro, convert case and align the cursors. Keys can be changed with `g:VM_maps` and `g:VM_leader`. See the [documentation](https://github.com/JetBrains/ideavim/blob/master/doc/IdeaVim%20Plugins.md) for all keys and options
|
||||
* Added the [`:function`](https://vimhelp.org/userfunc.txt.html#%3Afunction) command for listing user-defined functions. `:function` lists every global function with its arguments and flags. `:function MyFunc` shows a single function, and `:function /pattern` lists the functions whose names match the pattern
|
||||
* [VIM-1975](https://youtrack.jetbrains.com/issue/VIM-1975) New `clever-f` extension emulates [clever-f.vim](https://github.com/rhysd/clever-f.vim). Enable it with `Plug 'rhysd/clever-f.vim'`. After `f{char}`, pressing `f` again jumps to the next `{char}` and `F` to the previous one, so `;` and `,` are not needed. The same works for `F`, `t` and `T`. Searches cross line boundaries, and the characters the search can jump to on the cursor line are highlighted. Behaviour can be changed with `g:clever_f_*` variables such as `g:clever_f_smart_case` or `g:clever_f_across_no_line`. See the [documentation](https://github.com/JetBrains/ideavim/blob/master/doc/IdeaVim%20Plugins.md) for all options
|
||||
|
||||
### Fixes:
|
||||
|
||||
* [VIM-4341](https://youtrack.jetbrains.com/issue/VIM-4341) Fixed the IDE freezing when searching with `/`, `n` or `*` in large files. The search count is now worked out in the background and shown when it is ready
|
||||
* Fixed a single-line message, such as the search pattern shown by `n`, closing while it was still being updated. The [`'messagesopt'`](https://vimhelp.org/options.txt.html#%27messagesopt%27) `wait` delay now restarts each time the message changes, so pressing `n` repeatedly keeps the message open
|
||||
* Fixed [`histadd()`](https://vimhelp.org/builtin.txt.html#histadd%28%29), `histdel()`, `histget()` and `histnr()` failing with `"?"` as the history name. As in Vim, `"?"` now refers to the search history, the same as `"/"`
|
||||
* [VIM-1153](https://youtrack.jetbrains.com/issue/VIM-1153) [`.`](https://vimhelp.org/repeat.txt.html#.) now replays
|
||||
the keys you typed, as Vim does, instead of the recorded edits. Repeating a tag rename such as `cwdiv<Esc>` on
|
||||
another HTML tag now renames its closing tag too, and a completion accepted with `<C-Y>` or a mapping, or
|
||||
picked with `<C-N>`/`<C-P>`, is repeated with the completed text
|
||||
* [VIM-4333](https://youtrack.jetbrains.com/issue/VIM-4333) Fixed [`=`](https://vimhelp.org/change.txt.html#%3D) and `==` reformatting the whole file instead of only the given lines in C/C++ files in CLion Nova
|
||||
* [VIM-1314](https://youtrack.jetbrains.com/issue/VIM-1314) Pasting with the IDE paste action (`Cmd+V`/`Ctrl+V`) in Replace mode now overwrites the characters under the caret, as in Vim, instead of inserting the text before them. `<BS>` puts the original characters back
|
||||
* [VIM-1894](https://youtrack.jetbrains.com/issue/VIM-1894) Fixed [`V`](https://vimhelp.org/visual.txt.html#V) on a collapsed fold selecting only its first line. Linewise Visual mode now takes in the whole folded region, so `Vd` on a collapsed `<div>` deletes the entire tag, and `Vj` onto a collapsed fold selects all of it
|
||||
* [VIM-1503](https://youtrack.jetbrains.com/issue/VIM-1503) Fixed dead keys and input methods typing text into the document in Normal, Visual and Operator-pending modes. Pressing a dead key such as `'` after `vi` no longer replaces the Visual selection, and a composed character like `é` can be used as a command argument, e.g. `fé`
|
||||
* [VIM-2524](https://youtrack.jetbrains.com/issue/VIM-2524) [`cc`](https://vimhelp.org/change.txt.html#cc) and [`S`](https://vimhelp.org/change.txt.html#S) on an empty or blank line now indent it to match the surrounding code, the same indent `o` on the line above would give, instead of keeping whatever white space the line already had
|
||||
* Fixed [`%`](https://vimhelp.org/motion.txt.html#%25) jumping to the wrong bracket, or not moving, when an apostrophe that does not start a string sits between the brackets — in `(it's) y's`, `%` on `(` now jumps to the matching `)`
|
||||
* Fixed `%` throwing an exception in an empty file
|
||||
* Fixed `:/` and `:?` with an empty pattern throwing an exception. As in Vim, they now use the last search pattern, and report `E35: No previous regular expression` when there is none
|
||||
* Fixed [`:continue`](https://vimhelp.org/eval.txt.html#%3Acontinue) inside a `:for` loop over a List hanging the IDE — `for x in [1, 2] | continue | endfor` now finishes
|
||||
* Fixed [`slice()`](https://vimhelp.org/builtin.txt.html#slice%28%29) returning the whole list or string when the end index works out to zero — `slice([1, 2, 3], 0, 0)` now returns `[]`
|
||||
* Fixed [`:tags`](https://vimhelp.org/tagsrch.txt.html#%3Atags) reporting an error when the line a tag jump was made from has since been deleted
|
||||
* Fixed searching for a pattern containing `\<` throwing an exception while the [command-line window](https://vimhelp.org/cmdline.txt.html#cmdline-window) opened by `q:`, `q/` or `q?` is open
|
||||
* Fixed the `ReplaceWithRegister` extension's `gr` in Visual mode deleting whole lines when the selection spans lines — with `vj` selecting from the middle of one line into the next, `gr` now replaces only the selected text and keeps the rest of both lines
|
||||
|
||||
### Merged PRs:
|
||||
* [2082](https://github.com/JetBrains/ideavim/pull/2082) by [1grzyb1](https://github.com/1grzyb1): VIM-748 changing multiple commands with |
|
||||
* [2088](https://github.com/JetBrains/ideavim/pull/2088) by [1grzyb1](https://github.com/1grzyb1): VIM-1153 replay all keystrokes during .
|
||||
* [2096](https://github.com/JetBrains/ideavim/pull/2096) by [1grzyb1](https://github.com/1grzyb1): VIM-1306 Add keylog extension
|
||||
* [2097](https://github.com/JetBrains/ideavim/pull/2097) by [1grzyb1](https://github.com/1grzyb1): VIM-1314 Replace mode paste with cmd+v
|
||||
* [2090](https://github.com/JetBrains/ideavim/pull/2090) by [1grzyb1](https://github.com/1grzyb1): VIM-4333 Fix auto-indent handling in clion
|
||||
* [2091](https://github.com/JetBrains/ideavim/pull/2091) by [1grzyb1](https://github.com/1grzyb1): VIM-3963 Allow enabling vim for plugin editors and ai chat windows
|
||||
* [2098](https://github.com/JetBrains/ideavim/pull/2098) by [1grzyb1](https://github.com/1grzyb1): VIM-1894 include collapsed region during visual selection
|
||||
* [2102](https://github.com/JetBrains/ideavim/pull/2102) by [1grzyb1](https://github.com/1grzyb1): VIM-1948 Scroll caret while mouse scrolling
|
||||
* [2112](https://github.com/JetBrains/ideavim/pull/2112) by [1grzyb1](https://github.com/1grzyb1): VIM-720 Allow handling esc, enter, arrows directly by ide
|
||||
* [2113](https://github.com/JetBrains/ideavim/pull/2113) by [1grzyb1](https://github.com/1grzyb1): VIM-1503 don't type dead keys
|
||||
* [2115](https://github.com/JetBrains/ideavim/pull/2115) by [1grzyb1](https://github.com/1grzyb1): VIM-2372 Add missing mappings for navigation keys
|
||||
* [2117](https://github.com/JetBrains/ideavim/pull/2117) by [1grzyb1](https://github.com/1grzyb1): VIM-2293 Implement Vim visual multi plugin
|
||||
* [2119](https://github.com/JetBrains/ideavim/pull/2119) by [1grzyb1](https://github.com/1grzyb1): VIM-2524 calculate indent after cc
|
||||
* [2103](https://github.com/JetBrains/ideavim/pull/2103) by [citizenmatt](https://github.com/citizenmatt): Implement :function command to list functions
|
||||
* [2086](https://github.com/JetBrains/ideavim/pull/2086) by [valis](https://github.com/valis): Don't treat quotes inside non-string tokens as string delimiters in %
|
||||
|
||||
## 2.47.3, 2026-10-05
|
||||
|
||||
### Fixes:
|
||||
* [VIM-4336](https://youtrack.jetbrains.com/issue/VIM-4336) Fixed the caret ending up in the wrong place after IDE actions that run asynchronously in Rider, such as commenting a line
|
||||
|
||||
### Merged PRs:
|
||||
* [2107](https://github.com/JetBrains/ideavim/pull/2107) by [1grzyb1](https://github.com/1grzyb1): VIM-4336 postpone caret normalization for async actions
|
||||
|
||||
## 2.47.2, 2026-10-02
|
||||
|
||||
### Features:
|
||||
* [VIM-4335](https://youtrack.jetbrains.com/issue/VIM-4335) Added the `'ideaoutsideeditor'` option (on by default), which controls whether mappings to IDE actions such as `map <leader>sf <Action>(GotoFile)` keep working when no file is open. Use `:set noideaoutsideeditor` to leave all keys typed outside an editor to the IDE
|
||||
|
||||
### Merged PRs:
|
||||
* [2100](https://github.com/JetBrains/ideavim/pull/2100) by [1grzyb1](https://github.com/1grzyb1): VIM-4335 guard outside editor dispatcher when ideaoutsideeditor
|
||||
|
||||
## 2.47.1, 2026-09-25
|
||||
|
||||
### Fixes:
|
||||
|
||||
* [VIM-2478](https://youtrack.jetbrains.com/issue/VIM-2478) [
|
||||
`:set noautoindent`](https://vimhelp.org/options.txt.html#%27autoindent%27) no longer removes the indent the IDE works
|
||||
out from your code. The option only copies the indent of the current line, and in Vim an indent from [
|
||||
`'indentexpr'`](https://vimhelp.org/options.txt.html#%27indentexpr%27) or [
|
||||
`'cindent'`](https://vimhelp.org/options.txt.html#%27cindent%27) is added whatever `'autoindent'` is set to, so `o`,
|
||||
`O` and `<CR>` in Insert mode now keep the indent of the new line in a file of a language the IDE has a formatter for,
|
||||
as Neovim does with its default `filetype indent on`. The caret still goes to column 0 where the IDE cannot work an
|
||||
indent out on its own, such as in a plain text file, and the indent is still removed again if you leave Insert mode
|
||||
without typing anything
|
||||
|
||||
### Changes:
|
||||
|
||||
* [VIM-4331](https://youtrack.jetbrains.com/issue/VIM-4331) A new line opened with `o`, `O` or `<CR>` now keeps its indent when you leave Insert mode without typing anything. Vim
|
||||
deletes such an indent, so that `o<Esc>` leaves a completely empty line, but in the IDE a line you have just opened is
|
||||
expected to stay where it is. `cc` and `S` still follow Vim and leave the line they empty without any indent
|
||||
* IdeaVim now respects "Keep indents on empty lines" in Settings | Editor | Code Style. With it on, the line that `cc`
|
||||
or `S` empties keeps its indent instead of being left completely empty. The setting is off by default
|
||||
|
||||
### Merged PRs:
|
||||
* [2084](https://github.com/JetBrains/ideavim/pull/2084) by [1grzyb1](https://github.com/1grzyb1): VIM-4331 keep the indent of a new line when leaving Insert mode
|
||||
|
||||
## 2.47.0, 2026-09-24
|
||||
|
||||
### Features:
|
||||
* [VIM-3058](https://youtrack.jetbrains.com/issue/VIM-3058) Added the [`"#` register](https://vimhelp.org/change.txt.html#quote_%23), which holds the name of the alternate file — the file you last switched away from, the one [`CTRL-^`](https://vimhelp.org/editing.txt.html#CTRL-%5E) jumps back to. `"#p` puts it into the text, `@#` reads it in an expression, and `:registers` lists it under `%`. Like `%`, it reports the path relative to the content root, since IdeaVim has no current directory, and it is read only — `:let @# = 'other.txt'` reports `E354: Invalid register name: '#'`
|
||||
* [VIM-1266](https://youtrack.jetbrains.com/issue/VIM-1266) Added the [`"%` register](https://vimhelp.org/change.txt.html#quote_%25), which holds the name of the file you are editing — `"%p` puts it into the text, and `:registers` now lists it along with the rest. Vim's `%` holds the buffer name as it was typed, which is usually a path relative to the current directory; IdeaVim has no current directory, so it reports the path relative to the content root, the same path that `CTRL-G` and `:file` already show
|
||||
* [VIM-1370](https://youtrack.jetbrains.com/issue/VIM-1370) Added the [tag stack](https://vimhelp.org/tagsrch.txt.html#tag-stack), so a jump to a definition can be retraced the way Vim does it. [`CTRL-]`](https://vimhelp.org/tagsrch.txt.html#CTRL-%5D) now records the position it was jumped from, [`CTRL-T`](https://vimhelp.org/tagsrch.txt.html#CTRL-T) and [`:pop`](https://vimhelp.org/tagsrch.txt.html#%3Apop) walk back up those positions, [`:tag`](https://vimhelp.org/tagsrch.txt.html#%3Atag) without an argument walks back down again, and [`:tags`](https://vimhelp.org/tagsrch.txt.html#%3Atags) lists the stack with `>` marking where you are in it. All of them take a count, so `2<C-T>` and `:2pop` step back two tag jumps at once. The stack holds twenty entries, dropping the oldest once it is full, and a new tag jump made after walking back drops the entries you walked past. `gd` and `gD` are not tag commands and leave the stack alone, as in Vim
|
||||
* Added the [`'tagstack'`](https://vimhelp.org/options.txt.html#%27tagstack%27) option (`'tgst'`), on by default. With `:set notagstack` a tag jump still navigates but records nothing, which is useful for a mapping that should not disturb the stack. Walking the stack is unaffected — `CTRL-T` keeps using whatever is already on it
|
||||
* [VIM-1347](https://youtrack.jetbrains.com/issue/VIM-1347) Added built-in [`vim-signature`](https://github.com/kshenoy/vim-signature) extension — enable it with `Plug 'kshenoy/vim-signature'` and every local mark `a`-`z` of the file gets an icon in the gutter, so you can see where your marks are instead of having to remember. `m,` places the next free mark, `m-` clears the marks of the current line and `m<Space>` those of the whole buffer. `]'` and `['` walk through the marked lines in the order they appear, while `']`, `'[`, `` `] `` and `` `[ `` walk through the marks in the order of their letters — all of them take a count, and each one is a jump, so `''` and `<C-O>` bring you back. Those last four are Vim's own marks of the last change or yank and the plugin takes them over, as the original does; every key is a mapping to a `<Plug>` name, so `nmap gn <Plug>(SignatureGotoNextSpotAlpha)` and friends let you keep Vim's. `` ]` `` and `` [` `` are left alone, since IdeaVim already implements them as real motions. Global marks `A`-`Z` are not drawn, because with the default `ideamarks` they are IDE bookmarks, which the IDE shows in the gutter itself
|
||||
@@ -36,11 +133,22 @@ usual beta standards.
|
||||
* Added built-in [`vim-textobj-line`](https://github.com/kana/vim-textobj-line) extension — enable it with `Plug 'kana/vim-textobj-line'` and you get two text objects for the current line: `al` selects every character of the line without the end of line character (the same as `0v$h`), and `il` leaves the leading and trailing whitespace out of it (the same as `^vg_`). So on an indented line, `dil` deletes the code and keeps the indent, while `dal` empties the line completely, and `yil`, `cil`, `gUal` and the rest work the same way from anywhere in the line, no matter where the caret is. As in the original, neither selects anything on an empty line, and `il` also does nothing on a line of whitespace only, since there is no text there to select. Set `let g:textobj_line_no_default_key_mappings = 1` before enabling the plugin to keep `al` and `il` free and map `<Plug>(textobj-line-al)` and `<Plug>(textobj-line-il)` yourself
|
||||
* [VIM-2089](https://youtrack.jetbrains.com/issue/VIM-2089) Added built-in [`vim-visual-star-search`](https://github.com/bronson/vim-visual-star-search) extension — enable it with `Plug 'bronson/vim-visual-star-search'` and [`*`](https://vimhelp.org/pattern.txt.html#star) and [`#`](https://vimhelp.org/pattern.txt.html#%23) in Visual mode search forwards and backwards for the selected text instead of for the word under the caret. The selection is searched for literally, without the word boundaries the built-in `*` adds, so selecting `foo` also finds it inside `foobar`, and characters such as `.`, `*`, `[` and `~` match themselves instead of being read as a pattern. Every kind of selection works: `V*` searches for the whole line together with its end of line character, so it only matches text that ends a line, and a blockwise selection searches for its rows joined by end of line characters, as the original does. `n` and `N` repeat the search, [`gv`](https://vimhelp.org/visual.txt.html#gv) brings back the text you searched for, and the position you searched from goes on the [jump list](https://vimhelp.org/motion.txt.html#jumplist), so `''` and `<C-O>` return to it. Unlike the built-in `*`, the search honours [`'ignorecase'`](https://vimhelp.org/options.txt.html#%27ignorecase%27) and [`'smartcase'`](https://vimhelp.org/options.txt.html#%27smartcase%27), because the original searches with a plain `/`. A count is applied, so `3*` finds the third occurrence — the original loses it, as the `:<C-u>` in its mapping wipes the count off the command line. The plugin's `<leader>*` mapping, which greps the selection through the whole project, is not implemented
|
||||
* [VIM-2663](https://youtrack.jetbrains.com/issue/VIM-2663) Added the [`:startinsert`](https://vimhelp.org/insert.txt.html#%3Astartinsert) (`:star`) and [`:startreplace`](https://vimhelp.org/insert.txt.html#%3Astartreplace) (`:startr`) commands, the counterparts of [`:stopinsert`](https://vimhelp.org/insert.txt.html#%3Astopinsert). `:startinsert` enters Insert mode where the caret is, exactly as `i` does, and `:startinsert!` first moves to the end of the line, as `A` does. `:startreplace` enters Replace mode as `R` does, and `:startreplace!` works like `$R`, so it starts by overwriting the last character of the line rather than appending after it — with `:set virtualedit=onemore` it goes one further and appends, just as `$` itself does. `<BS>` in the Replace mode they start restores the overwritten characters, like any other Replace session. Every caret takes part, so with several carets each one moves to the end of its own line. Like `:stopinsert`, they are meant for autocommands — `autocmd BufEnter scratch startinsert` opens a scratch buffer ready to type in. Neither takes a range or an argument; `:1startinsert` reports `E481: No range allowed` and `:startinsert foo` reports `E488: Trailing characters`
|
||||
* [VIM-2478](https://youtrack.jetbrains.com/issue/VIM-2478) Added the [`'autoindent'`](https://vimhelp.org/options.txt.html#%27autoindent%27) option (`'ai'`). With `:set noautoindent`, `o`, `O`, `cc`, `S` and `<CR>` in Insert mode start the new line without any indent, and `:setlocal noautoindent` does this for the current buffer only. The option is on by default, as in Neovim. As in Vim, the indent that `cc` and `S` keep on the line is removed again when you leave Insert mode without typing anything, so `cc<Esc>` on an indented line now leaves an empty line instead of one full of white space
|
||||
* [VIM-4324](https://youtrack.jetbrains.com/issue/VIM-4324) Added [`CTRL-W q`](https://vimhelp.org/windows.txt.html#CTRL-W_q), which closes the current window like `CTRL-W c`
|
||||
* [VIM-2627](https://youtrack.jetbrains.com/issue/VIM-2627) The location of the ideavimrc file can now be set in Settings | Editor | Vim. The path may use `~`, environment variables such as `$VAR` or `${VAR}`, and IDE path macros such as `$APPLICATION_CONFIG_DIR$`, so `$APPLICATION_CONFIG_DIR$/ideavim/ideavimrc` keeps a separate configuration for each IDE. Leave it empty to use the default locations, which are also used while the configured file does not exist yet
|
||||
* [VIM-3999](https://youtrack.jetbrains.com/issue/VIM-3999) Vim now works in the console of a run or debug configuration, e.g. the console of a debugged application. The new `'ideaeditor'` option lists the non-file editors IdeaVim is active in — `python` for the [Python console](https://www.jetbrains.com/help/pycharm/using-consoles.html) and `debug` for run and debug consoles, both on by default — so `:set ideaeditor-=debug` gives the debug console its native behaviour back
|
||||
* [VIM-2629](https://youtrack.jetbrains.com/issue/VIM-2629) Added the `g:commentary_block_comments` variable to the [`vim-commentary`](https://github.com/tpope/vim-commentary) extension. IdeaVim uses a block comment when the range is not linewise, so `gciw` comments out a single word; set `let g:commentary_block_comments = 0` to always comment out whole lines, as the original plugin does
|
||||
* [VIM-3796](https://youtrack.jetbrains.com/issue/VIM-3796) The [`matchit`](https://github.com/adelarsq/vim-matchit) extension now supports [`b:match_words`](https://vimhelp.org/pi_matchit.txt.html#b%3Amatch_words), so `%` can jump between your own pairs of words — `let b:match_words = 'begin:end,if:elsif:else:end'` makes `%` cycle through `if`, `elsif`, `else` and `end`
|
||||
* [VIM-3198](https://youtrack.jetbrains.com/issue/VIM-3198) Added `<C-D>` and `<C-U>` to [NERDTree](https://github.com/JetBrains/ideavim/wiki/NERDTree-support), which move the selection half a page down and up
|
||||
|
||||
### Fixes:
|
||||
* [VIM-3667](https://youtrack.jetbrains.com/issue/VIM-3667) Fixed mappings to IDE actions not working when no file is open. With `map <leader>sf <Action>(GotoFile)` in `~/.ideavimrc`, pressing `<leader>sf` used to do nothing once every editor tab was closed, because IdeaVim only listened for keys inside an editor. The keys of such Normal mode mappings are now picked up while the focus is on a non-text component of a project with no open editor — the Project tool window, for example — and go through the regular mapping machinery, so counts, `'timeout'` and recursive mappings behave as in an editor; `<Esc>` cancels a half-typed sequence. Only mappings whose right-hand side consists of `<Action>(...)` sequences take part, since there is no editor for anything else to act on: `nmap j gj` keeps `j` for the tool window, and the `:action GotoFile<CR>` form of the mapping is not picked up. Text fields such as Search Everywhere keep receiving typed characters, modal dialogs are left alone, and keys that do not start a mapping are left to the IDE
|
||||
* Fixed [`gx`](https://vimhelp.org/pi_netrw.txt.html#netrw-gx) adding to the [jump list](https://vimhelp.org/motion.txt.html#jumplist) — pressing `gx` used to record the caret position as a jump and overwrite the [`'`](https://vimhelp.org/motion.txt.html#%27quote) mark, so a following [`CTRL-O`](https://vimhelp.org/motion.txt.html#CTRL-O) landed back on the caret's own line instead of returning to the previous jump. As in Vim, `gx` opens the URL without touching the jump list
|
||||
* Fixed the [word text objects](https://vimhelp.org/motion.txt.html#v_iw) throwing an exception when they reached the very start of the file — growing a right-to-left Visual selection with `iw`, `aw`, `iW` or `aW` looks one character back to decide where the previous word begins, and at offset 0 there is no such character. Starting on the second character of the file and pressing `v`, `h`, `iw` now selects back to the start of the file instead of reporting an internal error
|
||||
* Fixed [`g8`](https://vimhelp.org/various.txt.html#g8) reporting the hex value of a single UTF-16 code unit instead of the UTF-8 bytes of the character under the caret, which is what it is supposed to print — `é` now reports `c3 a9` rather than `e9`, and a character outside the Basic Multilingual Plane such as `😀` reports all four of its bytes rather than half of a surrogate pair. ASCII characters are unchanged. Vim also appends the bytes of any trailing composing characters, separated by `+`; IdeaVim still does not
|
||||
* Fixed `g8` throwing an exception when there is no character under the caret — in an empty file, or on an empty line, the caret sits where the line break is. As in Vim, which puts the cursor on the line's terminating NUL, `g8` now reports `NUL` instead of reporting an internal error or, on an empty line in the middle of the file, the hex value of the line feed
|
||||
* Fixed the caret position after [`r`](https://vimhelp.org/change.txt.html#v_r) in Visual mode — replacing a selection left the caret at the end of the replaced text instead of on its first character. With the caret on the first `f` of `abcdffffff`, `v2lra` now leaves it on the first of the three `a`s, as in Vim
|
||||
* Fixed `<End>` always being captured by Vim — `<End>`, `<S-End>`, `<C-End>` and `<C-S-End>` are no longer Vim-only editor keys, so [`sethandler <End> a:ide`](https://jb.gg/vim-sethandler) can release them to the IDE. IdeaVim still handles them when no handler is set
|
||||
* [VIM-4308](https://youtrack.jetbrains.com/issue/VIM-4308) Fixed the current search match being drawn with a box around it after the search was over. The box now only appears while [`'incsearch'`](https://vimhelp.org/options.txt.html#%27incsearch%27) is previewing the pattern you are typing, and only in the window you are searching in — accepting the search with `<CR>`, cancelling it with `<Esc>`, `n`, `N`, moving the caret, editing the text, toggling [`'hlsearch'`](https://vimhelp.org/options.txt.html#%27hlsearch%27) and clicking into another window all leave every match looking the same. This is what a stock Vim looks like: its default colour scheme links the [`CurSearch`](https://vimhelp.org/syntax.txt.html#hl-CurSearch) highlight group to `Search`, so the match under the cursor is drawn no differently from the rest unless you define `CurSearch` yourself. Neovim does define it, which is why the box shows up there
|
||||
* Fixed [`gv`](https://vimhelp.org/visual.txt.html#gv) reselecting one character too many. In Visual mode `gv` swaps the current selection for the previous one, and the restored selection ran one character past where it ended — with `foo bar` on the first line and `bar foo` on the second, `viw` on `foo`, `<Esc>`, `j`, `viw` and then `gv` selected `foo ` instead of `foo`
|
||||
* Fixed `da(` inside a nested block swallowing the enclosing brackets. A characterwise delete or yank spanning several lines is promoted to [linewise](https://vimhelp.org/change.txt.html#linewise) only when there is nothing but white space before its start and after its end, but the check for what comes after skipped the very first character following the range. So with an outer block opened on one line, an inner one opened on the next, and both closed by `))` on a third, `da(` on the inner `(` deleted the last two lines whole and took the outer `)` with them, instead of deleting just the inner block
|
||||
@@ -48,12 +156,32 @@ usual beta standards.
|
||||
* [VIM-4319](https://youtrack.jetbrains.com/issue/VIM-4319) Fixed IdeaVim reporting an internal error when it was disabled, uninstalled or updated without restarting the IDE. The plugin used to shut itself down after the platform had already unregistered its services, so the shutdown could no longer reach them and failed with a null pointer exception. It now shuts down just before the platform unloads it, while everything it needs is still there, which also means its listeners are properly removed and typing is handed back to the IDE — so after switching IdeaVim off the keys behave as they would with the plugin never installed, without a restart
|
||||
* Fixed [`CTRL-R CTRL-F`](https://vimhelp.org/cmdline.txt.html#c_CTRL-R_CTRL-F) reporting an internal error on the command line when the [`'incsearch'`](https://vimhelp.org/options.txt.html#%27incsearch%27) match ended at the very end of the file. The file name is looked for just past the match, where there is nothing left, so searching for the last word of the file and pressing `CTRL-R CTRL-F` now reports `E446: No file name under cursor`, as Vim does
|
||||
* [VIM-2010](https://youtrack.jetbrains.com/issue/VIM-2010) Fixed [`.`](https://vimhelp.org/repeat.txt.html#.) replaying an import that the IDE added while you were typing. IdeaVim records every change the document goes through during an insert, which is how auto-inserted brackets and indents get replayed too, but an `import` line put in by [Add unambiguous imports on the fly](https://www.jetbrains.com/help/idea/creating-and-optimizing-imports.html#auto-importing), by the Alt+Enter fix or by the completion popup is not something you typed. Those changes are now left out of the recording — in a Java file with two `Set<String>` fields, changing the first one with `cf>List<String>` also adds `import java.util.List;` at the top of the file, and `.` on the second field now changes just that field instead of typing the text of the import into it as well. Picking the class from the completion popup is recorded correctly too: it inserts the fully qualified `java.util.List`, adds the import and then shortens the name again, and `.` used to replay that whole round trip and leave the caret in the middle of it
|
||||
* [VIM-2127](https://youtrack.jetbrains.com/issue/VIM-2127) Fixed the closing HTML or XML tag not being renamed together with the opening one. Editing a tag name in Insert mode or with `r` now updates its pair as it does without IdeaVim — `r` on the last letter of `<tag></tag>` followed by `s` gives `<tas></tas>`
|
||||
* [VIM-4318](https://youtrack.jetbrains.com/issue/VIM-4318) Fixed [`gv`](https://vimhelp.org/visual.txt.html#gv) after `:'<,'>sort` selecting only the last line. `gv` now reselects all the sorted lines, and after `:'<,'>sort u` the lines that remain once the duplicates are gone. The same applied to other commands that rewrite the whole selection at once, such as `gU` and `g?`
|
||||
* [VIM-4326](https://youtrack.jetbrains.com/issue/VIM-4326) Fixed Visual mode mappings in [Select mode](https://vimhelp.org/visual.txt.html#Select-mode-mapping). As in Vim, the right-hand side of a `:map` or `:vmap` mapping is now run as if in Visual mode and Select mode is restored afterwards, instead of the keys being typed over the selection — with `:noremap j nzz`, pressing `j` in Select mode extends the selection to the next match
|
||||
* [VIM-3093](https://youtrack.jetbrains.com/issue/VIM-3093) Fixed typing and scrolling slowing down in large files with [`'hlsearch'`](https://vimhelp.org/options.txt.html#%27hlsearch%27) on and a pattern with many matches. Only the matches in and around the visible part of the editor are highlighted now, and more are highlighted as you scroll
|
||||
* [VIM-1180](https://youtrack.jetbrains.com/issue/VIM-1180) Fixed [`:!`](https://vimhelp.org/various.txt.html#%3A%21cmd) hanging on a command that reads standard input — `:!sort` never finished, because its input was left open and the command kept waiting for text that could never arrive, and the IDE stayed busy until the process was killed. The input is now closed as soon as there is nothing to send, so the command sees the end of input and exits, as it does in Neovim. Filtering lines through a command, as in `:%!sort`, is unaffected, and interactive commands are still not supported
|
||||
|
||||
### Changes:
|
||||
* IdeaVim's own options are now all prefixed with `idea`, so it is clear at a glance which options are IdeaVim's and which are Vim's. `'lookupkeys'`, `'pythonconsole'`, `'trackactionids'` and `'visualdelay'` are now `'idealookupkeys'`, `'ideapythonconsole'`, `'ideatrackactionids'` and `'ideavisualdelay'`. The old names still work, so an existing `~/.ideavimrc` needs no changes, but they are deprecated and only the new name is reported by `:set` and `:set all`. `'trackactionids'` keeps its `'tai'` abbreviation
|
||||
* IdeaVim's own options are now all prefixed with `idea`, so it is clear at a glance which options are IdeaVim's and which are Vim's. `'lookupkeys'`, `'trackactionids'` and `'visualdelay'` are now `'idealookupkeys'`, `'ideatrackactionids'` and `'ideavisualdelay'`. The old names still work, so an existing `~/.ideavimrc` needs no changes, but they are deprecated and only the new name is reported by `:set` and `:set all`. `'trackactionids'` keeps its `'tai'` abbreviation
|
||||
* The `'pythonconsole'` option has been removed in favour of the `python` value of the new `'ideaeditor'` option. Replace `set nopythonconsole` in `~/.ideavimrc` with `set ideaeditor-=python`
|
||||
* [`CTRL-T`](https://vimhelp.org/tagsrch.txt.html#CTRL-T) is no longer a second key for [`CTRL-O`](https://vimhelp.org/motion.txt.html#CTRL-O). It used to step back along the [jump list](https://vimhelp.org/motion.txt.html#jumplist); it now walks back up the [tag stack](https://vimhelp.org/tagsrch.txt.html#tag-stack) as in Vim, and reports `E73: Tag stack empty` when no tag jump has been made. `CTRL-O` is unchanged and remains the way to walk the jump list
|
||||
* Removed the user surveys — the "Take Survey" entry is gone from the IdeaVim status bar menu, and uninstalling IdeaVim no longer opens a survey in the browser
|
||||
|
||||
### Merged PRs:
|
||||
* [2074](https://github.com/JetBrains/ideavim/pull/2074) by [1grzyb1](https://github.com/1grzyb1): VIM-3796 support match_words in matchit
|
||||
* [2072](https://github.com/JetBrains/ideavim/pull/2072) by [1grzyb1](https://github.com/1grzyb1): VIM-3198 add fast scroll in nerdtree
|
||||
* [2071](https://github.com/JetBrains/ideavim/pull/2071) by [1grzyb1](https://github.com/1grzyb1): VIM-3093 Render only visible highlights
|
||||
* [2070](https://github.com/JetBrains/ideavim/pull/2070) by [1grzyb1](https://github.com/1grzyb1): VIM-4326 run mapping from select mode in visual mode
|
||||
* [2065](https://github.com/JetBrains/ideavim/pull/2065) by [1grzyb1](https://github.com/1grzyb1): VIM-3999 enable vim in debug console
|
||||
* [2064](https://github.com/JetBrains/ideavim/pull/2064) by [1grzyb1](https://github.com/1grzyb1): VIM-2629 commentary block comments variable
|
||||
* [2063](https://github.com/JetBrains/ideavim/pull/2063) by [1grzyb1](https://github.com/1grzyb1): VIM-2627 Configure ideavimrc path
|
||||
* [2062](https://github.com/JetBrains/ideavim/pull/2062) by [1grzyb1](https://github.com/1grzyb1): VIM-4324 add close window command
|
||||
* [2060](https://github.com/JetBrains/ideavim/pull/2060) by [1grzyb1](https://github.com/1grzyb1): VIM2663 Implement startinsert and startrepeat commands
|
||||
* [2059](https://github.com/JetBrains/ideavim/pull/2059) by [1grzyb1](https://github.com/1grzyb1): VIM-2478 restore auto indent after actions
|
||||
* [2058](https://github.com/JetBrains/ideavim/pull/2058) by [1grzyb1](https://github.com/1grzyb1): VIM-2127 Update corresponding html tag while editing
|
||||
* [2057](https://github.com/JetBrains/ideavim/pull/2057) by [1grzyb1](https://github.com/1grzyb1): VIM-2089 Implement visual star search plugin
|
||||
* [2038](https://github.com/JetBrains/ideavim/pull/2038) by [1grzyb1](https://github.com/1grzyb1): VIM-4318 Restore selection with gv after sort
|
||||
* [2053](https://github.com/JetBrains/ideavim/pull/2053) by [1grzyb1](https://github.com/1grzyb1): VIM-2010 fix repeat insert with import
|
||||
* [2052](https://github.com/JetBrains/ideavim/pull/2052) by [1grzyb1](https://github.com/1grzyb1): VIM-2700 Navigate between split diff windows
|
||||
* [2049](https://github.com/JetBrains/ideavim/pull/2049) by [1grzyb1](https://github.com/1grzyb1): VIM-1974 implement textobj-line extension
|
||||
@@ -63,10 +191,16 @@ usual beta standards.
|
||||
* [2037](https://github.com/JetBrains/ideavim/pull/2037) by [1grzyb1](https://github.com/1grzyb1): VIM-4314 Expand % signs in commands
|
||||
* [2034](https://github.com/JetBrains/ideavim/pull/2034) by [1grzyb1](https://github.com/1grzyb1): VIM-4308 Don't update the current search match highlight when no search is active
|
||||
* [2033](https://github.com/JetBrains/ideavim/pull/2033) by [AndDe-gourav](https://github.com/AndDe-gourav): fix range of visual selection
|
||||
* [2029](https://github.com/JetBrains/ideavim/pull/2029) by [AndDe-gourav](https://github.com/AndDe-gourav): Fix caret position after change visual character
|
||||
* [2022](https://github.com/JetBrains/ideavim/pull/2022) by [1grzyb1](https://github.com/1grzyb1): Remove end from Vim only editor keys
|
||||
* [2029](https://github.com/JetBrains/ideavim/pull/2029) by [AndDe-gourav](https://github.com/AndDe-gourav): Fix caret position after change visual character
|
||||
* [2022](https://github.com/JetBrains/ideavim/pull/2022) by [1grzyb1](https://github.com/1grzyb1): Remove end from Vim only editor keys
|
||||
* [2021](https://github.com/JetBrains/ideavim/pull/2021) by [1grzyb1](https://github.com/1grzyb1): VIM-3058 alternate file name register
|
||||
* [2020](https://github.com/JetBrains/ideavim/pull/2020) by [1grzyb1](https://github.com/1grzyb1): VIM-1370 Implement tag stack in IdeaVim
|
||||
* [2018](https://github.com/JetBrains/ideavim/pull/2018) by [1grzyb1](https://github.com/1grzyb1): VIM-1347 Implements Signature marks plugin
|
||||
* [2014](https://github.com/JetBrains/ideavim/pull/2014) by [1grzyb1](https://github.com/1grzyb1): VIM-4312 correct linewise delete promotion when range end has trailing non-whitespace
|
||||
* [2007](https://github.com/JetBrains/ideavim/pull/2007) by [1grzyb1](https://github.com/1grzyb1): VIM-1266 file name from percent register
|
||||
* [2005](https://github.com/JetBrains/ideavim/pull/2005) by [1grzyb1](https://github.com/1grzyb1): VIM-1180 Pass eof to external command
|
||||
* [2001](https://github.com/JetBrains/ideavim/pull/2001) by [1grzyb1](https://github.com/1grzyb1): VIM-934 support for wildmenu option
|
||||
* [2000](https://github.com/JetBrains/ideavim/pull/2000) by [1grzyb1](https://github.com/1grzyb1): VIM-2168 separate history per split window
|
||||
|
||||
@@ -78,6 +212,16 @@ usual beta standards.
|
||||
### Merged PRs:
|
||||
* [2013](https://github.com/JetBrains/ideavim/pull/2013) by [1grzyb1](https://github.com/1grzyb1): VIM-4312 Option to disbale vim in python console
|
||||
|
||||
## 2.46.1, 2026-08-21
|
||||
|
||||
### Fixes:
|
||||
* [VIM-4310](https://youtrack.jetbrains.com/issue/VIM-4310) Fixed an unknown `:` command reporting the parser's own diagnostic, complete with the full list of token names it was expecting — `:^` now reports `E492: Not an editor command: ^` as Vim does, and `:1,2^abc` reports the command without the range. An error inside a command that is recognised, such as `:echo ^523`, no longer lists the expected tokens either
|
||||
* [VIM-4309](https://youtrack.jetbrains.com/issue/VIM-4309) Fixed the `YankRing` extension's `<C-P>` and `<C-N>` appearing to do nothing when [`'clipboard'`](https://vimhelp.org/options.txt.html#%27clipboard%27) contains `unnamed` or `unnamedplus` — cycling to another entry in the ring re-pasted from the system clipboard, putting the text it was replacing straight back
|
||||
|
||||
### Merged PRs:
|
||||
* [2010](https://github.com/JetBrains/ideavim/pull/2010) by [1grzyb1](https://github.com/1grzyb1): VIM-4310 Don't show full parser error message
|
||||
* [2009](https://github.com/JetBrains/ideavim/pull/2009) by [1grzyb1](https://github.com/1grzyb1): VIM-4309 cycle pastes from unnamed register
|
||||
|
||||
## 2.46.0, 2026-08-18
|
||||
|
||||
### Features:
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Guidance for Claude Code when working with IdeaVim.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
Essential commands:
|
||||
- `./gradlew runIde` - Start dev IntelliJ with IdeaVim
|
||||
- `./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test` - Run standard tests
|
||||
|
||||
Avoid running all tests, this takes too long. It's preferred to run specific test.
|
||||
|
||||
When running gradle tasks, use `--console=plain` for cleaner output without progress bars.
|
||||
|
||||
See CONTRIBUTING.md for architecture details and a complete command list.
|
||||
|
||||
## IdeaVim-Specific Notes
|
||||
|
||||
- Property tests can be flaky - verify if failures relate to your changes
|
||||
- Use `<Action>` in mappings, not `:action`
|
||||
- Config file: `~/.ideavimrc` (XDG supported)
|
||||
- Goal: Match Vim functionality and architecture
|
||||
|
||||
## Issue Tracking
|
||||
|
||||
This project uses **YouTrack** for issue tracking, NOT GitHub Issues.
|
||||
- Tickets are prefixed with `VIM-` (e.g., VIM-1234)
|
||||
- YouTrack URL: https://youtrack.jetbrains.com/issues/VIM
|
||||
- `gh issue` commands will NOT work
|
||||
|
||||
## Commit Messages
|
||||
|
||||
For commits that fix a tracked issue, use the format `Fix(VIM-XXX): <description>` (e.g., `Fix(VIM-4115): NPE in CommandKeyConsumer after plugin disable/enable`).
|
||||
|
||||
## Additional Documentation
|
||||
|
||||
- Changelog maintenance: Handled by the `changelog` skill (auto-detected when updating changelog)
|
||||
|
||||
## Active Technologies
|
||||
- Kotlin (JVM 21) + IntelliJ Platform SDK, IdeaVim vim-engine (001-api-layer)
|
||||
|
||||
## Recent Changes
|
||||
- 001-api-layer: Added Kotlin (JVM 21) + IntelliJ Platform SDK, IdeaVim vim-engine
|
||||
@@ -81,6 +81,12 @@ If you are looking for:
|
||||
- Vim script parsing: package `com.maddyhome.idea.vim.vimscript.parser`.
|
||||
- Vim script executor: `Executor`.
|
||||
|
||||
- Help (`:help`):
|
||||
- Every `@CommandOrMotion` and `@ExCommand` has a `description`. It's rendered into the
|
||||
[help file](src/main/resources/help/ideavim.txt) that `:help` opens.
|
||||
- The help file is regenerated with `./gradlew generateHelp`. On `master`, the "Update Help" workflow does it
|
||||
automatically, so there is no need to commit it with your change.
|
||||
|
||||
- Extensions:
|
||||
- Extensions handler: `VimExtensionHandler`.
|
||||
- Available extensions: package `com/maddyhome/idea/vim/extension`.
|
||||
|
||||
@@ -12,6 +12,7 @@ plugins {
|
||||
}
|
||||
|
||||
val kotlinxSerializationVersion: String by project
|
||||
val kspVersion: String by project
|
||||
|
||||
group = "com.intellij"
|
||||
version = "SNAPSHOT"
|
||||
@@ -21,7 +22,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.google.devtools.ksp:symbol-processing-api:2.3.12")
|
||||
compileOnly("com.google.devtools.ksp:symbol-processing-api:$kspVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion") {
|
||||
// kotlin stdlib is provided by IJ, so there is no need to include it into the distribution
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
|
||||
|
||||
+18
-1
@@ -17,7 +17,24 @@ package com.intellij.vim.annotations
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class CommandOrMotion(val keys: Array<String>, vararg val modes: Mode)
|
||||
annotation class CommandOrMotion(
|
||||
val keys: Array<String>,
|
||||
vararg val modes: Mode,
|
||||
/**
|
||||
* Registers this handler for the same keys as its normal counterpart, to be used only while a completion popup is
|
||||
* open.
|
||||
*
|
||||
* `<C-Y>` accepts the selected item with a popup open and inserts the character above the caret without one. Those
|
||||
* are two commands sharing a keystroke, so they are two handlers: `AcceptLookupAction` declares `lookup = true` and
|
||||
* `InsertCharacterAboveCursorAction` does not. Exactly one of each pair may exist for a given key and mode.
|
||||
*/
|
||||
val lookup: Boolean = false,
|
||||
/**
|
||||
* A short description of what the command does, written for users. It's rendered into IdeaVim's help file
|
||||
* (`:help`), so keep it in the same style as Vim's own help: a sentence or two, in the imperative mood.
|
||||
*/
|
||||
val description: String,
|
||||
)
|
||||
|
||||
annotation class TextObject(val keys: String)
|
||||
|
||||
|
||||
@@ -10,7 +10,23 @@ package com.intellij.vim.annotations
|
||||
|
||||
/**
|
||||
* [command] is formatted the same way it is formatted in Vim (with optional part in square brackets).
|
||||
*
|
||||
* [barSeparates] and [delimitedSections] say how a `|` in the argument is treated, the job Vim's command table does
|
||||
* with its `TRLBAR` flag. Repeat the annotation when the names it declares need different values.
|
||||
* See `CommandVisitor.splitOffNextCommand` and `:help :bar`.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class ExCommand(val command: String)
|
||||
@Repeatable
|
||||
annotation class ExCommand(
|
||||
val command: String,
|
||||
/**
|
||||
* A short description of what the command does, written for users. It's rendered into IdeaVim's help file
|
||||
* (`:help`), so keep it in the same style as Vim's own help: a sentence or two, in the imperative mood.
|
||||
*/
|
||||
val description: String,
|
||||
/** `false` when the command takes a `|` as part of its argument, so it can never be followed by another command */
|
||||
val barSeparates: Boolean = true,
|
||||
/** The number of delimiter-wrapped sections the argument holds, like the pattern and replacement of `:s/pat/sub/` */
|
||||
val delimitedSections: Int = 0,
|
||||
)
|
||||
|
||||
@@ -11,4 +11,4 @@ package com.intellij.vim.processors
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CommandBean(val keys: String, val `class`: String, val modes: String)
|
||||
data class CommandBean(val keys: String, val `class`: String, val modes: String, val lookup: Boolean = false)
|
||||
+6
-1
@@ -41,7 +41,12 @@ class CommandOrMotionProcessor(private val environment: SymbolProcessorEnvironme
|
||||
val commandAnnotation = classDeclaration.getAnnotationsByType(CommandOrMotion::class).firstOrNull() ?: return
|
||||
for (key in commandAnnotation.keys) {
|
||||
commands.add(
|
||||
CommandBean(key, classDeclaration.qualifiedName!!.asString(), commandAnnotation.modes.map { it.abbrev }.joinToString(separator = ""))
|
||||
CommandBean(
|
||||
key,
|
||||
classDeclaration.qualifiedName!!.asString(),
|
||||
commandAnnotation.modes.map { it.abbrev }.joinToString(separator = ""),
|
||||
commandAnnotation.lookup,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-7
@@ -18,18 +18,28 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSFile
|
||||
import com.google.devtools.ksp.symbol.KSVisitorVoid
|
||||
import com.intellij.vim.annotations.ExCommand
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** One entry of the generated ex-command table, read back by `ExCommandProvider` */
|
||||
@Serializable
|
||||
internal data class ExCommandEntry(
|
||||
val className: String,
|
||||
val barSeparates: Boolean = true,
|
||||
val delimitedSections: Int = 0,
|
||||
)
|
||||
|
||||
class ExCommandProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {
|
||||
private val visitor = EXCommandVisitor()
|
||||
private val commandToClass = mutableMapOf<String, String>()
|
||||
private val commandToClass = mutableMapOf<String, ExCommandEntry>()
|
||||
private val fileWriter = JsonFileWriter(environment)
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val exCommandsFile = environment.options["ex_commands_file"] ?: return emptyList()
|
||||
|
||||
resolver.getAllFiles().forEach { it.accept(visitor, Unit) }
|
||||
|
||||
val sortedCommandToClass = commandToClass.toList().sortedWith(compareBy({ it.first }, { it.second })).toMap()
|
||||
|
||||
val sortedCommandToClass =
|
||||
commandToClass.toList().sortedWith(compareBy({ it.first }, { it.second.className })).toMap()
|
||||
fileWriter.write(exCommandsFile, sortedCommandToClass)
|
||||
|
||||
return emptyList()
|
||||
@@ -38,10 +48,15 @@ class ExCommandProcessor(private val environment: SymbolProcessorEnvironment) :
|
||||
private inner class EXCommandVisitor : KSVisitorVoid() {
|
||||
@OptIn(KspExperimental::class)
|
||||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
|
||||
val exCommandAnnotation = classDeclaration.getAnnotationsByType(ExCommand::class).firstOrNull() ?: return
|
||||
val commands = exCommandAnnotation.command.split(",")
|
||||
for (command in commands) {
|
||||
commandToClass[command] = classDeclaration.qualifiedName!!.asString()
|
||||
// The annotation is repeatable, so that one class can declare different bar handling for different names
|
||||
for (exCommandAnnotation in classDeclaration.getAnnotationsByType(ExCommand::class)) {
|
||||
for (command in exCommandAnnotation.command.split(",")) {
|
||||
commandToClass[command] = ExCommandEntry(
|
||||
classDeclaration.qualifiedName!!.asString(),
|
||||
exCommandAnnotation.barSeparates,
|
||||
exCommandAnnotation.delimitedSections,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.intellij.vim.processors
|
||||
|
||||
import com.google.devtools.ksp.KspExperimental
|
||||
import com.google.devtools.ksp.getAnnotationsByType
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSFile
|
||||
import com.google.devtools.ksp.symbol.KSVisitorVoid
|
||||
import com.intellij.vim.annotations.CommandOrMotion
|
||||
import com.intellij.vim.annotations.ExCommand
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** The descriptions of one module's commands, merged with the other modules' into the help file by `generateHelp` */
|
||||
@Serializable
|
||||
internal data class HelpData(
|
||||
val exCommands: List<ExCommandHelp>,
|
||||
val commands: List<CommandHelp>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class ExCommandHelp(
|
||||
/** The command names as declared by [ExCommand.command], e.g. `h[elp]` */
|
||||
val names: List<String>,
|
||||
val description: String,
|
||||
val className: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class CommandHelp(
|
||||
val keys: List<String>,
|
||||
/** The abbreviations of the modes, as in [CommandBean.modes] */
|
||||
val modes: String,
|
||||
val lookup: Boolean,
|
||||
val description: String,
|
||||
val className: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Collects the descriptions of [ExCommand] and [CommandOrMotion] declarations.
|
||||
*
|
||||
* Unlike the command tables, the output is not needed at runtime, so it goes to `help_directory` (in the build
|
||||
* directory) rather than to the resources.
|
||||
*/
|
||||
class HelpProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {
|
||||
private val visitor = HelpVisitor()
|
||||
private val exCommands = mutableListOf<ExCommandHelp>()
|
||||
private val commands = mutableListOf<CommandHelp>()
|
||||
private val fileWriter = JsonFileWriter(environment)
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val helpFile = environment.options["help_file"] ?: return emptyList()
|
||||
val helpDirectory = environment.options["help_directory"] ?: return emptyList()
|
||||
|
||||
resolver.getAllFiles().forEach { it.accept(visitor, Unit) }
|
||||
|
||||
val data = HelpData(
|
||||
exCommands.sortedWith(compareBy({ it.names.first() }, { it.className })),
|
||||
commands.sortedWith(compareBy({ it.keys.first() }, { it.modes }, { it.className })),
|
||||
)
|
||||
fileWriter.write(helpFile, data, helpDirectory)
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private inner class HelpVisitor : KSVisitorVoid() {
|
||||
@OptIn(KspExperimental::class)
|
||||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
|
||||
val className = classDeclaration.qualifiedName!!.asString()
|
||||
for (annotation in classDeclaration.getAnnotationsByType(ExCommand::class)) {
|
||||
exCommands.add(ExCommandHelp(annotation.command.split(","), annotation.description, className))
|
||||
}
|
||||
for (annotation in classDeclaration.getAnnotationsByType(CommandOrMotion::class)) {
|
||||
commands.add(
|
||||
CommandHelp(
|
||||
annotation.keys.toList(),
|
||||
annotation.modes.map { it.abbrev }.joinToString(separator = ""),
|
||||
annotation.lookup,
|
||||
annotation.description,
|
||||
className,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitFile(file: KSFile, data: Unit) {
|
||||
file.declarations.forEach { it.accept(this, Unit) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ internal class JsonFileWriter(
|
||||
prettyPrintIndent = " "
|
||||
}
|
||||
|
||||
inline fun <reified T> write(fileName: String, data: T) {
|
||||
val generatedDirPath = Path(environment.options["generated_directory"]!!)
|
||||
inline fun <reified T> write(fileName: String, data: T, directory: String = environment.options["generated_directory"]!!) {
|
||||
val generatedDirPath = Path(directory)
|
||||
Files.createDirectories(generatedDirPath)
|
||||
|
||||
val filePath = generatedDirPath.resolve(fileName)
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.intellij.vim.providers
|
||||
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import com.intellij.vim.processors.HelpProcessor
|
||||
|
||||
class HelpProcessorProvider : SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
return HelpProcessor(environment)
|
||||
}
|
||||
}
|
||||
+1
@@ -2,3 +2,4 @@ com.intellij.vim.providers.CommandOrMotionProcessorProvider
|
||||
com.intellij.vim.providers.ExCommandProcessorProvider
|
||||
com.intellij.vim.providers.VimscriptFunctionProcessorProvider
|
||||
com.intellij.vim.providers.ExtensionsProcessorProvider
|
||||
com.intellij.vim.providers.HelpProcessorProvider
|
||||
|
||||
+19
-22
@@ -28,11 +28,11 @@ buildscript {
|
||||
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.8.0.202609011348-r")
|
||||
classpath("org.kohsuke:github-api:1.305")
|
||||
|
||||
classpath("io.ktor:ktor-client-core:3.5.2")
|
||||
classpath("io.ktor:ktor-client-cio:3.5.2")
|
||||
classpath("io.ktor:ktor-client-auth:3.5.2")
|
||||
classpath("io.ktor:ktor-client-content-negotiation:3.5.2")
|
||||
classpath("io.ktor:ktor-serialization-kotlinx-json:3.5.2")
|
||||
classpath("io.ktor:ktor-client-core:3.6.0")
|
||||
classpath("io.ktor:ktor-client-cio:3.6.0")
|
||||
classpath("io.ktor:ktor-client-auth:3.6.0")
|
||||
classpath("io.ktor:ktor-client-content-negotiation:3.6.0")
|
||||
classpath("io.ktor:ktor-serialization-kotlinx-json:3.6.0")
|
||||
|
||||
// This comes from the changelog plugin
|
||||
// classpath("org.jetbrains:markdown:0.3.1")
|
||||
@@ -48,11 +48,11 @@ plugins {
|
||||
// NOTE: Unignore "test block comment falls back to line comment when not available" test
|
||||
// After changing this version. It supposed to work on the next version of the gradle plugin
|
||||
// Or go report to the devs that this test still fails.
|
||||
id("org.jetbrains.intellij.platform") version "2.18.0"
|
||||
id("org.jetbrains.intellij.platform") version "2.18.1"
|
||||
|
||||
id("org.jetbrains.changelog") version "2.5.0"
|
||||
id("com.dorongold.task-tree") version "4.0.2"
|
||||
id("com.google.devtools.ksp") version "2.3.7"
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
val moduleSources by configurations.registering
|
||||
@@ -134,10 +134,13 @@ dependencies {
|
||||
pluginModule(runtimeOnly(project(":ideavim-rider")))
|
||||
pluginModule(runtimeOnly(project(":ideavim-clion-nova")))
|
||||
pluginModule(runtimeOnly(project(":ideavim-terminal")))
|
||||
pluginModule(runtimeOnly(project(":ideavim-rpc")))
|
||||
|
||||
bundledModule("intellij.spellchecker")
|
||||
if (bookmarksIsSeparatePlugin) bundledPlugin("intellij.bookmarks.plugin")
|
||||
bundledModule("intellij.platform.kernel.impl")
|
||||
|
||||
compatiblePlugin("com.intellij.classic.ui")
|
||||
}
|
||||
|
||||
moduleSources(project(":vim-engine", "sourcesJarArtifacts"))
|
||||
@@ -173,7 +176,7 @@ dependencies {
|
||||
// Temp workaround suggested in https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#junit5-test-framework-refers-to-junit4
|
||||
// Can be removed when IJPL-159134 is fixed
|
||||
// testRuntimeOnly("junit:junit:4.13.2")
|
||||
testImplementation("org.junit.vintage:junit-vintage-engine:6.1.2")
|
||||
testImplementation("org.junit.vintage:junit-vintage-engine:6.1.3")
|
||||
// testFixturesImplementation("org.junit.vintage:junit-vintage-engine:5.10.3")
|
||||
}
|
||||
|
||||
@@ -264,6 +267,7 @@ tasks {
|
||||
|
||||
runIde {
|
||||
systemProperty("ideavim.use.debug.ideavimrc", "true")
|
||||
systemProperty("idea.trust.all.projects", "true")
|
||||
}
|
||||
|
||||
val runPycharm by intellijPlatformTesting.runIde.registering {
|
||||
@@ -323,7 +327,7 @@ tasks {
|
||||
}
|
||||
val runRider by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.Rider
|
||||
version = "2026.2.0.2"
|
||||
version = "2026.2.3.1"
|
||||
// Using Rider as a target IntelliJ Platform with `useInstaller = true` is currently not supported, please set `useInstaller = false` instead. See: https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1852
|
||||
useInstaller = false
|
||||
task {
|
||||
@@ -461,11 +465,6 @@ tasks {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildPlugin {
|
||||
dependsOn(sourcesJar)
|
||||
from(sourcesJar) { into("lib/src") }
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
@@ -508,7 +507,7 @@ gradle.projectsEvaluated {
|
||||
|
||||
// The rendered change notes body, plus the version whose CHANGES.md section it came from.
|
||||
// Patch releases have no section of their own (promoteChangelog.ts rolls them into the parent
|
||||
// minor), so 2.45.1 resolves to 2.45.0 - and so does its YouTrack link, because releaseActions.kt
|
||||
// minor), so 2.45.1 resolves to 2.45.0 - and so does its YouTrack link, because releaseActions.ts
|
||||
// only ever creates fix versions for minors. Note that getLatest() returns the first section in
|
||||
// file order, not the highest version, so CHANGES.md must stay sorted and free of empty headers.
|
||||
val changeNotesParts = provider {
|
||||
@@ -536,13 +535,7 @@ intellijPlatform {
|
||||
})
|
||||
|
||||
ideaVersion {
|
||||
// Let the Gradle plugin set the since-build version. It defaults to the version of the IDE we're building against
|
||||
// specified as two components, `{branch}.{build}` (e.g., "241.15989"). There is no third component specified.
|
||||
// The until-build version defaults to `{branch}.*`, but we want to support _all_ future versions, so we set it
|
||||
// with a null provider (the provider is important).
|
||||
// By letting the Gradle plugin handle this, the Plugin DevKit IntelliJ plugin cannot help us with the "Usage of
|
||||
// IntelliJ API not available in older IDEs" inspection. However, since our since-build is the version we compile
|
||||
// against, we can never get an API that's newer - it would be an unresolved symbol.
|
||||
sinceBuild.set("253")
|
||||
untilBuild.set(provider { null })
|
||||
}
|
||||
}
|
||||
@@ -578,9 +571,13 @@ ksp {
|
||||
arg("ex_commands_file", "frontend_ex_commands.json")
|
||||
arg("vimscript_functions_file", "frontend_vimscript_functions.json")
|
||||
arg("extensions_file", "ideavim_extensions.json")
|
||||
arg("help_directory", layout.buildDirectory.dir("help").get().asFile.path)
|
||||
arg("help_file", "frontend_help.json")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
// Written by HelpProcessor, see the generateHelp task
|
||||
tasks.named("kspKotlin").configure { outputs.dir(layout.buildDirectory.dir("help")) }
|
||||
tasks.named("kspTestFixturesKotlin").configure { enabled = false }
|
||||
tasks.named("kspTestKotlin").configure { enabled = false }
|
||||
}
|
||||
|
||||
@@ -221,6 +221,57 @@ When classes are nested, the innermost enclosing class is selected.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>clever-f: Repeat f, F, t and T by pressing the same key again</h2></summary>
|
||||
|
||||
Original plugin: [clever-f.vim](https://github.com/rhysd/clever-f.vim).
|
||||
|
||||
### Summary:
|
||||
Extends `f`, `F`, `t` and `T` so that pressing the same key again repeats the last search, instead of `;` and `,`.
|
||||
After `f{char}`, `f` jumps to the next `{char}` and `F` to the previous one. The direction is relative to the first
|
||||
search, so after `F{char}`, `f` keeps searching backward. Unlike the builtin motions, searches cross line boundaries.
|
||||
Moving the cursor in any other way, changing the text or entering Insert mode starts a new search. Works in normal,
|
||||
visual and operator-pending modes, and takes a `[count]`. The characters the search can jump to on the cursor line
|
||||
are highlighted until the search ends.
|
||||
|
||||
### Setup:
|
||||
- Add the following command to `~/.ideavimrc`: `Plug 'rhysd/clever-f.vim'`
|
||||
<details>
|
||||
<summary>Alternative syntax</summary>
|
||||
<code>Plugin 'rhysd/clever-f.vim'</code>
|
||||
<br/>
|
||||
<code>Plug 'https://github.com/rhysd/clever-f.vim'</code>
|
||||
<br/>
|
||||
<code>Plug 'clever-f.vim'</code>
|
||||
<br/>
|
||||
<code>set clever-f</code>
|
||||
</details>
|
||||
|
||||
### Instructions
|
||||
|
||||
https://github.com/rhysd/clever-f.vim/blob/master/doc/clever_f.txt
|
||||
|
||||
Supported variables (`g:clever_f_not_overwrites_standard_mappings` must be set before the plugin is loaded):
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `g:clever_f_across_no_line` | Search only in the current line |
|
||||
| `g:clever_f_ignore_case` | Ignore case |
|
||||
| `g:clever_f_smart_case` | Ignore case unless the input is an uppercase character |
|
||||
| `g:clever_f_fix_key_direction` | `f` always searches forward and `F` always backward |
|
||||
| `g:clever_f_chars_match_any_signs` | Characters which match any sign, e.g. `';'` |
|
||||
| `g:clever_f_repeat_last_char_inputs` | Keys which repeat the last searched character, `<CR>` by default |
|
||||
| `g:clever_f_timeout_ms` | Start a new search if the last one is older than this |
|
||||
| `g:clever_f_mark_char` | Highlight the characters the search can jump to, enabled by default |
|
||||
| `g:clever_f_not_overwrites_standard_mappings` | Don't map `f`, `F`, `t` and `T` |
|
||||
|
||||
Available `<Plug>` mappings: `<Plug>(clever-f-f)`, `<Plug>(clever-f-F)`, `<Plug>(clever-f-t)`, `<Plug>(clever-f-T)`,
|
||||
`<Plug>(clever-f-repeat-forward)`, `<Plug>(clever-f-repeat-back)` and `<Plug>(clever-f-reset)`.
|
||||
|
||||
`g:clever_f_mark_cursor`, `g:clever_f_mark_direct`, custom highlight colors and migemo are not currently supported.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>commentary: Adds mapping for quickly commenting stuff out</h2></summary>
|
||||
|
||||
@@ -504,6 +555,33 @@ absolute-indent motions (`[_`, `]_`) from the original plugin are not currently
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>keylog: Writes every typed key to a file</h2></summary>
|
||||
|
||||
### Summary:
|
||||
Logs all the keys you type, like Vim's `vim -w {scriptout}` command line argument. Use the log to find out which
|
||||
commands you use most often.
|
||||
|
||||
### Setup:
|
||||
- Add the following command to `~/.ideavimrc`: `set keylog`
|
||||
|
||||
### Instructions
|
||||
|
||||
The keys are appended to `ideavim.log` in the same directory as your ideavimrc file (e.g. `~/ideavim.log` for
|
||||
`~/.ideavimrc`). To use another file:
|
||||
`let g:keylog_file = "~/vimlogs/ideavim.log"`
|
||||
A relative path is relative to the directory of your ideavimrc file.
|
||||
|
||||
The keys are written in Vim's key notation, as used by `:map`, e.g. `ciwfoo<Esc>`. A typed `<` is written as `<lt>`.
|
||||
Only the keys you type are logged, not the keys produced by mappings, macros or `.`.
|
||||
|
||||
The keys are buffered in memory and written to the file every few seconds, and when the extension is disabled or the
|
||||
IDE exits.
|
||||
|
||||
The log contains everything you type, including passwords typed into the editor. Keep it private.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>matchit.vim: Extends the % key functionality</h2></summary>
|
||||
|
||||
@@ -528,6 +606,8 @@ The matchit plugin extends this in several ways...
|
||||
|
||||
### Instructions
|
||||
|
||||
Your own pairs can be defined in the `b:match_words` buffer variable, e.g. `let b:match_words = 'begin:end,if:elsif:else:end'`.
|
||||
|
||||
https://github.com/adelarsq/vim-matchit/blob/master/doc/matchit.txt
|
||||
|
||||
</details>
|
||||
@@ -1135,6 +1215,81 @@ component has focus.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>visual-multi: Multiple cursors and selections</h2></summary>
|
||||
|
||||
Original plugin: [vim-visual-multi](https://github.com/mg979/vim-visual-multi).
|
||||
|
||||
### Setup:
|
||||
- Add the following command to `~/.ideavimrc`: `Plug 'mg979/vim-visual-multi'`
|
||||
<details>
|
||||
<summary>Alternative syntax</summary>
|
||||
<code>Plugin 'mg979/vim-visual-multi'</code>
|
||||
<br/>
|
||||
<code>Plug 'https://github.com/mg979/vim-visual-multi'</code>
|
||||
<br/>
|
||||
<code>Plug 'vim-visual-multi'</code>
|
||||
<br/>
|
||||
<code>set visual-multi</code>
|
||||
</details>
|
||||
|
||||
Don't enable it together with `multiple-cursors`, both use `<C-n>`.
|
||||
|
||||
### Instructions
|
||||
|
||||
The cursors are the native IDE carets. The regions are their Visual selections, so motions and commands work on all
|
||||
of them. Press `<Tab>` to switch between regions and cursors.
|
||||
|
||||
`\\` is the leader, see `g:VM_leader` below.
|
||||
|
||||
Available anytime:
|
||||
|
||||
| Action | Shortcut |
|
||||
|---------------------------------------------------------|------------------------|
|
||||
| Select word under cursor / selection, add next one | `<C-n>` |
|
||||
| Select all occurrences of word / selection | `\\A` |
|
||||
| Add cursor down / up | `<C-Down>` / `<C-Up>` |
|
||||
| Cursor on each line of selection | `\\c` (Visual) |
|
||||
| Selection becomes regions | `\\a` (Visual) |
|
||||
| Reselect regions of the last session | `\\gS` |
|
||||
|
||||
With multiple cursors or regions (a session):
|
||||
|
||||
| Action | Shortcut |
|
||||
|---------------------------------------------------------|------------------------|
|
||||
| Find next / previous occurrence | `n` / `N` |
|
||||
| Go to next / previous region | `]` / `[` |
|
||||
| Skip region / remove region | `q` / `Q` |
|
||||
| Switch between regions and cursors | `<Tab>` |
|
||||
| Insert before / after regions, at line start / end | `i` / `a` / `I` / `A` |
|
||||
| Run `:normal` command / repeat it | `\\z` / `\\Z` |
|
||||
| Run macro | `\\@` |
|
||||
| Case conversion (`u` `U` `C` `t` `c` `P` `s` `S` `-` `.` `<Space>`) | `\\C` |
|
||||
| Align cursors | `\\a` |
|
||||
| Exit | `<Esc>` |
|
||||
|
||||
`n`, `N`, `]`, `[`, `q`, `Q`, `i`, `a`, `I`, `A` and `<Esc>` work on regions, the others on regions and cursors.
|
||||
Outside a session, these keys keep their Vim meaning. Every action has a `<Plug>` mapping named after it, e.g.
|
||||
`<Plug>(VM-Find-Under)`.
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
" Change mappings, an empty string disables one
|
||||
let g:VM_maps = {}
|
||||
let g:VM_maps['Find Under'] = '<C-d>'
|
||||
let g:VM_maps['Find Subword Under'] = '<C-d>'
|
||||
|
||||
let g:VM_leader = '\' " Default is '\\'
|
||||
let g:VM_default_mappings = 0 " Keep only <C-n>
|
||||
let g:VM_case_setting = 'smart' " 'smart', 'sensitive' or 'ignore', default follows 'ignorecase'
|
||||
let g:VM_skip_shorter_lines = 0 " <C-Down> and <C-Up> don't skip shorter lines
|
||||
let g:VM_skip_empty_lines = 1 " <C-Down> and <C-Up> skip empty lines
|
||||
let g:VM_quit_after_leaving_insert_mode = 1
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>visual-star-search: Makes * and # search for the selected text</h2></summary>
|
||||
|
||||
|
||||
+44
-29
@@ -40,35 +40,50 @@ Plug 'nerdtree'
|
||||
- `:NERDTreeFind`
|
||||
- `:NERDTreeRefreshRoot`
|
||||
|
||||
| Key | Description | Map Setting |
|
||||
|---------|--------------------------------------------------------|--------------------------------|
|
||||
| `o` | Open files, directories and bookmarks | `g:NERDTreeMapActivateNode` |
|
||||
| `go` | Open selected file, but leave cursor in the NERDTree | `g:NERDTreeMapPreview` |
|
||||
| `t` | Open selected node/bookmark in a new tab | `g:NERDTreeMapOpenInTab` |
|
||||
| `T` | Same as 't' but keep the focus on the current tab | `g:NERDTreeMapOpenInTabSilent` |
|
||||
| `i` | Open selected file in a split window | `g:NERDTreeMapOpenSplit` |
|
||||
| `gi` | Same as i, but leave the cursor on the NERDTree | `g:NERDTreeMapPreviewSplit` |
|
||||
| `s` | Open selected file in a new vsplit | `g:NERDTreeMapOpenVSplit` |
|
||||
| `gs` | Same as s, but leave the cursor on the NERDTree | `g:NERDTreeMapPreviewVSplit` |
|
||||
| `O` | Recursively open the selected directory | `g:NERDTreeMapOpenRecursively` |
|
||||
| `x` | Close the current nodes parent | `g:NERDTreeMapCloseDir` |
|
||||
| `X` | Recursively close all children of the current node | `g:NERDTreeMapCloseChildren` |
|
||||
| `P` | Jump to the root node | `g:NERDTreeMapJumpRoot` |
|
||||
| `p` | Jump to current nodes parent | `g:NERDTreeMapJumpParent` |
|
||||
| `K` | Jump up inside directories at the current tree depth | `g:NERDTreeMapJumpFirstChild` |
|
||||
| `J` | Jump down inside directories at the current tree depth | `g:NERDTreeMapJumpLastChild` |
|
||||
| `<C-J>` | Jump down to next sibling of the current directory | `g:NERDTreeMapJumpNextSibling` |
|
||||
| `<C-K>` | Jump up to previous sibling of the current directory | `g:NERDTreeMapJumpPrevSibling` |
|
||||
| `<C-D>` | Move the selection half a page down | - |
|
||||
| `<C-U>` | Move the selection half a page up | - |
|
||||
| `r` | Recursively refresh the current directory | `g:NERDTreeMapRefresh` |
|
||||
| `R` | Recursively refresh the current root | `g:NERDTreeMapRefreshRoot` |
|
||||
| `m` | Display the NERDTree menu | `g:NERDTreeMapMenu` |
|
||||
| `q` | Close the NERDTree window | `g:NERDTreeMapQuit` |
|
||||
| `A` | Zoom (maximize/minimize) the NERDTree window | `g:NERDTreeMapToggleZoom` |
|
||||
| `d` | Delete file or directory | `g:NERDTreeMapDelete` |
|
||||
| `n` | Create File | `g:NERDTreeMapNewFile` |
|
||||
| `N` | Create Directory | `g:NERDTreeMapNewDir` |
|
||||
| Key | Description | Map Setting |
|
||||
|---------|--------------------------------------------------------|-----------------------------------|
|
||||
| `o` | Open files, directories and bookmarks | `g:NERDTreeMapActivateNode` |
|
||||
| `go` | Open selected file, but leave cursor in the NERDTree | `g:NERDTreeMapPreview` |
|
||||
| `t` | Open selected node/bookmark in a new tab | `g:NERDTreeMapOpenInTab` |
|
||||
| `T` | Same as 't' but keep the focus on the current tab | `g:NERDTreeMapOpenInTabSilent` |
|
||||
| `i` | Open selected file in a split window | `g:NERDTreeMapOpenSplit` |
|
||||
| `gi` | Same as i, but leave the cursor on the NERDTree | `g:NERDTreeMapPreviewSplit` |
|
||||
| `s` | Open selected file in a new vsplit | `g:NERDTreeMapOpenVSplit` |
|
||||
| `gs` | Same as s, but leave the cursor on the NERDTree | `g:NERDTreeMapPreviewVSplit` |
|
||||
| `O` | Recursively open the selected directory | `g:NERDTreeMapOpenRecursively` |
|
||||
| `x` | Close the current nodes parent | `g:NERDTreeMapCloseDir` |
|
||||
| `X` | Recursively close all children of the current node | `g:NERDTreeMapCloseChildren` |
|
||||
| `P` | Jump to the root node | `g:NERDTreeMapJumpRoot` |
|
||||
| `p` | Jump to current nodes parent | `g:NERDTreeMapJumpParent` |
|
||||
| `K` | Jump up inside directories at the current tree depth | `g:NERDTreeMapJumpFirstChild` |
|
||||
| `J` | Jump down inside directories at the current tree depth | `g:NERDTreeMapJumpLastChild` |
|
||||
| `<C-J>` | Jump down to next sibling of the current directory | `g:NERDTreeMapJumpNextSibling` |
|
||||
| `<C-K>` | Jump up to previous sibling of the current directory | `g:NERDTreeMapJumpPrevSibling` |
|
||||
| `j` | Move the selection down | `g:NERDTreeMapSelectNext` |
|
||||
| `k` | Move the selection up | `g:NERDTreeMapSelectPrevious` |
|
||||
| `gg` | Jump to the first node | `g:NERDTreeMapSelectFirst` |
|
||||
| `G` | Jump to the last node | `g:NERDTreeMapSelectLast` |
|
||||
| `<C-D>` | Move the selection half a page down | `g:NERDTreeMapScrollHalfPageDown` |
|
||||
| `<C-U>` | Move the selection half a page up | `g:NERDTreeMapScrollHalfPageUp` |
|
||||
| `/` | Start speed search | `g:NERDTreeMapSpeedSearch` |
|
||||
| `r` | Recursively refresh the current directory | `g:NERDTreeMapRefresh` |
|
||||
| `R` | Recursively refresh the current root | `g:NERDTreeMapRefreshRoot` |
|
||||
| `m` | Display the NERDTree menu | `g:NERDTreeMapMenu` |
|
||||
| `q` | Close the NERDTree window | `g:NERDTreeMapQuit` |
|
||||
| `A` | Zoom (maximize/minimize) the NERDTree window | `g:NERDTreeMapToggleZoom` |
|
||||
| `d` | Delete file or directory | `g:NERDTreeMapDelete` |
|
||||
| `n` | Create File | `g:NERDTreeMapNewFile` |
|
||||
| `N` | Create Directory | `g:NERDTreeMapNewDir` |
|
||||
|
||||
### Changing the keys
|
||||
|
||||
Every key from the table above can be changed by setting the corresponding variable in your `~/.ideavimrc`:
|
||||
```vim
|
||||
let g:NERDTreeMapActivateNode = 'l'
|
||||
let g:NERDTreeMapJumpParent = 'h'
|
||||
let g:NERDTreeMapSelectNext = '<C-N>'
|
||||
set NERDTree
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
|
||||
+42
-4
@@ -9,9 +9,13 @@ However, some differences are inevitable.
|
||||
|
||||
```
|
||||
'autoindent' 'ai' Copy indent from current line when starting a new line
|
||||
Unlike Vim, this option is on by default, which matches Neovim. With
|
||||
the option on, the indent is removed again if you leave Insert mode
|
||||
without typing anything.
|
||||
On by default, unlike Vim, matching Neovim. Only the copying is
|
||||
controlled: the indent the IDE works out from the code is the
|
||||
counterpart of 'indentexpr' and stays either way, so 'noautoindent'
|
||||
gives column 0 only in a file the IDE has no formatter for.
|
||||
Unlike Vim, a line opened by 'o', 'O' or Enter keeps its indent when
|
||||
you leave Insert mode without typing. 'cc' and 'S' clear it, unless
|
||||
"Keep indents on empty lines" (Editor | Code Style) is on.
|
||||
|
||||
'clipboard' 'cb' Defines clipboard behavior
|
||||
A comma-separated list of words to control clipboard behaviour:
|
||||
@@ -251,10 +255,21 @@ The old name is deprecated, but still accepted by `:set` and by the `&{option}`
|
||||
focused in Normal mode, so the first <Esc> leaves
|
||||
Insert or Visual mode and the next one defocuses the
|
||||
tool window.
|
||||
main Tabs in the main editor area that are not backed by
|
||||
a regular file, e.g. editors opened by third-party
|
||||
plugins for in-memory or decrypted content. Not
|
||||
enabled by default. Enable with
|
||||
`:set ideaeditor+=main`.
|
||||
chat The prompt input of AI chats: JetBrains AI Assistant
|
||||
(including Junie in AI Assistant) and AIR. The input
|
||||
is focused in Insert mode, so <Enter> still sends the
|
||||
message; in Normal mode <Enter> is a Vim motion. Chat
|
||||
answers are not affected. Not enabled by default.
|
||||
Enable with `:set ideaeditor+=chat`.
|
||||
|
||||
Remove a value to revert that editor to its native behaviour with no
|
||||
Vim keybindings. The change takes effect immediately for all open
|
||||
console windows.
|
||||
console and chat input windows.
|
||||
|
||||
This option replaces 'ideapythonconsole', which has been removed.
|
||||
|
||||
@@ -274,6 +289,21 @@ The old name is deprecated, but still accepted by `:set` and by the `&{option}`
|
||||
global
|
||||
Maps Vim's global marks to IDE bookmarks.
|
||||
|
||||
'ideaoutsideeditor' boolean (default on)
|
||||
global
|
||||
When on, Normal mode mappings to IDE actions keep working when no
|
||||
file is open, e.g. `map <leader>sf <Action>(GotoFile)` pressed while
|
||||
the Project tool window has the focus and every editor tab is closed.
|
||||
|
||||
Only user mappings whose right-hand side consists of <Action>(...)
|
||||
sequences take part, since there is no editor for anything else to
|
||||
act on. Text fields, such as Search Everywhere, and modal dialogs are
|
||||
not affected, and keys that do not start such a mapping are left to
|
||||
the IDE. <Esc> cancels a partially typed mapping.
|
||||
|
||||
Turn the option off to leave all keys typed outside an editor to the
|
||||
IDE. The change takes effect immediately.
|
||||
|
||||
'idearefactormode' string (default "select")
|
||||
global or local to buffer
|
||||
Specifies the mode to be used when a refactoring selects text to be
|
||||
@@ -285,6 +315,14 @@ The old name is deprecated, but still accepted by `:set` and by the `&{option}`
|
||||
This option is only used when the refactoring is started in Normal,
|
||||
Insert or Replace mode. Visual or Select modes are not changed.
|
||||
|
||||
'ideascrollcursor' boolean (default off)
|
||||
global
|
||||
When on, scrolling with the mouse wheel or by dragging the scrollbar
|
||||
moves the cursor along with the text, so it stays on screen, like in
|
||||
Vim. The cursor is kept 'scrolloff' lines away from the edge of the
|
||||
window. When off, IntelliJ's default behaviour is kept and the cursor
|
||||
stays where it is, even if it scrolls out of view.
|
||||
|
||||
'ideastatusicon' string (default "enabled")
|
||||
global
|
||||
This option controls the behaviour and appearance of the IdeaVim icon
|
||||
|
||||
@@ -33,6 +33,9 @@ By using `sethandler` you can define handlers:
|
||||
- For a single shortcut: `sethandler <C-A> n:vim i-x:ide` - use Vim handler in normal mode and IDE handler in insert and visual modes,
|
||||
- For all shortcuts: `sethandler n:vim i:ide` - use Vim handlers in normal mode and IDE handlers in insert mode.
|
||||
|
||||
`<CR>`, `<Esc>` and the plain arrow keys (`<Up>`, `<Down>`, `<Left>`, `<Right>`) are always handled by Vim unless
|
||||
you set a handler for that particular key, e.g. `sethandler <CR> i:ide`. Handlers for all shortcuts don't apply to them.
|
||||
|
||||
Later definitions override earlier ones, so `a:ide c:vim` first assigns every mode to the IDE
|
||||
and then hands command-line mode back to Vim. This is useful for keeping a `cnoremap` working
|
||||
while the IDE owns the shortcut everywhere else:
|
||||
|
||||
+2
-2
@@ -18,13 +18,14 @@ ideaVersion=2026.1
|
||||
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
||||
ideaType=IU
|
||||
instrumentPluginCode=true
|
||||
version=SNAPSHOT
|
||||
version=9999.62-chylex
|
||||
javaVersion=21
|
||||
remoteRobotVersion=0.11.23
|
||||
antlrVersion=4.10.1
|
||||
# Please don't forget to update kotlin version in buildscript section
|
||||
# Also update kotlinxSerializationVersion version
|
||||
kotlinVersion=2.3.20
|
||||
kspVersion=2.3.7
|
||||
publishToken=token
|
||||
publishChannels=eap
|
||||
# Kotlinx serialization also uses some version of kotlin stdlib under the hood. However,
|
||||
@@ -35,7 +36,6 @@ slackUrl=
|
||||
youtrackToken=
|
||||
# Gradle settings
|
||||
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
Vendored
+41
-11
@@ -26,6 +26,33 @@
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
@rem Catch executions from older scripts and ensure they exit cleanly.
|
||||
@rem This can be removed once we can be reasonably confident that few people
|
||||
@rem will be migrating directly to this new wrapper.
|
||||
goto afterSafetyNet
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
goto exitWithErrorLevel
|
||||
:afterSafetyNet
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
@@ -45,13 +72,14 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
1>&2 echo.
|
||||
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
1>&2 echo.
|
||||
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||
1>&2 echo location of your Java installation.
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -59,13 +87,14 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
1>&2 echo.
|
||||
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
1>&2 echo.
|
||||
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||
1>&2 echo location of your Java installation.
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
@@ -75,8 +104,9 @@ echo location of your Java installation. 1>&2
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
|
||||
|
||||
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.group.action
|
||||
|
||||
/**
|
||||
* RPC handler for [ActionIdRemoteApi]. Stores the frontend's actions for the ideavimrc completion.
|
||||
*/
|
||||
internal class ActionIdRemoteApiImpl : ActionIdRemoteApi {
|
||||
override suspend fun setFrontendActions(actions: List<ActionIdInfo>) {
|
||||
FrontendActionIds.getInstance().actions = actions.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.group.action
|
||||
|
||||
import com.intellij.platform.rpc.backend.RemoteApiProvider
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
|
||||
/**
|
||||
* Registers [ActionIdRemoteApiImpl] as the backend RPC handler for [ActionIdRemoteApi].
|
||||
* Registered via `platform.rpc.backend.remoteApiProvider` extension point in ideavim-backend.xml.
|
||||
*/
|
||||
internal class ActionIdRemoteApiProvider : RemoteApiProvider {
|
||||
override fun RemoteApiProvider.Sink.remoteApis() {
|
||||
remoteApi(remoteApiDescriptor<ActionIdRemoteApi>()) {
|
||||
ActionIdRemoteApiImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
-35
@@ -8,66 +8,126 @@
|
||||
|
||||
package com.maddyhome.idea.vim.group.format
|
||||
|
||||
import com.intellij.application.options.CodeStyle
|
||||
import com.intellij.lang.LanguageFormatting
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.impl.EditorId
|
||||
import com.intellij.openapi.editor.impl.findEditorOrNull
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.codeStyle.ExternalFormatProcessor
|
||||
import com.intellij.psi.util.PsiUtilCore
|
||||
import com.maddyhome.idea.vim.group.onEdt
|
||||
|
||||
/**
|
||||
* RPC handler for [FormatRemoteApi].
|
||||
*
|
||||
* Sets selection on the backend editor and executes the platform's auto-indent action.
|
||||
* Because this runs on the backend, [CommandProcessor] groups all document modifications
|
||||
* as a single undo step.
|
||||
*
|
||||
* Multiple ranges are processed bottom-to-top so that formatting one range does not
|
||||
* shift the line numbers of ranges above it.
|
||||
*
|
||||
* Caret positioning after formatting is handled by the caller (vim-engine layer),
|
||||
* not by this backend handler, so that each caller can position the correct caret
|
||||
* (important for per-caret handlers like `==`).
|
||||
* Runs on the backend, so [CommandProcessor] groups the document modifications into a single undo
|
||||
* step. Caret positioning is left to the caller (vim-engine layer), which knows which caret the
|
||||
* range belongs to - important for per-caret handlers like `==`.
|
||||
*/
|
||||
class FormatRemoteApiImpl : FormatRemoteApi {
|
||||
internal class FormatRemoteApiImpl : FormatRemoteApi {
|
||||
override suspend fun format(
|
||||
editorId: EditorId,
|
||||
startOffsets: List<Int>,
|
||||
endOffsets: List<Int>,
|
||||
startOffset: Int,
|
||||
endOffset: Int,
|
||||
) = onEdt {
|
||||
val editor = editorId.findEditorOrNull() ?: return@onEdt
|
||||
val document = editor.document
|
||||
val project = editor.project ?: return@onEdt
|
||||
val lines = expandToWholeLines(editor.document, startOffset, endOffset)
|
||||
|
||||
// Convert offsets to line ranges, adjusting endLine when it falls exactly on a line start
|
||||
val lineRanges = getLineRanges(startOffsets, endOffsets, document)
|
||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
|
||||
val externalFormatter = psiFile?.let { ExternalFormatProcessor.activeExternalFormatProcessor(it) }
|
||||
if (psiFile != null && externalFormatter != null) {
|
||||
formatExternally(externalFormatter, project, psiFile, lines)
|
||||
} else {
|
||||
autoIndentLines(editor, lines)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort ranges by startLine descending so we format bottom-to-top.
|
||||
// This ensures formatting a later range doesn't shift line numbers of earlier ranges.
|
||||
val sortedRanges = lineRanges.sortedByDescending { it.first }
|
||||
/**
|
||||
* Goes through [CodeStyle.getLineIndent], as the Enter handler does: a
|
||||
* [LineIndentProvider][com.intellij.psi.codeStyle.lineIndent.LineIndentProvider] of the language answers straight
|
||||
* from the text, and the formatter, which needs the PSI to be up to date, is only asked if there is none.
|
||||
*
|
||||
* The document is committed up front because the fallback to the formatter would otherwise commit it from inside the
|
||||
* read action.
|
||||
*/
|
||||
override suspend fun lineIndent(editorId: EditorId, offset: Int): String? = onEdt {
|
||||
val editor = editorId.findEditorOrNull() ?: return@onEdt null
|
||||
val project = editor.project ?: return@onEdt null
|
||||
val documentManager = PsiDocumentManager.getInstance(project)
|
||||
ApplicationManager.getApplication().runWriteAction { documentManager.commitDocument(editor.document) }
|
||||
|
||||
ApplicationManager.getApplication().runReadAction<String?> {
|
||||
val psiFile = documentManager.getPsiFile(editor.document) ?: return@runReadAction null
|
||||
if (LanguageFormatting.INSTANCE.forContext(psiFile) == null) return@runReadAction null
|
||||
val language = PsiUtilCore.getLanguageAtOffset(psiFile, offset)
|
||||
CodeStyle.getLineIndent(editor, language, offset, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A file owned by an [ExternalFormatProcessor] (C/C++ in CLion Nova, where formatting lives in the
|
||||
* Radler process) cannot be handled by the auto-indent action: that action is delegated further,
|
||||
* to the external process, which reads its range from the editor selection - and a selection set
|
||||
* in the same event-loop turn as the dispatch does not get there in time, so the action falls back
|
||||
* to its no-selection behaviour and reformats the whole file.
|
||||
*
|
||||
* The processor is called directly rather than through `CodeStyleManager.reformatText`, which
|
||||
* re-derives the range from PSI smart pointers (`CoreCodeStyleUtil.RangeFormatInfo`). The C++ PSI
|
||||
* in Nova is a dummy tree of coarse nodes, so the range that comes out of it is not the one that
|
||||
* went in.
|
||||
*
|
||||
* Note that this reformats [lines] instead of only re-indenting them; Nova exposes no indent-only
|
||||
* entry point.
|
||||
*/
|
||||
private fun formatExternally(
|
||||
formatter: ExternalFormatProcessor,
|
||||
project: Project,
|
||||
psiFile: PsiFile,
|
||||
lines: TextRange,
|
||||
) {
|
||||
CommandProcessor.getInstance().executeCommand(project, {
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
val documentManager = PsiDocumentManager.getInstance(project)
|
||||
documentManager.getDocument(psiFile)?.let { documentManager.commitDocument(it) }
|
||||
formatter.format(psiFile, lines, false, false, true, -1)
|
||||
}
|
||||
}, COMMAND_NAME, null)
|
||||
}
|
||||
|
||||
/** The auto-indent action takes its range from the editor selection, hence the selection dance. */
|
||||
private fun autoIndentLines(editor: Editor, lines: TextRange) {
|
||||
CommandProcessor.getInstance().executeCommand(editor.project, {
|
||||
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_AUTO_INDENT_LINES)
|
||||
for ((startLine, endLine) in sortedRanges) {
|
||||
val selStart = document.getLineStartOffset(startLine)
|
||||
val selEnd = document.getLineEndOffset(endLine)
|
||||
editor.caretModel.currentCaret.setSelection(selStart, selEnd, false)
|
||||
editor.caretModel.currentCaret.setSelection(lines.startOffset, lines.endOffset, false)
|
||||
try {
|
||||
ActionManager.getInstance().tryToExecute(action, null, editor.contentComponent, "IdeaVim", true).waitFor(5_000)
|
||||
} finally {
|
||||
editor.selectionModel.removeSelection()
|
||||
}
|
||||
editor.selectionModel.removeSelection()
|
||||
}, "AutoIndent", null)
|
||||
}, COMMAND_NAME, null)
|
||||
}
|
||||
|
||||
private fun getLineRanges(
|
||||
startOffsets: List<Int>,
|
||||
endOffsets: List<Int>,
|
||||
document: Document,
|
||||
): List<Pair<Int, Int>> = startOffsets.zip(endOffsets).map { (start, end) ->
|
||||
val startLine = document.getLineNumber(start)
|
||||
var endLine = document.getLineNumber(end)
|
||||
if (endLine > startLine && document.getLineStartOffset(endLine) == end) {
|
||||
private fun expandToWholeLines(document: Document, startOffset: Int, endOffset: Int): TextRange {
|
||||
val startLine = document.getLineNumber(startOffset)
|
||||
var endLine = document.getLineNumber(endOffset)
|
||||
// A linewise range ends at the start of the line *after* the last one - don't pull that line in
|
||||
if (endLine > startLine && document.getLineStartOffset(endLine) == endOffset) {
|
||||
endLine--
|
||||
}
|
||||
startLine to endLine
|
||||
return TextRange(document.getLineStartOffset(startLine), document.getLineEndOffset(endLine))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val COMMAND_NAME = "AutoIndent"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,9 @@
|
||||
<!-- RPC: expose document changes with undo grouping for split mode -->
|
||||
<platform.rpc.backend.remoteApiProvider
|
||||
implementation="com.maddyhome.idea.vim.group.change.ChangeRemoteApiProvider"/>
|
||||
|
||||
<!-- RPC: receive the frontend's action IDs for the ideavimrc completion -->
|
||||
<platform.rpc.backend.remoteApiProvider
|
||||
implementation="com.maddyhome.idea.vim.group.action.ActionIdRemoteApiProvider"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -20,6 +20,16 @@ val ideaType: String by project
|
||||
val ideaVersion: String by project
|
||||
val javaVersion: String by project
|
||||
|
||||
val ideaBranchNumber = ideaVersion.substringBefore('-').split('.').let { parts ->
|
||||
val head = parts[0].toIntOrNull() ?: Int.MAX_VALUE // LATEST-EAP-SNAPSHOT
|
||||
val isProductVersion = head > 1000 // 2026.2 rather than 262.8665.258
|
||||
if (isProductVersion) (head % 100) * 10 + (parts.getOrNull(1)?.toIntOrNull() ?: 1) else head
|
||||
}
|
||||
|
||||
// intellij.platform.rpc has a module descriptor since 2026.1; in 2025.3 its classes are in the core and
|
||||
// product-info.json doesn't list it, which fails resolution.
|
||||
val rpcModuleExists = ideaBranchNumber >= 261
|
||||
|
||||
repositories {
|
||||
maven { url = uri("https://cache-redirector.jetbrains.com/repo.maven.apache.org/maven2") }
|
||||
maven("https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
||||
@@ -52,6 +62,7 @@ dependencies {
|
||||
|
||||
bundledModule("intellij.spellchecker")
|
||||
bundledModule("intellij.platform.kernel.impl")
|
||||
if (rpcModuleExists) bundledModule("intellij.platform.rpc")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +91,6 @@ ksp {
|
||||
arg("vimscript_functions_file", "intellij_vimscript_functions.json")
|
||||
arg("ex_commands_file", "intellij_ex_commands.json")
|
||||
arg("commands_file", "intellij_commands.json")
|
||||
arg("extensions_file", "ideavim_extensions.json")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.completion
|
||||
|
||||
import com.intellij.codeInsight.AutoPopupController
|
||||
import com.intellij.codeInsight.completion.CompletionContributor
|
||||
import com.intellij.codeInsight.completion.CompletionParameters
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet
|
||||
import com.intellij.codeInsight.completion.PlainPrefixMatcher
|
||||
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.maddyhome.idea.vim.api.ActionIdCompletion
|
||||
import com.maddyhome.idea.vim.group.action.ActionIdInfo
|
||||
import com.maddyhome.idea.vim.group.action.FrontendActionIds
|
||||
import com.maddyhome.idea.vim.vimscript.services.VimRcService
|
||||
|
||||
/**
|
||||
* Completes IDE action IDs in the ideavimrc file, in `<Action>(...)` and in the `:action` argument.
|
||||
*
|
||||
* IdeaVim doesn't register a file type for the ideavimrc: the platform opens it with the bundled TextMate VimL
|
||||
* grammar, or the user's own file type association. So the contributor is registered for any language and recognises
|
||||
* the file by its name. It is registered in the common module because in split mode the completion runs on the
|
||||
* backend.
|
||||
*/
|
||||
internal class IdeaVimRcCompletionContributor : CompletionContributor() {
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
if (!isIdeaVimRc(parameters.originalFile)) return
|
||||
|
||||
val text = parameters.originalFile.viewProvider.contents
|
||||
val prefix = ActionIdCompletion.findPrefix(lineBeforeOffset(text, parameters.offset))?.prefix ?: return
|
||||
|
||||
// PlainPrefixMatcher is one of the matchers that can be sent to the frontend lookup in split mode
|
||||
val actionIdResult = result.withPrefixMatcher(PlainPrefixMatcher(prefix, true))
|
||||
val actions = FrontendActionIds.getInstance()
|
||||
ActionIdCompletion.findMatches(actions.allActionIds(), prefix).forEach {
|
||||
actionIdResult.addElement(createLookupElement(it, actions.findInfo(it)))
|
||||
}
|
||||
// Don't mix action IDs with the words from the file offered by the TextMate completion
|
||||
result.stopHere()
|
||||
}
|
||||
}
|
||||
|
||||
/** `ReformatCode Reformat Code` with the description, if any, aligned to the right */
|
||||
private fun createLookupElement(actionId: String, info: ActionIdInfo?): LookupElement {
|
||||
return LookupElementBuilder.create(actionId)
|
||||
.withTailText(info?.text?.let { " $it" }, true)
|
||||
.withTypeText(info?.description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the completion lookup when an action ID is about to be typed: after `<Action>(` or `:action `.
|
||||
*/
|
||||
internal class IdeaVimRcTypedHandler : TypedHandlerDelegate() {
|
||||
override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result {
|
||||
if (charTyped != '(' && charTyped != ' ') return Result.CONTINUE
|
||||
if (!isIdeaVimRc(file)) return Result.CONTINUE
|
||||
|
||||
// Called before the typed character is inserted
|
||||
val line = lineBeforeOffset(editor.document.charsSequence, editor.caretModel.offset) + charTyped
|
||||
if (ActionIdCompletion.findPrefix(line)?.prefix?.isEmpty() != true) return Result.CONTINUE
|
||||
|
||||
AutoPopupController.getInstance(project).scheduleAutoPopup(editor)
|
||||
return Result.STOP
|
||||
}
|
||||
}
|
||||
|
||||
private fun isIdeaVimRc(file: PsiFile): Boolean = file.name.endsWith(VimRcService.VIMRC_FILE_NAME)
|
||||
|
||||
private fun lineBeforeOffset(text: CharSequence, offset: Int): String {
|
||||
val lineStart = text.lastIndexOf('\n', offset - 1) + 1
|
||||
return text.substring(lineStart, offset)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.group.action
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* An action ID with the action's text and description, shown in the ideavimrc completion lookup.
|
||||
* Sent via [ActionIdRemoteApi] from the frontend to the backend.
|
||||
*/
|
||||
@Serializable
|
||||
data class ActionIdInfo(
|
||||
val id: String,
|
||||
val text: String?,
|
||||
val description: String?,
|
||||
) {
|
||||
companion object {
|
||||
/** Reads the text and description of [action], which can be an action stub, so the action is not instantiated */
|
||||
fun of(id: String, action: AnAction): ActionIdInfo {
|
||||
val presentation = action.templatePresentation
|
||||
return ActionIdInfo(id, presentation.text?.ifBlank { null }, presentation.description?.ifBlank { null })
|
||||
}
|
||||
|
||||
/** All actions of the local ActionManager */
|
||||
fun collect(): List<ActionIdInfo> {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
return actionManager.getActionIdList("").mapNotNull { id -> actionManager.getActionOrStub(id)?.let { of(id, it) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.group.action
|
||||
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* RPC interface for sharing the frontend's actions with the backend.
|
||||
*
|
||||
* In split mode, completion in the ideavimrc editor runs on the backend, whose ActionManager doesn't know the
|
||||
* frontend-only actions. `:action` and `<Action>(...)` are executed on the frontend, so the frontend sends its action
|
||||
* IDs, with texts and descriptions for the lookup, and the backend offers them in completion alongside its own.
|
||||
* See [FrontendActionIds].
|
||||
*/
|
||||
@Rpc
|
||||
@ApiStatus.Internal
|
||||
interface ActionIdRemoteApi : RemoteApi<Unit> {
|
||||
suspend fun setFrontendActions(actions: List<ActionIdInfo>)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): ActionIdRemoteApi {
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<ActionIdRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.group.action
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
|
||||
/**
|
||||
* Actions that can be used in `:action` and `<Action>(...)`.
|
||||
*
|
||||
* Those are executed on the frontend, so in split mode the backend's own ActionManager isn't enough: the frontend
|
||||
* sends its actions over [ActionIdRemoteApi] and they are kept here. In monolith and on the frontend, nothing is
|
||||
* received and the local ActionManager already has every action.
|
||||
*/
|
||||
@Service(Service.Level.APP)
|
||||
class FrontendActionIds {
|
||||
/** Actions received from the frontend, by ID */
|
||||
@Volatile
|
||||
var actions: Map<String, ActionIdInfo> = emptyMap()
|
||||
|
||||
fun allActionIds(): Set<String> = ActionManager.getInstance().getActionIdList("").toSet() + actions.keys
|
||||
|
||||
/** Prefers the local action, the frontend only knows the actions missing here */
|
||||
fun findInfo(actionId: String): ActionIdInfo? {
|
||||
val action = ActionManager.getInstance().getActionOrStub(actionId) ?: return actions[actionId]
|
||||
return ActionIdInfo.of(actionId, action)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(): FrontendActionIds = service()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -10,7 +10,7 @@ package com.maddyhome.idea.vim.group.bookmark
|
||||
|
||||
import com.intellij.ide.vfs.VirtualFileId
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -78,7 +78,7 @@ interface BookmarkRemoteApi : RemoteApi<Unit> {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): BookmarkRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<BookmarkRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<BookmarkRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
package com.maddyhome.idea.vim.group.change
|
||||
|
||||
import com.intellij.openapi.editor.impl.EditorId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -34,7 +34,7 @@ interface ChangeRemoteApi : RemoteApi<Unit> {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): ChangeRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<ChangeRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<ChangeRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@
|
||||
package com.maddyhome.idea.vim.group.comment
|
||||
|
||||
import com.intellij.openapi.editor.impl.EditorId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -55,7 +55,7 @@ interface CommentaryRemoteApi : RemoteApi<Unit> {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): CommentaryRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<CommentaryRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<CommentaryRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ package com.maddyhome.idea.vim.group.file
|
||||
import com.intellij.ide.vfs.VirtualFileId
|
||||
import com.intellij.openapi.editor.impl.EditorId
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -62,7 +62,7 @@ interface FileRemoteApi : RemoteApi<Unit> {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): FileRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<FileRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<FileRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -9,7 +9,7 @@
|
||||
package com.maddyhome.idea.vim.group.format
|
||||
|
||||
import com.intellij.openapi.editor.impl.EditorId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -18,12 +18,22 @@ import org.jetbrains.annotations.ApiStatus
|
||||
@Rpc
|
||||
@ApiStatus.Internal
|
||||
interface FormatRemoteApi : RemoteApi<Unit> {
|
||||
suspend fun format(editorId: EditorId, startOffsets: List<Int>, endOffsets: List<Int>)
|
||||
/**
|
||||
* Auto-indents the lines covered by [startOffset]..[endOffset]. Several ranges mean several calls,
|
||||
* which the caller must issue bottom-to-top so that one range doesn't shift the ranges above it.
|
||||
*/
|
||||
suspend fun format(editorId: EditorId, startOffset: Int, endOffset: Int)
|
||||
|
||||
/**
|
||||
* The indent the language gives to the line at [offset], as the IDE works it out for a new line. Null if the
|
||||
* language has no formatter, or if the indent of the line is not to be adjusted.
|
||||
*/
|
||||
suspend fun lineIndent(editorId: EditorId, offset: Int): String?
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): FormatRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<FormatRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<FormatRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
package com.maddyhome.idea.vim.group.jump
|
||||
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -29,7 +29,7 @@ interface JumpRemoteApi : RemoteApi<Unit> {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): JumpRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<JumpRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<JumpRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
|
||||
package com.maddyhome.idea.vim.group.process
|
||||
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
@@ -50,7 +50,7 @@ interface ProcessRemoteApi : RemoteApi<Unit> {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
suspend fun getInstance(): ProcessRemoteApi {
|
||||
return RemoteApiProviderService.resolve(remoteApiDescriptor<ProcessRemoteApi>())
|
||||
return VimRemoteApiResolver.resolve(remoteApiDescriptor<ProcessRemoteApi>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.group.rpc
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.RemoteApiDescriptor
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
private val resolverEP = ExtensionPointName.create<VimRemoteApiResolver>("IdeaVIM.remoteApiResolver")
|
||||
|
||||
/**
|
||||
* Resolves Fleet RPC remote APIs via [RemoteApiProviderService] without making this module depend on the
|
||||
* `intellij.platform.rpc` module.
|
||||
*
|
||||
* That module doesn't exist in 2025.3 (the classes live in the core classloader), is embedded in the core
|
||||
* classloader in 2026.1 and 2026.2, and is a standalone module since 2026.3. A required dependency on it from this
|
||||
* embedded module would disable the whole plugin on 2025.3, so the dependency lives in the optional
|
||||
* `IdeaVIM.ideavim-rpc` module, which registers the resolver whenever the module exists. Without it, we fall back
|
||||
* to [CoreRemoteApiResolver], which works as long as [RemoteApiProviderService] is in the core classloader.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
interface VimRemoteApiResolver {
|
||||
suspend fun <T : RemoteApi<Unit>> resolve(descriptor: RemoteApiDescriptor<T>): T
|
||||
|
||||
companion object {
|
||||
suspend fun <T : RemoteApi<Unit>> resolve(descriptor: RemoteApiDescriptor<T>): T {
|
||||
val resolver = resolverEP.extensionList.firstOrNull() ?: CoreRemoteApiResolver
|
||||
return resolver.resolve(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same call as in `IdeaVIM.ideavim-rpc`, but [RemoteApiProviderService] is resolved through the plugin's own
|
||||
* classloader, so it only works on IDEs where the class is in the core classloader (2026.2 and earlier).
|
||||
*/
|
||||
private object CoreRemoteApiResolver : VimRemoteApiResolver {
|
||||
override suspend fun <T : RemoteApi<Unit>> resolve(descriptor: RemoteApiDescriptor<T>): T {
|
||||
return RemoteApiProviderService.resolve(descriptor)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,13 @@
|
||||
<idea-plugin>
|
||||
<!-- Shared code loaded in ALL product modes (monolith, client, backend). -->
|
||||
|
||||
<!-- No dependency on intellij.platform.rpc here: it doesn't exist in 2025.3, and an unresolved dependency of this
|
||||
embedded module disables the whole plugin. IdeaVIM.ideavim-rpc (optional) carries it instead. -->
|
||||
<extensionPoints>
|
||||
<extensionPoint interface="com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver" dynamic="true"
|
||||
name="remoteApiResolver"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<!-- Services that must be available in ALL product modes (used by both frontend and backend).
|
||||
Frontend-only services are registered in ideavim-frontend.xml.
|
||||
@@ -20,6 +27,12 @@
|
||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.IjVimSystemInfoService"
|
||||
serviceInterface="com.maddyhome.idea.vim.api.SystemInfoService"/>
|
||||
|
||||
<!-- Action ID completion in the ideavimrc file. Registered here, because in split mode the completion
|
||||
runs on the backend, while in monolith everything runs in one process. -->
|
||||
<completion.contributor language="any" order="first"
|
||||
implementationClass="com.maddyhome.idea.vim.completion.IdeaVimRcCompletionContributor"/>
|
||||
<typedHandler implementation="com.maddyhome.idea.vim.completion.IdeaVimRcTypedHandler"/>
|
||||
|
||||
</extensions>
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[
|
||||
]
|
||||
@@ -280,6 +280,10 @@
|
||||
<editorFactoryDocumentListener
|
||||
implementation="com.maddyhome.idea.vim.listener.VimListenerManager$VimDocumentListener"/>
|
||||
|
||||
<!-- Pasting with the IDE paste action replaces text in Replace mode -->
|
||||
<copyPastePostProcessor implementation="com.maddyhome.idea.vim.group.copy.ReplaceModePastePostProcessor"
|
||||
order="last"/>
|
||||
|
||||
<!-- Registry keys -->
|
||||
<registryKey defaultValue="true" description="Enable IdeaVim only in editor component and project tree"
|
||||
key="ideavim.only.in.editor.component"/>
|
||||
@@ -317,6 +321,7 @@
|
||||
</group>
|
||||
|
||||
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
|
||||
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
|
||||
</actions>
|
||||
|
||||
<!-- Frontend vim extensions (editor/text manipulation, no PSI/file-system dependency) -->
|
||||
@@ -376,6 +381,15 @@
|
||||
</aliases>
|
||||
</vimExtension>
|
||||
|
||||
<vimExtension implementation="com.maddyhome.idea.vim.extension.visualmulti.VimVisualMultiExtension"
|
||||
name="visual-multi">
|
||||
<aliases>
|
||||
<alias name="https://github.com/mg979/vim-visual-multi"/>
|
||||
<alias name="mg979/vim-visual-multi"/>
|
||||
<alias name="vim-visual-multi"/>
|
||||
</aliases>
|
||||
</vimExtension>
|
||||
|
||||
<vimExtension implementation="com.maddyhome.idea.vim.extension.textobjentire.VimTextObjEntireExtension"
|
||||
name="textobj-entire">
|
||||
<aliases>
|
||||
@@ -491,6 +505,8 @@
|
||||
</aliases>
|
||||
</vimExtension>
|
||||
|
||||
<vimExtension implementation="com.maddyhome.idea.vim.extension.keylog.KeyLogExtension" name="keylog"/>
|
||||
|
||||
<vimExtension implementation="com.maddyhome.idea.vim.extension.yankring.YankRingExtension"
|
||||
name="yankring">
|
||||
<aliases>
|
||||
@@ -523,6 +539,15 @@
|
||||
</aliases>
|
||||
</vimExtension>
|
||||
|
||||
<vimExtension implementation="com.maddyhome.idea.vim.extension.cleverf.CleverFExtension"
|
||||
name="clever-f">
|
||||
<aliases>
|
||||
<alias name="https://github.com/rhysd/clever-f.vim"/>
|
||||
<alias name="rhysd/clever-f.vim"/>
|
||||
<alias name="clever-f.vim"/>
|
||||
</aliases>
|
||||
</vimExtension>
|
||||
|
||||
<vimExtension implementation="com.maddyhome.idea.vim.extension.camelcasemotion.CamelCaseMotion"
|
||||
name="CamelCaseMotion">
|
||||
<aliases>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.
|
||||
*/
|
||||
|
||||
// Optional module carrying the dependency on intellij.platform.rpc, which IdeaVIM.ideavim-common can't declare
|
||||
// (see VimRemoteApiResolver). The platform skips it on 2025.3, where the module doesn't exist.
|
||||
|
||||
plugins {
|
||||
java
|
||||
kotlin("jvm")
|
||||
id("org.jetbrains.intellij.platform.module")
|
||||
}
|
||||
|
||||
val kotlinVersion: String by project
|
||||
val ideaType: String by project
|
||||
val ideaVersion: String by project
|
||||
val javaVersion: String by project
|
||||
|
||||
val ideaBranchNumber = ideaVersion.substringBefore('-').split('.').let { parts ->
|
||||
val head = parts[0].toIntOrNull() ?: Int.MAX_VALUE // LATEST-EAP-SNAPSHOT
|
||||
val isProductVersion = head > 1000 // 2026.2 rather than 262.8665.258
|
||||
if (isProductVersion) (head % 100) * 10 + (parts.getOrNull(1)?.toIntOrNull() ?: 1) else head
|
||||
}
|
||||
|
||||
// intellij.platform.rpc has a module descriptor since 2026.1; in 2025.3 its classes are in the core and
|
||||
// product-info.json doesn't list it, which fails resolution.
|
||||
val rpcModuleExists = ideaBranchNumber >= 261
|
||||
|
||||
repositories {
|
||||
maven { url = uri("https://cache-redirector.jetbrains.com/repo.maven.apache.org/maven2") }
|
||||
intellijPlatform {
|
||||
defaultRepositories()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":ideavim-common"))
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
|
||||
|
||||
intellijPlatform {
|
||||
var useInstaller = "EAP-SNAPSHOT" !in ideaVersion
|
||||
if (ideaType == "RD") {
|
||||
useInstaller = false
|
||||
}
|
||||
|
||||
create(ideaType, ideaVersion) { this.useInstaller = useInstaller }
|
||||
|
||||
if (rpcModuleExists) bundledModule("intellij.platform.rpc")
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(javaVersion))
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(javaVersion))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.rpcImpl
|
||||
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import com.maddyhome.idea.vim.group.rpc.VimRemoteApiResolver
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.RemoteApiDescriptor
|
||||
|
||||
class PlatformRemoteApiResolver : VimRemoteApiResolver {
|
||||
override suspend fun <T : RemoteApi<Unit>> resolve(descriptor: RemoteApiDescriptor<T>): T {
|
||||
return RemoteApiProviderService.resolve(descriptor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
~ Copyright 2003-2026 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.
|
||||
-->
|
||||
|
||||
<!-- Provides RemoteApiProviderService to IdeaVIM.ideavim-common on IDEs where intellij.platform.rpc is a module
|
||||
(2026.1+). loading="optional", so on 2025.3, where the module doesn't exist, this is a clean skip and
|
||||
ideavim-common falls back to the core classloader. -->
|
||||
<idea-plugin package="com.maddyhome.idea.vim.rpcImpl">
|
||||
<dependencies>
|
||||
<module name="intellij.platform.rpc"/>
|
||||
<module name="IdeaVIM.ideavim-common"/>
|
||||
</dependencies>
|
||||
<extensions defaultExtensionNs="IdeaVIM">
|
||||
<remoteApiResolver implementation="com.maddyhome.idea.vim.rpcImpl.PlatformRemoteApiResolver"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1,137 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {prepareReleaseChangelog, toBeReleasedSection} from "./prepareReleaseChangelog.js";
|
||||
|
||||
const preamble = `The Changelog
|
||||
=============
|
||||
`;
|
||||
|
||||
const older = `## 2.47.0, 2026-09-24
|
||||
|
||||
### Features:
|
||||
* Older feature
|
||||
`;
|
||||
|
||||
const masterContent = `${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
* [VIM-3](https://youtrack.jetbrains.com/issue/VIM-3) Feature landed after the release branch was cut
|
||||
* \`:help\` entry without a ticket that the release branch already documents
|
||||
* Entry without a ticket that only master documents
|
||||
|
||||
### Fixes:
|
||||
* [VIM-2](https://youtrack.jetbrains.com/issue/VIM-2) Fix cherry-picked to the release branch
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: Released PR
|
||||
* [2100](https://github.com/JetBrains/ideavim/pull/2100) by someone: PR merged after the cut
|
||||
* [2101](https://github.com/JetBrains/ideavim/pull/2101) by someone: PR merged after the cut without its number in the commit
|
||||
* [2102](https://github.com/JetBrains/ideavim/pull/2102) by someone: Released PR without its number in the commit
|
||||
|
||||
### Changes:
|
||||
* [VIM-4](https://youtrack.jetbrains.com/issue/VIM-4) Change released before the cut, documented on master after it
|
||||
|
||||
${older}`;
|
||||
|
||||
const releaseContent = `${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
* \`:help\` entry without a ticket that the release branch already documents
|
||||
|
||||
### Merged PRs:
|
||||
* [2102](https://github.com/JetBrains/ideavim/pull/2102) by someone: Released PR without its number in the commit
|
||||
|
||||
${older}`;
|
||||
|
||||
const masterOnlyCommits = [
|
||||
"Fix(VIM-3): landed after the cut",
|
||||
"Fix(VIM-2): cherry-picked",
|
||||
"Something for the next release (#2100)",
|
||||
"Update changelog",
|
||||
];
|
||||
const releaseOnlyCommits = ["Fix(VIM-2): cherry-picked", "Preparation to 2.47.2 release"];
|
||||
const releaseCommits = [
|
||||
...releaseOnlyCommits,
|
||||
"Fix(VIM-1): released",
|
||||
"Released PR (#2099)",
|
||||
"VIM-4 released before the cut",
|
||||
"Old commit",
|
||||
];
|
||||
|
||||
describe("prepareReleaseChangelog", () => {
|
||||
it("keeps master's entries for the release branch and drops the ones that are only on master", () => {
|
||||
const result = prepareReleaseChangelog({
|
||||
masterContent,
|
||||
releaseContent,
|
||||
commits: {masterOnly: masterOnlyCommits, releaseOnly: releaseOnlyCommits, release: releaseCommits},
|
||||
});
|
||||
expect(result).toBe(`${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
* \`:help\` entry without a ticket that the release branch already documents
|
||||
|
||||
### Fixes:
|
||||
* [VIM-2](https://youtrack.jetbrains.com/issue/VIM-2) Fix cherry-picked to the release branch
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: Released PR
|
||||
* [2102](https://github.com/JetBrains/ideavim/pull/2102) by someone: Released PR without its number in the commit
|
||||
|
||||
### Changes:
|
||||
* [VIM-4](https://youtrack.jetbrains.com/issue/VIM-4) Change released before the cut, documented on master after it
|
||||
|
||||
${older}`);
|
||||
});
|
||||
|
||||
it("uses master's changelog as is when the branches do not differ", () => {
|
||||
const result = prepareReleaseChangelog({
|
||||
masterContent,
|
||||
releaseContent: `${preamble}\n${older}`,
|
||||
commits: {masterOnly: [], releaseOnly: [], release: ["Old commit"]},
|
||||
});
|
||||
expect(result).toBe(masterContent);
|
||||
});
|
||||
|
||||
it("leaves a changelog without To Be Released alone", () => {
|
||||
const content = `${preamble}\n${older}`;
|
||||
expect(prepareReleaseChangelog({
|
||||
masterContent: content,
|
||||
releaseContent: content,
|
||||
commits: {masterOnly: ["Fix(VIM-9): whatever"], releaseOnly: [], release: []},
|
||||
})).toBe(content);
|
||||
});
|
||||
|
||||
it("drops an entry whose ticket is mentioned without the Fix() prefix", () => {
|
||||
const result = prepareReleaseChangelog({
|
||||
masterContent,
|
||||
releaseContent,
|
||||
commits: {
|
||||
masterOnly: ["VIM-1 follow-up after the cut", ...masterOnlyCommits],
|
||||
releaseOnly: releaseOnlyCommits,
|
||||
release: releaseCommits,
|
||||
},
|
||||
});
|
||||
expect(result).not.toContain("Released feature");
|
||||
});
|
||||
|
||||
it("gives the To Be Released section alone", () => {
|
||||
expect(toBeReleasedSection(`${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Fixes:
|
||||
* A fix
|
||||
|
||||
|
||||
${older}`)).toBe(`## To Be Released
|
||||
|
||||
### Fixes:
|
||||
* A fix
|
||||
`);
|
||||
expect(toBeReleasedSection(`${preamble}\n${older}`)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Writes the CHANGES.md the release branch is released with.
|
||||
*
|
||||
* The changelog is maintained on master, and master may be ahead of the release branch (the EAP
|
||||
* build is the only thing that moves the release branch to master). So the release gets master's
|
||||
* CHANGES.md with the `## To Be Released` entries that are only on master taken out, as releaseContent.ts
|
||||
* tells them apart:
|
||||
* - an entry that links a VIM ticket or a PR the release branch does not contain is dropped, and one
|
||||
* it contains is kept,
|
||||
* - an entry the commits say nothing about (no ticket or PR, or a PR merged without its number in
|
||||
* the commit subject) is kept only if the release branch's own CHANGES.md already has it.
|
||||
* The dropped entries stay under `## To Be Released` on master (see syncChangelogToMaster.ts) and
|
||||
* go out with the next release.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts-ts/src/prepareReleaseChangelog.ts <root-dir> <master-ref> [release-ref]
|
||||
* On the release branch: rewrites its CHANGES.md from <master-ref>'s.
|
||||
* npx tsx scripts-ts/src/prepareReleaseChangelog.ts --print <root-dir> <release-ref>
|
||||
* On master: prints the `## To Be Released` section the release would get from the working tree's
|
||||
* CHANGES.md. The Prepare What's New workflow describes exactly this on the page.
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx scripts-ts/src/prepareReleaseChangelog.ts .. origin/master
|
||||
* npx tsx scripts-ts/src/prepareReleaseChangelog.ts --print . origin/release
|
||||
*/
|
||||
|
||||
import {execFileSync} from "node:child_process";
|
||||
import {readFileSync, writeFileSync} from "node:fs";
|
||||
import {join} from "node:path";
|
||||
import {TO_BE_RELEASED_HEADER} from "./promoteChangelog.js";
|
||||
import {type BranchCommits, readBranchCommits, ReleaseContent} from "./releaseContent.js";
|
||||
import {entryKey, entryKeys, findSection, isEntry, unreleasedEntries} from "./syncChangelogToMaster.js";
|
||||
|
||||
export interface PrepareArgs {
|
||||
masterContent: string;
|
||||
releaseContent: string;
|
||||
commits: BranchCommits;
|
||||
}
|
||||
|
||||
export function prepareReleaseChangelog(args: PrepareArgs): string {
|
||||
const masterLines = args.masterContent.split("\n");
|
||||
const toBeReleased = findSection(masterLines, (line) => line === TO_BE_RELEASED_HEADER);
|
||||
if (!toBeReleased) return args.masterContent;
|
||||
|
||||
const content = new ReleaseContent(args.commits);
|
||||
// Nothing landed on master after the cut, so everything in the changelog is released
|
||||
if (content.isUpToDate()) return args.masterContent;
|
||||
|
||||
const releaseSection = findSection(args.releaseContent.split("\n"), (line) => line === TO_BE_RELEASED_HEADER);
|
||||
const knownToRelease = releaseSection ? entryKeys(releaseSection.lines) : new Set<string>();
|
||||
|
||||
const dropped = new Set<string>();
|
||||
for (const line of toBeReleased.lines.filter(isEntry)) {
|
||||
const key = entryKey(line);
|
||||
const verdict = content.verdict(key);
|
||||
if (verdict === "not-released" || (verdict === "unknown" && !knownToRelease.has(key))) dropped.add(key);
|
||||
}
|
||||
|
||||
const remaining = unreleasedEntries(toBeReleased.lines, dropped);
|
||||
const section = remaining.some(isEntry) ? [TO_BE_RELEASED_HEADER, "", ...remaining] : [];
|
||||
return [
|
||||
...masterLines.slice(0, toBeReleased.start),
|
||||
...section,
|
||||
...masterLines.slice(toBeReleased.end),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** The `## To Be Released` section of [content], or an empty string if it has none. */
|
||||
export function toBeReleasedSection(content: string): string {
|
||||
const section = findSection(content.split("\n"), (line) => line === TO_BE_RELEASED_HEADER);
|
||||
return section ? section.lines.join("\n").trimEnd() + "\n" : "";
|
||||
}
|
||||
|
||||
function showFile(rootDir: string, ref: string, path: string): string {
|
||||
return execFileSync("git", ["show", `${ref}:${path}`], {cwd: rootDir, encoding: "utf-8"});
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args[0] === "--print") {
|
||||
const [, rootDir, releaseRef] = args;
|
||||
if (!rootDir || !releaseRef) {
|
||||
console.error("Usage: prepareReleaseChangelog.ts --print <root-dir> <release-ref>");
|
||||
process.exit(1);
|
||||
}
|
||||
const prepared = prepareReleaseChangelog({
|
||||
masterContent: readFileSync(join(rootDir, "CHANGES.md"), "utf-8"),
|
||||
releaseContent: showFile(rootDir, releaseRef, "CHANGES.md"),
|
||||
commits: readBranchCommits(rootDir, "HEAD", releaseRef),
|
||||
});
|
||||
const section = toBeReleasedSection(prepared);
|
||||
process.stdout.write(section || `Nothing under ${TO_BE_RELEASED_HEADER} is on ${releaseRef}\n`);
|
||||
} else {
|
||||
const [rootDir, masterRef, releaseRef = "HEAD"] = args;
|
||||
if (!rootDir || !masterRef) {
|
||||
console.error("Usage: prepareReleaseChangelog.ts <root-dir> <master-ref> [release-ref]");
|
||||
process.exit(1);
|
||||
}
|
||||
const changelogPath = join(rootDir, "CHANGES.md");
|
||||
const commits = readBranchCommits(rootDir, masterRef, releaseRef);
|
||||
console.log(`${commits.masterOnly.length} commits only on ${masterRef}, ${commits.releaseOnly.length} only on ${releaseRef}`);
|
||||
|
||||
const updated = prepareReleaseChangelog({
|
||||
masterContent: showFile(rootDir, masterRef, "CHANGES.md"),
|
||||
releaseContent: readFileSync(changelogPath, "utf-8"),
|
||||
commits,
|
||||
});
|
||||
writeFileSync(changelogPath, updated);
|
||||
console.log(`Wrote ${changelogPath} from ${masterRef} without the entries that are only on ${masterRef}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {selectReleasedTickets} from "./releaseActions.js";
|
||||
import {ReleaseContent} from "./releaseContent.js";
|
||||
|
||||
describe("releaseActions", () => {
|
||||
const content = new ReleaseContent({
|
||||
masterOnly: ["VIM-2 landed after the cut", "VIM-3 cherry-picked", "Some refactoring"],
|
||||
releaseOnly: ["VIM-3 cherry-picked", "Preparation to 2.48.0 release"],
|
||||
release: ["VIM-3 cherry-picked", "VIM-1 released"],
|
||||
});
|
||||
|
||||
it("marks only the Ready To Release tickets that the release branch contains", () => {
|
||||
expect(selectReleasedTickets(["VIM-1", "VIM-2", "VIM-3"], content)).toEqual(["VIM-1", "VIM-3"]);
|
||||
});
|
||||
|
||||
it("releases a ticket moved to Ready To Release by hand", () => {
|
||||
expect(selectReleasedTickets(["VIM-4"], content)).toEqual(["VIM-4"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Marks the Ready To Release YouTrack tickets that the release contains as fixed in it.
|
||||
*
|
||||
* The release is built from the release branch, so a ticket whose change is only on master stays in
|
||||
* Ready To Release for the next release. releaseContent.ts decides that, the same way it decides which
|
||||
* changelog entries the release has.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts-ts/src/releaseActions.ts <version> <release-type> <root-dir> [master-ref] [release-ref]
|
||||
*
|
||||
* Example (after the release build pushed the release branch):
|
||||
* npx tsx scripts-ts/src/releaseActions.ts 2.48.0 minor .. origin/master release
|
||||
*/
|
||||
|
||||
import {readBranchCommits, ReleaseContent, ticketKey} from "./releaseContent.js";
|
||||
import {
|
||||
createReleaseVersion,
|
||||
getTicketsByQuery,
|
||||
getVersionIdByName,
|
||||
setFixVersion,
|
||||
setStatus
|
||||
} from "./tools/youtrack.js";
|
||||
|
||||
export const READY_TO_RELEASE_QUERY = "#{Ready To Release}";
|
||||
|
||||
/**
|
||||
* The tickets that are fixed in the release. A ticket no commit mentions was moved to Ready To Release
|
||||
* by hand, and nothing says it is missing from the release, so it is released.
|
||||
*/
|
||||
export function selectReleasedTickets(readyToRelease: string[], content: ReleaseContent): string[] {
|
||||
return readyToRelease.filter((ticket) => content.verdict(ticketKey(ticket)) !== "not-released");
|
||||
}
|
||||
|
||||
export async function runReleaseActions(version: string, content: ReleaseContent): Promise<void> {
|
||||
const readyToRelease = await getTicketsByQuery(READY_TO_RELEASE_QUERY);
|
||||
console.log(`Tickets in Ready To Release: ${JSON.stringify(readyToRelease)}`);
|
||||
|
||||
const tickets = selectReleasedTickets(readyToRelease, content);
|
||||
const skipped = readyToRelease.filter((ticket) => !tickets.includes(ticket));
|
||||
if (skipped.length > 0) {
|
||||
console.log(`Only on master, left in Ready To Release: ${JSON.stringify(skipped)}`);
|
||||
}
|
||||
if (tickets.length === 0) {
|
||||
console.log("No tickets to update");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ticket of tickets) await setStatus(ticket, "Fixed");
|
||||
|
||||
if (await getVersionIdByName(version) === null) await createReleaseVersion(version);
|
||||
for (const ticket of tickets) await setFixVersion(ticket, version);
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
const [version, releaseType, rootDir, masterRef = "origin/master", releaseRef = "release"] = process.argv.slice(2);
|
||||
if (!version || !releaseType || !rootDir) {
|
||||
console.error("Usage: releaseActions.ts <version> <release-type> <root-dir> [master-ref] [release-ref]");
|
||||
process.exit(1);
|
||||
}
|
||||
if (releaseType === "patch") {
|
||||
console.log("Skipping release actions for patch release");
|
||||
} else {
|
||||
const content = new ReleaseContent(readBranchCommits(rootDir, masterRef, releaseRef));
|
||||
runReleaseActions(version, content).catch((error) => {
|
||||
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {commitKeys, ReleaseContent} from "./releaseContent.js";
|
||||
|
||||
describe("commitKeys", () => {
|
||||
it("reads tickets and PR numbers out of commit subjects", () => {
|
||||
expect(commitKeys([
|
||||
"Fix(VIM-1): a",
|
||||
"VIM-2524 calculate indent after cc",
|
||||
"Squashed PR (#2099)",
|
||||
"Merge pull request #2100 from someone/branch",
|
||||
"vim-7 lower case",
|
||||
"VIM-8 and VIM-9 at once",
|
||||
])).toEqual(new Set([
|
||||
"ticket:VIM-1", "ticket:VIM-2524", "pr:2099", "pr:2100", "ticket:VIM-7", "ticket:VIM-8", "ticket:VIM-9",
|
||||
]));
|
||||
});
|
||||
|
||||
it("does not take a PR number from the middle of a subject", () => {
|
||||
expect(commitKeys(["Revert the change from #2100"])).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReleaseContent", () => {
|
||||
const content = new ReleaseContent({
|
||||
masterOnly: ["VIM-3 landed after the cut", "VIM-2 cherry-picked", "VIM-5 fixed again after the cut"],
|
||||
releaseOnly: ["VIM-2 cherry-picked"],
|
||||
release: ["VIM-2 cherry-picked", "VIM-1 released", "VIM-5 released before the cut"],
|
||||
});
|
||||
|
||||
it("tells released, not released and unknown tickets apart", () => {
|
||||
expect(content.verdict("ticket:VIM-1")).toBe("released");
|
||||
expect(content.verdict("ticket:VIM-2")).toBe("released");
|
||||
expect(content.verdict("ticket:VIM-3")).toBe("not-released");
|
||||
expect(content.verdict("ticket:VIM-4")).toBe("unknown");
|
||||
});
|
||||
|
||||
it("holds a ticket back while some of its commits are only on master", () => {
|
||||
expect(content.verdict("ticket:VIM-5")).toBe("not-released");
|
||||
});
|
||||
|
||||
it("is up to date only when master has no commits of its own", () => {
|
||||
expect(content.isUpToDate()).toBe(false);
|
||||
expect(new ReleaseContent({masterOnly: ["Some refactoring"], releaseOnly: [], release: []}).isUpToDate()).toBe(false);
|
||||
expect(new ReleaseContent({masterOnly: [], releaseOnly: ["Preparation to 2.48.0 release"], release: []})
|
||||
.isUpToDate()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Tells which tickets and PRs the release branch contains.
|
||||
*
|
||||
* The release is built from the `release` branch, which only follows master when the EAP build resets
|
||||
* it, so master can hold changes the release does not have. The changelog of the release
|
||||
* (prepareReleaseChangelog.ts), its What's New page (written from that changelog by the Prepare What's
|
||||
* New workflow) and the YouTrack tickets marked as fixed in it (releaseActions.ts) all decide what is
|
||||
* released here, so they never disagree.
|
||||
*
|
||||
* A ticket or a PR is identified by the commits that mention it: any `VIM-123` in a commit subject, and
|
||||
* `(#123)` or `Merge pull request #123` for a PR.
|
||||
*/
|
||||
|
||||
import {execFileSync} from "node:child_process";
|
||||
|
||||
export interface BranchCommits {
|
||||
/** Subjects of the commits on master but not on the release branch, `git log release..master`. */
|
||||
masterOnly: string[];
|
||||
/** Subjects of the commits on the release branch but not on master, `git log master..release`. */
|
||||
releaseOnly: string[];
|
||||
/** Subjects of all the commits on the release branch, `git log release`. */
|
||||
release: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* - `released`: a commit on the release branch mentions it,
|
||||
* - `not-released`: it is mentioned only by commits that landed on master after the release branch was
|
||||
* cut, and none of them was cherry-picked to the release branch,
|
||||
* - `unknown`: no commit mentions it, e.g. a changelog entry without a ticket or a ticket closed by hand.
|
||||
*/
|
||||
export type Verdict = "released" | "not-released" | "unknown";
|
||||
|
||||
export class ReleaseContent {
|
||||
private readonly onlyOnMaster: Set<string>;
|
||||
private readonly onRelease: Set<string>;
|
||||
private readonly masterIsAhead: boolean;
|
||||
|
||||
constructor(commits: BranchCommits) {
|
||||
this.masterIsAhead = commits.masterOnly.length > 0;
|
||||
const cherryPicked = commitKeys(commits.releaseOnly);
|
||||
this.onlyOnMaster = new Set([...commitKeys(commits.masterOnly)].filter((key) => !cherryPicked.has(key)));
|
||||
this.onRelease = commitKeys(commits.release);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether nothing landed on master after the release branch was cut, so everything master has is
|
||||
* released. Commits that mention no ticket count too: they can be changes without a ticket.
|
||||
*/
|
||||
isUpToDate(): boolean {
|
||||
return !this.masterIsAhead;
|
||||
}
|
||||
|
||||
/** [key] is `ticket:VIM-123` or `pr:123`, see [commitKeys]. */
|
||||
verdict(key: string): Verdict {
|
||||
// Checked first: a ticket fixed again on master was mentioned on the release branch before
|
||||
if (this.onlyOnMaster.has(key)) return "not-released";
|
||||
if (this.onRelease.has(key)) return "released";
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** The tickets and PRs a list of commit subjects mentions, as `ticket:VIM-123` and `pr:123` keys. */
|
||||
export function commitKeys(subjects: string[]): Set<string> {
|
||||
const keys = new Set<string>();
|
||||
for (const subject of subjects) {
|
||||
for (const match of subject.matchAll(/\b(VIM-\d+)\b/gi)) keys.add(ticketKey(match[1]));
|
||||
for (const match of subject.matchAll(/\(#(\d+)\)|^Merge pull request #(\d+)\b/g)) {
|
||||
keys.add(`pr:${match[1] ?? match[2]}`);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function ticketKey(ticket: string): string {
|
||||
return `ticket:${ticket.toUpperCase()}`;
|
||||
}
|
||||
|
||||
export function readBranchCommits(rootDir: string, masterRef: string, releaseRef: string): BranchCommits {
|
||||
const subjects = (...range: string[]) =>
|
||||
execFileSync("git", ["log", "--format=%s", ...range], {cwd: rootDir, encoding: "utf-8"})
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
return {
|
||||
masterOnly: subjects(`${releaseRef}..${masterRef}`),
|
||||
releaseOnly: subjects(`${masterRef}..${releaseRef}`),
|
||||
release: subjects(releaseRef),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {syncChangelogToMaster} from "./syncChangelogToMaster.js";
|
||||
|
||||
const preamble = `The Changelog
|
||||
=============
|
||||
|
||||
History of changes in IdeaVim for the IntelliJ platform.
|
||||
`;
|
||||
|
||||
const older = `## 2.47.0, 2026-09-24
|
||||
|
||||
### Features:
|
||||
* Older feature
|
||||
`;
|
||||
|
||||
const releaseContent = `${preamble}
|
||||
## 2.48.0, 2026-10-09
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
|
||||
### Fixes:
|
||||
* [VIM-2](https://youtrack.jetbrains.com/issue/VIM-2) Released fix
|
||||
|
||||
${older}`;
|
||||
|
||||
const masterContent = `${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
* [VIM-3](https://youtrack.jetbrains.com/issue/VIM-3) Feature landed after the release branch was cut
|
||||
|
||||
### Fixes:
|
||||
* [VIM-2](https://youtrack.jetbrains.com/issue/VIM-2) Released fix
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: PR landed after the cut
|
||||
|
||||
${older}`;
|
||||
|
||||
describe("syncChangelogToMaster", () => {
|
||||
it("copies the released section and keeps only the unreleased entries under To Be Released", () => {
|
||||
const result = syncChangelogToMaster({version: "2.48.0", masterContent, releaseContent});
|
||||
expect(result).toBe(`${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-3](https://youtrack.jetbrains.com/issue/VIM-3) Feature landed after the release branch was cut
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: PR landed after the cut
|
||||
|
||||
## 2.48.0, 2026-10-09
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
|
||||
### Fixes:
|
||||
* [VIM-2](https://youtrack.jetbrains.com/issue/VIM-2) Released fix
|
||||
|
||||
${older}`);
|
||||
});
|
||||
|
||||
it("drops To Be Released when everything in it was released", () => {
|
||||
const master = `${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
|
||||
### Fixes:
|
||||
* [VIM-2](https://youtrack.jetbrains.com/issue/VIM-2) Released fix
|
||||
|
||||
${older}`;
|
||||
const result = syncChangelogToMaster({version: "2.48.0", masterContent: master, releaseContent});
|
||||
expect(result).toBe(releaseContent);
|
||||
});
|
||||
|
||||
it("keeps the continuation lines of an unreleased entry", () => {
|
||||
const master = `${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-3](https://youtrack.jetbrains.com/issue/VIM-3) Multi-line entry
|
||||
that continues on the next line
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
|
||||
${older}`;
|
||||
const result = syncChangelogToMaster({version: "2.48.0", masterContent: master, releaseContent});
|
||||
expect(result).toContain(`### Features:
|
||||
* [VIM-3](https://youtrack.jetbrains.com/issue/VIM-3) Multi-line entry
|
||||
that continues on the next line
|
||||
|
||||
## 2.48.0, 2026-10-09`);
|
||||
expect(result).not.toContain("## To Be Released\n\n### Features:\n* [VIM-1]");
|
||||
});
|
||||
|
||||
it("inserts the released section above the first version when master has no To Be Released", () => {
|
||||
const master = `${preamble}
|
||||
${older}`;
|
||||
const result = syncChangelogToMaster({version: "2.48.0", masterContent: master, releaseContent});
|
||||
expect(result).toBe(releaseContent);
|
||||
});
|
||||
|
||||
it("leaves master alone for a patch release, which promotes nothing", () => {
|
||||
const result = syncChangelogToMaster({version: "2.48.1", masterContent, releaseContent});
|
||||
expect(result).toBe(masterContent);
|
||||
});
|
||||
|
||||
it("treats a differently worded entry for the same ticket or PR as released", () => {
|
||||
const master = `${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Same feature, worded by the master changelog run
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: Same PR, other wording
|
||||
* [2100](https://github.com/JetBrains/ideavim/pull/2100) by someone: Not released
|
||||
|
||||
${older}`;
|
||||
const release = `${preamble}
|
||||
## 2.48.0, 2026-10-09
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: PR
|
||||
|
||||
${older}`;
|
||||
const result = syncChangelogToMaster({version: "2.48.0", masterContent: master, releaseContent: release});
|
||||
expect(result).toBe(`${preamble}
|
||||
## To Be Released
|
||||
|
||||
### Merged PRs:
|
||||
* [2100](https://github.com/JetBrains/ideavim/pull/2100) by someone: Not released
|
||||
|
||||
## 2.48.0, 2026-10-09
|
||||
|
||||
### Features:
|
||||
* [VIM-1](https://youtrack.jetbrains.com/issue/VIM-1) Released feature
|
||||
|
||||
### Merged PRs:
|
||||
* [2099](https://github.com/JetBrains/ideavim/pull/2099) by someone: PR
|
||||
|
||||
${older}`);
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
const once = syncChangelogToMaster({version: "2.48.0", masterContent, releaseContent});
|
||||
const twice = syncChangelogToMaster({version: "2.48.0", masterContent: once, releaseContent});
|
||||
expect(twice).toBe(once);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Brings the released section of CHANGES.md from the release branch over to master.
|
||||
*
|
||||
* The release branch only follows master when the EAP build resets it, so by the time a release
|
||||
* is published master's `## To Be Released` section can hold entries the release does not contain.
|
||||
* Promoting master's section wholesale would label those as released. Instead, the section that
|
||||
* promoteChangelog.ts produced on the release branch is copied as is, and only the entries that
|
||||
* are not in it stay under `## To Be Released` on master.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts-ts/src/syncChangelogToMaster.ts <version> <master CHANGES.md> <release CHANGES.md>
|
||||
*
|
||||
* Example:
|
||||
* git show release:CHANGES.md > /tmp/release-CHANGES.md
|
||||
* npx tsx scripts-ts/src/syncChangelogToMaster.ts 2.48.0 CHANGES.md /tmp/release-CHANGES.md
|
||||
*/
|
||||
|
||||
import {readFileSync, writeFileSync} from "node:fs";
|
||||
import {TO_BE_RELEASED_HEADER} from "./promoteChangelog.js";
|
||||
|
||||
export interface SyncArgs {
|
||||
version: string;
|
||||
masterContent: string;
|
||||
releaseContent: string;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
/** Index of the header line. */
|
||||
start: number;
|
||||
/** Index of the next `## ` header, or the number of lines. */
|
||||
end: number;
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
export const isTopHeader = (line: string): boolean => line.startsWith("## ");
|
||||
export const isSubHeader = (line: string): boolean => line.startsWith("### ");
|
||||
export const isEntry = (line: string): boolean => /^[*-] /.test(line);
|
||||
export const isVersionHeader = (version: string) => (line: string): boolean =>
|
||||
line === `## ${version}` || line.startsWith(`## ${version},`);
|
||||
|
||||
export function syncChangelogToMaster(args: SyncArgs): string {
|
||||
const releaseLines = args.releaseContent.split("\n");
|
||||
const released = findSection(releaseLines, isVersionHeader(args.version));
|
||||
// Nothing was promoted on the release branch (a patch release), so there is nothing to bring over.
|
||||
if (!released) return args.masterContent;
|
||||
|
||||
const masterLines = args.masterContent.split("\n");
|
||||
if (findSection(masterLines, isVersionHeader(args.version))) return args.masterContent;
|
||||
|
||||
const releasedSection = withSingleTrailingBlank(released.lines);
|
||||
const toBeReleased = findSection(masterLines, (line) => line === TO_BE_RELEASED_HEADER);
|
||||
if (!toBeReleased) {
|
||||
const firstHeader = masterLines.findIndex(isTopHeader);
|
||||
const insertAt = firstHeader === -1 ? masterLines.length : firstHeader;
|
||||
return [
|
||||
...masterLines.slice(0, insertAt),
|
||||
...releasedSection,
|
||||
...masterLines.slice(insertAt),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const remaining = unreleasedEntries(toBeReleased.lines, entryKeys(released.lines));
|
||||
const unreleasedSection = remaining.some(isEntry) ? [TO_BE_RELEASED_HEADER, "", ...remaining] : [];
|
||||
return [
|
||||
...masterLines.slice(0, toBeReleased.start),
|
||||
...unreleasedSection,
|
||||
...releasedSection,
|
||||
...masterLines.slice(toBeReleased.end),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function findSection(lines: string[], isHeader: (line: string) => boolean): Section | null {
|
||||
const start = lines.findIndex(isHeader);
|
||||
if (start === -1) return null;
|
||||
let end = start + 1;
|
||||
while (end < lines.length && !isTopHeader(lines[end])) end++;
|
||||
return {start, end, lines: lines.slice(start, end)};
|
||||
}
|
||||
|
||||
export function entryKeys(sectionLines: string[]): Set<string> {
|
||||
return new Set(sectionLines.filter(isEntry).map(entryKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* What identifies an entry across the two branches. The changelog is maintained on both of them, so
|
||||
* the same change can be worded differently; the ticket or the PR it links is stable, the text is not.
|
||||
*/
|
||||
export function entryKey(line: string): string {
|
||||
const ticket = /\[(VIM-\d+)\]\(/.exec(line);
|
||||
if (ticket) return `ticket:${ticket[1]}`;
|
||||
const pr = /\[(\d+)\]\(https:\/\/github\.com\/[^)]*\/pull\/\1\)/.exec(line);
|
||||
if (pr) return `pr:${pr[1]}`;
|
||||
return `text:${line.trim()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of the `## To Be Released` section without the entries that are in [released],
|
||||
* and without the subsections that are left empty by that.
|
||||
*/
|
||||
export function unreleasedEntries(sectionLines: string[], released: Set<string>): string[] {
|
||||
const body = sectionLines.slice(1);
|
||||
const kept: string[] = [];
|
||||
for (let i = 0; i < body.length;) {
|
||||
if (!isEntry(body[i])) {
|
||||
kept.push(body[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// An entry is its bullet line plus the indented continuation lines after it
|
||||
let next = i + 1;
|
||||
while (next < body.length && body[next].trim() !== "" && !isEntry(body[next]) && !body[next].startsWith("#")) {
|
||||
next++;
|
||||
}
|
||||
if (!released.has(entryKey(body[i]))) kept.push(...body.slice(i, next));
|
||||
i = next;
|
||||
}
|
||||
|
||||
const withoutEmptySubsections: string[] = [];
|
||||
for (let i = 0; i < kept.length;) {
|
||||
if (!isSubHeader(kept[i])) {
|
||||
withoutEmptySubsections.push(kept[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
let next = i + 1;
|
||||
while (next < kept.length && !isSubHeader(kept[next])) next++;
|
||||
const subsection = kept.slice(i, next);
|
||||
if (subsection.some(isEntry)) withoutEmptySubsections.push(...subsection);
|
||||
i = next;
|
||||
}
|
||||
|
||||
return withSingleTrailingBlank(collapseBlankLines(withoutEmptySubsections));
|
||||
}
|
||||
|
||||
function collapseBlankLines(lines: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const line of lines) {
|
||||
const blank = line.trim() === "";
|
||||
if (blank && (result.length === 0 || result[result.length - 1].trim() === "")) continue;
|
||||
result.push(line);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withSingleTrailingBlank(lines: string[]): string[] {
|
||||
const result = [...lines];
|
||||
while (result.length > 0 && result[result.length - 1].trim() === "") result.pop();
|
||||
result.push("");
|
||||
return result;
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
const [version, masterPath, releasePath] = process.argv.slice(2);
|
||||
if (!version || !masterPath || !releasePath) {
|
||||
console.error("Usage: syncChangelogToMaster.ts <version> <master CHANGES.md> <release CHANGES.md>");
|
||||
process.exit(1);
|
||||
}
|
||||
const masterContent = readFileSync(masterPath, "utf-8");
|
||||
const updated = syncChangelogToMaster({
|
||||
version,
|
||||
masterContent,
|
||||
releaseContent: readFileSync(releasePath, "utf-8"),
|
||||
});
|
||||
if (updated === masterContent) {
|
||||
console.log(`Changelog unchanged (no ## ${version} section on the release branch, or master already has it)`);
|
||||
} else {
|
||||
writeFileSync(masterPath, updated);
|
||||
console.log(`Brought ## ${version} over to ${masterPath}`);
|
||||
}
|
||||
}
|
||||
@@ -295,12 +295,14 @@ export async function getVersionIdByName(name: string): Promise<string | null> {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
// The query also matches other names that contain this one, such as its EAP versions
|
||||
const version = data.find((v: { name: string }) => v.name === name);
|
||||
if (!version) {
|
||||
console.log(`Version "${name}" not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const versionId = data[0].id;
|
||||
const versionId = version.id;
|
||||
console.log(`Found version "${name}" with ID: ${versionId}`);
|
||||
return versionId;
|
||||
}
|
||||
|
||||
+15
-10
@@ -26,11 +26,11 @@ dependencies {
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
implementation("io.ktor:ktor-client-core:3.5.2")
|
||||
implementation("io.ktor:ktor-client-cio:3.5.2")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:3.5.2")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.5.2")
|
||||
implementation("io.ktor:ktor-client-auth:3.5.2")
|
||||
implementation("io.ktor:ktor-client-core:3.6.0")
|
||||
implementation("io.ktor:ktor-client-cio:3.6.0")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:3.6.0")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.0")
|
||||
implementation("io.ktor:ktor-client-auth:3.6.0")
|
||||
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
|
||||
|
||||
// This is needed for jgit to connect to ssh
|
||||
@@ -39,7 +39,6 @@ dependencies {
|
||||
}
|
||||
|
||||
val releaseType: String? by project
|
||||
val youtrackToken: String by project
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
@@ -82,12 +81,18 @@ tasks.register("calculateNewDevVersion", JavaExec::class) {
|
||||
args = listOf("${rootProject.rootDir}")
|
||||
}
|
||||
|
||||
tasks.register("releaseActions", JavaExec::class) {
|
||||
tasks.register("generateHelp", JavaExec::class) {
|
||||
group = "other"
|
||||
mainClass.set("scripts.ReleaseActionsKt")
|
||||
description = "Generates the help file that :help opens from the descriptions of the commands"
|
||||
// The descriptions are collected by HelpProcessor while running KSP over the modules that declare commands
|
||||
dependsOn(":vim-engine:kspKotlin", ":kspKotlin")
|
||||
mainClass.set("scripts.help.GenerateHelpKt")
|
||||
classpath = sourceSets["main"].runtimeClasspath
|
||||
args = listOf(project.version.toString(), releaseType ?: "")
|
||||
environment("YOUTRACK_TOKEN", youtrackToken)
|
||||
args = listOf(
|
||||
"${rootProject.rootDir}/src/main/resources/help/ideavim.txt",
|
||||
"${rootProject.rootDir}/vim-engine/build/help/engine_help.json",
|
||||
"${rootProject.rootDir}/build/help/frontend_help.json",
|
||||
)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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 scripts.help
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Renders IdeaVim's help file, the one `:help` opens, from the command descriptions collected by `HelpProcessor`.
|
||||
*
|
||||
* Arguments: the path of the help file to write, followed by the paths of the JSON files written by `HelpProcessor`
|
||||
* (one per module).
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
require(args.size >= 2) { "Usage: generateHelp <output file> <help json>..." }
|
||||
val output = File(args[0])
|
||||
val modules = args.drop(1).map { parseHelpData(File(it).readText()) }
|
||||
|
||||
output.parentFile.mkdirs()
|
||||
output.writeText(renderHelp(modules))
|
||||
println("Help file is written to $output")
|
||||
}
|
||||
|
||||
data class ExCommandHelp(val names: List<String>, val description: String)
|
||||
|
||||
data class CommandHelp(val keys: List<String>, val modes: String, val lookup: Boolean, val description: String)
|
||||
|
||||
data class HelpData(val exCommands: List<ExCommandHelp>, val commands: List<CommandHelp>)
|
||||
|
||||
fun parseHelpData(json: String): HelpData {
|
||||
val root = Json.parseToJsonElement(json).jsonObject
|
||||
val exCommands = root.getValue("exCommands").jsonArray.map {
|
||||
val obj = it.jsonObject
|
||||
ExCommandHelp(obj.strings("names"), obj.string("description"))
|
||||
}
|
||||
val commands = root.getValue("commands").jsonArray.map {
|
||||
val obj = it.jsonObject
|
||||
CommandHelp(
|
||||
obj.strings("keys"),
|
||||
obj.string("modes"),
|
||||
obj.getValue("lookup").jsonPrimitive.boolean,
|
||||
obj.string("description"),
|
||||
)
|
||||
}
|
||||
return HelpData(exCommands, commands)
|
||||
}
|
||||
|
||||
private fun JsonObject.string(key: String) = getValue(key).jsonPrimitive.content
|
||||
private fun JsonObject.strings(key: String) = getValue(key).jsonArray.map { it.jsonPrimitive.content }
|
||||
|
||||
/**
|
||||
* A part of the help file. A command shows up in every section whose modes it is available in, except that Visual
|
||||
* mode only lists the commands that aren't in Normal mode already, the same way Vim's `index.txt` does.
|
||||
*/
|
||||
enum class Section(val title: String, val tag: String, val tagPrefix: String) {
|
||||
INSERT("Insert mode", "ideavim-insert", "i_"),
|
||||
NORMAL("Normal and Operator-pending mode", "ideavim-normal", ""),
|
||||
VISUAL("Visual and Select mode", "ideavim-visual", "v_"),
|
||||
CMDLINE("Command-line editing", "ideavim-cmdline", "c_"),
|
||||
EX("Ex commands", "ideavim-ex", ":"),
|
||||
}
|
||||
|
||||
private class Entry(val display: String, val tags: List<String>, val description: String, val sortKey: String)
|
||||
|
||||
const val TEXT_WIDTH = 78
|
||||
private const val DESCRIPTION_COLUMN = 24
|
||||
|
||||
fun renderHelp(modules: List<HelpData>): String {
|
||||
val entries = Section.entries.associateWith { mutableListOf<Entry>() }
|
||||
for (command in modules.flatMap { it.commands }) {
|
||||
for (section in sectionsOf(command.modes)) {
|
||||
val tags = command.keys.map { section.tagPrefix + keyToTag(it) }
|
||||
entries.getValue(section).add(Entry(command.keys.joinToString(" "), tags, command.description, command.sortKey))
|
||||
}
|
||||
}
|
||||
for (command in modules.flatMap { it.exCommands }) {
|
||||
val tags = command.names.flatMap { exCommandTags(it) }
|
||||
val display = command.names.joinToString(" ") { ":$it" }
|
||||
entries.getValue(Section.EX).add(Entry(display, tags, command.description, display.lowercase()))
|
||||
}
|
||||
|
||||
return buildString {
|
||||
appendHeader()
|
||||
// A tag must lead to one place, so when two commands claim it, the first one keeps it. That happens for the
|
||||
// `lookup` handlers, which share their keys with the command they stand in for while a completion popup is open.
|
||||
val usedTags = mutableSetOf<String>()
|
||||
Section.entries.forEachIndexed { index, section ->
|
||||
append("=".repeat(TEXT_WIDTH)).append('\n')
|
||||
appendLine(alignRight("${index + 1}. ${section.title}", "*${section.tag}*"))
|
||||
append('\n')
|
||||
// Different handlers of the same keys, e.g. for different modes, can share the description
|
||||
val sectionEntries = entries.getValue(section).distinctBy { it.display to it.description }
|
||||
for (entry in sectionEntries.sortedWith(compareBy({ it.sortKey }, { it.display }))) {
|
||||
appendEntry(entry, entry.tags.distinct().filter { usedTags.add(it) })
|
||||
}
|
||||
}
|
||||
append("=".repeat(TEXT_WIDTH)).append('\n')
|
||||
append('\n')
|
||||
append("This file is generated from the IdeaVim sources by \"./gradlew generateHelp\".\n")
|
||||
append("Do not edit it by hand, edit the description of the command instead.\n")
|
||||
append('\n')
|
||||
append(" vim:tw=78:ts=8:noet:ft=help:norl:\n")
|
||||
}
|
||||
}
|
||||
|
||||
private val CommandHelp.sortKey: String
|
||||
// Keep a lookup handler right after the command it shares its keys with
|
||||
get() = keys.first().lowercase() + if (lookup) "\u0001" else ""
|
||||
|
||||
private fun sectionsOf(modes: String): List<Section> = buildList {
|
||||
if ('I' in modes) add(Section.INSERT)
|
||||
if ('N' in modes || 'O' in modes) add(Section.NORMAL)
|
||||
else if ('X' in modes || 'S' in modes) add(Section.VISUAL)
|
||||
if ('C' in modes) add(Section.CMDLINE)
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendHeader() {
|
||||
appendLine(alignRight("*ideavim.txt*", "*ideavim*"))
|
||||
append('\n')
|
||||
append(" ".repeat((TEXT_WIDTH - 16) / 2)).append("IDEAVIM COMMANDS\n")
|
||||
append('\n')
|
||||
append(wrap("This file lists the commands that IdeaVim implements. Type \":help\" followed by a subject to jump to it, for example:", 0))
|
||||
append('\n')
|
||||
append(" :help x Normal mode command \"x\"\n")
|
||||
append(" :help v_d Visual mode command \"d\"\n")
|
||||
append(" :help i_CTRL-W Insert mode command CTRL-W\n")
|
||||
append(" :help c_CTRL-R Command-line editing command CTRL-R\n")
|
||||
append(" :help :s Ex command \":s\"\n")
|
||||
append('\n')
|
||||
append("Contents:\n")
|
||||
Section.entries.forEachIndexed { index, section ->
|
||||
append(alignRight("${index + 1}. ${section.title}", "|${section.tag}|"))
|
||||
append('\n')
|
||||
}
|
||||
append('\n')
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendEntry(entry: Entry, tags: List<String>) {
|
||||
for (tagLine in tagLines(tags)) {
|
||||
append(" ".repeat(TEXT_WIDTH - tagLine.length)).append(tagLine).append('\n')
|
||||
}
|
||||
val description = wrap(entry.description, DESCRIPTION_COLUMN).trimStart()
|
||||
if (entry.display.length < DESCRIPTION_COLUMN - 1) {
|
||||
append(entry.display.padEnd(DESCRIPTION_COLUMN)).append(description)
|
||||
} else {
|
||||
append(wrap(entry.display, 0))
|
||||
append(" ".repeat(DESCRIPTION_COLUMN)).append(description)
|
||||
}
|
||||
append('\n')
|
||||
}
|
||||
|
||||
/** Splits the tags into right-aligned lines that fit into the text width */
|
||||
private fun tagLines(tags: List<String>): List<String> {
|
||||
val lines = mutableListOf<String>()
|
||||
var line = ""
|
||||
for (tag in tags.map { "*$it*" }) {
|
||||
line = when {
|
||||
line.isEmpty() -> tag
|
||||
line.length + 1 + tag.length <= TEXT_WIDTH -> "$line $tag"
|
||||
else -> {
|
||||
lines.add(line)
|
||||
tag
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line.isNotEmpty()) lines.add(line)
|
||||
return lines
|
||||
}
|
||||
|
||||
private fun alignRight(left: String, right: String): String =
|
||||
left + " ".repeat((TEXT_WIDTH - left.length - right.length).coerceAtLeast(1)) + right
|
||||
|
||||
/** Wraps [text] to the text width, indenting every line by [indent] spaces. The result ends with a line break. */
|
||||
private fun wrap(text: String, indent: Int): String = buildString {
|
||||
var line = StringBuilder()
|
||||
for (word in text.split(Regex("\\s+")).filter { it.isNotEmpty() }) {
|
||||
if (line.isNotEmpty() && indent + line.length + 1 + word.length > TEXT_WIDTH) {
|
||||
append(" ".repeat(indent)).append(line).append('\n')
|
||||
line = StringBuilder()
|
||||
}
|
||||
if (line.isNotEmpty()) line.append(' ')
|
||||
line.append(word)
|
||||
}
|
||||
if (line.isNotEmpty()) append(" ".repeat(indent)).append(line).append('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* The tags of an Ex command declared as `h[elp]` are `:h` and `:help`, the shortest abbreviation and the full name.
|
||||
*/
|
||||
fun exCommandTags(name: String): List<String> {
|
||||
val abbreviation = name.substringBefore('[')
|
||||
val fullName = name.replace("[", "").replace("]", "")
|
||||
return listOf(abbreviation, fullName).distinct().map { ":" + escapeTagChars(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts keys in IdeaVim's notation to a tag in Vim's: `<C-W>h` becomes `CTRL-W_h` and `g*` becomes `gstar`.
|
||||
*
|
||||
* Mirrors `HelpCommand.normalizeTopic`, which converts what the user types after `:help` the same way.
|
||||
*/
|
||||
fun keyToTag(keys: String): String {
|
||||
val parts = mutableListOf<String>()
|
||||
var i = 0
|
||||
while (i < keys.length) {
|
||||
val close = if (keys[i] == '<') keys.indexOf('>', i + 1) else -1
|
||||
if (close > i + 1) {
|
||||
parts.add(keys.substring(i, close + 1))
|
||||
i = close + 1
|
||||
} else {
|
||||
parts.add(keys[i].toString())
|
||||
i++
|
||||
}
|
||||
}
|
||||
return buildString {
|
||||
var previousIsCtrl = false
|
||||
for (part in parts) {
|
||||
val ctrl = ctrlKey(part)
|
||||
if (isNotEmpty() && (ctrl != null || previousIsCtrl)) append('_')
|
||||
append(ctrl ?: escapeTagChars(part))
|
||||
previousIsCtrl = ctrl != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** `<C-W>` is `CTRL-W` in Vim's help, `<C-Left>` stays as it is */
|
||||
private fun ctrlKey(part: String): String? {
|
||||
if (part.length != 5 || !part.startsWith("<C-", ignoreCase = true) || !part.endsWith('>')) return null
|
||||
return "CTRL-" + part[3].uppercaseChar()
|
||||
}
|
||||
|
||||
/** The characters that can't be a part of a tag in Vim's help, `*` and `|` delimit tags and links */
|
||||
private fun escapeTagChars(text: String): String = text.replace("*", "star").replace("|", "bar")
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package scripts
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val version = args.getOrNull(0) ?: error("Version not provided")
|
||||
val releaseType = args.getOrNull(1)
|
||||
|
||||
performReleaseActions(version, releaseType)
|
||||
}
|
||||
|
||||
fun performReleaseActions(version: String, releaseType: String?) = runBlocking {
|
||||
if (releaseType == "patch") {
|
||||
println("Skipping release actions for patch release")
|
||||
return@runBlocking
|
||||
}
|
||||
|
||||
val tickets = getYoutrackTicketsByQuery("#{Ready To Release}")
|
||||
|
||||
if (tickets.isNotEmpty()) {
|
||||
println("Updating statuses for tickets: $tickets")
|
||||
setYoutrackStatus(tickets, "Fixed")
|
||||
|
||||
println("Checking if version $version exists...")
|
||||
val versionId = getVersionIdByName(version)
|
||||
|
||||
if (versionId == null) {
|
||||
addReleaseToYoutrack(version)
|
||||
} else {
|
||||
println("Version $version already exists in YouTrack. Version id: $versionId")
|
||||
}
|
||||
|
||||
setYoutrackFixVersion(tickets, version)
|
||||
} else {
|
||||
println("No tickets to update statuses")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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 scripts.help
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class GenerateHelpTest {
|
||||
@Test
|
||||
fun `plain keys are the tag`() {
|
||||
assertEquals("dd", keyToTag("dd"))
|
||||
assertEquals("<Left>", keyToTag("<Left>"))
|
||||
assertEquals("<C-Left>", keyToTag("<C-Left>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `control keys use Vim's notation`() {
|
||||
assertEquals("CTRL-W", keyToTag("<C-W>"))
|
||||
assertEquals("CTRL-W", keyToTag("<c-w>"))
|
||||
assertEquals("CTRL-W_h", keyToTag("<C-W>h"))
|
||||
assertEquals("CTRL-W_CTRL-H", keyToTag("<C-W><C-H>"))
|
||||
assertEquals("g_CTRL-G", keyToTag("g<C-G>"))
|
||||
assertEquals("CTRL-\\_CTRL-N", keyToTag("<C-\\><C-N>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `characters that delimit tags are spelled out`() {
|
||||
assertEquals("star", keyToTag("*"))
|
||||
assertEquals("gstar", keyToTag("g*"))
|
||||
assertEquals("bar", keyToTag("|"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ex command tags are the abbreviation and the full name`() {
|
||||
assertEquals(listOf(":h", ":help"), exCommandTags("h[elp]"))
|
||||
assertEquals(listOf(":IdeaPlug"), exCommandTags("IdeaPlug"))
|
||||
assertEquals(listOf(":&"), exCommandTags("&"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `commands are placed into the sections of their modes`() {
|
||||
val help = renderHelp(
|
||||
listOf(
|
||||
HelpData(
|
||||
exCommands = listOf(ExCommandHelp(listOf("h[elp]"), "Open the help.")),
|
||||
commands = listOf(
|
||||
CommandHelp(listOf("w"), "NXO", lookup = false, description = "Move a word forward."),
|
||||
CommandHelp(listOf("d"), "X", lookup = false, description = "Delete the selection."),
|
||||
CommandHelp(listOf("<C-W>"), "IC", lookup = false, description = "Delete a word before the cursor."),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(help.contains("*:h* *:help*\n:h[elp] Open the help.\n"), help)
|
||||
assertTrue(help.contains("*w*\nw Move a word forward.\n"), help)
|
||||
assertTrue(help.contains("*v_d*\nd Delete the selection.\n"), help)
|
||||
assertTrue(help.contains("*i_CTRL-W*\n"), help)
|
||||
assertTrue(help.contains("*c_CTRL-W*\n"), help)
|
||||
// A command that is in Normal mode isn't repeated in the Visual mode section
|
||||
assertTrue(!help.contains("*v_w*"), help)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a tag claimed twice leads to the first command`() {
|
||||
val help = renderHelp(
|
||||
listOf(
|
||||
HelpData(
|
||||
exCommands = emptyList(),
|
||||
commands = listOf(
|
||||
CommandHelp(listOf("<C-Y>"), "I", lookup = true, description = "Accept the completion."),
|
||||
CommandHelp(listOf("<C-Y>"), "I", lookup = false, description = "Insert the character above."),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, Regex("""\*i_CTRL-Y\*""").findAll(help).count(), help)
|
||||
assertTrue(help.indexOf("Insert the character above.") < help.indexOf("Accept the completion."), help)
|
||||
assertTrue(help.contains("*i_CTRL-Y*\n<C-Y> Insert the character above."), help)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long descriptions are wrapped`() {
|
||||
val description = "word ".repeat(40).trim() + "."
|
||||
val help = renderHelp(
|
||||
listOf(HelpData(emptyList(), listOf(CommandHelp(listOf("x"), "N", lookup = false, description = description))))
|
||||
)
|
||||
|
||||
assertTrue(help.lines().all { it.length <= TEXT_WIDTH }, help)
|
||||
assertTrue(help.contains("\n word word"), help)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parses the output of HelpProcessor`() {
|
||||
val json = """
|
||||
{
|
||||
"exCommands": [ { "names": ["h[elp]"], "description": "Open the help.", "className": "HelpCommand" } ],
|
||||
"commands": [
|
||||
{ "keys": ["x"], "modes": "N", "lookup": false, "description": "Delete.", "className": "DeleteAction" }
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(
|
||||
HelpData(
|
||||
listOf(ExCommandHelp(listOf("h[elp]"), "Open the help.")),
|
||||
listOf(CommandHelp(listOf("x"), "N", lookup = false, description = "Delete.")),
|
||||
),
|
||||
parseHelpData(json),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@
|
||||
*/
|
||||
|
||||
pluginManagement {
|
||||
val kspVersion: String by settings
|
||||
plugins {
|
||||
id("com.google.devtools.ksp") version kspVersion
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = uri("https://cache-redirector.jetbrains.com/repo.maven.apache.org/maven2")
|
||||
@@ -48,5 +53,7 @@ include("ideavim-acejump")
|
||||
include("ideavim-rider")
|
||||
include("ideavim-clion-nova")
|
||||
include("ideavim-terminal")
|
||||
include("ideavim-rpc")
|
||||
include("tests:ui-rd-tests")
|
||||
include("tests:split-mode-tests")
|
||||
include("tests:large-file-tests")
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseWheelListener;
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
@@ -116,6 +117,13 @@ public class EventFacade {
|
||||
Disposer.register(disposable, () -> component.removeMouseListener(mouseListener));
|
||||
}
|
||||
|
||||
public void addComponentMouseWheelListener(@NotNull Component component,
|
||||
@NotNull MouseWheelListener mouseWheelListener,
|
||||
@NotNull Disposable disposable) {
|
||||
component.addMouseWheelListener(mouseWheelListener);
|
||||
Disposer.register(disposable, () -> component.removeMouseWheelListener(mouseWheelListener));
|
||||
}
|
||||
|
||||
public void addEditorMouseMotionListener(@NotNull Editor editor,
|
||||
@NotNull EditorMouseMotionListener listener,
|
||||
@NotNull Disposable disposable) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.updateSettings.impl.UpdateSettings
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.group.FrontendActionIdsSync
|
||||
import com.maddyhome.idea.vim.newapi.IjVimEnabler
|
||||
import com.maddyhome.idea.vim.ui.JoinEap
|
||||
import com.maddyhome.idea.vim.ui.JoinEap.EAP_LINK
|
||||
@@ -47,5 +48,7 @@ internal class PluginStartup : ProjectActivity/*, LightEditCompatible*/ {
|
||||
VimPlugin.getInstance().initialize()
|
||||
|
||||
(injector.enabler as IjVimEnabler).ideOpened()
|
||||
|
||||
FrontendActionIdsSync.sendToBackend()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.helper.inInsertMode
|
||||
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
|
||||
import com.maddyhome.idea.vim.key.KeyHandlerKeeper
|
||||
import com.maddyhome.idea.vim.key.KeySource
|
||||
import com.maddyhome.idea.vim.listener.ModelessSelection
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import java.awt.event.KeyAdapter
|
||||
@@ -86,7 +85,7 @@ class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedActionHandle
|
||||
val modifiers = if (charTyped == ' ' && VimKeyListener.isSpaceShift) KeyEvent.SHIFT_DOWN_MASK else 0
|
||||
val keyStroke = KeyStroke.getKeyStroke(charTyped, modifiers)
|
||||
val startTime = if (traceTime) System.currentTimeMillis() else null
|
||||
handler.handleKey(editor.vim, keyStroke, KeySource.TYPED, context.vim, handler.keyHandlerState)
|
||||
handler.handleUserKey(editor.vim, keyStroke, context.vim)
|
||||
if (startTime != null) {
|
||||
val duration = System.currentTimeMillis() - startTime
|
||||
LOG.info("VimTypedAction '$charTyped': $duration ms")
|
||||
|
||||
@@ -27,15 +27,18 @@ import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.command.MappingMode
|
||||
import com.maddyhome.idea.vim.group.IjOptions
|
||||
import com.maddyhome.idea.vim.helper.ActionEventKeyStrokeExtractor
|
||||
import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl
|
||||
import com.maddyhome.idea.vim.key.KeySource
|
||||
import com.maddyhome.idea.vim.key.MappingInfo
|
||||
import com.maddyhome.idea.vim.key.MappingOwner
|
||||
import com.maddyhome.idea.vim.key.ToActionMappingInfo
|
||||
import com.maddyhome.idea.vim.key.ToKeysMappingInfo
|
||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.KeyEvent
|
||||
@@ -64,6 +67,7 @@ class OutsideEditorKeyDispatcher : DumbAwareAction() {
|
||||
|
||||
private fun isEnabled(e: AnActionEvent): Boolean {
|
||||
if (VimPlugin.isNotEnabled()) return false
|
||||
if (!injector.globalIjOptions().ideaoutsideeditor) return false
|
||||
if (e.getData(PlatformDataKeys.EDITOR) != null) return false
|
||||
if (e.getData(PlatformDataKeys.SPEED_SEARCH_TEXT) != null) return false
|
||||
if (e.getData(PlatformDataKeys.IS_MODAL_CONTEXT) == true) return false
|
||||
@@ -82,15 +86,14 @@ class OutsideEditorKeyDispatcher : DumbAwareAction() {
|
||||
val keyHandler = KeyHandler.getInstance()
|
||||
val fallbackWindow = injector.fallbackWindow
|
||||
if (keyStroke.isEscape) {
|
||||
injector.listenersNotifier.notifyKeyTyped(fallbackWindow, keyStroke)
|
||||
resetToNormal()
|
||||
return
|
||||
}
|
||||
try {
|
||||
forceNormalModeSilently()
|
||||
keyHandler.withoutRecording {
|
||||
keyHandler.handleKey(
|
||||
fallbackWindow, keyStroke, KeySource.TYPED, e.dataContext.vim, keyHandler.keyHandlerState
|
||||
)
|
||||
keyHandler.handleUserKey(fallbackWindow, keyStroke, e.dataContext.vim)
|
||||
}
|
||||
} catch (ex: ProcessCanceledException) {
|
||||
throw ex
|
||||
@@ -113,6 +116,7 @@ class OutsideEditorKeyDispatcher : DumbAwareAction() {
|
||||
}
|
||||
|
||||
private fun register(component: JComponent) {
|
||||
if (!injector.globalIjOptions().ideaoutsideeditor) return
|
||||
// The shortcut set is rebuilt on every registration, so `:map`/`:unmap` executed since the last focus change are
|
||||
// picked up. Escape is always included so that a pending sequence can be cancelled.
|
||||
val keys = userMappingKeys().flatten().toMutableSet()
|
||||
@@ -139,18 +143,26 @@ class OutsideEditorKeyDispatcher : DumbAwareAction() {
|
||||
injector.keyGroup.getKeyMapping(MappingMode.NORMAL).getAll(emptyList())
|
||||
.filter { it.mappingInfo.owner.isUserDefined && it.mappingInfo.isActionsOnly }.map { it.getPath() }
|
||||
|
||||
private val focusListener = PropertyChangeListener { evt ->
|
||||
private fun onFocusOwnerChanged(newFocusOwner: Component?) {
|
||||
unregister()
|
||||
val newFocusOwner = evt.newValue as? JComponent ?: return@PropertyChangeListener
|
||||
if (shouldHandle(newFocusOwner)) register(newFocusOwner)
|
||||
if (newFocusOwner is JComponent && shouldHandle(newFocusOwner)) register(newFocusOwner)
|
||||
}
|
||||
|
||||
fun installFocusListener() {
|
||||
@VisibleForTesting
|
||||
internal val focusListener = PropertyChangeListener { evt -> onFocusOwnerChanged(evt.newValue as? Component) }
|
||||
|
||||
private val optionListener = GlobalOptionChangeListener {
|
||||
onFocusOwnerChanged(KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner)
|
||||
}
|
||||
|
||||
fun installListeners() {
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("focusOwner", focusListener)
|
||||
injector.optionGroup.addGlobalOptionChangeListener(IjOptions.ideaoutsideeditor, optionListener)
|
||||
}
|
||||
|
||||
fun removeFocusListener() {
|
||||
fun removeListeners() {
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener("focusOwner", focusListener)
|
||||
injector.optionGroup.removeGlobalOptionChangeListener(IjOptions.ideaoutsideeditor, optionListener)
|
||||
unregister()
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@ import com.maddyhome.idea.vim.handler.VimActionHandler
|
||||
* Typically, an IDE does not need to redraw the screen - it's editor handles scrolling, etc. However, it can be
|
||||
* necessary to clear the status line.
|
||||
*/
|
||||
@CommandOrMotion(keys = ["<C-L>"], modes = [Mode.NORMAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["<C-L>"],
|
||||
modes = [Mode.NORMAL],
|
||||
description = "Clear and redraw the screen, also clearing the status line."
|
||||
)
|
||||
internal class RedrawAction : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_READONLY
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
|
||||
import com.maddyhome.idea.vim.helper.isPrimaryEditor
|
||||
import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes
|
||||
import com.maddyhome.idea.vim.impl.state.toMappingMode
|
||||
import com.maddyhome.idea.vim.key.KeySource
|
||||
import com.maddyhome.idea.vim.key.ShortcutOwner
|
||||
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
|
||||
import com.maddyhome.idea.vim.listener.AceJumpService
|
||||
@@ -86,10 +85,13 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
if (editor != null && keyStroke != null) {
|
||||
// In the Python console, Enter/Up/Down should drive the console's own actions (execute / history) in every Vim
|
||||
// mode, with the console's native context-aware behaviour.
|
||||
if (handlePythonConsoleKey(editor, keyStroke, e)) return
|
||||
if (handlePythonConsoleKey(editor, keyStroke, e)) {
|
||||
injector.listenersNotifier.notifyKeyTyped(editor.vim, keyStroke)
|
||||
return
|
||||
}
|
||||
|
||||
val owner = VimPlugin.getKey().savedShortcutConflicts[keyStroke]
|
||||
if ((owner as? ShortcutOwnerInfo.AllModes)?.owner == ShortcutOwner.UNDEFINED) {
|
||||
if ((owner as? ShortcutOwnerInfo.AllModes)?.owner == ShortcutOwner.UNDEFINED && keyStroke !in VIM_DEFAULT_EDITOR_KEYS) {
|
||||
VimPlugin.getNotifications(editor.project).notifyAboutShortcutConflict(keyStroke)
|
||||
}
|
||||
// Should we use HelperKt.getTopLevelEditor(editor) here, as we did in former EditorKeyHandler?
|
||||
@@ -98,7 +100,7 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
// Vim removes a modeless selection as soon as a command is typed
|
||||
ModelessSelection.clearIfOwned(editor)
|
||||
val keyHandler = KeyHandler.getInstance()
|
||||
keyHandler.handleKey(editor.vim, keyStroke, KeySource.TYPED, e.dataContext.vim, keyHandler.keyHandlerState)
|
||||
keyHandler.handleUserKey(editor.vim, keyStroke, e.dataContext.vim)
|
||||
if (start != null) {
|
||||
val duration = System.currentTimeMillis() - start
|
||||
LOG.info("VimShortcut execution '$keyStroke': $duration ms")
|
||||
@@ -222,6 +224,12 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.VK_ESCAPE) {
|
||||
// Escape is Vim's by default, but the user can explicitly hand it over to the IDE, e.g. `sethandler <Esc> n:ide`
|
||||
if (keyStroke in VIM_DEFAULT_EDITOR_KEYS &&
|
||||
VimPlugin.getKey().savedShortcutConflicts[keyStroke]?.forEditor(editor.vim) == ShortcutOwner.IDE
|
||||
) {
|
||||
return ActionEnableStatus.no("Owner of Esc is IDE", LogLevel.DEBUG)
|
||||
}
|
||||
return if (isEnabledForEscape(editor)) {
|
||||
ActionEnableStatus.yes("Is enabled for Esc", LogLevel.INFO)
|
||||
} else {
|
||||
@@ -343,15 +351,10 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
|
||||
@JvmField
|
||||
val VIM_ONLY_EDITOR_KEYS: Set<KeyStroke> =
|
||||
ImmutableSet.builder<KeyStroke>().addAll(getKeyStrokes(KeyEvent.VK_ENTER, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_ESCAPE, 0))
|
||||
ImmutableSet.builder<KeyStroke>()
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_BACK_SPACE, 0, InputEvent.CTRL_DOWN_MASK))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_INSERT, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_DELETE, 0, InputEvent.CTRL_DOWN_MASK))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_UP, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_DOWN, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_LEFT, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_RIGHT, 0))
|
||||
.addAll(
|
||||
getKeyStrokes(
|
||||
KeyEvent.VK_HOME,
|
||||
@@ -378,6 +381,22 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
),
|
||||
).build()
|
||||
|
||||
/**
|
||||
* Keys that are handled by Vim unless the user explicitly assigns a handler to that particular key, either with
|
||||
* `sethandler <key> ...` or in the shortcut conflicts settings. They are pressed constantly and every keymap binds
|
||||
* them to editor actions, so we don't notify about their conflict and handlers set for all shortcuts at once
|
||||
* (`sethandler a:ide`, "Toggle all unset Handlers") skip them.
|
||||
*/
|
||||
@JvmField
|
||||
val VIM_DEFAULT_EDITOR_KEYS: Set<KeyStroke> =
|
||||
ImmutableSet.builder<KeyStroke>().addAll(getKeyStrokes(KeyEvent.VK_ENTER, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_ESCAPE, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_UP, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_DOWN, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_LEFT, 0))
|
||||
.addAll(getKeyStrokes(KeyEvent.VK_RIGHT, 0))
|
||||
.build()
|
||||
|
||||
private const val ACTION_ID = "VimShortcutKeyAction"
|
||||
|
||||
private val LOG = logger<VimShortcutKeyAction>()
|
||||
|
||||
@@ -73,7 +73,11 @@ private fun doOperatorAction(
|
||||
}
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["g@"], modes = [Mode.NORMAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["g@"],
|
||||
modes = [Mode.NORMAL],
|
||||
description = "Call the function set with 'operatorfunc' on the text moved over by {motion}."
|
||||
)
|
||||
internal class OperatorAction : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
|
||||
|
||||
@@ -123,7 +127,11 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
|
||||
}
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["g@"], modes = [Mode.VISUAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["g@"],
|
||||
modes = [Mode.VISUAL],
|
||||
description = "Call the function set with 'operatorfunc' on the highlighted text."
|
||||
)
|
||||
internal class VisualOperatorAction : VisualOperatorActionHandler.ForEachCaret() {
|
||||
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2023 The IdeaVim authors
|
||||
* Copyright 2003-2026 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
|
||||
@@ -10,17 +10,31 @@ package com.maddyhome.idea.vim.action.change
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.vim.annotations.CommandOrMotion
|
||||
import com.intellij.vim.annotations.Mode
|
||||
import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.command.Command
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.extension.ExtensionHandler
|
||||
import com.maddyhome.idea.vim.group.withVimUndoGroup
|
||||
import com.maddyhome.idea.vim.handler.VimActionHandler
|
||||
import com.maddyhome.idea.vim.key.KeySource
|
||||
import com.maddyhome.idea.vim.newapi.ij
|
||||
|
||||
@CommandOrMotion(keys = ["."], modes = [Mode.NORMAL])
|
||||
/**
|
||||
* `.` - repeat the last change.
|
||||
*
|
||||
* The last change is remembered as the keys typed for it (see [VimRedoBuffer]), so repeating it means feeding those
|
||||
* keys back through the key handler, the way `@` replays a macro. Extensions that register a repeat handler are the
|
||||
* exception - they are Kotlin callbacks rather than keys, and are still invoked directly.
|
||||
*/
|
||||
@CommandOrMotion(
|
||||
keys = ["."],
|
||||
modes = [Mode.NORMAL],
|
||||
description = "Repeat the last change, with [count] replacing the original count."
|
||||
)
|
||||
internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_WRITABLE
|
||||
|
||||
@@ -30,65 +44,86 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
|
||||
cmd: Command,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Boolean {
|
||||
val state = injector.vimState
|
||||
var lastCommand = VimRepeater.lastChangeCommand
|
||||
val extensionHandler = Extension.lastExtensionHandler.takeIf { VimRepeater.repeatHandler }
|
||||
if (extensionHandler == null && VimRedoBuffer.isEmpty) return false
|
||||
|
||||
if (lastCommand == null && Extension.lastExtensionHandler == null) return false
|
||||
|
||||
// Save state
|
||||
val save = state.executingCommand
|
||||
val lastFTCmd = injector.motion.lastFTCmd
|
||||
val lastFTChar = injector.motion.lastFTChar
|
||||
val reg = injector.registerGroup.currentRegister
|
||||
val lastHandler = Extension.lastExtensionHandler
|
||||
val repeatHandler = VimRepeater.repeatHandler
|
||||
|
||||
state.isDotRepeatInProgress = true
|
||||
|
||||
// Group the entire dot replay into one undo step on the JBC backend so that
|
||||
// `s`/`c`-style commands (which delete *and* insert) revert with a single
|
||||
// `u`. Without this the backend records each atomic edit (delete, insert,
|
||||
// caret) as a separate undo entry because speculative undo disables the
|
||||
// platform's command grouping.
|
||||
withVimUndoGroup(editor, "Vim Dot Repeat") {
|
||||
// A fancy 'redo-register' feature
|
||||
// VIM-2643, :h redo-register
|
||||
if (VimRepeater.lastChangeRegister in '1'..'8') {
|
||||
VimRepeater.lastChangeRegister = VimRepeater.lastChangeRegister.inc()
|
||||
}
|
||||
|
||||
injector.registerGroup.selectRegister(VimRepeater.lastChangeRegister)
|
||||
|
||||
if (repeatHandler && lastHandler != null) {
|
||||
val processor = CommandProcessor.getInstance()
|
||||
processor.executeCommand(
|
||||
editor.ij.project,
|
||||
{ lastHandler.execute(editor, context, operatorArguments) },
|
||||
"Vim " + lastHandler.javaClass.simpleName,
|
||||
null,
|
||||
)
|
||||
} else if (!repeatHandler && lastCommand != null) {
|
||||
if (cmd.rawCount > 0) {
|
||||
lastCommand = lastCommand.copy(rawCount = cmd.rawCount)
|
||||
preservingRepeatState {
|
||||
// One undo step for the whole repeat. The JBC backend otherwise records each atomic edit of a `s`/`c`-style
|
||||
// command separately, because speculative undo disables the platform's command grouping
|
||||
withVimUndoGroup(editor, "Vim Dot Repeat") {
|
||||
if (extensionHandler != null) {
|
||||
runExtensionHandler(extensionHandler, editor, context, operatorArguments)
|
||||
} else {
|
||||
replayKeys(editor, context, cmd.rawCount)
|
||||
}
|
||||
state.executingCommand = lastCommand
|
||||
|
||||
val arguments = operatorArguments.copy(count0 = lastCommand.rawCount)
|
||||
injector.actionExecutor.executeVimAction(editor, lastCommand.action, context, arguments)
|
||||
|
||||
VimRepeater.saveLastChange(lastCommand)
|
||||
}
|
||||
}
|
||||
|
||||
state.isDotRepeatInProgress = false
|
||||
|
||||
// Restore state
|
||||
if (save != null) state.executingCommand = save
|
||||
VimPlugin.getMotion().setLastFTCmd(lastFTCmd, lastFTChar)
|
||||
if (lastHandler != null) Extension.lastExtensionHandler = lastHandler
|
||||
VimRepeater.repeatHandler = repeatHandler
|
||||
Extension.reset()
|
||||
VimPlugin.getRegister().selectRegister(reg)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Feeds the keys of the last change back through the key handler.
|
||||
*
|
||||
* They are pulled off the key stack one at a time, as [com.maddyhome.idea.vim.key.MappingInfo] does for the
|
||||
* right-hand side of a mapping, and fed as typed keys: what was recorded is what the user typed, so a change made
|
||||
* through a mapping runs the mapping again.
|
||||
*/
|
||||
private fun replayKeys(editor: VimEditor, context: ExecutionContext, rawCount: Int) {
|
||||
val keys = VimRedoBuffer.keysForReplay(rawCount)
|
||||
VimRedoBuffer.replaying {
|
||||
val keyHandler = KeyHandler.getInstance()
|
||||
keyHandler.keyStack.addKeys(keys)
|
||||
try {
|
||||
while (keyHandler.keyStack.hasStroke()) {
|
||||
val key = keyHandler.keyStack.feedStroke()
|
||||
keyHandler.handleKey(editor, key, KeySource.TYPED, context, keyHandler.keyHandlerState)
|
||||
}
|
||||
} finally {
|
||||
keyHandler.keyStack.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runExtensionHandler(
|
||||
handler: ExtensionHandler,
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
operatorArguments: OperatorArguments,
|
||||
) {
|
||||
val state = injector.vimState
|
||||
state.isDotRepeatInProgress = true
|
||||
try {
|
||||
CommandProcessor.getInstance().executeCommand(
|
||||
editor.ij.project,
|
||||
{ handler.execute(editor, context, operatorArguments) },
|
||||
"Vim " + handler.javaClass.simpleName,
|
||||
null,
|
||||
)
|
||||
} finally {
|
||||
state.isDotRepeatInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [block] and puts back the state the replay would otherwise leave changed: `.` does not become the target of
|
||||
* `;`, does not change the register the next command uses, and stays repeatable itself.
|
||||
*/
|
||||
private inline fun preservingRepeatState(block: () -> Unit) {
|
||||
val state = injector.vimState
|
||||
val executingCommand = state.executingCommand
|
||||
val lastFTCmd = injector.motion.lastFTCmd
|
||||
val lastFTChar = injector.motion.lastFTChar
|
||||
val register = injector.registerGroup.currentRegister
|
||||
val extensionHandler = Extension.lastExtensionHandler
|
||||
val repeatHandler = VimRepeater.repeatHandler
|
||||
|
||||
block()
|
||||
|
||||
if (executingCommand != null) state.executingCommand = executingCommand
|
||||
VimPlugin.getMotion().setLastFTCmd(lastFTCmd, lastFTChar)
|
||||
if (extensionHandler != null) Extension.lastExtensionHandler = extensionHandler
|
||||
VimRepeater.repeatHandler = repeatHandler
|
||||
Extension.reset()
|
||||
VimPlugin.getRegister().selectRegister(register)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
|
||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||
|
||||
@CommandOrMotion(keys = ["gJ"], modes = [Mode.NORMAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["gJ"],
|
||||
modes = [Mode.NORMAL],
|
||||
description = "Join [count] lines (at least two) without inserting or removing any spaces."
|
||||
)
|
||||
class DeleteJoinLinesAction : ChangeEditorActionHandler.ConditionalSingleExecution() {
|
||||
override val type: Command.Type = Command.Type.DELETE
|
||||
override fun runAsMulticaret(
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
|
||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||
|
||||
@CommandOrMotion(keys = ["J"], modes = [Mode.NORMAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["J"],
|
||||
modes = [Mode.NORMAL],
|
||||
description = "Join [count] lines (at least two), replacing each line break and leading white space with a space."
|
||||
)
|
||||
class DeleteJoinLinesSpacesAction : ChangeEditorActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.DELETE
|
||||
|
||||
|
||||
+7
-2
@@ -16,13 +16,18 @@ import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.command.Command
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.group.visual.toJoinRange
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
*/
|
||||
@CommandOrMotion(keys = ["gJ"], modes = [Mode.VISUAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["gJ"],
|
||||
modes = [Mode.VISUAL],
|
||||
description = "Join the highlighted lines without inserting or removing any spaces."
|
||||
)
|
||||
class DeleteJoinVisualLinesAction : VisualOperatorActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.DELETE
|
||||
|
||||
@@ -46,7 +51,7 @@ class DeleteJoinVisualLinesAction : VisualOperatorActionHandler.SingleExecution(
|
||||
editor,
|
||||
context,
|
||||
caret,
|
||||
range.toVimTextRange(true).normalize(),
|
||||
range.toJoinRange(),
|
||||
false,
|
||||
operatorArguments,
|
||||
)
|
||||
|
||||
+7
-2
@@ -16,13 +16,18 @@ import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.command.Command
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.group.visual.toJoinRange
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
*/
|
||||
@CommandOrMotion(keys = ["J"], modes = [Mode.VISUAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["J"],
|
||||
modes = [Mode.VISUAL],
|
||||
description = "Join the highlighted lines, replacing each line break and leading white space with a space."
|
||||
)
|
||||
class DeleteJoinVisualLinesSpacesAction : VisualOperatorActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.DELETE
|
||||
|
||||
@@ -46,7 +51,7 @@ class DeleteJoinVisualLinesSpacesAction : VisualOperatorActionHandler.SingleExec
|
||||
editor,
|
||||
context,
|
||||
caret,
|
||||
range.toVimTextRange(true).normalize(),
|
||||
range.toJoinRange(),
|
||||
true,
|
||||
operatorArguments,
|
||||
)
|
||||
|
||||
@@ -26,12 +26,20 @@ import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
|
||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||
import java.util.*
|
||||
|
||||
@CommandOrMotion(keys = ["<Del>"], modes = [Mode.INSERT])
|
||||
@CommandOrMotion(
|
||||
keys = ["<Del>"],
|
||||
modes = [Mode.INSERT],
|
||||
description = "In Insert mode, delete the character under the cursor using the IDE's Delete action."
|
||||
)
|
||||
internal class VimEditorDelete : IdeActionHandler(IdeActions.ACTION_EDITOR_DELETE) {
|
||||
override val type: Command.Type = Command.Type.DELETE
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["<Down>", "<kDown>"], modes = [Mode.INSERT])
|
||||
@CommandOrMotion(
|
||||
keys = ["<Down>", "<kDown>"],
|
||||
modes = [Mode.INSERT],
|
||||
description = "In Insert mode, move the cursor one line down using the IDE's caret action, starting a new undo step."
|
||||
)
|
||||
internal class VimEditorDown : IdeActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN) {
|
||||
override val type: Command.Type = Command.Type.MOTION
|
||||
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_CLEAR_STROKES)
|
||||
@@ -75,13 +83,21 @@ internal class VimEditorDown : IdeActionHandler(IdeActions.ACTION_EDITOR_MOVE_CA
|
||||
* Also, by inserting `Tab` with our action, we will correctly update the scroll position to keep the caret visible,
|
||||
* applying `'scrolloff'` and `'sidescrolloff'`.
|
||||
*/
|
||||
@CommandOrMotion(keys = ["<Tab>", "<C-I>"], modes = [Mode.INSERT])
|
||||
@CommandOrMotion(
|
||||
keys = ["<Tab>", "<C-I>"],
|
||||
modes = [Mode.INSERT],
|
||||
description = "In Insert mode, insert a tab or the equivalent spaces using the IDE's Tab action."
|
||||
)
|
||||
internal class VimEditorTab : IdeActionHandler(IdeActions.ACTION_EDITOR_TAB) {
|
||||
override val type: Command.Type = Command.Type.INSERT
|
||||
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_SAVE_STROKE)
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["<Up>", "<kUp>"], modes = [Mode.INSERT])
|
||||
@CommandOrMotion(
|
||||
keys = ["<Up>", "<kUp>"],
|
||||
modes = [Mode.INSERT],
|
||||
description = "In Insert mode, move the cursor one line up using the IDE's caret action, starting a new undo step."
|
||||
)
|
||||
internal class VimEditorUp : IdeActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP) {
|
||||
override val type: Command.Type = Command.Type.MOTION
|
||||
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_CLEAR_STROKES)
|
||||
@@ -104,7 +120,11 @@ internal class VimEditorUp : IdeActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARE
|
||||
}
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["K"], modes = [Mode.NORMAL])
|
||||
@CommandOrMotion(
|
||||
keys = ["K"],
|
||||
modes = [Mode.NORMAL],
|
||||
description = "Show documentation for the symbol under the cursor using the IDE's Quick Documentation action."
|
||||
)
|
||||
internal class VimQuickJavaDoc : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_READONLY
|
||||
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.maddyhome.idea.vim.action.macro
|
||||
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.command.UndoConfirmationPolicy
|
||||
import com.intellij.openapi.command.impl.FinishMarkAction
|
||||
import com.intellij.openapi.command.impl.StartMarkAction
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.vim.annotations.CommandOrMotion
|
||||
import com.intellij.vim.annotations.Mode
|
||||
import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.command.Argument
|
||||
import com.maddyhome.idea.vim.command.Command
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.handler.VimActionHandler
|
||||
import com.maddyhome.idea.vim.newapi.ij
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
|
||||
@CommandOrMotion(keys = ["z@"], modes = [Mode.NORMAL], description = "Execute macro in all open files.")
|
||||
class PlaybackRegisterInOpenFilesAction : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
|
||||
|
||||
override val argumentType: Argument.Type = Argument.Type.CHARACTER
|
||||
|
||||
private val playbackRegisterAction = PlaybackRegisterAction()
|
||||
|
||||
override fun execute(
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
cmd: Command,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Boolean {
|
||||
val argument = cmd.argument as? Argument.Character ?: return false
|
||||
|
||||
val project = editor.ij.project ?: return false
|
||||
val fileEditorManager = FileEditorManagerEx.getInstanceExIfCreated(project) ?: return false
|
||||
|
||||
val register = argument.character.let { if (it == '@') injector.macro.lastRegister else it }
|
||||
val commandName = "Execute Macro '$register' in All Open Files"
|
||||
|
||||
val action = Runnable {
|
||||
CommandProcessor.getInstance().markCurrentCommandAsGlobal(project)
|
||||
|
||||
for (textEditor in fileEditorManager.allEditors.filterIsInstance<TextEditor>()) {
|
||||
fileEditorManager.openFile(textEditor.file, true)
|
||||
|
||||
val editor = textEditor.editor
|
||||
val vimEditor = editor.vim
|
||||
|
||||
vimEditor.mode = com.maddyhome.idea.vim.state.mode.Mode.NORMAL()
|
||||
KeyHandler.getInstance().reset(vimEditor)
|
||||
|
||||
val startMarkAction = StartMarkAction.start(editor, project, commandName)
|
||||
playbackRegisterAction.execute(vimEditor, context, cmd, operatorArguments)
|
||||
FinishMarkAction.finish(project, editor, startMarkAction)
|
||||
}
|
||||
}
|
||||
|
||||
CommandProcessor.getInstance()
|
||||
.executeCommand(project, action, commandName, null, UndoConfirmationPolicy.REQUEST_CONFIRMATION)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ class IndentConfig private constructor(indentOptions: IndentOptions) : VimIndent
|
||||
private val tabSize = indentOptions.TAB_SIZE
|
||||
private val isUseTabs = indentOptions.USE_TAB_CHARACTER
|
||||
|
||||
override val keepIndentsOnEmptyLines = indentOptions.KEEP_INDENTS_ON_EMPTY_LINES
|
||||
|
||||
override fun getIndentSize(depth: Int): Int = indentSize * depth
|
||||
override fun createIndentByDepth(depth: Int): String = createIndentBySize(getIndentSize(depth))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package com.maddyhome.idea.vim.config
|
||||
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import org.jdom.Element
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
@@ -16,11 +17,11 @@ import kotlin.reflect.KProperty
|
||||
* @author Alex Plate
|
||||
*/
|
||||
class VimState {
|
||||
var isIdeaJoinNotified by StateProperty("idea-join")
|
||||
var isIdeaPutNotified by StateProperty("idea-put")
|
||||
var wasSubscribedToEAPAutomatically by StateProperty("was-automatically-subscribed-to-eap")
|
||||
var firstIdeaVimVersion: String? by StringProperty("first-ideavim-version", null)
|
||||
var lastIdeaVimVersion: String? by StringProperty("last-ideavim-version", null)
|
||||
var isIdeaJoinNotified get() = true; set(_) = Unit
|
||||
var isIdeaPutNotified get() = true; set(_) = Unit
|
||||
var wasSubscribedToEAPAutomatically get() = false; set(_) = Unit
|
||||
var firstIdeaVimVersion: String? get() = "1"; set(_) = Unit
|
||||
var lastIdeaVimVersion: String? get() = VimPlugin.getVersion(); set(_) = Unit
|
||||
|
||||
fun readData(element: Element) {
|
||||
val notifications = element.getChild("notifications")
|
||||
|
||||
@@ -49,9 +49,23 @@ open class ShortcutDispatcher<T>(
|
||||
})
|
||||
|
||||
protected val trie = KeyStrokeTrie<T>(name)
|
||||
private val shortcutSet: ShortcutSet
|
||||
private var shortcutSet: ShortcutSet = CustomShortcutSet.EMPTY
|
||||
|
||||
protected val keyStrokes: MutableList<KeyStroke> = mutableListOf()
|
||||
|
||||
init {
|
||||
setMappings(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all the mappings of this dispatcher.
|
||||
*
|
||||
* The new keys start to reach the dispatcher once [register] is called again. The shortcut set belongs to the action,
|
||||
* so a single [register] call updates the keys for every component this dispatcher is registered on.
|
||||
*/
|
||||
protected fun setMappings(data: Map<List<KeyStroke>, T>) {
|
||||
keyStrokes.clear()
|
||||
trie.clear()
|
||||
val keys: MutableList<KeyStroke> = mutableListOf()
|
||||
for ((k, v) in data) {
|
||||
keys.addAll(k)
|
||||
@@ -61,8 +75,6 @@ open class ShortcutDispatcher<T>(
|
||||
shortcutSet = CustomShortcutSet(*shortcuts.toTypedArray())
|
||||
}
|
||||
|
||||
protected val keyStrokes: MutableList<KeyStroke> = mutableListOf()
|
||||
|
||||
final override fun actionPerformed(e: AnActionEvent) {
|
||||
var keyStroke = getKeyStroke(e) ?: return
|
||||
// Omit the modifier (shift) from keyStroke
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.extension.cleverf
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.markup.EffectType
|
||||
import com.intellij.openapi.editor.markup.HighlighterLayer
|
||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea
|
||||
import com.intellij.openapi.editor.markup.RangeHighlighter
|
||||
import com.intellij.openapi.editor.markup.TextAttributes
|
||||
import com.intellij.ui.JBColor
|
||||
import java.awt.Font
|
||||
|
||||
/**
|
||||
* Highlights the characters the current search can jump to, like clever-f's `g:clever_f_mark_char`.
|
||||
* Only one set of candidates is shown at a time.
|
||||
*/
|
||||
internal class CandidateHighlighter {
|
||||
private var editor: Editor? = null
|
||||
private val highlighters = mutableListOf<RangeHighlighter>()
|
||||
|
||||
fun show(editor: Editor, offsets: List<Int>) {
|
||||
clear()
|
||||
this.editor = editor
|
||||
offsets.mapTo(highlighters) { offset ->
|
||||
editor.markupModel.addRangeHighlighter(
|
||||
offset,
|
||||
offset + 1,
|
||||
HighlighterLayer.SELECTION,
|
||||
CANDIDATE_ATTRIBUTES,
|
||||
HighlighterTargetArea.EXACT_RANGE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val markupModel = editor?.takeUnless { it.isDisposed }?.markupModel
|
||||
if (markupModel != null) highlighters.forEach(markupModel::removeHighlighter)
|
||||
highlighters.clear()
|
||||
editor = null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** clever-f's `CleverFDefaultLabel`: red, bold and underlined text */
|
||||
val CANDIDATE_ATTRIBUTES = TextAttributes(JBColor.RED, null, JBColor.RED, EffectType.LINE_UNDERSCORE, Font.BOLD)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2003-2026 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.extension.cleverf
|
||||
|
||||
/**
|
||||
* Decides whether a character in the buffer matches the searched [char], honouring `g:clever_f_ignore_case`,
|
||||
* `g:clever_f_smart_case` and `g:clever_f_chars_match_any_signs`.
|
||||
*/
|
||||
internal class CharMatcher(private val char: Char) {
|
||||
private val matchesAnySign = char in CleverFSettings.charsMatchAnySigns
|
||||
private val ignoreCase = (CleverFSettings.smartCase && char.isLowerCase()) || CleverFSettings.ignoreCase
|
||||
|
||||
fun matches(c: Char): Boolean = if (matchesAnySign) c in SIGNS else c.equals(char, ignoreCase)
|
||||
|
||||
private companion object {
|
||||
const val SIGNS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user