1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2026-03-30 21:52:36 +02:00

Compare commits

..

29 Commits

Author SHA1 Message Date
7b813899f0 Set plugin version to chylex-53 2026-03-29 19:45:23 +02:00
727dee5b85 Preserve visual mode after executing IDE action 2026-03-29 19:45:23 +02:00
3e7ea8668c Make g0/g^/g$ work with soft wraps 2026-03-29 19:45:22 +02:00
89f7c76180 Make gj/gk jump over soft wraps 2026-03-29 19:45:22 +02:00
a7d0297e2d Make camelCase motions adjust based on direction of visual selection 2026-03-29 19:45:22 +02:00
45da61debe Make search highlights temporary 2026-03-29 19:45:22 +02:00
ebc77454ab Exit insert mode after refactoring 2026-03-29 19:45:22 +02:00
c9193cb6d4 Add action to run last macro in all opened files 2026-03-29 19:45:22 +02:00
13246c0a80 Stop macro execution after a failed search 2026-03-29 19:45:22 +02:00
b0ff57a4f5 Revert per-caret registers 2026-03-29 19:45:22 +02:00
f4e0684ca8 Apply scrolloff after executing native IDEA actions 2026-03-29 19:45:22 +02:00
3a3e7952b1 Automatically add unambiguous imports after running a macro 2026-03-29 19:45:22 +02:00
1ff6066e33 Fix(VIM-3986): Exception when pasting register contents containing new line 2026-03-29 19:45:21 +02:00
3a9abba410 Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2026-03-29 19:45:21 +02:00
510f8f948e Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2026-03-29 19:45:21 +02:00
b623bf739c Update search register when using f/t 2026-03-29 19:45:21 +02:00
c99d97b3bc Add support for count for visual and line motion surround 2026-03-29 19:45:21 +02:00
6b8eb8952f Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2026-03-29 19:45:21 +02:00
25d70ee975 Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2026-03-29 19:45:21 +02:00
cbc9637d17 Respect count with <Action> mappings 2026-03-29 19:45:21 +02:00
0d893d9961 Change matchit plugin to use HTML patterns in unrecognized files 2026-03-29 19:45:21 +02:00
4ac3a1eaaa Fix ex command panel causing Undock tool window to hide 2026-03-29 19:45:21 +02:00
86a6e9643f Reset insert mode when switching active editor 2026-03-29 19:45:21 +02:00
8b06078607 Remove notifications about configuration options 2026-03-29 19:45:20 +02:00
924455907a Remove AI 2026-03-29 19:45:20 +02:00
40367859b8 Set custom plugin version 2026-03-29 19:45:20 +02:00
45f7934d71 Revert "Fix(VIM-4108): Use default ANTLR output directory for Gradle 9+ compatibility"
This reverts commit a476583ea3.
2026-03-27 21:40:07 +01:00
0880e5f935 Revert "Upgrade Gradle wrapper to 9.2.1"
This reverts commit 517bda93
2026-03-27 21:40:07 +01:00
8af3788379 Revert "Fix(VIM-4109): Configure test source sets for Gradle 9+ compatibility"
This reverts commit 5c0d9569d9.
2026-03-27 21:40:07 +01:00
106 changed files with 972 additions and 2567 deletions

40
.beads/.gitignore vendored
View File

@@ -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.

View File

@@ -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*

View File

@@ -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

View File

@@ -1,4 +0,0 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl"
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -1,5 +0,0 @@
{
"enabledPlugins": {
"context7@claude-plugins-official": true
}
}

View File

@@ -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**

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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 |

View File

@@ -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
View File

@@ -0,0 +1 @@
* text=auto eol=lf

4
.gitignore vendored
View File

@@ -26,6 +26,10 @@
.teamcity/target
.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
settings.xml

2
.idea/misc.xml generated
View File

@@ -18,5 +18,5 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
</project>

View File

@@ -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

View File

@@ -112,6 +112,8 @@ dependencies {
testFramework(TestFrameworkType.Platform)
testFramework(TestFrameworkType.JUnit5)
plugin("com.intellij.classic.ui", "261.22158.185")
pluginModule(runtimeOnly(project(":modules:ideavim-common")))
pluginModule(runtimeOnly(project(":modules:ideavim-frontend")))
pluginModule(runtimeOnly(project(":modules:ideavim-backend")))
@@ -207,6 +209,7 @@ tasks {
// a custom task (see below)
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
@@ -300,11 +303,6 @@ tasks {
}
})
}
buildPlugin {
dependsOn(sourcesJar)
from(sourcesJar) { into("lib/src") }
}
}
java {
@@ -377,13 +375,7 @@ intellijPlatform {
)
ideaVersion {
// Let the Gradle plugin set the since-build version. It defaults to the version of the IDE we're building against
// specified as two components, `{branch}.{build}` (e.g., "241.15989"). There is no third component specified.
// The until-build version defaults to `{branch}.*`, but we want to support _all_ future versions, so we set it
// with a null provider (the provider is important).
// By letting the Gradle plugin handle this, the Plugin DevKit IntelliJ plugin cannot help us with the "Usage of
// IntelliJ API not available in older IDEs" inspection. However, since our since-build is the version we compile
// against, we can never get an API that's newer - it would be an unresolved symbol.
sinceBuild.set("253")
untilBuild.set(provider { null })
}
}

View File

@@ -16,11 +16,11 @@
# https://data.services.jetbrains.com/products?code=IU
# Maven releases are here: https://www.jetbrains.com/intellij-repository/releases
# And snapshots: https://www.jetbrains.com/intellij-repository/snapshots
ideaVersion=2025.3
ideaVersion=2026.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IU
instrumentPluginCode=true
version=SNAPSHOT
version=chylex-53
javaVersion=21
remoteRobotVersion=0.11.23
antlrVersion=4.10.1
@@ -42,7 +42,6 @@ youtrackToken=
# Gradle settings
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.configuration-cache=true
org.gradle.caching=true
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -311,6 +311,7 @@
</group>
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
</actions>
<!-- Frontend vim extensions (editor/text manipulation, no PSI/file-system dependency) -->

View File

@@ -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
}
}

View File

@@ -220,7 +220,7 @@ object VimExtensionFacade {
caret: ImmutableVimCaret,
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 */

View File

@@ -21,9 +21,7 @@ import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.Disposer
import com.intellij.util.Alarm
import com.intellij.util.Alarm.ThreadToUse
import com.jetbrains.rd.util.first
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.injector
import com.maddyhome.idea.vim.common.ModeChangeListener
@@ -123,9 +121,9 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
initialised = false
}
override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
override fun yankPerformed(editor: VimEditor, range: TextRange) {
ensureInitialised()
highlightHandler.highlightYankRange(caretToRange)
highlightHandler.highlightYankRange(editor.ij, range)
}
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
@@ -146,25 +144,22 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
private var lastEditor: Editor? = null
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
clearYankHighlighters()
val editor = caretToRange.first().key.editor.ij
lastEditor = editor
val attributes = getHighlightTextAttributes(editor)
for (range in caretToRange.values) {
for (i in 0 until range.size()) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.startOffsets[i],
range.endOffsets[i],
HighlighterLayer.SELECTION,
attributes,
HighlighterTargetArea.EXACT_RANGE,
)
highlighters.add(highlighter)
}
for (i in 0 until range.size()) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.startOffsets[i],
range.endOffsets[i],
HighlighterLayer.SELECTION,
attributes,
HighlighterTargetArea.EXACT_RANGE,
)
highlighters.add(highlighter)
}
// from vim-highlightedyank docs: A negative number makes the highlight persistent.

View File

@@ -268,7 +268,7 @@ private object FileTypePatterns {
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
this.cMakePatterns
} else {
return null
this.htmlPatterns
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.endsWithNewLine
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.group.findBlockRange
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.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
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 {
@@ -166,7 +170,7 @@ internal class VimSurroundExtension : VimExtension {
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
if (lastNonWhiteSpaceOffset != null) {
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
performSurround(pair, range, it)
performSurround(pair, range, it, count = operatorArguments.count1)
}
// it.moveToOffset(lineStartOffset)
}
@@ -189,15 +193,13 @@ internal class VimSurroundExtension : VimExtension {
private class VSurroundHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// 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
}
runWriteAction {
// Leave visual mode
editor.exitVisualMode()
editor.ij.caretModel.moveToOffset(selectionStart)
// 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
@@ -220,6 +222,10 @@ internal class VimSurroundExtension : VimExtension {
companion object {
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
val surroundings = editor.sortedCarets()
.map {
@@ -263,7 +269,7 @@ internal class VimSurroundExtension : VimExtension {
it.first + trimmedValue + it.second
} ?: innerValue
val textData =
PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.CHARACTER_WISE)
PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), null)
val putData =
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 {
val ijEditor = editor.ij
val c = injector.keyGroup.getChar(editor) ?: return true
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
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
runWriteAction {
val change = VimPlugin.getChange()
if (supportsMultipleCursors) {
ijEditor.runWithEveryCaretAndRestore {
applyOnce(ijEditor, change, pair, count)
}
}
else {
applyOnce(ijEditor, change, pair, count)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
}
}
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? {
val editor = caret.editor
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 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 hasNewLine = editor.endsWithNewLine()
val rightSurround = if (tagsOnNewLines) {
val rightSurround = (if (tagsOnNewLines) {
if (isEOF && !hasNewLine) {
"\n" + pair.second
} else {
@@ -459,7 +486,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare
}
} else {
pair.second
}
}).let { RepeatedCharSequence.of(it, count) }
change.insertText(editor, caret, range.startOffset, leftSurround)
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)

View File

@@ -141,7 +141,7 @@ object IjOptions {
// Temporary feature flags during development, not really intended for external use
val closenotebooks: ToggleOption =
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))
// This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which
// derives from Option<VimInt>

View File

@@ -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 }
}
}
}
})
}
}

