1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-11-30 01:38:25 +01:00

Compare commits

..

26 Commits

Author SHA1 Message Date
1d4a14defd Set plugin version to chylex-52 2025-10-08 05:36:15 +02:00
021a94d9bb Improve support for recording macros with code completion
Fixes wrong recorded inputs when code completion introduces an import.

Fixes wrong recorded inputs when completing a static member with a partial type name. Example: `WiE.A` -> `WindowEvent.ACTION_EVENT_MASK`
2025-10-08 05:36:01 +02:00
9f6bffcf7d Preserve visual mode after executing IDE action 2025-10-06 22:19:03 +02:00
80222af0bf Make g0/g^/g$ work with soft wraps 2025-10-06 22:19:03 +02:00
57ea1ecb69 Make gj/gk jump over soft wraps 2025-10-06 22:19:03 +02:00
3b65b55929 Make camelCase motions adjust based on direction of visual selection 2025-10-06 22:19:02 +02:00
097924e078 Make search highlights temporary 2025-10-06 22:19:02 +02:00
8d092693b9 Exit insert mode after refactoring 2025-10-06 22:19:02 +02:00
42780c052b Add action to run last macro in all opened files 2025-10-06 22:19:02 +02:00
bf5eb879f9 Stop macro execution after a failed search 2025-10-06 22:19:02 +02:00
51e9c9be1c Revert per-caret registers 2025-10-06 22:19:02 +02:00
f4137d2769 Apply scrolloff after executing native IDEA actions 2025-10-06 22:19:02 +02:00
9c9284a201 Stay on same line after reindenting 2025-10-06 22:19:02 +02:00
84c30d1afc Update search register when using f/t 2025-10-06 22:19:02 +02:00
0de4b4fdde Automatically add unambiguous imports after running a macro 2025-10-06 22:19:02 +02:00
1ccb75e6b8 Fix(VIM-3986): Exception when pasting register contents containing new line 2025-10-06 22:19:02 +02:00
6671642428 Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2025-10-06 22:19:02 +02:00
6cef05bfbb Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2025-10-06 22:19:02 +02:00
e961dce249 Add support for count for visual and line motion surround 2025-10-06 22:19:00 +02:00
47937cb382 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2025-10-06 22:18:55 +02:00
10552bef28 Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2025-10-05 01:25:09 +02:00
140fe1ec6c Respect count with <Action> mappings 2025-10-05 01:25:09 +02:00
ecb2416457 Change matchit plugin to use HTML patterns in unrecognized files 2025-10-05 01:25:09 +02:00
a8de488629 Reset insert mode when switching active editor 2025-10-05 01:25:09 +02:00
ba55ffe7e4 Remove notifications about configuration options 2025-10-05 01:25:09 +02:00
a6ba575ef9 Set custom plugin version 2025-10-05 01:25:08 +02:00
125 changed files with 1676 additions and 2911 deletions

View File

@@ -1,206 +0,0 @@
# Codebase Maintenance Instructions
## Goal
Perform routine maintenance on random parts of the IdeaVim codebase to ensure code quality, consistency, and catch potential issues early. This is not about being overly pedantic or making changes for the sake of changes - it's about keeping an eye on the codebase and identifying genuine issues.
## Approach
### 1. Select Random Area
Choose a random part of the codebase to inspect. Use one of these strategies:
```bash
# Get a random Kotlin file
find . -name "*.kt" -not -path "*/build/*" -not -path "*/.gradle/*" | shuf -n 1
# Get a random package/directory
find . -type d -name "*.kt" -not -path "*/build/*" | shuf -n 1 | xargs dirname
# Pick from core areas randomly
# - vim-engine/src/main/kotlin/com/maddyhome/idea/vim/
# - src/main/java/com/maddyhome/idea/vim/
# - tests/
```
**Important**: You're not limited to the file you randomly selected. If investigating reveals related files that need attention, follow the trail. The random selection is just a starting point.
## 2. What to Check
### Code Style & Formatting
- **Kotlin conventions**: Proper use of data classes, sealed classes, when expressions
- **Naming consistency**: Follow existing patterns in the codebase
- **Import organization**: Remove unused imports, prefer explicit imports over wildcards (wildcard imports are generally not welcome)
- **Code structure**: Proper indentation, spacing, line breaks
- **Documentation**: KDoc comments where needed (public APIs, complex logic)
- **Copyright years**: Do NOT update copyright years unless you're making substantive changes to the file. It's perfectly fine for copyright to show an older year. Don't mention copyright year updates in commit messages or change summaries
### Code Quality Issues
- **Null safety**: Proper use of nullable types, safe calls, Elvis operator
- **Error handling**: Appropriate exception handling, meaningful error messages
- **Code duplication**: Identify repeated code that could be extracted
- **Dead code**: Unused functions, parameters, variables
- **TODOs/FIXMEs**: Check if old TODOs are still relevant or can be addressed
- **Magic numbers/strings**: Should be named constants
- **Complex conditionals**: Can they be simplified or extracted?
### Potential Bugs
- **Off-by-one errors**: Especially in loops and range operations
- **Edge cases**: Empty collections, null values, boundary conditions
- **Type safety**: Unnecessary casts, unchecked casts
- **Resource handling**: Proper cleanup, try-with-resources
- **Concurrency issues**: Thread safety if applicable
- **State management**: Proper initialization, mutation patterns
- **IdeaVim enablement checks**: Verify that `injector.enabler.isEnabled()` or `Editor.isIdeaVimDisabledHere` are not missed in places where they should be checked. These functions determine if IdeaVim is active and should be called before performing Vim-specific operations
### Architecture & Design
- **Separation of concerns**: Does the code have a single responsibility?
- **Dependency direction**: Are dependencies pointing the right way?
- **Abstraction level**: Consistent level of abstraction within methods
- **Vim architecture alignment**: Does it match Vim's design philosophy?
- **IntelliJ Platform conventions**: Proper use of platform APIs
### Testing
- **Test coverage**: Are there tests for the code you're reviewing?
- If checking a specific command or function, verify that tests exist for it
- If tests exist, check if they cover the needed cases (edge cases, error conditions, typical usage)
- If tests don't exist or coverage is incomplete, consider creating comprehensive test coverage
- **Test quality**: Do tests cover edge cases?
- **Test naming**: Clear, descriptive test names
- **Flaky tests**: Any potentially unstable tests?
- **Regression tests for bug fixes**: When fixing a bug, always write a test that:
- Would fail with the old (buggy) implementation
- Passes with the fixed implementation
- Clearly documents what bug it's testing (include comments explaining the issue)
- Tests the specific boundary condition or edge case that exposed the bug
- This ensures the bug doesn't resurface in future refactorings
## 3. Investigation Strategy
Don't just look at surface-level issues. Dig deeper:
1. **Read the code**: Understand what it does before suggesting changes
2. **Check related files**: Look at callers, implementations, tests
3. **Look at git history**: `git log --oneline <file>` to understand context
4. **Find related issues**: Search for TODOs, FIXMEs, or commented code
5. **Run tests**: If you make changes, ensure tests pass
6. **Check YouTrack**: Look for related issues if you find bugs
## 4. When to Make Changes
**DO fix**:
- Clear bugs or logic errors
- Obvious code quality issues (unused imports, etc.)
- Misleading or incorrect documentation
- Code that violates established patterns
- Security vulnerabilities
- Performance issues with measurable impact
**DON'T fix**:
- Stylistic preferences if existing code is consistent
- Working code just to use "newer" patterns
- Minor formatting if it's consistent with surrounding code
- Things that are subjective or arguable
- Massive refactorings without clear benefit
**When in doubt**: Document the issue in your report but don't make changes.
## 5. Making Changes
If you decide to make changes:
1. **Make focused commits**: One logical change per commit
- If the change affects many files or is complicated, 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 Normal file
View File

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

View File

@@ -3,13 +3,26 @@ name: Claude Code Review
on: on:
pull_request: pull_request:
types: [opened, synchronize] types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs: jobs:
claude-review: claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: read
issues: read
id-token: write id-token: write
steps: steps:
@@ -24,10 +37,6 @@ jobs:
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: | prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
CONTRIBUTOR: ${{ github.event.pull_request.user.login }}
Please review this pull request and provide feedback on: Please review this pull request and provide feedback on:
- Code quality and best practices - Code quality and best practices
- Potential bugs or issues - Potential bugs or issues
@@ -35,13 +44,11 @@ jobs:
- Security concerns - Security concerns
- Test coverage - Test coverage
Provide detailed feedback using inline comments for specific issues.
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
claude_args: '--allowed-tools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"' 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:*)"'

View File

@@ -1,55 +0,0 @@
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:*)"'

View File

