mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-11-29 16:38:24 +01:00
Compare commits
47 Commits
customized
...
226f5e27b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
226f5e27b1 | ||
|
|
cc3c132ccb | ||
|
|
13fb2d53f1 | ||
|
|
c803a1cf24 | ||
|
|
1d60e47bb8 | ||
|
|
664c433ee9 | ||
|
|
67a44b4d25 | ||
|
|
b4915d08cd | ||
|
|
6b7b18d947 | ||
|
|
54cc89f641 | ||
|
|
1048d06586 | ||
|
|
7f0ab93ea7 | ||
|
|
a0f923512a | ||
|
|
d60af9afa1 | ||
|
|
1ef919f73a | ||
|
|
f6947d73f6 | ||
|
|
54c12470f3 | ||
|
|
8aa8725a8d | ||
|
|
1e1bbbac2a | ||
|
|
0bb3524118 | ||
|
|
12596540e9 | ||
|
|
3eadff6401 | ||
|
|
52f4c24b1e | ||
|
|
7de2ea0e0b | ||
|
|
a9fc2c58a6 | ||
|
|
1aa586484f | ||
|
|
aa24d53b18 | ||
|
|
c396d98022 | ||
|
|
db767f534f | ||
|
|
d955b1b85c | ||
|
|
0fdfc04068 | ||
|
|
244d13a3cc | ||
|
|
4a2600738f | ||
|
|
82ed26a701 | ||
|
|
cf5dce3ce7 | ||
|
|
c68e216c87 | ||
|
|
c12f4a75ac | ||
|
|
9a069e5ef8 | ||
|
|
27f2e56635 | ||
|
|
7a0bdb8f3d | ||
|
|
1d9bb6ec70 | ||
|
|
bb62dcdc15 | ||
|
|
fdcb954e31 | ||
|
|
747e4053ba | ||
|
|
448c19af47 | ||
|
|
f0edc797dc | ||
|
5aaa84744f
|
206
.claude/maintenance-instructions.md
Normal file
206
.claude/maintenance-instructions.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Codebase Maintenance Instructions
|
||||
|
||||
## Goal
|
||||
|
||||
Perform routine maintenance on random parts of the IdeaVim codebase to ensure code quality, consistency, and catch potential issues early. This is not about being overly pedantic or making changes for the sake of changes - it's about keeping an eye on the codebase and identifying genuine issues.
|
||||
|
||||
## Approach
|
||||
|
||||
### 1. Select Random Area
|
||||
|
||||
Choose a random part of the codebase to inspect. Use one of these strategies:
|
||||
|
||||
```bash
|
||||
# Get a random Kotlin file
|
||||
find . -name "*.kt" -not -path "*/build/*" -not -path "*/.gradle/*" | shuf -n 1
|
||||
|
||||
# Get a random package/directory
|
||||
find . -type d -name "*.kt" -not -path "*/build/*" | shuf -n 1 | xargs dirname
|
||||
|
||||
# Pick from core areas randomly
|
||||
# - vim-engine/src/main/kotlin/com/maddyhome/idea/vim/
|
||||
# - src/main/java/com/maddyhome/idea/vim/
|
||||
# - tests/
|
||||
```
|
||||
|
||||
**Important**: You're not limited to the file you randomly selected. If investigating reveals related files that need attention, follow the trail. The random selection is just a starting point.
|
||||
|
||||
## 2. What to Check
|
||||
|
||||
### Code Style & Formatting
|
||||
- **Kotlin conventions**: Proper use of data classes, sealed classes, when expressions
|
||||
- **Naming consistency**: Follow existing patterns in the codebase
|
||||
- **Import organization**: Remove unused imports, prefer explicit imports over wildcards (wildcard imports are generally not welcome)
|
||||
- **Code structure**: Proper indentation, spacing, line breaks
|
||||
- **Documentation**: KDoc comments where needed (public APIs, complex logic)
|
||||
- **Copyright years**: Do NOT update copyright years unless you're making substantive changes to the file. It's perfectly fine for copyright to show an older year. Don't mention copyright year updates in commit messages or change summaries
|
||||
|
||||
### Code Quality Issues
|
||||
- **Null safety**: Proper use of nullable types, safe calls, Elvis operator
|
||||
- **Error handling**: Appropriate exception handling, meaningful error messages
|
||||
- **Code duplication**: Identify repeated code that could be extracted
|
||||
- **Dead code**: Unused functions, parameters, variables
|
||||
- **TODOs/FIXMEs**: Check if old TODOs are still relevant or can be addressed
|
||||
- **Magic numbers/strings**: Should be named constants
|
||||
- **Complex conditionals**: Can they be simplified or extracted?
|
||||
|
||||
### Potential Bugs
|
||||
- **Off-by-one errors**: Especially in loops and range operations
|
||||
- **Edge cases**: Empty collections, null values, boundary conditions
|
||||
- **Type safety**: Unnecessary casts, unchecked casts
|
||||
- **Resource handling**: Proper cleanup, try-with-resources
|
||||
- **Concurrency issues**: Thread safety if applicable
|
||||
- **State management**: Proper initialization, mutation patterns
|
||||
- **IdeaVim enablement checks**: Verify that `injector.enabler.isEnabled()` or `Editor.isIdeaVimDisabledHere` are not missed in places where they should be checked. These functions determine if IdeaVim is active and should be called before performing Vim-specific operations
|
||||
|
||||
### Architecture & Design
|
||||
- **Separation of concerns**: Does the code have a single responsibility?
|
||||
- **Dependency direction**: Are dependencies pointing the right way?
|
||||
- **Abstraction level**: Consistent level of abstraction within methods
|
||||
- **Vim architecture alignment**: Does it match Vim's design philosophy?
|
||||
- **IntelliJ Platform conventions**: Proper use of platform APIs
|
||||
|
||||
### Testing
|
||||
- **Test coverage**: Are there tests for the code you're reviewing?
|
||||
- If checking a specific command or function, verify that tests exist for it
|
||||
- If tests exist, check if they cover the needed cases (edge cases, error conditions, typical usage)
|
||||
- If tests don't exist or coverage is incomplete, consider creating comprehensive test coverage
|
||||
- **Test quality**: Do tests cover edge cases?
|
||||
- **Test naming**: Clear, descriptive test names
|
||||
- **Flaky tests**: Any potentially unstable tests?
|
||||
- **Regression tests for bug fixes**: When fixing a bug, always write a test that:
|
||||
- Would fail with the old (buggy) implementation
|
||||
- Passes with the fixed implementation
|
||||
- Clearly documents what bug it's testing (include comments explaining the issue)
|
||||
- Tests the specific boundary condition or edge case that exposed the bug
|
||||
- This ensures the bug doesn't resurface in future refactorings
|
||||
|
||||
## 3. Investigation Strategy
|
||||
|
||||
Don't just look at surface-level issues. Dig deeper:
|
||||
|
||||
1. **Read the code**: Understand what it does before suggesting changes
|
||||
2. **Check related files**: Look at callers, implementations, tests
|
||||
3. **Look at git history**: `git log --oneline <file>` to understand context
|
||||
4. **Find related issues**: Search for TODOs, FIXMEs, or commented code
|
||||
5. **Run tests**: If you make changes, ensure tests pass
|
||||
6. **Check YouTrack**: Look for related issues if you find bugs
|
||||
|
||||
## 4. When to Make Changes
|
||||
|
||||
**DO fix**:
|
||||
- Clear bugs or logic errors
|
||||
- Obvious code quality issues (unused imports, etc.)
|
||||
- Misleading or incorrect documentation
|
||||
- Code that violates established patterns
|
||||
- Security vulnerabilities
|
||||
- Performance issues with measurable impact
|
||||
|
||||
**DON'T fix**:
|
||||
- Stylistic preferences if existing code is consistent
|
||||
- Working code just to use "newer" patterns
|
||||
- Minor formatting if it's consistent with surrounding code
|
||||
- Things that are subjective or arguable
|
||||
- Massive refactorings without clear benefit
|
||||
|
||||
**When in doubt**: Document the issue in your report but don't make changes.
|
||||
|
||||
## 5. Making Changes
|
||||
|
||||
If you decide to make changes:
|
||||
|
||||
1. **Make focused commits**: One logical change per commit
|
||||
- If the change affects many files or is complicated, split it into multiple step-by-step commits
|
||||
- This makes it easier for reviewers to understand the changes
|
||||
- Example: First commit renames a function, second commit updates callers, third commit adds new functionality
|
||||
2. **Write clear commit messages**: Explain why, not just what
|
||||
3. **Run tests**: `./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test`
|
||||
|
||||
## 6. Examples
|
||||
|
||||
### Good Maintenance Examples
|
||||
|
||||
**Example 1: Found and fixed null safety issue**
|
||||
```
|
||||
Inspected: vim-engine/.../motion/VimMotionHandler.kt
|
||||
|
||||
Issues found:
|
||||
- Several nullable properties accessed without safe checks
|
||||
- Could cause NPE in edge cases with cursor at document end
|
||||
|
||||
Changes:
|
||||
- Added null checks with Elvis operator
|
||||
- Added early returns for invalid state
|
||||
- Added KDoc explaining preconditions
|
||||
```
|
||||
|
||||
**Example 2: No changes needed**
|
||||
```
|
||||
Inspected: src/.../action/change/ChangeLineAction.kt
|
||||
|
||||
Checked:
|
||||
- Code style and formatting ✓
|
||||
- Null safety ✓
|
||||
- Error handling ✓
|
||||
- Tests present and comprehensive ✓
|
||||
|
||||
Observations:
|
||||
- Code is well-structured and follows conventions
|
||||
- Good test coverage including edge cases
|
||||
- Documentation is clear
|
||||
- No issues found
|
||||
```
|
||||
|
||||
**Example 3: Found issues but didn't fix**
|
||||
```
|
||||
Inspected: tests/.../motion/MotionTests.kt
|
||||
|
||||
Issues noted:
|
||||
- Some test names could be more descriptive
|
||||
- Potential for extracting common setup code
|
||||
- Tests are comprehensive but could add edge case for empty file
|
||||
|
||||
Recommendation: These are minor quality-of-life improvements.
|
||||
Not critical, but could be addressed in future cleanup.
|
||||
```
|
||||
|
||||
## IdeaVim-Specific Considerations
|
||||
|
||||
- **Vim compatibility**: Changes should maintain compatibility with Vim behavior
|
||||
- **IntelliJ Platform**: Follow IntelliJ platform conventions and APIs
|
||||
- **Property tests**: Can be flaky - verify if test failures relate to your changes
|
||||
- **Action syntax**: Use `<Action>` in mappings, not `:action`
|
||||
- **Architecture & Guidelines**: Refer to [CONTRIBUTING.md](../CONTRIBUTING.md) for:
|
||||
- Architecture overview and where to find specific code
|
||||
- Testing guidelines and corner cases to consider
|
||||
- Common patterns and conventions
|
||||
- Information about awards for quality contributions
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Run tests (standard suite)
|
||||
./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test
|
||||
|
||||
# Run specific test class
|
||||
./gradlew test --tests "ClassName"
|
||||
|
||||
# Check code style
|
||||
./gradlew ktlintCheck
|
||||
|
||||
# Format code
|
||||
./gradlew ktlintFormat
|
||||
|
||||
# Run IdeaVim in dev instance
|
||||
./gradlew runIde
|
||||
```
|
||||
|
||||
## Final Notes
|
||||
|
||||
- **Be thorough but practical**: Don't waste time on nitpicks
|
||||
- **Context matters**: Understand why code is the way it is before changing
|
||||
- **Quality over quantity**: One good fix is better than ten trivial changes
|
||||
- **Document your process**: Help future maintainers understand your thinking
|
||||
- **Learn from the code**: Use this as an opportunity to understand the codebase better
|
||||
|
||||
Remember: The goal is to keep the codebase healthy, not to achieve perfection. Focus on genuine improvements that make the code safer, clearer, or more maintainable.
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
23
.github/workflows/claude-code-review.yml
vendored
23
.github/workflows/claude-code-review.yml
vendored
@@ -3,26 +3,13 @@ name: Claude Code Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
@@ -37,6 +24,10 @@ jobs:
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
CONTRIBUTOR: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
Please review this pull request and provide feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
@@ -44,11 +35,13 @@ jobs:
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
|
||||
Provide detailed feedback using inline comments for specific issues.
|
||||
|
||||
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
||||
|
||||
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
||||
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
|
||||
claude_args: '--allowed-tools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"'
|
||||
|
||||
|
||||
55
.github/workflows/codebaseMaintenance.yml
vendored
Normal file
55
.github/workflows/codebaseMaintenance.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Codebase Maintenance with Claude
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every day at 6 AM UTC
|
||||
- cron: '0 6 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
maintain-codebase:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'JetBrains/ideavim'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
issues: read
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Need history for context
|
||||
|
||||
- name: Run Claude Code for Codebase Maintenance
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
prompt: |
|
||||
## Task: Perform Codebase Maintenance
|
||||
|
||||
Your goal is to inspect a random part of the IdeaVim codebase and perform maintenance checks.
|
||||
|
||||
Please follow the detailed maintenance instructions in `.claude/maintenance-instructions.md`.
|
||||
|
||||
## Creating Pull Requests
|
||||
|
||||
**Only create a pull request if you made changes to the codebase.**
|
||||
|
||||
If you made changes, create a PR with:
|
||||
- **Title**: "Maintenance: <area> - <brief description>"
|
||||
- Example: "Maintenance: VimMotionHandler - Fix null safety issues"
|
||||
- **Body** including:
|
||||
- What area you inspected
|
||||
- Issues you found
|
||||
- Changes you made
|
||||
- Why the changes improve the code
|
||||
|
||||
If no changes are needed, do not create a pull request.
|
||||
|
||||
# Allow Claude to use necessary tools for code inspection and maintenance
|
||||
claude_args: '--allowed-tools "Read,Edit,Write,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(./gradlew:*),Bash(find:*),Bash(shuf:*)"'
|
||||
5
.teamcity/_Self/Project.kt
vendored
5
.teamcity/_Self/Project.kt
vendored
@@ -5,6 +5,7 @@ import _Self.buildTypes.LongRunning
|
||||
import _Self.buildTypes.Nvim
|
||||
import _Self.buildTypes.PluginVerifier
|
||||
import _Self.buildTypes.PropertyBased
|
||||
import _Self.buildTypes.RandomOrderTests
|
||||
import _Self.buildTypes.TestingBuildType
|
||||
import _Self.subprojects.GitHub
|
||||
import _Self.subprojects.Releases
|
||||
@@ -16,7 +17,8 @@ import jetbrains.buildServer.configs.kotlin.v2019_2.Project
|
||||
object Project : Project({
|
||||
description = "Vim engine for JetBrains IDEs"
|
||||
|
||||
subProjects(Releases, GitHub)
|
||||
subProject(Releases)
|
||||
subProject(GitHub)
|
||||
|
||||
// VCS roots
|
||||
vcsRoot(GitHubPullRequest)
|
||||
@@ -30,6 +32,7 @@ object Project : Project({
|
||||
|
||||
buildType(PropertyBased)
|
||||
buildType(LongRunning)
|
||||
buildType(RandomOrderTests)
|
||||
|
||||
buildType(Nvim)
|
||||
buildType(PluginVerifier)
|
||||
|
||||
44
.teamcity/_Self/buildTypes/RandomOrderTests.kt
vendored
Normal file
44
.teamcity/_Self/buildTypes/RandomOrderTests.kt
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package _Self.buildTypes
|
||||
|
||||
import _Self.IdeaVimBuildType
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
|
||||
|
||||
object RandomOrderTests : IdeaVimBuildType({
|
||||
name = "Random order tests"
|
||||
description = "Running tests with random order on each run. This way we can catch order-dependent bugs."
|
||||
params {
|
||||
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
|
||||
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
|
||||
}
|
||||
|
||||
vcs {
|
||||
root(DslContext.settingsRoot)
|
||||
branchFilter = "+:<default>"
|
||||
|
||||
checkoutMode = CheckoutMode.AUTO
|
||||
}
|
||||
|
||||
steps {
|
||||
gradle {
|
||||
tasks = """
|
||||
clean test
|
||||
-x :tests:property-tests:test
|
||||
-x :tests:long-running-tests:test
|
||||
-Djunit.jupiter.execution.order.random.seed=default
|
||||
-Djunit.jupiter.testmethod.order.default=random
|
||||
""".trimIndent().replace("\n", " ")
|
||||
buildFile = ""
|
||||
enableStacktrace = true
|
||||
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
|
||||
}
|
||||
}
|
||||
|
||||
triggers {
|
||||
vcs {
|
||||
branchFilter = "+:<default>"
|
||||
}
|
||||
}
|
||||
})
|
||||
16
AUTHORS.md
16
AUTHORS.md
@@ -638,6 +638,22 @@ Contributors:
|
||||
[![icon][github]](https://github.com/magidc)
|
||||
|
||||
magidc
|
||||
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||
[![icon][github]](https://github.com/apps/github-actions)
|
||||
|
||||
github-actions[bot]
|
||||
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||
[![icon][github]](https://github.com/apps/github-actions)
|
||||
|
||||
github-actions[bot]
|
||||
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||
[![icon][github]](https://github.com/apps/github-actions)
|
||||
|
||||
github-actions[bot]
|
||||
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
|
||||
[![icon][github]](https://github.com/apps/github-actions)
|
||||
|
||||
github-actions[bot]
|
||||
|
||||
Previous contributors:
|
||||
|
||||
|
||||
@@ -130,8 +130,13 @@ Sed in orci mauris.
|
||||
Cras id tellus in ex imperdiet egestas.
|
||||
```
|
||||
|
||||
3. Don't forget to test your functionality with line start, line end, file start, file end, empty line, multiple
|
||||
carets, dollar motion, etc.
|
||||
3. Don't forget to test your functionality with various corner cases:
|
||||
- **Position-based**: line start, line end, file start, file end, empty line, single character line
|
||||
- **Content-based**: whitespace-only lines, lines with trailing spaces, mixed tabs and spaces, Unicode characters, multi-byte characters (e.g., emoji, CJK)
|
||||
- **Selection-based**: multiple carets, visual mode (character/line/block), empty selection
|
||||
- **Motion-based**: dollar motion, count with motion (e.g., `3w`, `5j`), zero-width motions
|
||||
- **Buffer state**: empty file, single line file, very long lines, read-only files
|
||||
- **Boundaries**: word boundaries with punctuation, sentence/paragraph boundaries, matching brackets at extremes
|
||||
|
||||
##### Neovim
|
||||
IdeaVim has an integration with neovim in tests. Tests that are performed with `doTest` also executed in
|
||||
|
||||
@@ -138,7 +138,6 @@ dependencies {
|
||||
|
||||
// AceJump is an optional dependency. We use their SessionManager class to check if it's active
|
||||
plugin("AceJump", "3.8.19")
|
||||
plugin("com.intellij.classic.ui", "251.23774.318")
|
||||
|
||||
bundledPlugins("org.jetbrains.plugins.terminal")
|
||||
|
||||
@@ -237,7 +236,6 @@ 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
|
||||
|
||||
@@ -180,12 +180,6 @@ Unless otherwise stated, these options do not have abbreviations.
|
||||
value is off. The equivalent processing for paste is controlled by the
|
||||
"ideaput" value to the 'clipboard' option.
|
||||
|
||||
'ideaglobalmode' boolean (default off)
|
||||
global
|
||||
This option will cause IdeaVim to share a single mode across all open
|
||||
windows. In other words, entering Insert mode in one window will
|
||||
enable Insert mode in all windows.
|
||||
|
||||
'ideajoin' boolean (default off)
|
||||
global or local to buffer
|
||||
When enabled, join commands will be handled by the IDE's "smart join"
|
||||
|
||||
@@ -20,7 +20,7 @@ ideaVersion=2025.1
|
||||
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
||||
ideaType=IC
|
||||
instrumentPluginCode=true
|
||||
version=chylex-52
|
||||
version=SNAPSHOT
|
||||
javaVersion=21
|
||||
remoteRobotVersion=0.11.23
|
||||
antlrVersion=4.10.1
|
||||
@@ -41,6 +41,7 @@ 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
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ object VimExtensionFacade {
|
||||
caret: ImmutableVimCaret,
|
||||
keys: List<KeyStroke?>?,
|
||||
) {
|
||||
caret.registerStorage.setKeys(register, keys?.filterNotNull() ?: emptyList())
|
||||
caret.registerStorage.setKeys(editor, context, register, keys?.filterNotNull() ?: emptyList())
|
||||
}
|
||||
|
||||
/** Set the current contents of the given register */
|
||||
|
||||
@@ -21,7 +21,9 @@ 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
|
||||
@@ -121,9 +123,9 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
|
||||
initialised = false
|
||||
}
|
||||
|
||||
override fun yankPerformed(editor: VimEditor, range: TextRange) {
|
||||
override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
|
||||
ensureInitialised()
|
||||
highlightHandler.highlightYankRange(editor.ij, range)
|
||||
highlightHandler.highlightYankRange(caretToRange)
|
||||
}
|
||||
|
||||
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
|
||||
@@ -144,13 +146,15 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
|
||||
private var lastEditor: Editor? = null
|
||||
private val highlighters = mutableSetOf<RangeHighlighter>()
|
||||
|
||||
fun highlightYankRange(editor: Editor, range: TextRange) {
|
||||
fun highlightYankRange(caretToRange: Map<ImmutableVimCaret, 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],
|
||||
@@ -161,6 +165,7 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
|
||||
)
|
||||
highlighters.add(highlighter)
|
||||
}
|
||||
}
|
||||
|
||||
// from vim-highlightedyank docs: A negative number makes the highlight persistent.
|
||||
val timeout = extractUsersHighlightDuration()
|
||||
|
||||
@@ -11,7 +11,7 @@ package com.maddyhome.idea.vim.extension.hints
|
||||
import com.intellij.ui.treeStructure.Tree
|
||||
import java.awt.Component
|
||||
import java.awt.Point
|
||||
import java.util.*
|
||||
import java.util.WeakHashMap
|
||||
import javax.accessibility.Accessible
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
@@ -44,9 +44,9 @@ internal sealed class HintGenerator {
|
||||
val hintIterator = alphabet.permutations(length).map { it.joinToString("") }.iterator()
|
||||
targets.forEach { target ->
|
||||
target.hint = if (preserve) {
|
||||
previousHints[target.component] ?: hintIterator.firstOrNull {
|
||||
previousHints[target.component] ?: hintIterator.firstOrNull { candidateHint ->
|
||||
// Check if the hint is not already used by previous targets
|
||||
!previousHints.values.any { hint -> hint.startsWith(it) || it.startsWith(hint) }
|
||||
!previousHints.values.any { existingHint -> existingHint.startsWith(candidateHint) || candidateHint.startsWith(existingHint) }
|
||||
} ?: return generate(targets, false) // do not preserve previous hints if failed
|
||||
} else {
|
||||
hintIterator.next()
|
||||
|
||||
@@ -230,7 +230,7 @@ private object FileTypePatterns {
|
||||
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
|
||||
this.cMakePatterns
|
||||
} else {
|
||||
this.htmlPatterns
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.maddyhome.idea.vim.extension.surround
|
||||
|
||||
import com.intellij.util.text.CharSequenceSubSequence
|
||||
|
||||
internal data class RepeatedCharSequence(val text: CharSequence, val count: Int) : CharSequence {
|
||||
override val length = text.length * count
|
||||
|
||||
override fun get(index: Int): Char {
|
||||
if (index < 0 || index >= length) throw IndexOutOfBoundsException()
|
||||
return text[index % text.length]
|
||||
}
|
||||
|
||||
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
|
||||
return CharSequenceSubSequence(this, startIndex, endIndex)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return text.repeat(count)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(text: CharSequence, count: Int): CharSequence {
|
||||
return when (count) {
|
||||
0 -> ""
|
||||
1 -> text
|
||||
else -> RepeatedCharSequence(text, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
@@ -37,10 +36,7 @@ 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
|
||||
@@ -82,7 +78,7 @@ internal class VimSurroundExtension : VimExtension {
|
||||
putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true)
|
||||
}
|
||||
|
||||
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false, count = 1)) // TODO
|
||||
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator())
|
||||
}
|
||||
|
||||
private class YSurroundHandler : ExtensionHandler {
|
||||
@@ -109,7 +105,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, count = operatorArguments.count1)
|
||||
performSurround(pair, range, it)
|
||||
}
|
||||
// it.moveToOffset(lineStartOffset)
|
||||
}
|
||||
@@ -132,13 +128,15 @@ 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(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
|
||||
if (!Operator().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
|
||||
@@ -161,10 +159,6 @@ 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 {
|
||||
@@ -207,7 +201,7 @@ internal class VimSurroundExtension : VimExtension {
|
||||
val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue
|
||||
it.first + trimmedValue + it.second
|
||||
} ?: innerValue
|
||||
val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), null)
|
||||
val textData = PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.CHARACTER_WISE)
|
||||
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false)
|
||||
|
||||
surrounding.caret to putData
|
||||
@@ -284,41 +278,20 @@ internal class VimSurroundExtension : VimExtension {
|
||||
}
|
||||
}
|
||||
|
||||
private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
|
||||
private class Operator : 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
|
||||
|
||||
runWriteAction {
|
||||
val change = VimPlugin.getChange()
|
||||
if (supportsMultipleCursors) {
|
||||
ijEditor.runWithEveryCaretAndRestore {
|
||||
applyOnce(ijEditor, change, pair, count)
|
||||
}
|
||||
}
|
||||
else {
|
||||
applyOnce(ijEditor, change, pair, count)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -407,15 +380,15 @@ private fun getOrInputPair(c: Char, editor: Editor, context: DataContext): Surro
|
||||
}
|
||||
|
||||
|
||||
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, count: Int, tagsOnNewLines: Boolean = false) {
|
||||
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, tagsOnNewLines: Boolean = false) {
|
||||
runWriteAction {
|
||||
val editor = caret.editor
|
||||
val change = VimPlugin.getChange()
|
||||
val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
|
||||
val leftSurround = pair.first + if (tagsOnNewLines) "\n" else ""
|
||||
|
||||
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 {
|
||||
@@ -423,7 +396,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)
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.maddyhome.idea.vim.newapi.ij
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
|
||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Provides all the insert/replace related functionality
|
||||
@@ -155,7 +156,6 @@ class ChangeGroup : VimChangeGroupBase() {
|
||||
context: ExecutionContext,
|
||||
range: TextRange,
|
||||
) {
|
||||
val startPos = editor.offsetToBufferPosition(caret.offset)
|
||||
val startOffset = editor.getLineStartForOffset(range.startOffset)
|
||||
val endOffset = editor.getLineEndForOffset(range.endOffset)
|
||||
val ijEditor = (editor as IjVimEditor).editor
|
||||
@@ -165,7 +165,7 @@ class ChangeGroup : VimChangeGroupBase() {
|
||||
var copiedText: IjVimCopiedText? = null
|
||||
try {
|
||||
if (injector.registerGroup.isPrimaryRegisterSupported()) {
|
||||
copiedText = injector.clipboardManager.getPrimaryContent() as IjVimCopiedText
|
||||
copiedText = injector.clipboardManager.getPrimaryContent(editor, context) as IjVimCopiedText
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// FIXME: [isPrimaryRegisterSupported()] is not implemented perfectly, so there might be thrown an exception after trying to access the primary selection
|
||||
@@ -180,7 +180,11 @@ class ChangeGroup : VimChangeGroupBase() {
|
||||
}
|
||||
}
|
||||
val afterAction = {
|
||||
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, startPos.line))
|
||||
val firstLine = editor.offsetToBufferPosition(
|
||||
min(startOffset.toDouble(), endOffset.toDouble()).toInt()
|
||||
).line
|
||||
val newOffset = injector.motion.moveCaretToLineStartSkipLeading(editor, firstLine)
|
||||
caret.moveToOffset(newOffset)
|
||||
restoreCursor(editor, caret, (caret as IjVimCaret).caret.logicalPosition.line)
|
||||
}
|
||||
if (project != null) {
|
||||
|
||||
@@ -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", true, isHidden = true))
|
||||
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true))
|
||||
val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
|
||||
val vimHints: ToggleOption = addOption(ToggleOption("vimhints", GLOBAL, "vimhints", false, isHidden = true))
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.helper.MessageHelper
|
||||
import com.maddyhome.idea.vim.macro.VimMacroBase
|
||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||
import com.maddyhome.idea.vim.newapi.ij
|
||||
|
||||
/**
|
||||
* Used to handle playback of macros
|
||||
@@ -90,9 +89,6 @@ internal class MacroGroup : VimMacroBase() {
|
||||
} finally {
|
||||
keyStack.removeFirst()
|
||||
}
|
||||
if (!isInternalMacro) {
|
||||
MacroAutoImport.run(editor.ij, context.ij)
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalMacro) {
|
||||
|
||||
@@ -87,9 +87,6 @@ internal 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)
|
||||
}
|
||||
@@ -98,15 +95,6 @@ internal 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)
|
||||
@@ -117,9 +105,6 @@ internal 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)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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,6 +41,8 @@ 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
|
||||
@@ -59,11 +62,55 @@ internal class NotificationService(private val project: Project?) {
|
||||
@Suppress("unused")
|
||||
constructor() : this(null)
|
||||
|
||||
fun notifyAboutNewUndo() {}
|
||||
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,
|
||||
)
|
||||
|
||||
fun notifyAboutIdeaPut() {}
|
||||
notification.addAction(OpenIdeaVimRcAction(notification))
|
||||
|
||||
fun notifyAboutIdeaJoin(editor: VimEditor) {}
|
||||
notification.addAction(
|
||||
AppendToIdeaVimRcAction(
|
||||
notification,
|
||||
"set clipboard^=ideaput",
|
||||
"ideaput",
|
||||
) {
|
||||
// Technically, we're supposed to prepend values to clipboard so that it's not added to the "exclude" item.
|
||||
// Since we don't handle exclude, it's safe to append. But let's be clean.
|
||||
injector.globalOptions().clipboard.prependValue(OptionConstants.clipboard_ideaput)
|
||||
},
|
||||
)
|
||||
|
||||
notification.notify(project)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun enableRepeatingMode() = Messages.showYesNoDialog(
|
||||
"Do you want to enable repeating keys in macOS on press and hold?\n\n" +
|
||||
|
||||
@@ -25,9 +25,10 @@ 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
|
||||
*/
|
||||
@@ -127,7 +128,7 @@ public class RegisterGroup extends VimRegisterGroupBase implements PersistentSta
|
||||
final String text = VimPlugin.getXML().getSafeXmlText(textElement);
|
||||
if (text != null) {
|
||||
logger.trace("Register data parsed");
|
||||
register = new Register(key, type, text, Collections.emptyList());
|
||||
register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type);
|
||||
}
|
||||
else {
|
||||
logger.trace("Cannot parse register data");
|
||||
|
||||
@@ -37,6 +37,7 @@ 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 +128,7 @@ internal class PutGroup : VimPutBase() {
|
||||
point.dispose()
|
||||
if (!caret.isValid) return@forEach
|
||||
|
||||
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.text.length)
|
||||
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.copiedText.text.length)
|
||||
val endOffset = if (data.indent) {
|
||||
doIndent(
|
||||
vimEditor,
|
||||
@@ -179,12 +180,10 @@ internal class PutGroup : VimPutBase() {
|
||||
val allContentsBefore = CopyPasteManager.getInstance().allContents
|
||||
val sizeBeforeInsert = allContentsBefore.size
|
||||
val firstItemBefore = allContentsBefore.firstOrNull()
|
||||
logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" }
|
||||
logger.debug { "Copied text: ${text.copiedText}" }
|
||||
val (textContent, transferableData) = text.copiedText as IjVimCopiedText
|
||||
val origContent: TextBlockTransferable =
|
||||
injector.clipboardManager.setClipboardText(
|
||||
text.text,
|
||||
transferableData = text.transferableData,
|
||||
) as TextBlockTransferable
|
||||
injector.clipboardManager.setClipboardText(textContent, textContent, transferableData) as TextBlockTransferable
|
||||
val allContentsAfter = CopyPasteManager.getInstance().allContents
|
||||
val sizeAfterInsert = allContentsAfter.size
|
||||
try {
|
||||
@@ -192,7 +191,7 @@ internal class PutGroup : VimPutBase() {
|
||||
} finally {
|
||||
val textInClipboard = (firstItemBefore as? TextBlockTransferable)
|
||||
?.getTransferData(DataFlavor.stringFlavor) as? String
|
||||
val textOnTop = textInClipboard != null && textInClipboard != text.text
|
||||
val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.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) }
|
||||
|
||||
@@ -81,8 +81,12 @@ internal object IdeaSelectionControl {
|
||||
return@singleTask
|
||||
}
|
||||
|
||||
// The editor changed the selection, but we want to keep the current Vim mode. This is the case if we're editing
|
||||
// a template and either 'idearefactormode' is set to `keep`, or the current Vim mode already has a selection.
|
||||
// (i.e., if the user explicitly enters Visual or Select and then moves to the next template variable, don't
|
||||
// switch to Select or Visual - keep the current Vim selection mode)
|
||||
if (dontChangeMode(editor)) {
|
||||
IdeaRefactorModeHelper.correctSelection(editor)
|
||||
IdeaRefactorModeHelper.correctEditorSelection(editor)
|
||||
logger.trace { "Selection corrected for refactoring" }
|
||||
return@singleTask
|
||||
}
|
||||
@@ -146,8 +150,13 @@ internal object IdeaSelectionControl {
|
||||
}
|
||||
|
||||
private fun chooseNonSelectionMode(editor: Editor): Mode {
|
||||
val templateActive = editor.isTemplateActive()
|
||||
if (templateActive && editor.vim.mode.inNormalMode || editor.inInsertMode) {
|
||||
// If we're in an active template and the editor has just removed a selection without adding a new one, we're in a
|
||||
// variable with nothing to select. When 'idearefactormode' is "select", enter Insert mode. Otherwise, stay in
|
||||
// Normal.
|
||||
// Note that when 'idearefactormode' is "visual", we enter Normal mode for an empty variable. While it might seem
|
||||
// natural to want to insert text here, we could also paste a register, dot-repeat an insertion or other Normal
|
||||
// commands. Normal is Visual without the selection.
|
||||
if ((editor.isTemplateActive() && editor.vim.mode.inNormalMode && editor.vim.isIdeaRefactorModeSelect) || editor.inInsertMode) {
|
||||
return Mode.INSERT
|
||||
}
|
||||
return Mode.NORMAL()
|
||||
|
||||
@@ -352,7 +352,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) + editor.getSettings().getAdditionalLinesCount();
|
||||
final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) - 1;
|
||||
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.
|
||||
|
||||
@@ -12,9 +12,7 @@ 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
|
||||
@@ -23,8 +21,6 @@ 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
|
||||
@@ -102,7 +98,8 @@ internal fun Editor.isPrimaryEditor(): Boolean {
|
||||
internal fun Editor.isTerminalEditor(): Boolean {
|
||||
return !isViewer
|
||||
&& document.isWritable
|
||||
&& this.editorKind == EditorKind.CONSOLE
|
||||
&& !EditorHelper.isFileEditor(this)
|
||||
&& !EditorHelper.isDiffEditor(this)
|
||||
}
|
||||
|
||||
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
|
||||
@@ -135,41 +132,3 @@ internal val Caret.vimLine: Int
|
||||
*/
|
||||
internal val Editor.vimLine: Int
|
||||
get() = this.caretModel.currentCaret.vimLine
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,19 +25,15 @@ 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
|
||||
@@ -74,12 +70,6 @@ internal 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
|
||||
@@ -89,20 +79,6 @@ internal 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
|
||||
|
||||
@@ -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() + editor.settings.additionalLinesCount
|
||||
val lastLine = vimEditor.getVisualLineCount() - 1
|
||||
|
||||
// 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
|
||||
|
||||
@@ -17,7 +17,6 @@ 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
|
||||
@@ -31,7 +30,6 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||
import org.jetbrains.annotations.Contract
|
||||
import java.awt.Font
|
||||
import java.util.*
|
||||
import javax.swing.Timer
|
||||
|
||||
internal fun updateSearchHighlights(
|
||||
pattern: String?,
|
||||
@@ -86,12 +84,6 @@ internal fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, en
|
||||
)
|
||||
}
|
||||
|
||||
val removeHighlightsEditors = mutableListOf<Editor>()
|
||||
val removeHighlightsTimer = Timer(400) {
|
||||
removeHighlightsEditors.forEach(::removeSearchHighlights)
|
||||
removeHighlightsEditors.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes current search highlights for all visible editors
|
||||
*/
|
||||
@@ -133,28 +125,17 @@ 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 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),
|
||||
searchStartLine,
|
||||
searchEndLine,
|
||||
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
|
||||
)
|
||||
if (results.isNotEmpty()) {
|
||||
@@ -162,14 +143,9 @@ private fun updateSearchHighlights(
|
||||
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
|
||||
}
|
||||
highlightSearchResults(editor, pattern, results, currentMatchOffset)
|
||||
if (!isSearching) {
|
||||
removeHighlightsEditors.add(editor)
|
||||
removeHighlightsTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -203,7 +179,6 @@ private fun updateSearchHighlights(
|
||||
}
|
||||
}
|
||||
|
||||
removeHighlightsTimer.restart()
|
||||
return currentEditorCurrentMatchOffset
|
||||
}
|
||||
|
||||
@@ -229,7 +204,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 != ""
|
||||
return hlSearch && newPattern != null && newPattern != editor.vimLastSearch && newPattern != ""
|
||||
}
|
||||
|
||||
private fun findClosestMatch(
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -30,8 +29,6 @@ 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
|
||||
|
||||
/**
|
||||
@@ -85,7 +82,15 @@ internal 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)
|
||||
restoreVisualMode(editor)
|
||||
|
||||
// We execute undo one more time if the previous one just restored selection
|
||||
if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
|
||||
undoManager.undo(fileEditor)
|
||||
}
|
||||
}
|
||||
|
||||
CommandProcessor.getInstance().runUndoTransparentAction {
|
||||
removeSelections(editor)
|
||||
}
|
||||
} else {
|
||||
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
|
||||
@@ -236,21 +241,4 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService {
|
||||
val hasChanges: Boolean
|
||||
get() = changeListener.hasChanged || initialPath != editor.getPath()
|
||||
}
|
||||
|
||||
private fun restoreVisualMode(editor: VimEditor) {
|
||||
if (!editor.inVisualMode && editor.getSelectionModel().hasSelection()) {
|
||||
val detectedMode = VimPlugin.getVisualMotion().detectSelectionType(editor)
|
||||
|
||||
// Visual block selection is restored into multiple carets, so multi-carets that form a block are always
|
||||
// identified as visual block mode, leading to false positives.
|
||||
// Since I use visual block mode much less often than multi-carets, this is a judgment call to never restore
|
||||
// visual block mode.
|
||||
val wantedMode = if (detectedMode == SelectionType.BLOCK_WISE)
|
||||
SelectionType.CHARACTER_WISE
|
||||
else
|
||||
detectedMode
|
||||
|
||||
VimPlugin.getVisualMotion().enterVisualMode(editor, wantedMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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.common.InsertSequence
|
||||
@@ -97,6 +98,7 @@ internal var Caret.vimInsertStart: RangeMarker by userDataOr {
|
||||
}
|
||||
|
||||
// TODO: Data could be lost during visual block motion
|
||||
internal var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor()
|
||||
internal var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
|
||||
internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
|
||||
|
||||
|
||||
@@ -64,10 +64,8 @@ class IJEditorFocusListener : EditorListener {
|
||||
VimPlugin.getChange().insertBeforeCaret(editor, context)
|
||||
KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true)
|
||||
}
|
||||
if (isCurrentEditorTerminal) {
|
||||
if (!ijEditor.inInsertMode) {
|
||||
if (isCurrentEditorTerminal && !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
|
||||
|
||||
@@ -16,9 +16,8 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
|
||||
import com.intellij.codeInsight.template.Template
|
||||
import com.intellij.codeInsight.template.TemplateEditingAdapter
|
||||
import com.intellij.codeInsight.template.TemplateManagerListener
|
||||
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
|
||||
import com.intellij.codeInsight.template.impl.TemplateImpl
|
||||
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
|
||||
@@ -33,25 +32,30 @@ 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
|
||||
import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.action.VimShortcutKeyAction
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.options
|
||||
import com.maddyhome.idea.vim.group.NotificationService
|
||||
import com.maddyhome.idea.vim.group.RegisterGroup
|
||||
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
|
||||
import com.maddyhome.idea.vim.helper.exitSelectMode
|
||||
import com.maddyhome.idea.vim.helper.exitVisualMode
|
||||
import com.maddyhome.idea.vim.helper.hasVisualSelection
|
||||
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
|
||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||
import com.maddyhome.idea.vim.newapi.initInjector
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import com.maddyhome.idea.vim.state.mode.inNormalMode
|
||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper
|
||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
|
||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
@@ -66,7 +70,6 @@ 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) {
|
||||
@@ -75,7 +78,6 @@ 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
|
||||
@@ -152,10 +154,8 @@ internal object IdeaSpecifics {
|
||||
if (VimPlugin.isNotEnabled()) return
|
||||
|
||||
val editor = editor
|
||||
if (editor != null) {
|
||||
if (action is ChooseItemAction && injector.registerGroup.isRecording) {
|
||||
completionData?.recordCompletion(editor, VimPlugin.getRegister()
|
||||
)
|
||||
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
|
||||
completionData?.recordCompletion(editor, VimPlugin.getRegister())
|
||||
}
|
||||
|
||||
//region Enter insert mode after surround with if
|
||||
@@ -171,27 +171,9 @@ internal object IdeaSpecifics {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
this.editor = null
|
||||
this.caretOffset = -1
|
||||
|
||||
this.completionData?.dispose()
|
||||
this.completionData = null
|
||||
@@ -235,7 +217,44 @@ internal object IdeaSpecifics {
|
||||
}
|
||||
}
|
||||
|
||||
//region Enter insert mode for surround templates without selection
|
||||
//region Handle mode and selection for Live Templates and refactorings
|
||||
/**
|
||||
* Listen to template notifications to provide additional handling of templates, selection and Vim mode
|
||||
*
|
||||
* Most of the handling of templates is done by [IdeaSelectionControl]. When moving between editable segments of a
|
||||
* template, the editor will remove the current selection and add a selection for the text of the new segment, if any.
|
||||
* [IdeaSelectionControl] will be notified and handle Vim mode based on the `'idearefactormode'` option. This will
|
||||
* switch to Select mode for each variable by default (or Insert if there's no text) or Visual/Normal mode when the
|
||||
* option is set to "visual".
|
||||
*
|
||||
* Select mode makes IdeaVim behaviour a little more like a traditional editor. Inserting a Live Template is typically
|
||||
* done in Insert mode, so moving to the next editable segment always switches to Select mode, ready to continue
|
||||
* typing. At the end of the template, the caret is still in Insert mode, still ready to continue typing.
|
||||
*
|
||||
* The exception is for an "inline" template. This is an editable segment placed on top of existing text and is
|
||||
* typically used to rename a symbol. This is usually a refactoring started with an explicit action, and switching to
|
||||
* Select mode means the user is ready to start typing. However, accepting the change should switch back to Normal, as
|
||||
* the editing/refactoring is complete. This also helps when the refactoring shows a progress dialog, since Escape to
|
||||
* switch back to Normal can cancel the refactoring.
|
||||
*
|
||||
* Again, this is like a traditional editor, and still only changes Vim mode based on an explicit action.
|
||||
*
|
||||
* This class handles the following edge cases that [IdeaSelectionControl] cannot:
|
||||
* * When `'idearefactormode'` is "keep" it will maintain the current Vim mode, by removing the current selection
|
||||
* (so [IdeaSelectionControl] does nothing). It also ensures that the current Vim selection mode matches the actual
|
||||
* selection (i.e., character wise vs line wise).
|
||||
* * When the editor is moving to the next segment but there is no current selection and the next segment is empty,
|
||||
* there will be no change in selection and [IdeaSelectionControl] will not be notified. This class will switch to
|
||||
* Insert mode when `'idearefactormode'` is set to "select" and Normal when it's "visual".
|
||||
* * A special case of the above scenario is moving to the end of the template, which always has no selection. If
|
||||
* there is no current selection [IdeaSelectionControl] is not called and the mode is not updated, so we stay in
|
||||
* whatever mode the user had last - Insert, Normal, whatever. When there is a selection, [IdeaSelectionControl]
|
||||
* will be called, but since there is no template active anymore, it would set the mode to Normal. This class will
|
||||
* switch to Insert when `'idearefactormode'` is "select" and Normal for "visual". It does nothing for "keep".
|
||||
* * If the template is an "inline" template, it is typically a rename refactoring on existing text.
|
||||
* When ending the template and `'idearefactormode'` is "select", the above would leave is in Insert mode. This
|
||||
* class will switch to Normal for inline templates, for both "select" and "visual". It does nothing for "keep".
|
||||
*/
|
||||
class VimTemplateManagerListener : TemplateManagerListener {
|
||||
override fun templateStarted(state: TemplateState) {
|
||||
if (VimPlugin.isNotEnabled()) return
|
||||
@@ -248,27 +267,65 @@ internal object IdeaSpecifics {
|
||||
oldIndex: Int,
|
||||
newIndex: Int,
|
||||
) {
|
||||
if (templateState.editor.vim.isIdeaRefactorModeKeep) {
|
||||
IdeaRefactorModeHelper.correctSelection(templateState.editor)
|
||||
fun VimEditor.exitMode() = when (this.mode) {
|
||||
is Mode.SELECT -> this.exitSelectMode(adjustCaretPosition = false)
|
||||
is Mode.VISUAL -> this.exitVisualMode()
|
||||
is Mode.INSERT -> this.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(this))
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
fun Template?.myIsInline() = this is TemplateImpl && this.isInline
|
||||
|
||||
val vimEditor = editor.vim
|
||||
|
||||
// This function is called when moving between variables. It is called with oldIndex == -1 when moving to the
|
||||
// first variable, and newIndex == -1 just before ending, when moving to the end of the template text, or to
|
||||
// $END$ (which is treated as a segment, but not a variable). If there are no variables, it is called with
|
||||
// oldIndex == newIndex == -1.
|
||||
if (vimEditor.isIdeaRefactorModeKeep) {
|
||||
IdeaRefactorModeHelper.correctEditorSelection(templateState.editor)
|
||||
}
|
||||
else {
|
||||
// The editor places the caret at the exclusive end of the variable. For Visual, unless we've enabled
|
||||
// exclusive selection, move it to the inclusive end.
|
||||
// Note that "keep" does this as part of IdeaRefactorModeHelper
|
||||
if (editor.selectionModel.hasSelection()
|
||||
&& !editor.vim.isIdeaRefactorModeSelect
|
||||
&& templateState.currentVariableRange?.endOffset == editor.caretModel.offset
|
||||
&& !injector.options(vimEditor).selection.contains("exclusive")
|
||||
) {
|
||||
vimEditor.primaryCaret()
|
||||
.moveToInlayAwareOffset((editor.selectionModel.selectionEnd - 1).coerceAtLeast(editor.selectionModel.selectionStart))
|
||||
}
|
||||
|
||||
if (newIndex == -1 && template.myIsInline() && vimEditor.isIdeaRefactorModeSelect) {
|
||||
// Rename refactoring has just completed with 'idearefactormode' in "select". Return to Normal instead of
|
||||
// our default behaviour of switching to Insert
|
||||
if (vimEditor.mode !is Mode.NORMAL) {
|
||||
vimEditor.exitMode()
|
||||
vimEditor.mode = Mode.NORMAL()
|
||||
}
|
||||
} else {
|
||||
// IdeaSelectionControl will not be called if we're moving to a new variable with no change in selection.
|
||||
// And if we're moving to the end of the template, the change in selection will reset us to Normal because
|
||||
// IdeaSelectionControl will be called when the template is no longer active.
|
||||
if ((!editor.selectionModel.hasSelection() && !vimEditor.mode.hasVisualSelection) || newIndex == -1) {
|
||||
if (vimEditor.isIdeaRefactorModeSelect) {
|
||||
if (vimEditor.mode !is Mode.INSERT) {
|
||||
vimEditor.exitMode()
|
||||
injector.application.runReadAction {
|
||||
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
|
||||
VimPlugin.getChange().insertBeforeCaret(editor.vim, context)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vimEditor.mode = Mode.NORMAL()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (state.editor.vim.isIdeaRefactorModeKeep) {
|
||||
IdeaRefactorModeHelper.correctSelection(editor)
|
||||
} else {
|
||||
if (!editor.selectionModel.hasSelection()) {
|
||||
// Enable insert mode if there is no selection in template
|
||||
// Template with selection is handled by [com.maddyhome.idea.vim.group.visual.VisualMotionGroup.controlNonVimSelectionChange]
|
||||
if (editor.vim.inNormalMode) {
|
||||
VimPlugin.getChange().insertBeforeCaret(
|
||||
editor.vim,
|
||||
injector.executionContextManager.getEditorExecutionContext(editor.vim),
|
||||
)
|
||||
KeyHandler.getInstance().reset(editor.vim)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
@@ -97,7 +97,6 @@ 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,20 +411,9 @@ internal 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)
|
||||
}
|
||||
|
||||
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
|
||||
FileGroup.fileEditorManagerSelectionChangedCallback(event)
|
||||
// VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
|
||||
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
|
||||
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
|
||||
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import java.io.IOException
|
||||
|
||||
@Service
|
||||
internal class IjClipboardManager : VimClipboardManager {
|
||||
override fun getPrimaryContent(): IjVimCopiedText? {
|
||||
override fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): IjVimCopiedText? {
|
||||
val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null
|
||||
val contents = clipboard.getContents(null) ?: return null
|
||||
val (text, transferableData) = getTextAndTransferableData(contents) ?: return null
|
||||
@@ -242,6 +242,6 @@ internal class IjClipboardManager : VimClipboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
data class IjVimCopiedText(override val text: String, override val transferableData: List<Any>) : VimCopiedText {
|
||||
data class IjVimCopiedText(override val text: String, val transferableData: List<Any>) : VimCopiedText {
|
||||
override fun updateText(newText: String): VimCopiedText = IjVimCopiedText(newText, transferableData)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ 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.ImmutableVimCaret
|
||||
import com.maddyhome.idea.vim.api.LocalMarkStorage
|
||||
import com.maddyhome.idea.vim.api.SelectionInfo
|
||||
@@ -19,7 +21,6 @@ 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,6 +29,7 @@ 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
|
||||
@@ -35,14 +37,22 @@ 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
|
||||
|
||||
internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
|
||||
|
||||
override val registerStorage: VimRegisterGroup
|
||||
get() = injector.registerGroup
|
||||
|
||||
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 markStorage: LocalMarkStorage
|
||||
get() {
|
||||
var storage = this.caret.markStorage
|
||||
|
||||
@@ -20,7 +20,6 @@ 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.maddyhome.idea.vim.api.BufferPosition
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
@@ -35,7 +34,6 @@ import com.maddyhome.idea.vim.api.VimEditorBase
|
||||
import com.maddyhome.idea.vim.api.VimFoldRegion
|
||||
import com.maddyhome.idea.vim.api.VimIndentConfig
|
||||
import com.maddyhome.idea.vim.api.VimScrollingModel
|
||||
import com.maddyhome.idea.vim.api.VimSelectionModel
|
||||
import com.maddyhome.idea.vim.api.VimVirtualFile
|
||||
import com.maddyhome.idea.vim.api.VimVisualPosition
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
@@ -150,7 +148,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.document.insertString(atPosition, StringUtil.convertLineSeparators(text, "\n"))
|
||||
editor.document.insertString(atPosition, text)
|
||||
}
|
||||
|
||||
override fun replaceString(start: Int, end: Int, newString: String) {
|
||||
@@ -179,38 +177,21 @@ internal 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 {
|
||||
try {
|
||||
editor.caretModel.runForEachCaret({
|
||||
if (it.isValid) {
|
||||
action(IjVimCaret(it))
|
||||
isFirstCaret = false
|
||||
}
|
||||
}, false)
|
||||
} finally {
|
||||
isFirstCaret = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {
|
||||
isReversingCarets = reverse
|
||||
try {
|
||||
editor.caretModel.runForEachCaret({
|
||||
action(IjVimCaret(it))
|
||||
isFirstCaret = false
|
||||
}, reverse)
|
||||
} finally {
|
||||
isFirstCaret = true
|
||||
isReversingCarets = false
|
||||
}
|
||||
editor.caretModel.runForEachCaret({ action(IjVimCaret(it)) }, reverse)
|
||||
}
|
||||
|
||||
override fun isInForEachCaretScope(): Boolean {
|
||||
@@ -316,18 +297,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
|
||||
editor.document.deleteString(range.startOffset, range.endOffset)
|
||||
}
|
||||
|
||||
override fun getSelectionModel(): VimSelectionModel {
|
||||
return object : VimSelectionModel {
|
||||
private val sm = editor.selectionModel
|
||||
override val selectionStart = sm.selectionStart
|
||||
override val selectionEnd = sm.selectionEnd
|
||||
|
||||
override fun hasSelection(): Boolean {
|
||||
return sm.hasSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScrollingModel(): VimScrollingModel {
|
||||
return object : VimScrollingModel {
|
||||
private val sm = editor.scrollingModel as ScrollingModelEx
|
||||
@@ -520,10 +489,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
|
||||
return editor.softWrapModel.getSoftWrap(offset)?.start
|
||||
}
|
||||
|
||||
override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T {
|
||||
return caret
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ internal class IjVimMessages : VimMessagesBase() {
|
||||
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
|
||||
// things happen.
|
||||
override fun clearStatusBarMessage() {
|
||||
if (message.isNullOrEmpty()) return
|
||||
val currentMessage = message
|
||||
if (currentMessage.isNullOrEmpty()) return
|
||||
|
||||
// Don't clear the status bar message if we've only just set it
|
||||
if (!allowClearStatusBarMessage) return
|
||||
@@ -71,7 +72,7 @@ internal class IjVimMessages : VimMessagesBase() {
|
||||
ProjectManager.getInstance().openProjects.forEach { project ->
|
||||
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
|
||||
// Only clear the status bar if it's showing our last message
|
||||
if (statusBar.info?.contains(message.toString()) == true) {
|
||||
if (statusBar.info?.contains(currentMessage) == true) {
|
||||
statusBar.info = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2023 The IdeaVim authors
|
||||
* Copyright 2003-2025 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -14,7 +14,7 @@ import com.intellij.util.ui.JBInsets
|
||||
import java.awt.Component
|
||||
import java.awt.Insets
|
||||
|
||||
internal class ExPanelBorder internal constructor() : SideBorder(JBColor.border(), TOP) {
|
||||
internal class ExPanelBorder : SideBorder(JBColor.border(), TOP) {
|
||||
|
||||
override fun getBorderInsets(component: Component?): Insets {
|
||||
return JBInsets(getThickness() + 2, 0, 2, 2)
|
||||
|
||||
@@ -283,7 +283,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
.calculateCount0Snapshot()
|
||||
)
|
||||
|
||||
if ((labelText == "/" || labelText == "?" || searchCommand) && !injector.macro.isExecutingMacro) {
|
||||
if (labelText == "/" || labelText == "?" || searchCommand) {
|
||||
val forwards = labelText != "?" // :s, :g, :v are treated as forwards
|
||||
val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0)
|
||||
val pattern = searchText.take(patternEnd)
|
||||
|
||||
@@ -20,7 +20,6 @@ import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
||||
import com.maddyhome.idea.vim.helper.VimLockLabel
|
||||
import com.maddyhome.idea.vim.helper.hasBlockOrUnderscoreCaret
|
||||
import com.maddyhome.idea.vim.helper.hasVisualSelection
|
||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||
@@ -87,36 +86,50 @@ internal object IdeaRefactorModeHelper {
|
||||
|
||||
@VimLockLabel.RequiresReadLock
|
||||
@RequiresReadLock
|
||||
fun calculateCorrections(editor: Editor): List<Action> {
|
||||
fun calculateCorrectionsToSyncEditorToMode(editor: Editor): List<Action> {
|
||||
val corrections = mutableListOf<Action>()
|
||||
val mode = editor.vim.mode
|
||||
|
||||
// If the current Vim mode doesn't have a selection, remove the editor's selection
|
||||
if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
|
||||
corrections.add(Action.RemoveSelection)
|
||||
}
|
||||
|
||||
// If the current mode does have a selection, make sure it matches the selection type of the editor
|
||||
if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
|
||||
val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim)
|
||||
if (mode.selectionType != selectionType) {
|
||||
val newMode = when (mode) {
|
||||
is Mode.SELECT -> mode.copy(selectionType)
|
||||
is Mode.VISUAL -> mode.copy(selectionType)
|
||||
is Mode.SELECT -> mode.copy(selectionType = selectionType)
|
||||
is Mode.VISUAL -> mode.copy(selectionType = selectionType)
|
||||
else -> error("IdeaVim should be either in visual or select modes")
|
||||
}
|
||||
corrections.add(Action.SetMode(newMode))
|
||||
}
|
||||
}
|
||||
|
||||
if (editor.hasBlockOrUnderscoreCaret()) {
|
||||
// IntelliJ places the caret on the exclusive end of the current variable. I.e. *after* the end of the variable.
|
||||
// This makes sense when selecting the current variable, and when the editor is using a bar caret - the selection is
|
||||
// naturally exclusive (and IdeaVim treats Select mode as having exclusive selection).
|
||||
// But we don't have a selection, so it's weird to be placed at the end of a selection that no longer exists. Move
|
||||
// the caret to the start of the (missing) selection instead.
|
||||
if (editor.vim.isIdeaRefactorModeKeep) {
|
||||
TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange ->
|
||||
if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) {
|
||||
corrections.add(Action.MoveToOffset(editor.caretModel.offset - 1))
|
||||
if (!segmentRange.isEmpty && segmentRange.startOffset != editor.caretModel.offset) {
|
||||
corrections.add(Action.MoveToOffset(segmentRange.startOffset))
|
||||
}
|
||||
}
|
||||
}
|
||||
return corrections
|
||||
}
|
||||
|
||||
fun correctSelection(editor: Editor) {
|
||||
val corrections = injector.application.runReadAction { calculateCorrections(editor) }
|
||||
/**
|
||||
* Correct the editor's selection to match the current Vim mode
|
||||
*/
|
||||
fun correctEditorSelection(editor: Editor) {
|
||||
injector.application.runReadAction {
|
||||
val corrections = calculateCorrectionsToSyncEditorToMode(editor)
|
||||
applyCorrections(corrections, editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<!--
|
||||
~ Copyright 2003-2023 The IdeaVim authors
|
||||
~
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE.txt file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
|
||||
<idea-plugin url="https://plugins.jetbrains.com/plugin/164">
|
||||
<name>IdeaVim</name>
|
||||
<id>IdeaVIM</id>
|
||||
<description><![CDATA[
|
||||
@@ -13,7 +21,7 @@
|
||||
<li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li>
|
||||
</ul>
|
||||
]]></description>
|
||||
<version>chylex</version>
|
||||
<version>SNAPSHOT</version>
|
||||
<vendor>JetBrains</vendor>
|
||||
|
||||
<!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) -->
|
||||
@@ -238,7 +246,6 @@
|
||||
</group>
|
||||
|
||||
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
|
||||
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
|
||||
</actions>
|
||||
|
||||
<extensions defaultExtensionNs="IdeaVIM">
|
||||
|
||||
@@ -72,7 +72,6 @@ nowrap
|
||||
nowrapscan
|
||||
|
||||
ideacopypreprocess
|
||||
ideaglobalmode
|
||||
ideajoin
|
||||
ideamarks
|
||||
idearefactormode
|
||||
|
||||
@@ -78,10 +78,5 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@@ -50,4 +50,10 @@ class DeletePreviousWordActionTest : VimExTestCase() {
|
||||
typeText(":set keymodel=continueselect,stopselect<C-W>")
|
||||
assertExText("set ")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test delete at beginning does nothing`() {
|
||||
typeText(":<C-W>")
|
||||
assertExText("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +721,17 @@ class MapCommandTest : VimTestCase() {
|
||||
assertState("zzz\n")
|
||||
}
|
||||
|
||||
// VIM-650 |mapleader|
|
||||
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "Bad replace of term codes")
|
||||
@Test
|
||||
fun testMapLeaderToCtrlSpace() {
|
||||
configureByText("\n")
|
||||
enterCommand("let mapleader = \"\\<C-SPACE>\"")
|
||||
enterCommand("nmap <Leader>z izzz<Esc>")
|
||||
typeText("<C-SPACE>z")
|
||||
assertState("zzz\n")
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "bad replace term codes")
|
||||
@Test
|
||||
fun testAmbiguousMapping() {
|
||||
|
||||
@@ -103,4 +103,22 @@ class PrintLineNumberTest : VimTestCase() {
|
||||
enterCommand("2=p#")
|
||||
assertStatusLineMessageContains("2 \t\t\tconsectetur adipiscing elit")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test out of bounds range is clamped to last line`() {
|
||||
configureByLines(10, "Lorem ipsum dolor sit amet")
|
||||
enterCommand("\$+100=")
|
||||
assertStatusLineMessageContains("10")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test out of bounds range with flags is clamped to last line`() {
|
||||
configureByText("""
|
||||
|Lorem ipsum dolor sit amet
|
||||
|consectetur adipiscing elit
|
||||
|Maecenas efficitur nec odio vel malesuada
|
||||
""".trimMargin())
|
||||
enterCommand("\$+100=p")
|
||||
assertStatusLineMessageContains("3 Maecenas efficitur nec odio vel malesuada")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +241,12 @@ class RegistersCommandTest : VimTestCase() {
|
||||
|
||||
val vimEditor = fixture.editor.vim
|
||||
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
|
||||
injector.registerGroup.saveRegister(vimEditor, context, '+', Register('+', SelectionType.LINE_WISE, "Lorem ipsum dolor", mutableListOf()))
|
||||
injector.registerGroup.saveRegister(
|
||||
vimEditor,
|
||||
context,
|
||||
'+',
|
||||
Register('+', injector.clipboardManager.dumbCopiedText("Lorem ipsum dolor"), SelectionType.LINE_WISE)
|
||||
)
|
||||
val clipboardContent = injector.clipboardManager.dumbCopiedText("clipboard content")
|
||||
injector.clipboardManager.setClipboardContent(vimEditor, context, clipboardContent)
|
||||
typeText("V<Esc>")
|
||||
@@ -448,7 +453,12 @@ 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('+', SelectionType.LINE_WISE, "Lorem ipsum dolor", mutableListOf()))
|
||||
injector.registerGroup.saveRegister(
|
||||
vimEditor,
|
||||
context,
|
||||
'+',
|
||||
Register('+', injector.clipboardManager.dumbCopiedText("Lorem ipsum dolor"), SelectionType.LINE_WISE)
|
||||
)
|
||||
injector.clipboardManager.setClipboardContent(vimEditor, context, clipboardContent)
|
||||
typeText("V<Esc>")
|
||||
|
||||
|
||||
@@ -144,6 +144,15 @@ class IndexedExpressionTest : VimTestCase("\n") {
|
||||
assertPluginError(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test indexed String expression at exactly length boundary returns empty String`() {
|
||||
// This test validates the off-by-one fix in IndexedExpression.kt line 56
|
||||
// With old code (idx > text.length), accessing index == length would cause IndexOutOfBoundsException
|
||||
// With new code (idx >= text.length), it correctly returns empty string
|
||||
assertCommandOutput("echo string('hello'[5])", "''")
|
||||
assertPluginError(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test indexed String expression with negative index returns empty String`() {
|
||||
// Surprisingly not the same as List
|
||||
|
||||
@@ -30,6 +30,51 @@ class LenFunctionTest : VimTestCase() {
|
||||
assertCommandOutput("echo len(12 . 4)", "3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with empty string`() {
|
||||
assertCommandOutput("echo len('')", "0")
|
||||
assertCommandOutput("echo len(\"\")", "0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with empty list`() {
|
||||
assertCommandOutput("echo len([])", "0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with zero number`() {
|
||||
assertCommandOutput("echo len(0)", "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with negative numbers`() {
|
||||
assertCommandOutput("echo len(-123)", "4")
|
||||
assertCommandOutput("echo len(-1)", "2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with large number`() {
|
||||
assertCommandOutput("echo len(9999999)", "7")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with multi-element list`() {
|
||||
assertCommandOutput("echo len([1, 2, 3, 4, 5])", "5")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with multi-element dictionary`() {
|
||||
assertCommandOutput("echo len(#{a: 1, b: 2, c: 3})", "3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with string containing special characters`() {
|
||||
// Single-quoted strings in Vim don't interpret escape sequences (except '')
|
||||
assertCommandOutput("echo len('hello\\nworld')", "12")
|
||||
// Double-quoted strings do interpret escape sequences
|
||||
assertCommandOutput("echo len(\"hello\\nworld\")", "11")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test len with float causes errors`() {
|
||||
enterCommand("echo len(4.2)")
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package org.jetbrains.plugins.ideavim.ex.implementation.functions.listFunctions
|
||||
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import kotlin.test.assertTrue
|
||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -59,17 +60,18 @@ class RangeFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test range with zero stride throws error`() {
|
||||
enterCommand("echo range(1, 5, 0)")
|
||||
kotlin.test.assertTrue(injector.messages.isError())
|
||||
assertTrue(injector.messages.isError())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test range with start past end throws error`() {
|
||||
enterCommand("echo range(2, 0)")
|
||||
kotlin.test.assertTrue(injector.messages.isError())
|
||||
assertTrue(injector.messages.isError())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test range negative with start past end throws error`() {
|
||||
enterCommand("echo range(-2, 0, -1)")
|
||||
assertTrue(injector.messages.isError())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,6 @@ class TrimFunctionTest : VimTestCase() {
|
||||
assertCommandOutput("echo trim('\t\nhello\n\t')", "hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test trim with carriage return and vertical tab`() {
|
||||
assertCommandOutput("echo trim('\r\t\t\r RESERVE \t\n\u000B\u00A0') .. '_TAIL'", "RESERVE_TAIL")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test trim with custom mask does not affect middle`() {
|
||||
assertCommandOutput("echo trim('rm<Xrm<>X>rrm', 'rm<>')", "Xrm<>X")
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.state.mode.selectionType
|
||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||
import org.jetbrains.plugins.ideavim.assertDoesntChange
|
||||
import org.jetbrains.plugins.ideavim.assertModeDoesNotChange
|
||||
import org.jetbrains.plugins.ideavim.rangeOf
|
||||
import org.jetbrains.plugins.ideavim.waitAndAssert
|
||||
import org.jetbrains.plugins.ideavim.waitAndAssertMode
|
||||
@@ -89,7 +89,7 @@ class NonVimVisualChangeTest : VimTestCase() {
|
||||
fixture.editor.selectionModel.removeSelection()
|
||||
}
|
||||
|
||||
assertDoesntChange { fixture.editor.vim.mode == Mode.INSERT }
|
||||
assertModeDoesNotChange(fixture.editor, Mode.INSERT)
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
||||
|
||||
@@ -10,6 +10,7 @@ package org.jetbrains.plugins.ideavim
|
||||
|
||||
import com.intellij.ide.IdeEventQueue
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
||||
import com.intellij.util.containers.toArray
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
@@ -17,6 +18,8 @@ import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
/**
|
||||
@@ -56,17 +59,9 @@ annotation class VimBehaviorDiffers(
|
||||
val shouldBeFixed: Boolean = true,
|
||||
)
|
||||
|
||||
inline fun waitAndAssert(timeInMillis: Int = 1000, crossinline condition: () -> Boolean) {
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
val end = System.currentTimeMillis() + timeInMillis
|
||||
while (end > System.currentTimeMillis()) {
|
||||
Thread.sleep(10)
|
||||
IdeEventQueue.getInstance().flushQueue()
|
||||
if (condition()) return@invokeAndWait
|
||||
}
|
||||
fail()
|
||||
}
|
||||
}
|
||||
// The selection is updated after 'visualdelay' milliseconds. Add an adjustment when we wait for it to be completed.
|
||||
// Since we wait for it on the main thread (to avoid reading in-progress state), we can get away with a short adjustment
|
||||
private const val visualDelayAdjustment = 200
|
||||
|
||||
fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) {
|
||||
assertDoesntChange(timeInMillis - precision) { !condition() }
|
||||
@@ -111,15 +106,58 @@ fun <T> product(vararg elements: List<T>): List<List<T>> {
|
||||
return res
|
||||
}
|
||||
|
||||
private inline fun invokeAndWaitUntil(timeout: Int = 1_000, crossinline condition: () -> Boolean): Boolean {
|
||||
// Run the check on the main thread to serialise access for our condition
|
||||
var result = false
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
val end = System.currentTimeMillis() + timeout
|
||||
while (end > System.currentTimeMillis()) {
|
||||
if (condition()) {
|
||||
result = true
|
||||
return@invokeAndWait
|
||||
}
|
||||
Thread.sleep(10)
|
||||
IdeEventQueue.getInstance().flushQueue()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun waitAndAssert(timeInMillis: Int = 1000, condition: () -> Boolean) {
|
||||
assertTrue(invokeAndWaitUntil(timeInMillis, condition), "Condition not met within timeout")
|
||||
}
|
||||
|
||||
fun waitAndAssertMode(
|
||||
fixture: CodeInsightTestFixture,
|
||||
mode: Mode,
|
||||
timeInMillis: Int? = null,
|
||||
) {
|
||||
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + 1000)
|
||||
waitAndAssert(timeout) { fixture.editor.vim.mode == mode }
|
||||
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment)
|
||||
val currentMode = fixture.editor.vim.mode
|
||||
waitAndAssert(timeout) {
|
||||
if (fixture.editor.vim.mode == currentMode && fixture.editor.vim.mode != mode) return@waitAndAssert false
|
||||
assertEquals(mode, fixture.editor.vim.mode)
|
||||
return@waitAndAssert true
|
||||
}
|
||||
}
|
||||
|
||||
fun assertModeDoesNotChange(editor: Editor, expectedMode: Mode, timeInMillis: Int? = null) {
|
||||
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment)
|
||||
val currentMode = editor.vim.mode
|
||||
assertEquals(expectedMode, currentMode, "Initial mode is not as expected")
|
||||
invokeAndWaitUntil(timeout) { editor.vim.mode != currentMode }
|
||||
assertEquals(currentMode, editor.vim.mode, "Mode should not change")
|
||||
}
|
||||
|
||||
// Note that the selection might not update, but we wait long enough to give it chance to change
|
||||
fun waitUntilSelectionUpdated(editor: Editor) {
|
||||
val timeout = injector.globalIjOptions().visualdelay + visualDelayAdjustment
|
||||
val currentMode = editor.vim.mode
|
||||
invokeAndWaitUntil(timeout) { editor.vim.mode != currentMode }
|
||||
}
|
||||
|
||||
// This waits on the current thread, which in tests isn't the main thread. If we're waiting on a callback on the main
|
||||
// thread, we might see in-progress changes rather than the final state.
|
||||
fun waitUntil(timeout: Int = 10_000, condition: () -> Boolean): Boolean {
|
||||
val timeEnd = System.currentTimeMillis() + timeout
|
||||
while (System.currentTimeMillis() < timeEnd) {
|
||||
|
||||
@@ -139,19 +139,25 @@ and some text after""",
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
|
||||
assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, true)
|
||||
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
|
||||
?: error("Expected fold region at line 0 for Javadoc comment")
|
||||
assertEquals(foldRegion.isExpanded, true)
|
||||
}
|
||||
}
|
||||
typeText(injector.parser.parseKeys("za"))
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, false)
|
||||
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
|
||||
?: error("Expected fold region at line 0 for Javadoc comment")
|
||||
assertEquals(foldRegion.isExpanded, false)
|
||||
}
|
||||
}
|
||||
typeText(injector.parser.parseKeys("za"))
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, true)
|
||||
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
|
||||
?: error("Expected fold region at line 0 for Javadoc comment")
|
||||
assertEquals(foldRegion.isExpanded, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +180,9 @@ and some text after""",
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
fixture.editor.foldingModel.runBatchFoldingOperation {
|
||||
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
|
||||
FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded = false
|
||||
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
|
||||
?: error("Expected fold region at line 0 for Javadoc comment")
|
||||
foldRegion.isExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ bar
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
fixture.editor.foldingModel.runBatchFoldingOperation {
|
||||
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
|
||||
FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded = false
|
||||
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)
|
||||
?: error("Expected fold region at line 0 for Javadoc comment")
|
||||
foldRegion.isExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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", "<C-R>"], modes = [Mode.NORMAL, Mode.VISUAL])
|
||||
@CommandOrMotion(keys = ["<C-R>"], modes = [Mode.NORMAL])
|
||||
class RedoAction : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
|
||||
|
||||
|
||||
@@ -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, Mode.VISUAL])
|
||||
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL])
|
||||
class UndoAction : VimActionHandler.SingleExecution() {
|
||||
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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
|
||||
@@ -20,8 +21,12 @@ 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 = [], modes = [])
|
||||
@CommandOrMotion(keys = ["u", "gu"], modes = [Mode.VISUAL])
|
||||
class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
|
||||
override val type: Command.Type = Command.Type.CHANGE
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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
|
||||
@@ -20,8 +21,12 @@ 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 = [], modes = [])
|
||||
@CommandOrMotion(keys = ["U", "gU"], modes = [Mode.VISUAL])
|
||||
class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() {
|
||||
override val type: Command.Type = Command.Type.CHANGE
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
|
||||
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
@CommandOrMotion(keys = ["s"], modes = [Mode.NORMAL])
|
||||
class ChangeCharactersAction : ChangeInInsertSequenceAction() {
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
|
||||
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
@CommandOrMotion(keys = ["C"], modes = [Mode.NORMAL])
|
||||
class ChangeEndOfLineAction : ChangeInInsertSequenceAction() {
|
||||
|
||||
@@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
|
||||
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
@CommandOrMotion(keys = ["S"], modes = [Mode.NORMAL])
|
||||
class ChangeLineAction : ChangeInInsertSequenceAction() {
|
||||
|
||||
@@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
|
||||
import com.maddyhome.idea.vim.command.DuplicableOperatorAction
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
@CommandOrMotion(keys = ["c"], modes = [Mode.NORMAL])
|
||||
class ChangeMotionAction : ChangeInInsertSequenceAction(), DuplicableOperatorAction {
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
|
||||
@@ -21,7 +21,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.handler.VimActionHandler
|
||||
import com.maddyhome.idea.vim.helper.endOffsetInclusive
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
@CommandOrMotion(keys = ["!"], modes = [Mode.VISUAL])
|
||||
class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterCommand {
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
|
||||
@@ -70,10 +70,15 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
|
||||
*/
|
||||
@VimLockLabel.SelfSynchronized
|
||||
private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean {
|
||||
val register: Register? = injector.registerGroup.getRegister(key)
|
||||
val register: Register? = injector.registerGroup.getRegister(editor, context, key)
|
||||
if (register != null) {
|
||||
val textData = PutData.TextData(register.text, SelectionType.CHARACTER_WISE, emptyList(), register.name)
|
||||
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
|
||||
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)
|
||||
injector.put.putText(editor, context, putData)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -35,40 +36,33 @@ sealed class PutTextBaseAction(
|
||||
val count = operatorArguments.count1
|
||||
val sortedCarets = editor.sortedCarets()
|
||||
return if (sortedCarets.size > 1) {
|
||||
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 }
|
||||
}
|
||||
|
||||
val caretToPutData = sortedCarets.associateWith { getPutDataForCaret(editor, context, it, count) }
|
||||
var result = true
|
||||
caretToPutData.forEach {
|
||||
result = injector.put.putTextForCaret(editor, it.key, context, it.value) && result
|
||||
}
|
||||
result
|
||||
} else {
|
||||
injector.put.putText(editor, context, getPutData(count))
|
||||
val putData = getPutDataForCaret(editor, context, sortedCarets.single(), count)
|
||||
injector.put.putText(editor, context, putData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPutData(count: Int,
|
||||
private fun getPutDataForCaret(
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
caret: ImmutableVimCaret,
|
||||
count: Int,
|
||||
): PutData {
|
||||
return PutData(getRegisterTextData(), null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
|
||||
val registerService = injector.registerGroup
|
||||
val registerChar = if (caret.editor.carets().size == 1) {
|
||||
registerService.currentRegister
|
||||
} else {
|
||||
registerService.getCurrentRegisterForMulticaret()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
val register = caret.registerStorage.getRegister(editor, context, registerChar)
|
||||
val textData = register?.let { TextData(register) }
|
||||
return PutData(textData, null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,22 +41,8 @@ sealed class PutVisualTextBaseAction(
|
||||
): Boolean {
|
||||
if (caretsAndSelections.isEmpty()) return false
|
||||
val count = cmd.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)
|
||||
}
|
||||
|
||||
val caretToPutData =
|
||||
editor.sortedCarets().associateWith { getPutDataForCaret(editor, context, it, caretsAndSelections[it], count) }
|
||||
injector.registerGroup.resetRegister()
|
||||
var result = true
|
||||
caretToPutData.forEach {
|
||||
@@ -65,10 +51,16 @@ sealed class PutVisualTextBaseAction(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getPutDataForCaret(textData: PutData.TextData?,
|
||||
private fun getPutDataForCaret(
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
caret: VimCaret,
|
||||
selection: VimSelection?,
|
||||
count: Int,): PutData {
|
||||
count: Int,
|
||||
): PutData {
|
||||
val lastRegisterChar = injector.registerGroup.lastRegisterChar
|
||||
val register = caret.registerStorage.getRegister(editor, context, lastRegisterChar)
|
||||
val textData = register?.let { PutData.TextData(register) }
|
||||
val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) }
|
||||
return PutData(textData, visualSelection, count, insertTextBeforeCaret, indent, caretAfterInsertedText)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
* Copyright 2003-2025 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
|
||||
@@ -118,7 +118,7 @@ open class InsertRegisterActionBase(insertLiterally: Boolean) : InsertCommandLin
|
||||
replayKeys(editor, context, register.keys)
|
||||
}
|
||||
else {
|
||||
insertText(commandLine, commandLine.caret.offset, register.text ?: return false)
|
||||
insertText(commandLine, commandLine.caret.offset, register.text)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
|
||||
|
||||
else -> throw ExException("Unexpected search label ${argument.label}")
|
||||
}
|
||||
if (offsetAndMotion == null) return Motion.Error
|
||||
// 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
|
||||
parentAction.motionType = offsetAndMotion.second
|
||||
return offsetAndMotion.first.toMotionOrError()
|
||||
}
|
||||
|
||||
@@ -76,13 +76,6 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,6 @@ 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
|
||||
}
|
||||
@@ -53,10 +47,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -70,6 +70,6 @@ class MotionDownNotLineWiseAction : MotionActionHandler.ForEachCaret() {
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion {
|
||||
return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1, bufferLines = true)
|
||||
return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,6 @@ class MotionUpNotLineWiseAction : MotionActionHandler.ForEachCaret() {
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion {
|
||||
return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1, bufferLines = true)
|
||||
return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
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
|
||||
@@ -16,11 +17,13 @@ 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.VimRegisterGroup
|
||||
import com.maddyhome.idea.vim.register.Register
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||
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.
|
||||
@@ -63,7 +66,7 @@ interface ImmutableVimCaret {
|
||||
fun hasSelection(): Boolean
|
||||
|
||||
var lastSelectionInfo: SelectionInfo
|
||||
val registerStorage: VimRegisterGroup
|
||||
val registerStorage: CaretRegisterStorage
|
||||
val markStorage: LocalMarkStorage
|
||||
}
|
||||
|
||||
@@ -149,3 +152,19 @@ 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)
|
||||
}
|
||||
|
||||
@@ -8,4 +8,94 @@
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ interface VimChangeGroup {
|
||||
operatorArguments: OperatorArguments,
|
||||
)
|
||||
|
||||
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret
|
||||
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret
|
||||
|
||||
fun insertText(editor: VimEditor, caret: VimCaret, str: String): VimCaret
|
||||
|
||||
|
||||
@@ -179,21 +179,13 @@ abstract class VimChangeGroupBase : VimChangeGroup {
|
||||
return false
|
||||
}
|
||||
}
|
||||
val mode = editor.mode
|
||||
if (type == null ||
|
||||
(mode == Mode.INSERT || mode == Mode.REPLACE) ||
|
||||
!saveToRegister ||
|
||||
injector.registerGroup.storeText(
|
||||
editor,
|
||||
context,
|
||||
caret,
|
||||
updatedRange,
|
||||
type,
|
||||
true,
|
||||
!editor.isFirstCaret,
|
||||
editor.isReversingCarets
|
||||
)
|
||||
) {
|
||||
|
||||
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) {
|
||||
@@ -213,8 +205,6 @@ abstract class VimChangeGroupBase : VimChangeGroup {
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts text into the document
|
||||
@@ -223,7 +213,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: CharSequence): VimCaret {
|
||||
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret {
|
||||
injector.application.runWriteAction {
|
||||
(editor as MutableVimEditor).insertText(caret, offset, str)
|
||||
}
|
||||
@@ -452,10 +442,12 @@ abstract class VimChangeGroupBase : VimChangeGroup {
|
||||
*/
|
||||
override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) {
|
||||
val state = injector.vimState
|
||||
injector.application.runReadAction {
|
||||
for (caret in editor.nativeCarets()) {
|
||||
caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset)
|
||||
injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
|
||||
}
|
||||
}
|
||||
val cmd = state.executingCommand
|
||||
if (cmd != null && state.isDotRepeatInProgress) {
|
||||
editor.mode = mode
|
||||
@@ -480,7 +472,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
|
||||
val myChangeListener = VimChangesListener()
|
||||
vimDocumentListener = myChangeListener
|
||||
vimDocument!!.addChangeListener(myChangeListener)
|
||||
injector.application.runReadAction {
|
||||
oldOffset = editor.currentCaret().offset
|
||||
}
|
||||
editor.insertMode = mode == Mode.INSERT
|
||||
editor.mode = mode
|
||||
}
|
||||
|
||||
@@ -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(): VimCopiedText?
|
||||
fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): VimCopiedText?
|
||||
|
||||
fun getClipboardContent(editor: VimEditor, context: ExecutionContext): VimCopiedText?
|
||||
|
||||
|
||||
@@ -111,8 +111,7 @@ 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
|
||||
@@ -163,7 +162,6 @@ interface VimEditor {
|
||||
return getText(start, end)
|
||||
}
|
||||
|
||||
fun getSelectionModel(): VimSelectionModel
|
||||
fun getScrollingModel(): VimScrollingModel
|
||||
|
||||
fun removeCaret(caret: VimCaret)
|
||||
@@ -212,7 +210,6 @@ interface VimEditor {
|
||||
|
||||
fun createIndentBySize(size: Int): String
|
||||
fun getFoldRegionAtOffset(offset: Int): VimFoldRegion?
|
||||
fun getSoftWrapStartAtOffset(offset: Int): Int?
|
||||
|
||||
/**
|
||||
* Mostly related to Fleet. After the editor is modified, the carets are modified. You can't use the old caret
|
||||
|
||||
@@ -25,7 +25,7 @@ interface VimMotionGroup {
|
||||
allowWrap: Boolean = false,
|
||||
): Motion
|
||||
|
||||
fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean = false): Motion
|
||||
fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
|
||||
|
||||
// TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion
|
||||
|
||||
|
||||
@@ -33,18 +33,14 @@ 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, bufferLines: Boolean): Motion {
|
||||
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): 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 = 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)
|
||||
val line = editor.normalizeVisualLine(pos.line + count)
|
||||
|
||||
if (intendedColumn == LAST_COLUMN) {
|
||||
val normalisedColumn = injector.engineEditorHelper.normalizeVisualColumn(
|
||||
|
||||
@@ -208,17 +208,4 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1425,7 +1425,8 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
||||
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
|
||||
* @param direction The direction to search
|
||||
*/
|
||||
override fun setLastSearchState(
|
||||
@TestOnly
|
||||
fun setLastSearchState(
|
||||
pattern: String,
|
||||
patternOffset: String,
|
||||
direction: Direction,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2003-2023 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.api
|
||||
|
||||
interface VimSelectionModel {
|
||||
val selectionStart: Int
|
||||
val selectionEnd: Int
|
||||
|
||||
fun hasSelection(): Boolean
|
||||
}
|
||||
@@ -27,26 +27,30 @@ abstract class VimStringParserBase : VimStringParser {
|
||||
override fun toPrintableString(keys: List<KeyStroke>): String {
|
||||
val builder = StringBuilder()
|
||||
for (key in keys) {
|
||||
val keyAsChar = keyStrokeToChar(key)
|
||||
builder.append(keyAsChar)
|
||||
val keyAsString = keyStrokeToString(key)
|
||||
builder.append(keyAsString)
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun keyStrokeToChar(key: KeyStroke): Char {
|
||||
private fun keyStrokeToString(key: KeyStroke): String {
|
||||
if (key.keyChar != KeyEvent.CHAR_UNDEFINED) {
|
||||
return key.keyChar
|
||||
return key.keyChar.toString()
|
||||
} else if (key.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
|
||||
return if (key.keyCode == 'J'.code) {
|
||||
return if (isControlCharacterKeyCode(key.keyCode)) {
|
||||
if (key.keyCode == 'J'.code) {
|
||||
// 'J' is a special case, keycode 10 is \n char
|
||||
0.toChar()
|
||||
0.toChar().toString()
|
||||
} else {
|
||||
(key.keyCode - 'A'.code + 1).toChar()
|
||||
(key.keyCode - 'A'.code + 1).toChar().toString()
|
||||
}
|
||||
} else {
|
||||
"^" + key.keyCode.toChar()
|
||||
}
|
||||
} else if (key.keyChar == KeyEvent.CHAR_UNDEFINED && key.keyCode == KeyEvent.VK_ENTER) {
|
||||
return '\u000D'
|
||||
return "\u000D"
|
||||
}
|
||||
return key.keyCode.toChar()
|
||||
return key.keyCode.toChar().toString()
|
||||
}
|
||||
|
||||
override fun toKeyNotation(keyStrokes: List<KeyStroke>): String {
|
||||
@@ -178,7 +182,18 @@ abstract class VimStringParserBase : VimStringParser {
|
||||
private fun getMapLeader(): List<KeyStroke> {
|
||||
val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader")
|
||||
return if (mapLeader is VimString) {
|
||||
val v: String = mapLeader.value
|
||||
// Minimum length is 4 for the shortest special key format: \<X> (e.g., "\<a>")
|
||||
if (v.startsWith("\\<") && v.length >= 4 && v[v.length - 1] == '>') {
|
||||
val specialKey = parseSpecialKey(v.substring(2, v.length - 1), 0)
|
||||
if (specialKey != null) {
|
||||
listOf(specialKey)
|
||||
} else {
|
||||
stringToKeys(mapLeader.value)
|
||||
}
|
||||
} else {
|
||||
stringToKeys(mapLeader.value)
|
||||
}
|
||||
} else {
|
||||
stringToKeys("\\")
|
||||
}
|
||||
@@ -207,6 +222,11 @@ abstract class VimStringParserBase : VimStringParser {
|
||||
return c < '\u0020'
|
||||
}
|
||||
|
||||
private fun isControlCharacterKeyCode(code: Int): Boolean {
|
||||
// Ctrl-(A..Z [\]^_) are ASCII control characters
|
||||
return code >= 'A'.code && code <= '_'.code
|
||||
}
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private fun getVimKeyValue(c: Int): @NonNls String? {
|
||||
return when (c) {
|
||||
@@ -430,17 +450,25 @@ abstract class VimStringParserBase : VimStringParser {
|
||||
val specialKey = parseSpecialKey(specialKeyBuilder.toString(), 0)
|
||||
if (specialKey != null) {
|
||||
var keyCode = specialKey.keyCode
|
||||
var useKeyCode = true
|
||||
if (specialKey.keyCode == 0) {
|
||||
keyCode = specialKey.keyChar.code
|
||||
} else if (specialKey.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
|
||||
if (isControlCharacterKeyCode(specialKey.keyCode)) {
|
||||
keyCode = if (specialKey.keyCode == 'J'.code) {
|
||||
// 'J' is a special case, keycode 10 is \n char
|
||||
0
|
||||
} else {
|
||||
specialKey.keyCode - 'A'.code + 1
|
||||
}
|
||||
} else {
|
||||
useKeyCode = false
|
||||
result.append("\\<${specialKeyBuilder}>")
|
||||
}
|
||||
}
|
||||
if (useKeyCode) {
|
||||
result.append(keyCode.toChar())
|
||||
}
|
||||
} else {
|
||||
result.append("<").append(specialKeyBuilder).append(">")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
|
||||
@@ -17,10 +17,12 @@ class VimEditorReplaceMask {
|
||||
fun recordChangeAtCaret(editor: VimEditor) {
|
||||
for (caret in editor.carets()) {
|
||||
val offset = caret.offset
|
||||
if (offset < editor.fileSize()) {
|
||||
val marker = editor.createLiveMarker(offset, offset)
|
||||
changedChars[marker] = editor.charAt(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun popChange(editor: VimEditor, offset: Int): Char? {
|
||||
val marker = editor.createLiveMarker(offset, offset)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
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
|
||||
@@ -71,9 +72,9 @@ class VimListenersNotifier {
|
||||
isReplaceCharListeners.forEach { it.isReplaceCharChanged(editor) }
|
||||
}
|
||||
|
||||
fun notifyYankPerformed(editor: VimEditor, range: TextRange) {
|
||||
fun notifyYankPerformed(caretToRange: Map<ImmutableVimCaret, 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(editor, range) }
|
||||
yankListeners.forEach { it.yankPerformed(caretToRange) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
package com.maddyhome.idea.vim.common
|
||||
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
||||
|
||||
interface VimYankListener: Listener {
|
||||
fun yankPerformed(editor: VimEditor, range: TextRange)
|
||||
fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ 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
|
||||
@@ -227,15 +226,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
|
||||
StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset)
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user