View File

@@ -92,6 +92,9 @@ class MacroGroup : VimMacroBase() {
} finally {
keyStack.removeFirst()
}
if (!isInternalMacro) {
MacroAutoImport.run(editor.ij, context.ij)
}
}
if (isInternalMacro) {

View File

@@ -86,6 +86,9 @@ class MotionGroup : VimMotionGroupBase() {
}
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)
return moveCaretToColumn(editor, caret, col, false)
}
@@ -94,6 +97,15 @@ class MotionGroup : VimMotionGroupBase() {
editor: VimEditor,
caret: ImmutableVimCaret,
): @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 bufferLine = caret.getLine()
return editor.getLeadingCharacterOffset(bufferLine, col)
@@ -104,6 +116,9 @@ class MotionGroup : VimMotionGroupBase() {
caret: ImmutableVimCaret,
allowEnd: Boolean,
): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineEnd - 1)
}
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, allowEnd)
}

View File

@@ -32,7 +32,6 @@ import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.SystemInfo
import com.maddyhome.idea.vim.VimPlugin
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.handler.KeyMapIssue
import com.maddyhome.idea.vim.helper.MessageHelper
@@ -40,8 +39,6 @@ import com.maddyhome.idea.vim.icons.VimIcons
import com.maddyhome.idea.vim.key.ShortcutOwner
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
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.ui.VimEmulationConfigurable
import com.maddyhome.idea.vim.vimscript.services.VimRcService
@@ -64,55 +61,9 @@ internal class NotificationService(private val project: Project?) : VimNotificat
@Suppress("unused")
constructor() : this(null)
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,
)
override fun notifyAboutIdeaPut() {}
notification.addAction(OpenIdeaVimRcAction(notification))
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 notifyAboutIdeaJoin(editor: VimEditor) {}
override fun enableRepeatingMode() = Messages.showYesNoDialog(
"Do you want to enable repeating keys in macOS on press and hold?\n\n" +

View File

@@ -24,10 +24,9 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
/**
* 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);
if (text != null) {
logger.trace("Register data parsed");
register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type);
register = new Register(key, type, text, Collections.emptyList());
}
else {
logger.trace("Cannot parse register data");

View File

@@ -37,7 +37,6 @@ import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.ide.isRider
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
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.ij
import com.maddyhome.idea.vim.newapi.vim
@@ -127,7 +126,7 @@ internal class PutGroup : VimPutBase() {
point.dispose()
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) {
doIndent(
vimEditor,
@@ -179,10 +178,12 @@ internal class PutGroup : VimPutBase() {
val allContentsBefore = CopyPasteManager.getInstance().allContents
val sizeBeforeInsert = allContentsBefore.size
val firstItemBefore = allContentsBefore.firstOrNull()
logger.debug { "Copied text: ${text.copiedText}" }
val (textContent, transferableData) = text.copiedText as IjVimCopiedText
logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" }
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 sizeAfterInsert = allContentsAfter.size
try {
@@ -190,7 +191,7 @@ internal class PutGroup : VimPutBase() {
} finally {
val textInClipboard = (firstItemBefore as? TextBlockTransferable)
?.getTransferData(DataFlavor.stringFlavor) as? String
val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.text
val textOnTop = textInClipboard != null && textInClipboard != text.text
if (sizeBeforeInsert != sizeAfterInsert || textOnTop) {
// Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }

View File

@@ -351,7 +351,7 @@ public class EditorHelper {
final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
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);
// For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen.

View File

@@ -12,7 +12,9 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.util.ui.table.JBTableRowEditor
@@ -22,6 +24,8 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
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 javax.swing.JComponent
import javax.swing.JTable
@@ -111,8 +115,7 @@ internal fun Editor.isPrimaryEditor(): Boolean {
internal fun Editor.isTerminalEditor(): Boolean {
return !isViewer
&& document.isWritable
&& !EditorHelper.isFileEditor(this)
&& !EditorHelper.isDiffEditor(this)
&& this.editorKind == EditorKind.CONSOLE
}
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
@@ -141,3 +144,41 @@ private fun vimEnabled(editor: Editor?): Boolean {
if (editor != null && editor.isIdeaVimDisabledHere) return false
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
}
}

View File

@@ -24,15 +24,19 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.util.NlsContexts
import com.intellij.refactoring.actions.BaseRefactoringAction
import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction
import com.maddyhome.idea.vim.api.VimActionExecutor
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.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.newapi.IjNativeAction
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 java.awt.Component
import javax.swing.JComponent
@@ -68,6 +72,12 @@ class IjActionExecutor : VimActionExecutor {
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
try {
isRunningActionFromVim = true
@@ -77,6 +87,20 @@ class IjActionExecutor : VimActionExecutor {
val place = ijAction.choosePlace()
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
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
} finally {
isRunningActionFromVim = false

View File

@@ -58,7 +58,7 @@ internal object ScrollViewHelper {
// that this needs to be replaced as a more or less dumb line for line rewrite.
val topLine = getVisualLineAtTopOfScreen(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
val scrollOffset = injector.options(vimEditor).scrolloff

View File

@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.application
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
@@ -30,6 +31,7 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.Contract
import java.awt.Font
import java.util.*
import javax.swing.Timer
fun updateSearchHighlights(
pattern: String?,
@@ -84,6 +86,12 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
)
}
val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = Timer(400) {
removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear()
}
/**
* Refreshes current search highlights for all visible editors
*/
@@ -125,27 +133,43 @@ private fun updateSearchHighlights(
// hlsearch (+ incsearch/noincsearch)
// 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
val vimEditor = editor.vim
val editorLastLine = vimEditor.lineCount() - 1
val searchStartLine = searchRange?.startLine ?: 0
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
if (searchStartLine <= editorLastLine) {
val results =
injector.searchHelper.findAll(
vimEditor,
pattern,
searchStartLine,
searchEndLine,
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
)
if (results.isNotEmpty()) {
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
val isSearching = injector.commandLine.getActiveCommandLine() != null
application.invokeLater {
val vimEditor = editor.vim
val editorLastLine = vimEditor.lineCount() - 1
val searchStartLine = searchRange?.startLine ?: 0
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
if (searchStartLine <= editorLastLine) {
val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished
val visibleTopLeft = visibleArea.location
val visibleBottomRight = visibleArea.location.apply { translate(visibleArea.width, visibleArea.height) }
val visibleStartOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleTopLeft))
val visibleEndOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleBottomRight))
val visibleStartLine = editor.document.getLineNumber(visibleStartOffset)
val visibleEndLine = editor.document.getLineNumber(visibleEndOffset)
removeSearchHighlights(editor)
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, pattern, 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)) {
// 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
@@ -179,6 +203,7 @@ private fun updateSearchHighlights(
}
}
removeHighlightsTimer.restart()
return currentEditorCurrentMatchOffset
}
@@ -204,7 +229,7 @@ private fun removeSearchHighlights(editor: Editor) {
*/
@Contract("_, _, false -> false; _, null, true -> false")
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(

View File

@@ -19,6 +19,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.PlatformUtils
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
@@ -28,6 +29,8 @@ import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
/**
@@ -81,15 +84,7 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking {
undoManager.undo(fileEditor)
// 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)
restoreVisualMode(editor)
}
} else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
@@ -240,4 +235,21 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
val hasChanges: Boolean
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)
}
}
}

View File

@@ -18,7 +18,6 @@ import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key
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.SelectionInfo
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
var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor()
var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
internal var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
var Editor.vimInitialised: Boolean by userDataOr { false }

View File

@@ -64,8 +64,10 @@ class IJEditorFocusListener : EditorListener {
VimPlugin.getChange().insertBeforeCaret(editor, context)
KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true)
}
if (isCurrentEditorTerminal && !ijEditor.inInsertMode) {
switchToInsertMode.run()
if (isCurrentEditorTerminal) {
if (!ijEditor.inInsertMode) {
switchToInsertMode.run()
}
} else if (ijEditor.isInsertMode && (oldEditorInfo.isInsertModeForced || !ijEditor.document.isWritable)) {
val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor)
val mode = injector.vimState.mode

View File

@@ -17,7 +17,9 @@ import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction
import com.intellij.openapi.actionSystem.ActionManager
@@ -32,6 +34,7 @@ import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange
@@ -71,6 +74,7 @@ internal object IdeaSpecifics {
private val surrounderAction =
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
private var editor: Editor? = null
private var caretOffset = -1
private var completionData: CompletionData? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
@@ -79,6 +83,7 @@ internal object IdeaSpecifics {
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) {
editor = hostEditor
caretOffset = hostEditor.caretModel.offset
}
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
@@ -155,26 +160,46 @@ internal object IdeaSpecifics {
if (VimPlugin.isNotEnabled()) return
val editor = editor
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
completionData?.recordCompletion(editor, VimPlugin.getRegister())
}
if (editor != null) {
if (action is ChooseItemAction && injector.registerGroup.isRecording) {
completionData?.recordCompletion(editor, VimPlugin.getRegister()
)
}
//region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith(
it,
)
//region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith(
it,
)
}
) {
editor?.let {
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim)
KeyHandler.getInstance().reset(it.vim)
}
}
) {
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.vim.exitInsertMode(event.dataContext.vim)
KeyHandler.getInstance().reset(editor.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.caretOffset = -1
this.completionData?.dispose()
this.completionData = null

View File

@@ -98,6 +98,7 @@ import com.maddyhome.idea.vim.newapi.IjVimSearchGroup
import com.maddyhome.idea.vim.newapi.InsertTimeRecorder
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
@@ -412,10 +413,21 @@ object VimListenerManager {
// We can't rely on being passed a non-null editor, so check for Code With Me scenarios explicitly
if (VimPlugin.isNotEnabled() || !ClientId.isCurrentlyUnderLocalId) return
val newEditor = event.newEditor
if (newEditor is TextEditor) {
val editor = newEditor.editor
if (editor.isInsertMode) {
editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim)
}
// Breaks relativenumber for some reason
// injector.scroll.scrollCaretIntoView(editor.vim)
}
injector.outputPanel.getCurrentOutputPanel()?.close()
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
(VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
// (VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
}

View File

@@ -38,7 +38,7 @@ import java.io.IOException
internal class IjClipboardManager : VimClipboardManager {
override fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): IjVimCopiedText? {
override fun getPrimaryContent(): IjVimCopiedText? {
val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null
val contents = clipboard.getContents(null) ?: return null
val (text, transferableData) = getTextAndTransferableData(contents) ?: return null
@@ -241,6 +241,6 @@ internal class IjClipboardManager : VimClipboardManager {
}
}
data class IjVimCopiedText(override val text: String, val transferableData: List<Any>) : VimCopiedText {
data class IjVimCopiedText(override val text: String, override val transferableData: List<Any>) : VimCopiedText {
override fun updateText(newText: String): VimCopiedText = IjVimCopiedText(newText, transferableData)
}

View File

@@ -12,14 +12,13 @@ import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.VisualPosition
import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.CaretRegisterStorage
import com.maddyhome.idea.vim.api.CaretRegisterStorageBase
import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretBase
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.group.visual.VisualChange
@@ -28,7 +27,6 @@ import com.maddyhome.idea.vim.helper.insertHistory
import com.maddyhome.idea.vim.helper.lastSelectionInfo
import com.maddyhome.idea.vim.helper.markStorage
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
import com.maddyhome.idea.vim.helper.registerStorage
import com.maddyhome.idea.vim.helper.resetVimLastColumn
import com.maddyhome.idea.vim.helper.vimInsertStart
import com.maddyhome.idea.vim.helper.vimLastColumn
@@ -36,22 +34,14 @@ import com.maddyhome.idea.vim.helper.vimLastVisualOperatorRange
import com.maddyhome.idea.vim.helper.vimLine
import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimSelectionStartClear
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.SelectionType
class IjVimCaret(val caret: Caret) : VimCaretBase() {
override val registerStorage: CaretRegisterStorage
get() {
var storage = this.caret.registerStorage
if (storage == null) {
initInjector() // To initialize injector used in CaretRegisterStorageBase
storage = CaretRegisterStorageBase(this)
this.caret.registerStorage = storage
} else if (storage.caret != this) {
storage.caret = this
}
return storage
}
override val registerStorage: VimRegisterGroup
get() = injector.registerGroup
override val markStorage: LocalMarkStorage
get() {
var storage = this.caret.markStorage

View File

@@ -22,6 +22,7 @@ import com.intellij.openapi.editor.ex.ScrollingModelEx
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.impl.CaretModelImpl
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.platform.project.projectId
import com.maddyhome.idea.vim.api.BufferPosition
@@ -150,7 +151,7 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
}
}
if (!isWritable()) return
editor.document.insertString(atPosition, text)
editor.document.insertString(atPosition, StringUtil.convertLineSeparators(text, "\n"))
}
override fun replaceString(start: Int, end: Int, newString: String) {
@@ -179,21 +180,38 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
return editor.caretModel.allCarets.map { IjVimCaret(it) }
}
override var isFirstCaret = true
override var isReversingCarets = false
@Suppress("ideavimRunForEachCaret")
override fun forEachCaret(action: (VimCaret) -> Unit) {
if (editor.vim.inBlockSelection) {
action(IjVimCaret(editor.caretModel.primaryCaret))
} else {
editor.caretModel.runForEachCaret({
if (it.isValid) {
action(IjVimCaret(it))
}
}, false)
try {
editor.caretModel.runForEachCaret({
if (it.isValid) {
action(IjVimCaret(it))
isFirstCaret = false
}
}, false)
} finally {
isFirstCaret = true
}
}
}
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {
editor.caretModel.runForEachCaret({ action(IjVimCaret(it)) }, reverse)
isReversingCarets = reverse
try {
editor.caretModel.runForEachCaret({
action(IjVimCaret(it))
isFirstCaret = false
}, reverse)
} finally {
isFirstCaret = true
isReversingCarets = false
}
}
override fun isInForEachCaretScope(): Boolean {
@@ -613,6 +631,10 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
get() = ijFoldRegion.endOffset
}
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return editor.softWrapModel.getSoftWrap(offset)?.start
}
override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T {
return caret
}

View File

@@ -12,6 +12,7 @@ import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.ClientProperty
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBPanel
@@ -90,6 +91,7 @@ class OutputPanel private constructor(
// Suppress the fancy frame background used in the Islands theme
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, editor.component)
// Initialize panel
setLayout(BorderLayout(0, 0))

View File

@@ -15,6 +15,7 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.DocumentAdapter
import com.intellij.util.IJSwingUtilities
import com.maddyhome.idea.vim.EventFacade
@@ -144,6 +145,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
glassPane.addComponentListener(resizePanelListener)
positionPanel()
glassPane.isVisible = true
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, parent)
entry.requestFocusInWindow()
}
this.isActive = true
@@ -191,6 +193,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
requestFocus(parent!!)
}
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, null)
oldGlass!!.removeComponentListener(resizePanelListener)
oldGlass!!.isVisible = false
oldGlass!!.remove(this)
@@ -284,7 +287,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
.calculateCount0Snapshot()
)
if (labelText == "/" || labelText == "?" || searchCommand) {
if ((labelText == "/" || labelText == "?" || searchCommand) && !injector.macro.isExecutingMacro) {
val forwards = labelText != "?" // :s, :g, :v are treated as forwards
val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0)
val pattern = searchText.take(patternEnd)

View File

@@ -96,7 +96,7 @@ data class ReadCommand(val range: Range, val modifier: CommandModifier, val argu
val copiedText = injector.clipboardManager.dumbCopiedText(content)
val caret = editor.currentCaret()
val address = if (range.addresses.isEmpty()) -1 else range.addresses.last().getLine1(editor, caret)
val textData = PutData.TextData(null, copiedText, SelectionType.LINE_WISE)
val textData = PutData.TextData(null, SelectionType.LINE_WISE, copiedText.transferableData, null)
return PutData(
textData,
null,

View File

@@ -20,5 +20,5 @@ import com.maddyhome.idea.vim.vimscript.model.functions.BuiltinFunctionHandler
@VimscriptFunction(name = "pumvisible")
internal class PopupMenuVisibleFunctionHandler : BuiltinFunctionHandler<VimInt>() {
override fun doFunction(arguments: Arguments, editor: VimEditor, context: ExecutionContext, vimContext: VimLContext) =
(CompletionService.getCompletionService().currentCompletion != null).asVimInt()
(CompletionService.getCompletionService().currentCompletion == null).asVimInt()
}

View File

@@ -1,12 +1,4 @@
<!--
~ 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.
-->
<idea-plugin url="https://plugins.jetbrains.com/plugin/164">
<idea-plugin>
<name>IdeaVim</name>
<id>IdeaVIM</id>
<description><![CDATA[
@@ -40,7 +32,7 @@
<li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a> — feature requests and bug reports</li>
</ul>
]]></description>
<version>SNAPSHOT</version>
<version>chylex</version>
<vendor>JetBrains</vendor>
<!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) -->

View File

@@ -78,5 +78,10 @@
"keys": "gJ",
"class": "com.maddyhome.idea.vim.action.change.delete.DeleteJoinVisualLinesAction",
"modes": "X"
},
{
"keys": "z@",
"class": "com.maddyhome.idea.vim.action.macro.PlaybackRegisterInOpenFilesAction",
"modes": "N"
}
]

View File

@@ -241,12 +241,7 @@ class RegistersCommandTest : VimTestCase() {
val vimEditor = fixture.editor.vim
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
injector.registerGroup.saveRegister(
vimEditor,
context,
'+',
Register('+', injector.clipboardManager.dumbCopiedText("Lorem ipsum dolor"), SelectionType.LINE_WISE)
)
injector.registerGroup.saveRegister(vimEditor, context, '+', Register('+', SelectionType.LINE_WISE, "Lorem ipsum dolor", mutableListOf()))
val clipboardContent = injector.clipboardManager.dumbCopiedText("clipboard content")
injector.clipboardManager.setClipboardContent(vimEditor, context, clipboardContent)
typeText("V<Esc>")
@@ -453,12 +448,7 @@ class RegistersCommandTest : VimTestCase() {
val vimEditor = fixture.editor.vim
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
val clipboardContent = injector.clipboardManager.dumbCopiedText("clipboard content")
injector.registerGroup.saveRegister(
vimEditor,
context,
'+',
Register('+', injector.clipboardManager.dumbCopiedText("Lorem ipsum dolor"), SelectionType.LINE_WISE)
)
injector.registerGroup.saveRegister(vimEditor, context, '+', Register('+', SelectionType.LINE_WISE, "Lorem ipsum dolor", mutableListOf()))
injector.clipboardManager.setClipboardContent(vimEditor, context, clipboardContent)
typeText("V<Esc>")

View File

@@ -50,4 +50,4 @@ class RegisterVariableTest : VimTestCase() {
assertEquals("ab", register.text)
}
}
}

View File

@@ -608,7 +608,7 @@ class NavigateBetweenFoldsTest : FoldActionTestBase() {
typeText("yzj")
val context = injector.executionContextManager.getEditorExecutionContext(fixture.editor.vim)
val regText = injector.registerGroup.getRegister(fixture.editor.vim, context, '0')!!.text
val regText = injector.registerGroup.getRegister(fixture.editor.vim, context, '0')!!.text!!
// Linewise yank includes all lines from cursor to fold start (inclusive)
assertEquals(true, regText.contains("int x = 5"))
assertEquals(true, regText.contains("int y = 10"))

View File

@@ -36,10 +36,6 @@ tasks {
group = "verification"
useJUnitPlatform()
// Gradle 9+ requires explicit test source set configuration for custom Test tasks
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath
// This is needed for the robot to access the message of the exception
// Usually these opens are provided by the intellij gradle plugin
// https://github.com/JetBrains/gradle-intellij-plugin/blob/b21e3f382e9885948a6427001d5e64234c602613/src/main/kotlin/org/jetbrains/intellij/utils/OpenedPackages.kt#L26

View File

@@ -37,10 +37,6 @@ tasks {
group = "verification"
useJUnitPlatform()
// Gradle 9+ requires explicit test source set configuration for custom Test tasks
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath
// This is needed for the robot to access the message of the exception
// Usually these opens are provided by the intellij gradle plugin
// https://github.com/JetBrains/gradle-intellij-plugin/blob/b21e3f382e9885948a6427001d5e64234c602613/src/main/kotlin/org/jetbrains/intellij/utils/OpenedPackages.kt#L26

View File

@@ -37,10 +37,6 @@ tasks {
group = "verification"
useJUnitPlatform()
// Gradle 9+ requires explicit test source set configuration for custom Test tasks
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath
// This is needed for the robot to access the message of the exception
// Usually these opens are provided by the intellij gradle plugin
// https://github.com/JetBrains/gradle-intellij-plugin/blob/b21e3f382e9885948a6427001d5e64234c602613/src/main/kotlin/org/jetbrains/intellij/utils/OpenedPackages.kt#L26

View File

@@ -82,6 +82,7 @@ tasks {
generateGrammarSource {
maxHeapSize = "128m"
arguments.addAll(listOf("-package", "com.maddyhome.idea.vim.parser.generated", "-visitor"))
outputDirectory = file("src/main/java/com/maddyhome/idea/vim/parser/generated")
}
named("compileKotlin") {

View File

@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["<C-R>"], modes = [Mode.NORMAL])
@CommandOrMotion(keys = ["U", "<C-R>"], modes = [Mode.NORMAL, Mode.VISUAL])
class RedoAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED

View File

@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL])
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL, Mode.VISUAL])
class UndoAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -21,12 +20,8 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/**
* @author vlan
*
* Note: This implementation assumes that the 'gu' command in visual mode is equivalent to 'u'.
* While 'v_gu' is not explicitly documented in Vim help, we treat these commands as identical
* based on observed behavior, without examining Vim's source code.
*/
@CommandOrMotion(keys = ["u", "gu"], modes = [Mode.VISUAL])
@CommandOrMotion(keys = [], modes = [])
class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -21,12 +20,8 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/**
* @author vlan
*
* Note: This implementation assumes that the 'gU' command in visual mode is equivalent to 'U'.
* While 'v_gU' is not explicitly documented in Vim help, we treat these commands as identical
* based on observed behavior, without examining Vim's source code.
*/
@CommandOrMotion(keys = ["U", "gU"], modes = [Mode.VISUAL])
@CommandOrMotion(keys = [], modes = [])
class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE

View File

@@ -69,15 +69,10 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
*/
@VimLockLabel.SelfSynchronized
private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean {
val register: Register? = injector.registerGroup.getRegister(editor, context, key)
val register: Register? = injector.registerGroup.getRegister(key)
if (register != null) {
val textData = PutData.TextData(
register.name,
injector.clipboardManager.dumbCopiedText(register.text),
SelectionType.CHARACTER_WISE
)
val putData =
PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
val textData = PutData.TextData(register.text, SelectionType.CHARACTER_WISE, emptyList(), register.name)
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
injector.put.putText(editor, context, putData)
return true
}

View File

@@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.action.copy
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
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.Argument
@@ -36,33 +35,40 @@ sealed class PutTextBaseAction(
val count = operatorArguments.count1
val sortedCarets = editor.sortedCarets()
return if (sortedCarets.size > 1) {
val caretToPutData = sortedCarets.associateWith { getPutDataForCaret(editor, context, it, count) }
val putData = getPutData(count)
val splitText = putData.textData?.rawText?.split('\n')?.dropLastWhile(String::isEmpty)
val caretToPutData = if (splitText != null && splitText.size == sortedCarets.size) {
sortedCarets.mapIndexed { index, caret -> caret to putData.copy(textData = putData.textData.copy(rawText = splitText[splitText.lastIndex - index])) }.toMap()
} else {
sortedCarets.associateWith { putData }
}
var result = true
caretToPutData.forEach {
result = injector.put.putTextForCaret(editor, it.key, context, it.value) && result
}
result
} else {
val putData = getPutDataForCaret(editor, context, sortedCarets.single(), count)
injector.put.putText(editor, context, putData)
injector.put.putText(editor, context, getPutData(count))
}
}
private fun getPutDataForCaret(
editor: VimEditor,
context: ExecutionContext,
caret: ImmutableVimCaret,
count: Int,
private fun getPutData(count: Int,
): PutData {
val registerService = injector.registerGroup
val registerChar = if (caret.editor.carets().size == 1) {
registerService.currentRegister
} else {
registerService.getCurrentRegisterForMulticaret()
}
val register = caret.registerStorage.getRegister(editor, context, registerChar)
val textData = register?.let { TextData(register) }
return PutData(textData, null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
return PutData(getRegisterTextData(), null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
}
}
fun getRegisterTextData(): TextData? {
val register = injector.registerGroup.getRegister(injector.registerGroup.currentRegister)
return register?.let {
TextData(
register.text ?: injector.parser.toPrintableString(register.keys),
register.type,
register.transferableData,
register.name,
)
}
}

View File

@@ -41,8 +41,22 @@ sealed class PutVisualTextBaseAction(
): Boolean {
if (caretsAndSelections.isEmpty()) return false
val count = cmd.count
val caretToPutData =
editor.sortedCarets().associateWith { getPutDataForCaret(editor, context, it, caretsAndSelections[it], count) }
val sortedCarets =
editor.sortedCarets()
val textData = getRegisterTextData()
val splitText = textData?.rawText?.split('\n')?.dropLastWhile(String::isEmpty)
val caretToTextData = if (splitText != null && splitText.size == sortedCarets.size) {
sortedCarets.mapIndexed { index, caret -> caret to textData.copy(rawText = splitText[splitText.lastIndex - index]) }.toMap()
} else {
sortedCarets.associateWith { textData }
}
val caretToPutData = caretToTextData.mapValues { (caret, textData) ->
getPutDataForCaret(textData, caret, caretsAndSelections[caret], count)
}
injector.registerGroup.resetRegister()
var result = true
caretToPutData.forEach {
@@ -50,17 +64,11 @@ sealed class PutVisualTextBaseAction(
}
return result
}
private fun getPutDataForCaret(
editor: VimEditor,
context: ExecutionContext,
private fun getPutDataForCaret(textData: PutData.TextData?,
caret: VimCaret,
selection: VimSelection?,
count: Int,
): PutData {
val lastRegisterChar = injector.registerGroup.lastRegisterChar
val register = caret.registerStorage.getRegister(editor, context, lastRegisterChar)
val textData = register?.let { PutData.TextData(register) }
count: Int,): PutData {
val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) }
return PutData(textData, visualSelection, count, insertTextBeforeCaret, indent, caretAfterInsertedText)
}

View File

@@ -118,7 +118,7 @@ open class InsertRegisterActionBase(insertLiterally: Boolean) : InsertCommandLin
replayKeys(editor, context, register.keys)
}
else {
insertText(commandLine, commandLine.caret.offset, register.text)
insertText(commandLine, commandLine.caret.offset, register.text ?: return false)
}
return true
}

View File

@@ -88,8 +88,7 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
else -> throw ExException("Unexpected search label ${argument.label}")
}
// Vim doesn't treat not finding something as an error, although it might report either an error or warning message
if (offsetAndMotion == null) return Motion.NoMotion
if (offsetAndMotion == null) return Motion.Error
parentAction.motionType = offsetAndMotion.second
return offsetAndMotion.first.toMotionOrError()
}

View File

@@ -76,6 +76,13 @@ sealed class TillCharacterMotion(
)
}
injector.motion.setLastFTCmd(tillCharacterMotionType, argument.character)
val offset = if (!finishBeforeCharacter) ""
else if (direction == Direction.FORWARDS) "s-1"
else "s+1"
injector.searchGroup.setLastSearchState(argument.character.let { if (it in "`^$.*[~/\\") "\\$it" else it.toString() }, offset, direction)
return res.toMotionOrError()
}
}