@@ -5,7 +5,6 @@ import _Self.buildTypes.LongRunning
import _Self.buildTypes.Nvim import _Self.buildTypes.Nvim
import _Self.buildTypes.PluginVerifier import _Self.buildTypes.PluginVerifier
import _Self.buildTypes.PropertyBased import _Self.buildTypes.PropertyBased
import _Self.buildTypes.RandomOrderTests
import _Self.buildTypes.TestingBuildType import _Self.buildTypes.TestingBuildType
import _Self.subprojects.GitHub import _Self.subprojects.GitHub
import _Self.subprojects.Releases import _Self.subprojects.Releases
@@ -17,8 +16,7 @@ import jetbrains.buildServer.configs.kotlin.v2019_2.Project
object Project : Project({ object Project : Project({
description = "Vim engine for JetBrains IDEs" description = "Vim engine for JetBrains IDEs"
subProject(Releases) subProjects(Releases, GitHub)
subProject(GitHub)
// VCS roots // VCS roots
vcsRoot(GitHubPullRequest) vcsRoot(GitHubPullRequest)
@@ -32,7 +30,6 @@ object Project : Project({
buildType(PropertyBased) buildType(PropertyBased)
buildType(LongRunning) buildType(LongRunning)
buildType(RandomOrderTests)
buildType(Nvim) buildType(Nvim)
buildType(PluginVerifier) buildType(PluginVerifier)

View File

@@ -1,44 +0,0 @@
package _Self.buildTypes
import _Self.IdeaVimBuildType
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.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>"
}
}
})

View File

@@ -638,22 +638,6 @@ Contributors:
[![icon][github]](https://github.com/magidc) [![icon][github]](https://github.com/magidc)
&nbsp; &nbsp;
magidc magidc
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
* [![icon][mail]](mailto:41898282+claude[bot]@users.noreply.github.com)
[![icon][github]](https://github.com/apps/github-actions)
&nbsp;
github-actions[bot]
Previous contributors: Previous contributors:

View File

@@ -130,13 +130,8 @@ Sed in orci mauris.
Cras id tellus in ex imperdiet egestas. Cras id tellus in ex imperdiet egestas.
``` ```
3. Don't forget to test your functionality with various corner cases: 3. Don't forget to test your functionality with line start, line end, file start, file end, empty line, multiple
- **Position-based**: line start, line end, file start, file end, empty line, single character line carets, dollar motion, etc.
- **Content-based**: whitespace-only lines, lines with trailing spaces, mixed tabs and spaces, Unicode characters, multi-byte characters (e.g., emoji, CJK)
- **Selection-based**: multiple carets, visual mode (character/line/block), empty selection
- **Motion-based**: dollar motion, count with motion (e.g., `3w`, `5j`), zero-width motions
- **Buffer state**: empty file, single line file, very long lines, read-only files
- **Boundaries**: word boundaries with punctuation, sentence/paragraph boundaries, matching brackets at extremes
##### Neovim ##### Neovim
IdeaVim has an integration with neovim in tests. Tests that are performed with `doTest` also executed in IdeaVim has an integration with neovim in tests. Tests that are performed with `doTest` also executed in

View File

@@ -138,6 +138,7 @@ dependencies {
// AceJump is an optional dependency. We use their SessionManager class to check if it's active // AceJump is an optional dependency. We use their SessionManager class to check if it's active
plugin("AceJump", "3.8.19") plugin("AceJump", "3.8.19")
plugin("com.intellij.classic.ui", "251.23774.318")
bundledPlugins("org.jetbrains.plugins.terminal") bundledPlugins("org.jetbrains.plugins.terminal")
@@ -236,6 +237,7 @@ tasks {
// a custom task (see below) // a custom task (see below)
runIde { runIde {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true) systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
systemProperty("idea.trust.all.projects", "true")
} }
// Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies // Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies

View File

@@ -180,6 +180,12 @@ Unless otherwise stated, these options do not have abbreviations.
value is off. The equivalent processing for paste is controlled by the value is off. The equivalent processing for paste is controlled by the
"ideaput" value to the 'clipboard' option. "ideaput" value to the 'clipboard' option.
'ideaglobalmode' boolean (default off)
global
This option will cause IdeaVim to share a single mode across all open
windows. In other words, entering Insert mode in one window will
enable Insert mode in all windows.
'ideajoin' boolean (default off) 'ideajoin' boolean (default off)
global or local to buffer global or local to buffer
When enabled, join commands will be handled by the IDE's "smart join" When enabled, join commands will be handled by the IDE's "smart join"

View File

@@ -20,7 +20,7 @@ ideaVersion=2025.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IC ideaType=IC
instrumentPluginCode=true instrumentPluginCode=true
version=SNAPSHOT version=chylex-52
javaVersion=21 javaVersion=21
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1
@@ -41,7 +41,6 @@ youtrackToken=
# Gradle settings # Gradle settings
org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.configuration-cache=true
org.gradle.caching=true org.gradle.caching=true
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary # Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary

View File

@@ -0,0 +1,67 @@
package com.maddyhome.idea.vim.action.macro
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.impl.FinishMarkAction
import com.intellij.openapi.command.impl.StartMarkAction
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
@CommandOrMotion(keys = ["z@"], modes = [Mode.NORMAL])
class PlaybackRegisterInOpenFilesAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override val argumentType: Argument.Type = Argument.Type.CHARACTER
private val playbackRegisterAction = PlaybackRegisterAction()
override fun execute(
editor: VimEditor,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
val argument = cmd.argument as? Argument.Character ?: return false
val project = editor.ij.project ?: return false
val fileEditorManager = FileEditorManagerEx.getInstanceExIfCreated(project) ?: return false
val register = argument.character.let { if (it == '@') injector.macro.lastRegister else it }
val commandName = "Execute Macro '$register' in All Open Files"
val action = Runnable {
CommandProcessor.getInstance().markCurrentCommandAsGlobal(project)
for (textEditor in fileEditorManager.allEditors.filterIsInstance<TextEditor>()) {
fileEditorManager.openFile(textEditor.file, true)
val editor = textEditor.editor
val vimEditor = editor.vim
vimEditor.mode = com.maddyhome.idea.vim.state.mode.Mode.NORMAL()
KeyHandler.Companion.getInstance().reset(vimEditor)
val startMarkAction = StartMarkAction.start(editor, project, commandName)
playbackRegisterAction.execute(vimEditor, context, cmd, operatorArguments)
FinishMarkAction.finish(project, editor, startMarkAction)
}
}
CommandProcessor.getInstance()
.executeCommand(project, action, commandName, null, UndoConfirmationPolicy.REQUEST_CONFIRMATION)
return true
}
}

View File

@@ -221,7 +221,7 @@ object VimExtensionFacade {
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
keys: List<KeyStroke?>?, keys: List<KeyStroke?>?,
) { ) {
caret.registerStorage.setKeys(editor, context, register, keys?.filterNotNull() ?: emptyList()) caret.registerStorage.setKeys(register, keys?.filterNotNull() ?: emptyList())
} }
/** Set the current contents of the given register */ /** Set the current contents of the given register */

View File

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

View File

@@ -11,7 +11,7 @@ package com.maddyhome.idea.vim.extension.hints
import com.intellij.ui.treeStructure.Tree import com.intellij.ui.treeStructure.Tree
import java.awt.Component import java.awt.Component
import java.awt.Point import java.awt.Point
import java.util.WeakHashMap import java.util.*
import javax.accessibility.Accessible import javax.accessibility.Accessible
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -44,9 +44,9 @@ internal sealed class HintGenerator {
val hintIterator = alphabet.permutations(length).map { it.joinToString("") }.iterator() val hintIterator = alphabet.permutations(length).map { it.joinToString("") }.iterator()
targets.forEach { target -> targets.forEach { target ->
target.hint = if (preserve) { target.hint = if (preserve) {
previousHints[target.component] ?: hintIterator.firstOrNull { candidateHint -> previousHints[target.component] ?: hintIterator.firstOrNull {
// Check if the hint is not already used by previous targets // Check if the hint is not already used by previous targets
!previousHints.values.any { existingHint -> existingHint.startsWith(candidateHint) || candidateHint.startsWith(existingHint) } !previousHints.values.any { hint -> hint.startsWith(it) || it.startsWith(hint) }
} ?: return generate(targets, false) // do not preserve previous hints if failed } ?: return generate(targets, false) // do not preserve previous hints if failed
} else { } else {
hintIterator.next() hintIterator.next()

View File

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

View File

@@ -0,0 +1,30 @@
package com.maddyhome.idea.vim.extension.surround
import com.intellij.util.text.CharSequenceSubSequence
internal data class RepeatedCharSequence(val text: CharSequence, val count: Int) : CharSequence {
override val length = text.length * count
override fun get(index: Int): Char {
if (index < 0 || index >= length) throw IndexOutOfBoundsException()
return text[index % text.length]
}
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
return CharSequenceSubSequence(this, startIndex, endIndex)
}
override fun toString(): String {
return text.repeat(count)
}
companion object {
fun of(text: CharSequence, count: Int): CharSequence {
return when (count) {
0 -> ""
1 -> text
else -> RepeatedCharSequence(text, count)
}
}
}
}

View File

@@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.endsWithNewLine import com.maddyhome.idea.vim.api.endsWithNewLine
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
@@ -36,7 +37,10 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
import com.maddyhome.idea.vim.extension.exportOperatorFunction import com.maddyhome.idea.vim.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.findBlockRange import com.maddyhome.idea.vim.group.findBlockRange
import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
import com.maddyhome.idea.vim.key.OperatorFunction import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
@@ -78,7 +82,7 @@ internal class VimSurroundExtension : VimExtension {
putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true) putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true)
} }
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator()) VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false, count = 1)) // TODO
} }
private class YSurroundHandler : ExtensionHandler { private class YSurroundHandler : ExtensionHandler {
@@ -105,7 +109,7 @@ internal class VimSurroundExtension : VimExtension {
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset) val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
if (lastNonWhiteSpaceOffset != null) { if (lastNonWhiteSpaceOffset != null) {
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1) val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
performSurround(pair, range, it) performSurround(pair, range, it, count = operatorArguments.count1)
} }
// it.moveToOffset(lineStartOffset) // it.moveToOffset(lineStartOffset)
} }
@@ -128,15 +132,13 @@ internal class VimSurroundExtension : VimExtension {
private class VSurroundHandler : ExtensionHandler { private class VSurroundHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// NB: Operator ignores SelectionType anyway // NB: Operator ignores SelectionType anyway
if (!Operator().apply(editor, context, editor.mode.selectionType)) { if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
return return
} }
runWriteAction { runWriteAction {
// Leave visual mode // Leave visual mode
editor.exitVisualMode() editor.exitVisualMode()
editor.ij.caretModel.moveToOffset(selectionStart)
// Reset the key handler so that the command trie is updated for the new mode (Normal) // Reset the key handler so that the command trie is updated for the new mode (Normal)
// TODO: This should probably be handled by ToHandlerMapping.execute // TODO: This should probably be handled by ToHandlerMapping.execute
@@ -159,6 +161,10 @@ internal class VimSurroundExtension : VimExtension {
companion object { companion object {
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) { fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) }
}
fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
// Save old register values for carets // Save old register values for carets
val surroundings = editor.sortedCarets() val surroundings = editor.sortedCarets()
.map { .map {
@@ -201,7 +207,7 @@ internal class VimSurroundExtension : VimExtension {
val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue
it.first + trimmedValue + it.second it.first + trimmedValue + it.second
} ?: innerValue } ?: innerValue
val textData = PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.CHARACTER_WISE) val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), null)
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false) val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false)
surrounding.caret to putData surrounding.caret to putData
@@ -278,20 +284,41 @@ internal class VimSurroundExtension : VimExtension {
} }
} }
private class Operator : OperatorFunction { private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean { override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val ijEditor = editor.ij val ijEditor = editor.ij
val c = injector.keyGroup.getChar(editor) ?: return true val c = injector.keyGroup.getChar(editor) ?: return true
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
// XXX: Will it work with line-wise or block-wise selections?
val range = getSurroundRange(editor.currentCaret()) ?: return false runWriteAction {
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE) val change = VimPlugin.getChange()
// Jump back to start if (supportsMultipleCursors) {
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor) ijEditor.runWithEveryCaretAndRestore {
applyOnce(ijEditor, change, pair, count)
}
}
else {
applyOnce(ijEditor, change, pair, count)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
}
}
return true return true
} }
private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: SurroundPair, count: Int) {
// XXX: Will it work with line-wise or block-wise selections?
val primaryCaret = editor.caretModel.primaryCaret
val range = getSurroundRange(primaryCaret.vim)
if (range != null) {
val start = RepeatedCharSequence.of(pair.first, count)
val end = RepeatedCharSequence.of(pair.second, count)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end)
}
}
private fun getSurroundRange(caret: VimCaret): TextRange? { private fun getSurroundRange(caret: VimCaret): TextRange? {
val editor = caret.editor val editor = caret.editor
if (editor.mode is Mode.CMD_LINE) { if (editor.mode is Mode.CMD_LINE) {
@@ -380,15 +407,15 @@ private fun getOrInputPair(c: Char, editor: Editor, context: DataContext): Surro
} }
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, tagsOnNewLines: Boolean = false) { private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, count: Int, tagsOnNewLines: Boolean = false) {
runWriteAction { runWriteAction {
val editor = caret.editor val editor = caret.editor
val change = VimPlugin.getChange() val change = VimPlugin.getChange()
val leftSurround = pair.first + if (tagsOnNewLines) "\n" else "" val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
val isEOF = range.endOffset == editor.text().length val isEOF = range.endOffset == editor.text().length
val hasNewLine = editor.endsWithNewLine() val hasNewLine = editor.endsWithNewLine()
val rightSurround = if (tagsOnNewLines) { val rightSurround = (if (tagsOnNewLines) {
if (isEOF && !hasNewLine) { if (isEOF && !hasNewLine) {
"\n" + pair.second "\n" + pair.second
} else { } else {
@@ -396,7 +423,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare
} }
} else { } else {
pair.second pair.second
} }).let { RepeatedCharSequence.of(it, count) }
change.insertText(editor, caret, range.startOffset, leftSurround) change.insertText(editor, caret, range.startOffset, leftSurround)
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround) change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)

View File

@@ -43,7 +43,6 @@ import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
import kotlin.math.min
/** /**
* Provides all the insert/replace related functionality * Provides all the insert/replace related functionality
@@ -156,6 +155,7 @@ class ChangeGroup : VimChangeGroupBase() {
context: ExecutionContext, context: ExecutionContext,
range: TextRange, range: TextRange,
) { ) {
val startPos = editor.offsetToBufferPosition(caret.offset)
val startOffset = editor.getLineStartForOffset(range.startOffset) val startOffset = editor.getLineStartForOffset(range.startOffset)
val endOffset = editor.getLineEndForOffset(range.endOffset) val endOffset = editor.getLineEndForOffset(range.endOffset)
val ijEditor = (editor as IjVimEditor).editor val ijEditor = (editor as IjVimEditor).editor
@@ -165,7 +165,7 @@ class ChangeGroup : VimChangeGroupBase() {
var copiedText: IjVimCopiedText? = null var copiedText: IjVimCopiedText? = null
try { try {
if (injector.registerGroup.isPrimaryRegisterSupported()) { if (injector.registerGroup.isPrimaryRegisterSupported()) {
copiedText = injector.clipboardManager.getPrimaryContent(editor, context) as IjVimCopiedText copiedText = injector.clipboardManager.getPrimaryContent() as IjVimCopiedText
} }
} catch (e: Exception) { } catch (e: Exception) {
// FIXME: [isPrimaryRegisterSupported()] is not implemented perfectly, so there might be thrown an exception after trying to access the primary selection // FIXME: [isPrimaryRegisterSupported()] is not implemented perfectly, so there might be thrown an exception after trying to access the primary selection
@@ -180,11 +180,7 @@ class ChangeGroup : VimChangeGroupBase() {
} }
} }
val afterAction = { val afterAction = {
val firstLine = editor.offsetToBufferPosition( caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, startPos.line))
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) restoreCursor(editor, caret, (caret as IjVimCaret).caret.logicalPosition.line)
} }
if (project != null) { if (project != null) {

View File

@@ -141,7 +141,7 @@ object IjOptions {
// Temporary feature flags during development, not really intended for external use // Temporary feature flags during development, not really intended for external use
val closenotebooks: ToggleOption = val closenotebooks: ToggleOption =
addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true)) addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true))
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true)) val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isHidden = true))
val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true)) val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
val vimHints: ToggleOption = addOption(ToggleOption("vimhints", GLOBAL, "vimhints", false, isHidden = true)) val vimHints: ToggleOption = addOption(ToggleOption("vimhints", GLOBAL, "vimhints", false, isHidden = true))

View File

@@ -0,0 +1,68 @@
package com.maddyhome.idea.vim.group
import com.intellij.codeInsight.daemon.ReferenceImporter
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
import java.util.function.BooleanSupplier
internal object MacroAutoImport {
fun run(editor: Editor, dataContext: DataContext) {
val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
if (!FileDocumentManager.getInstance().requestWriting(editor.document, project)) {
return
}
val importers = ReferenceImporter.EP_NAME.extensionList
if (importers.isEmpty()) {
return
}
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Auto import", true) {
override fun run(indicator: ProgressIndicator) {
val fixes = ReadAction.nonBlocking<List<BooleanSupplier>> {
val fixes = mutableListOf<BooleanSupplier>()
file.accept(object : PsiRecursiveElementWalkingVisitor() {
override fun visitElement(element: PsiElement) {
for (reference in element.references) {
if (reference.resolve() != null) {
continue
}
for (importer in importers) {
importer.computeAutoImportAtOffset(editor, file, element.textRange.startOffset, true)
?.let(fixes::add)
}
}
super.visitElement(element)
}
})
return@nonBlocking fixes
}.executeSynchronously()
ApplicationManager.getApplication().invokeAndWait {
WriteCommandAction.writeCommandAction(project)
.withName("Auto Import")
.withGroupId("IdeaVimAutoImportAfterMacro")
.shouldRecordActionForActiveDocument(true)
.run<RuntimeException> {
fixes.forEach { it.asBoolean }
}
}
}
})
}
}

View File

@@ -21,6 +21,7 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.macro.VimMacroBase import com.maddyhome.idea.vim.macro.VimMacroBase
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
/** /**
* Used to handle playback of macros * Used to handle playback of macros
@@ -89,6 +90,9 @@ internal class MacroGroup : VimMacroBase() {
} finally { } finally {
keyStack.removeFirst() keyStack.removeFirst()
} }
if (!isInternalMacro) {
MacroAutoImport.run(editor.ij, context.ij)
}
} }
if (isInternalMacro) { if (isInternalMacro) {

View File

@@ -87,6 +87,9 @@ internal class MotionGroup : VimMotionGroupBase() {
} }
override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion { override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineStart)
}
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, false) return moveCaretToColumn(editor, caret, col, false)
} }
@@ -95,6 +98,15 @@ internal class MotionGroup : VimMotionGroupBase() {
editor: VimEditor, editor: VimEditor,
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int { ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
val offset = caret.ij.visualLineStart
val line = editor.offsetToBufferPosition(offset).line
return if (offset == editor.getLineStartOffset(line)) {
editor.getLeadingCharacterOffset(line, 0)
} else {
offset
}
}
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
val bufferLine = caret.getLine() val bufferLine = caret.getLine()
return editor.getLeadingCharacterOffset(bufferLine, col) return editor.getLeadingCharacterOffset(bufferLine, col)
@@ -105,6 +117,9 @@ internal class MotionGroup : VimMotionGroupBase() {
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
allowEnd: Boolean, allowEnd: Boolean,
): Motion { ): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineEnd - 1)
}
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, allowEnd) return moveCaretToColumn(editor, caret, col, allowEnd)
} }

View File

@@ -33,7 +33,6 @@ import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.handler.KeyMapIssue import com.maddyhome.idea.vim.handler.KeyMapIssue
import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.MessageHelper
@@ -41,8 +40,6 @@ import com.maddyhome.idea.vim.icons.VimIcons
import com.maddyhome.idea.vim.key.ShortcutOwner import com.maddyhome.idea.vim.key.ShortcutOwner
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ijOptions
import com.maddyhome.idea.vim.options.OptionConstants
import com.maddyhome.idea.vim.statistic.ActionTracker import com.maddyhome.idea.vim.statistic.ActionTracker
import com.maddyhome.idea.vim.ui.VimEmulationConfigurable import com.maddyhome.idea.vim.ui.VimEmulationConfigurable
import com.maddyhome.idea.vim.vimscript.services.VimRcService import com.maddyhome.idea.vim.vimscript.services.VimRcService
@@ -62,55 +59,11 @@ internal class NotificationService(private val project: Project?) {
@Suppress("unused") @Suppress("unused")
constructor() : this(null) constructor() : this(null)
fun notifyAboutIdeaPut() { fun notifyAboutNewUndo() {}
val notification = Notification(
IDEAVIM_NOTIFICATION_ID,
IDEAVIM_NOTIFICATION_TITLE,
"""Add <code>ideaput</code> to <code>clipboard</code> option to perform a put via the IDE<br/><b><code>set clipboard+=ideaput</code></b>""",
NotificationType.INFORMATION,
)
notification.addAction(OpenIdeaVimRcAction(notification)) fun notifyAboutIdeaPut() {}
notification.addAction( fun notifyAboutIdeaJoin(editor: VimEditor) {}
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( fun enableRepeatingMode() = Messages.showYesNoDialog(
"Do you want to enable repeating keys in macOS on press and hold?\n\n" + "Do you want to enable repeating keys in macOS on press and hold?\n\n" +
@@ -305,16 +258,16 @@ internal class NotificationService(private val project: Project?) {
notification = notification =
Notification(IDEAVIM_NOTIFICATION_ID, IDEAVIM_NOTIFICATION_TITLE, content, NotificationType.INFORMATION).also { Notification(IDEAVIM_NOTIFICATION_ID, IDEAVIM_NOTIFICATION_TITLE, content, NotificationType.INFORMATION).also {
it.whenExpired { notification = null } it.whenExpired { notification = null }
it.addAction(StopTracking()) it.addAction(StopTracking())
if (id != null || possibleIDs?.size == 1) { if (id != null || possibleIDs?.size == 1) {
it.addAction(CopyActionId(id ?: possibleIDs?.get(0), project)) it.addAction(CopyActionId(id ?: possibleIDs?.get(0), project))
}
it.notify(project)
} }
it.notify(project)
}
if (id != null) { if (id != null) {
ActionTracker.Util.logTrackedAction(id) ActionTracker.Util.logTrackedAction(id)
} }

View File

@@ -25,10 +25,9 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
/** /**
* This group works with command associated with copying and pasting text * This group works with command associated with copying and pasting text
*/ */
@@ -128,7 +127,7 @@ public class RegisterGroup extends VimRegisterGroupBase implements PersistentSta
final String text = VimPlugin.getXML().getSafeXmlText(textElement); final String text = VimPlugin.getXML().getSafeXmlText(textElement);
if (text != null) { if (text != null) {
logger.trace("Register data parsed"); logger.trace("Register data parsed");
register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type); register = new Register(key, type, text, Collections.emptyList());
} }
else { else {
logger.trace("Cannot parse register data"); logger.trace("Cannot parse register data");

View File

@@ -37,7 +37,6 @@ import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.ide.isRider import com.maddyhome.idea.vim.ide.isRider
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import com.maddyhome.idea.vim.newapi.IjVimCaret import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimCopiedText
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
@@ -128,7 +127,7 @@ internal class PutGroup : VimPutBase() {
point.dispose() point.dispose()
if (!caret.isValid) return@forEach if (!caret.isValid) return@forEach
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.copiedText.text.length) val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.text.length)
val endOffset = if (data.indent) { val endOffset = if (data.indent) {
doIndent( doIndent(
vimEditor, vimEditor,
@@ -180,10 +179,12 @@ internal class PutGroup : VimPutBase() {
val allContentsBefore = CopyPasteManager.getInstance().allContents val allContentsBefore = CopyPasteManager.getInstance().allContents
val sizeBeforeInsert = allContentsBefore.size val sizeBeforeInsert = allContentsBefore.size
val firstItemBefore = allContentsBefore.firstOrNull() val firstItemBefore = allContentsBefore.firstOrNull()
logger.debug { "Copied text: ${text.copiedText}" } logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" }
val (textContent, transferableData) = text.copiedText as IjVimCopiedText
val origContent: TextBlockTransferable = val origContent: TextBlockTransferable =
injector.clipboardManager.setClipboardText(textContent, textContent, transferableData) as TextBlockTransferable injector.clipboardManager.setClipboardText(
text.text,
transferableData = text.transferableData,
) as TextBlockTransferable
val allContentsAfter = CopyPasteManager.getInstance().allContents val allContentsAfter = CopyPasteManager.getInstance().allContents
val sizeAfterInsert = allContentsAfter.size val sizeAfterInsert = allContentsAfter.size
try { try {
@@ -191,7 +192,7 @@ internal class PutGroup : VimPutBase() {
} finally { } finally {
val textInClipboard = (firstItemBefore as? TextBlockTransferable) val textInClipboard = (firstItemBefore as? TextBlockTransferable)
?.getTransferData(DataFlavor.stringFlavor) as? String ?.getTransferData(DataFlavor.stringFlavor) as? String
val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.text val textOnTop = textInClipboard != null && textInClipboard != text.text
if (sizeBeforeInsert != sizeAfterInsert || textOnTop) { if (sizeBeforeInsert != sizeAfterInsert || textOnTop) {
// Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register // Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) } (CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }

View File

@@ -81,12 +81,8 @@ internal object IdeaSelectionControl {
return@singleTask return@singleTask
} }
// The editor changed the selection, but we want to keep the current Vim mode. This is the case if we're editing
// a template and either 'idearefactormode' is set to `keep`, or the current Vim mode already has a selection.
// (i.e., if the user explicitly enters Visual or Select and then moves to the next template variable, don't
// switch to Select or Visual - keep the current Vim selection mode)
if (dontChangeMode(editor)) { if (dontChangeMode(editor)) {
IdeaRefactorModeHelper.correctEditorSelection(editor) IdeaRefactorModeHelper.correctSelection(editor)
logger.trace { "Selection corrected for refactoring" } logger.trace { "Selection corrected for refactoring" }
return@singleTask return@singleTask
} }
@@ -150,13 +146,8 @@ internal object IdeaSelectionControl {
} }
private fun chooseNonSelectionMode(editor: Editor): Mode { private fun chooseNonSelectionMode(editor: Editor): Mode {
// If we're in an active template and the editor has just removed a selection without adding a new one, we're in a val templateActive = editor.isTemplateActive()
// variable with nothing to select. When 'idearefactormode' is "select", enter Insert mode. Otherwise, stay in if (templateActive && editor.vim.mode.inNormalMode || editor.inInsertMode) {
// Normal.
// Note that when 'idearefactormode' is "visual", we enter Normal mode for an empty variable. While it might seem
// natural to want to insert text here, we could also paste a register, dot-repeat an insertion or other Normal
// commands. Normal is Visual without the selection.
if ((editor.isTemplateActive() && editor.vim.mode.inNormalMode && editor.vim.isIdeaRefactorModeSelect) || editor.inInsertMode) {
return Mode.INSERT return Mode.INSERT
} }
return Mode.NORMAL() return Mode.NORMAL()

View File

@@ -352,7 +352,7 @@ public class EditorHelper {
final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight); final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
final @NotNull VimEditor editor1 = new IjVimEditor(editor); final @NotNull VimEditor editor1 = new IjVimEditor(editor);
final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) - 1; final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) + editor.getSettings().getAdditionalLinesCount();
final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine); final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine);
// For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen. // For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen.

View File

@@ -12,7 +12,9 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.util.ui.table.JBTableRowEditor import com.intellij.util.ui.table.JBTableRowEditor
@@ -21,6 +23,8 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import java.awt.Component import java.awt.Component
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JTable import javax.swing.JTable
@@ -98,8 +102,7 @@ internal fun Editor.isPrimaryEditor(): Boolean {
internal fun Editor.isTerminalEditor(): Boolean { internal fun Editor.isTerminalEditor(): Boolean {
return !isViewer return !isViewer
&& document.isWritable && document.isWritable
&& !EditorHelper.isFileEditor(this) && this.editorKind == EditorKind.CONSOLE
&& !EditorHelper.isDiffEditor(this)
} }
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor // Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
@@ -132,3 +135,41 @@ internal val Caret.vimLine: Int
*/ */
internal val Editor.vimLine: Int internal val Editor.vimLine: Int
get() = this.caretModel.currentCaret.vimLine 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
}
}

View File

@@ -25,15 +25,19 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.progress.util.ProgressIndicatorUtils import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.refactoring.actions.BaseRefactoringAction
import com.maddyhome.idea.vim.RegisterActions import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction import com.maddyhome.idea.vim.api.NativeAction
import com.maddyhome.idea.vim.api.VimActionExecutor import com.maddyhome.idea.vim.api.VimActionExecutor
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.newapi.IjNativeAction import com.maddyhome.idea.vim.newapi.IjNativeAction
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.awt.Component import java.awt.Component
import javax.swing.JComponent import javax.swing.JComponent
@@ -70,6 +74,12 @@ internal class IjActionExecutor : VimActionExecutor {
thisLogger().error("Actions cannot be updated when write-action is running or pending") thisLogger().error("Actions cannot be updated when write-action is running or pending")
} }
val startVisualModeType = (editor?.mode as? Mode.VISUAL)?.selectionType
val startVisualCaretSelection = if (editor != null && startVisualModeType != null && action.action !is BaseRefactoringAction)
editor.primaryCaret().let { Triple(it.offset, it.selectionStart, it.selectionEnd) }
else
null
val ijAction = (action as IjNativeAction).action val ijAction = (action as IjNativeAction).action
try { try {
isRunningActionFromVim = true isRunningActionFromVim = true
@@ -79,6 +89,20 @@ internal class IjActionExecutor : VimActionExecutor {
val place = ijAction.choosePlace() val place = ijAction.choosePlace()
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true) val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
res.waitFor(5_000) res.waitFor(5_000)
if (startVisualModeType != null && startVisualCaretSelection != null) {
val primaryCaret = editor.primaryCaret()
val endVisualCaretOffset = primaryCaret.offset
if (startVisualCaretSelection.first != endVisualCaretOffset) {
if (!editor.inVisualMode || (editor.mode as Mode.VISUAL).selectionType != startVisualModeType) {
injector.visualMotionGroup.toggleVisual(editor, 1, 0, startVisualModeType)
}
primaryCaret.moveToOffset(startVisualCaretSelection.first)
primaryCaret.setSelection(startVisualCaretSelection.second, startVisualCaretSelection.third)
primaryCaret.moveToOffset(endVisualCaretOffset)
}
}
return res.isDone return res.isDone
} finally { } finally {
isRunningActionFromVim = false isRunningActionFromVim = false

View File

@@ -58,7 +58,7 @@ internal object ScrollViewHelper {
// that this needs to be replaced as a more or less dumb line for line rewrite. // that this needs to be replaced as a more or less dumb line for line rewrite.
val topLine = getVisualLineAtTopOfScreen(editor) val topLine = getVisualLineAtTopOfScreen(editor)
val bottomLine = getVisualLineAtBottomOfScreen(editor) val bottomLine = getVisualLineAtBottomOfScreen(editor)
val lastLine = vimEditor.getVisualLineCount() - 1 val lastLine = vimEditor.getVisualLineCount() + editor.settings.additionalLinesCount
// We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred // We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred
val scrollOffset = injector.options(vimEditor).scrolloff val scrollOffset = injector.options(vimEditor).scrolloff

View File

@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.application
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
@@ -30,6 +31,7 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.Contract import org.jetbrains.annotations.Contract
import java.awt.Font import java.awt.Font
import java.util.* import java.util.*
import javax.swing.Timer
internal fun updateSearchHighlights( internal fun updateSearchHighlights(
pattern: String?, pattern: String?,
@@ -84,6 +86,12 @@ 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 * Refreshes current search highlights for all visible editors
*/ */
@@ -125,27 +133,43 @@ private fun updateSearchHighlights(
// hlsearch (+ incsearch/noincsearch) // hlsearch (+ incsearch/noincsearch)
// Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given // Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given
// `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows // `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows
val vimEditor = editor.vim val isSearching = injector.commandLine.getActiveCommandLine() != null
val editorLastLine = vimEditor.lineCount() - 1 application.invokeLater {
val searchStartLine = searchRange?.startLine ?: 0 val vimEditor = editor.vim
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine) val editorLastLine = vimEditor.lineCount() - 1
if (searchStartLine <= editorLastLine) { val searchStartLine = searchRange?.startLine ?: 0
val results = val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
injector.searchHelper.findAll( if (searchStartLine <= editorLastLine) {
vimEditor, val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished
pattern, val visibleTopLeft = visibleArea.location
searchStartLine, val visibleBottomRight = visibleArea.location.apply { translate(visibleArea.width, visibleArea.height) }
searchEndLine, val visibleStartOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleTopLeft))
shouldIgnoreCase(pattern, shouldIgnoreSmartCase) val visibleEndOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleBottomRight))
) val visibleStartLine = editor.document.getLineNumber(visibleStartOffset)
if (results.isNotEmpty()) { val visibleEndLine = editor.document.getLineNumber(visibleEndOffset)
if (editor === currentEditor?.ij) { removeSearchHighlights(editor)
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
val results =
injector.searchHelper.findAll(
vimEditor,
pattern,
searchStartLine.coerceAtLeast(visibleStartLine),
searchEndLine.coerceAtMost(visibleEndLine),
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
)
if (results.isNotEmpty()) {
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
}
highlightSearchResults(editor, pattern, results, currentMatchOffset)
if (!isSearching) {
removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart()
}
} }
highlightSearchResults(editor, pattern, results, currentMatchOffset)
} }
editor.vimLastSearch = pattern
} }
editor.vimLastSearch = pattern
} else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) { } else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
// nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor // nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor
// only), because 'incsearch' is active. But we don't show a search if Visual is active (behind Command-line of // only), because 'incsearch' is active. But we don't show a search if Visual is active (behind Command-line of
@@ -179,6 +203,7 @@ private fun updateSearchHighlights(
} }
} }
removeHighlightsTimer.restart()
return currentEditorCurrentMatchOffset return currentEditorCurrentMatchOffset
} }
@@ -204,7 +229,7 @@ private fun removeSearchHighlights(editor: Editor) {
*/ */
@Contract("_, _, false -> false; _, null, true -> false") @Contract("_, _, false -> false; _, null, true -> false")
private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean { private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean {
return hlSearch && newPattern != null && newPattern != editor.vimLastSearch && newPattern != "" return hlSearch && newPattern != null && newPattern != ""
} }
private fun findClosestMatch( private fun findClosestMatch(

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.registry.Registry
import com.intellij.util.PlatformUtils import com.intellij.util.PlatformUtils
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
@@ -29,6 +30,8 @@ import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.newapi.IjVimCaret import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
/** /**
@@ -82,15 +85,7 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking { editor.runWithChangeTracking {
undoManager.undo(fileEditor) undoManager.undo(fileEditor)
restoreVisualMode(editor)
// We execute undo one more time if the previous one just restored selection
if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
undoManager.undo(fileEditor)
}
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
} }
} else { } else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) { runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
@@ -241,4 +236,21 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService {
val hasChanges: Boolean val hasChanges: Boolean
get() = changeListener.hasChanged || initialPath != editor.getPath() get() = changeListener.hasChanged || initialPath != editor.getPath()
} }
private fun restoreVisualMode(editor: VimEditor) {
if (!editor.inVisualMode && editor.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)
}
}
} }

View File

@@ -18,7 +18,6 @@ import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolder
import com.maddyhome.idea.vim.api.CaretRegisterStorageBase
import com.maddyhome.idea.vim.api.LocalMarkStorage import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo import com.maddyhome.idea.vim.api.SelectionInfo
import com.maddyhome.idea.vim.common.InsertSequence import com.maddyhome.idea.vim.common.InsertSequence
@@ -98,7 +97,6 @@ internal var Caret.vimInsertStart: RangeMarker by userDataOr {
} }
// TODO: Data could be lost during visual block motion // 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.markStorage: LocalMarkStorage? by userDataCaretToEditor()
internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor() internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()

View File

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

View File

@@ -16,8 +16,9 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
import com.intellij.codeInsight.template.Template import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManagerListener import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateImpl import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction import com.intellij.ide.actions.ApplyIntentionAction
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
@@ -32,30 +33,25 @@ import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actions.EnterAction import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.keymap.KeymapManager import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.action.VimShortcutKeyAction import com.maddyhome.idea.vim.action.VimShortcutKeyAction
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.group.NotificationService import com.maddyhome.idea.vim.group.NotificationService
import com.maddyhome.idea.vim.group.RegisterGroup import com.maddyhome.idea.vim.group.RegisterGroup
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
import com.maddyhome.idea.vim.helper.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.hasVisualSelection
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.initInjector import com.maddyhome.idea.vim.newapi.initInjector
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inNormalMode
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@@ -70,6 +66,7 @@ internal object IdeaSpecifics {
private val surrounderAction = private val surrounderAction =
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction" "com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
private var editor: Editor? = null private var editor: Editor? = null
private var caretOffset = -1
private var completionData: CompletionData? = null private var completionData: CompletionData? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) { override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
@@ -78,6 +75,7 @@ internal object IdeaSpecifics {
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR) val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) { if (hostEditor != null) {
editor = hostEditor editor = hostEditor
caretOffset = hostEditor.caretModel.offset
} }
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
@@ -154,26 +152,46 @@ internal object IdeaSpecifics {
if (VimPlugin.isNotEnabled()) return if (VimPlugin.isNotEnabled()) return
val editor = editor val editor = editor
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) { if (editor != null) {
completionData?.recordCompletion(editor, VimPlugin.getRegister()) if (action is ChooseItemAction && injector.registerGroup.isRecording) {
} completionData?.recordCompletion(editor, VimPlugin.getRegister()
)
}
//region Enter insert mode after surround with if //region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any { if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith( action.templatePresentation.text.endsWith(
it, it,
) )
}
) {
editor?.let {
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim)
KeyHandler.getInstance().reset(it.vim)
}
} }
) { else if (action is NextVariableAction && TemplateManagerImpl.getTemplateState(editor) == null) {
editor?.let { editor.vim.exitInsertMode(event.dataContext.vim)
it.vim.mode = Mode.NORMAL() KeyHandler.getInstance().reset(editor.vim)
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim) }
KeyHandler.getInstance().reset(it.vim) //endregion
if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {
val scrollModel = editor.scrollingModel as ScrollingModelImpl
if (scrollModel.isScrollingNow) {
val v = scrollModel.verticalScrollOffset
val h = scrollModel.horizontalScrollOffset
scrollModel.finishAnimation()
scrollModel.scroll(h, v)
scrollModel.finishAnimation()
}
injector.scroll.scrollCaretIntoView(editor.vim)
} }
} }
//endregion
this.editor = null this.editor = null
this.caretOffset = -1
this.completionData?.dispose() this.completionData?.dispose()
this.completionData = null this.completionData = null
@@ -217,44 +235,7 @@ internal object IdeaSpecifics {
} }
} }
//region Handle mode and selection for Live Templates and refactorings //region Enter insert mode for surround templates without selection
/**
* Listen to template notifications to provide additional handling of templates, selection and Vim mode
*
* Most of the handling of templates is done by [IdeaSelectionControl]. When moving between editable segments of a
* template, the editor will remove the current selection and add a selection for the text of the new segment, if any.
* [IdeaSelectionControl] will be notified and handle Vim mode based on the `'idearefactormode'` option. This will
* switch to Select mode for each variable by default (or Insert if there's no text) or Visual/Normal mode when the
* option is set to "visual".
*
* Select mode makes IdeaVim behaviour a little more like a traditional editor. Inserting a Live Template is typically
* done in Insert mode, so moving to the next editable segment always switches to Select mode, ready to continue
* typing. At the end of the template, the caret is still in Insert mode, still ready to continue typing.
*
* The exception is for an "inline" template. This is an editable segment placed on top of existing text and is
* typically used to rename a symbol. This is usually a refactoring started with an explicit action, and switching to
* Select mode means the user is ready to start typing. However, accepting the change should switch back to Normal, as
* the editing/refactoring is complete. This also helps when the refactoring shows a progress dialog, since Escape to
* switch back to Normal can cancel the refactoring.
*
* Again, this is like a traditional editor, and still only changes Vim mode based on an explicit action.
*
* This class handles the following edge cases that [IdeaSelectionControl] cannot:
* * When `'idearefactormode'` is "keep" it will maintain the current Vim mode, by removing the current selection
* (so [IdeaSelectionControl] does nothing). It also ensures that the current Vim selection mode matches the actual
* selection (i.e., character wise vs line wise).
* * When the editor is moving to the next segment but there is no current selection and the next segment is empty,
* there will be no change in selection and [IdeaSelectionControl] will not be notified. This class will switch to
* Insert mode when `'idearefactormode'` is set to "select" and Normal when it's "visual".
* * A special case of the above scenario is moving to the end of the template, which always has no selection. If
* there is no current selection [IdeaSelectionControl] is not called and the mode is not updated, so we stay in
* whatever mode the user had last - Insert, Normal, whatever. When there is a selection, [IdeaSelectionControl]
* will be called, but since there is no template active anymore, it would set the mode to Normal. This class will
* switch to Insert when `'idearefactormode'` is "select" and Normal for "visual". It does nothing for "keep".
* * If the template is an "inline" template, it is typically a rename refactoring on existing text.
* When ending the template and `'idearefactormode'` is "select", the above would leave is in Insert mode. This
* class will switch to Normal for inline templates, for both "select" and "visual". It does nothing for "keep".
*/
class VimTemplateManagerListener : TemplateManagerListener { class VimTemplateManagerListener : TemplateManagerListener {
override fun templateStarted(state: TemplateState) { override fun templateStarted(state: TemplateState) {
if (VimPlugin.isNotEnabled()) return if (VimPlugin.isNotEnabled()) return
@@ -267,65 +248,27 @@ internal object IdeaSpecifics {
oldIndex: Int, oldIndex: Int,
newIndex: Int, newIndex: Int,
) { ) {
fun VimEditor.exitMode() = when (this.mode) { if (templateState.editor.vim.isIdeaRefactorModeKeep) {
is Mode.SELECT -> this.exitSelectMode(adjustCaretPosition = false) IdeaRefactorModeHelper.correctSelection(templateState.editor)
is Mode.VISUAL -> this.exitVisualMode()
is Mode.INSERT -> this.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(this))
else -> Unit
}
fun Template?.myIsInline() = this is TemplateImpl && this.isInline
val vimEditor = editor.vim
// This function is called when moving between variables. It is called with oldIndex == -1 when moving to the
// first variable, and newIndex == -1 just before ending, when moving to the end of the template text, or to
// $END$ (which is treated as a segment, but not a variable). If there are no variables, it is called with
// oldIndex == newIndex == -1.
if (vimEditor.isIdeaRefactorModeKeep) {
IdeaRefactorModeHelper.correctEditorSelection(templateState.editor)
}
else {
// The editor places the caret at the exclusive end of the variable. For Visual, unless we've enabled
// exclusive selection, move it to the inclusive end.
// Note that "keep" does this as part of IdeaRefactorModeHelper
if (editor.selectionModel.hasSelection()
&& !editor.vim.isIdeaRefactorModeSelect
&& templateState.currentVariableRange?.endOffset == editor.caretModel.offset
&& !injector.options(vimEditor).selection.contains("exclusive")
) {
vimEditor.primaryCaret()
.moveToInlayAwareOffset((editor.selectionModel.selectionEnd - 1).coerceAtLeast(editor.selectionModel.selectionStart))
}
if (newIndex == -1 && template.myIsInline() && vimEditor.isIdeaRefactorModeSelect) {
// Rename refactoring has just completed with 'idearefactormode' in "select". Return to Normal instead of
// our default behaviour of switching to Insert
if (vimEditor.mode !is Mode.NORMAL) {
vimEditor.exitMode()
vimEditor.mode = Mode.NORMAL()
}
} else {
// IdeaSelectionControl will not be called if we're moving to a new variable with no change in selection.
// And if we're moving to the end of the template, the change in selection will reset us to Normal because
// IdeaSelectionControl will be called when the template is no longer active.
if ((!editor.selectionModel.hasSelection() && !vimEditor.mode.hasVisualSelection) || newIndex == -1) {
if (vimEditor.isIdeaRefactorModeSelect) {
if (vimEditor.mode !is Mode.INSERT) {
vimEditor.exitMode()
injector.application.runReadAction {
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
VimPlugin.getChange().insertBeforeCaret(editor.vim, context)
}
}
} else {
vimEditor.mode = Mode.NORMAL()
}
}
}
} }
} }
}) })
if (state.editor.vim.isIdeaRefactorModeKeep) {
IdeaRefactorModeHelper.correctSelection(editor)
} else {
if (!editor.selectionModel.hasSelection()) {
// Enable insert mode if there is no selection in template
// Template with selection is handled by [com.maddyhome.idea.vim.group.visual.VisualMotionGroup.controlNonVimSelectionChange]
if (editor.vim.inNormalMode) {
VimPlugin.getChange().insertBeforeCaret(
editor.vim,
injector.executionContextManager.getEditorExecutionContext(editor.vim),
)
KeyHandler.getInstance().reset(editor.vim)
}
}
}
} }
} }
//endregion //endregion

