mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-06-14 17:02:31 +02:00
Compare commits
141 Commits
customized
...
6711486812
| Author | SHA1 | Date | |
|---|---|---|---|
|
6711486812
|
|||
|
e295270cc2
|
|||
|
e05be6da9c
|
|||
|
e4ec2abaef
|
|||
|
07a1ea1a34
|
|||
|
3807bc619f
|
|||
|
38d1caf795
|
|||
|
c22f7ec8a8
|
|||
|
42ea7fa70d
|
|||
|
8a5fb3920d
|
|||
|
920529e112
|
|||
|
c07ce16a19
|
|||
|
f0a6e85895
|
|||
|
be817241d5
|
|||
|
ab34f0bcdc
|
|||
|
6dda6b6772
|
|||
|
8e1bfe6d01
|
|||
|
7ff4d45d1c
|
|||
|
874eeb59a2
|
|||
|
5c9b787f30
|
|||
|
ecc649ea7b
|
|||
|
03bcec7fe0
|
|||
|
9e64c6d36b
|
|||
|
b812ecbd29
|
|||
|
cb5e88d8cf
|
|||
|
f9b071bb47
|
|||
|
8d41d01d6e
|
|||
|
9665ebd1b7
|
|||
|
6ef6425037
|
|||
|
3bc9a7816f
|
|||
|
5c2f67faca
|
|||
|
735dfcbd4c
|
|||
|
cda49601c0
|
|||
|
a8893067c6
|
|||
|
|
8b74fa25a5 | ||
|
|
7db9b0b58b | ||
|
|
0527ad3359 | ||
|
|
4f95756351 | ||
|
|
c17bb06f8a | ||
|
|
7f23a06447 | ||
|
|
5bb18a2cb7 | ||
|
|
7d6315af2d | ||
|
|
ff7b392e67 | ||
|
|
2e5c5d815c | ||
|
|
e8c12a5526 | ||
|
|
4fc912eee7 | ||
|
|
f4418b833f | ||
|
|
925cdbd8ac | ||
|
|
bc5ac2bcf4 | ||
|
|
9278425a5d | ||
|
|
26b9515279 | ||
|
|
a41f197bc5 | ||
|
|
e551bfe535 | ||
|
|
093244c07d | ||
|
|
a08c253e6c | ||
|
|
073d3b40d5 | ||
|
|
281039c76e | ||
|
|
ec06a522da | ||
|
|
c6ca4428ba | ||
|
|
a440bcb108 | ||
|
|
4be7a68ebd | ||
|
|
ec14d06ecd | ||
|
|
a2b0e7b273 | ||
|
|
4e9f3a5e11 | ||
|
|
057e3f6d19 | ||
|
|
b5d2f9c3d8 | ||
|
|
705ce474f1 | ||
|
|
4869fe68e5 | ||
|
|
852ea2feb0 | ||
|
|
3dc0ebd2d1 | ||
|
|
c33b0928dd | ||
|
|
02b4b4969a | ||
|
|
f1d971c239 | ||
|
|
45ce2143fe | ||
|
|
81bf421436 | ||
|
|
ebf637a367 | ||
|
|
4dec9a95a4 | ||
|
|
cd5c8e9e17 | ||
|
|
64e110f27a | ||
|
|
eaf39234b2 | ||
|
|
ac029ee889 | ||
|
|
607920d262 | ||
|
|
ab7ca0e32e | ||
|
|
7ec1e4c58d | ||
|
|
edce848ebd | ||
|
|
8cda987b6e | ||
|
|
558d670f4e | ||
|
|
0df627ab19 | ||
|
|
7546b0737d | ||
|
|
d58a68df78 | ||
|
|
aedf114576 | ||
|
|
9decc7095b | ||
|
|
4edc23006e | ||
|
|
6e21fbd61a | ||
|
|
1be8183399 | ||
|
|
f7718b6dd8 | ||
|
|
5cfd1d1fe6 | ||
|
|
66ed07e6f5 | ||
|
|
720d8fab40 | ||
|
|
6c803e3154 | ||
|
|
38c74d6b9d | ||
|
|
55a451ac2f | ||
|
|
29a02a102b | ||
|
|
bebce05950 | ||
|
|
f86ba678e5 | ||
|
|
64f2d5b628 | ||
|
|
98abfedb98 | ||
|
|
1dcb386b92 | ||
|
|
d085b3ffce | ||
|
|
319b5164dc | ||
|
|
c49ac943fc | ||
|
|
9c4f6d0989 | ||
|
|
f9b4059224 | ||
|
|
ef42ce6aa5 | ||
|
|
270dc391b5 | ||
|
|
7a31c774d0 | ||
|
|
9dd2b7c743 | ||
|
|
ddfcf07735 | ||
|
|
9ea5116bf4 | ||
|
|
ac53a63adb | ||
|
|
499069177c | ||
|
|
f5afa628d9 | ||
|
|
70066dffc1 | ||
|
|
75b4d3fce3 | ||
|
|
08aea2b202 | ||
|
|
f5fdc217be | ||
|
|
75a05a8f12 | ||
|
|
5416d1505f | ||
|
|
01789c6a7b | ||
|
|
ebc09ddab1 | ||
|
|
e7722bef61 | ||
|
|
d28889fc7b | ||
|
|
9251a83031 | ||
|
|
3793458d64 | ||
|
|
34196bc0dd | ||
|
|
2a7d23586a | ||
|
|
d47a22e96f | ||
|
|
f4c6e04558 | ||
|
|
c82539e379 | ||
|
|
11f1745c63 | ||
|
|
581001f199 |
40
.beads/.gitignore
vendored
40
.beads/.gitignore
vendored
@@ -1,40 +0,0 @@
|
|||||||
# SQLite databases
|
|
||||||
*.db
|
|
||||||
*.db?*
|
|
||||||
*.db-journal
|
|
||||||
*.db-wal
|
|
||||||
*.db-shm
|
|
||||||
|
|
||||||
# Daemon runtime files
|
|
||||||
daemon.lock
|
|
||||||
daemon.log
|
|
||||||
daemon.pid
|
|
||||||
bd.sock
|
|
||||||
sync-state.json
|
|
||||||
last-touched
|
|
||||||
.sync.lock
|
|
||||||
|
|
||||||
# Local version tracking (prevents upgrade notification spam after git ops)
|
|
||||||
.local_version
|
|
||||||
|
|
||||||
# Legacy database files
|
|
||||||
db.sqlite
|
|
||||||
bd.db
|
|
||||||
|
|
||||||
# Worktree redirect file (contains relative path to main repo's .beads/)
|
|
||||||
# Must not be committed as paths would be wrong in other clones
|
|
||||||
redirect
|
|
||||||
|
|
||||||
# Merge artifacts (temporary files from 3-way merge)
|
|
||||||
beads.base.jsonl
|
|
||||||
beads.base.meta.json
|
|
||||||
beads.left.jsonl
|
|
||||||
beads.left.meta.json
|
|
||||||
beads.right.jsonl
|
|
||||||
beads.right.meta.json
|
|
||||||
|
|
||||||
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
|
|
||||||
# They would override fork protection in .git/info/exclude, allowing
|
|
||||||
# contributors to accidentally commit upstream issue databases.
|
|
||||||
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
|
|
||||||
# are tracked by git by default since no pattern above ignores them.
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Beads - AI-Native Issue Tracking
|
|
||||||
|
|
||||||
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
|
|
||||||
|
|
||||||
## What is Beads?
|
|
||||||
|
|
||||||
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
|
|
||||||
|
|
||||||
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Essential Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create new issues
|
|
||||||
bd create "Add user authentication"
|
|
||||||
|
|
||||||
# View all issues
|
|
||||||
bd list
|
|
||||||
|
|
||||||
# View issue details
|
|
||||||
bd show <issue-id>
|
|
||||||
|
|
||||||
# Update issue status
|
|
||||||
bd update <issue-id> --status in_progress
|
|
||||||
bd update <issue-id> --status done
|
|
||||||
|
|
||||||
# Sync with git remote
|
|
||||||
bd sync
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working with Issues
|
|
||||||
|
|
||||||
Issues in Beads are:
|
|
||||||
- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code
|
|
||||||
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
|
|
||||||
- **Branch-aware**: Issues can follow your branch workflow
|
|
||||||
- **Always in sync**: Auto-syncs with your commits
|
|
||||||
|
|
||||||
## Why Beads?
|
|
||||||
|
|
||||||
✨ **AI-Native Design**
|
|
||||||
- Built specifically for AI-assisted development workflows
|
|
||||||
- CLI-first interface works seamlessly with AI coding agents
|
|
||||||
- No context switching to web UIs
|
|
||||||
|
|
||||||
🚀 **Developer Focused**
|
|
||||||
- Issues live in your repo, right next to your code
|
|
||||||
- Works offline, syncs when you push
|
|
||||||
- Fast, lightweight, and stays out of your way
|
|
||||||
|
|
||||||
🔧 **Git Integration**
|
|
||||||
- Automatic sync with git commits
|
|
||||||
- Branch-aware issue tracking
|
|
||||||
- Intelligent JSONL merge resolution
|
|
||||||
|
|
||||||
## Get Started with Beads
|
|
||||||
|
|
||||||
Try Beads in your own projects:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install Beads
|
|
||||||
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
|
|
||||||
|
|
||||||
# Initialize in your repo
|
|
||||||
bd init
|
|
||||||
|
|
||||||
# Create your first issue
|
|
||||||
bd create "Try out Beads"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
|
|
||||||
- **Quick Start Guide**: Run `bd quickstart`
|
|
||||||
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Beads: Issue tracking that moves at the speed of thought* ⚡
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Beads Configuration File
|
|
||||||
# This file configures default behavior for all bd commands in this repository
|
|
||||||
# All settings can also be set via environment variables (BD_* prefix)
|
|
||||||
# or overridden with command-line flags
|
|
||||||
|
|
||||||
# Issue prefix for this repository (used by bd init)
|
|
||||||
# If not set, bd init will auto-detect from directory name
|
|
||||||
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
|
||||||
# issue-prefix: ""
|
|
||||||
|
|
||||||
# Use no-db mode: load from JSONL, no SQLite, write back after each command
|
|
||||||
# When true, bd will use .beads/issues.jsonl as the source of truth
|
|
||||||
# instead of SQLite database
|
|
||||||
# no-db: false
|
|
||||||
|
|
||||||
# Disable daemon for RPC communication (forces direct database access)
|
|
||||||
# no-daemon: false
|
|
||||||
|
|
||||||
# Disable auto-flush of database to JSONL after mutations
|
|
||||||
# no-auto-flush: false
|
|
||||||
|
|
||||||
# Disable auto-import from JSONL when it's newer than database
|
|
||||||
# no-auto-import: false
|
|
||||||
|
|
||||||
# Enable JSON output by default
|
|
||||||
# json: false
|
|
||||||
|
|
||||||
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
|
|
||||||
# actor: ""
|
|
||||||
|
|
||||||
# Path to database (overridden by BEADS_DB or --db)
|
|
||||||
# db: ""
|
|
||||||
|
|
||||||
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
|
|
||||||
# auto-start-daemon: true
|
|
||||||
|
|
||||||
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
|
|
||||||
# flush-debounce: "5s"
|
|
||||||
|
|
||||||
# Git branch for beads commits (bd sync will commit to this branch)
|
|
||||||
# IMPORTANT: Set this for team projects so all clones use the same sync branch.
|
|
||||||
# This setting persists across clones (unlike database config which is gitignored).
|
|
||||||
# Can also use BEADS_SYNC_BRANCH env var for local override.
|
|
||||||
# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.
|
|
||||||
# sync-branch: "beads-sync"
|
|
||||||
|
|
||||||
# Multi-repo configuration (experimental - bd-307)
|
|
||||||
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
|
|
||||||
# repos:
|
|
||||||
# primary: "." # Primary repo (where this database lives)
|
|
||||||
# additional: # Additional repos to hydrate from (read-only)
|
|
||||||
# - ~/beads-planning # Personal planning repo
|
|
||||||
# - ~/work-planning # Work planning repo
|
|
||||||
|
|
||||||
# Integration settings (access with 'bd config get/set')
|
|
||||||
# These are stored in the database, not in this file:
|
|
||||||
# - jira.url
|
|
||||||
# - jira.project
|
|
||||||
# - linear.url
|
|
||||||
# - linear.api-key
|
|
||||||
# - github.org
|
|
||||||
# - github.repo
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"database": "beads.db",
|
|
||||||
"jsonl_export": "issues.jsonl"
|
|
||||||
}
|
|
||||||
@@ -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,234 +0,0 @@
|
|||||||
---
|
|
||||||
name: changelog
|
|
||||||
description: Maintains IdeaVim changelog (CHANGES.md) and build.gradle.kts changeNotes. Use when updating 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) and build.gradle.kts changeNotes in sync with code changes.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
### 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. Update the `changeNotes` section in `build.gradle.kts` (see detailed instructions below)
|
|
||||||
8. 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
|
|
||||||
|
|
||||||
## Updating changeNotes in build.gradle.kts
|
|
||||||
|
|
||||||
The `changeNotes` section in `build.gradle.kts` displays on the JetBrains Marketplace plugin page. Follow these rules:
|
|
||||||
|
|
||||||
### Content Requirements
|
|
||||||
- **Match CHANGES.md exactly**: Use the same content from the `[To Be Released]` section
|
|
||||||
- **Don't create a shorter version**: Include all entries as they appear in CHANGES.md
|
|
||||||
- **Keep the same level of detail**: Don't summarize or condense
|
|
||||||
|
|
||||||
### HTML Formatting
|
|
||||||
Convert Markdown to HTML format:
|
|
||||||
- Headers: `### Features:` -> `<b>Features:</b>`
|
|
||||||
- Line breaks: Use `<br>` between items
|
|
||||||
- Links: Convert markdown links to HTML `<a href="">` tags
|
|
||||||
- Bullet points: Use `*` or keep `*` with proper spacing
|
|
||||||
- Code blocks: Use `<code>` tags for commands like `<code>gn</code>`
|
|
||||||
|
|
||||||
### Special Notes
|
|
||||||
- **IMPORTANT**: Keep any existing information about the reward program in changeNotes
|
|
||||||
- This content appears in the plugin description on JetBrains Marketplace
|
|
||||||
|
|
||||||
### Example Conversion
|
|
||||||
Markdown in CHANGES.md:
|
|
||||||
```
|
|
||||||
### Features:
|
|
||||||
* Added support for `gn` text object
|
|
||||||
* [VIM-3456](https://youtrack.jetbrains.com/issue/VIM-3456) Fixed cursor position
|
|
||||||
```
|
|
||||||
|
|
||||||
HTML in changeNotes:
|
|
||||||
```html
|
|
||||||
<b>Features:</b><br>
|
|
||||||
* Added support for <code>gn</code> text object<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3456">VIM-3456</a> Fixed cursor position<br>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
20
.github/workflows/mergeClaudeChangelogPR.yml
vendored
Normal file
20
.github/workflows/mergeClaudeChangelogPR.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Claude changelog auto-merge
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-changelog:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.repository == 'JetBrains/ideavim' &&
|
||||||
|
github.event.pull_request.user.login == 'claude[bot]' &&
|
||||||
|
startsWith(github.event.pull_request.title, 'Update changelog:')
|
||||||
|
steps:
|
||||||
|
- name: Auto-merge Claude changelog PR
|
||||||
|
run: gh pr merge --auto --rebase "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{github.event.pull_request.html_url}}
|
||||||
|
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
182
.github/workflows/runUiOctopusTests.yml
vendored
182
.github/workflows/runUiOctopusTests.yml
vendored
@@ -1,182 +0,0 @@
|
|||||||
name: Run Non Octopus UI 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
|
|
||||||
- name: Build Plugin
|
|
||||||
run: gradle :buildPlugin
|
|
||||||
- name: Run Idea
|
|
||||||
run: |
|
|
||||||
mkdir -p build/reports
|
|
||||||
gradle runIdeForUiTests -Doctopus.handler=false > 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: 20
|
|
||||||
retry-delay: 10s
|
|
||||||
- name: Tests
|
|
||||||
run: gradle :tests:ui-ij-tests:testUi
|
|
||||||
- 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/IU-*/log_runIdeForUiTests idea-sandbox-log
|
|
||||||
- name: AI Analysis of Test Failures
|
|
||||||
if: failure()
|
|
||||||
uses: anthropics/claude-code-action@v1
|
|
||||||
with:
|
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
||||||
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-ij-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-ij-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 Non-Octopus UI tests: `gradle :tests:ui-ij-tests:testUi --tests "YourTestClassName.yourTestMethod"`
|
|
||||||
- To run all Non-Octopus UI tests: `gradle :tests:ui-ij-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-ij-tests/build/reports
|
|
||||||
idea-sandbox-log
|
|
||||||
# build-for-ui-test-linux:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v2
|
|
||||||
# - name: Setup Java
|
|
||||||
# uses: actions/setup-java@v2.1.0
|
|
||||||
# with:
|
|
||||||
# distribution: zulu
|
|
||||||
# java-version: 11
|
|
||||||
# - name: Build Plugin
|
|
||||||
# run: gradle :buildPlugin
|
|
||||||
# - name: Run Idea
|
|
||||||
# run: |
|
|
||||||
# export DISPLAY=:99.0
|
|
||||||
# Xvfb -ac :99 -screen 0 1920x1080x16 &
|
|
||||||
# mkdir -p build/reports
|
|
||||||
# gradle :runIdeForUiTests #> build/reports/idea.log
|
|
||||||
# - name: Wait for Idea started
|
|
||||||
# uses: jtalk/url-health-check-action@1.5
|
|
||||||
# with:
|
|
||||||
# url: http://127.0.0.1:8082
|
|
||||||
# max-attempts: 15
|
|
||||||
# retry-delay: 30s
|
|
||||||
# - name: Tests
|
|
||||||
# run: gradle :testUi
|
|
||||||
# - name: Save fails report
|
|
||||||
# if: ${{ failure() }}
|
|
||||||
# uses: actions/upload-artifact@v2
|
|
||||||
# with:
|
|
||||||
# name: ui-test-fails-report-linux
|
|
||||||
# path: |
|
|
||||||
# ui-test-example/build/reports
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -26,6 +26,10 @@
|
|||||||
.teamcity/target
|
.teamcity/target
|
||||||
.teamcity/*.iml
|
.teamcity/*.iml
|
||||||
|
|
||||||
|
# Generated by gradle task "generateGrammarSource"
|
||||||
|
vim-engine/src/main/java/com/maddyhome/idea/vim/parser/generated
|
||||||
|
vim-engine/src/main/java/com/maddyhome/idea/vim/regexp/parser/generated
|
||||||
|
|
||||||
# Created by github automation
|
# Created by github automation
|
||||||
settings.xml
|
settings.xml
|
||||||
|
|
||||||
|
|||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -5,7 +5,6 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -18,5 +18,5 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
||||||
4
.teamcity/_Self/Project.kt
vendored
4
.teamcity/_Self/Project.kt
vendored
@@ -17,7 +17,6 @@ import _Self.buildTypes.RandomOrderTests
|
|||||||
import _Self.buildTypes.SplitModeTests
|
import _Self.buildTypes.SplitModeTests
|
||||||
|
|
||||||
import _Self.buildTypes.TestingBuildType
|
import _Self.buildTypes.TestingBuildType
|
||||||
import _Self.buildTypes.TypeScriptTest
|
|
||||||
import _Self.subprojects.Releases
|
import _Self.subprojects.Releases
|
||||||
import _Self.vcsRoots.ReleasesVcsRoot
|
import _Self.vcsRoots.ReleasesVcsRoot
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType
|
import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType
|
||||||
@@ -44,9 +43,6 @@ object Project : Project({
|
|||||||
buildType(Nvim)
|
buildType(Nvim)
|
||||||
buildType(PluginVerifier)
|
buildType(PluginVerifier)
|
||||||
buildType(Compatibility)
|
buildType(Compatibility)
|
||||||
|
|
||||||
// TypeScript scripts test
|
|
||||||
buildType(TypeScriptTest)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Agent size configurations (CPU count)
|
// Agent size configurations (CPU count)
|
||||||
|
|||||||
63
.teamcity/_Self/buildTypes/Compatibility.kt
vendored
63
.teamcity/_Self/buildTypes/Compatibility.kt
vendored
@@ -33,40 +33,47 @@ object Compatibility : IdeaVimBuildType({
|
|||||||
name = "Load Verifier"
|
name = "Load Verifier"
|
||||||
scriptContent = """
|
scriptContent = """
|
||||||
mkdir verifier1
|
mkdir verifier1
|
||||||
curl -f -L -o verifier1/verifier-cli-dev-all-2.jar "https://packages.jetbrains.team/files/p/ideavim/plugin-verifier/verifier-cli-dev-all-2.jar"
|
curl -f -L -o verifier1/verifier-cli-ideavim.jar "https://github.com/AlexPl292/intellij-plugin-verifier/releases/download/cli-3/verifier-cli-1.403-ideavim-3-all.jar"
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
script {
|
script {
|
||||||
name = "Check"
|
name = "Check"
|
||||||
scriptContent = """
|
scriptContent = """
|
||||||
# We use a custom build of verifier that downloads IdeaVim from dev channel
|
# We use a custom build of plugin-verifier that resolves IdeaVim from the dev channel.
|
||||||
# To create a custom build: Download plugin verifier repo, add an if that switches to dev channel for IdeaVim repo
|
# The fork lives at https://github.com/AlexPl292/intellij-plugin-verifier — the patch is in
|
||||||
# At the moment it's com.jetbrains.pluginverifier.repository.repositories.marketplace.MarketplaceRepository#getLastCompatibleVersionOfPlugin
|
# com.jetbrains.pluginverifier.repository.repositories.marketplace.MarketplaceRepository#getLastCompatibleVersionOfPlugin
|
||||||
# Build using gradlew :intellij-plugin-verifier:verifier-cli:shadowJar
|
# (switches the marketplace channel to "dev" when pluginId is org.jetbrains.IdeaVim).
|
||||||
# Upload verifier-cli-dev-all.jar artifact to the repo in IdeaVim space repo
|
#
|
||||||
|
# To refresh against upstream:
|
||||||
|
# 1. In the fork, pull from upstream and re-apply the dev-channel patch.
|
||||||
|
# 2. Run the "Publish verifier-cli" workflow:
|
||||||
|
# https://github.com/AlexPl292/intellij-plugin-verifier/actions/workflows/publish-verifier-cli.yml
|
||||||
|
# It builds the shadow jar and attaches it to a new GitHub Release.
|
||||||
|
# 3. Update the release URL in the "Load Verifier" step above to point at the new jar.
|
||||||
|
|
||||||
java --version
|
java --version
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}org.jetbrains.IdeaVim-EasyMotion' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}org.jetbrains.IdeaVim-EasyMotion' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}eu.theblob42.idea.whichkey' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}eu.theblob42.idea.whichkey' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}IdeaVimExtension' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}IdeaVimExtension' [latest-IU] -team-city
|
||||||
# Outdated java -jar verifier/verifier-cli-dev-all.jar check-plugin '${'$'}github.zgqq.intellij-enhance' [latest-IU] -team-city
|
# Outdated java -jar verifier/verifier-cli-ideavim.jar check-plugin '${'$'}github.zgqq.intellij-enhance' [latest-IU] -team-city
|
||||||
# java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.copilot' [latest-IU] -team-city
|
# java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.copilot' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.dankinsoid.multicursor' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.dankinsoid.multicursor' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.joshestein.ideavim-quickscope' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.joshestein.ideavim-quickscope' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.peekaboo' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.julienphalip.ideavim.peekaboo' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.switch' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.julienphalip.ideavim.switch' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.functiontextobj' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.julienphalip.ideavim.functiontextobj' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.miksuki.HighlightCursor' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.miksuki.HighlightCursor' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.ugarosa.idea.edgemotion' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.ugarosa.idea.edgemotion' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}cn.mumukehao.plugin' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}cn.mumukehao.plugin' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.magidc.ideavim.dial' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.magidc.ideavim.dial' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}dev.ghostflyby.ideavim.toggleIME' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}dev.ghostflyby.ideavim.toggleIME' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.magidc.ideavim.anyObject' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.magidc.ideavim.anyObject' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.yelog.ideavim.cmdfloat' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.yelog.ideavim.cmdfloat' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}gg.ninetyfive' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}gg.ninetyfive' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.pooryam92.vimcoach' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.pooryam92.vimcoach' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}lazyideavim.whichkeylazy' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}lazyideavim.whichkeylazy' [latest-IU] -team-city
|
||||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.vimkeysuggest' [latest-IU] -team-city
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.vimkeysuggest' [latest-IU] -team-city
|
||||||
|
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}dev.ckob.lazygit' [latest-IU] -team-city
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
9
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
@@ -28,7 +28,6 @@ object SplitModeTests : IdeaVimBuildType({
|
|||||||
params {
|
params {
|
||||||
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
|
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
|
||||||
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
|
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
|
||||||
param("env.DISPLAY", ":99")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vcs {
|
vcs {
|
||||||
@@ -40,12 +39,8 @@ object SplitModeTests : IdeaVimBuildType({
|
|||||||
|
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
name = "Start Xvfb and run split mode tests"
|
name = "Run split mode tests"
|
||||||
scriptContent = """
|
scriptContent = "xvfb-run -a -s '-screen 0 1920x1080x24' ./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace"
|
||||||
Xvfb :99 -screen 0 1920x1080x24 &
|
|
||||||
sleep 2
|
|
||||||
./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
.teamcity/_Self/buildTypes/TypeScriptTest.kt
vendored
45
.teamcity/_Self/buildTypes/TypeScriptTest.kt
vendored
@@ -1,45 +0,0 @@
|
|||||||
package _Self.buildTypes
|
|
||||||
|
|
||||||
import _Self.IdeaVimBuildType
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
|
|
||||||
|
|
||||||
object TypeScriptTest : IdeaVimBuildType({
|
|
||||||
id("IdeaVimTests_TypeScript")
|
|
||||||
name = "TypeScript Scripts Test"
|
|
||||||
description = "Test that TypeScript scripts can run on TeamCity"
|
|
||||||
|
|
||||||
vcs {
|
|
||||||
root(DslContext.settingsRoot)
|
|
||||||
branchFilter = "+:<default>"
|
|
||||||
|
|
||||||
checkoutMode = CheckoutMode.AUTO
|
|
||||||
}
|
|
||||||
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
name = "Set up Node.js"
|
|
||||||
scriptContent = """
|
|
||||||
wget https://nodejs.org/dist/v20.18.1/node-v20.18.1-linux-x64.tar.xz
|
|
||||||
tar xf node-v20.18.1-linux-x64.tar.xz
|
|
||||||
export PATH="${"$"}PWD/node-v20.18.1-linux-x64/bin:${"$"}PATH"
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
script {
|
|
||||||
name = "Run TypeScript test"
|
|
||||||
scriptContent = """
|
|
||||||
export PATH="${"$"}PWD/node-v20.18.1-linux-x64/bin:${"$"}PATH"
|
|
||||||
cd scripts-ts
|
|
||||||
npm install
|
|
||||||
npx tsx src/teamcityTest.ts
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requirements {
|
|
||||||
equals("teamcity.agent.os.family", "Linux")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
56
CHANGES.md
56
CHANGES.md
@@ -26,6 +26,8 @@ usual beta standards.
|
|||||||
## [To Be Released]
|
## [To Be Released]
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
|
* [VIM-1693](https://youtrack.jetbrains.com/issue/VIM-1693) Added `:autocmd` command - run Ex commands on editor events such as `BufRead`, `BufWrite`, `BufEnter`, `BufLeave`, `InsertEnter`, `InsertLeave`, `WinEnter`, `WinLeave`, `FocusGained`, `FocusLost`, and `FileType`; supports `augroup` and file pattern matching (e.g., `autocmd BufWritePre *.py echo "saving python"`)
|
||||||
|
* [VIM-268](https://youtrack.jetbrains.com/issue/VIM-268) Added file name completion in ex commands - press `<Tab>`/`<S-Tab>` to cycle through file matches in `:edit`, `:split`, `:vsplit`, `:write`, `:read`, `:source`, and `:find` commands; use arrow keys to navigate the completion panel
|
||||||
* New VimScript functions: `add()`, `call()`, `extend()`, `extendnew()`, `filter()`, `flatten()`, `flattennew()`, `foreach()`, `has_key()`, `indexof()`, `insert()`, `items()`, `keys()`, `map()`, `mapnew()`, `reduce()`, `remove()`, `slice()`, `sort()`, `uniq()`, `values()`
|
* New VimScript functions: `add()`, `call()`, `extend()`, `extendnew()`, `filter()`, `flatten()`, `flattennew()`, `foreach()`, `has_key()`, `indexof()`, `insert()`, `items()`, `keys()`, `map()`, `mapnew()`, `reduce()`, `remove()`, `slice()`, `sort()`, `uniq()`, `values()`
|
||||||
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read` command - insert file content below current line (e.g., `:read file.txt`, `0read file.txt`)
|
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read` command - insert file content below current line (e.g., `:read file.txt`, `0read file.txt`)
|
||||||
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read!` command - insert shell command output below current line (e.g., `:read! echo "hello"`)
|
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read!` command - insert shell command output below current line (e.g., `:read! echo "hello"`)
|
||||||
@@ -34,8 +36,16 @@ usual beta standards.
|
|||||||
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zm` command - decrease fold level to hide more folds
|
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zm` command - decrease fold level to hide more folds
|
||||||
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zf` command - create fold from selection or motion
|
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zf` command - create fold from selection or motion
|
||||||
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `:set foldlevel` option - control fold visibility level
|
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `:set foldlevel` option - control fold visibility level
|
||||||
|
* [VIM-1158](https://youtrack.jetbrains.com/issue/VIM-1158) Added `gw` command - reformat code like `gq` but preserving the cursor position
|
||||||
|
* [VIM-3975](https://youtrack.jetbrains.com/issue/VIM-3975) Added `mode()` VimScript function - returns the current editing mode (e.g., `'n'` for normal, `'i'` for insert, `'v'` for visual, `'R'` for replace)
|
||||||
|
* [VIM-519](https://youtrack.jetbrains.com/issue/VIM-519) Added `g;` and `g,` commands - navigate the change list to jump to previous (`g;`) or next (`g,`) edit location
|
||||||
|
* [VIM-258](https://youtrack.jetbrains.com/issue/VIM-258) Added command name completion in ex commands - press `<Tab>` to cycle through matching command names (e.g., `:e<Tab>` shows `:edit`, `:earlier`, etc.)
|
||||||
|
|
||||||
### Fixes:
|
### Fixes:
|
||||||
|
* [VIM-4197](https://youtrack.jetbrains.com/issue/VIM-4197) Fixed Vim features (e.g., `f`, `w`, text objects) not working in Java files decompiled from Kotlin class files
|
||||||
|
* [VIM-4112](https://youtrack.jetbrains.com/issue/VIM-4112) Fixed undo after block-visual edit (`<C-V>...x`, `<C-V>...c`, `<C-V>...I`) leaving stray carets in normal mode
|
||||||
|
* [VIM-4176](https://youtrack.jetbrains.com/issue/VIM-4176) Fixed race condition in single-line output panel that could cause `*` search wrapping to behave unreliably
|
||||||
|
* [VIM-4175](https://youtrack.jetbrains.com/issue/VIM-4175) Fixed search "not found" showing previous "Hit ENTER" text alongside the error - panel is now cleared before displaying errors like "E486: Pattern not found"
|
||||||
* [VIM-4135](https://youtrack.jetbrains.com/issue/VIM-4135) Fixed IdeaVim not loading in Rider
|
* [VIM-4135](https://youtrack.jetbrains.com/issue/VIM-4135) Fixed IdeaVim not loading in Rider
|
||||||
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed undo in commentary - `gcc`/`gc{motion}` changes are now properly grouped as a single undo step
|
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed undo in commentary - `gcc`/`gc{motion}` changes are now properly grouped as a single undo step
|
||||||
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed `=` (format/auto-indent) action in split mode
|
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed `=` (format/auto-indent) action in split mode
|
||||||
@@ -45,6 +55,7 @@ usual beta standards.
|
|||||||
* [VIM-4094](https://youtrack.jetbrains.com/issue/VIM-4094) Fixed UninitializedPropertyAccessException when loading history
|
* [VIM-4094](https://youtrack.jetbrains.com/issue/VIM-4094) Fixed UninitializedPropertyAccessException when loading history
|
||||||
* [VIM-4016](https://youtrack.jetbrains.com/issue/VIM-4016) Fixed `:edit` command when project has no source roots
|
* [VIM-4016](https://youtrack.jetbrains.com/issue/VIM-4016) Fixed `:edit` command when project has no source roots
|
||||||
* [VIM-3948](https://youtrack.jetbrains.com/issue/VIM-3948) Improved hint generation visibility checks for better UI component detection
|
* [VIM-3948](https://youtrack.jetbrains.com/issue/VIM-3948) Improved hint generation visibility checks for better UI component detection
|
||||||
|
* [VIM-4195](https://youtrack.jetbrains.com/issue/VIM-4195) Fixed settings not being saved in remote development (split) mode
|
||||||
* [VIM-3473](https://youtrack.jetbrains.com/issue/VIM-3473) Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs
|
* [VIM-3473](https://youtrack.jetbrains.com/issue/VIM-3473) Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs
|
||||||
* [VIM-2821](https://youtrack.jetbrains.com/issue/VIM-2821) Fixed undo grouping when repeating text insertion with `.` in remote development (split mode)
|
* [VIM-2821](https://youtrack.jetbrains.com/issue/VIM-2821) Fixed undo grouping when repeating text insertion with `.` in remote development (split mode)
|
||||||
* [VIM-1705](https://youtrack.jetbrains.com/issue/VIM-1705) Fixed window-switching commands (e.g., `<C-w>h`) during macro playback
|
* [VIM-1705](https://youtrack.jetbrains.com/issue/VIM-1705) Fixed window-switching commands (e.g., `<C-w>h`) during macro playback
|
||||||
@@ -60,8 +71,52 @@ usual beta standards.
|
|||||||
* Fixed `IndexOutOfBoundsException` when using text objects like `a)` at end of file
|
* Fixed `IndexOutOfBoundsException` when using text objects like `a)` at end of file
|
||||||
* Fixed high CPU usage while showing command line
|
* Fixed high CPU usage while showing command line
|
||||||
* Fixed comparison of String and Number in VimScript expressions
|
* Fixed comparison of String and Number in VimScript expressions
|
||||||
|
* Fixed `\/`, `\?`, and `\&` in Ex command ranges now correctly report E35/E33 errors when no previous search or substitute pattern exists, instead of crashing
|
||||||
|
* [VIM-4172](https://youtrack.jetbrains.com/issue/VIM-4172) IdeaVim is now disabled in Python Console to prevent key interference
|
||||||
|
* [VIM-4113](https://youtrack.jetbrains.com/issue/VIM-4113) Fixed Visual mode commands (e.g., `:'<,'>sort`) failing when run off the Event Dispatch Thread
|
||||||
|
* [VIM-3727](https://youtrack.jetbrains.com/issue/VIM-3727) Fixed Enter and arrow keys not working in Python Console in split mode
|
||||||
|
* Fixed NERDTree navigation (`j`/`k`/`G`/`gg`/`p`/`<C-J>`/`<C-K>`) poor performance in split mode - navigation now uses Swing actions directly instead of going through backend RPC
|
||||||
|
* [VIM-4180](https://youtrack.jetbrains.com/issue/VIM-4180) Fixed ReplaceWithRegister plugin's default `gr`/`grr` mappings overriding user-defined key mappings
|
||||||
|
* Fixed `IndexOutOfBoundsException` when using `:command` with `-nargs` option but without a command name
|
||||||
|
* Fixed spurious beep when pressing `<Esc>` to cancel register selection in normal mode (after pressing `"`)
|
||||||
|
* [VIM-4202](https://youtrack.jetbrains.com/issue/VIM-4202) Fixed `<S-Tab>` being intercepted by IdeaVim - users can now remap `<S-Tab>` to other IntelliJ actions
|
||||||
|
* [VIM-4202](https://youtrack.jetbrains.com/issue/VIM-4202) Fixed `gcc`/`gc{motion}` commentary leaving editor in incorrect mode in Rider/CLion split mode
|
||||||
|
* [VIM-4115](https://youtrack.jetbrains.com/issue/VIM-4115) Fixed NullPointerException in `CommandKeyConsumer` when pressing Esc after disabling and re-enabling IdeaVim with an open command line
|
||||||
|
* [VIM-4209](https://youtrack.jetbrains.com/issue/VIM-4209) Fixed `<Esc>` not exiting insert mode in Rider/CLion when a `<C-Space>` completion popup intercepts the key before IdeaVim
|
||||||
|
* [VIM-4211](https://youtrack.jetbrains.com/issue/VIM-4211) Fixed IdeaVim not working in the Git commit window
|
||||||
|
* [VIM-4202](https://youtrack.jetbrains.com/issue/VIM-4202) Fixed `gcc`/`gc{motion}` commentary not adding space after `//` prefix in C/C++/C# files in Rider/CLion split mode
|
||||||
|
* [VIM-4219](https://youtrack.jetbrains.com/issue/VIM-4219) Fixed NullPointerException when IdeaVim is being disabled/unloaded
|
||||||
|
* [VIM-4221](https://youtrack.jetbrains.com/issue/VIM-4221) Fixed error sound being played on each keypress when `incsearch` is enabled and the typed pattern is an invalid regex
|
||||||
|
* [VIM-4196](https://youtrack.jetbrains.com/issue/VIM-4196) Fixed NERDTree file selection not being restored after pressing `<Esc>` to cancel a `/` speed search
|
||||||
|
* [VIM-4211](https://youtrack.jetbrains.com/issue/VIM-4211) Fixed IdeaVim not working in Git commit window when the Conventional Commits plugin is installed
|
||||||
|
* [VIM-4224](https://youtrack.jetbrains.com/issue/VIM-4224) Fixed `:s` `e` flag now properly suppresses "Pattern not found" errors - e.g., `%s/\s\+$//e` no longer errors when there is no trailing whitespace
|
||||||
|
* [VIM-4226](https://youtrack.jetbrains.com/issue/VIM-4226) Fixed race condition crash when the editor is disposed while the ex panel is open
|
||||||
|
|
||||||
### Merged PRs:
|
### Merged PRs:
|
||||||
|
* [1747](https://github.com/JetBrains/ideavim/pull/1747) by [1grzyb1](https://github.com/1grzyb1): feat(VIM-519): cycle through recent edits with g; and g,
|
||||||
|
* [1745](https://github.com/JetBrains/ideavim/pull/1745) by [1grzyb1](https://github.com/1grzyb1): feat(VIM-258): tab command completion
|
||||||
|
* [1744](https://github.com/JetBrains/ideavim/pull/1744) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4226): check if editor is disposed on focus
|
||||||
|
* [1741](https://github.com/JetBrains/ideavim/pull/1741) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4224): respect e flag in search patterns
|
||||||
|
* [1740](https://github.com/JetBrains/ideavim/pull/1740) by [1grzyb1](https://github.com/1grzyb1): feat(VIM-3975): support vim mode() function
|
||||||
|
* [1739](https://github.com/JetBrains/ideavim/pull/1739) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4196): restore file selection after esc in nerdtree
|
||||||
|
* [1738](https://github.com/JetBrains/ideavim/pull/1738) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4211): commit window work with conectional commits plugin
|
||||||
|
* [1730](https://github.com/JetBrains/ideavim/pull/1730) by [1grzyb1](https://github.com/1grzyb1): FIX(VIM-4221) Don't make angry sounds on search
|
||||||
|
* [1728](https://github.com/JetBrains/ideavim/pull/1728) by [1grzyb1](https://github.com/1grzyb1): VIM-4202 Add space after c langauges comments
|
||||||
|
* [1727](https://github.com/JetBrains/ideavim/pull/1727) by [1grzyb1](https://github.com/1grzyb1): FIX(VIM-4219) check for in VimPLugin is not null
|
||||||
|
* [1720](https://github.com/JetBrains/ideavim/pull/1720) by [1grzyb1](https://github.com/1grzyb1): fix: make ideavim work in commit window
|
||||||
|
* [1717](https://github.com/JetBrains/ideavim/pull/1717) by [1grzyb1](https://github.com/1grzyb1): Fix(VIM-4209): handle esc in rider before popup
|
||||||
|
* [1704](https://github.com/JetBrains/ideavim/pull/1704) by [1grzyb1](https://github.com/1grzyb1): VIM-4202 Don't intercept all <S-Tab>
|
||||||
|
* [1703](https://github.com/JetBrains/ideavim/pull/1703) by [1grzyb1](https://github.com/1grzyb1): VIM-4202 Fix state after commentary action
|
||||||
|
* [1700](https://github.com/JetBrains/ideavim/pull/1700) by [1grzyb1](https://github.com/1grzyb1): VIM-4139 Compute nesting depth for fold regions
|
||||||
|
* [1699](https://github.com/JetBrains/ideavim/pull/1699) by [1grzyb1](https://github.com/1grzyb1): VIM-4112 collapse restored carets after undo of block-visual edit
|
||||||
|
* [1696](https://github.com/JetBrains/ideavim/pull/1696) by [citizenmatt](https://github.com/citizenmatt): VIM-4197 Fix missing Vim features in Java files decompiled from Kotlin class files
|
||||||
|
* [1695](https://github.com/JetBrains/ideavim/pull/1695) by [1grzyb1](https://github.com/1grzyb1): VIM-1693 Implement autocmd
|
||||||
|
* [1690](https://github.com/JetBrains/ideavim/pull/1690) by [1grzyb1](https://github.com/1grzyb1): Make nerdtree work without calling backend actions
|
||||||
|
* [1688](https://github.com/JetBrains/ideavim/pull/1688) by [1grzyb1](https://github.com/1grzyb1): VIM-4172 Disable ideavim in Python Console
|
||||||
|
* [1687](https://github.com/JetBrains/ideavim/pull/1687) by [1grzyb1](https://github.com/1grzyb1): Restore old VimPLugin method signatures
|
||||||
|
* [1685](https://github.com/JetBrains/ideavim/pull/1685) by [1grzyb1](https://github.com/1grzyb1): VIM-3727 Fix Python console Enter and arrow keys in split mode
|
||||||
|
* [1548](https://github.com/JetBrains/ideavim/pull/1548) by [1grzyb1](https://github.com/1grzyb1): VIM-1158 Add `gw` to reformat code with preserving the cursor position
|
||||||
|
* [1682](https://github.com/JetBrains/ideavim/pull/1682) by [1grzyb1](https://github.com/1grzyb1): VIM-268 Complete file names in edit command
|
||||||
* [1632](https://github.com/JetBrains/ideavim/pull/1632) by [chylex](https://github.com/chylex): Fix pumvisible returning opposite result
|
* [1632](https://github.com/JetBrains/ideavim/pull/1632) by [chylex](https://github.com/chylex): Fix pumvisible returning opposite result
|
||||||
* [1615](https://github.com/JetBrains/ideavim/pull/1615) by [1grzyb1](https://github.com/1grzyb1): Fix IndexOutOfBoundsException in findBlock when caret is at end of file
|
* [1615](https://github.com/JetBrains/ideavim/pull/1615) by [1grzyb1](https://github.com/1grzyb1): Fix IndexOutOfBoundsException in findBlock when caret is at end of file
|
||||||
* [1613](https://github.com/JetBrains/ideavim/pull/1613) by [1grzyb1](https://github.com/1grzyb1): VIM-3473 Sync ideavim in remdev
|
* [1613](https://github.com/JetBrains/ideavim/pull/1613) by [1grzyb1](https://github.com/1grzyb1): VIM-3473 Sync ideavim in remdev
|
||||||
@@ -69,6 +124,7 @@ usual beta standards.
|
|||||||
* [1585](https://github.com/JetBrains/ideavim/pull/1585) by [1grzyb1](https://github.com/1grzyb1): Break in case of maximum recursion depth
|
* [1585](https://github.com/JetBrains/ideavim/pull/1585) by [1grzyb1](https://github.com/1grzyb1): Break in case of maximum recursion depth
|
||||||
* [1414](https://github.com/JetBrains/ideavim/pull/1414) by [Matt Ellis](https://github.com/citizenmatt): Refactor/functions
|
* [1414](https://github.com/JetBrains/ideavim/pull/1414) by [Matt Ellis](https://github.com/citizenmatt): Refactor/functions
|
||||||
* [1442](https://github.com/JetBrains/ideavim/pull/1442) by [Matt Ellis](https://github.com/citizenmatt): Fix high CPU usage while showing command line
|
* [1442](https://github.com/JetBrains/ideavim/pull/1442) by [Matt Ellis](https://github.com/citizenmatt): Fix high CPU usage while showing command line
|
||||||
|
* [1665](https://github.com/JetBrains/ideavim/pull/1665) by [1grzyb1](https://github.com/1grzyb1): Fix visual selection commands failing off-EDT due to nested write-in-read action
|
||||||
|
|
||||||
## 2.28.0, 2025-12-09
|
## 2.28.0, 2025-12-09
|
||||||
|
|
||||||
|
|||||||
39
CLAUDE.md
39
CLAUDE.md
@@ -1,39 +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
|
|
||||||
|
|
||||||
## 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
|
|
||||||
18
README.md
18
README.md
@@ -241,6 +241,24 @@ ShowHoverInfo - Quick Documentation and Error Description
|
|||||||
QuickImplementations - Quick Definition
|
QuickImplementations - Quick Definition
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Autocmd
|
||||||
|
----------
|
||||||
|
|
||||||
|
IdeaVim supports Vim’s `:autocmd` for running commands on editor events, including
|
||||||
|
`InsertEnter`/`InsertLeave`, buffer events (`BufEnter`, `BufLeave`, `BufRead`,
|
||||||
|
`BufNewFile`, `BufWritePre`, `BufWritePost`), window events (`WinEnter`, `WinLeave`),
|
||||||
|
focus events (`FocusGained`, `FocusLost`), and `FileType`. Full glob patterns
|
||||||
|
(`*`, `**`, `?`, `[abc]`, `{a,b}`) and augroups are supported.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
autocmd BufWritePre *.py echo "saving python"
|
||||||
|
autocmd FileType python setlocal shiftwidth=4
|
||||||
|
```
|
||||||
|
|
||||||
|
See [doc/autocmd.md](doc/autocmd.md) for the full event reference, firing order, and notes on IntelliJ-specific
|
||||||
|
differences.
|
||||||
|
|
||||||
|
|
||||||
Vim Script
|
Vim Script
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ IdeaVim project is licensed under MIT license except the following parts of it:
|
|||||||
|
|
||||||
* File [ScrollViewHelper.kt](com/maddyhome/idea/vim/helper/ScrollViewHelper.kt) is licensed under Vim License.
|
* File [ScrollViewHelper.kt](com/maddyhome/idea/vim/helper/ScrollViewHelper.kt) is licensed under Vim License.
|
||||||
* File [Tutor.kt](src/main/java/com/maddyhome/idea/vim/ui/Tutor.kt) is licensed under Vim License.
|
* File [Tutor.kt](src/main/java/com/maddyhome/idea/vim/ui/Tutor.kt) is licensed under Vim License.
|
||||||
|
* File [CodeWrapper.kt](vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/CodeWrapper.kt) is licensed under Vim
|
||||||
|
License.
|
||||||
|
|
||||||
```
|
```
|
||||||
VIM LICENSE
|
VIM LICENSE
|
||||||
|
|||||||
141
build.gradle.kts
141
build.gradle.kts
@@ -27,11 +27,11 @@ buildscript {
|
|||||||
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
|
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
|
||||||
classpath("org.kohsuke:github-api:1.305")
|
classpath("org.kohsuke:github-api:1.305")
|
||||||
|
|
||||||
classpath("io.ktor:ktor-client-core:3.4.2")
|
classpath("io.ktor:ktor-client-core:3.4.3")
|
||||||
classpath("io.ktor:ktor-client-cio:3.4.2")
|
classpath("io.ktor:ktor-client-cio:3.4.3")
|
||||||
classpath("io.ktor:ktor-client-auth:3.4.2")
|
classpath("io.ktor:ktor-client-auth:3.4.3")
|
||||||
classpath("io.ktor:ktor-client-content-negotiation:3.4.2")
|
classpath("io.ktor:ktor-client-content-negotiation:3.4.3")
|
||||||
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
|
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.3")
|
||||||
|
|
||||||
// This comes from the changelog plugin
|
// This comes from the changelog plugin
|
||||||
// classpath("org.jetbrains:markdown:0.3.1")
|
// classpath("org.jetbrains:markdown:0.3.1")
|
||||||
@@ -113,6 +113,8 @@ dependencies {
|
|||||||
testFramework(TestFrameworkType.Platform)
|
testFramework(TestFrameworkType.Platform)
|
||||||
testFramework(TestFrameworkType.JUnit5)
|
testFramework(TestFrameworkType.JUnit5)
|
||||||
|
|
||||||
|
compatiblePlugin("com.intellij.classic.ui")
|
||||||
|
|
||||||
pluginModule(runtimeOnly(project(":modules:ideavim-common")))
|
pluginModule(runtimeOnly(project(":modules:ideavim-common")))
|
||||||
pluginModule(runtimeOnly(project(":modules:ideavim-frontend")))
|
pluginModule(runtimeOnly(project(":modules:ideavim-frontend")))
|
||||||
pluginModule(runtimeOnly(project(":modules:ideavim-backend")))
|
pluginModule(runtimeOnly(project(":modules:ideavim-backend")))
|
||||||
@@ -207,7 +209,7 @@ tasks {
|
|||||||
// Note that this will run the plugin installed in the IDE specified in dependencies. To run in a different IDE, use
|
// Note that this will run the plugin installed in the IDE specified in dependencies. To run in a different IDE, use
|
||||||
// a custom task (see below)
|
// a custom task (see below)
|
||||||
runIde {
|
runIde {
|
||||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
systemProperty("idea.trust.all.projects", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies
|
// Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies
|
||||||
@@ -225,29 +227,21 @@ tasks {
|
|||||||
|
|
||||||
val runPycharm by intellijPlatformTesting.runIde.registering {
|
val runPycharm by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.PyCharmProfessional
|
type = IntelliJPlatformType.PyCharmProfessional
|
||||||
version = "2025.3.2"
|
version = "2026.1"
|
||||||
task {
|
|
||||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val runWebstorm by intellijPlatformTesting.runIde.registering {
|
val runWebstorm by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.WebStorm
|
type = IntelliJPlatformType.WebStorm
|
||||||
version = "2025.3.2"
|
version = "2025.3.2"
|
||||||
task {
|
|
||||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val runClion by intellijPlatformTesting.runIde.registering {
|
val runClion by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.CLion
|
type = IntelliJPlatformType.CLion
|
||||||
version = "2025.3.2"
|
version = "2026.1"
|
||||||
task {
|
|
||||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
|
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
|
||||||
|
version = "2026.1"
|
||||||
task {
|
task {
|
||||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -256,7 +250,6 @@ tasks {
|
|||||||
"-Djb.privacy.policy.text=<!--999.999-->",
|
"-Djb.privacy.policy.text=<!--999.999-->",
|
||||||
"-Djb.consents.confirmation.enabled=false",
|
"-Djb.consents.confirmation.enabled=false",
|
||||||
"-Dide.show.tips.on.startup.default.value=false",
|
"-Dide.show.tips.on.startup.default.value=false",
|
||||||
"-Doctopus.handler=" + (System.getProperty("octopus.handler") ?: true),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,7 +292,7 @@ tasks {
|
|||||||
}
|
}
|
||||||
val runCLionSplitMode by intellijPlatformTesting.runIde.registering {
|
val runCLionSplitMode by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.CLion
|
type = IntelliJPlatformType.CLion
|
||||||
version = "2025.3.2"
|
version = "2026.1"
|
||||||
splitMode = true
|
splitMode = true
|
||||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||||
|
|
||||||
@@ -310,7 +303,7 @@ tasks {
|
|||||||
}
|
}
|
||||||
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
|
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.PyCharmProfessional
|
type = IntelliJPlatformType.PyCharmProfessional
|
||||||
version = "2025.3.2"
|
version = "2026.1"
|
||||||
splitMode = true
|
splitMode = true
|
||||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||||
|
|
||||||
@@ -359,6 +352,45 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val runPycharmSplitModeDebugFrontend by intellijPlatformTesting.runIde.registering {
|
||||||
|
type = IntelliJPlatformType.PyCharmProfessional
|
||||||
|
version = "2026.1"
|
||||||
|
splitMode = true
|
||||||
|
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
plugin("AceJump", "3.8.22")
|
||||||
|
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareSandboxTask {
|
||||||
|
val sandboxDir = project.layout.buildDirectory.dir("idea-sandbox").map { it.asFile }
|
||||||
|
doLast {
|
||||||
|
val debugLine = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5006"
|
||||||
|
val vmoptions = sandboxDir.get().walkTopDown()
|
||||||
|
.filter { it.name == "jetbrains_client64.vmoptions" && it.path.contains("runPycharmSplitModeDebugFrontend") }
|
||||||
|
.firstOrNull()
|
||||||
|
?: sandboxDir.get().walkTopDown()
|
||||||
|
.filter { it.name == "jetbrains_client64.vmoptions" }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
if (vmoptions != null) {
|
||||||
|
val content = vmoptions.readText()
|
||||||
|
if (debugLine !in content) {
|
||||||
|
vmoptions.appendText("\n$debugLine\n")
|
||||||
|
logger.lifecycle("Patched frontend vmoptions with JDWP debug agent: ${vmoptions.absolutePath}")
|
||||||
|
}
|
||||||
|
logger.lifecycle("Connect a Remote JVM Debug configuration to localhost:5006")
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Could not find jetbrains_client64.vmoptions in sandbox. " +
|
||||||
|
"Run `./gradlew runPycharmSplitMode` once first to populate the sandbox, then use this task."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val testIdeSplitMode by intellijPlatformTesting.testIde.registering {
|
val testIdeSplitMode by intellijPlatformTesting.testIde.registering {
|
||||||
splitMode = true
|
splitMode = true
|
||||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||||
@@ -385,11 +417,6 @@ tasks {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPlugin {
|
|
||||||
dependsOn(sourcesJar)
|
|
||||||
from(sourcesJar) { into("lib/src") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -436,6 +463,8 @@ intellijPlatform {
|
|||||||
changeNotes.set(
|
changeNotes.set(
|
||||||
"""
|
"""
|
||||||
<b>Features:</b><br>
|
<b>Features:</b><br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1693">VIM-1693</a> Added <code>:autocmd</code> command - run Ex commands on editor events such as <code>BufRead</code>, <code>BufWrite</code>, <code>BufEnter</code>, <code>BufLeave</code>, <code>InsertEnter</code>, <code>InsertLeave</code>, <code>WinEnter</code>, <code>WinLeave</code>, <code>FocusGained</code>, <code>FocusLost</code>, and <code>FileType</code>; supports <code>augroup</code> and file pattern matching (e.g., <code>autocmd BufWritePre *.py echo "saving python"</code>)<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-268">VIM-268</a> Added file name completion in ex commands - press <code><Tab></code>/<code><S-Tab></code> to cycle through file matches in <code>:edit</code>, <code>:split</code>, <code>:vsplit</code>, <code>:write</code>, <code>:read</code>, <code>:source</code>, and <code>:find</code> commands; use arrow keys to navigate the completion panel<br>
|
||||||
* New VimScript functions: <code>add()</code>, <code>call()</code>, <code>extend()</code>, <code>extendnew()</code>, <code>filter()</code>, <code>flatten()</code>, <code>flattennew()</code>, <code>foreach()</code>, <code>has_key()</code>, <code>indexof()</code>, <code>insert()</code>, <code>items()</code>, <code>keys()</code>, <code>map()</code>, <code>mapnew()</code>, <code>reduce()</code>, <code>remove()</code>, <code>slice()</code>, <code>sort()</code>, <code>uniq()</code>, <code>values()</code><br>
|
* New VimScript functions: <code>add()</code>, <code>call()</code>, <code>extend()</code>, <code>extendnew()</code>, <code>filter()</code>, <code>flatten()</code>, <code>flattennew()</code>, <code>foreach()</code>, <code>has_key()</code>, <code>indexof()</code>, <code>insert()</code>, <code>items()</code>, <code>keys()</code>, <code>map()</code>, <code>mapnew()</code>, <code>reduce()</code>, <code>remove()</code>, <code>slice()</code>, <code>sort()</code>, <code>uniq()</code>, <code>values()</code><br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read</code> command - insert file content below current line (e.g., <code>:read file.txt</code>, <code>0read file.txt</code>)<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read</code> command - insert file content below current line (e.g., <code>:read file.txt</code>, <code>0read file.txt</code>)<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read!</code> command - insert shell command output below current line (e.g., <code>:read! echo "hello"</code>)<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read!</code> command - insert shell command output below current line (e.g., <code>:read! echo "hello"</code>)<br>
|
||||||
@@ -444,8 +473,16 @@ intellijPlatform {
|
|||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zm</code> command - decrease fold level to hide more folds<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zm</code> command - decrease fold level to hide more folds<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zf</code> command - create fold from selection or motion<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zf</code> command - create fold from selection or motion<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>:set foldlevel</code> option - control fold visibility level<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>:set foldlevel</code> option - control fold visibility level<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1158">VIM-1158</a> Added <code>gw</code> command - reformat code like <code>gq</code> but preserving the cursor position<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-3975">VIM-3975</a> Added <code>mode()</code> VimScript function - returns the current editing mode (e.g., <code>'n'</code> for normal, <code>'i'</code> for insert, <code>'v'</code> for visual, <code>'R'</code> for replace)<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-519">VIM-519</a> Added <code>g;</code> and <code>g,</code> commands - navigate the change list to jump to previous (<code>g;</code>) or next (<code>g,</code>) edit location<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-258">VIM-258</a> Added command name completion in ex commands - press <code><Tab></code> to cycle through matching command names (e.g., <code>:e<Tab></code> shows <code>:edit</code>, <code>:earlier</code>, etc.)<br>
|
||||||
<br>
|
<br>
|
||||||
<b>Fixes:</b><br>
|
<b>Fixes:</b><br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4197">VIM-4197</a> Fixed Vim features (e.g., <code>f</code>, <code>w</code>, text objects) not working in Java files decompiled from Kotlin class files<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4112">VIM-4112</a> Fixed undo after block-visual edit (<code><C-V>...x</code>, <code><C-V>...c</code>, <code><C-V>...I</code>) leaving stray carets in normal mode<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4176">VIM-4176</a> Fixed race condition in single-line output panel that could cause <code>*</code> search wrapping to behave unreliably<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4175">VIM-4175</a> Fixed search "not found" showing previous "Hit ENTER" text alongside the error - panel is now cleared before displaying errors like "E486: Pattern not found"<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4135">VIM-4135</a> Fixed IdeaVim not loading in Rider<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4135">VIM-4135</a> Fixed IdeaVim not loading in Rider<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed undo in commentary - <code>gcc</code>/<code>gc{motion}</code> changes are now properly grouped as a single undo step<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed undo in commentary - <code>gcc</code>/<code>gc{motion}</code> changes are now properly grouped as a single undo step<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed <code>=</code> (format/auto-indent) action in split mode<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed <code>=</code> (format/auto-indent) action in split mode<br>
|
||||||
@@ -455,6 +492,7 @@ intellijPlatform {
|
|||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4094">VIM-4094</a> Fixed UninitializedPropertyAccessException when loading history<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4094">VIM-4094</a> Fixed UninitializedPropertyAccessException when loading history<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4016">VIM-4016</a> Fixed <code>:edit</code> command when project has no source roots<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4016">VIM-4016</a> Fixed <code>:edit</code> command when project has no source roots<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3948">VIM-3948</a> Improved hint generation visibility checks for better UI component detection<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-3948">VIM-3948</a> Improved hint generation visibility checks for better UI component detection<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4195">VIM-4195</a> Fixed settings not being saved in remote development (split) mode<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3473">VIM-3473</a> Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-3473">VIM-3473</a> Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-2821">VIM-2821</a> Fixed undo grouping when repeating text insertion with <code>.</code> in remote development (split mode)<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-2821">VIM-2821</a> Fixed undo grouping when repeating text insertion with <code>.</code> in remote development (split mode)<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1705">VIM-1705</a> Fixed window-switching commands (e.g., <code><C-w>h</code>) during macro playback<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1705">VIM-1705</a> Fixed window-switching commands (e.g., <code><C-w>h</code>) during macro playback<br>
|
||||||
@@ -470,8 +508,52 @@ intellijPlatform {
|
|||||||
* Fixed <code>IndexOutOfBoundsException</code> when using text objects like <code>a)</code> at end of file<br>
|
* Fixed <code>IndexOutOfBoundsException</code> when using text objects like <code>a)</code> at end of file<br>
|
||||||
* Fixed high CPU usage while showing command line<br>
|
* Fixed high CPU usage while showing command line<br>
|
||||||
* Fixed comparison of String and Number in VimScript expressions<br>
|
* Fixed comparison of String and Number in VimScript expressions<br>
|
||||||
|
* Fixed <code>\/</code>, <code>\?</code>, and <code>\&</code> in Ex command ranges now correctly report E35/E33 errors when no previous search or substitute pattern exists, instead of crashing<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4172">VIM-4172</a> IdeaVim is now disabled in Python Console to prevent key interference<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4113">VIM-4113</a> Fixed Visual mode commands (e.g., <code>:'<,'>sort</code>) failing when run off the Event Dispatch Thread<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-3727">VIM-3727</a> Fixed Enter and arrow keys not working in Python Console in split mode<br>
|
||||||
|
* Fixed NERDTree navigation (<code>j</code>/<code>k</code>/<code>G</code>/<code>gg</code>/<code>p</code>/<code><C-J></code>/<code><C-K></code>) poor performance in split mode - navigation now uses Swing actions directly instead of going through backend RPC<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4180">VIM-4180</a> Fixed ReplaceWithRegister plugin's default <code>gr</code>/<code>grr</code> mappings overriding user-defined key mappings<br>
|
||||||
|
* Fixed <code>IndexOutOfBoundsException</code> when using <code>:command</code> with <code>-nargs</code> option but without a command name<br>
|
||||||
|
* Fixed spurious beep when pressing <code><Esc></code> to cancel register selection in normal mode (after pressing <code>"</code>)<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4202">VIM-4202</a> Fixed <code><S-Tab></code> being intercepted by IdeaVim - users can now remap <code><S-Tab></code> to other IntelliJ actions<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4202">VIM-4202</a> Fixed <code>gcc</code>/<code>gc{motion}</code> commentary leaving editor in incorrect mode in Rider/CLion split mode<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4115">VIM-4115</a> Fixed NullPointerException in <code>CommandKeyConsumer</code> when pressing Esc after disabling and re-enabling IdeaVim with an open command line<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4209">VIM-4209</a> Fixed <code><Esc></code> not exiting insert mode in Rider/CLion when a <code><C-Space></code> completion popup intercepts the key before IdeaVim<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4211">VIM-4211</a> Fixed IdeaVim not working in the Git commit window<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4202">VIM-4202</a> Fixed <code>gcc</code>/<code>gc{motion}</code> commentary not adding space after <code>//</code> prefix in C/C++/C# files in Rider/CLion split mode<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4219">VIM-4219</a> Fixed NullPointerException when IdeaVim is being disabled/unloaded<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4221">VIM-4221</a> Fixed error sound being played on each keypress when <code>incsearch</code> is enabled and the typed pattern is an invalid regex<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4196">VIM-4196</a> Fixed NERDTree file selection not being restored after pressing <code><Esc></code> to cancel a <code>/</code> speed search<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4211">VIM-4211</a> Fixed IdeaVim not working in Git commit window when the Conventional Commits plugin is installed<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4224">VIM-4224</a> Fixed <code>:s</code> <code>e</code> flag now properly suppresses "Pattern not found" errors - e.g., <code>%s/\s\+$//e</code> no longer errors when there is no trailing whitespace<br>
|
||||||
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4226">VIM-4226</a> Fixed race condition crash when the editor is disposed while the ex panel is open<br>
|
||||||
<br>
|
<br>
|
||||||
<b>Merged PRs:</b><br>
|
<b>Merged PRs:</b><br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1747">1747</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: feat(VIM-519): cycle through recent edits with g; and g,<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1745">1745</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: feat(VIM-258): tab command completion<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1744">1744</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4226): check if editor is disposed on focus<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1741">1741</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4224): respect e flag in search patterns<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1740">1740</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: feat(VIM-3975): support vim mode() function<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1739">1739</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4196): restore file selection after esc in nerdtree<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1738">1738</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4211): commit window work with conectional commits plugin<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1730">1730</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: FIX(VIM-4221) Don't make angry sounds on search<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1728">1728</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4202 Add space after c langauges comments<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1727">1727</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: FIX(VIM-4219) check for in VimPLugin is not null<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1720">1720</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix: make ideavim work in commit window<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1717">1717</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix(VIM-4209): handle esc in rider before popup<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1704">1704</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4202 Don't intercept all <S-Tab><br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1703">1703</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4202 Fix state after commentary action<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1700">1700</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4139 Compute nesting depth for fold regions<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1699">1699</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4112 collapse restored carets after undo of block-visual edit<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1696">1696</a> by <a href="https://github.com/citizenmatt">citizenmatt</a>: VIM-4197 Fix missing Vim features in Java files decompiled from Kotlin class files<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1695">1695</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-1693 Implement autocmd<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1690">1690</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Make nerdtree work without calling backend actions<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1688">1688</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4172 Disable ideavim in Python Console<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1687">1687</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Restore old VimPLugin method signatures<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1685">1685</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-3727 Fix Python console Enter and arrow keys in split mode<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1548">1548</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-1158 Add <code>gw</code> to reformat code with preserving the cursor position<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1682">1682</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-268 Complete file names in edit command<br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1632">1632</a> by <a href="https://github.com/chylex">chylex</a>: Fix pumvisible returning opposite result<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1632">1632</a> by <a href="https://github.com/chylex">chylex</a>: Fix pumvisible returning opposite result<br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1615">1615</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix IndexOutOfBoundsException in findBlock when caret is at end of file<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1615">1615</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix IndexOutOfBoundsException in findBlock when caret is at end of file<br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1613">1613</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-3473 Sync ideavim in remdev<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1613">1613</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-3473 Sync ideavim in remdev<br>
|
||||||
@@ -479,19 +561,14 @@ intellijPlatform {
|
|||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1585">1585</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Break in case of maximum recursion depth<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1585">1585</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Break in case of maximum recursion depth<br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1414">1414</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Refactor/functions<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1414">1414</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Refactor/functions<br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1442">1442</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Fix high CPU usage while showing command line<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1442">1442</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Fix high CPU usage while showing command line<br>
|
||||||
|
* <a href="https://github.com/JetBrains/ideavim/pull/1665">1665</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix visual selection commands failing off-EDT due to nested write-in-read action<br>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>
|
<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
ideaVersion {
|
ideaVersion {
|
||||||
// Let the Gradle plugin set the since-build version. It defaults to the version of the IDE we're building against
|
sinceBuild.set("253")
|
||||||
// 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.
|
|
||||||
untilBuild.set(provider { null })
|
untilBuild.set(provider { null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -618,6 +618,34 @@ https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><h2>VimEverywhere: Keyboard-driven IDE navigation outside the editor</h2></summary>
|
||||||
|
|
||||||
|
### Summary:
|
||||||
|
Brings vim-style keyboard navigation to the rest of the IDE. Enabling `VimEverywhere` turns on three
|
||||||
|
behaviors:
|
||||||
|
|
||||||
|
- **Hints overlay.** Press `Ctrl+Shift+\` (`Ctrl+Cmd+\` on macOS) to display hint labels over
|
||||||
|
interactive UI components — buttons, tool window tabs, tree nodes, text fields, scroll panes, and
|
||||||
|
so on. Type the letters next to a target to focus or click it without touching the mouse.
|
||||||
|
- **NERDTree-style mappings everywhere.** NERDTree file-opening mappings (`o`, `t`, `T`, `s`, `i`,
|
||||||
|
`go`, `gs`, `gi`) work in any focused tree, not just the Project tool window.
|
||||||
|
- **Tool window navigation.** Vim-style window-motion keys work inside tool windows, so you can move
|
||||||
|
between split panes without leaving the keyboard.
|
||||||
|
|
||||||
|
### Setup:
|
||||||
|
- Install the [AceJump](https://plugins.jetbrains.com/plugin/7086-acejump/) plugin.
|
||||||
|
- Add the following command to `~/.ideavimrc`: `set VimEverywhere`
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
Press `Ctrl+Shift+\` (`Ctrl+Cmd+\` on macOS) to toggle the hints overlay. Type the letters shown
|
||||||
|
next to a target to activate it, or press `Esc` to dismiss the overlay without activating anything.
|
||||||
|
NERDTree-style and window-nav mappings are active automatically whenever the corresponding
|
||||||
|
component has focus.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><h2>Which-Key: Displays available keybindings in popup</h2></summary>
|
<summary><h2>Which-Key: Displays available keybindings in popup</h2></summary>
|
||||||
|
|
||||||
|
|||||||
137
doc/autocmd.md
Normal file
137
doc/autocmd.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
Autocommands
|
||||||
|
============
|
||||||
|
|
||||||
|
IdeaVim supports Vim's `:autocmd` for running commands on editor events.
|
||||||
|
Handlers are registered from `~/.ideavimrc` or interactively in Command-line mode.
|
||||||
|
Every effort is made to match Vim's behaviour, but some differences are inevitable
|
||||||
|
because the IDE's event model doesn't map 1:1 onto Vim's.
|
||||||
|
|
||||||
|
Syntax
|
||||||
|
------
|
||||||
|
|
||||||
|
```
|
||||||
|
autocmd [group] {event}[,{event}...] {pattern} {command}
|
||||||
|
autocmd!
|
||||||
|
autocmd! {group}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `{event}` — one or more comma-separated event names (see below).
|
||||||
|
- `{pattern}` — file pattern (see "Patterns" below). For `FileType`, the pattern matches the filetype name, not the file
|
||||||
|
path.
|
||||||
|
- `{command}` — any Ex command or Vimscript expression.
|
||||||
|
- `autocmd!` — clears all registered handlers, or all handlers in the given augroup.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
augroup my_group
|
||||||
|
autocmd!
|
||||||
|
autocmd BufWritePre *.py echo "saving python"
|
||||||
|
augroup END
|
||||||
|
```
|
||||||
|
|
||||||
|
Patterns
|
||||||
|
--------
|
||||||
|
|
||||||
|
Autocmd file patterns support the following glob syntax:
|
||||||
|
|
||||||
|
| Pattern | Matches |
|
||||||
|
|-------------|------------------------------------------|
|
||||||
|
| `*` | Any characters except path separators |
|
||||||
|
| `**` | Any characters including path separators |
|
||||||
|
| `?` | Any single non-separator character |
|
||||||
|
| `[abc]` | Any character in the set |
|
||||||
|
| `{foo,bar}` | Either `foo` or `bar` |
|
||||||
|
|
||||||
|
If the pattern contains `/` or `\`, it matches against the full path;
|
||||||
|
otherwise it matches against the filename only.
|
||||||
|
|
||||||
|
`FileType` is special: its pattern matches against the filetype name
|
||||||
|
(e.g. `python`, `java`) rather than the file path.
|
||||||
|
|
||||||
|
Supported events
|
||||||
|
----------------
|
||||||
|
|
||||||
|
### Insert mode
|
||||||
|
|
||||||
|
| Event | Fires when |
|
||||||
|
|---------------|----------------------|
|
||||||
|
| `InsertEnter` | Entering Insert mode |
|
||||||
|
| `InsertLeave` | Leaving Insert mode |
|
||||||
|
|
||||||
|
### Buffers
|
||||||
|
|
||||||
|
| Event | Fires when |
|
||||||
|
|----------------|-------------------------------------------------------------------|
|
||||||
|
| `BufEnter` | A buffer becomes active (every switch) |
|
||||||
|
| `BufLeave` | A buffer stops being active |
|
||||||
|
| `BufRead` | A file is loaded into a buffer for the first time |
|
||||||
|
| `BufReadPost` | Alias of `BufRead` (same event, two names) |
|
||||||
|
| `BufNewFile` | Editing a file that was just created (fires instead of `BufRead`) |
|
||||||
|
| `BufWrite` | Alias of `BufWritePre` |
|
||||||
|
| `BufWritePre` | Before the buffer is written to disk |
|
||||||
|
| `BufWritePost` | After the buffer has been written to disk |
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
| Event | Fires when |
|
||||||
|
|------------|----------------------------------------|
|
||||||
|
| `WinEnter` | A window becomes active (every switch) |
|
||||||
|
| `WinLeave` | A window stops being active |
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| Event | Fires when |
|
||||||
|
|------------|--------------------------------------------------------------------------------------------------|
|
||||||
|
| `FileType` | A buffer's filetype is determined (typically once per file load). Pattern matches filetype name. |
|
||||||
|
|
||||||
|
### Focus
|
||||||
|
|
||||||
|
| Event | Fires when |
|
||||||
|
|---------------|----------------------------|
|
||||||
|
| `FocusGained` | The IDE window gains focus |
|
||||||
|
| `FocusLost` | The IDE window loses focus |
|
||||||
|
|
||||||
|
### Event order
|
||||||
|
|
||||||
|
When opening a file for the first time:
|
||||||
|
|
||||||
|
```
|
||||||
|
BufRead/BufReadPost → FileType → BufEnter
|
||||||
|
```
|
||||||
|
|
||||||
|
When opening a just-created file:
|
||||||
|
|
||||||
|
```
|
||||||
|
BufNewFile → FileType → BufEnter
|
||||||
|
```
|
||||||
|
|
||||||
|
When switching buffers:
|
||||||
|
|
||||||
|
```
|
||||||
|
BufLeave → WinLeave → WinEnter → BufEnter
|
||||||
|
```
|
||||||
|
|
||||||
|
When saving:
|
||||||
|
|
||||||
|
```
|
||||||
|
BufWrite/BufWritePre → (write) → BufWritePost
|
||||||
|
```
|
||||||
|
|
||||||
|
Differences from Vim
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**`FileType` names.** IdeaVim maps IntelliJ's file type name to a Vim-style
|
||||||
|
filetype. For most languages the lowercased IJ name matches Vim's filetype
|
||||||
|
(`Python`→`python`, `JAVA`→`java`). A small override table handles cases where
|
||||||
|
Vim's convention differs: `PLAIN_TEXT`→`text`, `C++`→`cpp`, `C#`→`cs`,
|
||||||
|
`Shell Script`→`sh`, `ObjectiveC`→`objc`, `JavaScript`→`javascript`,
|
||||||
|
`TypeScript`→`typescript`, `Vue.js`→`vue`, `CMakeLists.txt`→`cmake`,
|
||||||
|
`Handlebars/Mustache`→`handlebars`.
|
||||||
|
|
||||||
|
**`BufNewFile` detection.** IdeaVim tracks files created during the session
|
||||||
|
via the VFS. When such a file is opened in an editor, `BufNewFile` fires
|
||||||
|
instead of `BufRead`. Files created by VCS pulls, build tools, or external
|
||||||
|
processes that you later open in an editor will also be treated as new files.
|
||||||
|
|
||||||
|
**`BufWritePre` / `BufWritePost` frequency.** IntelliJ auto-saves on focus
|
||||||
|
loss, tab switch, build, and other events. These autocmds fire more often
|
||||||
|
than Vim's `:w`, so handlers should be idempotent.
|
||||||
@@ -20,7 +20,7 @@ ideaVersion=2026.1
|
|||||||
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
||||||
ideaType=IU
|
ideaType=IU
|
||||||
instrumentPluginCode=true
|
instrumentPluginCode=true
|
||||||
version=SNAPSHOT
|
version=9999.58-chylex
|
||||||
javaVersion=21
|
javaVersion=21
|
||||||
remoteRobotVersion=0.11.23
|
remoteRobotVersion=0.11.23
|
||||||
antlrVersion=4.10.1
|
antlrVersion=4.10.1
|
||||||
@@ -42,7 +42,6 @@ youtrackToken=
|
|||||||
|
|
||||||
# Gradle settings
|
# Gradle settings
|
||||||
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||||
org.gradle.configuration-cache=true
|
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
|
|
||||||
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary
|
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
2
gradlew
vendored
2
gradlew
vendored
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
31
gradlew.bat
vendored
Executable file → Normal file
31
gradlew.bat
vendored
Executable file → Normal file
@@ -23,8 +23,8 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 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
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@@ -65,7 +65,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 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
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
@@ -73,21 +73,10 @@ goto fail
|
|||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
@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
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.changelist
|
||||||
|
|
||||||
|
import com.intellij.openapi.editor.Document
|
||||||
|
import com.intellij.openapi.editor.EditorFactory
|
||||||
|
import com.intellij.openapi.editor.event.DocumentEvent
|
||||||
|
import com.intellij.openapi.editor.event.DocumentListener
|
||||||
|
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||||
|
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.PlaceInfo
|
||||||
|
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.RecentPlacesListener
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.platform.rpc.topics.broadcast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridges IntelliJ's `RecentPlacesListener` into [CHANGE_LIST_REMOTE_TOPIC].
|
||||||
|
*
|
||||||
|
* `PlaceInfo.caretPosition` carries the *post-command* caret (one past `iX<Esc>`,
|
||||||
|
* end of `rA`, etc.) but Vim's `g;` targets where the edit *began*, so we capture
|
||||||
|
* `event.offset` from a `DocumentListener` and prefer it when available.
|
||||||
|
*
|
||||||
|
* Line/col are computed here on the backend (where the document lives) and sent
|
||||||
|
* pre-resolved over the topic; the frontend then has no VirtualFile lookup to
|
||||||
|
* race with editor loading in split mode.
|
||||||
|
*
|
||||||
|
* `recentPlaceRemoved` is intentionally NOT mirrored: IntelliJ's `putLastOrMerge`
|
||||||
|
* fires "remove A, add B" across different lines (`canBeMergedWith(NAVIGATION)`),
|
||||||
|
* which is far more aggressive than Vim's same-line/textwidth merge rule. The
|
||||||
|
* frontend service does its own merging and capping, so platform eviction is moot.
|
||||||
|
*/
|
||||||
|
internal class ChangeListPlacesListener(private val project: Project) : RecentPlacesListener {
|
||||||
|
|
||||||
|
private data class Pending(val document: Document, val offset: Int)
|
||||||
|
|
||||||
|
private val pendingByPath = mutableMapOf<String, Pending>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
EditorFactory.getInstance().eventMulticaster.addDocumentListener(
|
||||||
|
object : DocumentListener {
|
||||||
|
override fun documentChanged(event: DocumentEvent) {
|
||||||
|
val file = FileDocumentManager.getInstance().getFile(event.document) ?: return
|
||||||
|
pendingByPath[file.path] = Pending(event.document, event.offset)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
project,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
override fun recentPlaceAdded(changePlace: PlaceInfo, isChanged: Boolean) {
|
||||||
|
if (!isChanged) return
|
||||||
|
val file = changePlace.file
|
||||||
|
val path = file.path
|
||||||
|
|
||||||
|
val pending = pendingByPath.remove(path)
|
||||||
|
val (document, offset) = pending?.let { it.document to it.offset }
|
||||||
|
?: run {
|
||||||
|
val doc = FileDocumentManager.getInstance().getDocument(file) ?: return
|
||||||
|
val off = changePlace.caretPosition?.startOffset ?: return
|
||||||
|
doc to off
|
||||||
|
}
|
||||||
|
|
||||||
|
val safeOffset = offset.coerceIn(0, document.textLength)
|
||||||
|
val line = document.getLineNumber(safeOffset)
|
||||||
|
val col = safeOffset - document.getLineStartOffset(line)
|
||||||
|
|
||||||
|
CHANGE_LIST_REMOTE_TOPIC.broadcast(
|
||||||
|
project,
|
||||||
|
ChangeListInfo(
|
||||||
|
line = line,
|
||||||
|
col = col,
|
||||||
|
filepath = path,
|
||||||
|
protocol = file.fileSystem.protocol,
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
override fun recentPlaceRemoved(changePlace: PlaceInfo, isChanged: Boolean) {
|
||||||
|
// Intentionally empty -- see class kdoc.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,23 +8,25 @@
|
|||||||
|
|
||||||
package com.maddyhome.idea.vim.group.comment
|
package com.maddyhome.idea.vim.group.comment
|
||||||
|
|
||||||
import com.intellij.openapi.actionSystem.ActionManager
|
import com.intellij.application.options.CodeStyle
|
||||||
import com.intellij.openapi.actionSystem.IdeActions
|
import com.intellij.codeInsight.actions.MultiCaretCodeInsightActionHandler
|
||||||
|
import com.intellij.codeInsight.generation.CommentByBlockCommentHandler
|
||||||
|
import com.intellij.codeInsight.generation.CommentByLineCommentHandler
|
||||||
|
import com.intellij.lang.LanguageCommenters
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.command.CommandProcessor
|
import com.intellij.openapi.command.CommandProcessor
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.impl.EditorId
|
import com.intellij.openapi.editor.impl.EditorId
|
||||||
import com.intellij.openapi.editor.impl.findEditorOrNull
|
import com.intellij.openapi.editor.impl.findEditorOrNull
|
||||||
|
import com.intellij.psi.PsiDocumentManager
|
||||||
|
import com.intellij.psi.PsiFile
|
||||||
import com.maddyhome.idea.vim.group.onEdt
|
import com.maddyhome.idea.vim.group.onEdt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RPC handler for [CommentaryRemoteApi].
|
* Handlers are invoked directly rather than via `ActionManager.tryToExecute` because in
|
||||||
*
|
* Rider / CLion Nova the action dispatch is async — `ActionCallback` signals `done` at
|
||||||
* Sets selection on the backend editor and executes the platform's comment action.
|
* dispatch, not completion — so the action's selection survived `removeSelection()` and
|
||||||
* Because this runs on the backend, [com.intellij.openapi.command.CommandProcessor]
|
* the selection listener dropped IdeaVim into Visual-Line mode.
|
||||||
* groups all document modifications as a single undo step.
|
|
||||||
*
|
|
||||||
* The selection is set on the backend editor only — it doesn't affect the frontend
|
|
||||||
* editor's visual state, and is cleaned up immediately after the action executes.
|
|
||||||
*/
|
*/
|
||||||
internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
||||||
|
|
||||||
@@ -35,40 +37,66 @@ internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
|||||||
val startOffset = document.getLineStartOffset(startLine)
|
val startOffset = document.getLineStartOffset(startLine)
|
||||||
val endOffset = document.getLineEndOffset(endLine)
|
val endOffset = document.getLineEndOffset(endLine)
|
||||||
|
|
||||||
executeCommentAction(editor, startOffset, endOffset, caretOffset, IdeActions.ACTION_COMMENT_LINE)
|
runCommenter(editor, startOffset, endOffset, caretOffset, lineWise = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toggleBlockComment(editorId: EditorId, startOffset: Int, endOffset: Int, caretOffset: Int) =
|
override suspend fun toggleBlockComment(editorId: EditorId, startOffset: Int, endOffset: Int, caretOffset: Int) =
|
||||||
onEdt {
|
onEdt {
|
||||||
val editor = editorId.findEditorOrNull() ?: return@onEdt
|
val editor = editorId.findEditorOrNull() ?: return@onEdt
|
||||||
// Try block comment first, fall back to line comment
|
runCommenter(editor, startOffset, endOffset, caretOffset, lineWise = false)
|
||||||
if (!executeCommentAction(editor, startOffset, endOffset, caretOffset, IdeActions.ACTION_COMMENT_BLOCK)) {
|
|
||||||
executeCommentAction(editor, startOffset, endOffset, caretOffset, IdeActions.ACTION_COMMENT_LINE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun executeCommentAction(
|
private fun runCommenter(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
startOffset: Int,
|
startOffset: Int,
|
||||||
endOffset: Int,
|
endOffset: Int,
|
||||||
caretOffset: Int,
|
caretOffset: Int,
|
||||||
actionId: String,
|
lineWise: Boolean,
|
||||||
): Boolean {
|
) {
|
||||||
var result = false
|
val project = editor.project ?: return
|
||||||
// Wrap selection + action + caret reset + cleanup in a single command so everything
|
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
|
||||||
// is a single undo step. In remdev, undo restores pre-command editor state — if
|
|
||||||
// selection is set before the command, undo would restore it. The nested tryToExecute
|
val invokeHandler = {
|
||||||
// command merges into this outer command.
|
CommandProcessor.getInstance().executeCommand(project, {
|
||||||
CommandProcessor.getInstance().executeCommand(editor.project, {
|
ApplicationManager.getApplication().runWriteAction {
|
||||||
editor.selectionModel.setSelection(startOffset, endOffset)
|
val caret = editor.caretModel.primaryCaret
|
||||||
val action = ActionManager.getInstance().getAction(actionId)
|
caret.setSelection(startOffset, endOffset)
|
||||||
result = ActionManager.getInstance().tryToExecute(action, null, editor.contentComponent, "IdeaVim", true)
|
try {
|
||||||
.let { it.waitFor(5_000); it.isDone }
|
val handler = pickHandler(psiFile, lineWise)
|
||||||
editor.selectionModel.removeSelection()
|
handler.invoke(project, editor, caret, psiFile)
|
||||||
if (caretOffset >= 0) {
|
handler.postInvoke()
|
||||||
editor.caretModel.moveToOffset(caretOffset)
|
} finally {
|
||||||
|
caret.removeSelection()
|
||||||
|
if (caretOffset >= 0) {
|
||||||
|
caret.moveToOffset(caretOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "Commentary", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normally comment action goes through rider backend comment action running on .net nto jvm so we cannot call it directly.
|
||||||
|
// But we still want to apply space after comment as it's default bahavior there so we overrite this flag for intelij comment handler
|
||||||
|
if (isCFamily(psiFile)) {
|
||||||
|
val baseSettings = CodeStyle.getSettings(psiFile)
|
||||||
|
CodeStyle.runWithLocalSettings(project, baseSettings) { localSettings ->
|
||||||
|
localSettings.getCommonSettings(psiFile.language).LINE_COMMENT_ADD_SPACE = true
|
||||||
|
invokeHandler()
|
||||||
}
|
}
|
||||||
}, "Commentary", null)
|
} else {
|
||||||
return result
|
invokeHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCFamily(psiFile: PsiFile): Boolean {
|
||||||
|
val fileTypeName = psiFile.fileType.name
|
||||||
|
return fileTypeName == "C++" || fileTypeName == "C#" || fileTypeName == "ObjectiveC"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pickHandler(psiFile: PsiFile, lineWise: Boolean): MultiCaretCodeInsightActionHandler {
|
||||||
|
if (lineWise) return CommentByLineCommentHandler()
|
||||||
|
val commenter = LanguageCommenters.INSTANCE.forLanguage(psiFile.language)
|
||||||
|
val hasBlock = commenter?.blockCommentPrefix != null && commenter.blockCommentSuffix != null
|
||||||
|
return if (hasBlock) CommentByBlockCommentHandler() else CommentByLineCommentHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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.file
|
||||||
|
|
||||||
|
import com.intellij.openapi.vfs.LocalFileSystem
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a user-typed path prefix into a list of matching file/directory names
|
||||||
|
* for command-line completion. Directories are suffixed with `/`.
|
||||||
|
*/
|
||||||
|
internal object FileCompletionHelper {
|
||||||
|
|
||||||
|
fun listMatchingFiles(pathPrefix: String, basePath: String?): List<String> {
|
||||||
|
val (parentDir, namePrefix) = resolveParentAndPrefix(pathPrefix, basePath)
|
||||||
|
if (parentDir == null || !parentDir.isDirectory) return emptyList()
|
||||||
|
|
||||||
|
return filterAndFormat(parentDir, namePrefix, pathPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterAndFormat(parentDir: VirtualFile, namePrefix: String, pathPrefix: String): List<String> {
|
||||||
|
val dirPrefix = pathPrefix.substringBeforeLast('/', "")
|
||||||
|
|
||||||
|
return parentDir.children
|
||||||
|
.filter { it.name.startsWith(namePrefix, ignoreCase = true) }
|
||||||
|
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
.map { formatChild(it, dirPrefix) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatChild(child: VirtualFile, dirPrefix: String): String {
|
||||||
|
val name = if (child.isDirectory) child.name + "/" else child.name
|
||||||
|
if (dirPrefix.isEmpty()) return name
|
||||||
|
return "$dirPrefix/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveParentAndPrefix(pathPrefix: String, basePath: String?): Pair<VirtualFile?, String> {
|
||||||
|
if (pathPrefix.isEmpty()) return resolveProjectRoot(basePath)
|
||||||
|
if (pathPrefix.startsWith("~/") || pathPrefix.startsWith("~\\")) return resolveHomePath(pathPrefix)
|
||||||
|
if (Path(pathPrefix).isAbsolute) return resolveAbsolutePath(pathPrefix)
|
||||||
|
return resolveRelativePath(pathPrefix, basePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveProjectRoot(basePath: String?): Pair<VirtualFile?, String> {
|
||||||
|
val dir = basePath?.let { LocalFileSystem.getInstance().findFileByNioFile(Path(it)) }
|
||||||
|
return dir to ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveHomePath(pathPrefix: String): Pair<VirtualFile?, String> {
|
||||||
|
val home = System.getProperty("user.home")
|
||||||
|
val relativePath = pathPrefix.substring(2)
|
||||||
|
return splitDirAndPrefix(relativePath) { dirPath ->
|
||||||
|
LocalFileSystem.getInstance().findFileByNioFile(Path(home, dirPath))
|
||||||
|
} ?: (LocalFileSystem.getInstance().findFileByNioFile(Path(home)) to relativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveAbsolutePath(pathPrefix: String): Pair<VirtualFile?, String> {
|
||||||
|
return splitDirAndPrefix(pathPrefix) { dirPath ->
|
||||||
|
LocalFileSystem.getInstance().findFileByNioFile(Path(dirPath.ifEmpty { "/" }))
|
||||||
|
} ?: (null to "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveRelativePath(pathPrefix: String, basePath: String?): Pair<VirtualFile?, String> {
|
||||||
|
val baseDir = basePath?.let { LocalFileSystem.getInstance().findFileByNioFile(Path(it)) }
|
||||||
|
return splitDirAndPrefix(pathPrefix) { dirPath ->
|
||||||
|
baseDir?.findFileByRelativePath(dirPath)
|
||||||
|
} ?: (baseDir to pathPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun splitDirAndPrefix(
|
||||||
|
path: String,
|
||||||
|
resolveDir: (String) -> VirtualFile?,
|
||||||
|
): Pair<VirtualFile?, String>? {
|
||||||
|
val lastSlash = path.lastIndexOf('/')
|
||||||
|
if (lastSlash < 0) return null
|
||||||
|
|
||||||
|
val dirPath = path.substring(0, lastSlash)
|
||||||
|
val prefix = path.substring(lastSlash + 1)
|
||||||
|
return resolveDir(dirPath) to prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,11 @@ internal class FileRemoteApiImpl : FileRemoteApi {
|
|||||||
if (first is TextEditor) !first.editor.isDisposed else false
|
if (first is TextEditor) !first.editor.isDisposed else false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun listFilesForCompletion(pathPrefix: String, projectId: ProjectId?): List<String> = readAction {
|
||||||
|
val basePath = projectId?.findProjectOrNull()?.basePath
|
||||||
|
FileCompletionHelper.listMatchingFiles(pathPrefix, basePath)
|
||||||
|
}
|
||||||
|
|
||||||
// ======================== Private helpers ========================
|
// ======================== Private helpers ========================
|
||||||
|
|
||||||
private fun findFile(filename: String, project: Project): VirtualFile? {
|
private fun findFile(filename: String, project: Project): VirtualFile? {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<projectListeners>
|
<projectListeners>
|
||||||
<listener class="com.maddyhome.idea.vim.group.jump.JumpsListener"
|
<listener class="com.maddyhome.idea.vim.group.jump.JumpsListener"
|
||||||
topic="com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl$RecentPlacesListener"/>
|
topic="com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl$RecentPlacesListener"/>
|
||||||
|
<listener class="com.maddyhome.idea.vim.group.changelist.ChangeListPlacesListener"
|
||||||
|
topic="com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl$RecentPlacesListener"/>
|
||||||
</projectListeners>
|
</projectListeners>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
|
|||||||
@@ -10,12 +10,6 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<plugin id="org.jetbrains.plugins.clion.radler"/>
|
<plugin id="org.jetbrains.plugins.clion.radler"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
|
||||||
<editorActionHandler action="EditorEscape"
|
|
||||||
implementationClass="com.maddyhome.idea.vim.handler.VimEscForRiderHandler"
|
|
||||||
id="ideavim-clion-nova-esc"
|
|
||||||
order="first, before idea.only.escape"/>
|
|
||||||
</extensions>
|
|
||||||
<extensions defaultExtensionNs="IdeaVIM">
|
<extensions defaultExtensionNs="IdeaVIM">
|
||||||
<clionNovaProvider implementation="com.maddyhome.idea.vim.ide.ClionNovaProviderImpl"/>
|
<clionNovaProvider implementation="com.maddyhome.idea.vim.ide.ClionNovaProviderImpl"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.changelist
|
||||||
|
|
||||||
|
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend-to-frontend change-list event. Line/col are pre-computed on the
|
||||||
|
* backend so the frontend doesn't need a VirtualFile lookup -- which can race
|
||||||
|
* with editor loading in split mode (mirrors the `JumpInfo` pattern).
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ChangeListInfo(
|
||||||
|
val line: Int,
|
||||||
|
val col: Int,
|
||||||
|
val filepath: String,
|
||||||
|
val protocol: String,
|
||||||
|
val timestamp: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
val CHANGE_LIST_REMOTE_TOPIC: ProjectRemoteTopic<ChangeListInfo> =
|
||||||
|
ProjectRemoteTopic("ideavim.changelist", ChangeListInfo.serializer())
|
||||||
@@ -44,6 +44,7 @@ interface FileRemoteApi : RemoteApi<Unit> {
|
|||||||
suspend fun selectNextFile(count: Int, projectId: ProjectId?)
|
suspend fun selectNextFile(count: Int, projectId: ProjectId?)
|
||||||
suspend fun buildFileInfoMessage(editorId: EditorId, fullPath: Boolean): String?
|
suspend fun buildFileInfoMessage(editorId: EditorId, fullPath: Boolean): String?
|
||||||
suspend fun selectEditor(projectId: ProjectId, documentPath: String, protocol: String): Boolean
|
suspend fun selectEditor(projectId: ProjectId, documentPath: String, protocol: String): Boolean
|
||||||
|
suspend fun listFilesForCompletion(pathPrefix: String, projectId: ProjectId?): List<String>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -36,10 +36,8 @@
|
|||||||
topic="com.intellij.ide.ui.LafManagerListener"/>
|
topic="com.intellij.ide.ui.LafManagerListener"/>
|
||||||
<listener class="com.maddyhome.idea.vim.extension.highlightedyank.HighlightColorResetter"
|
<listener class="com.maddyhome.idea.vim.extension.highlightedyank.HighlightColorResetter"
|
||||||
topic="com.intellij.ide.ui.LafManagerListener"/>
|
topic="com.intellij.ide.ui.LafManagerListener"/>
|
||||||
<listener class="com.maddyhome.idea.vim.handler.IdeaVimKeymapChangedListener"
|
<listener class="com.maddyhome.idea.vim.listener.VimAppActivationListener"
|
||||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
|
||||||
<listener class="com.maddyhome.idea.vim.handler.IdeaVimCorrectorKeymapChangedListener"
|
|
||||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
|
||||||
</applicationListeners>
|
</applicationListeners>
|
||||||
|
|
||||||
<projectListeners>
|
<projectListeners>
|
||||||
@@ -169,9 +167,18 @@
|
|||||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.process.IjProcessGroup"
|
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.process.IjProcessGroup"
|
||||||
serviceInterface="com.maddyhome.idea.vim.api.VimProcessGroup"/>
|
serviceInterface="com.maddyhome.idea.vim.api.VimProcessGroup"/>
|
||||||
|
|
||||||
|
<applicationService serviceImplementation="com.maddyhome.idea.vim.autocmd.AutoCmdImpl"
|
||||||
|
serviceInterface="com.maddyhome.idea.vim.api.AutoCmdService"/>
|
||||||
|
|
||||||
<platform.rpc.projectRemoteTopicListener
|
<platform.rpc.projectRemoteTopicListener
|
||||||
implementation="com.maddyhome.idea.vim.group.JumpRemoteTopicListener"/>
|
implementation="com.maddyhome.idea.vim.group.JumpRemoteTopicListener"/>
|
||||||
|
|
||||||
|
<!-- Frontend change-list service (g; / g,) + topic listener that mirrors
|
||||||
|
backend RecentPlacesListener events into it. -->
|
||||||
|
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.changelist.ChangeListService"/>
|
||||||
|
<platform.rpc.projectRemoteTopicListener
|
||||||
|
implementation="com.maddyhome.idea.vim.group.changelist.ChangeListRemoteTopicListener"/>
|
||||||
|
|
||||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.IjFileGroup"
|
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.IjFileGroup"
|
||||||
serviceInterface="com.maddyhome.idea.vim.api.VimFile"/>
|
serviceInterface="com.maddyhome.idea.vim.api.VimFile"/>
|
||||||
|
|
||||||
@@ -220,11 +227,6 @@
|
|||||||
implementation="com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetFactory"
|
implementation="com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetFactory"
|
||||||
order="first, after IdeaVimShowCmd"/>
|
order="first, after IdeaVimShowCmd"/>
|
||||||
|
|
||||||
<!-- Editor-specific startup activities -->
|
|
||||||
<postStartupActivity implementation="com.maddyhome.idea.vim.handler.EditorHandlersChainLogger"/>
|
|
||||||
<postStartupActivity implementation="com.maddyhome.idea.vim.handler.KeymapChecker"/>
|
|
||||||
<postStartupActivity implementation="com.maddyhome.idea.vim.handler.CopilotKeymapCorrector"/>
|
|
||||||
|
|
||||||
<editorFloatingToolbarProvider implementation="com.maddyhome.idea.vim.ui.ReloadFloatingToolbar"/>
|
<editorFloatingToolbarProvider implementation="com.maddyhome.idea.vim.ui.ReloadFloatingToolbar"/>
|
||||||
|
|
||||||
<actionPromoter implementation="com.maddyhome.idea.vim.key.VimActionsPromoter" order="last"/>
|
<actionPromoter implementation="com.maddyhome.idea.vim.key.VimActionsPromoter" order="last"/>
|
||||||
@@ -244,35 +246,6 @@
|
|||||||
<statistics.applicationUsagesCollector implementation="com.maddyhome.idea.vim.statistic.WidgetState"/>
|
<statistics.applicationUsagesCollector implementation="com.maddyhome.idea.vim.statistic.WidgetState"/>
|
||||||
<statistics.counterUsagesCollector implementationClass="com.maddyhome.idea.vim.statistic.ActionTracker"/>
|
<statistics.counterUsagesCollector implementationClass="com.maddyhome.idea.vim.statistic.ActionTracker"/>
|
||||||
|
|
||||||
<!-- Editor action handlers -->
|
|
||||||
<!-- Do not care about red handlers in order. They are necessary for proper ordering, and they'll be resolved when needed -->
|
|
||||||
<editorActionHandler action="EditorEnter" implementationClass="com.maddyhome.idea.vim.handler.VimEnterHandler"
|
|
||||||
id="ideavim-enter"
|
|
||||||
order="before editorEnter, before inline.completion.enter, before rd.client.editor.enter, after smart-step-into-enter, after AceHandlerEnter, after jupyterCommandModeEnterKeyHandler, after swift.placeholder.enter"/>
|
|
||||||
<editorActionHandler action="EditorEnter"
|
|
||||||
implementationClass="com.maddyhome.idea.vim.handler.CaretShapeEnterEditorHandler"
|
|
||||||
id="ideavim-enter-shape"
|
|
||||||
order="before jupyterCommandModeEnterKeyHandler"/>
|
|
||||||
|
|
||||||
<!-- "first" is not defined for this handler as it leads to "unsatisfied ordering exception". Not sure exectly why, but it appears in tests-->
|
|
||||||
<editorActionHandler action="EditorEscape" implementationClass="com.maddyhome.idea.vim.handler.VimEscHandler"
|
|
||||||
id="ideavim-esc"
|
|
||||||
order="after smart-step-into-escape, after AceHandlerEscape, before jupyterCommandModeEscKeyHandler, before templateEscape, before backend.escape"/>
|
|
||||||
<editorActionHandler action="EditorEscape" implementationClass="com.maddyhome.idea.vim.handler.VimEscLoggerHandler"
|
|
||||||
id="ideavim-esc-logger"
|
|
||||||
order="first"/>
|
|
||||||
<editorActionHandler action="EditorEnter" implementationClass="com.maddyhome.idea.vim.handler.VimEnterLoggerHandler"
|
|
||||||
id="ideavim-enter-logger"
|
|
||||||
order="first"/>
|
|
||||||
<editorActionHandler action="EditorStartNewLine"
|
|
||||||
implementationClass="com.maddyhome.idea.vim.handler.StartNewLineDetector"
|
|
||||||
id="ideavim-start-new-line-detector"
|
|
||||||
order="first"/>
|
|
||||||
<editorActionHandler action="EditorStartNewLineBefore"
|
|
||||||
implementationClass="com.maddyhome.idea.vim.handler.StartNewLineBeforeCurrentDetector"
|
|
||||||
id="ideavim-start-new-line-before-current-detector"
|
|
||||||
order="first"/>
|
|
||||||
|
|
||||||
<editorFactoryDocumentListener
|
<editorFactoryDocumentListener
|
||||||
implementation="com.maddyhome.idea.vim.listener.VimListenerManager$VimDocumentListener"/>
|
implementation="com.maddyhome.idea.vim.listener.VimListenerManager$VimDocumentListener"/>
|
||||||
|
|
||||||
@@ -311,6 +284,7 @@
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
|
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
|
||||||
|
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
|
||||||
</actions>
|
</actions>
|
||||||
|
|
||||||
<!-- Frontend vim extensions (editor/text manipulation, no PSI/file-system dependency) -->
|
<!-- Frontend vim extensions (editor/text manipulation, no PSI/file-system dependency) -->
|
||||||
@@ -458,7 +432,8 @@
|
|||||||
|
|
||||||
<actions>
|
<actions>
|
||||||
<action class="com.maddyhome.idea.vim.extension.hints.ToggleHintsAction" text="Toggle Hints">
|
<action class="com.maddyhome.idea.vim.extension.hints.ToggleHintsAction" text="Toggle Hints">
|
||||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl BACK_SLASH"/>
|
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift BACK_SLASH"/>
|
||||||
|
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl meta BACK_SLASH"/>
|
||||||
</action>
|
</action>
|
||||||
</actions>
|
</actions>
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
|
|||||||
@@ -14,12 +14,6 @@
|
|||||||
<listener class="com.maddyhome.idea.vim.listener.RiderActionListener"
|
<listener class="com.maddyhome.idea.vim.listener.RiderActionListener"
|
||||||
topic="com.intellij.openapi.actionSystem.ex.AnActionListener"/>
|
topic="com.intellij.openapi.actionSystem.ex.AnActionListener"/>
|
||||||
</projectListeners>
|
</projectListeners>
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
|
||||||
<editorActionHandler action="EditorEscape"
|
|
||||||
implementationClass="com.maddyhome.idea.vim.handler.VimEscForRiderHandler"
|
|
||||||
id="ideavim-rider-esc"
|
|
||||||
order="first, before idea.only.escape"/>
|
|
||||||
</extensions>
|
|
||||||
<extensions defaultExtensionNs="IdeaVIM">
|
<extensions defaultExtensionNs="IdeaVIM">
|
||||||
<riderProvider implementation="com.maddyhome.idea.vim.ide.RiderProviderImpl"/>
|
<riderProvider implementation="com.maddyhome.idea.vim.ide.RiderProviderImpl"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const knownPlugins = new Set([
|
|||||||
"com.github.pooryam92.vimcoach", // https://plugins.jetbrains.com/plugin/30148-vim-coach
|
"com.github.pooryam92.vimcoach", // https://plugins.jetbrains.com/plugin/30148-vim-coach
|
||||||
"lazyideavim.whichkeylazy", // https://plugins.jetbrains.com/plugin/30446-which-key-lazy
|
"lazyideavim.whichkeylazy", // https://plugins.jetbrains.com/plugin/30446-which-key-lazy
|
||||||
"com.github.vimkeysuggest", // https://plugins.jetbrains.com/plugin/30486-vimkeysuggest
|
"com.github.vimkeysuggest", // https://plugins.jetbrains.com/plugin/30486-vimkeysuggest
|
||||||
|
"dev.ckob.lazygit", // https://plugins.jetbrains.com/plugin/30919-lazygit
|
||||||
]);
|
]);
|
||||||
|
|
||||||
async function getPluginLinkByXmlId(xmlId: string): Promise<string | null> {
|
async function getPluginLinkByXmlId(xmlId: string): Promise<string | null> {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simple test script to verify TeamCity can run TypeScript scripts.
|
|
||||||
* Run with: npx tsx src/teamcityTest.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
console.log("=== TeamCity TypeScript Test Script ===");
|
|
||||||
console.log(`Node version: ${process.version}`);
|
|
||||||
console.log(`Platform: ${process.platform}`);
|
|
||||||
console.log(`Current directory: ${process.cwd()}`);
|
|
||||||
console.log(`Script arguments: ${process.argv.slice(2).join(", ") || "(none)"}`);
|
|
||||||
|
|
||||||
// Test that we can import modules
|
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
||||||
console.log(`Package name: ${pkg.name}`);
|
|
||||||
console.log(`Package version: ${pkg.version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Demonstrate TeamCity service messages (for build status reporting)
|
|
||||||
// See: https://www.jetbrains.com/help/teamcity/service-messages.html
|
|
||||||
console.log("");
|
|
||||||
console.log("##teamcity[message text='TypeScript script executed successfully' status='NORMAL']");
|
|
||||||
|
|
||||||
// Exit with success
|
|
||||||
console.log("");
|
|
||||||
console.log("✓ Test completed successfully!");
|
|
||||||
process.exit(0);
|
|
||||||
@@ -21,16 +21,16 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.3.10")
|
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.3.21")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
|
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-core:3.4.2")
|
implementation("io.ktor:ktor-client-core:3.4.3")
|
||||||
implementation("io.ktor:ktor-client-cio:3.4.2")
|
implementation("io.ktor:ktor-client-cio:3.4.3")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:3.4.2")
|
implementation("io.ktor:ktor-client-content-negotiation:3.4.3")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
|
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.3")
|
||||||
implementation("io.ktor:ktor-client-auth:3.4.2")
|
implementation("io.ktor:ktor-client-auth:3.4.3")
|
||||||
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
|
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
|
||||||
|
|
||||||
// This is needed for jgit to connect to ssh
|
// This is needed for jgit to connect to ssh
|
||||||
|
|||||||
@@ -106,9 +106,13 @@ internal class IjVimPluginActivator : VimPluginActivator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use getServiceIfCreated to avoid creating the service during the dispose (this is prohibited by the platform)
|
// Use getServiceIfCreated to avoid creating the service during the dispose (this is prohibited by the platform)
|
||||||
ApplicationManager.getApplication()
|
val commandLineService = ApplicationManager.getApplication()
|
||||||
.getServiceIfCreated(com.maddyhome.idea.vim.api.VimCommandLineService::class.java)
|
.getServiceIfCreated(com.maddyhome.idea.vim.api.VimCommandLineService::class.java)
|
||||||
?.fullReset()
|
// VIM-4115: close() clears editor mode, KeyHandlerState.commandLineCommandBuilder, and the panel
|
||||||
|
// together. fullReset() alone only deactivates the panel; the KeyHandler singleton retains the
|
||||||
|
// stale CMD_LINE builder across disable/enable and NPEs on the next Esc.
|
||||||
|
commandLineService?.getActiveCommandLine()?.close(refocusOwningEditor = true, resetCaret = false)
|
||||||
|
commandLineService?.fullReset()
|
||||||
|
|
||||||
// Unregister vim actions in command mode
|
// Unregister vim actions in command mode
|
||||||
RegisterActions.unregisterActions()
|
RegisterActions.unregisterActions()
|
||||||
|
|||||||
@@ -22,13 +22,11 @@ import com.intellij.openapi.util.Disposer;
|
|||||||
import com.maddyhome.idea.vim.api.*;
|
import com.maddyhome.idea.vim.api.*;
|
||||||
import com.maddyhome.idea.vim.config.VimState;
|
import com.maddyhome.idea.vim.config.VimState;
|
||||||
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator;
|
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator;
|
||||||
import com.maddyhome.idea.vim.group.ChangeGroup;
|
import com.maddyhome.idea.vim.group.*;
|
||||||
import com.maddyhome.idea.vim.group.KeyGroup;
|
import com.maddyhome.idea.vim.group.visual.VisualMotionGroup;
|
||||||
import com.maddyhome.idea.vim.group.VimNotifications;
|
|
||||||
import com.maddyhome.idea.vim.group.VimWindowGroup;
|
|
||||||
import com.maddyhome.idea.vim.history.VimHistory;
|
import com.maddyhome.idea.vim.history.VimHistory;
|
||||||
import com.maddyhome.idea.vim.macro.VimMacro;
|
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimInjectorKt;
|
import com.maddyhome.idea.vim.newapi.IjVimInjectorKt;
|
||||||
|
import com.maddyhome.idea.vim.newapi.IjVimSearchGroup;
|
||||||
import com.maddyhome.idea.vim.newapi.VimLegacyStateLoader;
|
import com.maddyhome.idea.vim.newapi.VimLegacyStateLoader;
|
||||||
import com.maddyhome.idea.vim.newapi.VimSearchGroupLegacyLoader;
|
import com.maddyhome.idea.vim.newapi.VimSearchGroupLegacyLoader;
|
||||||
import com.maddyhome.idea.vim.put.VimPut;
|
import com.maddyhome.idea.vim.put.VimPut;
|
||||||
@@ -48,7 +46,7 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
* This is an application level plugin meaning that all open projects will share a common instance of the plugin.
|
* This is an application level plugin meaning that all open projects will share a common instance of the plugin.
|
||||||
* Registers and marks are shared across open projects so you can copy and paste between files of different projects.
|
* Registers and marks are shared across open projects so you can copy and paste between files of different projects.
|
||||||
*/
|
*/
|
||||||
@State(name = "VimSettings", storages = {@Storage("$APP_CONFIG$/vim_settings.xml")})
|
@State(name = "VimSettings", storages = {@Storage("vim_settings.xml")})
|
||||||
public class VimPlugin implements PersistentStateComponent<Element>, Disposable {
|
public class VimPlugin implements PersistentStateComponent<Element>, Disposable {
|
||||||
|
|
||||||
public static final int STATE_VERSION = 7;
|
public static final int STATE_VERSION = 7;
|
||||||
@@ -87,49 +85,48 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static @NotNull VimMotionGroup getMotion() {
|
public static @NotNull MotionGroup getMotion() {
|
||||||
return VimInjectorKt.getInjector().getMotion();
|
return ((MotionGroup)VimInjectorKt.getInjector().getMotion());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull ChangeGroup getChange() {
|
public static @NotNull ChangeGroup getChange() {
|
||||||
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup());
|
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimCommandGroup getCommand() {
|
public static @NotNull CommandGroup getCommand() {
|
||||||
return VimInjectorKt.getInjector().getCommandGroup();
|
return ((CommandGroup)VimInjectorKt.getInjector().getCommandGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimRegisterGroup getRegister() {
|
public static @NotNull RegisterGroup getRegister() {
|
||||||
return VimInjectorKt.getInjector().getRegisterGroup();
|
return ((RegisterGroup)VimInjectorKt.getInjector().getRegisterGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimFile getFile() {
|
public static @NotNull VimFile getFile() {
|
||||||
return VimInjectorKt.getInjector().getFile();
|
return VimInjectorKt.getInjector().getFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimSearchGroup getSearch() {
|
public static @NotNull IjVimSearchGroup getSearch() {
|
||||||
return VimInjectorKt.getInjector().getSearchGroup();
|
return ((IjVimSearchGroup)VimInjectorKt.getInjector().getSearchGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable VimSearchGroup getSearchIfCreated() {
|
public static @Nullable IjVimSearchGroup getSearchIfCreated() {
|
||||||
VimSearchGroup searchGroup = ApplicationManager.getApplication().getServiceIfCreated(VimSearchGroup.class);
|
return ApplicationManager.getApplication().getServiceIfCreated(IjVimSearchGroup.class);
|
||||||
return searchGroup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimProcessGroup getProcess() {
|
public static @NotNull VimProcessGroup getProcess() {
|
||||||
return VimInjectorKt.getInjector().getProcessGroup();
|
return VimInjectorKt.getInjector().getProcessGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimMacro getMacro() {
|
public static @NotNull MacroGroup getMacro() {
|
||||||
return VimInjectorKt.getInjector().getMacro();
|
return ((MacroGroup)VimInjectorKt.getInjector().getMacro());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimDigraphGroup getDigraph() {
|
public static @NotNull VimDigraphGroup getDigraph() {
|
||||||
return VimInjectorKt.getInjector().getDigraphGroup();
|
return VimInjectorKt.getInjector().getDigraphGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimHistory getHistory() {
|
public static @NotNull HistoryGroup getHistory() {
|
||||||
return VimInjectorKt.getInjector().getHistoryGroup();
|
return ((HistoryGroup)VimInjectorKt.getInjector().getHistoryGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull KeyGroup getKey() {
|
public static @NotNull KeyGroup getKey() {
|
||||||
@@ -140,20 +137,20 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
|||||||
return ApplicationManager.getApplication().getServiceIfCreated(KeyGroup.class);
|
return ApplicationManager.getApplication().getServiceIfCreated(KeyGroup.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimWindowGroup getWindow() {
|
public static @NotNull WindowGroup getWindow() {
|
||||||
return VimInjectorKt.getInjector().getWindow();
|
return ((WindowGroup)VimInjectorKt.getInjector().getWindow());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimEditorGroup getEditor() {
|
public static @NotNull EditorGroup getEditor() {
|
||||||
return VimInjectorKt.getInjector().getEditorGroup();
|
return ((EditorGroup)VimInjectorKt.getInjector().getEditorGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable VimEditorGroup getEditorIfCreated() {
|
public static @Nullable EditorGroup getEditorIfCreated() {
|
||||||
return ApplicationManager.getApplication().getServiceIfCreated(VimEditorGroup.class);
|
return ApplicationManager.getApplication().getServiceIfCreated(EditorGroup.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimVisualMotionGroup getVisualMotion() {
|
public static @NotNull VisualMotionGroup getVisualMotion() {
|
||||||
return VimInjectorKt.getInjector().getVisualMotionGroup();
|
return ((VisualMotionGroup)VimInjectorKt.getInjector().getVisualMotionGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull YankGroupBase getYank() {
|
public static @NotNull YankGroupBase getYank() {
|
||||||
@@ -186,7 +183,8 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEnabled() {
|
public static boolean isEnabled() {
|
||||||
return getInstance().enabled;
|
final VimPlugin instance = ApplicationManager.getApplication().getService(VimPlugin.class);
|
||||||
|
return instance != null && instance.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setEnabled(final boolean enabled) {
|
public static void setEnabled(final boolean enabled) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.intellij.openapi.editor.impl.EditorComponentImpl
|
|||||||
import com.intellij.openapi.progress.ProcessCanceledException
|
import com.intellij.openapi.progress.ProcessCanceledException
|
||||||
import com.intellij.openapi.project.DumbAware
|
import com.intellij.openapi.project.DumbAware
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
|
import com.intellij.openapi.util.SystemInfoRt
|
||||||
import com.intellij.openapi.util.registry.Registry
|
import com.intellij.openapi.util.registry.Registry
|
||||||
import com.intellij.ui.KeyStrokeAdapter
|
import com.intellij.ui.KeyStrokeAdapter
|
||||||
import com.maddyhome.idea.vim.KeyHandler
|
import com.maddyhome.idea.vim.KeyHandler
|
||||||
@@ -31,8 +32,6 @@ import com.maddyhome.idea.vim.api.globalOptions
|
|||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
import com.maddyhome.idea.vim.group.IjOptionConstants
|
||||||
import com.maddyhome.idea.vim.group.IjOptions
|
import com.maddyhome.idea.vim.group.IjOptions
|
||||||
import com.maddyhome.idea.vim.handler.enableOctopus
|
|
||||||
import com.maddyhome.idea.vim.handler.isOctopusEnabled
|
|
||||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
import com.maddyhome.idea.vim.helper.EditorHelper
|
||||||
import com.maddyhome.idea.vim.helper.HandlerInjector
|
import com.maddyhome.idea.vim.helper.HandlerInjector
|
||||||
import com.maddyhome.idea.vim.helper.inNormalMode
|
import com.maddyhome.idea.vim.helper.inNormalMode
|
||||||
@@ -91,8 +90,8 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
// Control-flow exceptions (like ProcessCanceledException) should never be logged and should be rethrown
|
// Control-flow exceptions (like ProcessCanceledException) should never be logged and should be rethrown
|
||||||
// See {@link com.intellij.openapi.diagnostic.Logger.checkException}
|
// See {@link com.intellij.openapi.diagnostic.Logger.checkException}
|
||||||
throw e
|
throw e
|
||||||
} catch (throwable: Throwable) {
|
} catch (e: Exception) {
|
||||||
LOG.error(throwable)
|
LOG.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,15 +117,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
if (VimPlugin.isNotEnabled()) return ActionEnableStatus.no("IdeaVim is disabled", LogLevel.DEBUG)
|
if (VimPlugin.isNotEnabled()) return ActionEnableStatus.no("IdeaVim is disabled", LogLevel.DEBUG)
|
||||||
val editor = getEditor(e) ?: return ActionEnableStatus.no("Can't get Editor", LogLevel.DEBUG)
|
val editor = getEditor(e) ?: return ActionEnableStatus.no("Can't get Editor", LogLevel.DEBUG)
|
||||||
|
|
||||||
if (enableOctopus) {
|
|
||||||
if (isOctopusEnabled(keyStroke, editor)) {
|
|
||||||
return ActionEnableStatus.no(
|
|
||||||
"Processing VimShortcutKeyAction for the key that is used in the octopus handler",
|
|
||||||
LogLevel.ERROR
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.dataContext.isNotSupportedContextComponent && Registry.`is`("ideavim.only.in.editor.component")) {
|
if (e.dataContext.isNotSupportedContextComponent && Registry.`is`("ideavim.only.in.editor.component")) {
|
||||||
// Note: Currently, IdeaVim works ONLY in the editor & ExTextField component. However, the presence of the
|
// Note: Currently, IdeaVim works ONLY in the editor & ExTextField component. However, the presence of the
|
||||||
// PlatformDataKeys.EDITOR in the data context does not mean that the current focused component is editor.
|
// PlatformDataKeys.EDITOR in the data context does not mean that the current focused component is editor.
|
||||||
@@ -226,8 +216,9 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
|
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
|
||||||
val strokeCache = keyStrokeCache
|
val strokeCache = keyStrokeCache
|
||||||
if (defaultKeyStroke != null) {
|
if (defaultKeyStroke != null) {
|
||||||
keyStrokeCache = inputEvent.`when` to defaultKeyStroke
|
val fixedKeyStroke = fixKeyStroke(defaultKeyStroke)
|
||||||
return defaultKeyStroke
|
keyStrokeCache = inputEvent.`when` to fixedKeyStroke
|
||||||
|
return fixedKeyStroke
|
||||||
} else if (strokeCache.first == inputEvent.`when`) {
|
} else if (strokeCache.first == inputEvent.`when`) {
|
||||||
keyStrokeCache = null to null
|
keyStrokeCache = null to null
|
||||||
return strokeCache.second
|
return strokeCache.second
|
||||||
@@ -237,6 +228,19 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fixKeyStroke(key: KeyStroke): KeyStroke {
|
||||||
|
return if (
|
||||||
|
key.modifiers and CTRL_ALT_MASK != 0 &&
|
||||||
|
key.isOnKeyRelease &&
|
||||||
|
SystemInfoRt.isWindows &&
|
||||||
|
Registry.`is`("actionSystem.fix.alt.gr", true)
|
||||||
|
) {
|
||||||
|
KeyStroke.getKeyStroke(key.keyCode, key.modifiers)
|
||||||
|
} else {
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getEditor(e: AnActionEvent): Editor? {
|
private fun getEditor(e: AnActionEvent): Editor? {
|
||||||
return e.getData(PlatformDataKeys.EDITOR)
|
return e.getData(PlatformDataKeys.EDITOR)
|
||||||
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
|
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
|
||||||
@@ -317,6 +321,7 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
).build()
|
).build()
|
||||||
|
|
||||||
private const val ACTION_ID = "VimShortcutKeyAction"
|
private const val ACTION_ID = "VimShortcutKeyAction"
|
||||||
|
private const val CTRL_ALT_MASK = InputEvent.CTRL_DOWN_MASK or InputEvent.ALT_DOWN_MASK
|
||||||
|
|
||||||
private val LOG = logger<VimShortcutKeyAction>()
|
private val LOG = logger<VimShortcutKeyAction>()
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
|
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.Companion.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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/java/com/maddyhome/idea/vim/autocmd/AuCommand.kt
Normal file
11
src/main/java/com/maddyhome/idea/vim/autocmd/AuCommand.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* 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.autocmd
|
||||||
|
|
||||||
|
data class AuCommand(val command: String, val group: String?, val pattern: AutoCmdPattern = AutoCmdPattern("*"))
|
||||||
64
src/main/java/com/maddyhome/idea/vim/autocmd/AutoCmdImpl.kt
Normal file
64
src/main/java/com/maddyhome/idea/vim/autocmd/AutoCmdImpl.kt
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.autocmd
|
||||||
|
|
||||||
|
import com.maddyhome.idea.vim.api.AutoCmdService
|
||||||
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
|
import com.maddyhome.idea.vim.api.injector
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
class AutoCmdImpl : AutoCmdService {
|
||||||
|
|
||||||
|
private val eventHandlers: MutableMap<AutoCmdEvent, MutableList<AuCommand>> = ConcurrentHashMap()
|
||||||
|
private var currentAugroup: String? = null
|
||||||
|
|
||||||
|
override fun registerEventCommand(command: String, event: AutoCmdEvent, pattern: String) {
|
||||||
|
eventHandlers.getOrPut(event.canonical) { CopyOnWriteArrayList() }
|
||||||
|
.add(AuCommand(command, currentAugroup, AutoCmdPattern(pattern)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearEvents() {
|
||||||
|
val group = currentAugroup
|
||||||
|
if (group != null) {
|
||||||
|
clearAugroup(group)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
eventHandlers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startAugroup(name: String) {
|
||||||
|
currentAugroup = name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endAugroup() {
|
||||||
|
currentAugroup = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearAugroup(name: String) {
|
||||||
|
eventHandlers.values.forEach { handlers ->
|
||||||
|
handlers.removeAll { it.group == name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleEvent(event: AutoCmdEvent, filePath: String?, editor: VimEditor?) {
|
||||||
|
val resolvedEditor = editor ?: injector.editorGroup.getSelectedEditor() ?: return
|
||||||
|
val path = filePath ?: resolvedEditor.getPath()
|
||||||
|
eventHandlers[event.canonical]?.forEach { auCommand ->
|
||||||
|
if (auCommand.pattern.matches(path)) {
|
||||||
|
executeCommand(auCommand.command, resolvedEditor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeCommand(command: String, editor: VimEditor) {
|
||||||
|
val context = injector.executionContextManager.getEditorExecutionContext(editor)
|
||||||
|
injector.vimscriptExecutor.execute(command, editor, context, skipHistory = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.autocmd
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vim-style file pattern for autocmd matching.
|
||||||
|
*
|
||||||
|
* Supports glob patterns:
|
||||||
|
* - `*` matches any characters except path separators
|
||||||
|
* - `**` matches any characters including path separators
|
||||||
|
* - `?` matches a single non-separator character
|
||||||
|
* - `[abc]` matches any character in the set
|
||||||
|
* - `{foo,bar}` matches "foo" or "bar"
|
||||||
|
*
|
||||||
|
* If the pattern contains `/`, it matches against the full path.
|
||||||
|
* Otherwise, it matches against only the file name.
|
||||||
|
*/
|
||||||
|
class AutoCmdPattern(val pattern: String) {
|
||||||
|
|
||||||
|
private val matchesAll = pattern == "*"
|
||||||
|
private val matchesFullPath = '/' in pattern || '\\' in pattern
|
||||||
|
private val regex: Regex by lazy { toRegex(pattern) }
|
||||||
|
|
||||||
|
fun matches(filePath: String?): Boolean {
|
||||||
|
if (matchesAll) return true
|
||||||
|
if (filePath == null) return false
|
||||||
|
|
||||||
|
val target = if (matchesFullPath) filePath else fileName(filePath)
|
||||||
|
return regex.matches(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileName(path: String): String {
|
||||||
|
return path.substringAfterLast('/').substringAfterLast('\\')
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REGEX_SPECIAL = "\\+^$|()"
|
||||||
|
|
||||||
|
private fun toRegex(pattern: String): Regex {
|
||||||
|
val result = StringBuilder("^")
|
||||||
|
var i = 0
|
||||||
|
var inGroup = false
|
||||||
|
|
||||||
|
while (i < pattern.length) {
|
||||||
|
when (val ch = pattern[i]) {
|
||||||
|
'*' -> if (isDoubleStar(pattern, i)) {
|
||||||
|
result.append(".*")
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result.append("[^/\\\\]*")
|
||||||
|
}
|
||||||
|
|
||||||
|
'?' -> result.append("[^/\\\\]")
|
||||||
|
'.' -> result.append("\\.")
|
||||||
|
'{' -> {
|
||||||
|
result.append("(?:"); inGroup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
'}' -> {
|
||||||
|
result.append(")"); inGroup = false
|
||||||
|
}
|
||||||
|
|
||||||
|
',' -> if (inGroup) result.append("|") else result.append(",")
|
||||||
|
'[' -> result.append("[")
|
||||||
|
']' -> result.append("]")
|
||||||
|
in REGEX_SPECIAL -> {
|
||||||
|
result.append("\\"); result.append(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> result.append(ch)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append("$")
|
||||||
|
return Regex(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDoubleStar(pattern: String, i: Int): Boolean {
|
||||||
|
return i + 1 < pattern.length && pattern[i + 1] == '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.autocmd
|
||||||
|
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps IntelliJ's [com.intellij.openapi.fileTypes.FileType] name to a Vim-style filetype string
|
||||||
|
* suitable for matching against a `FileType` autocmd pattern.
|
||||||
|
*
|
||||||
|
* Most Vim filetypes are just the lowercase form of the IntelliJ name (e.g. `JAVA` → `java`,
|
||||||
|
* `Python` → `python`). A small override table covers the common cases where the conventional
|
||||||
|
* Vim name differs from IntelliJ's, so users can write `autocmd FileType python ...` and have
|
||||||
|
* it work out of the box.
|
||||||
|
*/
|
||||||
|
object IjFileTypeMapping {
|
||||||
|
|
||||||
|
private val overrides: Map<String, String> = mapOf(
|
||||||
|
"PLAIN_TEXT" to "text",
|
||||||
|
"C++" to "cpp",
|
||||||
|
"C#" to "cs",
|
||||||
|
"ObjectiveC" to "objc",
|
||||||
|
"Shell Script" to "sh",
|
||||||
|
"JavaScript" to "javascript",
|
||||||
|
"TypeScript" to "typescript",
|
||||||
|
"Vue.js" to "vue",
|
||||||
|
"Handlebars/Mustache" to "handlebars",
|
||||||
|
"CMakeLists.txt" to "cmake",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toVimFileType(virtualFile: VirtualFile?): String? {
|
||||||
|
val name = virtualFile?.fileType?.name ?: return null
|
||||||
|
return overrides[name] ?: name.lowercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -220,7 +220,7 @@ object VimExtensionFacade {
|
|||||||
caret: ImmutableVimCaret,
|
caret: ImmutableVimCaret,
|
||||||
keys: List<KeyStroke?>?,
|
keys: List<KeyStroke?>?,
|
||||||
) {
|
) {
|
||||||
caret.registerStorage.setKeys(editor, context, register, keys?.filterNotNull() ?: emptyList())
|
caret.registerStorage.setKeys(register, keys?.filterNotNull() ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the current contents of the given register */
|
/** Set the current contents of the given register */
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,7 @@ import com.intellij.openapi.editor.markup.TextAttributes
|
|||||||
import com.intellij.openapi.util.Disposer
|
import com.intellij.openapi.util.Disposer
|
||||||
import com.intellij.util.Alarm
|
import com.intellij.util.Alarm
|
||||||
import com.intellij.util.Alarm.ThreadToUse
|
import com.intellij.util.Alarm.ThreadToUse
|
||||||
import com.jetbrains.rd.util.first
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.common.ModeChangeListener
|
import com.maddyhome.idea.vim.common.ModeChangeListener
|
||||||
@@ -123,9 +121,9 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
|
|||||||
initialised = false
|
initialised = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
|
override fun yankPerformed(editor: VimEditor, range: TextRange) {
|
||||||
ensureInitialised()
|
ensureInitialised()
|
||||||
highlightHandler.highlightYankRange(caretToRange)
|
highlightHandler.highlightYankRange(editor.ij, range)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
|
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
|
||||||
@@ -146,25 +144,22 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
|
|||||||
private var lastEditor: Editor? = null
|
private var lastEditor: Editor? = null
|
||||||
private val highlighters = mutableSetOf<RangeHighlighter>()
|
private val highlighters = mutableSetOf<RangeHighlighter>()
|
||||||
|
|
||||||
fun highlightYankRange(caretToRange: Map<ImmutableVimCaret, TextRange>) {
|
fun highlightYankRange(editor: Editor, range: TextRange) {
|
||||||
// from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted
|
// from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted
|
||||||
clearYankHighlighters()
|
clearYankHighlighters()
|
||||||
|
|
||||||
val editor = caretToRange.first().key.editor.ij
|
|
||||||
lastEditor = editor
|
lastEditor = editor
|
||||||
|
|
||||||
val attributes = getHighlightTextAttributes(editor)
|
val attributes = getHighlightTextAttributes(editor)
|
||||||
for (range in caretToRange.values) {
|
for (i in 0 until range.size()) {
|
||||||
for (i in 0 until range.size()) {
|
val highlighter = editor.markupModel.addRangeHighlighter(
|
||||||
val highlighter = editor.markupModel.addRangeHighlighter(
|
range.startOffsets[i],
|
||||||
range.startOffsets[i],
|
range.endOffsets[i],
|
||||||
range.endOffsets[i],
|
HighlighterLayer.SELECTION,
|
||||||
HighlighterLayer.SELECTION,
|
attributes,
|
||||||
attributes,
|
HighlighterTargetArea.EXACT_RANGE,
|
||||||
HighlighterTargetArea.EXACT_RANGE,
|
)
|
||||||
)
|
highlighters.add(highlighter)
|
||||||
highlighters.add(highlighter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// from vim-highlightedyank docs: A negative number makes the highlight persistent.
|
// from vim-highlightedyank docs: A negative number makes the highlight persistent.
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ private object FileTypePatterns {
|
|||||||
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
|
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
|
||||||
this.cMakePatterns
|
this.cMakePatterns
|
||||||
} else {
|
} else {
|
||||||
return null
|
this.htmlPatterns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2025 The IdeaVim authors
|
* Copyright 2003-2026 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -12,6 +12,7 @@ import com.intellij.openapi.options.advanced.AdvancedSettings
|
|||||||
import com.intellij.util.ui.tree.TreeUtil
|
import com.intellij.util.ui.tree.TreeUtil
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
import javax.swing.tree.TreeNode
|
import javax.swing.tree.TreeNode
|
||||||
@@ -54,10 +55,13 @@ fun MutableMap<List<KeyStroke>, NerdTreeAction>.register(mapping: String, action
|
|||||||
*/
|
*/
|
||||||
val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List<KeyStroke>, NerdTreeAction>().apply {
|
val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List<KeyStroke>, NerdTreeAction>().apply {
|
||||||
// TODO support going [count] lines upward/downward or to line [count]
|
// TODO support going [count] lines upward/downward or to line [count]
|
||||||
register("k", NerdTreeAction.ij("Tree-selectPrevious"))
|
// Delegate to JTree's Swing ActionMap (same path as native arrow keys via TreeAction/DefaultTreeUI).
|
||||||
register("j", NerdTreeAction.ij("Tree-selectNext"))
|
// This avoids ActionManager.tryToExecute which can RPC to backend in split mode,
|
||||||
register("G", NerdTreeAction.ij("Tree-selectLast"))
|
// while preserving platform features (separator skipping, cycle scrolling, loading node handling).
|
||||||
register("gg", NerdTreeAction.ij("Tree-selectFirst"))
|
register("k", NerdTreeAction.swing("selectPrevious"))
|
||||||
|
register("j", NerdTreeAction.swing("selectNext"))
|
||||||
|
register("G", NerdTreeAction.swing("selectLast"))
|
||||||
|
register("gg", NerdTreeAction.swing("selectFirst"))
|
||||||
|
|
||||||
// FIXME lazy loaded tree nodes are not expanded
|
// FIXME lazy loaded tree nodes are not expanded
|
||||||
register("NERDTreeMapOpenRecursively", "O", NerdTreeAction.ij("FullyExpandTreeNode"))
|
register("NERDTreeMapOpenRecursively", "O", NerdTreeAction.ij("FullyExpandTreeNode"))
|
||||||
@@ -102,7 +106,7 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
|||||||
tree.selectionPath = path
|
tree.selectionPath = path
|
||||||
tree.scrollPathToVisible(path)
|
tree.scrollPathToVisible(path)
|
||||||
})
|
})
|
||||||
register("NERDTreeMapJumpParent", "p", NerdTreeAction.ij("Tree-selectParentNoCollapse"))
|
register("NERDTreeMapJumpParent", "p", NerdTreeAction.swing("selectParentNoCollapse"))
|
||||||
register(
|
register(
|
||||||
"NERDTreeMapJumpFirstChild",
|
"NERDTreeMapJumpFirstChild",
|
||||||
"K",
|
"K",
|
||||||
@@ -129,9 +133,12 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
|||||||
tree.scrollPathToVisible(path)
|
tree.scrollPathToVisible(path)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.ij("Tree-selectNextSibling"))
|
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.swing("selectNextSibling"))
|
||||||
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.ij("Tree-selectPreviousSibling"))
|
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.swing("selectPreviousSibling"))
|
||||||
|
|
||||||
register("/", NerdTreeAction.ij("SpeedSearch"))
|
register("/", NerdTreeAction { event, tree ->
|
||||||
|
armSelectionRestoreOnEscape(tree)
|
||||||
|
NerdTreeAction.callAction(null, "SpeedSearch", event.dataContext.vim)
|
||||||
|
})
|
||||||
register("<ESC>", NerdTreeAction { _, _ -> })
|
register("<ESC>", NerdTreeAction { _, _ -> })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2025 The IdeaVim authors
|
* Copyright 2003-2026 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -11,6 +11,7 @@ package com.maddyhome.idea.vim.extension.nerdtree
|
|||||||
import com.intellij.openapi.actionSystem.ActionManager
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.ui.SwingActionDelegate
|
||||||
import com.intellij.ui.treeStructure.Tree
|
import com.intellij.ui.treeStructure.Tree
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
@@ -47,5 +48,11 @@ class NerdTreeAction(val action: (AnActionEvent, Tree) -> Unit) {
|
|||||||
* @return An [NerdTreeAction] that runs the specified action when triggered.
|
* @return An [NerdTreeAction] that runs the specified action when triggered.
|
||||||
*/
|
*/
|
||||||
fun ij(id: String) = NerdTreeAction { event, _ -> callAction(null, id, event.dataContext.vim) }
|
fun ij(id: String) = NerdTreeAction { event, _ -> callAction(null, id, event.dataContext.vim) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an [NerdTreeAction] that delegates to the JTree's Swing ActionMap.
|
||||||
|
*/
|
||||||
|
fun swing(swingActionId: String) =
|
||||||
|
NerdTreeAction { _, tree -> SwingActionDelegate.performAction(swingActionId, tree) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.nerdtree
|
||||||
|
|
||||||
|
import com.intellij.ui.treeStructure.Tree
|
||||||
|
import java.awt.event.FocusAdapter
|
||||||
|
import java.awt.event.FocusEvent
|
||||||
|
import java.awt.event.KeyAdapter
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the current tree selection and arranges to restore it if the
|
||||||
|
* upcoming SpeedSearch session is dismissed via ESC, so the cursor returns
|
||||||
|
* to the file the user was on before pressing `/`.
|
||||||
|
*
|
||||||
|
* No-op if no row is currently selected. Any non-ESC key (e.g. ENTER) cancels
|
||||||
|
* the restoration so committing the search keeps the matched item selected.
|
||||||
|
*/
|
||||||
|
internal fun armSelectionRestoreOnEscape(tree: Tree) {
|
||||||
|
val originalPath = tree.selectionPath ?: return
|
||||||
|
|
||||||
|
lateinit var keyListener: KeyAdapter
|
||||||
|
lateinit var focusListener: FocusAdapter
|
||||||
|
val disarm = {
|
||||||
|
tree.removeKeyListener(keyListener)
|
||||||
|
tree.removeFocusListener(focusListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyListener = object : KeyAdapter() {
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
when (e.keyCode) {
|
||||||
|
KeyEvent.VK_ESCAPE -> {
|
||||||
|
disarm()
|
||||||
|
// Defer until SpeedSearch finishes processing the ESC and clearing
|
||||||
|
// its own state, so our restored selection is the one that sticks.
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
tree.selectionPath = originalPath
|
||||||
|
tree.scrollPathToVisible(originalPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEvent.VK_ENTER -> disarm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If focus leaves the tree before ESC/ENTER (mouse click elsewhere, popup
|
||||||
|
// dismissed by tool window switch), drop both listeners so we don't leak
|
||||||
|
// or surprise the user with a delayed jump on a later ESC.
|
||||||
|
focusListener = object : FocusAdapter() {
|
||||||
|
override fun focusLost(e: FocusEvent) = disarm()
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.addKeyListener(keyListener)
|
||||||
|
tree.addFocusListener(focusListener)
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
package com.maddyhome.idea.vim.extension.replacewithregister
|
package com.maddyhome.idea.vim.extension.replacewithregister
|
||||||
|
|
||||||
import com.intellij.vim.api.VimInitApi
|
import com.intellij.vim.api.VimInitApi
|
||||||
|
import com.intellij.vim.api.scopes.nmapPluginAction
|
||||||
|
import com.intellij.vim.api.scopes.vmapPluginAction
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
|
|
||||||
internal class ReplaceWithRegister : VimExtension {
|
internal class ReplaceWithRegister : VimExtension {
|
||||||
@@ -17,21 +19,15 @@ internal class ReplaceWithRegister : VimExtension {
|
|||||||
|
|
||||||
override fun init(initApi: VimInitApi) {
|
override fun init(initApi: VimInitApi) {
|
||||||
initApi.mappings {
|
initApi.mappings {
|
||||||
// Step 1: Non-recursive <Plug> → action mappings
|
nmapPluginAction("gr", RWR_OPERATOR, keepDefaultMapping = true) {
|
||||||
nnoremap(RWR_OPERATOR) {
|
|
||||||
rewriteMotion()
|
rewriteMotion()
|
||||||
}
|
}
|
||||||
nnoremap(RWR_LINE) {
|
nmapPluginAction("grr", RWR_LINE, keepDefaultMapping = true) {
|
||||||
rewriteLine()
|
rewriteLine()
|
||||||
}
|
}
|
||||||
vnoremap(RWR_VISUAL) {
|
vmapPluginAction("gr", RWR_VISUAL, keepDefaultMapping = true) {
|
||||||
rewriteVisual()
|
rewriteVisual()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Recursive key → <Plug> mappings
|
|
||||||
nmap("gr", RWR_OPERATOR)
|
|
||||||
nmap("grr", RWR_LINE)
|
|
||||||
vmap("gr", RWR_VISUAL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initApi.commands {
|
initApi.commands {
|
||||||
|
|||||||
@@ -16,27 +16,23 @@ import com.intellij.vim.api.models.Mode
|
|||||||
import com.intellij.vim.api.models.Range
|
import com.intellij.vim.api.models.Range
|
||||||
import com.intellij.vim.api.models.TextType
|
import com.intellij.vim.api.models.TextType
|
||||||
import com.intellij.vim.api.scopes.editor.caret.CaretTransaction
|
import com.intellij.vim.api.scopes.editor.caret.CaretTransaction
|
||||||
|
import com.intellij.vim.api.scopes.nmapPluginAction
|
||||||
|
import com.intellij.vim.api.scopes.vmapPluginAction
|
||||||
|
|
||||||
private const val PLUGIN_NAME: String = "ReplaceWithRegisterNew"
|
private const val PLUGIN_NAME: String = "ReplaceWithRegisterNew"
|
||||||
|
|
||||||
@VimPlugin(name = PLUGIN_NAME)
|
@VimPlugin(name = PLUGIN_NAME)
|
||||||
fun VimInitApi.init() {
|
fun VimInitApi.init() {
|
||||||
mappings {
|
mappings {
|
||||||
// Step 1: Non-recursive <Plug> → action mappings
|
nmapPluginAction("gr", RWR_OPERATOR, keepDefaultMapping = true) {
|
||||||
nnoremap(RWR_OPERATOR) {
|
|
||||||
rewriteMotion()
|
rewriteMotion()
|
||||||
}
|
}
|
||||||
nnoremap(RWR_LINE) {
|
nmapPluginAction("grr", RWR_LINE, keepDefaultMapping = true) {
|
||||||
rewriteLine()
|
rewriteLine()
|
||||||
}
|
}
|
||||||
vnoremap(RWR_VISUAL) {
|
vmapPluginAction("gr", RWR_VISUAL, keepDefaultMapping = true) {
|
||||||
rewriteVisual()
|
rewriteVisual()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Recursive key → <Plug> mappings
|
|
||||||
nmap("gr", RWR_OPERATOR)
|
|
||||||
nmap("grr", RWR_LINE)
|
|
||||||
vmap("gr", RWR_VISUAL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commands {
|
commands {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.maddyhome.idea.vim.extension.surround
|
||||||
|
|
||||||
|
import com.intellij.util.text.CharSequenceSubSequence
|
||||||
|
|
||||||
|
internal data class RepeatedCharSequence(val text: CharSequence, val count: Int) : CharSequence {
|
||||||
|
override val length = text.length * count
|
||||||
|
|
||||||
|
override fun get(index: Int): Char {
|
||||||
|
if (index < 0 || index >= length) throw IndexOutOfBoundsException()
|
||||||
|
return text[index % text.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
|
||||||
|
return CharSequenceSubSequence(this, startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return text.repeat(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun of(text: CharSequence, count: Int): CharSequence {
|
||||||
|
return when (count) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> text
|
||||||
|
else -> RepeatedCharSequence(text, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.KeyHandler
|
|||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
import com.maddyhome.idea.vim.api.VimCaret
|
import com.maddyhome.idea.vim.api.VimCaret
|
||||||
|
import com.maddyhome.idea.vim.api.VimChangeGroup
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
import com.maddyhome.idea.vim.api.endsWithNewLine
|
import com.maddyhome.idea.vim.api.endsWithNewLine
|
||||||
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
|
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
|
||||||
@@ -36,7 +37,10 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
|
|||||||
import com.maddyhome.idea.vim.extension.exportOperatorFunction
|
import com.maddyhome.idea.vim.extension.exportOperatorFunction
|
||||||
import com.maddyhome.idea.vim.group.findBlockRange
|
import com.maddyhome.idea.vim.group.findBlockRange
|
||||||
import com.maddyhome.idea.vim.helper.exitVisualMode
|
import com.maddyhome.idea.vim.helper.exitVisualMode
|
||||||
|
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
|
||||||
import com.maddyhome.idea.vim.key.OperatorFunction
|
import com.maddyhome.idea.vim.key.OperatorFunction
|
||||||
|
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||||
|
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
|
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
|
||||||
@@ -139,7 +143,7 @@ internal class VimSurroundExtension : VimExtension {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator())
|
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false, count = 1)) // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
private class YSurroundHandler : ExtensionHandler {
|
private class YSurroundHandler : ExtensionHandler {
|
||||||
@@ -166,7 +170,7 @@ internal class VimSurroundExtension : VimExtension {
|
|||||||
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
|
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
|
||||||
if (lastNonWhiteSpaceOffset != null) {
|
if (lastNonWhiteSpaceOffset != null) {
|
||||||
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
|
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
|
||||||
performSurround(pair, range, it)
|
performSurround(pair, range, it, count = operatorArguments.count1)
|
||||||
}
|
}
|
||||||
// it.moveToOffset(lineStartOffset)
|
// it.moveToOffset(lineStartOffset)
|
||||||
}
|
}
|
||||||
@@ -189,15 +193,13 @@ internal class VimSurroundExtension : VimExtension {
|
|||||||
|
|
||||||
private class VSurroundHandler : ExtensionHandler {
|
private class VSurroundHandler : ExtensionHandler {
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
||||||
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
|
|
||||||
// NB: Operator ignores SelectionType anyway
|
// NB: Operator ignores SelectionType anyway
|
||||||
if (!Operator().apply(editor, context, editor.mode.selectionType)) {
|
if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runWriteAction {
|
runWriteAction {
|
||||||
// Leave visual mode
|
// Leave visual mode
|
||||||
editor.exitVisualMode()
|
editor.exitVisualMode()
|
||||||
editor.ij.caretModel.moveToOffset(selectionStart)
|
|
||||||
|
|
||||||
// Reset the key handler so that the command trie is updated for the new mode (Normal)
|
// Reset the key handler so that the command trie is updated for the new mode (Normal)
|
||||||
// TODO: This should probably be handled by ToHandlerMapping.execute
|
// TODO: This should probably be handled by ToHandlerMapping.execute
|
||||||
@@ -220,6 +222,10 @@ internal class VimSurroundExtension : VimExtension {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
|
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
|
||||||
|
editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
|
||||||
// Save old register values for carets
|
// Save old register values for carets
|
||||||
val surroundings = editor.sortedCarets()
|
val surroundings = editor.sortedCarets()
|
||||||
.map {
|
.map {
|
||||||
@@ -263,7 +269,7 @@ internal class VimSurroundExtension : VimExtension {
|
|||||||
it.first + trimmedValue + it.second
|
it.first + trimmedValue + it.second
|
||||||
} ?: innerValue
|
} ?: innerValue
|
||||||
val textData =
|
val textData =
|
||||||
PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.CHARACTER_WISE)
|
PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), null)
|
||||||
val putData =
|
val putData =
|
||||||
PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false)
|
PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false)
|
||||||
|
|
||||||
@@ -342,20 +348,41 @@ internal class VimSurroundExtension : VimExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Operator : OperatorFunction {
|
private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
|
||||||
override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
|
override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
|
||||||
val ijEditor = editor.ij
|
val ijEditor = editor.ij
|
||||||
val c = injector.keyGroup.getChar(editor) ?: return true
|
val c = injector.keyGroup.getChar(editor) ?: return true
|
||||||
|
|
||||||
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
|
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
|
||||||
// XXX: Will it work with line-wise or block-wise selections?
|
|
||||||
val range = getSurroundRange(editor.currentCaret()) ?: return false
|
runWriteAction {
|
||||||
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE)
|
val change = VimPlugin.getChange()
|
||||||
// Jump back to start
|
if (supportsMultipleCursors) {
|
||||||
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
|
ijEditor.runWithEveryCaretAndRestore {
|
||||||
|
applyOnce(ijEditor, change, pair, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
applyOnce(ijEditor, change, pair, count)
|
||||||
|
// Jump back to start
|
||||||
|
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: SurroundPair, count: Int) {
|
||||||
|
// XXX: Will it work with line-wise or block-wise selections?
|
||||||
|
val primaryCaret = editor.caretModel.primaryCaret
|
||||||
|
val range = getSurroundRange(primaryCaret.vim)
|
||||||
|
if (range != null) {
|
||||||
|
val start = RepeatedCharSequence.of(pair.first, count)
|
||||||
|
val end = RepeatedCharSequence.of(pair.second, count)
|
||||||
|
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start)
|
||||||
|
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSurroundRange(caret: VimCaret): TextRange? {
|
private fun getSurroundRange(caret: VimCaret): TextRange? {
|
||||||
val editor = caret.editor
|
val editor = caret.editor
|
||||||
if (editor.mode is Mode.CMD_LINE) {
|
if (editor.mode is Mode.CMD_LINE) {
|
||||||
@@ -444,14 +471,14 @@ private fun getOrInputPair(c: Char, editor: Editor, context: DataContext): Surro
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, tagsOnNewLines: Boolean = false) {
|
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, count: Int, tagsOnNewLines: Boolean = false) {
|
||||||
val editor = caret.editor
|
val editor = caret.editor
|
||||||
val change = VimPlugin.getChange()
|
val change = VimPlugin.getChange()
|
||||||
val leftSurround = pair.first + if (tagsOnNewLines) "\n" else ""
|
val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
|
||||||
|
|
||||||
val isEOF = range.endOffset == editor.text().length
|
val isEOF = range.endOffset == editor.text().length
|
||||||
val hasNewLine = editor.endsWithNewLine()
|
val hasNewLine = editor.endsWithNewLine()
|
||||||
val rightSurround = if (tagsOnNewLines) {
|
val rightSurround = (if (tagsOnNewLines) {
|
||||||
if (isEOF && !hasNewLine) {
|
if (isEOF && !hasNewLine) {
|
||||||
"\n" + pair.second
|
"\n" + pair.second
|
||||||
} else {
|
} else {
|
||||||
@@ -459,7 +486,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pair.second
|
pair.second
|
||||||
}
|
}).let { RepeatedCharSequence.of(it, count) }
|
||||||
|
|
||||||
change.insertText(editor, caret, range.startOffset, leftSurround)
|
change.insertText(editor, caret, range.startOffset, leftSurround)
|
||||||
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)
|
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -7,181 +7,273 @@
|
|||||||
*/
|
*/
|
||||||
package com.maddyhome.idea.vim.extension.textobjindent
|
package com.maddyhome.idea.vim.extension.textobjindent
|
||||||
|
|
||||||
import com.intellij.vim.api.VimApi
|
import com.intellij.openapi.editor.Caret
|
||||||
import com.intellij.vim.api.VimInitApi
|
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
||||||
import com.intellij.vim.api.scopes.TextObjectRange
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
|
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
||||||
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
|
import com.maddyhome.idea.vim.api.injector
|
||||||
|
import com.maddyhome.idea.vim.command.MappingMode
|
||||||
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
|
import com.maddyhome.idea.vim.command.TextObjectVisualType
|
||||||
|
import com.maddyhome.idea.vim.common.TextRange
|
||||||
|
import com.maddyhome.idea.vim.extension.ExtensionHandler
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
|
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping
|
||||||
|
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping
|
||||||
|
import com.maddyhome.idea.vim.group.visual.vimSetSelection
|
||||||
|
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
|
||||||
|
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
|
||||||
|
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
||||||
|
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||||
|
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||||
|
import com.maddyhome.idea.vim.state.mode.Mode.OP_PENDING
|
||||||
|
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Port of vim-indent-object:
|
* Port of vim-indent-object:
|
||||||
* [vim-indent-object](https://github.com/michaeljsmith/vim-indent-object)
|
* [vim-indent-object](https://github.com/michaeljsmith/vim-indent-object)
|
||||||
*
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
* vim-indent-object provides these text objects based on the cursor line's indentation:
|
* vim-indent-object provides these text objects based on the cursor line's indentation:
|
||||||
*
|
*
|
||||||
* * `ai` **A**n **I**ndentation level and line above.
|
* * `ai` **A**n **I**ndentation level and line above.
|
||||||
* * `ii` **I**nner **I**ndentation level (no line above).
|
* * `ii` **I**nner **I**ndentation level (no line above).
|
||||||
* * `aI` **A**n **I**ndentation level and lines above and below.
|
* * `aI` **A**n **I**ndentation level and lines above and below.
|
||||||
* * `iI` **I**nner **I**ndentation level (no lines above and below). Synonym of `ii`
|
* * `iI` **I**nner **I**ndentation level (no lines above and below). Synonym of `ii`
|
||||||
*
|
*
|
||||||
|
*
|
||||||
* See also the reference manual for more details at:
|
* See also the reference manual for more details at:
|
||||||
* [indent-object.txt](https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt)
|
* [indent-object.txt](https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt)
|
||||||
*/
|
*/
|
||||||
class VimIndentObject : VimExtension {
|
class VimIndentObject : VimExtension {
|
||||||
override fun getName(): String = "textobj-indent"
|
override fun getName(): String {
|
||||||
|
return "textobj-indent"
|
||||||
|
}
|
||||||
|
|
||||||
override fun init(initApi: VimInitApi) {
|
override fun init() {
|
||||||
initApi.textObjects {
|
putExtensionHandlerMapping(
|
||||||
register("ai") { _ -> findIndentRange(includeAbove = true, includeBelow = false) }
|
MappingMode.XO, injector.parser.parseKeys("<Plug>textobj-indent-ai"), getOwner(),
|
||||||
register("aI") { _ -> findIndentRange(includeAbove = true, includeBelow = true) }
|
IndentObject(true, false), false
|
||||||
register("ii") { _ -> findIndentRange(includeAbove = false, includeBelow = false) }
|
)
|
||||||
|
putExtensionHandlerMapping(
|
||||||
|
MappingMode.XO, injector.parser.parseKeys("<Plug>textobj-indent-aI"), getOwner(),
|
||||||
|
IndentObject(true, true), false
|
||||||
|
)
|
||||||
|
putExtensionHandlerMapping(
|
||||||
|
MappingMode.XO, injector.parser.parseKeys("<Plug>textobj-indent-ii"), getOwner(),
|
||||||
|
IndentObject(false, false), false
|
||||||
|
)
|
||||||
|
|
||||||
|
putKeyMapping(
|
||||||
|
MappingMode.XO,
|
||||||
|
injector.parser.parseKeys("ai"),
|
||||||
|
getOwner(),
|
||||||
|
injector.parser.parseKeys("<Plug>textobj-indent-ai"),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
putKeyMapping(
|
||||||
|
MappingMode.XO,
|
||||||
|
injector.parser.parseKeys("aI"),
|
||||||
|
getOwner(),
|
||||||
|
injector.parser.parseKeys("<Plug>textobj-indent-aI"),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
putKeyMapping(
|
||||||
|
MappingMode.XO,
|
||||||
|
injector.parser.parseKeys("ii"),
|
||||||
|
getOwner(),
|
||||||
|
injector.parser.parseKeys("<Plug>textobj-indent-ii"),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class IndentObject(val includeAbove: Boolean, val includeBelow: Boolean) : ExtensionHandler {
|
||||||
|
override val isRepeatable: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
internal class IndentObjectHandler(val includeAbove: Boolean, val includeBelow: Boolean) :
|
||||||
|
TextObjectActionHandler() {
|
||||||
|
override fun getRange(
|
||||||
|
editor: VimEditor,
|
||||||
|
caret: ImmutableVimCaret,
|
||||||
|
context: ExecutionContext,
|
||||||
|
count: Int,
|
||||||
|
rawCount: Int,
|
||||||
|
): TextRange {
|
||||||
|
val charSequence = (editor as IjVimEditor).editor.getDocument().getCharsSequence()
|
||||||
|
val caretOffset = (caret as IjVimCaret).caret.getOffset()
|
||||||
|
|
||||||
|
// Part 1: Find the start of the caret line.
|
||||||
|
var caretLineStartOffset = caretOffset
|
||||||
|
var accumulatedWhitespace = 0
|
||||||
|
while (--caretLineStartOffset >= 0) {
|
||||||
|
val ch = charSequence.get(caretLineStartOffset)
|
||||||
|
if (ch == ' ' || ch == '\t') {
|
||||||
|
++accumulatedWhitespace
|
||||||
|
} else if (ch == '\n') {
|
||||||
|
++caretLineStartOffset
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
accumulatedWhitespace = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (caretLineStartOffset < 0) {
|
||||||
|
caretLineStartOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// `caretLineStartOffset` points to the first character in the line where the caret is located.
|
||||||
|
|
||||||
|
// Part 2: Compute the indentation level of the caret line.
|
||||||
|
// This is done as a separate step so that it works even when the caret is inside the indentation.
|
||||||
|
var offset = caretLineStartOffset
|
||||||
|
var indentSize = 0
|
||||||
|
while (offset < charSequence.length) {
|
||||||
|
val ch = charSequence.get(offset)
|
||||||
|
if (ch == ' ' || ch == '\t') {
|
||||||
|
++indentSize
|
||||||
|
++offset
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `indentSize` contains the amount of indent to be used for the text object range to be returned.
|
||||||
|
var upperBoundaryOffset: Int? = null
|
||||||
|
// Part 3: Find a line above the caret line, that has an indentation lower than `indentSize`.
|
||||||
|
var pos1 = caretLineStartOffset - 1
|
||||||
|
var isUpperBoundaryFound = false
|
||||||
|
while (upperBoundaryOffset == null) {
|
||||||
|
// 3.1: Going backwards from `caretLineStartOffset`, find the first non-whitespace character.
|
||||||
|
while (--pos1 >= 0) {
|
||||||
|
val ch = charSequence.get(pos1)
|
||||||
|
if (ch != ' ' && ch != '\t' && ch != '\n') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3.2: Find the indent size of the line with this non-whitespace character and check against `indentSize`.
|
||||||
|
accumulatedWhitespace = 0
|
||||||
|
while (--pos1 >= 0) {
|
||||||
|
val ch = charSequence.get(pos1)
|
||||||
|
if (ch == ' ' || ch == '\t') {
|
||||||
|
++accumulatedWhitespace
|
||||||
|
} else if (ch == '\n') {
|
||||||
|
if (accumulatedWhitespace < indentSize) {
|
||||||
|
upperBoundaryOffset = pos1 + 1
|
||||||
|
isUpperBoundaryFound = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
accumulatedWhitespace = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos1 < 0) {
|
||||||
|
// Reached start of the buffer.
|
||||||
|
upperBoundaryOffset = 0
|
||||||
|
isUpperBoundaryFound = accumulatedWhitespace < indentSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now `upperBoundaryOffset` marks the beginning of an `ai` text object.
|
||||||
|
if (isUpperBoundaryFound && !includeAbove) {
|
||||||
|
while (++upperBoundaryOffset < charSequence.length) {
|
||||||
|
val ch = charSequence.get(upperBoundaryOffset)
|
||||||
|
if (ch == '\n') {
|
||||||
|
++upperBoundaryOffset
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (charSequence.get(upperBoundaryOffset) == '\n') {
|
||||||
|
++upperBoundaryOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 4: Find the start of the caret line.
|
||||||
|
var caretLineEndOffset = caretOffset
|
||||||
|
while (++caretLineEndOffset < charSequence.length) {
|
||||||
|
val ch = charSequence.get(caretLineEndOffset)
|
||||||
|
if (ch == '\n') {
|
||||||
|
++caretLineEndOffset
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `caretLineEndOffset` points to the first charater in the line below caret line.
|
||||||
|
var lowerBoundaryOffset: Int? = null
|
||||||
|
// Part 5: Find a line below the caret line, that has an indentation lower than `indentSize`.
|
||||||
|
var pos2 = caretLineEndOffset - 1
|
||||||
|
var isLowerBoundaryFound = false
|
||||||
|
while (lowerBoundaryOffset == null) {
|
||||||
|
var accumulatedWhitespace2 = 0
|
||||||
|
var lastNewlinePos = caretLineEndOffset - 1
|
||||||
|
var isInIndent = true
|
||||||
|
while (++pos2 < charSequence.length) {
|
||||||
|
val ch = charSequence.get(pos2)
|
||||||
|
if (isIndentChar(ch) && isInIndent) {
|
||||||
|
++accumulatedWhitespace2
|
||||||
|
} else if (ch == '\n') {
|
||||||
|
accumulatedWhitespace2 = 0
|
||||||
|
lastNewlinePos = pos2
|
||||||
|
isInIndent = true
|
||||||
|
} else {
|
||||||
|
if (isInIndent && accumulatedWhitespace2 < indentSize) {
|
||||||
|
lowerBoundaryOffset = lastNewlinePos
|
||||||
|
isLowerBoundaryFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isInIndent = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos2 >= charSequence.length) {
|
||||||
|
// Reached end of the buffer.
|
||||||
|
lowerBoundaryOffset = charSequence.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now `lowerBoundaryOffset` marks the end of an `ii` text object.
|
||||||
|
if (isLowerBoundaryFound && includeBelow) {
|
||||||
|
while (++lowerBoundaryOffset < charSequence.length) {
|
||||||
|
val ch = charSequence.get(lowerBoundaryOffset)
|
||||||
|
if (ch == '\n') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextRange(upperBoundaryOffset, lowerBoundaryOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val visualType: TextObjectVisualType
|
||||||
|
get() = TextObjectVisualType.LINE_WISE
|
||||||
|
|
||||||
|
private fun isIndentChar(ch: Char): Boolean {
|
||||||
|
return ch == ' ' || ch == '\t'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
||||||
|
val vimEditor = editor as IjVimEditor
|
||||||
|
val keyHandlerState = getInstance().keyHandlerState
|
||||||
|
|
||||||
|
val textObjectHandler = IndentObjectHandler(includeAbove, includeBelow)
|
||||||
|
|
||||||
|
if (editor.mode !is OP_PENDING) {
|
||||||
|
val count0 = operatorArguments.count0
|
||||||
|
editor.editor.getCaretModel().runForEachCaret { caret: Caret ->
|
||||||
|
val range = textObjectHandler.getRange(vimEditor, IjVimCaret(caret), context, max(1, count0), count0)
|
||||||
|
SelectionVimListenerSuppressor.lock().use { ignored ->
|
||||||
|
if (editor.mode is VISUAL) {
|
||||||
|
IjVimCaret(caret).vimSetSelection(range.startOffset, range.endOffset - 1, true)
|
||||||
|
} else {
|
||||||
|
caret.moveToInlayAwareOffset(range.startOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keyHandlerState.commandBuilder.addAction(textObjectHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun VimApi.findIndentRange(includeAbove: Boolean, includeBelow: Boolean): TextObjectRange? {
|
|
||||||
val charSequence = editor { read { text } }
|
|
||||||
val caretOffset = editor { read { withPrimaryCaret { offset } } }
|
|
||||||
|
|
||||||
// Part 1: Find the start of the caret line.
|
|
||||||
var caretLineStartOffset = caretOffset
|
|
||||||
var accumulatedWhitespace = 0
|
|
||||||
while (--caretLineStartOffset >= 0) {
|
|
||||||
val ch = charSequence[caretLineStartOffset]
|
|
||||||
if (ch == ' ' || ch == '\t') {
|
|
||||||
++accumulatedWhitespace
|
|
||||||
} else if (ch == '\n') {
|
|
||||||
++caretLineStartOffset
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
accumulatedWhitespace = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (caretLineStartOffset < 0) {
|
|
||||||
caretLineStartOffset = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// `caretLineStartOffset` points to the first character in the line where the caret is located.
|
|
||||||
|
|
||||||
// Part 2: Compute the indentation level of the caret line.
|
|
||||||
// This is done as a separate step so that it works even when the caret is inside the indentation.
|
|
||||||
var offset = caretLineStartOffset
|
|
||||||
var indentSize = 0
|
|
||||||
while (offset < charSequence.length) {
|
|
||||||
val ch = charSequence[offset]
|
|
||||||
if (ch == ' ' || ch == '\t') {
|
|
||||||
++indentSize
|
|
||||||
++offset
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// `indentSize` contains the amount of indent to be used for the text object range to be returned.
|
|
||||||
var upperBoundaryOffset: Int? = null
|
|
||||||
// Part 3: Find a line above the caret line, that has an indentation lower than `indentSize`.
|
|
||||||
var pos1 = caretLineStartOffset - 1
|
|
||||||
var isUpperBoundaryFound = false
|
|
||||||
while (upperBoundaryOffset == null) {
|
|
||||||
// 3.1: Going backwards from `caretLineStartOffset`, find the first non-whitespace character.
|
|
||||||
while (--pos1 >= 0) {
|
|
||||||
val ch = charSequence[pos1]
|
|
||||||
if (ch != ' ' && ch != '\t' && ch != '\n') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 3.2: Find the indent size of the line with this non-whitespace character and check against `indentSize`.
|
|
||||||
accumulatedWhitespace = 0
|
|
||||||
while (--pos1 >= 0) {
|
|
||||||
val ch = charSequence[pos1]
|
|
||||||
if (ch == ' ' || ch == '\t') {
|
|
||||||
++accumulatedWhitespace
|
|
||||||
} else if (ch == '\n') {
|
|
||||||
if (accumulatedWhitespace < indentSize) {
|
|
||||||
upperBoundaryOffset = pos1 + 1
|
|
||||||
isUpperBoundaryFound = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
accumulatedWhitespace = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pos1 < 0) {
|
|
||||||
// Reached start of the buffer.
|
|
||||||
upperBoundaryOffset = 0
|
|
||||||
isUpperBoundaryFound = accumulatedWhitespace < indentSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now `upperBoundaryOffset` marks the beginning of an `ai` text object.
|
|
||||||
if (isUpperBoundaryFound && !includeAbove) {
|
|
||||||
while (++upperBoundaryOffset < charSequence.length) {
|
|
||||||
val ch = charSequence[upperBoundaryOffset]
|
|
||||||
if (ch == '\n') {
|
|
||||||
++upperBoundaryOffset
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (charSequence[upperBoundaryOffset] == '\n') {
|
|
||||||
++upperBoundaryOffset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Part 4: Find the end of the caret line.
|
|
||||||
var caretLineEndOffset = caretOffset
|
|
||||||
while (++caretLineEndOffset < charSequence.length) {
|
|
||||||
val ch = charSequence[caretLineEndOffset]
|
|
||||||
if (ch == '\n') {
|
|
||||||
++caretLineEndOffset
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// `caretLineEndOffset` points to the first character in the line below caret line.
|
|
||||||
var lowerBoundaryOffset: Int? = null
|
|
||||||
// Part 5: Find a line below the caret line, that has an indentation lower than `indentSize`.
|
|
||||||
var pos2 = caretLineEndOffset - 1
|
|
||||||
var isLowerBoundaryFound = false
|
|
||||||
while (lowerBoundaryOffset == null) {
|
|
||||||
var accumulatedWhitespace2 = 0
|
|
||||||
var lastNewlinePos = caretLineEndOffset - 1
|
|
||||||
var isInIndent = true
|
|
||||||
while (++pos2 < charSequence.length) {
|
|
||||||
val ch = charSequence[pos2]
|
|
||||||
if (isIndentChar(ch) && isInIndent) {
|
|
||||||
++accumulatedWhitespace2
|
|
||||||
} else if (ch == '\n') {
|
|
||||||
accumulatedWhitespace2 = 0
|
|
||||||
lastNewlinePos = pos2
|
|
||||||
isInIndent = true
|
|
||||||
} else {
|
|
||||||
if (isInIndent && accumulatedWhitespace2 < indentSize) {
|
|
||||||
lowerBoundaryOffset = lastNewlinePos
|
|
||||||
isLowerBoundaryFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
isInIndent = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pos2 >= charSequence.length) {
|
|
||||||
// Reached end of the buffer.
|
|
||||||
lowerBoundaryOffset = charSequence.length - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now `lowerBoundaryOffset` marks the end of an `ii` text object.
|
|
||||||
if (isLowerBoundaryFound && includeBelow) {
|
|
||||||
while (++lowerBoundaryOffset < charSequence.length) {
|
|
||||||
val ch = charSequence[lowerBoundaryOffset]
|
|
||||||
if (ch == '\n') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert offsets to line numbers for LineWise result
|
|
||||||
val startLine = editor { read { getLine(upperBoundaryOffset).number } }
|
|
||||||
val endLine = editor { read { getLine(lowerBoundaryOffset).number } }
|
|
||||||
return TextObjectRange.LineWise(startLine, endLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isIndentChar(ch: Char): Boolean = ch == ' ' || ch == '\t'
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.intellij.openapi.project.ProjectManager
|
|||||||
import com.intellij.openapi.wm.ToolWindowManager
|
import com.intellij.openapi.wm.ToolWindowManager
|
||||||
import com.intellij.openapi.wm.ToolWindowType
|
import com.intellij.openapi.wm.ToolWindowType
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
|
import com.maddyhome.idea.vim.helper.EditorHelper
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
@@ -31,7 +32,7 @@ internal class ToolWindowNavEverywhere : VimExtension {
|
|||||||
val oldFocusOwner = evt.oldValue as? JComponent
|
val oldFocusOwner = evt.oldValue as? JComponent
|
||||||
val dispatcher = service<ToolWindowNavDispatcher>()
|
val dispatcher = service<ToolWindowNavDispatcher>()
|
||||||
|
|
||||||
if (newFocusOwner != null && isInsideToolWindow(newFocusOwner)) {
|
if (newFocusOwner != null && isInsideToolWindow(newFocusOwner) && !isPythonConsoleComponent(newFocusOwner)) {
|
||||||
dispatcher.register(newFocusOwner)
|
dispatcher.register(newFocusOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,18 @@ internal class ToolWindowNavEverywhere : VimExtension {
|
|||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isPythonConsoleComponent(component: Component): Boolean {
|
||||||
|
for (project in ProjectManager.getInstance().openProjects) {
|
||||||
|
if (project.isDisposed) continue
|
||||||
|
val toolWindowManager = ToolWindowManager.getInstance(project)
|
||||||
|
val tw = toolWindowManager.getToolWindow(EditorHelper.PYTHON_CONSOLE_TOOL_WINDOW_ID) ?: continue
|
||||||
|
if (SwingUtilities.isDescendingFrom(component, tw.component)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun isInsideToolWindow(component: Component): Boolean {
|
private fun isInsideToolWindow(component: Component): Boolean {
|
||||||
for (project in ProjectManager.getInstance().openProjects) {
|
for (project in ProjectManager.getInstance().openProjects) {
|
||||||
if (project.isDisposed) continue
|
if (project.isDisposed) continue
|
||||||
|
|||||||
@@ -14,15 +14,14 @@ import com.intellij.openapi.command.CommandProcessor
|
|||||||
import com.intellij.openapi.command.UndoConfirmationPolicy
|
import com.intellij.openapi.command.UndoConfirmationPolicy
|
||||||
import com.intellij.openapi.diagnostic.logger
|
import com.intellij.openapi.diagnostic.logger
|
||||||
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
|
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
|
||||||
import com.intellij.openapi.editor.actions.EnterAction
|
|
||||||
import com.intellij.openapi.editor.event.EditorMouseEvent
|
import com.intellij.openapi.editor.event.EditorMouseEvent
|
||||||
import com.intellij.openapi.editor.event.EditorMouseListener
|
import com.intellij.openapi.editor.event.EditorMouseListener
|
||||||
import com.intellij.openapi.editor.impl.editorId
|
import com.intellij.openapi.editor.impl.editorId
|
||||||
import com.intellij.openapi.util.UserDataHolder
|
|
||||||
import com.intellij.psi.codeStyle.CodeStyleManager
|
import com.intellij.psi.codeStyle.CodeStyleManager
|
||||||
import com.intellij.psi.util.PsiUtilBase
|
import com.intellij.psi.util.PsiUtilBase
|
||||||
import com.maddyhome.idea.vim.EventFacade
|
import com.maddyhome.idea.vim.EventFacade
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
|
import com.maddyhome.idea.vim.api.Options
|
||||||
import com.maddyhome.idea.vim.api.VimCaret
|
import com.maddyhome.idea.vim.api.VimCaret
|
||||||
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
@@ -30,12 +29,15 @@ import com.maddyhome.idea.vim.api.injector
|
|||||||
import com.maddyhome.idea.vim.common.TextRange
|
import com.maddyhome.idea.vim.common.TextRange
|
||||||
import com.maddyhome.idea.vim.group.change.ChangeRemoteApi
|
import com.maddyhome.idea.vim.group.change.ChangeRemoteApi
|
||||||
import com.maddyhome.idea.vim.group.format.FormatRemoteApi
|
import com.maddyhome.idea.vim.group.format.FormatRemoteApi
|
||||||
import com.maddyhome.idea.vim.handler.commandContinuation
|
import com.maddyhome.idea.vim.helper.CodeWrapper
|
||||||
|
import com.maddyhome.idea.vim.helper.CommentLeaderParser
|
||||||
import com.maddyhome.idea.vim.helper.inInsertMode
|
import com.maddyhome.idea.vim.helper.inInsertMode
|
||||||
import com.maddyhome.idea.vim.key.KeyHandlerKeeper
|
import com.maddyhome.idea.vim.key.KeyHandlerKeeper
|
||||||
import com.maddyhome.idea.vim.listener.VimInsertListener
|
import com.maddyhome.idea.vim.listener.VimInsertListener
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
|
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||||
|
import com.maddyhome.idea.vim.options.OptionAccessScope
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
import com.maddyhome.idea.vim.state.mode.Mode
|
||||||
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
|
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
|
||||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||||
@@ -94,41 +96,14 @@ class ChangeGroup : VimChangeGroupBase() {
|
|||||||
injector.scroll.scrollCaretIntoView(vimEditor)
|
injector.scroll.scrollCaretIntoView(vimEditor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is REPLACE mode we need to turn off OVERWRITE before and then turn OVERWRITE back on after sending the
|
|
||||||
* "ENTER" key.
|
|
||||||
*/
|
|
||||||
override fun processEnter(
|
|
||||||
editor: VimEditor,
|
|
||||||
caret: VimCaret,
|
|
||||||
context: ExecutionContext,
|
|
||||||
) {
|
|
||||||
if (editor.mode is Mode.REPLACE) {
|
|
||||||
editor.insertMode = true
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val continuation = (context.context as UserDataHolder).getUserData(commandContinuation)
|
|
||||||
val ijEditor = editor.ij
|
|
||||||
val ij = context.ij
|
|
||||||
val ijCaret = caret.ij
|
|
||||||
if (continuation != null) {
|
|
||||||
continuation.execute(ijEditor, ijCaret, ij)
|
|
||||||
} else {
|
|
||||||
EnterAction().handler.execute(ijEditor, ijCaret, ij)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (editor.mode is Mode.REPLACE) {
|
|
||||||
editor.insertMode = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun processBackspace(editor: VimEditor, context: ExecutionContext) {
|
override fun processBackspace(editor: VimEditor, context: ExecutionContext) {
|
||||||
injector.actionExecutor.executeAction(editor, name = IdeActions.ACTION_EDITOR_BACKSPACE, context = context)
|
injector.actionExecutor.executeAction(editor, name = IdeActions.ACTION_EDITOR_BACKSPACE, context = context)
|
||||||
injector.scroll.scrollCaretIntoView(editor)
|
injector.scroll.scrollCaretIntoView(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun repeatInsertText(editor: VimEditor, context: ExecutionContext, count: Int) {
|
override fun repeatInsertText(editor: VimEditor, context: ExecutionContext, count: Int) {
|
||||||
|
if (count <= 0) return
|
||||||
|
|
||||||
val ijEditor = (editor as IjVimEditor).editor
|
val ijEditor = (editor as IjVimEditor).editor
|
||||||
val editorId = ijEditor.editorId()
|
val editorId = ijEditor.editorId()
|
||||||
|
|
||||||
@@ -152,6 +127,39 @@ class ChangeGroup : VimChangeGroupBase() {
|
|||||||
injector.application.runWriteAction {
|
injector.application.runWriteAction {
|
||||||
CodeStyleManager.getInstance(project).reformatText(file, listOf(textRange))
|
CodeStyleManager.getInstance(project).reformatText(file, listOf(textRange))
|
||||||
}
|
}
|
||||||
|
wrapText(editor, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wrapText(editor: IjVimEditor, start: Int, end: Int) {
|
||||||
|
val textwidth = injector.ijOptions(editor).textwidth
|
||||||
|
if (textwidth <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wrapTextToWidth(editor, start, end, textwidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wrapTextToWidth(editor: IjVimEditor, start: Int, end: Int, width: Int) {
|
||||||
|
val ijEditor = editor.editor
|
||||||
|
val document = ijEditor.document
|
||||||
|
|
||||||
|
val text = document.getText(com.intellij.openapi.util.TextRange.create(start, end))
|
||||||
|
val commentsValue = injector.optionGroup
|
||||||
|
.getOptionValue(Options.comments, OptionAccessScope.LOCAL(editor))
|
||||||
|
.value
|
||||||
|
val wrapper = CodeWrapper(
|
||||||
|
width = width,
|
||||||
|
tabWidth = ijEditor.settings.getTabSize(ijEditor.project),
|
||||||
|
leaders = CommentLeaderParser.parse(commentsValue),
|
||||||
|
)
|
||||||
|
val wrapped = wrapper.wrap(text)
|
||||||
|
|
||||||
|
if (wrapped == text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
injector.application.runWriteAction {
|
||||||
|
document.replaceString(start, end, wrapped)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun autoIndentRange(
|
override fun autoIndentRange(
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.intellij.lang.LanguageCommenters
|
||||||
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import com.intellij.psi.PsiDocumentManager
|
||||||
|
import com.maddyhome.idea.vim.api.Options
|
||||||
|
import com.maddyhome.idea.vim.api.injector
|
||||||
|
import com.maddyhome.idea.vim.autocmd.IjFileTypeMapping
|
||||||
|
import com.maddyhome.idea.vim.helper.CommenterMarkers
|
||||||
|
import com.maddyhome.idea.vim.helper.CommenterToComments
|
||||||
|
import com.maddyhome.idea.vim.helper.FiletypePresets
|
||||||
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
|
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a buffer-local `'comments'` value when an editor is created.
|
||||||
|
*
|
||||||
|
* Delegates to [OptionGroup.setBufferLocalDefaultIfUntouched], which preserves
|
||||||
|
* any value the user explicitly set via `.ideavimrc` or interactive `:set`.
|
||||||
|
*/
|
||||||
|
object CommentsOptionInitializer {
|
||||||
|
fun initializeForEditor(editor: Editor) {
|
||||||
|
val optionGroup = injector.optionGroup as? OptionGroup ?: return
|
||||||
|
val resolved = resolveComments(editor) ?: return
|
||||||
|
optionGroup.setBufferLocalDefaultIfUntouched(
|
||||||
|
Options.comments,
|
||||||
|
editor.vim,
|
||||||
|
VimString(resolved),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveComments(editor: Editor): String? {
|
||||||
|
val filetypeName = filetypeOf(editor) ?: return null
|
||||||
|
return FiletypePresets.presetFor(filetypeName) ?: deriveFromCommenter(editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filetypeOf(editor: Editor): String? {
|
||||||
|
val virtualFile: VirtualFile = FileDocumentManager.getInstance().getFile(editor.document) ?: return null
|
||||||
|
return IjFileTypeMapping.toVimFileType(virtualFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deriveFromCommenter(editor: Editor): String? {
|
||||||
|
val project = editor.project ?: return null
|
||||||
|
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return null
|
||||||
|
val commenter = LanguageCommenters.INSTANCE.forLanguage(psiFile.language) ?: return null
|
||||||
|
return CommenterToComments.derive(
|
||||||
|
CommenterMarkers(
|
||||||
|
linePrefix = commenter.lineCommentPrefix,
|
||||||
|
blockPrefix = commenter.blockCommentPrefix,
|
||||||
|
blockSuffix = commenter.blockCommentSuffix,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ import static com.maddyhome.idea.vim.newapi.IjVimInjectorKt.ijOptions;
|
|||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
*/
|
*/
|
||||||
@State(name = "VimEditorSettings", storages = {@Storage(value = "$APP_CONFIG$/vim_settings.xml")})
|
@State(name = "VimEditorSettings", storages = {@Storage(value = "vim_settings.xml")})
|
||||||
public class EditorGroup implements PersistentStateComponent<Element>, VimEditorGroup {
|
public class EditorGroup implements PersistentStateComponent<Element>, VimEditorGroup {
|
||||||
public static final @NonNls String EDITOR_STORE_ELEMENT = "editor";
|
public static final @NonNls String EDITOR_STORE_ELEMENT = "editor";
|
||||||
private final CaretListener myLineNumbersCaretListener = new CaretListener() {
|
private final CaretListener myLineNumbersCaretListener = new CaretListener() {
|
||||||
@@ -321,6 +321,18 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable VimEditor getSelectedEditor() {
|
||||||
|
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
|
||||||
|
if (project.isDisposed()) continue;
|
||||||
|
Editor selectedEditor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
||||||
|
if (selectedEditor != null) {
|
||||||
|
return new IjVimEditor(selectedEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Collection<VimEditor> getEditorsRaw() {
|
public @NotNull Collection<VimEditor> getEditorsRaw() {
|
||||||
return getLocalEditors().map(IjVimEditor::new).collect(Collectors.toList());
|
return getLocalEditors().map(IjVimEditor::new).collect(Collectors.toList());
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ class IjFileGroup : VimFileBase() {
|
|||||||
return if (editor != null) editor.vim else null
|
return if (editor != null) editor.vim else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun listFilesForCompletion(pathPrefix: String, context: ExecutionContext): List<String> {
|
||||||
|
return rpc { FileRemoteApi.getInstance().listFilesForCompletion(pathPrefix, extractProjectId(context)) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun getProjectId(project: Any): String {
|
override fun getProjectId(project: Any): String {
|
||||||
require(project is Project)
|
require(project is Project)
|
||||||
return project.projectId().serializeToString()
|
return project.projectId().serializeToString()
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ object IjOptions {
|
|||||||
// Temporary feature flags during development, not really intended for external use
|
// Temporary feature flags during development, not really intended for external use
|
||||||
val closenotebooks: ToggleOption =
|
val closenotebooks: ToggleOption =
|
||||||
addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true))
|
addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true))
|
||||||
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true))
|
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isHidden = true))
|
||||||
val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
|
val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
|
||||||
// This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which
|
// This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which
|
||||||
// derives from Option<VimInt>
|
// derives from Option<VimInt>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand;
|
|||||||
import com.maddyhome.idea.vim.api.*;
|
import com.maddyhome.idea.vim.api.*;
|
||||||
import com.maddyhome.idea.vim.command.MappingMode;
|
import com.maddyhome.idea.vim.command.MappingMode;
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade;
|
import com.maddyhome.idea.vim.extension.VimExtensionFacade;
|
||||||
|
import com.maddyhome.idea.vim.helper.EditorHelper;
|
||||||
|
import com.maddyhome.idea.vim.helper.EditorHelperRt;
|
||||||
import com.maddyhome.idea.vim.helper.ShortcutHelper;
|
import com.maddyhome.idea.vim.helper.ShortcutHelper;
|
||||||
import com.maddyhome.idea.vim.key.*;
|
import com.maddyhome.idea.vim.key.*;
|
||||||
import com.maddyhome.idea.vim.newapi.IjNativeAction;
|
import com.maddyhome.idea.vim.newapi.IjNativeAction;
|
||||||
@@ -51,7 +53,7 @@ import static java.util.stream.Collectors.toList;
|
|||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
*/
|
*/
|
||||||
@State(name = "VimKeySettings", storages = {@Storage(value = "$APP_CONFIG$/vim_settings.xml")})
|
@State(name = "VimKeySettings", storages = {@Storage(value = "vim_settings.xml")})
|
||||||
public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponent<Element> {
|
public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponent<Element> {
|
||||||
public static final @NonNls String SHORTCUT_CONFLICTS_ELEMENT = "shortcut-conflicts";
|
public static final @NonNls String SHORTCUT_CONFLICTS_ELEMENT = "shortcut-conflicts";
|
||||||
private static final @NonNls String SHORTCUT_CONFLICT_ELEMENT = "shortcut-conflict";
|
private static final @NonNls String SHORTCUT_CONFLICT_ELEMENT = "shortcut-conflict";
|
||||||
@@ -180,9 +182,15 @@ public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponen
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerRequiredShortcutKeys(@NotNull VimEditor editor) {
|
public void registerRequiredShortcutKeys(@NotNull VimEditor editor) {
|
||||||
|
Editor ijEditor = ((IjVimEditor)editor).getEditor();
|
||||||
|
if (EditorHelperRt.isIdeaVimDisabledHere(ijEditor)) return;
|
||||||
|
|
||||||
|
var vf = editor.getVirtualFile();
|
||||||
|
if (vf != null && vf.getPath().contains(EditorHelper.PYTHON_CONSOLE_FILE_NAME)) return;
|
||||||
|
|
||||||
EventFacade.getInstance().registerCustomShortcutSet(VimShortcutKeyAction.getInstance(),
|
EventFacade.getInstance().registerCustomShortcutSet(VimShortcutKeyAction.getInstance(),
|
||||||
ShortcutHelper.toShortcutSet(getRequiredShortcutKeys()),
|
ShortcutHelper.toShortcutSet(getRequiredShortcutKeys()),
|
||||||
((IjVimEditor)editor).getEditor().getContentComponent());
|
ijEditor.getContentComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -318,11 +326,7 @@ public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponen
|
|||||||
private void registerRequiredShortcut(@NotNull List<KeyStroke> keys, MappingOwner owner) {
|
private void registerRequiredShortcut(@NotNull List<KeyStroke> keys, MappingOwner owner) {
|
||||||
for (KeyStroke key : keys) {
|
for (KeyStroke key : keys) {
|
||||||
if (key.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
|
if (key.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
|
||||||
if (!injector.getApplication().isOctopusEnabled() ||
|
getRequiredShortcutKeys().add(new RequiredShortcut(key, owner));
|
||||||
!(key.getKeyCode() == KeyEvent.VK_ESCAPE && key.getModifiers() == 0) &&
|
|
||||||
!(key.getKeyCode() == KeyEvent.VK_ENTER && key.getModifiers() == 0)) {
|
|
||||||
getRequiredShortcutKeys().add(new RequiredShortcut(key, owner));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.maddyhome.idea.vim.group
|
||||||
|
|
||||||
|
import com.intellij.codeInsight.daemon.ReferenceImporter
|
||||||
|
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||||
|
import com.intellij.openapi.actionSystem.DataContext
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.application.ReadAction
|
||||||
|
import com.intellij.openapi.command.WriteCommandAction
|
||||||
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||||
|
import com.intellij.openapi.progress.ProgressIndicator
|
||||||
|
import com.intellij.openapi.progress.ProgressManager
|
||||||
|
import com.intellij.openapi.progress.Task
|
||||||
|
import com.intellij.psi.PsiDocumentManager
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
|
||||||
|
import java.util.function.BooleanSupplier
|
||||||
|
|
||||||
|
internal object MacroAutoImport {
|
||||||
|
fun run(editor: Editor, dataContext: DataContext) {
|
||||||
|
val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return
|
||||||
|
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
|
||||||
|
|
||||||
|
if (!FileDocumentManager.getInstance().requestWriting(editor.document, project)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val importers = ReferenceImporter.EP_NAME.extensionList
|
||||||
|
if (importers.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Auto import", true) {
|
||||||
|
override fun run(indicator: ProgressIndicator) {
|
||||||
|
val fixes = ReadAction.nonBlocking<List<BooleanSupplier>> {
|
||||||
|
val fixes = mutableListOf<BooleanSupplier>()
|
||||||
|
|
||||||
|
file.accept(object : PsiRecursiveElementWalkingVisitor() {
|
||||||
|
override fun visitElement(element: PsiElement) {
|
||||||
|
for (reference in element.references) {
|
||||||
|
if (reference.resolve() != null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (importer in importers) {
|
||||||
|
importer.computeAutoImportAtOffset(editor, file, element.textRange.startOffset, true)
|
||||||
|
?.let(fixes::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.visitElement(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return@nonBlocking fixes
|
||||||
|
}.executeSynchronously()
|
||||||
|
|
||||||
|
ApplicationManager.getApplication().invokeAndWait {
|
||||||
|
WriteCommandAction.writeCommandAction(project)
|
||||||
|
.withName("Auto Import")
|
||||||
|
.withGroupId("IdeaVimAutoImportAfterMacro")
|
||||||
|
.shouldRecordActionForActiveDocument(true)
|
||||||
|
.run<RuntimeException> {
|
||||||
|
fixes.forEach { it.asBoolean }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,9 @@ class MacroGroup : VimMacroBase() {
|
|||||||
} finally {
|
} finally {
|
||||||
keyStack.removeFirst()
|
keyStack.removeFirst()
|
||||||
}
|
}
|
||||||
|
if (!isInternalMacro) {
|
||||||
|
MacroAutoImport.run(editor.ij, context.ij)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInternalMacro) {
|
if (isInternalMacro) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
package com.maddyhome.idea.vim.group
|
package com.maddyhome.idea.vim.group
|
||||||
|
|
||||||
import com.intellij.openapi.actionSystem.DataContext
|
import com.intellij.openapi.actionSystem.DataContext
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.editor.Caret
|
import com.intellij.openapi.editor.Caret
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.VisualPosition
|
import com.intellij.openapi.editor.VisualPosition
|
||||||
@@ -15,7 +16,9 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent
|
|||||||
import com.intellij.openapi.fileEditor.TextEditor
|
import com.intellij.openapi.fileEditor.TextEditor
|
||||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||||
import com.intellij.openapi.fileEditor.impl.EditorWindow
|
import com.intellij.openapi.fileEditor.impl.EditorWindow
|
||||||
|
import com.intellij.platform.project.projectId
|
||||||
import com.maddyhome.idea.vim.KeyHandler
|
import com.maddyhome.idea.vim.KeyHandler
|
||||||
|
import com.maddyhome.idea.vim.api.BufferPosition
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
||||||
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
||||||
@@ -26,12 +29,14 @@ import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
|
|||||||
import com.maddyhome.idea.vim.api.getVisualLineCount
|
import com.maddyhome.idea.vim.api.getVisualLineCount
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.api.lineLength
|
import com.maddyhome.idea.vim.api.lineLength
|
||||||
|
import com.maddyhome.idea.vim.api.normalizeOffset
|
||||||
import com.maddyhome.idea.vim.api.normalizeVisualLine
|
import com.maddyhome.idea.vim.api.normalizeVisualLine
|
||||||
import com.maddyhome.idea.vim.api.visualLineToBufferLine
|
import com.maddyhome.idea.vim.api.visualLineToBufferLine
|
||||||
import com.maddyhome.idea.vim.command.Argument
|
import com.maddyhome.idea.vim.command.Argument
|
||||||
import com.maddyhome.idea.vim.command.MotionType
|
import com.maddyhome.idea.vim.command.MotionType
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.common.TextRange
|
import com.maddyhome.idea.vim.common.TextRange
|
||||||
|
import com.maddyhome.idea.vim.group.changelist.ChangeListService
|
||||||
import com.maddyhome.idea.vim.handler.ExternalActionHandler
|
import com.maddyhome.idea.vim.handler.ExternalActionHandler
|
||||||
import com.maddyhome.idea.vim.handler.Motion
|
import com.maddyhome.idea.vim.handler.Motion
|
||||||
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
|
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
|
||||||
@@ -57,6 +62,39 @@ import kotlin.math.min
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class MotionGroup : VimMotionGroupBase() {
|
class MotionGroup : VimMotionGroupBase() {
|
||||||
|
|
||||||
|
override fun moveCaretToChange(
|
||||||
|
editor: VimEditor,
|
||||||
|
caret: ImmutableVimCaret,
|
||||||
|
count: Int,
|
||||||
|
): Motion {
|
||||||
|
val project = editor.ij.project ?: return Motion.Error
|
||||||
|
val result = service<ChangeListService>().goToChange(project.projectId().serializeToString(), count)
|
||||||
|
return when (result) {
|
||||||
|
ChangeListService.MoveResult.Empty -> reportChangeListError(editor, "E664")
|
||||||
|
ChangeListService.MoveResult.AtStart -> reportChangeListError(editor, "E662")
|
||||||
|
ChangeListService.MoveResult.AtEnd -> reportChangeListError(editor, "E663")
|
||||||
|
is ChangeListService.MoveResult.At -> motionToChange(editor, result.change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reportChangeListError(editor: VimEditor, code: String): Motion {
|
||||||
|
injector.messages.showErrorMessage(editor, injector.messages.message(code))
|
||||||
|
return Motion.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun motionToChange(editor: VimEditor, change: ChangeListService.Change): Motion {
|
||||||
|
val target = BufferPosition(change.line, change.col, false)
|
||||||
|
if (editor.getPath() == change.filepath) {
|
||||||
|
return AbsoluteOffset(editor.bufferPositionToOffset(target))
|
||||||
|
}
|
||||||
|
injector.file.selectEditor(editor.projectId, change.filepath, change.protocol)?.let { newEditor ->
|
||||||
|
val offset = newEditor.bufferPositionToOffset(target)
|
||||||
|
newEditor.currentCaret().moveToOffset(newEditor.normalizeOffset(offset, false))
|
||||||
|
}
|
||||||
|
return Motion.Error
|
||||||
|
}
|
||||||
|
|
||||||
override fun moveCaretToFirstDisplayLine(
|
override fun moveCaretToFirstDisplayLine(
|
||||||
editor: VimEditor,
|
editor: VimEditor,
|
||||||
caret: ImmutableVimCaret,
|
caret: ImmutableVimCaret,
|
||||||
@@ -86,6 +124,9 @@ class MotionGroup : VimMotionGroupBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
|
override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
|
||||||
|
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
|
||||||
|
return AbsoluteOffset(caret.ij.visualLineStart)
|
||||||
|
}
|
||||||
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
|
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
|
||||||
return moveCaretToColumn(editor, caret, col, false)
|
return moveCaretToColumn(editor, caret, col, false)
|
||||||
}
|
}
|
||||||
@@ -94,6 +135,15 @@ class MotionGroup : VimMotionGroupBase() {
|
|||||||
editor: VimEditor,
|
editor: VimEditor,
|
||||||
caret: ImmutableVimCaret,
|
caret: ImmutableVimCaret,
|
||||||
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
|
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
|
||||||
|
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
|
||||||
|
val offset = caret.ij.visualLineStart
|
||||||
|
val line = editor.offsetToBufferPosition(offset).line
|
||||||
|
return if (offset == editor.getLineStartOffset(line)) {
|
||||||
|
editor.getLeadingCharacterOffset(line, 0)
|
||||||
|
} else {
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
}
|
||||||
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
|
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
|
||||||
val bufferLine = caret.getLine()
|
val bufferLine = caret.getLine()
|
||||||
return editor.getLeadingCharacterOffset(bufferLine, col)
|
return editor.getLeadingCharacterOffset(bufferLine, col)
|
||||||
@@ -104,6 +154,9 @@ class MotionGroup : VimMotionGroupBase() {
|
|||||||
caret: ImmutableVimCaret,
|
caret: ImmutableVimCaret,
|
||||||
allowEnd: Boolean,
|
allowEnd: Boolean,
|
||||||
): Motion {
|
): Motion {
|
||||||
|
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
|
||||||
|
return AbsoluteOffset(caret.ij.visualLineEnd - 1)
|
||||||
|
}
|
||||||
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
|
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
|
||||||
return moveCaretToColumn(editor, caret, col, allowEnd)
|
return moveCaretToColumn(editor, caret, col, allowEnd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,16 +32,12 @@ import com.intellij.openapi.ui.Messages
|
|||||||
import com.intellij.openapi.util.SystemInfo
|
import com.intellij.openapi.util.SystemInfo
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
import com.maddyhome.idea.vim.api.globalOptions
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.handler.KeyMapIssue
|
|
||||||
import com.maddyhome.idea.vim.helper.MessageHelper
|
import com.maddyhome.idea.vim.helper.MessageHelper
|
||||||
import com.maddyhome.idea.vim.icons.VimIcons
|
import com.maddyhome.idea.vim.icons.VimIcons
|
||||||
import com.maddyhome.idea.vim.key.ShortcutOwner
|
import com.maddyhome.idea.vim.key.ShortcutOwner
|
||||||
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
|
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
|
||||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
|
||||||
import com.maddyhome.idea.vim.options.OptionConstants
|
|
||||||
import com.maddyhome.idea.vim.statistic.ActionTracker
|
import com.maddyhome.idea.vim.statistic.ActionTracker
|
||||||
import com.maddyhome.idea.vim.ui.VimEmulationConfigurable
|
import com.maddyhome.idea.vim.ui.VimEmulationConfigurable
|
||||||
import com.maddyhome.idea.vim.vimscript.services.VimRcService
|
import com.maddyhome.idea.vim.vimscript.services.VimRcService
|
||||||
@@ -64,55 +60,9 @@ internal class NotificationService(private val project: Project?) : VimNotificat
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor() : this(null)
|
constructor() : this(null)
|
||||||
|
|
||||||
override fun notifyAboutIdeaPut() {
|
override fun notifyAboutIdeaPut() {}
|
||||||
val notification = Notification(
|
|
||||||
IDEAVIM_NOTIFICATION_ID,
|
|
||||||
IDEAVIM_NOTIFICATION_TITLE,
|
|
||||||
"""Add <code>ideaput</code> to <code>clipboard</code> option to perform a put via the IDE<br/><b><code>set clipboard+=ideaput</code></b>""",
|
|
||||||
NotificationType.INFORMATION,
|
|
||||||
)
|
|
||||||
|
|
||||||
notification.addAction(OpenIdeaVimRcAction(notification))
|
override fun notifyAboutIdeaJoin(editor: VimEditor) {}
|
||||||
|
|
||||||
notification.addAction(
|
|
||||||
AppendToIdeaVimRcAction(
|
|
||||||
notification,
|
|
||||||
"set clipboard^=ideaput",
|
|
||||||
"ideaput",
|
|
||||||
) {
|
|
||||||
// Technically, we're supposed to prepend values to clipboard so that it's not added to the "exclude" item.
|
|
||||||
// Since we don't handle exclude, it's safe to append. But let's be clean.
|
|
||||||
injector.globalOptions().clipboard.prependValue(OptionConstants.clipboard_ideaput)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
notification.notify(project)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notifyAboutIdeaJoin(editor: VimEditor) {
|
|
||||||
val notification = Notification(
|
|
||||||
IDEAVIM_NOTIFICATION_ID,
|
|
||||||
IDEAVIM_NOTIFICATION_TITLE,
|
|
||||||
"""Put <b><code>set ideajoin</code></b> into your <code>~/.ideavimrc</code> to perform a join via the IDE""",
|
|
||||||
NotificationType.INFORMATION,
|
|
||||||
)
|
|
||||||
|
|
||||||
notification.addAction(OpenIdeaVimRcAction(notification))
|
|
||||||
|
|
||||||
notification.addAction(
|
|
||||||
AppendToIdeaVimRcAction(
|
|
||||||
notification,
|
|
||||||
"set ideajoin",
|
|
||||||
"ideajoin"
|
|
||||||
) {
|
|
||||||
// This is a global-local option. Setting it will always set the global value
|
|
||||||
injector.ijOptions(editor).ideajoin = true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
notification.addAction(HelpLink(ideajoinExamplesUrl))
|
|
||||||
notification.notify(project)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun enableRepeatingMode() = Messages.showYesNoDialog(
|
override fun enableRepeatingMode() = Messages.showYesNoDialog(
|
||||||
"Do you want to enable repeating keys in macOS on press and hold?\n\n" +
|
"Do you want to enable repeating keys in macOS on press and hold?\n\n" +
|
||||||
@@ -210,78 +160,6 @@ internal class NotificationService(private val project: Project?) : VimNotificat
|
|||||||
ActionIdNotifier.notifyActionId(id, project, candidates, intentionName)
|
ActionIdNotifier.notifyActionId(id, project, candidates, intentionName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun notifyKeymapIssues(issues: ArrayList<KeyMapIssue>) {
|
|
||||||
val keymapManager = KeymapManagerEx.getInstanceEx()
|
|
||||||
val keymap = keymapManager.activeKeymap
|
|
||||||
val message = buildString {
|
|
||||||
appendLine("Current IDE keymap (${keymap.name}) has issues:<br/>")
|
|
||||||
issues.forEach {
|
|
||||||
when (it) {
|
|
||||||
is KeyMapIssue.AddShortcut -> {
|
|
||||||
appendLine("- ${it.key} key is not assigned to the ${it.action} action.<br/>")
|
|
||||||
}
|
|
||||||
|
|
||||||
is KeyMapIssue.RemoveShortcut -> {
|
|
||||||
appendLine("- ${it.shortcut} key is incorrectly assigned to the ${it.action} action.<br/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val notification = IDEAVIM_STICKY_GROUP.createNotification(
|
|
||||||
IDEAVIM_NOTIFICATION_TITLE,
|
|
||||||
message,
|
|
||||||
NotificationType.ERROR,
|
|
||||||
)
|
|
||||||
notification.subtitle = "IDE keymap misconfigured"
|
|
||||||
notification.addAction(object : DumbAwareAction("Fix Keymap") {
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
|
||||||
issues.forEach {
|
|
||||||
when (it) {
|
|
||||||
is KeyMapIssue.AddShortcut -> {
|
|
||||||
keymap.addShortcut(it.actionId, KeyboardShortcut(it.keyStroke, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
is KeyMapIssue.RemoveShortcut -> {
|
|
||||||
keymap.removeShortcut(it.actionId, it.shortcut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.info("Shortcuts updated $issues")
|
|
||||||
notification.expire()
|
|
||||||
requiredShortcutsAssigned()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
notification.addAction(object : DumbAwareAction("Open Keymap Settings") {
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
|
||||||
ShowSettingsUtil.getInstance().showSettingsDialog(e.project, KeymapPanel::class.java)
|
|
||||||
notification.hideBalloon()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
notification.addAction(object : DumbAwareAction("Ignore") {
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
|
||||||
LOG.info("Ignored to update shortcuts $issues")
|
|
||||||
notification.hideBalloon()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
notification.notify(project)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requiredShortcutsAssigned() {
|
|
||||||
val notification = Notification(
|
|
||||||
IDEAVIM_NOTIFICATION_ID,
|
|
||||||
IDEAVIM_NOTIFICATION_TITLE,
|
|
||||||
"Keymap fixed",
|
|
||||||
NotificationType.INFORMATION,
|
|
||||||
)
|
|
||||||
notification.addAction(object : DumbAwareAction("Open Keymap Settings") {
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
|
||||||
ShowSettingsUtil.getInstance().showSettingsDialog(e.project, KeymapPanel::class.java)
|
|
||||||
notification.hideBalloon()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
notification.notify(project)
|
|
||||||
}
|
|
||||||
|
|
||||||
object ActionIdNotifier {
|
object ActionIdNotifier {
|
||||||
private var notification: Notification? = null
|
private var notification: Notification? = null
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,22 @@ class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup, InternalOptionValueA
|
|||||||
super.setOptionValueInternal(option, scope, value)
|
super.setOptionValueInternal(option, scope, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the buffer-local value of [option] as a Vim default — but only if the
|
||||||
|
* current value is still a [OptionValue.Default]. Preserves any value the user
|
||||||
|
* explicitly set via `.ideavimrc` or interactive `:set`/`:setlocal`.
|
||||||
|
*/
|
||||||
|
fun <T : VimDataType> setBufferLocalDefaultIfUntouched(
|
||||||
|
option: Option<T>,
|
||||||
|
editor: VimEditor,
|
||||||
|
value: T,
|
||||||
|
) {
|
||||||
|
val scope = OptionAccessScope.LOCAL(editor)
|
||||||
|
val current = getOptionValueInternal(option, scope)
|
||||||
|
if (current !is OptionValue.Default) return
|
||||||
|
setOptionValueInternal(option, scope, OptionValue.Default(value))
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun editorReleased(editor: Editor) {
|
fun editorReleased(editor: Editor) {
|
||||||
// Vim always has at least one window; it's not possible to close it. Editing a new file will open a new buffer in
|
// Vim always has at least one window; it's not possible to close it. Editing a new file will open a new buffer in
|
||||||
|
|||||||
@@ -24,10 +24,9 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This group works with command associated with copying and pasting text
|
* This group works with command associated with copying and pasting text
|
||||||
*/
|
*/
|
||||||
@@ -128,7 +127,7 @@ public class RegisterGroup extends VimRegisterGroupBase
|
|||||||
final String text = XMLGroup.getInstance().getSafeXmlText(textElement);
|
final String text = XMLGroup.getInstance().getSafeXmlText(textElement);
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
logger.trace("Register data parsed");
|
logger.trace("Register data parsed");
|
||||||
register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type);
|
register = new Register(key, type, text, Collections.emptyList());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.trace("Cannot parse register data");
|
logger.trace("Cannot parse register data");
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.group
|
|||||||
|
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
import com.maddyhome.idea.vim.handler.KeyMapIssue
|
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,5 +31,4 @@ interface VimNotifications {
|
|||||||
fun notifyEapFinished()
|
fun notifyEapFinished()
|
||||||
fun showReenableNotification(project: Project)
|
fun showReenableNotification(project: Project)
|
||||||
fun notifyActionId(id: String?, candidates: List<String>? = null, intentionName: String?)
|
fun notifyActionId(id: String?, candidates: List<String>? = null, intentionName: String?)
|
||||||
fun notifyKeymapIssues(issues: ArrayList<KeyMapIssue>)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.changelist
|
||||||
|
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.platform.project.projectId
|
||||||
|
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
|
||||||
|
import com.intellij.platform.rpc.topics.ProjectRemoteTopicListener
|
||||||
|
|
||||||
|
internal class ChangeListRemoteTopicListener : ProjectRemoteTopicListener<ChangeListInfo> {
|
||||||
|
override val topic: ProjectRemoteTopic<ChangeListInfo> = CHANGE_LIST_REMOTE_TOPIC
|
||||||
|
|
||||||
|
override fun handleEvent(project: Project, event: ChangeListInfo) {
|
||||||
|
service<ChangeListService>().addChange(
|
||||||
|
project.projectId().serializeToString(),
|
||||||
|
ChangeListService.Change(event.line, event.col, event.filepath, event.protocol),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.changelist
|
||||||
|
|
||||||
|
import com.intellij.openapi.components.Service
|
||||||
|
import org.jetbrains.annotations.TestOnly
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-project change list backing `g;` and `g,` (`:help changelist`).
|
||||||
|
*
|
||||||
|
* Index/merge semantics follow Neovim's `get_changelist` (`src/nvim/mark.c`)
|
||||||
|
* and `changed_common` (`src/nvim/change.c`): after each recorded change the
|
||||||
|
* index sits past the end, so the first `g;` lands on the newest entry.
|
||||||
|
*/
|
||||||
|
@Service(Service.Level.APP)
|
||||||
|
internal class ChangeListService {
|
||||||
|
|
||||||
|
private val projectToChanges = mutableMapOf<String, MutableList<Change>>()
|
||||||
|
private val projectToIndex = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
data class Change(
|
||||||
|
val line: Int,
|
||||||
|
val col: Int,
|
||||||
|
val filepath: String,
|
||||||
|
val protocol: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface MoveResult {
|
||||||
|
object Empty : MoveResult
|
||||||
|
object AtStart : MoveResult
|
||||||
|
object AtEnd : MoveResult
|
||||||
|
data class At(val change: Change) : MoveResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addChange(projectId: String, change: Change) {
|
||||||
|
val list = projectToChanges.getOrPut(projectId) { mutableListOf() }
|
||||||
|
if (list.lastOrNull()?.shouldMergeWith(change) == true) {
|
||||||
|
list[list.lastIndex] = change
|
||||||
|
} else {
|
||||||
|
list.add(change)
|
||||||
|
if (list.size > CHANGE_LIST_LIMIT) list.removeAt(0)
|
||||||
|
}
|
||||||
|
projectToIndex[projectId] = list.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun goToChange(projectId: String, count: Int): MoveResult {
|
||||||
|
val list = projectToChanges[projectId]
|
||||||
|
if (list.isNullOrEmpty()) return MoveResult.Empty
|
||||||
|
|
||||||
|
val current = projectToIndex.getOrPut(projectId) { list.size }
|
||||||
|
val target = current + count
|
||||||
|
|
||||||
|
if (target < 0 && current == 0) return MoveResult.AtStart
|
||||||
|
if (target >= list.size && current == list.size - 1) return MoveResult.AtEnd
|
||||||
|
|
||||||
|
val newIndex = target.coerceIn(0, list.size - 1)
|
||||||
|
projectToIndex[projectId] = newIndex
|
||||||
|
return MoveResult.At(list[newIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Change.shouldMergeWith(next: Change): Boolean =
|
||||||
|
filepath == next.filepath &&
|
||||||
|
line == next.line &&
|
||||||
|
kotlin.math.abs(col - next.col) < TEXTWIDTH_FALLBACK
|
||||||
|
|
||||||
|
@TestOnly
|
||||||
|
@Synchronized
|
||||||
|
fun reset() {
|
||||||
|
projectToChanges.clear()
|
||||||
|
projectToIndex.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANGE_LIST_LIMIT = 100
|
||||||
|
private const val TEXTWIDTH_FALLBACK = 79
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,6 @@ import com.maddyhome.idea.vim.ide.isClionNova
|
|||||||
import com.maddyhome.idea.vim.ide.isRider
|
import com.maddyhome.idea.vim.ide.isRider
|
||||||
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
|
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimCopiedText
|
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
@@ -127,7 +126,7 @@ internal class PutGroup : VimPutBase() {
|
|||||||
point.dispose()
|
point.dispose()
|
||||||
if (!caret.isValid) return@forEach
|
if (!caret.isValid) return@forEach
|
||||||
|
|
||||||
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.copiedText.text.length)
|
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.text.length)
|
||||||
val endOffset = if (data.indent) {
|
val endOffset = if (data.indent) {
|
||||||
doIndent(
|
doIndent(
|
||||||
vimEditor,
|
vimEditor,
|
||||||
@@ -179,10 +178,12 @@ internal class PutGroup : VimPutBase() {
|
|||||||
val allContentsBefore = CopyPasteManager.getInstance().allContents
|
val allContentsBefore = CopyPasteManager.getInstance().allContents
|
||||||
val sizeBeforeInsert = allContentsBefore.size
|
val sizeBeforeInsert = allContentsBefore.size
|
||||||
val firstItemBefore = allContentsBefore.firstOrNull()
|
val firstItemBefore = allContentsBefore.firstOrNull()
|
||||||
logger.debug { "Copied text: ${text.copiedText}" }
|
logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" }
|
||||||
val (textContent, transferableData) = text.copiedText as IjVimCopiedText
|
|
||||||
val origContent: TextBlockTransferable =
|
val origContent: TextBlockTransferable =
|
||||||
injector.clipboardManager.setClipboardText(textContent, textContent, transferableData) as TextBlockTransferable
|
injector.clipboardManager.setClipboardText(
|
||||||
|
text.text,
|
||||||
|
transferableData = text.transferableData,
|
||||||
|
) as TextBlockTransferable
|
||||||
val allContentsAfter = CopyPasteManager.getInstance().allContents
|
val allContentsAfter = CopyPasteManager.getInstance().allContents
|
||||||
val sizeAfterInsert = allContentsAfter.size
|
val sizeAfterInsert = allContentsAfter.size
|
||||||
try {
|
try {
|
||||||
@@ -190,7 +191,7 @@ internal class PutGroup : VimPutBase() {
|
|||||||
} finally {
|
} finally {
|
||||||
val textInClipboard = (firstItemBefore as? TextBlockTransferable)
|
val textInClipboard = (firstItemBefore as? TextBlockTransferable)
|
||||||
?.getTransferData(DataFlavor.stringFlavor) as? String
|
?.getTransferData(DataFlavor.stringFlavor) as? String
|
||||||
val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.text
|
val textOnTop = textInClipboard != null && textInClipboard != text.text
|
||||||
if (sizeBeforeInsert != sizeAfterInsert || textOnTop) {
|
if (sizeBeforeInsert != sizeAfterInsert || textOnTop) {
|
||||||
// Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register
|
// Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register
|
||||||
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }
|
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2023 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.maddyhome.idea.vim.handler
|
|
||||||
|
|
||||||
import com.intellij.openapi.actionSystem.KeyboardShortcut
|
|
||||||
import com.intellij.openapi.components.Service
|
|
||||||
import com.intellij.openapi.components.service
|
|
||||||
import com.intellij.openapi.diagnostic.logger
|
|
||||||
import com.intellij.openapi.keymap.Keymap
|
|
||||||
import com.intellij.openapi.keymap.KeymapManagerListener
|
|
||||||
import com.intellij.openapi.keymap.ex.KeymapManagerEx
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.openapi.startup.ProjectActivity
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.api.key
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jetbrains.annotations.NonNls
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
|
|
||||||
// We use alarm with delay to avoid many actions in case many events are fired at the same time
|
|
||||||
internal val correctorRequester = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
||||||
|
|
||||||
private val LOG = logger<CopilotKeymapCorrector>()
|
|
||||||
|
|
||||||
internal class CopilotKeymapCorrector : ProjectActivity {
|
|
||||||
override suspend fun execute(project: Project) {
|
|
||||||
project.service<CopilotKeymapCorrectorService>().start()
|
|
||||||
correctorRequester.emit(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* At the moment of release 2023.3 there is a problem that starting a coroutine like this
|
|
||||||
* right in the project activity will block this project activity in tests.
|
|
||||||
* To avoid that, there is an intermediate service that will allow to avoid this issue.
|
|
||||||
*
|
|
||||||
* However, in general we should start this coroutine right in the [CopilotKeymapCorrector]
|
|
||||||
*/
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
@Service(Service.Level.PROJECT)
|
|
||||||
internal class CopilotKeymapCorrectorService(private val cs: CoroutineScope) {
|
|
||||||
fun start() {
|
|
||||||
cs.launch {
|
|
||||||
correctorRequester
|
|
||||||
.debounce(5_000)
|
|
||||||
.collectLatest { correctCopilotKeymap() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class IdeaVimCorrectorKeymapChangedListener : KeymapManagerListener {
|
|
||||||
override fun activeKeymapChanged(keymap: Keymap?) {
|
|
||||||
check(correctorRequester.tryEmit(Unit))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shortcutsChanged(keymap: Keymap, actionIds: @NonNls Collection<String>, fromSettings: Boolean) {
|
|
||||||
check(correctorRequester.tryEmit(Unit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val copilotHideActionMap = ConcurrentHashMap<String, Unit>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See VIM-3206
|
|
||||||
* The user expected to both copilot suggestion and the insert mode to be exited on a single esc.
|
|
||||||
* However, for the moment, the first esc hides copilot suggestion and the second one exits insert mode.
|
|
||||||
* To fix this, we remove the esc shortcut from the copilot action if the IdeaVim is active.
|
|
||||||
*
|
|
||||||
* This workaround is not the best solution, however, I don't see the better way with the current architecture of
|
|
||||||
* actions and EditorHandlers. Firstly, I wanted to suggest to copilot to migrate to EditorActionHandler as well,
|
|
||||||
* but this doesn't seem correct for me because in this case the user will lose an ability to change the shorcut for
|
|
||||||
* it. It seems like copilot has a similar problem as we do - we don't want to make a handler for "Editor enter action",
|
|
||||||
* but a handler for the esc key press. And, moreover, be able to communicate with other plugins about the ordering.
|
|
||||||
* Before this feature is implemented, hiding the copilot suggestion on esc looks like a good workaround.
|
|
||||||
*/
|
|
||||||
private fun correctCopilotKeymap() {
|
|
||||||
// This is needed to initialize the injector in case this verification is called to fast
|
|
||||||
VimPlugin.getInstance()
|
|
||||||
|
|
||||||
if (!enableOctopus) return
|
|
||||||
if (injector.enabler.isEnabled()) {
|
|
||||||
val keymap = KeymapManagerEx.getInstanceEx().activeKeymap
|
|
||||||
val res = keymap.getShortcuts("copilot.disposeInlays")
|
|
||||||
if (res.isEmpty()) return
|
|
||||||
|
|
||||||
|
|
||||||
val escapeShortcut = res.find { it.toString() == "[pressed ESCAPE]" } ?: return
|
|
||||||
keymap.removeShortcut("copilot.disposeInlays", escapeShortcut)
|
|
||||||
copilotHideActionMap[keymap.name] = Unit
|
|
||||||
LOG.info("Remove copilot escape shortcut from keymap ${keymap.name}")
|
|
||||||
} else {
|
|
||||||
copilotHideActionMap.forEach { (name, _) ->
|
|
||||||
val keymap = KeymapManagerEx.getInstanceEx().getKeymap(name) ?: return@forEach
|
|
||||||
val currentShortcuts = keymap.getShortcuts("copilot.disposeInlays")
|
|
||||||
if ("[pressed ESCAPE]" !in currentShortcuts.map { it.toString() }) {
|
|
||||||
keymap.addShortcut("copilot.disposeInlays", KeyboardShortcut(key("<esc>"), null))
|
|
||||||
}
|
|
||||||
LOG.info("Restore copilot escape shortcut in keymap ${keymap.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2023 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.maddyhome.idea.vim.handler
|
|
||||||
|
|
||||||
import com.intellij.openapi.actionSystem.IdeActions
|
|
||||||
import com.intellij.openapi.diagnostic.logger
|
|
||||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandlerBean
|
|
||||||
import com.intellij.openapi.extensions.ExtensionPointName
|
|
||||||
import com.intellij.openapi.keymap.ex.KeymapManagerEx
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.openapi.startup.ProjectActivity
|
|
||||||
import com.maddyhome.idea.vim.api.key
|
|
||||||
import com.maddyhome.idea.vim.newapi.initInjector
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs the chain of handlers for esc and enter
|
|
||||||
*
|
|
||||||
* As we made a migration to the new way of handling esc keys (VIM-2974), we may face several issues around that
|
|
||||||
* One of the possible issues is that some plugin may also register a shortcut for this key and do not pass
|
|
||||||
* the control to the next handler. In this way, the esc won't work, but there will be no exceptions.
|
|
||||||
*
|
|
||||||
* This is a logger that logs the chain of handlers.
|
|
||||||
*
|
|
||||||
* Strictly speaking, such access to the extension point is not allowed by the platform. But we can't do this thing
|
|
||||||
* otherwise, so let's use it as long as we can.
|
|
||||||
*/
|
|
||||||
internal class EditorHandlersChainLogger : ProjectActivity {
|
|
||||||
@Suppress("UnresolvedPluginConfigReference")
|
|
||||||
private val editorHandlers = ExtensionPointName<EditorActionHandlerBean>("com.intellij.editorActionHandler")
|
|
||||||
|
|
||||||
override suspend fun execute(project: Project) {
|
|
||||||
initInjector()
|
|
||||||
|
|
||||||
if (!enableOctopus) return
|
|
||||||
|
|
||||||
val escHandlers = editorHandlers.extensionList
|
|
||||||
.filter { it.action == "EditorEscape" }
|
|
||||||
.joinToString("\n") { it.implementationClass }
|
|
||||||
val enterHandlers = editorHandlers.extensionList
|
|
||||||
.filter { it.action == "EditorEnter" }
|
|
||||||
.joinToString("\n") { it.implementationClass }
|
|
||||||
|
|
||||||
LOG.info("Esc handlers chain:\n$escHandlers")
|
|
||||||
LOG.info("Enter handlers chain:\n$enterHandlers")
|
|
||||||
|
|
||||||
val keymapManager = KeymapManagerEx.getInstanceEx()
|
|
||||||
val keymap = keymapManager.activeKeymap
|
|
||||||
val keymapShortcutsForEsc = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ESCAPE).joinToString()
|
|
||||||
val keymapShortcutsForEnter = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ENTER).joinToString()
|
|
||||||
|
|
||||||
LOG.info("Active keymap (${keymap.name}) shortcuts for esc: $keymapShortcutsForEsc, Shortcuts for enter: $keymapShortcutsForEnter")
|
|
||||||
|
|
||||||
val actionsForEsc = keymap.getActionIds(key("<esc>")).joinToString("\n")
|
|
||||||
val actionsForEnter = keymap.getActionIds(key("<enter>")).joinToString("\n")
|
|
||||||
|
|
||||||
LOG.info(
|
|
||||||
"Also keymap (${keymap.name}) has " +
|
|
||||||
"the following actions assigned to esc:\n$actionsForEsc " +
|
|
||||||
"\nand following actions assigned to enter:\n$actionsForEnter"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val LOG = logger<EditorHandlersChainLogger>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.handler
|
|
||||||
|
|
||||||
import com.intellij.openapi.actionSystem.IdeActions
|
|
||||||
import com.intellij.openapi.actionSystem.KeyboardShortcut
|
|
||||||
import com.intellij.openapi.actionSystem.Shortcut
|
|
||||||
import com.intellij.openapi.components.Service
|
|
||||||
import com.intellij.openapi.components.service
|
|
||||||
import com.intellij.openapi.keymap.Keymap
|
|
||||||
import com.intellij.openapi.keymap.KeymapManagerListener
|
|
||||||
import com.intellij.openapi.keymap.ex.KeymapManagerEx
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.openapi.startup.ProjectActivity
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.api.key
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jetbrains.annotations.NonNls
|
|
||||||
import javax.swing.KeyStroke
|
|
||||||
|
|
||||||
// We use alarm with delay to avoid many notifications in case many events are fired at the same time
|
|
||||||
internal val keyCheckRequests = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This checker verifies that the keymap has a correct configuration that is required for IdeaVim plugin
|
|
||||||
*/
|
|
||||||
internal class KeymapChecker : ProjectActivity {
|
|
||||||
override suspend fun execute(project: Project) {
|
|
||||||
project.service<KeymapCheckerService>().start()
|
|
||||||
keyCheckRequests.emit(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* At the moment of release 2023.3 there is a problem that starting a coroutine like this
|
|
||||||
* right in the project activity will block this project activity in tests.
|
|
||||||
* To avoid that, there is an intermediate service that will allow to avoid this issue.
|
|
||||||
*
|
|
||||||
* However, in general we should start this coroutine right in the [KeymapChecker]
|
|
||||||
*/
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
@Service(Service.Level.PROJECT)
|
|
||||||
internal class KeymapCheckerService(private val cs: CoroutineScope) {
|
|
||||||
fun start() {
|
|
||||||
cs.launch {
|
|
||||||
keyCheckRequests
|
|
||||||
.debounce(5_000)
|
|
||||||
.collectLatest { verifyKeymap() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class IdeaVimKeymapChangedListener : KeymapManagerListener {
|
|
||||||
override fun activeKeymapChanged(keymap: Keymap?) {
|
|
||||||
check(keyCheckRequests.tryEmit(Unit))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shortcutsChanged(keymap: Keymap, actionIds: @NonNls Collection<String>, fromSettings: Boolean) {
|
|
||||||
check(keyCheckRequests.tryEmit(Unit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After migration to the editor action handlers, we have to make sure that the keymap has a correct configuration.
|
|
||||||
* For example, that esc key is assigned to esc editor action
|
|
||||||
*
|
|
||||||
* Usually this is not a problem because this is a standard mapping, but the problem may appear in a misconfiguration
|
|
||||||
* like it was in VIM-3204
|
|
||||||
*/
|
|
||||||
private fun verifyKeymap() {
|
|
||||||
// This is needed to initialize the injector in case this verification is called to fast
|
|
||||||
VimPlugin.getInstance()
|
|
||||||
|
|
||||||
if (!enableOctopus) return
|
|
||||||
if (!injector.enabler.isEnabled()) return
|
|
||||||
|
|
||||||
val keymap = KeymapManagerEx.getInstanceEx().activeKeymap
|
|
||||||
val keymapShortcutsForEsc = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ESCAPE)
|
|
||||||
val keymapShortcutsForEnter = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ENTER)
|
|
||||||
|
|
||||||
val issues = ArrayList<KeyMapIssue>()
|
|
||||||
val correctShortcutMissing = keymapShortcutsForEsc
|
|
||||||
.filterIsInstance<KeyboardShortcut>()
|
|
||||||
.none { it.firstKeyStroke.toString() == "pressed ESCAPE" && it.secondKeyStroke == null }
|
|
||||||
|
|
||||||
// We also check if there are any shortcuts starting from esc and with a second key. This should also be removed.
|
|
||||||
// For example, VIM-3162 has a case when two escapes were assigned to editor escape action
|
|
||||||
val shortcutsStartingFromEsc = keymapShortcutsForEsc
|
|
||||||
.filterIsInstance<KeyboardShortcut>()
|
|
||||||
.filter { it.firstKeyStroke.toString() == "pressed ESCAPE" && it.secondKeyStroke != null }
|
|
||||||
if (correctShortcutMissing) {
|
|
||||||
issues += KeyMapIssue.AddShortcut(
|
|
||||||
"esc",
|
|
||||||
"editor escape",
|
|
||||||
IdeActions.ACTION_EDITOR_ESCAPE,
|
|
||||||
key("<esc>")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
shortcutsStartingFromEsc.forEach {
|
|
||||||
issues += KeyMapIssue.RemoveShortcut("editor escape", IdeActions.ACTION_EDITOR_ESCAPE, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val correctEnterShortcutMissing = keymapShortcutsForEnter
|
|
||||||
.filterIsInstance<KeyboardShortcut>()
|
|
||||||
.none { it.firstKeyStroke.toString() == "pressed ENTER" && it.secondKeyStroke == null }
|
|
||||||
val shortcutsStartingFromEnter = keymapShortcutsForEnter
|
|
||||||
.filterIsInstance<KeyboardShortcut>()
|
|
||||||
.filter { it.firstKeyStroke.toString() == "pressed ENTER" && it.secondKeyStroke != null }
|
|
||||||
if (correctEnterShortcutMissing) {
|
|
||||||
issues += KeyMapIssue.AddShortcut(
|
|
||||||
"enter",
|
|
||||||
"editor enter",
|
|
||||||
IdeActions.ACTION_EDITOR_ENTER,
|
|
||||||
key("<enter>")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
shortcutsStartingFromEnter.forEach {
|
|
||||||
issues += KeyMapIssue.RemoveShortcut("editor enter", IdeActions.ACTION_EDITOR_ENTER, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (issues.isNotEmpty()) {
|
|
||||||
VimPlugin.getNotifications(null).notifyKeymapIssues(issues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface KeyMapIssue {
|
|
||||||
data class AddShortcut(
|
|
||||||
val key: String,
|
|
||||||
val action: String,
|
|
||||||
val actionId: String,
|
|
||||||
val keyStroke: KeyStroke,
|
|
||||||
) : KeyMapIssue
|
|
||||||
|
|
||||||
data class RemoveShortcut(
|
|
||||||
val action: String,
|
|
||||||
val actionId: String,
|
|
||||||
val shortcut: Shortcut,
|
|
||||||
) : KeyMapIssue
|
|
||||||
}
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.handler
|
|
||||||
|
|
||||||
import com.intellij.codeInsight.editorActions.AutoHardWrapHandler
|
|
||||||
import com.intellij.codeInsight.lookup.LookupManager
|
|
||||||
import com.intellij.formatting.LineWrappingUtil
|
|
||||||
import com.intellij.ide.DataManager
|
|
||||||
import com.intellij.openapi.actionSystem.DataContext
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.application.invokeLater
|
|
||||||
import com.intellij.openapi.diagnostic.logger
|
|
||||||
import com.intellij.openapi.editor.Caret
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
|
||||||
import com.intellij.openapi.editor.actions.SplitLineAction
|
|
||||||
import com.intellij.openapi.editor.impl.CaretModelImpl
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
|
||||||
import com.intellij.openapi.util.Key
|
|
||||||
import com.intellij.openapi.util.UserDataHolder
|
|
||||||
import com.intellij.openapi.util.removeUserData
|
|
||||||
import com.maddyhome.idea.vim.KeyHandler
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.api.key
|
|
||||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
|
||||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
|
||||||
import com.maddyhome.idea.vim.helper.IjActionExecutor
|
|
||||||
import com.maddyhome.idea.vim.helper.inNormalMode
|
|
||||||
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.newapi.actionStartedFromVim
|
|
||||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import javax.swing.KeyStroke
|
|
||||||
|
|
||||||
internal val commandContinuation = Key.create<EditorActionHandler>("commandContinuation")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler that corrects the shape of the caret in python notebooks.
|
|
||||||
*
|
|
||||||
* By default, py notebooks show a thin caret after entering the cell.
|
|
||||||
* However, we're in normal mode, so this handler fixes it.
|
|
||||||
*/
|
|
||||||
internal class CaretShapeEnterEditorHandler(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
|
||||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
|
||||||
if (VimPlugin.isEnabled() && !editor.isIdeaVimDisabledHere && enableOctopus) {
|
|
||||||
invokeLater {
|
|
||||||
editor.updateCaretsVisualAttributes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextHandler.execute(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
|
||||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This handler doesn't work in tests for ex commands
|
|
||||||
*
|
|
||||||
* About this handler: VIM-2974
|
|
||||||
*/
|
|
||||||
internal abstract class OctopusHandler(private val nextHandler: EditorActionHandler?) : EditorActionHandler() {
|
|
||||||
|
|
||||||
abstract fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?)
|
|
||||||
open fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
|
||||||
if (isThisHandlerEnabled(editor, caret, dataContext)) {
|
|
||||||
val executeInInvokeLater = executeInInvokeLater(editor)
|
|
||||||
val executionHandler = {
|
|
||||||
try {
|
|
||||||
(dataContext as? UserDataHolder)?.putUserData(commandContinuation, nextHandler)
|
|
||||||
executeHandler(editor, caret, dataContext)
|
|
||||||
} finally {
|
|
||||||
(dataContext as? UserDataHolder)?.removeUserData(commandContinuation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (executeInInvokeLater) {
|
|
||||||
// This `invokeLater` is used to escape the potential `runForEachCaret` function.
|
|
||||||
//
|
|
||||||
// The `runForEachCaret` function is disallowed to be called recursively. However, with this new handler, we lose
|
|
||||||
// control if we execute the code inside this function or not. See IDEA-300030 for details.
|
|
||||||
// This means the code in IdeaVim MUST NOT call `runForEachCaret` function. While this is possible for most cases,
|
|
||||||
// the user may make a mapping to some intellij action where the `runForEachCaret` is called. This breaks
|
|
||||||
// the condition (see VIM-3103 for example).
|
|
||||||
// Since we can't make sure we don't execute `runForEachCaret`, we have to "escape" out of this function. This is
|
|
||||||
// done by scheduling the execution of our code later via the invokeLater function.
|
|
||||||
//
|
|
||||||
// We run this job only once for a primary caret. In the handler itself, we'll multiply the execution by the
|
|
||||||
// number of carets. If we run this job for each caret, we may end up in the issue like VIM-3186.
|
|
||||||
// However, I think that we may do some refactoring to run this job for each caret (if needed).
|
|
||||||
//
|
|
||||||
// For the moment, the known case when the caret is null - work in injected editor - VIM-3195
|
|
||||||
if (caret == null || caret == editor.caretModel.primaryCaret) {
|
|
||||||
ApplicationManager.getApplication().invokeLater(executionHandler)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
executionHandler()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nextHandler?.execute(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun executeInInvokeLater(editor: Editor): Boolean {
|
|
||||||
// Currently we have a workaround for the PY console VIM-3157
|
|
||||||
val fileName = FileDocumentManager.getInstance().getFile(editor.document)?.name
|
|
||||||
if (
|
|
||||||
fileName == "Python Console.py" || // This is the name in 232+
|
|
||||||
fileName == "Python Console" // This is the name in 231
|
|
||||||
) return false
|
|
||||||
return (editor.caretModel as? CaretModelImpl)?.isIteratingOverCarets ?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isThisHandlerEnabled(editor: Editor, caret: Caret?, dataContext: DataContext?): Boolean {
|
|
||||||
if (VimPlugin.isNotEnabled()) return false
|
|
||||||
if (editor.isIdeaVimDisabledHere) return false
|
|
||||||
if (!isHandlerEnabled(editor, dataContext)) return false
|
|
||||||
if (isNotActualKeyPress(dataContext)) return false
|
|
||||||
if (!enableOctopus) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In some cases IJ runs handlers to imitate "enter" or other key. In such cases we should not process it on the
|
|
||||||
* IdeaVim side because the user may have mappings on enter the we'll get an unexpected behaviour.
|
|
||||||
* This method should return true if we detect that this handler is called in such case and this is not an
|
|
||||||
* actual keypress from the user.
|
|
||||||
*/
|
|
||||||
private fun isNotActualKeyPress(dataContext: DataContext?): Boolean {
|
|
||||||
if (dataContext != null) {
|
|
||||||
// This flag is set when the enter handlers are executed as a part of moving the comment on the new line
|
|
||||||
val dataManager = DataManager.getInstance()
|
|
||||||
if (dataManager.loadFromDataContext(dataContext, AutoHardWrapHandler.AUTO_WRAP_LINE_IN_PROGRESS_KEY) == true) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// From VIM-3177
|
|
||||||
val wrapLongLineDuringFormattingInProgress = dataManager
|
|
||||||
.loadFromDataContext(dataContext, LineWrappingUtil.WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY)
|
|
||||||
if (wrapLongLineDuringFormattingInProgress == true) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// From VIM-3203
|
|
||||||
val splitLineInProgress = dataManager.loadFromDataContext(dataContext, SplitLineAction.SPLIT_LINE_KEY)
|
|
||||||
if (splitLineInProgress == true) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataManager.loadFromDataContext(dataContext, StartNewLineDetectorBase.Util.key) == true) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataContext?.actionStartedFromVim == true) return true
|
|
||||||
if ((injector.actionExecutor as? IjActionExecutor)?.isRunningActionFromVim == true) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
|
||||||
return isThisHandlerEnabled(editor, caret, dataContext)
|
|
||||||
|| nextHandler?.isEnabled(editor, caret, dataContext) == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Known conflicts & solutions:
|
|
||||||
* - Smart step into - set handler after
|
|
||||||
* - Python notebooks - set handler after
|
|
||||||
* - Ace jump - set handler after
|
|
||||||
* - Lookup - doesn't intersect with enter anymore
|
|
||||||
* - App code - set handler after
|
|
||||||
* - Template - doesn't intersect with enter anymore
|
|
||||||
* - rd.client.editor.enter - set handler before. Otherwise, rider will add new line on enter even in normal mode
|
|
||||||
* - inline.completion.enter - set handler before. Otherwise, AI completion is not invoked on enter.
|
|
||||||
*
|
|
||||||
* This rule is disabled due to VIM-3124
|
|
||||||
* - before terminalEnter - not necessary, but terminalEnter causes "file is read-only" tooltip for readonly files VIM-3122
|
|
||||||
* - `first` is set to satisfy sorting condition "before terminalEnter".
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* DO NOT add handlers that force to add "first" ordering. This doesn't work with jupyterCommandModeEnterKeyHandler (see VIM-3124)
|
|
||||||
*/
|
|
||||||
internal class VimEnterHandler(nextHandler: EditorActionHandler?) : VimKeyHandler(nextHandler) {
|
|
||||||
override val key: String = "<CR>"
|
|
||||||
|
|
||||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
|
||||||
if (!super.isHandlerEnabled(editor, dataContext)) return false
|
|
||||||
// This is important for one-line editors, to turn off enter.
|
|
||||||
// Some one-line editors rely on the fact that there are no enter actions registered. For example, hash search in git
|
|
||||||
// See VIM-2974 for example where it was broken
|
|
||||||
return !editor.isOneLineMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Known conflicts & solutions:
|
|
||||||
*
|
|
||||||
* - Smart step into - set handler after
|
|
||||||
* - Python notebooks - set handler before - yes, we have `<CR>` as "after" and `<esc>` as before. I'm not completely sure
|
|
||||||
* why this combination is correct, but other versions don't work.
|
|
||||||
* - Ace jump - set handler after
|
|
||||||
* - Lookup - It disappears after putting our esc before templateEscape. But I'm not sure why it works like that
|
|
||||||
* - App code - Need to review
|
|
||||||
* - Template - Need to review
|
|
||||||
* - before backend.escape - to handle our handlers before Rider processing. Also, without this rule, we get problems like VIM-3146
|
|
||||||
*/
|
|
||||||
internal class VimEscHandler(nextHandler: EditorActionHandler) : VimKeyHandler(nextHandler) {
|
|
||||||
override val key: String = "<Esc>"
|
|
||||||
|
|
||||||
private val ideaVimSupportDialog
|
|
||||||
get() = injector.globalIjOptions().ideavimsupport.contains(IjOptionConstants.ideavimsupport_dialog)
|
|
||||||
|
|
||||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
|
||||||
return editor.isPrimaryEditor() ||
|
|
||||||
EditorHelper.isFileEditor(editor) && vimStateNeedsToHandleEscape(editor) ||
|
|
||||||
ideaVimSupportDialog && vimStateNeedsToHandleEscape(editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun vimStateNeedsToHandleEscape(editor: Editor): Boolean {
|
|
||||||
return !editor.vim.mode.inNormalMode || KeyHandler.getInstance().keyHandlerState.mappingState.hasKeys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rider (and CLion Nova) uses a separate handler for esc to close the completion. IdeaOnlyEscapeHandlerAction is especially
|
|
||||||
* designed to get all the esc presses, and if there is a completion close it and do not pass the execution further.
|
|
||||||
* This doesn't work the same as in IJ.
|
|
||||||
* In IdeaVim, we'd like to exit insert mode on closing completion. This is a requirement as the change of this
|
|
||||||
* behaviour causes a lot of complaining from users. Since the rider handler gets execution control, we don't
|
|
||||||
* receive an event and don't exit the insert mode.
|
|
||||||
* To fix it, this special handler exists only for rider and stands before the rider's handler. We don't execute the
|
|
||||||
* handler from rider because the autocompletion is closed automatically anyway.
|
|
||||||
*
|
|
||||||
* NOTE: This handler only works when octopus is enabled (non-Rider IDEs). For Rider, where octopus is disabled
|
|
||||||
* (VIM-3815) and Escape is consumed by the popup manager before the EditorEscape chain fires, the fix is in
|
|
||||||
* [com.maddyhome.idea.vim.listener.IdeaSpecifics.LookupTopicListener] via a LookupListener.
|
|
||||||
*/
|
|
||||||
internal class VimEscForRiderHandler(nextHandler: EditorActionHandler) : VimKeyHandler(nextHandler) {
|
|
||||||
override val key: String = "<Esc>"
|
|
||||||
|
|
||||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
|
||||||
if (!enableOctopus) return false
|
|
||||||
return LookupManager.getActiveLookup(editor) != null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty logger for esc presses
|
|
||||||
*
|
|
||||||
* As we made a migration to the new way of handling esc keys (VIM-2974), we may face several issues around that
|
|
||||||
* One of the possible issues is that some plugin may also register a shortcut for this key and do not pass
|
|
||||||
* the control to the next handler. In this way, the esc won't work, but there will be no exceptions.
|
|
||||||
* This handler, that should stand in front of handlers change, just logs the event of pressing the key
|
|
||||||
* and passes the execution.
|
|
||||||
*/
|
|
||||||
internal class VimEscLoggerHandler(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
|
||||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
|
||||||
if (enableOctopus) {
|
|
||||||
LOG.info("Esc pressed")
|
|
||||||
}
|
|
||||||
nextHandler.execute(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
|
||||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val LOG = logger<VimEscLoggerHandler>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workaround to support "Start New Line" action in normal mode.
|
|
||||||
* IJ executes enter handler on "Start New Line". This causes an issue that IdeaVim thinks that this is just an enter key.
|
|
||||||
* This thing should be refactored, but for now we'll use this workaround VIM-3159
|
|
||||||
*
|
|
||||||
* The Same thing happens with "Start New Line Before Current" action.
|
|
||||||
*/
|
|
||||||
internal class StartNewLineDetector(nextHandler: EditorActionHandler) : StartNewLineDetectorBase(nextHandler)
|
|
||||||
internal class StartNewLineBeforeCurrentDetector(nextHandler: EditorActionHandler) :
|
|
||||||
StartNewLineDetectorBase(nextHandler)
|
|
||||||
|
|
||||||
internal open class StartNewLineDetectorBase(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
|
||||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
|
||||||
if (enableOctopus) {
|
|
||||||
DataManager.getInstance().saveInDataContext(dataContext, Util.key, true)
|
|
||||||
}
|
|
||||||
nextHandler.execute(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
|
||||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
object Util {
|
|
||||||
val key = Key.create<Boolean>("vim.is.start.new.line")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val LOG = logger<VimEscLoggerHandler>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty logger for enter presses
|
|
||||||
*
|
|
||||||
* As we made a migration to the new way of handling enter keys (VIM-2974), we may face several issues around that
|
|
||||||
* One of the possible issues is that some plugin may also register a shortcut for this key and do not pass
|
|
||||||
* the control to the next handler. In this way, the esc won't work, but there will be no exceptions.
|
|
||||||
* This handler, that should stand in front of handlers change, just logs the event of pressing the key
|
|
||||||
* and passes the execution.
|
|
||||||
*/
|
|
||||||
internal class VimEnterLoggerHandler(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
|
||||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
|
||||||
if (enableOctopus) {
|
|
||||||
LOG.info("Enter pressed")
|
|
||||||
}
|
|
||||||
nextHandler.execute(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
|
||||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val LOG = logger<VimEnterLoggerHandler>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract class VimKeyHandler(nextHandler: EditorActionHandler?) : OctopusHandler(nextHandler) {
|
|
||||||
|
|
||||||
abstract val key: String
|
|
||||||
|
|
||||||
override fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
|
||||||
val enterKey = key(key)
|
|
||||||
val context = dataContext?.vim ?: injector.executionContextManager.getEditorExecutionContext(editor.vim)
|
|
||||||
val keyHandler = KeyHandler.getInstance()
|
|
||||||
keyHandler.handleKey(editor.vim, enterKey, context, keyHandler.keyHandlerState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
|
||||||
val enterKey = key(key)
|
|
||||||
return isOctopusEnabled(enterKey, editor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isOctopusEnabled(s: KeyStroke, editor: Editor): Boolean {
|
|
||||||
if (!enableOctopus) return false
|
|
||||||
// CMD line has a different processing mechanizm: the processing actions are registered
|
|
||||||
// for the input field component. These keys are not dispatched via the octopus handler.
|
|
||||||
if (editor.vim.mode is Mode.CMD_LINE) return false
|
|
||||||
when {
|
|
||||||
s.keyCode == KeyEvent.VK_ENTER && s.modifiers == 0 -> return true
|
|
||||||
s.keyCode == KeyEvent.VK_ESCAPE && s.modifiers == 0 -> return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val enableOctopus: Boolean
|
|
||||||
get() = injector.application.isOctopusEnabled()
|
|
||||||
@@ -172,6 +172,7 @@ class CaretVisualAttributesListener : IsReplaceCharListener, ModeChangeListener,
|
|||||||
@RequiresEdt
|
@RequiresEdt
|
||||||
private fun updateCaretsVisual(editor: VimEditor) {
|
private fun updateCaretsVisual(editor: VimEditor) {
|
||||||
val ijEditor = (editor as IjVimEditor).editor
|
val ijEditor = (editor as IjVimEditor).editor
|
||||||
|
if (ijEditor.isDisposed) return
|
||||||
ijEditor.updateCaretsVisualAttributes()
|
ijEditor.updateCaretsVisualAttributes()
|
||||||
ijEditor.updateCaretsVisualPosition()
|
ijEditor.updateCaretsVisualPosition()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.intellij.openapi.editor.*;
|
|||||||
import com.intellij.openapi.editor.ex.util.EditorUtil;
|
import com.intellij.openapi.editor.ex.util.EditorUtil;
|
||||||
import com.intellij.openapi.editor.impl.EditorImpl;
|
import com.intellij.openapi.editor.impl.EditorImpl;
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||||
|
import com.intellij.openapi.util.Key;
|
||||||
import com.intellij.openapi.util.SystemInfo;
|
import com.intellij.openapi.util.SystemInfo;
|
||||||
import com.intellij.openapi.util.registry.Registry;
|
import com.intellij.openapi.util.registry.Registry;
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
import com.intellij.openapi.vfs.VirtualFile;
|
||||||
@@ -45,6 +46,9 @@ public class EditorHelper {
|
|||||||
// mitigates the visible area bouncing around too much and even pushing the cursor line off screen with large
|
// mitigates the visible area bouncing around too much and even pushing the cursor line off screen with large
|
||||||
// multiline rendered doc comments, while still providing some visibility of the block inlay (e.g. Rider's single line
|
// multiline rendered doc comments, while still providing some visibility of the block inlay (e.g. Rider's single line
|
||||||
// Code Vision)
|
// Code Vision)
|
||||||
|
public static final String PYTHON_CONSOLE_FILE_NAME = "Python Console.py";
|
||||||
|
public static final String PYTHON_CONSOLE_TOOL_WINDOW_ID = "Python Console";
|
||||||
|
|
||||||
private static final int BLOCK_INLAY_MAX_LINE_HEIGHT = 3;
|
private static final int BLOCK_INLAY_MAX_LINE_HEIGHT = 3;
|
||||||
|
|
||||||
public static @NotNull Rectangle getVisibleArea(final @NotNull Editor editor) {
|
public static @NotNull Rectangle getVisibleArea(final @NotNull Editor editor) {
|
||||||
@@ -351,7 +355,7 @@ public class EditorHelper {
|
|||||||
|
|
||||||
final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
|
final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
|
||||||
final @NotNull VimEditor editor1 = new IjVimEditor(editor);
|
final @NotNull VimEditor editor1 = new IjVimEditor(editor);
|
||||||
final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) - 1;
|
final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) + editor.getSettings().getAdditionalLinesCount();
|
||||||
final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine);
|
final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine);
|
||||||
|
|
||||||
// For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen.
|
// For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen.
|
||||||
@@ -679,6 +683,49 @@ public class EditorHelper {
|
|||||||
return editor.getEditorKind() == EditorKind.DIFF;
|
return editor.getEditorKind() == EditorKind.DIFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the editor is the Python console, so we can disable Vim features
|
||||||
|
*/
|
||||||
|
public static boolean isPythonConsole(@NotNull Editor editor) {
|
||||||
|
var file = EditorHelper.getVirtualFile(editor);
|
||||||
|
if (file == null) return false;
|
||||||
|
// In split mode, the projected VirtualFile may have a different getName() result,
|
||||||
|
// so we also check getPath() to reliably detect the Python console.
|
||||||
|
return file.getName().contains(PYTHON_CONSOLE_FILE_NAME) || file.getPath().contains(PYTHON_CONSOLE_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the editor is hosted in the Commit tool window, so we can enable Vim features
|
||||||
|
*/
|
||||||
|
public static boolean isCommitWindowEditor(@NotNull Editor editor) {
|
||||||
|
@SuppressWarnings("deprecation") Key<?> dataKey = Key.findKeyByName("Vcs.CommitMessage.Panel");
|
||||||
|
if (dataKey != null && editor.getDocument().getUserData(dataKey) != null) return true;
|
||||||
|
var file = EditorHelper.getVirtualFile(editor);
|
||||||
|
return file != null && file.getName().contains("Dummy.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the editor is a Kotlin class file decompiled to a Java file, so we can enable Vim features
|
||||||
|
* <p>
|
||||||
|
* The platform changed the implementation of decompiling a Kotlin .class file to Java in 2026.2. Previously, it
|
||||||
|
* used a dummy virtual file implementation. Now it uses an instance of {@link LightVirtualFile}. Typically, this
|
||||||
|
* means an in-memory file that we don't want to have Vim features for, but in this case, we do.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* To test, open a .class file generated from a Kotlin file. Then use the "Decompile to Java" action to create a
|
||||||
|
* separate (in-memory) `.decompiled.java` file. Java-based .class files are decompiled directly in the document for
|
||||||
|
* the .class file, so the editor is always backed by a valid file.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Perhaps a future implementation would have an allow-list for {@link VirtualFile#getFileType()} and allow "JAVA"?
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static boolean isKotlinClassDecompiledToJavaFile(@NotNull Editor editor) {
|
||||||
|
@SuppressWarnings("deprecation") @Nullable Key<?> key = Key.findKeyByName("IS_KOTLIN_DECOMPILED_FILE");
|
||||||
|
var file = EditorHelper.getVirtualFile(editor);
|
||||||
|
return file != null && key != null && file.getUserData(key) == Boolean.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the document in the editor is modified.
|
* Checks if the document in the editor is modified.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ package com.maddyhome.idea.vim.helper
|
|||||||
|
|
||||||
import com.intellij.codeWithMe.ClientId
|
import com.intellij.codeWithMe.ClientId
|
||||||
import com.intellij.openapi.editor.Caret
|
import com.intellij.openapi.editor.Caret
|
||||||
|
import com.intellij.openapi.editor.CaretState
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.editor.EditorKind
|
||||||
import com.intellij.openapi.editor.ex.util.EditorUtil
|
import com.intellij.openapi.editor.ex.util.EditorUtil
|
||||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||||
|
import com.intellij.openapi.ui.popup.util.PopupUtil
|
||||||
import com.intellij.util.ui.table.JBTableRowEditor
|
import com.intellij.util.ui.table.JBTableRowEditor
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.StringListOptionValue
|
import com.maddyhome.idea.vim.api.StringListOptionValue
|
||||||
@@ -22,6 +25,8 @@ import com.maddyhome.idea.vim.api.injector
|
|||||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
import com.maddyhome.idea.vim.group.IjOptionConstants
|
||||||
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
|
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
|
||||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||||
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
|
import com.maddyhome.idea.vim.state.mode.inBlockSelection
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JTable
|
import javax.swing.JTable
|
||||||
@@ -49,7 +54,7 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
|
|||||||
!ClientId.isCurrentlyUnderLocalId || // CWM-927
|
!ClientId.isCurrentlyUnderLocalId || // CWM-927
|
||||||
(ideaVimDisabledForSingleLine(ideaVimSupportValue) && isSingleLine()) ||
|
(ideaVimDisabledForSingleLine(ideaVimSupportValue) && isSingleLine()) ||
|
||||||
IdeaVimDisablerExtensionPoint.isDisabledForEditor(this) ||
|
IdeaVimDisablerExtensionPoint.isDisabledForEditor(this) ||
|
||||||
isNotFileEditorExceptAllowed()
|
!isAllowedFileEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,18 +66,21 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
|
|||||||
* Here are issues when non-file editors were supported:
|
* Here are issues when non-file editors were supported:
|
||||||
* AI Chat – VIM-3786
|
* AI Chat – VIM-3786
|
||||||
* Debug evaluate console – VIM-3929
|
* Debug evaluate console – VIM-3929
|
||||||
|
* Python console - VIM-4172
|
||||||
*
|
*
|
||||||
* However, we still support IdeaVim in a commit window because it works fine there, and removing vim from this place will
|
* We do want to support Vim actions in some windows, such as the commit window, diff windows, and decompiled Java
|
||||||
* be quite a visible change for users.
|
* files. We don't support the Python console.
|
||||||
* We detect the commit window by the name of the editor (Dummy.txt). If this causes issues, let's disable IdeaVim
|
|
||||||
* in the commit window as well.
|
|
||||||
*
|
|
||||||
* Also, we support IdeaVim in diff viewers.
|
|
||||||
*/
|
*/
|
||||||
private fun Editor.isNotFileEditorExceptAllowed(): Boolean {
|
private fun Editor.isAllowedFileEditor(): Boolean {
|
||||||
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) return false
|
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) {
|
||||||
if (EditorHelper.isDiffEditor(this)) return false
|
return PopupUtil.getPopupContainerFor(component) == null
|
||||||
return !EditorHelper.isFileEditor(this)
|
}
|
||||||
|
if (EditorHelper.isPythonConsole(this)) return false
|
||||||
|
|
||||||
|
return EditorHelper.isCommitWindowEditor(this)
|
||||||
|
|| EditorHelper.isKotlinClassDecompiledToJavaFile(this)
|
||||||
|
|| EditorHelper.isDiffEditor(this)
|
||||||
|
|| EditorHelper.isFileEditor(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ideaVimDisabledInDialog(ideaVimSupportValue: StringListOptionValue): Boolean {
|
private fun ideaVimDisabledInDialog(ideaVimSupportValue: StringListOptionValue): Boolean {
|
||||||
@@ -111,8 +119,7 @@ internal fun Editor.isPrimaryEditor(): Boolean {
|
|||||||
internal fun Editor.isTerminalEditor(): Boolean {
|
internal fun Editor.isTerminalEditor(): Boolean {
|
||||||
return !isViewer
|
return !isViewer
|
||||||
&& document.isWritable
|
&& document.isWritable
|
||||||
&& !EditorHelper.isFileEditor(this)
|
&& this.editorKind == EditorKind.CONSOLE
|
||||||
&& !EditorHelper.isDiffEditor(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
|
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
|
||||||
@@ -141,3 +148,41 @@ private fun vimEnabled(editor: Editor?): Boolean {
|
|||||||
if (editor != null && editor.isIdeaVimDisabledHere) return false
|
if (editor != null && editor.isIdeaVimDisabledHere) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) {
|
||||||
|
val caretModel = this.caretModel
|
||||||
|
val carets = if (this.vim.inBlockSelection) null else caretModel.allCarets
|
||||||
|
if (carets == null || carets.size == 1) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var initialDocumentSize = this.document.textLength
|
||||||
|
var documentSizeDifference = 0
|
||||||
|
|
||||||
|
val caretOffsets = carets.map { it.selectionStart to it.selectionEnd }
|
||||||
|
val restoredCarets = mutableListOf<CaretState>()
|
||||||
|
|
||||||
|
caretModel.removeSecondaryCarets()
|
||||||
|
|
||||||
|
for ((selectionStart, selectionEnd) in caretOffsets) {
|
||||||
|
if (selectionStart == selectionEnd) {
|
||||||
|
caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
caretModel.primaryCaret.setSelection(
|
||||||
|
selectionStart + documentSizeDifference,
|
||||||
|
selectionEnd + documentSizeDifference
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
action()
|
||||||
|
restoredCarets.add(caretModel.caretsAndSelections.single())
|
||||||
|
|
||||||
|
val documentLength = this.document.textLength
|
||||||
|
documentSizeDifference += documentLength - initialDocumentSize
|
||||||
|
initialDocumentSize = documentLength
|
||||||
|
}
|
||||||
|
|
||||||
|
caretModel.caretsAndSelections = restoredCarets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,15 +24,19 @@ import com.intellij.openapi.diagnostic.thisLogger
|
|||||||
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
|
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
|
||||||
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
|
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
|
||||||
import com.intellij.openapi.util.NlsContexts
|
import com.intellij.openapi.util.NlsContexts
|
||||||
|
import com.intellij.refactoring.actions.BaseRefactoringAction
|
||||||
import com.maddyhome.idea.vim.RegisterActions
|
import com.maddyhome.idea.vim.RegisterActions
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
import com.maddyhome.idea.vim.api.NativeAction
|
import com.maddyhome.idea.vim.api.NativeAction
|
||||||
import com.maddyhome.idea.vim.api.VimActionExecutor
|
import com.maddyhome.idea.vim.api.VimActionExecutor
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
|
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
|
||||||
import com.maddyhome.idea.vim.newapi.IjNativeAction
|
import com.maddyhome.idea.vim.newapi.IjNativeAction
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
|
import com.maddyhome.idea.vim.state.mode.Mode
|
||||||
|
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||||
import org.jetbrains.annotations.NonNls
|
import org.jetbrains.annotations.NonNls
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
@@ -68,6 +72,12 @@ class IjActionExecutor : VimActionExecutor {
|
|||||||
thisLogger().error("Actions cannot be updated when write-action is running or pending")
|
thisLogger().error("Actions cannot be updated when write-action is running or pending")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val startVisualModeType = (editor?.mode as? Mode.VISUAL)?.selectionType
|
||||||
|
val startVisualCaretSelection = if (editor != null && startVisualModeType != null && action.action !is BaseRefactoringAction)
|
||||||
|
editor.primaryCaret().let { Triple(it.offset, it.selectionStart, it.selectionEnd) }
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
val ijAction = (action as IjNativeAction).action
|
val ijAction = (action as IjNativeAction).action
|
||||||
try {
|
try {
|
||||||
isRunningActionFromVim = true
|
isRunningActionFromVim = true
|
||||||
@@ -77,6 +87,20 @@ class IjActionExecutor : VimActionExecutor {
|
|||||||
val place = ijAction.choosePlace()
|
val place = ijAction.choosePlace()
|
||||||
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
|
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
|
||||||
res.waitFor(5_000)
|
res.waitFor(5_000)
|
||||||
|
|
||||||
|
if (startVisualModeType != null && startVisualCaretSelection != null) {
|
||||||
|
val primaryCaret = editor.primaryCaret()
|
||||||
|
val endVisualCaretOffset = primaryCaret.offset
|
||||||
|
if (startVisualCaretSelection.first != endVisualCaretOffset) {
|
||||||
|
if (!editor.inVisualMode || (editor.mode as Mode.VISUAL).selectionType != startVisualModeType) {
|
||||||
|
injector.visualMotionGroup.toggleVisual(editor, 1, 0, startVisualModeType)
|
||||||
|
}
|
||||||
|
primaryCaret.moveToOffset(startVisualCaretSelection.first)
|
||||||
|
primaryCaret.setSelection(startVisualCaretSelection.second, startVisualCaretSelection.third)
|
||||||
|
primaryCaret.moveToOffset(endVisualCaretOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.isDone
|
return res.isDone
|
||||||
} finally {
|
} finally {
|
||||||
isRunningActionFromVim = false
|
isRunningActionFromVim = false
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ internal object ScrollViewHelper {
|
|||||||
// that this needs to be replaced as a more or less dumb line for line rewrite.
|
// that this needs to be replaced as a more or less dumb line for line rewrite.
|
||||||
val topLine = getVisualLineAtTopOfScreen(editor)
|
val topLine = getVisualLineAtTopOfScreen(editor)
|
||||||
val bottomLine = getVisualLineAtBottomOfScreen(editor)
|
val bottomLine = getVisualLineAtBottomOfScreen(editor)
|
||||||
val lastLine = vimEditor.getVisualLineCount() - 1
|
val lastLine = vimEditor.getVisualLineCount() + editor.settings.additionalLinesCount
|
||||||
|
|
||||||
// We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred
|
// We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred
|
||||||
val scrollOffset = injector.options(vimEditor).scrolloff
|
val scrollOffset = injector.options(vimEditor).scrolloff
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.markup.HighlighterLayer
|
|||||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea
|
import com.intellij.openapi.editor.markup.HighlighterTargetArea
|
||||||
import com.intellij.openapi.editor.markup.RangeHighlighter
|
import com.intellij.openapi.editor.markup.RangeHighlighter
|
||||||
import com.intellij.openapi.editor.markup.TextAttributes
|
import com.intellij.openapi.editor.markup.TextAttributes
|
||||||
|
import com.intellij.util.application
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
import com.maddyhome.idea.vim.api.globalOptions
|
import com.maddyhome.idea.vim.api.globalOptions
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
@@ -28,8 +29,10 @@ import com.maddyhome.idea.vim.newapi.vim
|
|||||||
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
|
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
|
||||||
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||||
import org.jetbrains.annotations.Contract
|
import org.jetbrains.annotations.Contract
|
||||||
|
import java.awt.Color
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.swing.Timer
|
||||||
|
|
||||||
fun updateSearchHighlights(
|
fun updateSearchHighlights(
|
||||||
pattern: String?,
|
pattern: String?,
|
||||||
@@ -84,6 +87,12 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val removeHighlightsEditors = mutableListOf<Editor>()
|
||||||
|
val removeHighlightsTimer = Timer(450) {
|
||||||
|
removeHighlightsEditors.forEach(::removeSearchHighlights)
|
||||||
|
removeHighlightsEditors.clear()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refreshes current search highlights for all visible editors
|
* Refreshes current search highlights for all visible editors
|
||||||
*/
|
*/
|
||||||
@@ -125,27 +134,43 @@ private fun updateSearchHighlights(
|
|||||||
// hlsearch (+ incsearch/noincsearch)
|
// hlsearch (+ incsearch/noincsearch)
|
||||||
// Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given
|
// Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given
|
||||||
// `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows
|
// `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows
|
||||||
val vimEditor = editor.vim
|
val isSearching = injector.commandLine.getActiveCommandLine() != null
|
||||||
val editorLastLine = vimEditor.lineCount() - 1
|
application.invokeLater {
|
||||||
val searchStartLine = searchRange?.startLine ?: 0
|
val vimEditor = editor.vim
|
||||||
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
|
val editorLastLine = vimEditor.lineCount() - 1
|
||||||
if (searchStartLine <= editorLastLine) {
|
val searchStartLine = searchRange?.startLine ?: 0
|
||||||
val results =
|
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
|
||||||
injector.searchHelper.findAll(
|
if (searchStartLine <= editorLastLine) {
|
||||||
vimEditor,
|
val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished
|
||||||
pattern,
|
val visibleTopLeft = visibleArea.location
|
||||||
searchStartLine,
|
val visibleBottomRight = visibleArea.location.apply { translate(visibleArea.width, visibleArea.height) }
|
||||||
searchEndLine,
|
val visibleStartOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleTopLeft))
|
||||||
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
|
val visibleEndOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleBottomRight))
|
||||||
)
|
val visibleStartLine = editor.document.getLineNumber(visibleStartOffset)
|
||||||
if (results.isNotEmpty()) {
|
val visibleEndLine = editor.document.getLineNumber(visibleEndOffset)
|
||||||
if (editor === currentEditor?.ij) {
|
removeSearchHighlights(editor)
|
||||||
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
|
|
||||||
|
val results =
|
||||||
|
injector.searchHelper.findAll(
|
||||||
|
vimEditor,
|
||||||
|
pattern,
|
||||||
|
searchStartLine.coerceAtLeast(visibleStartLine),
|
||||||
|
searchEndLine.coerceAtMost(visibleEndLine),
|
||||||
|
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
|
||||||
|
)
|
||||||
|
if (results.isNotEmpty()) {
|
||||||
|
if (editor === currentEditor?.ij) {
|
||||||
|
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
|
||||||
|
}
|
||||||
|
highlightSearchResults(editor, results, currentMatchOffset)
|
||||||
|
if (!isSearching) {
|
||||||
|
removeHighlightsEditors.add(editor)
|
||||||
|
removeHighlightsTimer.restart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
highlightSearchResults(editor, pattern, results, currentMatchOffset)
|
|
||||||
}
|
}
|
||||||
|
editor.vimLastSearch = pattern
|
||||||
}
|
}
|
||||||
editor.vimLastSearch = pattern
|
|
||||||
} else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
|
} else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
|
||||||
// nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor
|
// nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor
|
||||||
// only), because 'incsearch' is active. But we don't show a search if Visual is active (behind Command-line of
|
// only), because 'incsearch' is active. But we don't show a search if Visual is active (behind Command-line of
|
||||||
@@ -160,7 +185,7 @@ private fun updateSearchHighlights(
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
|
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
|
||||||
val results = listOf(result)
|
val results = listOf(result)
|
||||||
highlightSearchResults(editor, pattern, results, result.startOffset)
|
highlightSearchResults(editor, results, result.startOffset)
|
||||||
}
|
}
|
||||||
currentMatchOffset = result.startOffset
|
currentMatchOffset = result.startOffset
|
||||||
}
|
}
|
||||||
@@ -179,6 +204,7 @@ private fun updateSearchHighlights(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeHighlightsTimer.restart()
|
||||||
return currentEditorCurrentMatchOffset
|
return currentEditorCurrentMatchOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +230,7 @@ private fun removeSearchHighlights(editor: Editor) {
|
|||||||
*/
|
*/
|
||||||
@Contract("_, _, false -> false; _, null, true -> false")
|
@Contract("_, _, false -> false; _, null, true -> false")
|
||||||
private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean {
|
private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean {
|
||||||
return hlSearch && newPattern != null && newPattern != editor.vimLastSearch && newPattern != ""
|
return hlSearch && newPattern != null && newPattern != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findClosestMatch(
|
private fun findClosestMatch(
|
||||||
@@ -240,9 +266,18 @@ private fun findClosestMatch(
|
|||||||
return sortedResults[nextIndex % results.size].startOffset
|
return sortedResults[nextIndex % results.size].startOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UseJBColor")
|
||||||
|
private val DEFAULT_RESULT_ATTRIBUTES = TextAttributes().apply {
|
||||||
|
backgroundColor = Color(50, 81, 61)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UseJBColor")
|
||||||
|
private val NEARBY_RESULT_ATTRIBUTES = TextAttributes().apply {
|
||||||
|
backgroundColor = Color(89, 80, 50)
|
||||||
|
}
|
||||||
|
|
||||||
fun highlightSearchResults(
|
fun highlightSearchResults(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
pattern: String,
|
|
||||||
results: List<TextRange>,
|
results: List<TextRange>,
|
||||||
currentMatchOffset: Int,
|
currentMatchOffset: Int,
|
||||||
) {
|
) {
|
||||||
@@ -251,38 +286,28 @@ fun highlightSearchResults(
|
|||||||
highlighters = mutableListOf()
|
highlighters = mutableListOf()
|
||||||
editor.vimLastHighlighters = highlighters
|
editor.vimLastHighlighters = highlighters
|
||||||
}
|
}
|
||||||
for (range in results) {
|
|
||||||
val current = range.startOffset == currentMatchOffset
|
val allCaretOffsets = editor.caretModel.allCarets.map { it.offset }
|
||||||
val highlighter = highlightMatch(editor, range.startOffset, range.endOffset, current, pattern)
|
|
||||||
highlighters.add(highlighter)
|
for ((index, range) in results.withIndex()) {
|
||||||
|
if (allCaretOffsets.any { range.startOffset == it }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val attributes = if (allCaretOffsets.any { (index > 0 && results[index - 1].startOffset == it) || (index < results.lastIndex && results[index + 1].startOffset == it) })
|
||||||
|
NEARBY_RESULT_ATTRIBUTES
|
||||||
|
else
|
||||||
|
DEFAULT_RESULT_ATTRIBUTES
|
||||||
|
|
||||||
|
highlighters.add(highlightMatch(editor, range.startOffset, range.endOffset, attributes))
|
||||||
}
|
}
|
||||||
editor.vimIncsearchCurrentMatchOffset = currentMatchOffset
|
editor.vimIncsearchCurrentMatchOffset = currentMatchOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun highlightMatch(editor: Editor, start: Int, end: Int, current: Boolean, tooltip: String): RangeHighlighter {
|
private fun highlightMatch(editor: Editor, start: Int, end: Int, attributes: TextAttributes): RangeHighlighter {
|
||||||
val layer = HighlighterLayer.SELECTION - 1
|
val layer = HighlighterLayer.SELECTION - 1
|
||||||
val targetArea = HighlighterTargetArea.EXACT_RANGE
|
val targetArea = HighlighterTargetArea.EXACT_RANGE
|
||||||
if (!current) {
|
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea)
|
||||||
// If we use a text attribute key, it will update automatically when the editor's colour scheme changes
|
|
||||||
val highlighter =
|
|
||||||
editor.markupModel.addRangeHighlighter(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES, start, end, layer, targetArea)
|
|
||||||
highlighter.errorStripeTooltip = tooltip
|
|
||||||
return highlighter
|
|
||||||
}
|
|
||||||
|
|
||||||
// There isn't a text attribute key for current selection. This means we won't update automatically when the editor's
|
|
||||||
// colour scheme changes. However, this is only used during incsearch, so it should be replaced pretty quickly. It's a
|
|
||||||
// small visual glitch that will fix itself quickly. Let's not bother implementing an editor colour scheme listener
|
|
||||||
// just for this.
|
|
||||||
// These are the same modifications that the Find live preview does. We could look at using LivePreviewPresentation,
|
|
||||||
// which might also be useful for text attributes in selection (if we supported that)
|
|
||||||
val attributes = editor.colorsScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES).clone().apply {
|
|
||||||
effectType = EffectType.ROUNDED_BOX
|
|
||||||
effectColor = editor.colorsScheme.getColor(EditorColors.CARET_COLOR)
|
|
||||||
}
|
|
||||||
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea).apply {
|
|
||||||
errorStripeTooltip = tooltip
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview
|
|||||||
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
|
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
|
||||||
import com.intellij.openapi.util.registry.Registry
|
import com.intellij.openapi.util.registry.Registry
|
||||||
import com.intellij.util.PlatformUtils
|
import com.intellij.util.PlatformUtils
|
||||||
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
import com.maddyhome.idea.vim.api.VimCaret
|
import com.maddyhome.idea.vim.api.VimCaret
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
@@ -28,6 +29,9 @@ import com.maddyhome.idea.vim.common.InsertSequence
|
|||||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
|
import com.maddyhome.idea.vim.state.mode.Mode
|
||||||
|
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||||
|
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +55,7 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
val textEditor = getTextEditor(editor.ij)
|
val textEditor = getTextEditor(editor.ij)
|
||||||
val undoManager = UndoManager.getInstance(project)
|
val undoManager = UndoManager.getInstance(project)
|
||||||
if (undoManager.isUndoAvailable(textEditor)) {
|
if (undoManager.isUndoAvailable(textEditor)) {
|
||||||
|
val caretCountBeforeUndo = editor.ij.caretModel.allCarets.size
|
||||||
val scrollingModel = editor.getScrollingModel()
|
val scrollingModel = editor.getScrollingModel()
|
||||||
scrollingModel.accumulateViewportChanges()
|
scrollingModel.accumulateViewportChanges()
|
||||||
|
|
||||||
@@ -58,6 +63,8 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
|
|
||||||
scrollingModel.flushViewportChanges()
|
scrollingModel.flushViewportChanges()
|
||||||
|
|
||||||
|
collapseRestoredBlockVisualCarets(editor, caretCountBeforeUndo)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -81,15 +88,7 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
|
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
|
||||||
editor.runWithChangeTracking {
|
editor.runWithChangeTracking {
|
||||||
undoManager.undo(fileEditor)
|
undoManager.undo(fileEditor)
|
||||||
|
restoreVisualMode(editor)
|
||||||
// We execute undo one more time if the previous one just restored selection
|
|
||||||
if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
|
|
||||||
undoManager.undo(fileEditor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandProcessor.getInstance().runUndoTransparentAction {
|
|
||||||
removeSelections(editor)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
|
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
|
||||||
@@ -196,6 +195,23 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIM-4112. IntelliJ's undo restores the pre-edit `CaretState`; for a block-visual edit that
|
||||||
|
* means one caret per block row. A 1 → N caret-count jump across undo uniquely identifies
|
||||||
|
* this, since [com.maddyhome.idea.vim.helper.exitVisualMode] is the only flow that collapses
|
||||||
|
* multi-carets to one. The remaining caret is placed at the block's top-left, matching Vim's
|
||||||
|
* convention of cursor-at-start-of-undone-change.
|
||||||
|
*/
|
||||||
|
private fun collapseRestoredBlockVisualCarets(editor: VimEditor, caretCountBeforeUndo: Int) {
|
||||||
|
val caretModel = editor.ij.caretModel
|
||||||
|
val restoredExtraCarets = caretCountBeforeUndo == 1 && caretModel.allCarets.size > 1
|
||||||
|
if (!restoredExtraCarets || editor.mode !is Mode.NORMAL) return
|
||||||
|
|
||||||
|
val blockTopOffset = caretModel.allCarets.minOf { it.offset }
|
||||||
|
caretModel.removeSecondaryCarets()
|
||||||
|
caretModel.primaryCaret.moveToOffset(blockTopOffset)
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeSelections(editor: VimEditor) {
|
private fun removeSelections(editor: VimEditor) {
|
||||||
editor.carets().forEach {
|
editor.carets().forEach {
|
||||||
val ijCaret = it.ij
|
val ijCaret = it.ij
|
||||||
@@ -240,4 +256,21 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
val hasChanges: Boolean
|
val hasChanges: Boolean
|
||||||
get() = changeListener.hasChanged || initialPath != editor.getPath()
|
get() = changeListener.hasChanged || initialPath != editor.getPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restoreVisualMode(editor: VimEditor) {
|
||||||
|
if (!editor.inVisualMode && editor.ij.selectionModel.hasSelection()) {
|
||||||
|
val detectedMode = VimPlugin.getVisualMotion().detectSelectionType(editor)
|
||||||
|
|
||||||
|
// Visual block selection is restored into multiple carets, so multi-carets that form a block are always
|
||||||
|
// identified as visual block mode, leading to false positives.
|
||||||
|
// Since I use visual block mode much less often than multi-carets, this is a judgment call to never restore
|
||||||
|
// visual block mode.
|
||||||
|
val wantedMode = if (detectedMode == SelectionType.BLOCK_WISE)
|
||||||
|
SelectionType.CHARACTER_WISE
|
||||||
|
else
|
||||||
|
detectedMode
|
||||||
|
|
||||||
|
VimPlugin.getVisualMotion().enterVisualMode(editor, wantedMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import com.intellij.openapi.editor.VisualPosition
|
|||||||
import com.intellij.openapi.editor.markup.RangeHighlighter
|
import com.intellij.openapi.editor.markup.RangeHighlighter
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.openapi.util.UserDataHolder
|
import com.intellij.openapi.util.UserDataHolder
|
||||||
import com.maddyhome.idea.vim.api.CaretRegisterStorageBase
|
|
||||||
import com.maddyhome.idea.vim.api.LocalMarkStorage
|
import com.maddyhome.idea.vim.api.LocalMarkStorage
|
||||||
import com.maddyhome.idea.vim.api.SelectionInfo
|
import com.maddyhome.idea.vim.api.SelectionInfo
|
||||||
import com.maddyhome.idea.vim.api.VimOutputPanel
|
import com.maddyhome.idea.vim.api.VimOutputPanel
|
||||||
@@ -97,9 +96,8 @@ var Caret.vimInsertStart: RangeMarker by userDataOr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Data could be lost during visual block motion
|
// TODO: Data could be lost during visual block motion
|
||||||
var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor()
|
internal var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
|
||||||
var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
|
internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
|
||||||
var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
|
|
||||||
|
|
||||||
var Editor.vimInitialised: Boolean by userDataOr { false }
|
var Editor.vimInitialised: Boolean by userDataOr { false }
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ class IJEditorFocusListener : EditorListener {
|
|||||||
VimPlugin.getChange().insertBeforeCaret(editor, context)
|
VimPlugin.getChange().insertBeforeCaret(editor, context)
|
||||||
KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true)
|
KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true)
|
||||||
}
|
}
|
||||||
if (isCurrentEditorTerminal && !ijEditor.inInsertMode) {
|
if (isCurrentEditorTerminal) {
|
||||||
switchToInsertMode.run()
|
if (!ijEditor.inInsertMode) {
|
||||||
|
switchToInsertMode.run()
|
||||||
|
}
|
||||||
} else if (ijEditor.isInsertMode && (oldEditorInfo.isInsertModeForced || !ijEditor.document.isWritable)) {
|
} else if (ijEditor.isInsertMode && (oldEditorInfo.isInsertModeForced || !ijEditor.document.isWritable)) {
|
||||||
val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor)
|
val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor)
|
||||||
val mode = injector.vimState.mode
|
val mode = injector.vimState.mode
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import com.intellij.codeInsight.template.Template
|
|||||||
import com.intellij.codeInsight.template.TemplateEditingAdapter
|
import com.intellij.codeInsight.template.TemplateEditingAdapter
|
||||||
import com.intellij.codeInsight.template.TemplateManagerListener
|
import com.intellij.codeInsight.template.TemplateManagerListener
|
||||||
import com.intellij.codeInsight.template.impl.TemplateImpl
|
import com.intellij.codeInsight.template.impl.TemplateImpl
|
||||||
|
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
|
||||||
import com.intellij.codeInsight.template.impl.TemplateState
|
import com.intellij.codeInsight.template.impl.TemplateState
|
||||||
|
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
|
||||||
import com.intellij.find.FindModelListener
|
import com.intellij.find.FindModelListener
|
||||||
import com.intellij.ide.actions.ApplyIntentionAction
|
import com.intellij.ide.actions.ApplyIntentionAction
|
||||||
import com.intellij.openapi.actionSystem.ActionManager
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
@@ -33,6 +35,7 @@ import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
|
|||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.RangeMarker
|
import com.intellij.openapi.editor.RangeMarker
|
||||||
import com.intellij.openapi.editor.actions.EnterAction
|
import com.intellij.openapi.editor.actions.EnterAction
|
||||||
|
import com.intellij.openapi.editor.impl.ScrollingModelImpl
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||||
import com.intellij.openapi.keymap.KeymapManager
|
import com.intellij.openapi.keymap.KeymapManager
|
||||||
import com.intellij.openapi.project.DumbAwareToggleAction
|
import com.intellij.openapi.project.DumbAwareToggleAction
|
||||||
@@ -48,7 +51,6 @@ import com.maddyhome.idea.vim.group.NotificationService
|
|||||||
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
|
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
|
||||||
import com.maddyhome.idea.vim.helper.exitSelectMode
|
import com.maddyhome.idea.vim.helper.exitSelectMode
|
||||||
import com.maddyhome.idea.vim.helper.exitVisualMode
|
import com.maddyhome.idea.vim.helper.exitVisualMode
|
||||||
import com.maddyhome.idea.vim.helper.hasVisualSelection
|
|
||||||
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
|
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
|
||||||
import com.maddyhome.idea.vim.ide.isClionNova
|
import com.maddyhome.idea.vim.ide.isClionNova
|
||||||
import com.maddyhome.idea.vim.ide.isRider
|
import com.maddyhome.idea.vim.ide.isRider
|
||||||
@@ -62,6 +64,7 @@ import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHe
|
|||||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
|
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
|
||||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
|
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
|
||||||
import org.jetbrains.annotations.NonNls
|
import org.jetbrains.annotations.NonNls
|
||||||
|
import java.awt.AWTEvent
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@ internal object IdeaSpecifics {
|
|||||||
private val surrounderAction =
|
private val surrounderAction =
|
||||||
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
|
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
|
||||||
private var editor: Editor? = null
|
private var editor: Editor? = null
|
||||||
|
private var caretOffset = -1
|
||||||
private var completionData: CompletionData? = null
|
private var completionData: CompletionData? = null
|
||||||
|
|
||||||
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
|
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
|
||||||
@@ -83,6 +87,7 @@ internal object IdeaSpecifics {
|
|||||||
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
|
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
|
||||||
if (hostEditor != null) {
|
if (hostEditor != null) {
|
||||||
editor = hostEditor
|
editor = hostEditor
|
||||||
|
caretOffset = hostEditor.caretModel.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
val actionId = ActionManager.getInstance().getId(action)
|
val actionId = ActionManager.getInstance().getId(action)
|
||||||
@@ -164,26 +169,46 @@ internal object IdeaSpecifics {
|
|||||||
if (VimPlugin.isNotEnabled()) return
|
if (VimPlugin.isNotEnabled()) return
|
||||||
|
|
||||||
val editor = editor
|
val editor = editor
|
||||||
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
|
if (editor != null) {
|
||||||
completionData?.recordCompletion(editor, VimPlugin.getRegister())
|
if (action is ChooseItemAction && injector.registerGroup.isRecording) {
|
||||||
}
|
completionData?.recordCompletion(editor, VimPlugin.getRegister()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//region Enter insert mode after surround with if
|
//region Enter insert mode after surround with if
|
||||||
if (surrounderAction == action.javaClass.name && surrounderItems.any {
|
if (surrounderAction == action.javaClass.name && surrounderItems.any {
|
||||||
action.templatePresentation.text.endsWith(
|
action.templatePresentation.text.endsWith(
|
||||||
it,
|
it,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
editor?.let {
|
||||||
|
it.vim.mode = Mode.NORMAL()
|
||||||
|
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim)
|
||||||
|
KeyHandler.getInstance().reset(it.vim)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) {
|
else if (action is NextVariableAction && TemplateManagerImpl.getTemplateState(editor) == null) {
|
||||||
editor?.let {
|
editor.vim.exitInsertMode(event.dataContext.vim)
|
||||||
it.vim.mode = Mode.NORMAL()
|
KeyHandler.getInstance().reset(editor.vim)
|
||||||
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim)
|
}
|
||||||
KeyHandler.getInstance().reset(it.vim)
|
//endregion
|
||||||
|
|
||||||
|
if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {
|
||||||
|
val scrollModel = editor.scrollingModel as ScrollingModelImpl
|
||||||
|
if (scrollModel.isScrollingNow) {
|
||||||
|
val v = scrollModel.verticalScrollOffset
|
||||||
|
val h = scrollModel.horizontalScrollOffset
|
||||||
|
scrollModel.finishAnimation()
|
||||||
|
scrollModel.scroll(h, v)
|
||||||
|
scrollModel.finishAnimation()
|
||||||
|
}
|
||||||
|
injector.scroll.scrollCaretIntoView(editor.vim)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
|
||||||
|
|
||||||
this.editor = null
|
this.editor = null
|
||||||
|
this.caretOffset = -1
|
||||||
|
|
||||||
this.completionData?.dispose()
|
this.completionData?.dispose()
|
||||||
this.completionData = null
|
this.completionData = null
|
||||||
@@ -331,23 +356,6 @@ internal object IdeaSpecifics {
|
|||||||
vimEditor.exitMode()
|
vimEditor.exitMode()
|
||||||
vimEditor.mode = Mode.NORMAL()
|
vimEditor.mode = Mode.NORMAL()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// IdeaSelectionControl will not be called if we're moving to a new variable with no change in selection.
|
|
||||||
// And if we're moving to the end of the template, the change in selection will reset us to Normal because
|
|
||||||
// IdeaSelectionControl will be called when the template is no longer active.
|
|
||||||
if ((!editor.selectionModel.hasSelection() && !vimEditor.mode.hasVisualSelection) || newIndex == -1) {
|
|
||||||
if (vimEditor.isIdeaRefactorModeSelect) {
|
|
||||||
if (vimEditor.mode !is Mode.INSERT) {
|
|
||||||
vimEditor.exitMode()
|
|
||||||
injector.application.runReadAction {
|
|
||||||
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
|
|
||||||
VimPlugin.getChange().insertBeforeCaret(editor.vim, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vimEditor.mode = Mode.NORMAL()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,12 +375,11 @@ internal object IdeaSpecifics {
|
|||||||
|
|
||||||
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
|
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
|
||||||
|
|
||||||
// In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
|
// In Rider/CLion Nova, the popup manager (due to LookupSummaryInfo parameter info popup)
|
||||||
// (due to LookupSummaryInfo popup) before the action system runs, so IdeaVim never sees it.
|
// consumes Escape before the action system runs, so IdeaVim never sees it.
|
||||||
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
|
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
|
||||||
// Note: we check isRider/isClionNova specifically, not !isOctopusEnabled(), because
|
// Note: this listener must NOT be attached in JetBrains Client (split mode), because
|
||||||
// JetBrains Client (split mode) also has octopus disabled but doesn't need this workaround,
|
// isCanceledExplicitly can be true for non-Escape keys (e.g. space) there.
|
||||||
// and isCanceledExplicitly can be true for non-Escape keys (e.g. space) in that environment.
|
|
||||||
if (isRider() || isClionNova()) {
|
if (isRider() || isClionNova()) {
|
||||||
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
|
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
|
||||||
}
|
}
|
||||||
@@ -389,13 +396,37 @@ internal object IdeaSpecifics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
|
* Tracks whether the last KEY_PRESSED was Escape. Needed because [LookupEvent.isCanceledExplicitly]
|
||||||
* (due to LookupSummaryInfo parameter info popup) before the action system runs, so IdeaVim never sees it.
|
* is also true for non-Esc keys in Rider/CLion Nova (e.g. space), so it can't be used on its own
|
||||||
|
* to decide whether to exit insert mode. Wired up as an IdeEventQueue preprocessor in
|
||||||
|
* [VimListenerManager.GlobalListeners.enable].
|
||||||
|
*/
|
||||||
|
internal object RiderEscAwtKeyTracker {
|
||||||
|
private val LOG = com.intellij.openapi.diagnostic.Logger.getInstance(RiderEscAwtKeyTracker::class.java)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var lastKeyPressedWasEscape: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun onAwtEvent(event: AWTEvent) {
|
||||||
|
if (event is KeyEvent && event.id == KeyEvent.KEY_PRESSED) {
|
||||||
|
val isEsc = event.keyCode == KeyEvent.VK_ESCAPE
|
||||||
|
lastKeyPressedWasEscape = isEsc
|
||||||
|
if (LOG.isTraceEnabled) {
|
||||||
|
LOG.trace("RiderEscAwtKeyTracker KEY_PRESSED keyCode=${event.keyCode} isEsc=$isEsc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In Rider/CLion Nova, the popup manager (due to LookupSummaryInfo parameter info popup)
|
||||||
|
* consumes Escape before the action system runs, so IdeaVim never sees it.
|
||||||
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
|
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
|
||||||
*/
|
*/
|
||||||
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
|
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
|
||||||
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
|
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
|
||||||
if (event.isCanceledExplicitly && editor.vim.mode is Mode.INSERT) {
|
if (RiderEscAwtKeyTracker.lastKeyPressedWasEscape && editor.vim.mode is Mode.INSERT) {
|
||||||
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
|
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
|
||||||
KeyHandler.getInstance().reset(editor.vim)
|
KeyHandler.getInstance().reset(editor.vim)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user