View File

@@ -31,6 +31,12 @@ class MotionCamelLeftAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
if (caret.hasSelection() && caret.offset > caret.vimSelectionStart) {
val target = injector.searchHelper.findPreviousCamelEnd(editor.text(), caret.offset, operatorArguments.count1)
if (target != null && target > caret.vimSelectionStart) {
return target.toMotionOrError()
}
}
return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error
}
@@ -47,6 +53,10 @@ class MotionCamelRightAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
if (caret.hasSelection() && caret.offset >= caret.vimSelectionStart) {
return injector.searchHelper.findNextCamelEnd(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error
}
return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error
}

View File

@@ -70,6 +70,6 @@ class MotionDownNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1)
return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1, bufferLines = true)
}
}

View File

@@ -70,6 +70,6 @@ class MotionUpNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1)
return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1, bufferLines = true)
}
}

View File

@@ -9,7 +9,6 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.group.visual.VisualChange
import com.maddyhome.idea.vim.group.visual.vimMoveBlockSelectionToOffset
import com.maddyhome.idea.vim.group.visual.vimMoveSelectionToCaret
@@ -17,13 +16,11 @@ import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.helper.VimLockLabel
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.inVisualMode
import javax.swing.KeyStroke
/**
* Immutable interface of the caret. Immutable caret is an important concept of Fleet.
@@ -66,7 +63,7 @@ interface ImmutableVimCaret {
fun hasSelection(): Boolean
var lastSelectionInfo: SelectionInfo
val registerStorage: CaretRegisterStorage
val registerStorage: VimRegisterGroup
val markStorage: LocalMarkStorage
}
@@ -152,19 +149,3 @@ fun VimCaret.moveToMotion(motion: Motion): VimCaret {
this
}
}
interface CaretRegisterStorage {
val caret: ImmutableVimCaret
fun storeText(
editor: VimEditor,
context: ExecutionContext,
range: TextRange,
type: SelectionType,
isDelete: Boolean,
): Boolean
fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register?
fun setKeys(editor: VimEditor, context: ExecutionContext, register: Char, keys: List<KeyStroke>)
fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register)
}

View File

@@ -8,94 +8,4 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.register.RegisterConstants
import com.maddyhome.idea.vim.register.VimRegisterGroupBase
import com.maddyhome.idea.vim.state.mode.SelectionType
import javax.swing.KeyStroke
abstract class VimCaretBase : VimCaret
open class CaretRegisterStorageBase(override var caret: ImmutableVimCaret) : CaretRegisterStorage,
VimRegisterGroupBase() {
companion object {
private const val ALLOWED_TO_STORE_REGISTERS = RegisterConstants.RECORDABLE_REGISTERS +
RegisterConstants.SMALL_DELETION_REGISTER +
RegisterConstants.BLACK_HOLE_REGISTER +
RegisterConstants.LAST_INSERTED_TEXT_REGISTER +
RegisterConstants.LAST_SEARCH_REGISTER
}
override var lastRegisterChar: Char
get() {
return injector.registerGroup.lastRegisterChar
}
set(_) {}
override var isRegisterSpecifiedExplicitly: Boolean
get() {
return injector.registerGroup.isRegisterSpecifiedExplicitly
}
set(_) {}
override fun storeText(
editor: VimEditor,
context: ExecutionContext,
range: TextRange,
type: SelectionType,
isDelete: Boolean,
): Boolean {
val registerChar = if (caret.editor.carets().size == 1) currentRegister else getCurrentRegisterForMulticaret()
if (caret.isPrimary) {
val registerService = injector.registerGroup
registerService.lastRegisterChar = registerChar
return registerService.storeText(editor, context, caret, range, type, isDelete)
} else {
if (!ALLOWED_TO_STORE_REGISTERS.contains(registerChar)) {
return false
}
val text = preprocessTextBeforeStoring(editor.getText(range), type)
return storeTextInternal(editor, context, range, text, type, registerChar, isDelete)
}
}
override fun getRegister(r: Char): Register? {
val editorStub = injector.fallbackWindow
val contextStub = injector.executionContextManager.getEditorExecutionContext(editorStub)
return getRegister(editorStub, contextStub, r)
}
override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
if (caret.isPrimary || !RegisterConstants.RECORDABLE_REGISTERS.contains(r)) {
return injector.registerGroup.getRegister(editor, context, r)
}
return super.getRegister(editor, context, r) ?: injector.registerGroup.getRegister(editor, context, r)
}
override fun setKeys(register: Char, keys: List<KeyStroke>) {
val editorStub = injector.fallbackWindow
val contextStub = injector.executionContextManager.getEditorExecutionContext(editorStub)
setKeys(editorStub, contextStub, register, keys)
}
override fun setKeys(editor: VimEditor, context: ExecutionContext, register: Char, keys: List<KeyStroke>) {
if (caret.isPrimary) {
injector.registerGroup.setKeys(register, keys)
}
if (!RegisterConstants.RECORDABLE_REGISTERS.contains(register)) {
return
}
return super.setKeys(register, keys)
}
override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) {
if (caret.isPrimary) {
injector.registerGroup.saveRegister(editor, context, r, register)
}
if (!RegisterConstants.RECORDABLE_REGISTERS.contains(r)) {
return
}
return super.saveRegister(editor, context, r, register)
}
}

View File

@@ -236,7 +236,7 @@ interface VimChangeGroup {
operatorArguments: OperatorArguments,
)
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret
fun insertText(editor: VimEditor, caret: VimCaret, str: String): VimCaret

View File

@@ -179,31 +179,41 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return false
}
}
val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE
val shouldYank = type != null && !isInsertMode && saveToRegister
if (shouldYank && !caret.registerStorage.storeText(editor, context, updatedRange, type, isDelete = true)) {
return false
}
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
val mode = editor.mode
if (type == null ||
(mode == Mode.INSERT || mode == Mode.REPLACE) ||
!saveToRegister ||
injector.registerGroup.storeText(
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
context,
caret,
updatedRange,
type,
true,
!editor.isFirstCaret,
editor.isReversingCarets
)
) {
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
editor.deleteString(TextRange(newRange.first, newRange.second))
}
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
return false
}
/**
@@ -213,7 +223,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param caret The caret to start insertion in
* @param str The text to insert
*/
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret {
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret {
injector.application.runWriteAction {
(editor as MutableVimEditor).insertText(caret, offset, str)
}

View File

@@ -20,7 +20,7 @@ import java.awt.datatransfer.Transferable
* - **Clipboard**: This is supported by all operating systems. It functions as a storage for the common 'copy and paste' operations typically done with Ctrl-C and Ctrl-V.
*/
interface VimClipboardManager {
fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): VimCopiedText?
fun getPrimaryContent(): VimCopiedText?
fun getClipboardContent(editor: VimEditor, context: ExecutionContext): VimCopiedText?

View File

@@ -111,7 +111,8 @@ interface VimEditor {
* This method should perform caret merging after the operations. This is similar to IJ runForEachCaret
* TODO review
*/
val isFirstCaret: Boolean
val isReversingCarets: Boolean
fun forEachCaret(action: (VimCaret) -> Unit)
fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false)
fun isInForEachCaretScope(): Boolean
@@ -209,6 +210,7 @@ interface VimEditor {
}
fun createIndentBySize(size: Int): String
fun getSoftWrapStartAtOffset(offset: Int): Int?
/**
* Returns the collapsed fold region at the specified offset, if any.

View File

@@ -25,7 +25,7 @@ interface VimMotionGroup {
allowWrap: Boolean = false,
): Motion
fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean = false): Motion
// TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion

View File

@@ -33,14 +33,18 @@ abstract class VimMotionGroupBase : VimMotionGroup {
override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T
override var lastFTChar: Char = ' '
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean): Motion {
val pos = caret.getVisualPosition()
if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) {
return Motion.Error
}
val intendedColumn = caret.vimLastColumn
val line = editor.normalizeVisualLine(pos.line + count)
val line = if (bufferLines)
// TODO Does not work with folds, but I don't use those.
editor.normalizeVisualLine(editor.bufferLineToVisualLine(editor.visualLineToBufferLine(pos.line) + count))
else
editor.normalizeVisualLine(pos.line + count)
if (intendedColumn == LAST_COLUMN) {
val normalisedColumn = injector.engineEditorHelper.normalizeVisualColumn(

View File

@@ -208,4 +208,17 @@ interface VimSearchGroup {
fun isSomeTextHighlighted(): Boolean
fun getCurrentIncsearchResultRange(editor: VimEditor): TextRange?
/**
* Sets the last search state purely for tests
*
* @param pattern The pattern to save. This is the last search pattern, not the last substitute pattern
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search
*/
fun setLastSearchState(
pattern: String,
patternOffset: String,
direction: Direction,
)
}

View File

@@ -1425,8 +1425,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search
*/
@TestOnly
fun setLastSearchState(
override fun setLastSearchState(
pattern: String,
patternOffset: String,
direction: Direction,

View File

@@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.common
interface VimCopiedText {
val text: String
val transferableData: List<Any>
// TODO Looks like sticky tape, I'm not sure that we need to modify already stored text
fun updateText(newText: String): VimCopiedText

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.common
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.state.mode.Mode
@@ -72,9 +71,9 @@ class VimListenersNotifier {
isReplaceCharListeners.forEach { it.isReplaceCharChanged(editor) }
}
fun notifyYankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
fun notifyYankPerformed(editor: VimEditor, range: TextRange) {
if (!injector.enabler.isEnabled()) return // we remove all the listeners when turning the plugin off, but let's do it just in case
yankListeners.forEach { it.yankPerformed(caretToRange) }
yankListeners.forEach { it.yankPerformed(editor, range) }
}
/**

View File

@@ -8,8 +8,8 @@
package com.maddyhome.idea.vim.common
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
interface VimYankListener: Listener {
fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>)
}
fun yankPerformed(editor: VimEditor, range: TextRange)
}

View File

@@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretListener
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.command.Argument
@@ -226,7 +227,15 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret")
}
val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset)
val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset).let {
if (offset.intendedColumn == VimMotionGroupBase.LAST_COLUMN) {
val softWrapStart = editor.getSoftWrapStartAtOffset(it)
if (softWrapStart != null) softWrapStart - 1 else it
}
else {
it
}
}
StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action")
// Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode
@@ -274,7 +283,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
foldRegion.isExpanded = true
}
}
return resultOffset
}

View File

@@ -272,7 +272,11 @@ class ToActionMappingInfo(
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToAction' mapping...")
injector.actionExecutor.executeAction(editor, name = action, context = context)
val commandBuilder = KeyHandler.getInstance().keyHandlerState.commandBuilder
for (i in 0 until commandBuilder.calculateCount0Snapshot().coerceAtLeast(1)) {
injector.actionExecutor.executeAction(editor, name = action, context = context)
}
commandBuilder.resetCount()
}
companion object {

View File

@@ -8,11 +8,11 @@
package com.maddyhome.idea.vim.put
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.state.mode.SelectionType
data class ProcessedTextData(
val registerChar: Char?,
val copiedText: VimCopiedText,
val text: String,
val typeInRegister: SelectionType,
val transferableData: List<Any>,
val registerChar: Char?,
)

View File

@@ -9,9 +9,7 @@
package com.maddyhome.idea.vim.put
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.state.mode.SelectionType
/**
@@ -37,12 +35,9 @@ data class PutData(
)
data class TextData(
val registerChar: Char?,
val copiedText: VimCopiedText,
val rawText: String?,
val typeInRegister: SelectionType,
) {
constructor(register: Register) : this(register.name, register.copiedText, register.type)
val rawText = copiedText.text // TODO do not call it raw text...
}
val transferableData: List<Any>,
val registerChar: Char?,
)
}

View File

@@ -141,7 +141,6 @@ abstract class VimPutBase : VimPut {
if (data.visualSelection?.typeInEditor?.isLine == true && data.textData.typeInRegister.isChar) text += "\n"
// TODO: shouldn't it be adjusted when we are storing the text?
if (data.textData.typeInRegister.isLine && text.isNotEmpty() && text.last() != '\n') text += '\n'
if (data.textData.typeInRegister.isChar && text.lastOrNull() == '\n' && data.visualSelection?.typeInEditor?.isLine == false) {
@@ -150,9 +149,10 @@ abstract class VimPutBase : VimPut {
}
return ProcessedTextData(
data.textData.registerChar,
data.textData.copiedText.updateText(text),
text,
data.textData.typeInRegister,
data.textData.transferableData,
data.textData.registerChar,
)
}
@@ -515,7 +515,7 @@ abstract class VimPutBase : VimPut {
startOffsets.forEach { startOffset ->
val selectionType = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE
val (endOffset, updatedCaret) = putTextInternal(
editor, updated, context, text.copiedText.text, text.typeInRegister, selectionType,
editor, updated, context, text.text, text.typeInRegister, selectionType,
startOffset, data.count, data.indent, data.caretAfterInsertedText,
)
updated = updatedCaret

View File

@@ -620,6 +620,12 @@ class VimRegex(pattern: String) {
override fun nativeCarets(): List<VimCaret> = emptyList()
override val isFirstCaret: Boolean
get() = false
override val isReversingCarets: Boolean
get() = false
override fun forEachCaret(action: (VimCaret) -> Unit) {}
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {}
@@ -787,6 +793,10 @@ class VimRegex(pattern: String) {
return null
}
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return null
}
override fun getAllFoldRegions(): List<VimFoldRegion> {
return emptyList()
}

View File

@@ -8,40 +8,86 @@
package com.maddyhome.idea.vim.register
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.helper.EngineStringHelper
import com.maddyhome.idea.vim.state.mode.SelectionType
import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
// TODO should we prefer keys over text, as they are more informative?
// TODO e.g.  could be both <Esc> and <C-[> after trying to restore original keys
data class Register(
val name: Char,
val keys: List<KeyStroke>,
val type: SelectionType,
val copiedText: VimCopiedText,
) {
val text = copiedText.text
val printableString: String =
EngineStringHelper.toPrintableCharacters(keys) // should be the same as [text], but we can't render control notation properly
class Register {
var name: Char
val type: SelectionType
val keys: MutableList<KeyStroke>
val transferableData: MutableList<out Any>
val rawText: String?
constructor(name: Char, type: SelectionType, keys: MutableList<KeyStroke>) {
this.name = name
this.type = type
this.keys = keys
this.transferableData = mutableListOf()
this.rawText = text
}
constructor(name: Char, type: SelectionType, keys: List<KeyStroke>) : this(
name,
keys,
type,
injector.clipboardManager.dumbCopiedText(injector.parser.toPrintableString(keys))
)
constructor(
name: Char,
type: SelectionType,
text: String,
transferableData: MutableList<out Any>,
) {
this.name = name
this.type = type
this.keys = injector.parser.stringToKeys(text).toMutableList()
this.transferableData = transferableData
this.rawText = text
}
constructor(name: Char, copiedText: VimCopiedText, type: SelectionType) : this(
name,
injector.parser.stringToKeys(copiedText.text),
type,
copiedText
)
constructor(
name: Char,
type: SelectionType,
text: String,
transferableData: MutableList<out Any>,
rawText: String,
) {
this.name = name
this.type = type
this.keys = injector.parser.stringToKeys(text).toMutableList()
this.transferableData = transferableData
this.rawText = rawText
}
override fun toString(): String = "@$name = $printableString"
val text: String?
get() {
val builder = StringBuilder()
for (key in keys) {
val c = key.keyChar
if (c == KeyEvent.CHAR_UNDEFINED) {
return null
}
builder.append(c)
}
return builder.toString()
}
val printableString: String
get() = EngineStringHelper.toPrintableCharacters(keys) // should be the same as [text], but we can't render control notation properly
/**
* Append the supplied text to any existing text.
*/
fun addTextAndResetTransferableData(text: String) {
addKeys(injector.parser.stringToKeys(text))
transferableData.clear()
}
fun prependTextAndResetTransferableData(text: String) {
this.keys.addAll(0, injector.parser.stringToKeys(text))
transferableData.clear()
}
fun addKeys(keys: List<KeyStroke>) {
this.keys.addAll(keys)
}
object KeySorter : Comparator<Register> {
@NonNls
@@ -52,41 +98,3 @@ data class Register(
}
}
}
/**
* Imagine you yanked two lines and have the following content in your register a - foo\nbar\n (register type is line-wise)
* Now, there are three different ways to append content, each with a different outcome:
* - If you append a macro qAbazq, you'll get foo\nbarbaz\n in register `a` and it stays line-wise
* - If you use Vim script and execute let @A = "baz", the result will be foo\nbar\nbaz and the register becomes character-wise
* - If you copy "baz" to register A, it becomes foo\nbar\nbaz\n and stays line-wise
*
* At the moment, we will stick to the third option to not overcomplicate the plugin
* (until there is a user who notices the difference)
*/
fun Register.addText(text: String): Register {
return when (this.type) {
SelectionType.CHARACTER_WISE -> {
Register(
this.name,
injector.clipboardManager.dumbCopiedText(this.text + text),
SelectionType.CHARACTER_WISE
) // todo it's empty for historical reasons, but should we really clear transferable data?
}
SelectionType.LINE_WISE -> {
Register(
this.name,
injector.clipboardManager.dumbCopiedText(this.text + text + (if (text.endsWith('\n')) "" else "\n")),
SelectionType.LINE_WISE
) // todo it's empty for historical reasons, but should we really clear transferable data?
}
SelectionType.BLOCK_WISE -> {
Register(
this.name,
injector.clipboardManager.dumbCopiedText(this.text + "\n" + text),
SelectionType.BLOCK_WISE
) // todo it's empty for historical reasons, but should we really clear transferable data?
}
}
}

View File

@@ -17,6 +17,13 @@ import javax.swing.KeyStroke
interface VimRegisterGroup {
/**
* Get the last register selected by the user
*
* @return The register, null if no such register
*/
@Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getLastRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext)")
val lastRegister: Register?
var lastRegisterChar: Char
val currentRegister: Char
@@ -32,7 +39,6 @@ interface VimRegisterGroup {
val isRegisterSpecifiedExplicitly: Boolean
val defaultRegister: Char
fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register?
fun isValid(reg: Char): Boolean
fun selectRegister(reg: Char): Boolean
fun resetRegister()
@@ -41,6 +47,7 @@ interface VimRegisterGroup {
fun isRegisterWritable(): Boolean
fun isRegisterWritable(reg: Char): Boolean
/** Store text into the last register. */
fun storeText(
editor: VimEditor,
context: ExecutionContext,
@@ -48,18 +55,20 @@ interface VimRegisterGroup {
range: TextRange,
type: SelectionType,
isDelete: Boolean,
forceAppend: Boolean = false,
prependInsteadOfAppend: Boolean = false
): Boolean
/**
* Stores text to any writable register (used for the let command)
*/
fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean
/**
* Stores text to any writable register (used for multicaret tests)
*/
@TestOnly
fun storeText(
editor: VimEditor,
context: ExecutionContext,
register: Char,
text: String,
selectionType: SelectionType,
): Boolean
fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType): Boolean
/**
* Stores text, character wise, in the given special register
@@ -75,7 +84,6 @@ interface VimRegisterGroup {
* preferable to yank from the fixture editor.
*/
fun storeTextSpecial(register: Char, text: String): Boolean
@Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext, char)")
fun getRegister(r: Char): Register?
fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register?

View File

@@ -17,7 +17,6 @@ import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger
@@ -75,9 +74,13 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
override val defaultRegister: Char
get() = defaultRegisterChar
override fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register? {
return getRegister(editor, context, lastRegisterChar)
}
/**
* Get the last register selected by the user
*
* @return The register, null if no such register
*/
override val lastRegister: Register?
get() = getRegister(lastRegisterChar)
private val onClipboardChanged: () -> Unit = {
val clipboardOptionValue = injector.globalOptions().clipboard
@@ -113,13 +116,20 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return if (isValid(reg)) {
isRegisterSpecifiedExplicitly = true
lastRegisterChar = reg
logger.debug { "register selected: $lastRegisterChar" }
logger.debug { "register selected: $lastRegister" }
true
} else {
false
}
}
override fun getRegister(r: Char): Register? {
val dummyEditor = injector.fallbackWindow
val dummyContext = injector.executionContextManager.getEditorExecutionContext(dummyEditor)
return getRegister(dummyEditor, dummyContext, r)
}
/**
* Reset the selected register back to the default register.
*/
@@ -175,6 +185,8 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
type: SelectionType,
register: Char,
isDelete: Boolean,
forceAppend: Boolean,
prependInsteadOfAppend: Boolean,
): Boolean {
// Null register doesn't get saved, but acts like it was
if (lastRegisterChar == BLACK_HOLE_REGISTER) return true
@@ -193,50 +205,62 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
end = t
}
val copiedText =
if (start != -1) { // FIXME: so, we had invalid ranges all the time?.. I've never handled such cases
injector.clipboardManager.collectCopiedText(editor, context, range, text)
} else {
injector.clipboardManager.dumbCopiedText(text)
}
logger.debug { "Copy to '$lastRegisterChar' with copied text: $copiedText" }
// If this is an uppercase register, we need to append the text to the corresponding lowercase register
if (Character.isUpperCase(register)) {
val transferableData: List<Any> =
if (start != -1) injector.clipboardManager.getTransferableData(editor, range) else ArrayList()
var processedText =
if (start != -1) injector.clipboardManager.preprocessText(editor, range, text, transferableData) else text
logger.debug {
val transferableClasses = transferableData.joinToString(",") { it.javaClass.name }
"Copy to '$lastRegister' with transferable data: $transferableClasses"
}
if (Character.isUpperCase(register) || forceAppend) {
if (forceAppend && type == SelectionType.CHARACTER_WISE) {
processedText = if (prependInsteadOfAppend)
processedText + '\n'
else
'\n' + processedText
}
val lreg = Character.toLowerCase(register)
val r = myRegisters[lreg]
// Append the text if the lowercase register existed
if (r != null) {
myRegisters[lreg] = r.addText(copiedText.text)
if (prependInsteadOfAppend) {
r.prependTextAndResetTransferableData(processedText)
}
else {
r.addTextAndResetTransferableData(processedText)
}
} else {
myRegisters[lreg] = Register(lreg, copiedText, type)
logger.debug { "register '$register' contains: \"$copiedText\"" }
myRegisters[lreg] = Register(lreg, type, processedText, ArrayList(transferableData))
logger.debug { "register '$register' contains: \"$processedText\"" }
} // Set the text if the lowercase register didn't exist yet
} else {
myRegisters[register] = Register(register, copiedText, type)
logger.debug { "register '$register' contains: \"$copiedText\"" }
myRegisters[register] = Register(register, type, processedText, ArrayList(transferableData))
logger.debug { "register '$register' contains: \"$processedText\"" }
} // Put the text in the specified register
if (register == CLIPBOARD_REGISTER) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
if (!isRegisterSpecifiedExplicitly && !isDelete && isPrimaryRegisterSupported() && OptionConstants.clipboard_unnamedplus in injector.globalOptions().clipboard) {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
}
}
if (register == PRIMARY_REGISTER) {
if (isPrimaryRegisterSupported()) {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
if (!isRegisterSpecifiedExplicitly && !isDelete && OptionConstants.clipboard_unnamed in injector.globalOptions().clipboard) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
}
} else {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
}
}
// Also add it to the unnamed register if the default wasn't specified
if (register != UNNAMED_REGISTER && ".:/".indexOf(register) == -1) {
myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, copiedText, type)
logger.debug { "register '$UNNAMED_REGISTER' contains: \"$copiedText\"" }
myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, type, processedText, ArrayList(transferableData))
logger.debug { "register '$UNNAMED_REGISTER' contains: \"$processedText\"" }
}
if (isDelete) {
@@ -256,26 +280,26 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
while (d >= '1') {
val t = myRegisters[d]
if (t != null) {
val incName = (d.code + 1).toChar()
myRegisters[incName] = Register(incName, t.copiedText, t.type)
t.name = (d.code + 1).toChar()
myRegisters[(d.code + 1).toChar()] = t
}
d--
}
myRegisters['1'] = Register('1', copiedText, type)
myRegisters['1'] = Register('1', type, processedText, ArrayList(transferableData))
}
// Deletes smaller than one line and without specified register go the the "-" register
if (smallInlineDeletion && register == defaultRegister) {
myRegisters[SMALL_DELETION_REGISTER] =
Register(SMALL_DELETION_REGISTER, copiedText, type)
Register(SMALL_DELETION_REGISTER, type, processedText, ArrayList(transferableData))
}
} else if (register == defaultRegister) {
myRegisters['0'] = Register('0', copiedText, type)
logger.debug { "register '0' contains: \"$copiedText\"" }
myRegisters['0'] = Register('0', type, processedText, ArrayList(transferableData))
logger.debug { "register '0' contains: \"$processedText\"" }
} // Yanks also go to register 0 if the default register was used
return true
}
/**
* Store text into the last register.
*
@@ -292,10 +316,12 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
range: TextRange,
type: SelectionType,
isDelete: Boolean,
forceAppend: Boolean,
prependInsteadOfAppend: Boolean
): Boolean {
if (isRegisterWritable()) {
val text = preprocessTextBeforeStoring(editor.getText(range), type)
return storeTextInternal(editor, context, range, text, type, lastRegisterChar, isDelete)
return storeTextInternal(editor, context, range, text, type, lastRegisterChar, isDelete, forceAppend, prependInsteadOfAppend)
}
return false
@@ -330,53 +356,35 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
if (READONLY_REGISTERS.indexOf(register) == -1 && register != LAST_SEARCH_REGISTER && register != UNNAMED_REGISTER) {
return false
}
myRegisters[register] = Register(
register,
injector.clipboardManager.dumbCopiedText(text),
myRegisters[register] = Register(register,
SelectionType.CHARACTER_WISE
) // TODO why transferable data is not collected?
, text, ArrayList())
logger.debug { "register '$register' contains: \"$text\"" }
return true
}
@Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext, char)")
override fun getRegister(r: Char): Register? {
val dummyEditor = injector.fallbackWindow
val dummyContext = injector.executionContextManager.getEditorExecutionContext(dummyEditor)
return getRegister(dummyEditor, dummyContext, r)
}
override fun storeText(
editor: VimEditor,
context: ExecutionContext,
register: Char,
text: String,
selectionType: SelectionType,
): Boolean {
if (!WRITABLE_REGISTERS.contains(register)) {
return false
}
logger.debug { "register '$register' contains: \"$text\"" }
val oldRegister = getRegister(editor, context, register.lowercaseChar())
val newRegister = if (register.isUpperCase() && oldRegister != null) {
oldRegister.addText(text)
} else {
Register(
register,
injector.clipboardManager.dumbCopiedText(text),
selectionType
) // FIXME why don't we collect transferable data?
}
saveRegister(editor, context, register, newRegister)
if (register == '/') {
injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register
}
return true
}
override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean {
return storeText(editor, context, register, text, SelectionType.CHARACTER_WISE)
}
override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType,
): Boolean {
if (!WRITABLE_REGISTERS.contains(register)) {
return false
}
logger.debug { "register '$register' contains: \"$text\"" }
val textToStore = if (register.isUpperCase()) {
(getRegister(register.lowercaseChar())?.rawText ?: "") + text
} else {
text
}
val reg = Register(register, selectionType, textToStore, ArrayList())
saveRegister(editor, context, register, reg)
if (register == '/') {
injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register
}
return true
}
private fun guessSelectionType(text: String): SelectionType {
return if (text.endsWith("\n")) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE
@@ -389,10 +397,10 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
* @param r - the register character corresponding to either the primary selection (*) or clipboard selection (+)
* @return the content of the selection, if available, otherwise null
*/
private fun refreshClipboardRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
private fun refreshClipboardRegister(r: Char): Register? {
return when (r) {
PRIMARY_REGISTER -> refreshPrimaryRegister(editor, context)
CLIPBOARD_REGISTER -> refreshClipboardRegister(editor, context)
PRIMARY_REGISTER -> refreshPrimaryRegister()
CLIPBOARD_REGISTER -> refreshClipboardRegister()
else -> throw RuntimeException("Clipboard register expected, got $r")
}
}
@@ -401,56 +409,60 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return System.getenv("DISPLAY") != null && injector.systemInfoService.isXWindow
}
private fun setSystemPrimaryRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) {
logger.trace("Setting text: $copiedText to primary selection...")
private fun setSystemPrimaryRegisterText(text: String, rawText: String, transferableData: List<Any>) {
logger.trace("Setting text: $text to primary selection...")
if (isPrimaryRegisterSupported()) {
try {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(text, rawText, transferableData)
} catch (e: Exception) {
logger.warn("False positive X11 primary selection support")
logger.trace("Setting text to primary selection failed. Setting it to clipboard selection instead")
setSystemClipboardRegisterText(editor, context, copiedText)
setSystemClipboardRegisterText(text, rawText, transferableData)
}
} else {
logger.trace("X11 primary selection is not supporting. Setting clipboard selection instead")
setSystemClipboardRegisterText(editor, context, copiedText)
setSystemClipboardRegisterText(text, rawText, transferableData)
}
}
private fun setSystemClipboardRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
private fun setSystemClipboardRegisterText(text: String, rawText: String, transferableData: List<Any>) {
injector.clipboardManager.setClipboardText(text, rawText, transferableData)
}
private fun refreshPrimaryRegister(editor: VimEditor, context: ExecutionContext): Register? {
private fun refreshPrimaryRegister(): Register? {
logger.trace("Syncing cached primary selection value..")
if (!isPrimaryRegisterSupported()) {
logger.trace("X11 primary selection is not supported. Syncing clipboard selection..")
return refreshClipboardRegister(editor, context)
return refreshClipboardRegister()
}
try {
val clipboardData = injector.clipboardManager.getPrimaryContent(editor, context) ?: return null
val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null
val currentRegister = myRegisters[PRIMARY_REGISTER]
if (currentRegister != null && clipboardData.text == currentRegister.text) {
val text = clipboardData.text
val transferableData = clipboardData.transferableData.toMutableList()
if (currentRegister != null && text == currentRegister.text) {
return currentRegister
}
return Register(PRIMARY_REGISTER, clipboardData, guessSelectionType(clipboardData.text))
return transferableData?.let { Register(PRIMARY_REGISTER, guessSelectionType(text), text, it) }
} catch (e: Exception) {
logger.warn("False positive X11 primary selection support")
logger.trace("Syncing primary selection failed. Syncing clipboard selection instead")
return refreshClipboardRegister(editor, context)
return refreshClipboardRegister()
}
}
private fun refreshClipboardRegister(editor: VimEditor, context: ExecutionContext): Register? {
private fun refreshClipboardRegister(): Register? {
// for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage
val systemAwareClipboardRegister = if (isPrimaryRegisterSupported()) CLIPBOARD_REGISTER else PRIMARY_REGISTER
val clipboardData = injector.clipboardManager.getClipboardContent(editor, context) ?: return null
val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null
val currentRegister = myRegisters[systemAwareClipboardRegister]
if (currentRegister != null && clipboardData.text == currentRegister.text) {
val text = clipboardData.text
val transferableData = clipboardData.transferableData.toMutableList()
if (currentRegister != null && text == currentRegister.text) {
return currentRegister
}
return Register(systemAwareClipboardRegister, clipboardData, guessSelectionType(clipboardData.text))
return transferableData?.let { Register(systemAwareClipboardRegister, guessSelectionType(text), text, it) }
}
override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
@@ -460,36 +472,34 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
myR = Character.toLowerCase(myR)
}
return if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) refreshClipboardRegister(
editor,
context,
myR
) else myRegisters[myR]
myR) else myRegisters[myR]
}
override fun getRegisters(editor: VimEditor, context: ExecutionContext): List<Register> {
val filteredRegisters = myRegisters.values.filterNot { CLIPBOARD_REGISTERS.contains(it.name) }.toMutableList()
val clipboardRegisters = CLIPBOARD_REGISTERS
.filterNot { it == CLIPBOARD_REGISTER && !isPrimaryRegisterSupported() } // for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage
.mapNotNull { refreshClipboardRegister(editor, context, it) }
.mapNotNull { refreshClipboardRegister(it) }
return (filteredRegisters + clipboardRegisters).sortedWith(Register.KeySorter)
}
override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) {
var myR = if (Character.isUpperCase(r)) Character.toLowerCase(r) else r
val text = register.text
val rawText = register.rawText
if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) {
if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0 && text != null && rawText != null) {
when (myR) {
CLIPBOARD_REGISTER -> {
if (!isPrimaryRegisterSupported()) {
// it looks wrong, but for some reason non-X systems use the * register to store the clipboard content
myR = PRIMARY_REGISTER
}
setSystemClipboardRegisterText(editor, context, register.copiedText)
setSystemClipboardRegisterText(text, rawText, ArrayList(register.transferableData))
}
PRIMARY_REGISTER -> {
setSystemPrimaryRegisterText(editor, context, register.copiedText)
setSystemPrimaryRegisterText(text, rawText, ArrayList(register.transferableData))
}
}
}
@@ -507,7 +517,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
}
override fun getPlaybackRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
return if (PLAYBACK_REGISTERS.indexOf(r) != 0) getRegister(editor, context, r) else null
return if (PLAYBACK_REGISTERS.indexOf(r) != 0) getRegister(r) else null
}
override fun recordText(text: String) {
@@ -530,7 +540,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
if (register != null) {
var reg: Register? = null
if (Character.isUpperCase(register)) {
reg = getRegister(editor, context, register)
reg = getRegister(register)
}
val myRecordList = recordList
@@ -539,7 +549,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
reg = Register(Character.toLowerCase(register), SelectionType.CHARACTER_WISE, myRecordList)
myRegisters[Character.toLowerCase(register)] = reg
} else {
myRegisters[reg.name.lowercaseChar()] = reg.addText(injector.parser.toPrintableString(myRecordList))
reg.addKeys(myRecordList)
}
}
}