View File

@@ -97,6 +97,7 @@ import com.maddyhome.idea.vim.newapi.IjVimSearchGroup
import com.maddyhome.idea.vim.newapi.InsertTimeRecorder import com.maddyhome.idea.vim.newapi.InsertTimeRecorder
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.selectionType import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
@@ -411,9 +412,20 @@ internal object VimListenerManager {
// We can't rely on being passed a non-null editor, so check for Code With Me scenarios explicitly // 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 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) MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroup.fileEditorManagerSelectionChangedCallback(event) FileGroup.fileEditorManagerSelectionChangedCallback(event)
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event) // VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event) IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor) VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
} }

View File

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

View File

@@ -12,8 +12,6 @@ import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.editor.VisualPosition
import com.maddyhome.idea.vim.api.BufferPosition 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.ImmutableVimCaret
import com.maddyhome.idea.vim.api.LocalMarkStorage import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo import com.maddyhome.idea.vim.api.SelectionInfo
@@ -21,6 +19,7 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretBase import com.maddyhome.idea.vim.api.VimCaretBase
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimVisualPosition import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.InsertSequence import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.group.visual.VisualChange import com.maddyhome.idea.vim.group.visual.VisualChange
@@ -29,7 +28,6 @@ import com.maddyhome.idea.vim.helper.insertHistory
import com.maddyhome.idea.vim.helper.lastSelectionInfo import com.maddyhome.idea.vim.helper.lastSelectionInfo
import com.maddyhome.idea.vim.helper.markStorage import com.maddyhome.idea.vim.helper.markStorage
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset 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.resetVimLastColumn
import com.maddyhome.idea.vim.helper.vimInsertStart import com.maddyhome.idea.vim.helper.vimInsertStart
import com.maddyhome.idea.vim.helper.vimLastColumn import com.maddyhome.idea.vim.helper.vimLastColumn
@@ -37,22 +35,14 @@ import com.maddyhome.idea.vim.helper.vimLastVisualOperatorRange
import com.maddyhome.idea.vim.helper.vimLine import com.maddyhome.idea.vim.helper.vimLine
import com.maddyhome.idea.vim.helper.vimSelectionStart import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimSelectionStartClear import com.maddyhome.idea.vim.helper.vimSelectionStartClear
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
internal class IjVimCaret(val caret: Caret) : VimCaretBase() { internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
override val registerStorage: CaretRegisterStorage override val registerStorage: VimRegisterGroup
get() { get() = injector.registerGroup
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 override val markStorage: LocalMarkStorage
get() { get() {
var storage = this.caret.markStorage var storage = this.caret.markStorage

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.editor.ex.ScrollingModelEx
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.impl.CaretModelImpl import com.intellij.openapi.editor.impl.CaretModelImpl
import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.VirtualFileManager
import com.maddyhome.idea.vim.api.BufferPosition import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
@@ -34,6 +35,7 @@ import com.maddyhome.idea.vim.api.VimEditorBase
import com.maddyhome.idea.vim.api.VimFoldRegion import com.maddyhome.idea.vim.api.VimFoldRegion
import com.maddyhome.idea.vim.api.VimIndentConfig import com.maddyhome.idea.vim.api.VimIndentConfig
import com.maddyhome.idea.vim.api.VimScrollingModel import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVirtualFile import com.maddyhome.idea.vim.api.VimVirtualFile
import com.maddyhome.idea.vim.api.VimVisualPosition import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
@@ -148,7 +150,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
} }
} }
} }
editor.document.insertString(atPosition, text) editor.document.insertString(atPosition, StringUtil.convertLineSeparators(text, "\n"))
} }
override fun replaceString(start: Int, end: Int, newString: String) { override fun replaceString(start: Int, end: Int, newString: String) {
@@ -177,21 +179,38 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
return editor.caretModel.allCarets.map { IjVimCaret(it) } return editor.caretModel.allCarets.map { IjVimCaret(it) }
} }
override var isFirstCaret = true
override var isReversingCarets = false
@Suppress("ideavimRunForEachCaret") @Suppress("ideavimRunForEachCaret")
override fun forEachCaret(action: (VimCaret) -> Unit) { override fun forEachCaret(action: (VimCaret) -> Unit) {
if (editor.vim.inBlockSelection) { if (editor.vim.inBlockSelection) {
action(IjVimCaret(editor.caretModel.primaryCaret)) action(IjVimCaret(editor.caretModel.primaryCaret))
} else { } else {
editor.caretModel.runForEachCaret({ try {
if (it.isValid) { editor.caretModel.runForEachCaret({
action(IjVimCaret(it)) if (it.isValid) {
} action(IjVimCaret(it))
}, false) isFirstCaret = false
}
}, false)
} finally {
isFirstCaret = true
}
} }
} }
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) { override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {
editor.caretModel.runForEachCaret({ action(IjVimCaret(it)) }, reverse) isReversingCarets = reverse
try {
editor.caretModel.runForEachCaret({
action(IjVimCaret(it))
isFirstCaret = false
}, reverse)
} finally {
isFirstCaret = true
isReversingCarets = false
}
} }
override fun isInForEachCaretScope(): Boolean { override fun isInForEachCaretScope(): Boolean {
@@ -297,6 +316,18 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
editor.document.deleteString(range.startOffset, range.endOffset) editor.document.deleteString(range.startOffset, range.endOffset)
} }
override fun getSelectionModel(): VimSelectionModel {
return object : VimSelectionModel {
private val sm = editor.selectionModel
override val selectionStart = sm.selectionStart
override val selectionEnd = sm.selectionEnd
override fun hasSelection(): Boolean {
return sm.hasSelection()
}
}
}
override fun getScrollingModel(): VimScrollingModel { override fun getScrollingModel(): VimScrollingModel {
return object : VimScrollingModel { return object : VimScrollingModel {
private val sm = editor.scrollingModel as ScrollingModelEx private val sm = editor.scrollingModel as ScrollingModelEx
@@ -489,6 +520,10 @@ 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 { override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T {
return caret return caret
} }

View File

@@ -63,8 +63,7 @@ internal class IjVimMessages : VimMessagesBase() {
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these // scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
// things happen. // things happen.
override fun clearStatusBarMessage() { override fun clearStatusBarMessage() {
val currentMessage = message if (message.isNullOrEmpty()) return
if (currentMessage.isNullOrEmpty()) return
// Don't clear the status bar message if we've only just set it // Don't clear the status bar message if we've only just set it
if (!allowClearStatusBarMessage) return if (!allowClearStatusBarMessage) return
@@ -72,7 +71,7 @@ internal class IjVimMessages : VimMessagesBase() {
ProjectManager.getInstance().openProjects.forEach { project -> ProjectManager.getInstance().openProjects.forEach { project ->
WindowManager.getInstance().getStatusBar(project)?.let { statusBar -> WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
// Only clear the status bar if it's showing our last message // Only clear the status bar if it's showing our last message
if (statusBar.info?.contains(currentMessage) == true) { if (statusBar.info?.contains(message.toString()) == true) {
statusBar.info = "" statusBar.info = ""
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2023 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -14,7 +14,7 @@ import com.intellij.util.ui.JBInsets
import java.awt.Component import java.awt.Component
import java.awt.Insets import java.awt.Insets
internal class ExPanelBorder : SideBorder(JBColor.border(), TOP) { internal class ExPanelBorder internal constructor() : SideBorder(JBColor.border(), TOP) {
override fun getBorderInsets(component: Component?): Insets { override fun getBorderInsets(component: Component?): Insets {
return JBInsets(getThickness() + 2, 0, 2, 2) return JBInsets(getThickness() + 2, 0, 2, 2)

View File

@@ -283,7 +283,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
.calculateCount0Snapshot() .calculateCount0Snapshot()
) )
if (labelText == "/" || labelText == "?" || searchCommand) { if ((labelText == "/" || labelText == "?" || searchCommand) && !injector.macro.isExecutingMacro) {
val forwards = labelText != "?" // :s, :g, :v are treated as forwards val forwards = labelText != "?" // :s, :g, :v are treated as forwards
val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0) val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0)
val pattern = searchText.take(patternEnd) val pattern = searchText.take(patternEnd)

View File

@@ -20,6 +20,7 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.helper.VimLockLabel import com.maddyhome.idea.vim.helper.VimLockLabel
import com.maddyhome.idea.vim.helper.hasBlockOrUnderscoreCaret
import com.maddyhome.idea.vim.helper.hasVisualSelection import com.maddyhome.idea.vim.helper.hasVisualSelection
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
import com.maddyhome.idea.vim.newapi.ijOptions import com.maddyhome.idea.vim.newapi.ijOptions
@@ -86,50 +87,36 @@ internal object IdeaRefactorModeHelper {
@VimLockLabel.RequiresReadLock @VimLockLabel.RequiresReadLock
@RequiresReadLock @RequiresReadLock
fun calculateCorrectionsToSyncEditorToMode(editor: Editor): List<Action> { fun calculateCorrections(editor: Editor): List<Action> {
val corrections = mutableListOf<Action>() val corrections = mutableListOf<Action>()
val mode = editor.vim.mode val mode = editor.vim.mode
// If the current Vim mode doesn't have a selection, remove the editor's selection
if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) { if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
corrections.add(Action.RemoveSelection) corrections.add(Action.RemoveSelection)
} }
// If the current mode does have a selection, make sure it matches the selection type of the editor
if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) { if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim) val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim)
if (mode.selectionType != selectionType) { if (mode.selectionType != selectionType) {
val newMode = when (mode) { val newMode = when (mode) {
is Mode.SELECT -> mode.copy(selectionType = selectionType) is Mode.SELECT -> mode.copy(selectionType)
is Mode.VISUAL -> mode.copy(selectionType = selectionType) is Mode.VISUAL -> mode.copy(selectionType)
else -> error("IdeaVim should be either in visual or select modes") else -> error("IdeaVim should be either in visual or select modes")
} }
corrections.add(Action.SetMode(newMode)) corrections.add(Action.SetMode(newMode))
} }
} }
// IntelliJ places the caret on the exclusive end of the current variable. I.e. *after* the end of the variable. if (editor.hasBlockOrUnderscoreCaret()) {
// This makes sense when selecting the current variable, and when the editor is using a bar caret - the selection is
// naturally exclusive (and IdeaVim treats Select mode as having exclusive selection).
// But we don't have a selection, so it's weird to be placed at the end of a selection that no longer exists. Move
// the caret to the start of the (missing) selection instead.
if (editor.vim.isIdeaRefactorModeKeep) {
TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange -> TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange ->
if (!segmentRange.isEmpty && segmentRange.startOffset != editor.caretModel.offset) { if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) {
corrections.add(Action.MoveToOffset(segmentRange.startOffset)) corrections.add(Action.MoveToOffset(editor.caretModel.offset - 1))
} }
} }
} }
return corrections return corrections
} }
/** fun correctSelection(editor: Editor) {
* Correct the editor's selection to match the current Vim mode val corrections = injector.application.runReadAction { calculateCorrections(editor) }
*/ applyCorrections(corrections, editor)
fun correctEditorSelection(editor: Editor) {
injector.application.runReadAction {
val corrections = calculateCorrectionsToSyncEditorToMode(editor)
applyCorrections(corrections, editor)
}
} }
} }

View File

@@ -1,12 +1,4 @@
<!-- <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> <name>IdeaVim</name>
<id>IdeaVIM</id> <id>IdeaVIM</id>
<description><![CDATA[ <description><![CDATA[
@@ -21,7 +13,7 @@
<li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li> <li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li>
</ul> </ul>
]]></description> ]]></description>
<version>SNAPSHOT</version> <version>chylex</version>
<vendor>JetBrains</vendor> <vendor>JetBrains</vendor>
<!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) --> <!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) -->
@@ -246,6 +238,7 @@
</group> </group>
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/> <action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
</actions> </actions>
<extensions defaultExtensionNs="IdeaVIM"> <extensions defaultExtensionNs="IdeaVIM">

View File

@@ -72,6 +72,7 @@ nowrap
nowrapscan nowrapscan
ideacopypreprocess ideacopypreprocess
ideaglobalmode
ideajoin ideajoin
ideamarks ideamarks
idearefactormode idearefactormode

View File

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

View File

@@ -50,10 +50,4 @@ class DeletePreviousWordActionTest : VimExTestCase() {
typeText(":set keymodel=continueselect,stopselect<C-W>") typeText(":set keymodel=continueselect,stopselect<C-W>")
assertExText("set ") assertExText("set ")
} }
@Test
fun `test delete at beginning does nothing`() {
typeText(":<C-W>")
assertExText("")
}
} }

View File

@@ -721,17 +721,6 @@ class MapCommandTest : VimTestCase() {
assertState("zzz\n") assertState("zzz\n")
} }
// VIM-650 |mapleader|
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "Bad replace of term codes")
@Test
fun testMapLeaderToCtrlSpace() {
configureByText("\n")
enterCommand("let mapleader = \"\\<C-SPACE>\"")
enterCommand("nmap <Leader>z izzz<Esc>")
typeText("<C-SPACE>z")
assertState("zzz\n")
}
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "bad replace term codes") @TestWithoutNeovim(SkipNeovimReason.DIFFERENT, "bad replace term codes")
@Test @Test
fun testAmbiguousMapping() { fun testAmbiguousMapping() {

View File

@@ -103,22 +103,4 @@ class PrintLineNumberTest : VimTestCase() {
enterCommand("2=p#") enterCommand("2=p#")
assertStatusLineMessageContains("2 \t\t\tconsectetur adipiscing elit") assertStatusLineMessageContains("2 \t\t\tconsectetur adipiscing elit")
} }
@Test
fun `test out of bounds range is clamped to last line`() {
configureByLines(10, "Lorem ipsum dolor sit amet")
enterCommand("\$+100=")
assertStatusLineMessageContains("10")
}
@Test
fun `test out of bounds range with flags is clamped to last line`() {
configureByText("""
|Lorem ipsum dolor sit amet
|consectetur adipiscing elit
|Maecenas efficitur nec odio vel malesuada
""".trimMargin())
enterCommand("\$+100=p")
assertStatusLineMessageContains("3 Maecenas efficitur nec odio vel malesuada")
}
} }