View File

@@ -1,187 +0,0 @@
/*
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.thinapi
import com.intellij.vim.api.VimApi
import com.intellij.vim.api.models.CaretId
import com.intellij.vim.api.models.Mode
import com.intellij.vim.api.models.Range
import com.intellij.vim.api.scopes.ListenersScope
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.common.EditorListener
import com.maddyhome.idea.vim.common.Listener
import com.maddyhome.idea.vim.common.ListenerOwner
import com.maddyhome.idea.vim.common.MacroRecordingListener
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimPluginListener
import com.maddyhome.idea.vim.common.VimYankListener
import com.maddyhome.idea.vim.key.MappingOwner
import kotlinx.coroutines.runBlocking
import com.maddyhome.idea.vim.state.mode.Mode as EngineMode
class ListenerScopeImpl(
private val listenerOwner: ListenerOwner,
private val mappingOwner: MappingOwner,
private val projectId: String,
) : ListenersScope {
private abstract class ListenerBase(listenerOwner: ListenerOwner) : Listener {
final override val owner: ListenerOwner = listenerOwner
fun launch(block: suspend () -> Unit) {
runBlocking { block() }
}
}
override fun onModeChange(callback: suspend VimApi.(Mode) -> Unit) {
val listener = object : ModeChangeListener, ListenerBase(listenerOwner) {
override fun modeChanged(
editor: VimEditor,
oldMode: EngineMode,
) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback(oldMode.toMode())
}
}
}
injector.listenersNotifier.modeChangeListeners.add(listener)
}
override fun onYank(callback: suspend VimApi.(Map<CaretId, Range.Simple>) -> Unit) {
val listener = object : VimYankListener, ListenerBase(listenerOwner) {
override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
val caretToRangeMap: Map<CaretId, Range.Simple> =
caretToRange.map { (caret, range) ->
CaretId(caret.id) to Range.Simple(range.startOffset, range.endOffset)
}.toMap()
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback(caretToRangeMap)
}
}
}
injector.listenersNotifier.yankListeners.add(listener)
}
override fun onEditorCreate(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun created(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onEditorRelease(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun released(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onEditorFocusGain(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun focusGained(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onEditorFocusLost(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun focusLost(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onMacroRecordingStart(callback: suspend VimApi.() -> Unit) {
val listener = object : MacroRecordingListener, ListenerBase(listenerOwner) {
override fun recordingStarted() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
override fun recordingFinished() {
// Not used in this listener
}
}
injector.listenersNotifier.macroRecordingListeners.add(listener)
}
override fun onMacroRecordingFinish(callback: suspend VimApi.() -> Unit) {
val listener = object : MacroRecordingListener, ListenerBase(listenerOwner) {
override fun recordingStarted() {
// Not used in this listener
}
override fun recordingFinished() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.macroRecordingListeners.add(listener)
}
override fun onIdeaVimEnabled(callback: suspend VimApi.() -> Unit) {
val listener = object : VimPluginListener, ListenerBase(listenerOwner) {
override fun turnedOn() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
override fun turnedOff() {
// Not used in this listener
}
}
injector.listenersNotifier.vimPluginListeners.add(listener)
}
override fun onIdeaVimDisabled(callback: suspend VimApi.() -> Unit) {
val listener = object : VimPluginListener, ListenerBase(listenerOwner) {
override fun turnedOn() {
// Not used in this listener
}
override fun turnedOff() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.vimPluginListeners.add(listener)
}
}

View File

@@ -132,7 +132,7 @@ class CaretReadImpl(
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
val caret: VimCaret = vimEditor.carets().find { it.id == caretId.id } ?: return null
val register: Register = caret.registerStorage.getRegister(vimEditor, context, register) ?: return null
return RegisterData(register.text, register.type.toTextSelectionType())
return RegisterData(register.text ?: return null, register.type.toTextSelectionType())
}
override fun getReg(register: Char): String? {

View File

@@ -81,7 +81,7 @@ class CaretTransactionImpl(
beforeCaret: Boolean,
): Boolean {
val copiedText = injector.clipboardManager.dumbCopiedText(text)
val textData = PutData.TextData(null, copiedText, SelectionType.CHARACTER_WISE)
val textData = PutData.TextData(null, SelectionType.CHARACTER_WISE, copiedText.transferableData, null)
val putData = PutData(
textData = textData,

View File

@@ -11,6 +11,7 @@ package com.maddyhome.idea.vim.vimscript.model.commands
import com.intellij.vim.annotations.ExCommand
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getText
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range
@@ -37,7 +38,7 @@ data class CopyTextCommand(val range: Range, val modifier: CommandModifier, val
val carets = editor.sortedCarets()
for (caret in carets) {
val range = getLineRange(editor, caret).toTextRange(editor)
val copiedText = injector.clipboardManager.collectCopiedText(editor, context, range)
val text = editor.getText(range)
// Copy is defined as:
// :[range]co[py] {address}
@@ -46,7 +47,8 @@ data class CopyTextCommand(val range: Range, val modifier: CommandModifier, val
// the line _before_ the first line (i.e., copy to above the first line).
val address1 = getAddressFromArgument(editor)
val textData = PutData.TextData(null, copiedText, SelectionType.LINE_WISE)
val transferableData = injector.clipboardManager.getTransferableData(editor, range)
val textData = PutData.TextData(text, SelectionType.LINE_WISE, transferableData, null)
var mutableCaret = caret
val putData = if (address1 == 0) {
// TODO: This should maintain current column location

Some files were not shown because too many files have changed in this diff Show More