View File

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

View File

@@ -144,15 +144,6 @@ class IndexedExpressionTest : VimTestCase("\n") {
assertPluginError(false) assertPluginError(false)
} }
@Test
fun `test indexed String expression at exactly length boundary returns empty String`() {
// This test validates the off-by-one fix in IndexedExpression.kt line 56
// With old code (idx > text.length), accessing index == length would cause IndexOutOfBoundsException
// With new code (idx >= text.length), it correctly returns empty string
assertCommandOutput("echo string('hello'[5])", "''")
assertPluginError(false)
}
@Test @Test
fun `test indexed String expression with negative index returns empty String`() { fun `test indexed String expression with negative index returns empty String`() {
// Surprisingly not the same as List // Surprisingly not the same as List

View File

@@ -30,51 +30,6 @@ class LenFunctionTest : VimTestCase() {
assertCommandOutput("echo len(12 . 4)", "3") assertCommandOutput("echo len(12 . 4)", "3")
} }
@Test
fun `test len with empty string`() {
assertCommandOutput("echo len('')", "0")
assertCommandOutput("echo len(\"\")", "0")
}
@Test
fun `test len with empty list`() {
assertCommandOutput("echo len([])", "0")
}
@Test
fun `test len with zero number`() {
assertCommandOutput("echo len(0)", "1")
}
@Test
fun `test len with negative numbers`() {
assertCommandOutput("echo len(-123)", "4")
assertCommandOutput("echo len(-1)", "2")
}
@Test
fun `test len with large number`() {
assertCommandOutput("echo len(9999999)", "7")
}
@Test
fun `test len with multi-element list`() {
assertCommandOutput("echo len([1, 2, 3, 4, 5])", "5")
}
@Test
fun `test len with multi-element dictionary`() {
assertCommandOutput("echo len(#{a: 1, b: 2, c: 3})", "3")
}
@Test
fun `test len with string containing special characters`() {
// Single-quoted strings in Vim don't interpret escape sequences (except '')
assertCommandOutput("echo len('hello\\nworld')", "12")
// Double-quoted strings do interpret escape sequences
assertCommandOutput("echo len(\"hello\\nworld\")", "11")
}
@Test @Test
fun `test len with float causes errors`() { fun `test len with float causes errors`() {
enterCommand("echo len(4.2)") enterCommand("echo len(4.2)")

View File

@@ -9,7 +9,6 @@
package org.jetbrains.plugins.ideavim.ex.implementation.functions.listFunctions package org.jetbrains.plugins.ideavim.ex.implementation.functions.listFunctions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import kotlin.test.assertTrue
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -60,18 +59,17 @@ class RangeFunctionTest : VimTestCase() {
@Test @Test
fun `test range with zero stride throws error`() { fun `test range with zero stride throws error`() {
enterCommand("echo range(1, 5, 0)") enterCommand("echo range(1, 5, 0)")
assertTrue(injector.messages.isError()) kotlin.test.assertTrue(injector.messages.isError())
} }
@Test @Test
fun `test range with start past end throws error`() { fun `test range with start past end throws error`() {
enterCommand("echo range(2, 0)") enterCommand("echo range(2, 0)")
assertTrue(injector.messages.isError()) kotlin.test.assertTrue(injector.messages.isError())
} }
@Test @Test
fun `test range negative with start past end throws error`() { fun `test range negative with start past end throws error`() {
enterCommand("echo range(-2, 0, -1)") enterCommand("echo range(-2, 0, -1)")
assertTrue(injector.messages.isError())
} }
} }

View File

@@ -65,6 +65,11 @@ class TrimFunctionTest : VimTestCase() {
assertCommandOutput("echo trim('\t\nhello\n\t')", "hello") assertCommandOutput("echo trim('\t\nhello\n\t')", "hello")
} }
@Test
fun `test trim with carriage return and vertical tab`() {
assertCommandOutput("echo trim('\r\t\t\r RESERVE \t\n\u000B\u00A0') .. '_TAIL'", "RESERVE_TAIL")
}
@Test @Test
fun `test trim with custom mask does not affect middle`() { fun `test trim with custom mask does not affect middle`() {
assertCommandOutput("echo trim('rm<Xrm<>X>rrm', 'rm<>')", "Xrm<>X") assertCommandOutput("echo trim('rm<Xrm<>X>rrm', 'rm<>')", "Xrm<>X")

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.state.mode.selectionType
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
import org.jetbrains.plugins.ideavim.assertModeDoesNotChange import org.jetbrains.plugins.ideavim.assertDoesntChange
import org.jetbrains.plugins.ideavim.rangeOf import org.jetbrains.plugins.ideavim.rangeOf
import org.jetbrains.plugins.ideavim.waitAndAssert import org.jetbrains.plugins.ideavim.waitAndAssert
import org.jetbrains.plugins.ideavim.waitAndAssertMode import org.jetbrains.plugins.ideavim.waitAndAssertMode
@@ -89,7 +89,7 @@ class NonVimVisualChangeTest : VimTestCase() {
fixture.editor.selectionModel.removeSelection() fixture.editor.selectionModel.removeSelection()
} }
assertModeDoesNotChange(fixture.editor, Mode.INSERT) assertDoesntChange { fixture.editor.vim.mode == Mode.INSERT }
} }
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)

View File

@@ -10,7 +10,6 @@ package org.jetbrains.plugins.ideavim
import com.intellij.ide.IdeEventQueue import com.intellij.ide.IdeEventQueue
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.util.containers.toArray import com.intellij.util.containers.toArray
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
@@ -18,8 +17,6 @@ import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
/** /**
@@ -59,9 +56,17 @@ annotation class VimBehaviorDiffers(
val shouldBeFixed: Boolean = true, val shouldBeFixed: Boolean = true,
) )
// The selection is updated after 'visualdelay' milliseconds. Add an adjustment when we wait for it to be completed. inline fun waitAndAssert(timeInMillis: Int = 1000, crossinline condition: () -> Boolean) {
// Since we wait for it on the main thread (to avoid reading in-progress state), we can get away with a short adjustment ApplicationManager.getApplication().invokeAndWait {
private const val visualDelayAdjustment = 200 val end = System.currentTimeMillis() + timeInMillis
while (end > System.currentTimeMillis()) {
Thread.sleep(10)
IdeEventQueue.getInstance().flushQueue()
if (condition()) return@invokeAndWait
}
fail()
}
}
fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) { fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) {
assertDoesntChange(timeInMillis - precision) { !condition() } assertDoesntChange(timeInMillis - precision) { !condition() }
@@ -106,58 +111,15 @@ fun <T> product(vararg elements: List<T>): List<List<T>> {
return res return res
} }
private inline fun invokeAndWaitUntil(timeout: Int = 1_000, crossinline condition: () -> Boolean): Boolean {
// Run the check on the main thread to serialise access for our condition
var result = false
ApplicationManager.getApplication().invokeAndWait {
val end = System.currentTimeMillis() + timeout
while (end > System.currentTimeMillis()) {
if (condition()) {
result = true
return@invokeAndWait
}
Thread.sleep(10)
IdeEventQueue.getInstance().flushQueue()
}
}
return result
}
fun waitAndAssert(timeInMillis: Int = 1000, condition: () -> Boolean) {
assertTrue(invokeAndWaitUntil(timeInMillis, condition), "Condition not met within timeout")
}
fun waitAndAssertMode( fun waitAndAssertMode(
fixture: CodeInsightTestFixture, fixture: CodeInsightTestFixture,
mode: Mode, mode: Mode,
timeInMillis: Int? = null, timeInMillis: Int? = null,
) { ) {
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment) val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + 1000)
val currentMode = fixture.editor.vim.mode waitAndAssert(timeout) { fixture.editor.vim.mode == mode }
waitAndAssert(timeout) {
if (fixture.editor.vim.mode == currentMode && fixture.editor.vim.mode != mode) return@waitAndAssert false
assertEquals(mode, fixture.editor.vim.mode)
return@waitAndAssert true
}
} }
fun assertModeDoesNotChange(editor: Editor, expectedMode: Mode, timeInMillis: Int? = null) {
val timeout = timeInMillis ?: (injector.globalIjOptions().visualdelay + visualDelayAdjustment)
val currentMode = editor.vim.mode
assertEquals(expectedMode, currentMode, "Initial mode is not as expected")
invokeAndWaitUntil(timeout) { editor.vim.mode != currentMode }
assertEquals(currentMode, editor.vim.mode, "Mode should not change")
}
// Note that the selection might not update, but we wait long enough to give it chance to change
fun waitUntilSelectionUpdated(editor: Editor) {
val timeout = injector.globalIjOptions().visualdelay + visualDelayAdjustment
val currentMode = editor.vim.mode
invokeAndWaitUntil(timeout) { editor.vim.mode != currentMode }
}
// This waits on the current thread, which in tests isn't the main thread. If we're waiting on a callback on the main
// thread, we might see in-progress changes rather than the final state.
fun waitUntil(timeout: Int = 10_000, condition: () -> Boolean): Boolean { fun waitUntil(timeout: Int = 10_000, condition: () -> Boolean): Boolean {
val timeEnd = System.currentTimeMillis() + timeout val timeEnd = System.currentTimeMillis() + timeout
while (System.currentTimeMillis() < timeEnd) { while (System.currentTimeMillis() < timeEnd) {

View File

@@ -139,25 +139,19 @@ and some text after""",
ApplicationManager.getApplication().invokeAndWait { ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().runWriteAction { ApplicationManager.getApplication().runWriteAction {
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor) CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0) assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, true)
?: error("Expected fold region at line 0 for Javadoc comment")
assertEquals(foldRegion.isExpanded, true)
} }
} }
typeText(injector.parser.parseKeys("za")) typeText(injector.parser.parseKeys("za"))
ApplicationManager.getApplication().invokeAndWait { ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().runWriteAction { ApplicationManager.getApplication().runWriteAction {
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0) assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, false)
?: error("Expected fold region at line 0 for Javadoc comment")
assertEquals(foldRegion.isExpanded, false)
} }
} }
typeText(injector.parser.parseKeys("za")) typeText(injector.parser.parseKeys("za"))
ApplicationManager.getApplication().invokeAndWait { ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().runWriteAction { ApplicationManager.getApplication().runWriteAction {
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0) assertEquals(FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded, true)
?: error("Expected fold region at line 0 for Javadoc comment")
assertEquals(foldRegion.isExpanded, true)
} }
} }
} }
@@ -180,9 +174,7 @@ and some text after""",
ApplicationManager.getApplication().invokeAndWait { ApplicationManager.getApplication().invokeAndWait {
fixture.editor.foldingModel.runBatchFoldingOperation { fixture.editor.foldingModel.runBatchFoldingOperation {
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor) CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0) FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded = false
?: error("Expected fold region at line 0 for Javadoc comment")
foldRegion.isExpanded = false
} }
} }

View File

@@ -34,9 +34,7 @@ bar
ApplicationManager.getApplication().invokeAndWait { ApplicationManager.getApplication().invokeAndWait {
fixture.editor.foldingModel.runBatchFoldingOperation { fixture.editor.foldingModel.runBatchFoldingOperation {
CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor) CodeFoldingManager.getInstance(fixture.project).updateFoldRegions(fixture.editor)
val foldRegion = FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0) FoldingUtil.findFoldRegionStartingAtLine(fixture.editor, 0)!!.isExpanded = false
?: error("Expected fold region at line 0 for Javadoc comment")
foldRegion.isExpanded = false
} }
} }

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
/** /**
* @author vlan * @author vlan

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
@CommandOrMotion(keys = ["s"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["s"], modes = [Mode.NORMAL])
class ChangeCharactersAction : ChangeInInsertSequenceAction() { class ChangeCharactersAction : ChangeInInsertSequenceAction() {

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
@CommandOrMotion(keys = ["C"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["C"], modes = [Mode.NORMAL])
class ChangeEndOfLineAction : ChangeInInsertSequenceAction() { class ChangeEndOfLineAction : ChangeInInsertSequenceAction() {

View File

@@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
@CommandOrMotion(keys = ["S"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["S"], modes = [Mode.NORMAL])
class ChangeLineAction : ChangeInInsertSequenceAction() { class ChangeLineAction : ChangeInInsertSequenceAction() {

View File

@@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.command.CommandFlags.FLAG_NO_REPEAT_INSERT
import com.maddyhome.idea.vim.command.DuplicableOperatorAction import com.maddyhome.idea.vim.command.DuplicableOperatorAction
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
@CommandOrMotion(keys = ["c"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["c"], modes = [Mode.NORMAL])
class ChangeMotionAction : ChangeInInsertSequenceAction(), DuplicableOperatorAction { class ChangeMotionAction : ChangeInInsertSequenceAction(), DuplicableOperatorAction {

View File

@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import java.util.EnumSet import java.util.*
/** /**
* @author vlan * @author vlan

View File

@@ -24,7 +24,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import java.util.EnumSet import java.util.*
/** /**
* @author vlan * @author vlan

View File

@@ -21,7 +21,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.endOffsetInclusive import com.maddyhome.idea.vim.helper.endOffsetInclusive
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
@CommandOrMotion(keys = ["!"], modes = [Mode.VISUAL]) @CommandOrMotion(keys = ["!"], modes = [Mode.VISUAL])
class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterCommand { class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterCommand {

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.EnumSet import java.util.*
/** /**
* @author vlan * @author vlan

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2024 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at

View File

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

View File

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

View File

@@ -76,6 +76,13 @@ sealed class TillCharacterMotion(
) )
} }
injector.motion.setLastFTCmd(tillCharacterMotionType, argument.character) 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() return res.toMotionOrError()
} }
} }

View File

@@ -31,6 +31,12 @@ class MotionCamelLeftAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): 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) return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error ?.toMotionOrError() ?: Motion.Error
} }
@@ -47,6 +53,10 @@ class MotionCamelRightAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): 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) return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error ?.toMotionOrError() ?: Motion.Error
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -179,31 +179,41 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return false return false
} }
} }
val mode = editor.mode
val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE if (type == null ||
val shouldYank = type != null && !isInsertMode && saveToRegister (mode == Mode.INSERT || mode == Mode.REPLACE) ||
if (shouldYank && !caret.registerStorage.storeText(editor, context, updatedRange, type, isDelete = true)) { !saveToRegister ||
return false injector.registerGroup.storeText(
}
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
editor, editor,
LineDeleteShift.NL_ON_END context,
) ?: continue caret,
injector.application.runWriteAction { updatedRange,
type,
true,
!editor.isFirstCaret,
editor.isReversingCarets
)
) {
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
editor.deleteString(TextRange(newRange.first, newRange.second)) editor.deleteString(TextRange(newRange.first, newRange.second))
} }
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
} }
if (type != null) { return false
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
} }
/** /**
@@ -213,7 +223,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param caret The caret to start insertion in * @param caret The caret to start insertion in
* @param str The text to insert * @param str The text to insert
*/ */
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret { override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret {
injector.application.runWriteAction { injector.application.runWriteAction {
(editor as MutableVimEditor).insertText(caret, offset, str) (editor as MutableVimEditor).insertText(caret, offset, str)
} }
@@ -442,11 +452,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
*/ */
override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) { override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) {
val state = injector.vimState val state = injector.vimState
injector.application.runReadAction { for (caret in editor.nativeCarets()) {
for (caret in editor.nativeCarets()) { caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset)
caret.vimInsertStart = editor.createLiveMarker(caret.offset, caret.offset) injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
injector.markService.setMark(caret, MARK_CHANGE_START, caret.offset)
}
} }
val cmd = state.executingCommand val cmd = state.executingCommand
if (cmd != null && state.isDotRepeatInProgress) { if (cmd != null && state.isDotRepeatInProgress) {
@@ -472,9 +480,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val myChangeListener = VimChangesListener() val myChangeListener = VimChangesListener()
vimDocumentListener = myChangeListener vimDocumentListener = myChangeListener
vimDocument!!.addChangeListener(myChangeListener) vimDocument!!.addChangeListener(myChangeListener)
injector.application.runReadAction { oldOffset = editor.currentCaret().offset
oldOffset = editor.currentCaret().offset
}
editor.insertMode = mode == Mode.INSERT editor.insertMode = mode == Mode.INSERT
editor.mode = mode editor.mode = mode
} }

View File

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

View File

@@ -111,7 +111,8 @@ interface VimEditor {
* This method should perform caret merging after the operations. This is similar to IJ runForEachCaret * This method should perform caret merging after the operations. This is similar to IJ runForEachCaret
* TODO review * TODO review
*/ */
val isFirstCaret: Boolean
val isReversingCarets: Boolean
fun forEachCaret(action: (VimCaret) -> Unit) fun forEachCaret(action: (VimCaret) -> Unit)
fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false) fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false)
fun isInForEachCaretScope(): Boolean fun isInForEachCaretScope(): Boolean
@@ -162,6 +163,7 @@ interface VimEditor {
return getText(start, end) return getText(start, end)
} }
fun getSelectionModel(): VimSelectionModel
fun getScrollingModel(): VimScrollingModel fun getScrollingModel(): VimScrollingModel
fun removeCaret(caret: VimCaret) fun removeCaret(caret: VimCaret)
@@ -210,6 +212,7 @@ interface VimEditor {
fun createIndentBySize(size: Int): String fun createIndentBySize(size: Int): String
fun getFoldRegionAtOffset(offset: Int): VimFoldRegion? 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 * Mostly related to Fleet. After the editor is modified, the carets are modified. You can't use the old caret

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
/*
* 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
}

View File

@@ -27,30 +27,26 @@ abstract class VimStringParserBase : VimStringParser {
override fun toPrintableString(keys: List<KeyStroke>): String { override fun toPrintableString(keys: List<KeyStroke>): String {
val builder = StringBuilder() val builder = StringBuilder()
for (key in keys) { for (key in keys) {
val keyAsString = keyStrokeToString(key) val keyAsChar = keyStrokeToChar(key)
builder.append(keyAsString) builder.append(keyAsChar)
} }
return builder.toString() return builder.toString()
} }
private fun keyStrokeToString(key: KeyStroke): String { private fun keyStrokeToChar(key: KeyStroke): Char {
if (key.keyChar != KeyEvent.CHAR_UNDEFINED) { if (key.keyChar != KeyEvent.CHAR_UNDEFINED) {
return key.keyChar.toString() return key.keyChar
} else if (key.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) { } else if (key.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
return if (isControlCharacterKeyCode(key.keyCode)) { return if (key.keyCode == 'J'.code) {
if (key.keyCode == 'J'.code) { // 'J' is a special case, keycode 10 is \n char
// 'J' is a special case, keycode 10 is \n char 0.toChar()
0.toChar().toString()
} else {
(key.keyCode - 'A'.code + 1).toChar().toString()
}
} else { } else {
"^" + key.keyCode.toChar() (key.keyCode - 'A'.code + 1).toChar()
} }
} else if (key.keyChar == KeyEvent.CHAR_UNDEFINED && key.keyCode == KeyEvent.VK_ENTER) { } else if (key.keyChar == KeyEvent.CHAR_UNDEFINED && key.keyCode == KeyEvent.VK_ENTER) {
return "\u000D" return '\u000D'
} }
return key.keyCode.toChar().toString() return key.keyCode.toChar()
} }
override fun toKeyNotation(keyStrokes: List<KeyStroke>): String { override fun toKeyNotation(keyStrokes: List<KeyStroke>): String {
@@ -182,18 +178,7 @@ abstract class VimStringParserBase : VimStringParser {
private fun getMapLeader(): List<KeyStroke> { private fun getMapLeader(): List<KeyStroke> {
val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader") val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader")
return if (mapLeader is VimString) { return if (mapLeader is VimString) {
val v: String = mapLeader.value stringToKeys(mapLeader.value)
// Minimum length is 4 for the shortest special key format: \<X> (e.g., "\<a>")
if (v.startsWith("\\<") && v.length >= 4 && v[v.length - 1] == '>') {
val specialKey = parseSpecialKey(v.substring(2, v.length - 1), 0)
if (specialKey != null) {
listOf(specialKey)
} else {
stringToKeys(mapLeader.value)
}
} else {
stringToKeys(mapLeader.value)
}
} else { } else {
stringToKeys("\\") stringToKeys("\\")
} }
@@ -222,11 +207,6 @@ abstract class VimStringParserBase : VimStringParser {
return c < '\u0020' return c < '\u0020'
} }
private fun isControlCharacterKeyCode(code: Int): Boolean {
// Ctrl-(A..Z [\]^_) are ASCII control characters
return code >= 'A'.code && code <= '_'.code
}
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
private fun getVimKeyValue(c: Int): @NonNls String? { private fun getVimKeyValue(c: Int): @NonNls String? {
return when (c) { return when (c) {
@@ -450,25 +430,17 @@ abstract class VimStringParserBase : VimStringParser {
val specialKey = parseSpecialKey(specialKeyBuilder.toString(), 0) val specialKey = parseSpecialKey(specialKeyBuilder.toString(), 0)
if (specialKey != null) { if (specialKey != null) {
var keyCode = specialKey.keyCode var keyCode = specialKey.keyCode
var useKeyCode = true
if (specialKey.keyCode == 0) { if (specialKey.keyCode == 0) {
keyCode = specialKey.keyChar.code keyCode = specialKey.keyChar.code
} else if (specialKey.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) { } else if (specialKey.modifiers and InputEvent.CTRL_DOWN_MASK == InputEvent.CTRL_DOWN_MASK) {
if (isControlCharacterKeyCode(specialKey.keyCode)) { keyCode = if (specialKey.keyCode == 'J'.code) {
keyCode = if (specialKey.keyCode == 'J'.code) { // 'J' is a special case, keycode 10 is \n char
// 'J' is a special case, keycode 10 is \n char 0
0
} else {
specialKey.keyCode - 'A'.code + 1
}
} else { } else {
useKeyCode = false specialKey.keyCode - 'A'.code + 1
result.append("\\<${specialKeyBuilder}>")
} }
} }
if (useKeyCode) { result.append(keyCode.toChar())
result.append(keyCode.toChar())
}
} else { } else {
result.append("<").append(specialKeyBuilder).append(">") result.append("<").append(specialKeyBuilder).append(">")
} }

View File

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

View File

@@ -17,10 +17,8 @@ class VimEditorReplaceMask {
fun recordChangeAtCaret(editor: VimEditor) { fun recordChangeAtCaret(editor: VimEditor) {
for (caret in editor.carets()) { for (caret in editor.carets()) {
val offset = caret.offset val offset = caret.offset
if (offset < editor.fileSize()) { val marker = editor.createLiveMarker(offset, offset)
val marker = editor.createLiveMarker(offset, offset) changedChars[marker] = editor.charAt(offset)
changedChars[marker] = editor.charAt(offset)
}
} }
} }

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretListener import com.maddyhome.idea.vim.api.VimCaretListener
import com.maddyhome.idea.vim.api.VimEditor 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.injector
import com.maddyhome.idea.vim.api.normalizeOffset import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
@@ -226,7 +227,15 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret") StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret")
} }
val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset) val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset).let {
if (offset.intendedColumn == VimMotionGroupBase.LAST_COLUMN) {
val softWrapStart = editor.getSoftWrapStartAtOffset(it)
if (softWrapStart != null) softWrapStart - 1 else it
}
else {
it
}
}
StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action") 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 // 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