1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-08-17 16:31:45 +02:00

Compare commits

...

65 Commits

Author SHA1 Message Date
1acf5d682d Set plugin version to chylex-51 2025-07-20 10:11:27 +02:00
f2832f7dc2 Preserve visual mode after executing IDE action 2025-07-20 10:11:27 +02:00
fca7b518b3 Make g0/g^/g$ work with soft wraps 2025-07-19 19:47:02 +02:00
8aa4178449 Make gj/gk jump over soft wraps 2025-07-19 19:47:02 +02:00
2e035f1260 Make camelCase motions adjust based on direction of visual selection 2025-07-19 19:47:02 +02:00
c5f17a68f5 Make search highlights temporary 2025-07-19 19:47:02 +02:00
68b7788fe4 Exit insert mode after refactoring 2025-07-19 19:47:02 +02:00
3299059ab9 Add action to run last macro in all opened files 2025-07-19 19:47:02 +02:00
548ed30b5b Stop macro execution after a failed search 2025-07-19 19:47:02 +02:00
b171ccb96c Revert per-caret registers 2025-07-19 19:47:02 +02:00
93affef6d3 Apply scrolloff after executing native IDEA actions 2025-07-19 19:47:02 +02:00
0b788153cd Stay on same line after reindenting 2025-07-19 19:47:02 +02:00
70d8167e17 Update search register when using f/t 2025-07-19 19:47:02 +02:00
67c3dec51b Automatically add unambiguous imports after running a macro 2025-07-19 19:47:02 +02:00
3dab706f37 Fix(VIM-3986): Exception when pasting register contents containing new line 2025-07-19 19:47:02 +02:00
5228bca65e Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2025-07-19 19:46:34 +02:00
853a208eba Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2025-07-19 19:46:34 +02:00
fd9297edb1 Add support for count for visual and line motion surround 2025-07-19 19:46:34 +02:00
e01e4d8ecd Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2025-07-19 19:46:34 +02:00
b304692c4e Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2025-07-19 19:46:33 +02:00
05b9f44a0b Respect count with <Action> mappings 2025-07-19 15:32:32 +02:00
d878b119c4 Change matchit plugin to use HTML patterns in unrecognized files 2025-07-19 15:32:32 +02:00
def57128b0 Reset insert mode when switching active editor 2025-07-19 15:32:32 +02:00
fd4422bf95 Remove notifications about configuration options 2025-07-19 15:32:32 +02:00
d10a0c4ee7 Set custom plugin version 2025-07-19 15:32:30 +02:00
Alex Plate
f12b0b04f6 Fix the contribution name of Jakub 2025-07-18 16:06:45 +03:00
Alex Plate
ea4fc85e5b Add a new plugin for the verification 2025-07-18 16:04:00 +03:00
Alex Plate
4af8fc1868 Update TC configuration to run only needed tests 2025-07-18 16:04:00 +03:00
Alex Plate
1482ac0335 Fix(VIM-3970): Get rid of VimStandalonePluginUpdateChecker 2025-07-18 16:03:59 +03:00
IdeaVim Bot
79168b00f3 Add zuberol to contributors list 2025-07-18 09:03:36 +00:00
zuberol
07990847c6 Merge pull request #1223 from JetBrains/feat/VIM-3791-nerdtree-gg-G-jumps
Feat(VIM-3791): support for "G" and "gg" motions inside the NERDtree
2025-07-17 11:44:07 +02:00
dependabot[bot]
8c40e19c44 Bump org.mockito.kotlin:mockito-kotlin from 5.4.0 to 6.0.0
Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 5.4.0 to 6.0.0.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/5.4.0...v6.0.0)

---
updated-dependencies:
- dependency-name: org.mockito.kotlin:mockito-kotlin
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-16 19:15:13 +03:00
dependabot[bot]
371769c508 Bump io.ktor:ktor-client-cio from 3.2.1 to 3.2.2
Bumps [io.ktor:ktor-client-cio](https://github.com/ktorio/ktor) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-cio
  dependency-version: 3.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-16 19:14:55 +03:00
dependabot[bot]
7ee34d0b27 Bump io.ktor:ktor-client-cio from 3.2.0 to 3.2.1
Bumps [io.ktor:ktor-client-cio](https://github.com/ktorio/ktor) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.2.0...3.2.1)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-cio
  dependency-version: 3.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-09 18:57:15 +03:00
dependabot[bot]
d1ec7d617d Bump org.junit.jupiter:junit-jupiter-engine from 5.13.2 to 5.13.3
Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit-framework) from 5.13.2 to 5.13.3.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.2...r5.13.3)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-version: 5.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-09 18:56:55 +03:00
dependabot[bot]
898fd0537d Bump org.junit.jupiter:junit-jupiter from 5.13.2 to 5.13.3
Bumps [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit-framework) from 5.13.2 to 5.13.3.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.2...r5.13.3)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter
  dependency-version: 5.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-09 18:56:22 +03:00
Xinhe Wang
353603b546 Fix(VIM-3266): Set FileSaveCloseAction to OTHER_SELF_SYNCHRONIZED 2025-07-07 10:55:53 +03:00
Xinhe Wang
2f7f0dcacb Fix(VIM-3044): correct the behavior of I in (linewise) Visual mode
For visual selections spanning multiple lines, keep caret position
if it's on the first line. Otherwise move the caret to the start of
the first selected line.
2025-07-07 10:55:30 +03:00
Xinhe Wang
af9023af4b Rename insertBeforeCursor and insertAfterCursor helpers
The terminology used in IntelliJ is "caret".
2025-07-07 10:55:30 +03:00
Xinhe Wang
c393c902b2 Fix(VIM-2375): do not save file with ZQ
ZQ is defined to `Quit without checking for changes (same as ":q!").`
2025-07-04 14:26:56 +03:00
Xinhe Wang
c355cb7ed7 Make VimChangeGroup::changeCaseMotion not accept non-Motion argument
Error is logged if `ChangeCaseMotion` actions receive non-`Motion` args
2025-07-03 17:25:38 +03:00
Xinhe Wang
0803a1c195 Fix(VIM-2413): correct the range of line-wise case change commands
The start of the range is the leftmost non-whitespace character
OR the current position, whichever is closer to the left.
2025-07-03 17:25:38 +03:00
Xinhe Wang
5208412b46 Refine Argument.Motion::isLineWiseMotion and VimMotionGroupBase::getMotionRange
This does not change any actual behavior.
2025-07-03 17:25:38 +03:00
IdeaVim Bot
78c463cf7b Add Xinhe Wang to contributors list 2025-07-03 09:02:01 +00:00
dependabot[bot]
8f5a44bf44 Bump org.junit.vintage:junit-vintage-engine from 5.13.1 to 5.13.2
Bumps [org.junit.vintage:junit-vintage-engine](https://github.com/junit-team/junit-framework) from 5.13.1 to 5.13.2.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

---
updated-dependencies:
- dependency-name: org.junit.vintage:junit-vintage-engine
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 19:19:55 +03:00
Xinhe Wang
2377408028 Fix(VIM-2142): support gU and gu in VISUAL mode
Note that this implementation assumes that the 'gU' / 'gu' command in
visual mode is equivalent to 'U' / 'u'. While 'v_gU' and 'v_gu' are not
explicitly documented in Vim help, we treat these commands as identical
based on observed behavior, without examining Vim's source code.
2025-07-02 18:56:29 +03:00
Alex Plate
246425b1fb Mark IdeaVim as a plugin that supports vim configuration
In this way, we'll be able to actively promote IdeaVim as a suggested plugin in the IDE.

GO-17806
2025-06-27 19:43:08 +03:00
Jakub
4eadfc1fba feat: support for "G" and "gg" motions inside the NERDtree 2025-06-27 13:51:44 +02:00
dependabot[bot]
d3c945cd6d Bump org.junit.jupiter:junit-jupiter-api from 5.13.1 to 5.13.2
Bumps [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit-framework) from 5.13.1 to 5.13.2.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-25 19:35:31 +03:00
dependabot[bot]
2ac46129ac Bump org.junit.jupiter:junit-jupiter-params from 5.13.1 to 5.13.2
Bumps [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit-framework) from 5.13.1 to 5.13.2.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-25 19:14:44 +03:00
dependabot[bot]
c8d40be1ce Bump org.junit.jupiter:junit-jupiter from 5.13.1 to 5.13.2
Bumps [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit-framework) from 5.13.1 to 5.13.2.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-25 19:14:31 +03:00
dependabot[bot]
97159a33fe Bump org.jetbrains.kotlin:kotlin-stdlib from 2.1.21 to 2.2.0
Bumps [org.jetbrains.kotlin:kotlin-stdlib](https://github.com/JetBrains/kotlin) from 2.1.21 to 2.2.0.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.1.21...v2.2.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin:kotlin-stdlib
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-25 19:13:44 +03:00
Alex Plate
861d585102 Also, specify new task names in build scripts 2025-06-25 18:43:01 +03:00
Alex Plate
126925b4eb Fix long running and property tests
Because of some changes, if we define the test using `testIde` registering, they're not executed properly and don't work.
Now, we don't exclude these tests from the main test execution, so they have to be excluded explicitly
2025-06-25 18:33:50 +03:00
Alex Plate
9302c0a057 Use intellij.spellchecker module for 2025.2+ builds of IJ 2025-06-25 17:35:46 +03:00
Alex Plate
ddea72f803 Update kotlin version to 2.2.0
This is required to support IJ 2025.2 platform, which uses 2.2.0 for compilation.
2025-06-25 17:23:54 +03:00
Alex Plate
e991aa922c Try to increase timeout for UI tests for Rider 2025-06-24 17:24:56 +03:00
dependabot[bot]
5ffaa7b084 Bump org.junit.jupiter:junit-jupiter-api from 5.13.0 to 5.13.1
Bumps [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit5) from 5.13.0 to 5.13.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-18 19:10:53 +03:00
dependabot[bot]
0d4183129d Bump org.eclipse.jgit:org.eclipse.jgit.ssh.apache
Bumps [org.eclipse.jgit:org.eclipse.jgit.ssh.apache](https://github.com/eclipse-jgit/jgit) from 7.2.1.202505142326-r to 7.3.0.202506031305-r.
- [Commits](https://github.com/eclipse-jgit/jgit/compare/v7.2.1.202505142326-r...v7.3.0.202506031305-r)

---
updated-dependencies:
- dependency-name: org.eclipse.jgit:org.eclipse.jgit.ssh.apache
  dependency-version: 7.3.0.202506031305-r
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-18 19:10:13 +03:00
dependabot[bot]
8a7fbac389 Bump org.junit.jupiter:junit-jupiter-params from 5.13.0 to 5.13.1
Bumps [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit5) from 5.13.0 to 5.13.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-18 18:53:18 +03:00
dependabot[bot]
dbab006f83 Bump io.ktor:ktor-client-content-negotiation from 3.1.3 to 3.2.0
Bumps [io.ktor:ktor-client-content-negotiation](https://github.com/ktorio/ktor) from 3.1.3 to 3.2.0.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.3...3.2.0)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-content-negotiation
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-18 18:52:47 +03:00
dependabot[bot]
3149de7b73 Bump org.junit.vintage:junit-vintage-engine from 5.13.0 to 5.13.1
Bumps [org.junit.vintage:junit-vintage-engine](https://github.com/junit-team/junit5) from 5.13.0 to 5.13.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit.vintage:junit-vintage-engine
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 18:30:27 +03:00
dependabot[bot]
28a71f0e09 Bump com.google.devtools.ksp:symbol-processing-api
Bumps [com.google.devtools.ksp:symbol-processing-api](https://github.com/google/ksp) from 2.1.21-2.0.1 to 2.1.21-2.0.2.
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/2.1.21-2.0.1...2.1.21-2.0.2)

---
updated-dependencies:
- dependency-name: com.google.devtools.ksp:symbol-processing-api
  dependency-version: 2.1.21-2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 18:30:20 +03:00
dependabot[bot]
fc7d4e614b Bump org.junit.jupiter:junit-jupiter from 5.13.0 to 5.13.1
Bumps [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit5) from 5.13.0 to 5.13.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 18:30:12 +03:00
dependabot[bot]
5b1aade876 Bump org.junit.jupiter:junit-jupiter-engine from 5.13.0 to 5.13.1
Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.13.0 to 5.13.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 18:29:52 +03:00
116 changed files with 1365 additions and 928 deletions

1
.gitattributes vendored Normal file
View File

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

View File

@@ -5,7 +5,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="scriptParameters" value="-x :tests:property-tests:test -x :tests:long-running-tests:test" />
<option name="taskDescriptions">
<list />
</option>
@@ -19,6 +19,7 @@
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -45,6 +45,7 @@ object Compatibility : IdeaVimBuildType({
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.functiontextobj' [latest-IU] -team-city
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.miksuki.HighlightCursor' [latest-IU] -team-city
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.ugarosa.idea.edgemotion' [latest-IU] -team-city
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}cn.mumukehao.plugin' [latest-IU] -team-city
""".trimIndent()
}
}

View File

@@ -25,7 +25,7 @@ object LongRunning : IdeaVimBuildType({
steps {
gradle {
tasks = "clean :tests:long-running-tests:testLongRunning"
tasks = "clean :tests:long-running-tests:test"
buildFile = ""
enableStacktrace = true
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"

View File

@@ -39,7 +39,7 @@ object Nvim : IdeaVimBuildType({
""".trimIndent()
}
gradle {
tasks = "clean test -Dnvim"
tasks = "clean test -x :tests:property-tests:test -x :tests:long-running-tests:test -Dnvim"
buildFile = ""
enableStacktrace = true
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"

View File

@@ -25,7 +25,7 @@ object PropertyBased : IdeaVimBuildType({
steps {
gradle {
clearConditions()
tasks = "clean :tests:property-tests:testPropertyBased"
tasks = "clean :tests:property-tests:test"
buildFile = ""
enableStacktrace = true
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"

View File

@@ -115,7 +115,7 @@ sealed class ReleasePlugin(private val releaseType: String) : IdeaVimBuildType({
}
script {
name = "Run tests"
scriptContent = "./gradlew test"
scriptContent = "./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test"
}
gradle {
name = "Publish release"

View File

@@ -40,7 +40,7 @@ open class TestingBuildType(
steps {
gradle {
clearConditions()
tasks = "clean test"
tasks = "clean test -x :tests:property-tests:test -x :tests:long-running-tests:test"
buildFile = ""
enableStacktrace = true
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"

View File

@@ -15,7 +15,7 @@ object GitHub : Project({
name = "Pull Requests checks"
description = "Automatic checking of GitHub Pull Requests"
buildType(GithubBuildType("clean test", "Tests"))
buildType(GithubBuildType("clean test -x :tests:property-tests:test -x :tests:long-running-tests:test", "Tests"))
})
class GithubBuildType(command: String, desc: String) : IdeaVimBuildType({

View File

@@ -614,6 +614,14 @@ Contributors:
[![icon][github]](https://github.com/Malandril)
&nbsp;
Thomas Canava
* [![icon][mail]](mailto:xinhe.wang@jetbrains.com)
[![icon][github]](https://github.com/wxh06)
&nbsp;
Xinhe Wang
* [![icon][mail]](mailto:zuber.kuba@gmail.com)
[![icon][github]](https://github.com/zuberol)
&nbsp;
Jakub Zuber
Previous contributors:

View File

@@ -65,7 +65,7 @@ We've prepared some useful configurations for you:
And here are useful gradle commands:
* `./gradlew runIde` — start the dev version of IntelliJ IDEA with IdeaVim installed.
* `./gradlew test` — run tests.
* `./gradlew test -x :tests:property-tests:test -x :tests:long-running-tests:test` — run tests.
* `./gradlew buildPlugin` — build the plugin. The result will be located in `build/distributions`. This file can be
installed by using `Settings | Plugin | >Gear Icon< | Install Plugin from Disk...`. You can stay with your personal build
for a few days or send it to a friend for testing.

View File

@@ -8,7 +8,7 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization") version "2.0.21"
kotlin("plugin.serialization") version "2.2.0"
}
val kotlinxSerializationVersion: String by project
@@ -21,7 +21,7 @@ repositories {
}
dependencies {
compileOnly("com.google.devtools.ksp:symbol-processing-api:2.1.21-2.0.1")
compileOnly("com.google.devtools.ksp:symbol-processing-api:2.1.21-2.0.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion") {
// kotlin stdlib is provided by IJ, so there is no need to include it into the distribution
exclude("org.jetbrains.kotlin", "kotlin-stdlib")

View File

@@ -35,6 +35,8 @@ import org.intellij.markdown.ast.impl.ListCompositeNode
import org.jetbrains.changelog.Changelog
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.kohsuke.github.GHUser
import java.net.HttpURLConnection
import java.net.URL
@@ -46,19 +48,19 @@ buildscript {
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0")
classpath("com.github.AlexPl292:mark-down-to-slack:1.1.2")
classpath("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
// This is needed for jgit to connect to ssh
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.2.1.202505142326-r")
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.3.0.202506031305-r")
classpath("org.kohsuke:github-api:1.305")
classpath("io.ktor:ktor-client-core:3.1.3")
classpath("io.ktor:ktor-client-cio:3.1.3")
classpath("io.ktor:ktor-client-auth:3.1.3")
classpath("io.ktor:ktor-client-content-negotiation:3.1.3")
classpath("io.ktor:ktor-serialization-kotlinx-json:3.1.3")
classpath("io.ktor:ktor-client-core:3.2.2")
classpath("io.ktor:ktor-client-cio:3.2.2")
classpath("io.ktor:ktor-client-auth:3.2.2")
classpath("io.ktor:ktor-client-content-negotiation:3.2.2")
classpath("io.ktor:ktor-serialization-kotlinx-json:3.2.2")
// This comes from the changelog plugin
// classpath("org.jetbrains:markdown:0.3.1")
@@ -67,7 +69,7 @@ buildscript {
plugins {
java
kotlin("jvm") version "2.0.21"
kotlin("jvm") version "2.2.0"
application
id("java-test-fixtures")
@@ -79,7 +81,7 @@ plugins {
id("org.jetbrains.changelog") version "2.2.1"
id("org.jetbrains.kotlinx.kover") version "0.6.1"
id("com.dorongold.task-tree") version "4.0.1"
id("com.google.devtools.ksp") version "2.0.21-1.0.25"
id("com.google.devtools.ksp") version "2.2.0-2.0.2"
}
val moduleSources by configurations.registering
@@ -137,8 +139,14 @@ dependencies {
// AceJump is an optional dependency. We use their SessionManager class to check if it's active
plugin("AceJump", "3.8.19")
plugin("com.intellij.classic.ui", "251.23774.318")
bundledPlugins("org.jetbrains.plugins.terminal")
// VERSION UPDATE: This module is required since 2025.2
if (ideaVersion == "LATEST-EAP-SNAPSHOT") {
bundledModule("intellij.spellchecker")
}
}
moduleSources(project(":vim-engine", "sourcesJarArtifacts"))
@@ -158,19 +166,19 @@ dependencies {
testFixturesImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
// https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:6.0.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.13.0")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.13.0")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api:5.13.0")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-engine:5.13.0")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-params:5.13.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.3")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.13.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.13.3")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api:5.13.3")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-engine:5.13.3")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-params:5.13.3")
// Temp workaround suggested in https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#junit5-test-framework-refers-to-junit4
// Can be removed when IJPL-159134 is fixed
// testRuntimeOnly("junit:junit:4.13.2")
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.0")
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.2")
// testFixturesImplementation("org.junit.vintage:junit-vintage-engine:5.10.3")
}
@@ -220,40 +228,11 @@ tasks {
options.encoding = "UTF-8"
}
compileKotlin {
kotlinOptions {
jvmTarget = javaVersion
// See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library
// For the list of bundled versions
apiVersion = "2.0"
freeCompilerArgs = listOf(
"-Xjvm-default=all-compatibility",
// Needed to compile the AceJump which uses kotlin beta
// Without these two option compilation fails
"-Xskip-prerelease-check",
"-Xallow-unstable-dependencies",
)
// allWarningsAsErrors = true
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = javaVersion
apiVersion = "2.0"
// Needed to compile the AceJump which uses kotlin beta
// Without these two option compilation fails
freeCompilerArgs += listOf("-Xskip-prerelease-check", "-Xallow-unstable-dependencies")
// allWarningsAsErrors = true
}
}
// Note that this will run the plugin installed in the IDE specified in dependencies. To run in a different IDE, use
// a custom task (see below)
runIde {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
systemProperty("idea.trust.all.projects", "true")
}
// Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies
@@ -322,6 +301,23 @@ kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion))
}
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(javaVersion))
// See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library
// For the list of bundled versions
apiVersion.set(KotlinVersion.KOTLIN_2_0)
freeCompilerArgs = listOf(
"-Xjvm-default=all-compatibility",
// Needed to compile the AceJump which uses kotlin beta
// Without these two option compilation fails
"-Xskip-prerelease-check",
"-Xallow-unstable-dependencies",
)
// allWarningsAsErrors = true
}
}
gradle.projectsEvaluated {

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
ideaType=IC
instrumentPluginCode=true
version=SNAPSHOT
version=chylex-51
javaVersion=21
remoteRobotVersion=0.11.23
antlrVersion=4.10.1
@@ -28,7 +28,7 @@ antlrVersion=4.10.1
# Please don't forget to update kotlin version in buildscript section
# Also update kotlinxSerializationVersion version
kotlinVersion=2.0.21
kotlinVersion=2.2.0
publishToken=token
publishChannels=eap
@@ -41,7 +41,6 @@ youtrackToken=
# Gradle settings
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.configuration-cache=true
org.gradle.caching=true
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary

View File

@@ -20,27 +20,25 @@ repositories {
}
dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.1.21")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.2.0")
implementation("io.ktor:ktor-client-core:3.1.3")
implementation("io.ktor:ktor-client-cio:3.1.3")
implementation("io.ktor:ktor-client-content-negotiation:3.1.3")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.3")
implementation("io.ktor:ktor-client-auth:3.1.3")
implementation("io.ktor:ktor-client-core:3.2.2")
implementation("io.ktor:ktor-client-cio:3.2.2")
implementation("io.ktor:ktor-client-content-negotiation:3.2.2")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.2.2")
implementation("io.ktor:ktor-client-auth:3.2.2")
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
// This is needed for jgit to connect to ssh
implementation("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.2.1.202505142326-r")
implementation("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.3.0.202506031305-r")
implementation("com.vdurmont:semver4j:3.1.0")
}
val releaseType: String? by project
tasks {
compileKotlin {
kotlinOptions {
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
kotlin {
compilerOptions {
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
}

View File

@@ -45,6 +45,8 @@ val knownPlugins = setOf(
"com.julienphalip.ideavim.functiontextobj", // https://plugins.jetbrains.com/plugin/25897-vim-functiontextobj
"com.miksuki.HighlightCursor", // https://plugins.jetbrains.com/plugin/26743-highlightcursor
"com.ugarosa.idea.edgemotion", // https://plugins.jetbrains.com/plugin/27211-edgemotion
"cn.mumukehao.plugin",
)
suspend fun main() {

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,
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 */
@@ -288,4 +288,4 @@ fun VimExtensionFacade.exportOperatorFunction(name: String, function: OperatorFu
fun interface ScriptFunction {
fun execute(editor: VimEditor, context: ExecutionContext, args: Map<String, VimDataType>): ExecutionResult
}
}

View File

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

View File

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

View File

@@ -455,6 +455,17 @@ internal class NerdTree : VimExtension {
tree.scrollRowToVisible(expectedRow)
},
)
registerCommand("gg", NerdAction.Code { project, _, _ ->
val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
tree.setSelectionRow(0)
tree.scrollRowToVisible(0)
})
registerCommand("G", NerdAction.Code { project, _, _ ->
val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
val lastRowIndex = tree.rowCount -1
tree.setSelectionRow(lastRowIndex)
tree.scrollRowToVisible(lastRowIndex)
})
registerCommand(
"NERDTreeMapJumpNextSibling",
"<C-J>",

View File

@@ -29,7 +29,6 @@ import com.maddyhome.idea.vim.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCopiedText
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
@@ -154,8 +153,7 @@ private fun doReplace(editor: Editor, context: DataContext, caret: ImmutableVimC
usedType = SelectionType.CHARACTER_WISE
}
val copiedText = IjVimCopiedText(usedText, (savedRegister.copiedText as IjVimCopiedText).transferableData)
val textData = PutData.TextData(savedRegister.name, copiedText, usedType)
val textData = PutData.TextData(usedText, usedType, savedRegister.transferableData, savedRegister.name)
val putData = PutData(
textData,

View File

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

View File

@@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.endsWithNewLine
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
@@ -37,7 +38,10 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
import com.maddyhome.idea.vim.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.findBlockRange
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
@@ -80,7 +84,7 @@ internal class VimSurroundExtension : VimExtension {
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 {
@@ -108,7 +112,7 @@ internal class VimSurroundExtension : VimExtension {
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
if (lastNonWhiteSpaceOffset != null) {
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
performSurround(pair, range, it)
performSurround(pair, range, it, count = operatorArguments.count1)
}
// it.moveToOffset(lineStartOffset)
}
@@ -131,16 +135,14 @@ internal class VimSurroundExtension : VimExtension {
private class VSurroundHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// NB: Operator ignores SelectionType anyway
if (!Operator().apply(editor, context, editor.mode.selectionType)) {
if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
return
}
runWriteAction {
// Leave visual mode
editor.exitVisualMode()
editor.ij.caretModel.moveToOffset(selectionStart)
// Reset the key handler so that the command trie is updated for the new mode (Normal)
// TODO: This should probably be handled by ToHandlerMapping.execute
KeyHandler.getInstance().reset(editor)
@@ -164,6 +166,10 @@ internal class VimSurroundExtension : VimExtension {
companion object {
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) }
}
fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
// Save old register values for carets
val surroundings = editor.sortedCarets()
.map {
@@ -206,7 +212,7 @@ internal class VimSurroundExtension : VimExtension {
val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue
it.first + trimmedValue + it.second
} ?: 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)
surrounding.caret to putData
@@ -284,20 +290,41 @@ internal class VimSurroundExtension : VimExtension {
}
}
private class Operator : OperatorFunction {
override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val ijEditor = editor.ij
private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
override fun apply(vimEditor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val ijEditor = vimEditor.ij
val c = getChar(ijEditor)
if (c.code == 0) return true
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
// XXX: Will it work with line-wise or block-wise selections?
val range = getSurroundRange(editor.currentCaret()) ?: return false
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
runWriteAction {
val change = VimPlugin.getChange()
if (supportsMultipleCursors) {
ijEditor.runWithEveryCaretAndRestore {
applyOnce(ijEditor, change, pair, count)
}
}
else {
applyOnce(ijEditor, change, pair, count)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
}
}
return true
}
private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: SurroundPair, count: Int) {
// XXX: Will it work with line-wise or block-wise selections?
val primaryCaret = editor.caretModel.primaryCaret
val range = getSurroundRange(primaryCaret.vim)
if (range != null) {
val start = RepeatedCharSequence.of(pair.first, count)
val end = RepeatedCharSequence.of(pair.second, count)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end)
}
}
private fun getSurroundRange(caret: VimCaret): TextRange? {
val editor = caret.editor
@@ -398,15 +425,15 @@ private fun getChar(editor: Editor): Char {
return res
}
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 {
val editor = caret.editor
val change = VimPlugin.getChange()
val leftSurround = pair.first + if (tagsOnNewLines) "\n" else ""
val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
val isEOF = range.endOffset == editor.text().length
val hasNewLine = editor.endsWithNewLine()
val rightSurround = if (tagsOnNewLines) {
val rightSurround = (if (tagsOnNewLines) {
if (isEOF && !hasNewLine) {
"\n" + pair.second
} else {
@@ -414,7 +441,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare
}
} else {
pair.second
}
}).let { RepeatedCharSequence.of(it, count) }
change.insertText(editor, caret, range.startOffset, leftSurround)
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)

View File

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

View File

@@ -141,7 +141,7 @@ object IjOptions {
// Temporary feature flags during development, not really intended for external use
val closenotebooks: ToggleOption =
addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true))
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true))
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isHidden = true))
val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
// This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which

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.message
import com.maddyhome.idea.vim.macro.VimMacroBase
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
/**
* Used to handle playback of macros
@@ -89,6 +90,9 @@ internal class MacroGroup : VimMacroBase() {
} finally {
keyStack.removeFirst()
}
if (!isInternalMacro) {
MacroAutoImport.run(editor.ij, context.ij)
}
}
if (isInternalMacro) {

View File

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

View File

@@ -33,7 +33,6 @@ import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.SystemInfo
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.handler.KeyMapIssue
import com.maddyhome.idea.vim.helper.MessageHelper
@@ -41,8 +40,6 @@ import com.maddyhome.idea.vim.icons.VimIcons
import com.maddyhome.idea.vim.key.ShortcutOwner
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ijOptions
import com.maddyhome.idea.vim.options.OptionConstants
import com.maddyhome.idea.vim.statistic.ActionTracker
import com.maddyhome.idea.vim.ui.VimEmulationConfigurable
import com.maddyhome.idea.vim.vimscript.services.VimRcService
@@ -62,55 +59,11 @@ internal class NotificationService(private val project: Project?) {
@Suppress("unused")
constructor() : this(null)
fun notifyAboutIdeaPut() {
val notification = Notification(
IDEAVIM_NOTIFICATION_ID,
IDEAVIM_NOTIFICATION_TITLE,
"""Add <code>ideaput</code> to <code>clipboard</code> option to perform a put via the IDE<br/><b><code>set clipboard+=ideaput</code></b>""",
NotificationType.INFORMATION,
)
fun notifyAboutNewUndo() {}
notification.addAction(OpenIdeaVimRcAction(notification))
fun notifyAboutIdeaPut() {}
notification.addAction(
AppendToIdeaVimRcAction(
notification,
"set clipboard^=ideaput",
"ideaput",
) {
// Technically, we're supposed to prepend values to clipboard so that it's not added to the "exclude" item.
// Since we don't handle exclude, it's safe to append. But let's be clean.
injector.globalOptions().clipboard.prependValue(OptionConstants.clipboard_ideaput)
},
)
notification.notify(project)
}
fun notifyAboutIdeaJoin(editor: VimEditor) {
val notification = Notification(
IDEAVIM_NOTIFICATION_ID,
IDEAVIM_NOTIFICATION_TITLE,
"""Put <b><code>set ideajoin</code></b> into your <code>~/.ideavimrc</code> to perform a join via the IDE""",
NotificationType.INFORMATION,
)
notification.addAction(OpenIdeaVimRcAction(notification))
notification.addAction(
AppendToIdeaVimRcAction(
notification,
"set ideajoin",
"ideajoin"
) {
// This is a global-local option. Setting it will always set the global value
injector.ijOptions(editor).ideajoin = true
},
)
notification.addAction(HelpLink(ideajoinExamplesUrl))
notification.notify(project)
}
fun notifyAboutIdeaJoin(editor: VimEditor) {}
fun enableRepeatingMode() = Messages.showYesNoDialog(
"Do you want to enable repeating keys in macOS on press and hold?\n\n" +
@@ -218,7 +171,7 @@ internal class NotificationService(private val project: Project?) {
is KeyMapIssue.AddShortcut -> {
appendLine("- ${it.key} key is not assigned to the ${it.action} action.<br/>")
}
is KeyMapIssue.RemoveShortcut -> {
appendLine("- ${it.shortcut} key is incorrectly assigned to the ${it.action} action.<br/>")
}
@@ -305,16 +258,16 @@ internal class NotificationService(private val project: Project?) {
notification =
Notification(IDEAVIM_NOTIFICATION_ID, IDEAVIM_NOTIFICATION_TITLE, content, NotificationType.INFORMATION).also {
it.whenExpired { notification = null }
it.addAction(StopTracking())
it.whenExpired { notification = null }
it.addAction(StopTracking())
if (id != null || possibleIDs?.size == 1) {
it.addAction(CopyActionId(id ?: possibleIDs?.get(0), project))
}
it.notify(project)
if (id != null || possibleIDs?.size == 1) {
it.addAction(CopyActionId(id ?: possibleIDs?.get(0), project))
}
it.notify(project)
}
if (id != null) {
ActionTracker.Util.logTrackedAction(id)
}

View File

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

View File

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

View File

@@ -134,7 +134,7 @@ internal object IdeaSelectionControl {
is Mode.VISUAL -> VimPlugin.getVisualMotion().enterVisualMode(editor.vim, mode.selectionType)
is Mode.SELECT -> VimPlugin.getVisualMotion().enterSelectMode(editor.vim, mode.selectionType)
is Mode.INSERT -> VimPlugin.getChange()
.insertBeforeCursor(editor.vim, injector.executionContextManager.getEditorExecutionContext(editor.vim))
.insertBeforeCaret(editor.vim, injector.executionContextManager.getEditorExecutionContext(editor.vim))
is Mode.NORMAL -> Unit
else -> error("Unexpected mode: $mode")

View File

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

View File

@@ -12,7 +12,9 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.util.ui.table.JBTableRowEditor
@@ -21,6 +23,8 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import java.awt.Component
import javax.swing.JComponent
import javax.swing.JTable
@@ -102,8 +106,7 @@ internal fun Editor.isPrimaryEditor(): Boolean {
internal fun Editor.isTerminalEditor(): Boolean {
return !isViewer
&& document.isWritable
&& !EditorHelper.isFileEditor(this)
&& !EditorHelper.isDiffEditor(this)
&& this.editorKind == EditorKind.CONSOLE
}
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
@@ -136,3 +139,41 @@ internal val Caret.vimLine: Int
*/
internal val Editor.vimLine: Int
get() = this.caretModel.currentCaret.vimLine
internal inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) {
val caretModel = this.caretModel
val carets = if (this.vim.inBlockSelection) null else caretModel.allCarets
if (carets == null || carets.size == 1) {
action()
}
else {
var initialDocumentSize = this.document.textLength
var documentSizeDifference = 0
val caretOffsets = carets.map { it.selectionStart to it.selectionEnd }
val restoredCarets = mutableListOf<CaretState>()
caretModel.removeSecondaryCarets()
for ((selectionStart, selectionEnd) in caretOffsets) {
if (selectionStart == selectionEnd) {
caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference)
}
else {
caretModel.primaryCaret.setSelection(
selectionStart + documentSizeDifference,
selectionEnd + documentSizeDifference
)
}
action()
restoredCarets.add(caretModel.caretsAndSelections.single())
val documentLength = this.document.textLength
documentSizeDifference += documentLength - initialDocumentSize
initialDocumentSize = documentLength
}
caretModel.caretsAndSelections = restoredCarets
}
}

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.PlatformUtils
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
@@ -29,6 +30,8 @@ import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
/**
@@ -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
editor.runWithChangeTracking {
undoManager.undo(fileEditor)
// We execute undo one more time if the previous one just restored selection
if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
undoManager.undo(fileEditor)
}
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
restoreVisualMode(editor)
}
} else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
@@ -241,4 +236,21 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService {
val hasChanges: Boolean
get() = changeListener.hasChanged || initialPath != editor.getPath()
}
private fun restoreVisualMode(editor: VimEditor) {
if (!editor.inVisualMode && editor.getSelectionModel().hasSelection()) {
val detectedMode = VimPlugin.getVisualMotion().detectSelectionType(editor)
// Visual block selection is restored into multiple carets, so multi-carets that form a block are always
// identified as visual block mode, leading to false positives.
// Since I use visual block mode much less often than multi-carets, this is a judgment call to never restore
// visual block mode.
val wantedMode = if (detectedMode == SelectionType.BLOCK_WISE)
SelectionType.CHARACTER_WISE
else
detectedMode
VimPlugin.getVisualMotion().enterVisualMode(editor, wantedMode)
}
}
}

View File

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

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.helper
import com.intellij.ide.plugins.StandalonePluginUpdateChecker
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.group.NotificationService
import com.maddyhome.idea.vim.icons.VimIcons
@Service(Service.Level.APP)
internal class VimStandalonePluginUpdateChecker : StandalonePluginUpdateChecker(
VimPlugin.getPluginId(),
updateTimestampProperty = PROPERTY_NAME,
NotificationService.IDEAVIM_STICKY_GROUP,
VimIcons.IDEAVIM,
) {
override fun skipUpdateCheck(): Boolean = VimPlugin.isNotEnabled() || "dev" in VimPlugin.getVersion()
companion object {
private const val PROPERTY_NAME = "ideavim.statistics.timestamp"
fun getInstance(): VimStandalonePluginUpdateChecker = service()
}
}

View File

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

View File

@@ -16,7 +16,9 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction
import com.intellij.openapi.actionSystem.ActionManager
@@ -30,6 +32,7 @@ import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange
@@ -61,6 +64,7 @@ internal object IdeaSpecifics {
private val surrounderAction =
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
private var editor: Editor? = null
private var caretOffset = -1
private var completionPrevDocumentLength: Int? = null
private var completionPrevDocumentOffset: Int? = null
@@ -70,6 +74,7 @@ internal object IdeaSpecifics {
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) {
editor = hostEditor
caretOffset = hostEditor.caretModel.offset
}
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
@@ -127,17 +132,18 @@ internal object IdeaSpecifics {
if (VimPlugin.isNotEnabled()) return
val editor = editor
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
val prevDocumentLength = completionPrevDocumentLength
val prevDocumentOffset = completionPrevDocumentOffset
if (editor != null) {
if (action is ChooseItemAction && injector.registerGroup.isRecording) {
val prevDocumentLength = completionPrevDocumentLength
val prevDocumentOffset = completionPrevDocumentOffset
if (prevDocumentLength != null && prevDocumentOffset != null) {
val register = VimPlugin.getRegister()
val addedTextLength = editor.document.textLength - prevDocumentLength
val caretShift = addedTextLength - (editor.caretModel.primaryCaret.offset - prevDocumentOffset)
val leftArrow = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)
if (prevDocumentLength != null && prevDocumentOffset != null) {
val register = VimPlugin.getRegister()
val addedTextLength = editor.document.textLength - prevDocumentLength
val caretShift = addedTextLength - (editor.caretModel.primaryCaret.offset - prevDocumentOffset)
val leftArrow = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)
register.recordText(
register.recordText(
editor.document.getText(
TextRange(
prevDocumentOffset,
@@ -145,31 +151,49 @@ internal object IdeaSpecifics {
)
)
)
repeat(caretShift.coerceAtLeast(0)) {
register.recordKeyStroke(leftArrow)
repeat(caretShift.coerceAtLeast(0)) {
register.recordKeyStroke(leftArrow)
}
}
this.completionPrevDocumentLength = null
this.completionPrevDocumentOffset = null
}
//region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith(
it,
)
}
) {
editor?.let {
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim)
KeyHandler.getInstance().reset(it.vim)
}
}
this.completionPrevDocumentLength = null
this.completionPrevDocumentOffset = null
}
//region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith(
it,
)
else if (action is NextVariableAction && TemplateManagerImpl.getTemplateState(editor) == null) {
editor.vim.exitInsertMode(event.dataContext.vim)
KeyHandler.getInstance().reset(editor.vim)
}
) {
editor?.let {
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCursor(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.caretOffset = -1
}
}
@@ -199,7 +223,7 @@ internal object IdeaSpecifics {
// 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().insertBeforeCursor(
VimPlugin.getChange().insertBeforeCaret(
editor.vim,
injector.executionContextManager.getEditorExecutionContext(editor.vim),
)

View File

@@ -81,7 +81,6 @@ import com.maddyhome.idea.vim.handler.keyCheckRequests
import com.maddyhome.idea.vim.helper.CaretVisualAttributesListener
import com.maddyhome.idea.vim.helper.GuicursorChangeListener
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.VimStandalonePluginUpdateChecker
import com.maddyhome.idea.vim.helper.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.forceBarCursor
@@ -98,6 +97,7 @@ import com.maddyhome.idea.vim.newapi.IjVimSearchGroup
import com.maddyhome.idea.vim.newapi.InsertTimeRecorder
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
@@ -411,10 +411,21 @@ internal object VimListenerManager {
override fun selectionChanged(event: FileEditorManagerEvent) {
// We can't rely on being passed a non-null editor, so check for Code With Me scenarios explicitly
if (VimPlugin.isNotEnabled() || !ClientId.isCurrentlyUnderLocalId) return
val newEditor = event.newEditor
if (newEditor is TextEditor) {
val editor = newEditor.editor
if (editor.isInsertMode) {
editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim)
}
// Breaks relativenumber for some reason
// injector.scroll.scrollCaretIntoView(editor.vim)
}
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroup.fileEditorManagerSelectionChangedCallback(event)
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
// VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
}
@@ -487,8 +498,6 @@ internal object VimListenerManager {
OpeningEditor(openingEditor, owningEditorWindow, isPreview, canBeReused)
)
}
VimStandalonePluginUpdateChecker.getInstance().pluginUsed()
}
override fun editorReleased(event: EditorFactoryEvent) {

View File

@@ -39,7 +39,7 @@ import java.io.IOException
@Service
internal class IjClipboardManager : VimClipboardManager {
override fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): IjVimCopiedText? {
override fun getPrimaryContent(): IjVimCopiedText? {
val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null
val contents = clipboard.getContents(null) ?: return null
val (text, transferableData) = getTextAndTransferableData(contents) ?: return null
@@ -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)
}

View File

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

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.editor.ex.ScrollingModelEx
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.impl.CaretModelImpl
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFileManager
import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.ExecutionContext
@@ -35,8 +36,8 @@ import com.maddyhome.idea.vim.api.VimFoldRegion
import com.maddyhome.idea.vim.api.VimIndentConfig
import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.VimVirtualFile
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.IndentConfig
import com.maddyhome.idea.vim.common.LiveRange
@@ -150,7 +151,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) {
@@ -179,21 +180,38 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
return editor.caretModel.allCarets.map { IjVimCaret(it) }
}
override var isFirstCaret = true
override var isReversingCarets = false
@Suppress("ideavimRunForEachCaret")
override fun forEachCaret(action: (VimCaret) -> Unit) {
if (editor.vim.inBlockSelection) {
action(IjVimCaret(editor.caretModel.primaryCaret))
} else {
editor.caretModel.runForEachCaret({
if (it.isValid) {
action(IjVimCaret(it))
}
}, false)
try {
editor.caretModel.runForEachCaret({
if (it.isValid) {
action(IjVimCaret(it))
isFirstCaret = false
}
}, false)
} finally {
isFirstCaret = true
}
}
}
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {
editor.caretModel.runForEachCaret({ action(IjVimCaret(it)) }, reverse)
isReversingCarets = reverse
try {
editor.caretModel.runForEachCaret({
action(IjVimCaret(it))
isFirstCaret = false
}, reverse)
} finally {
isFirstCaret = true
isReversingCarets = false
}
}
override fun isInForEachCaretScope(): Boolean {
@@ -497,6 +515,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 {
return caret
}

View File

@@ -353,7 +353,7 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder()
.calculateCount0Snapshot());
if (labelText.equals("/") || labelText.equals("?") || searchCommand) {
if ((labelText.equals("/") || labelText.equals("?") || searchCommand) && !injector.getMacro().isExecutingMacro()) {
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
int patternEnd = injector.getSearchGroup().findEndOfPattern(searchText, separator, 0);
final String pattern = searchText.substring(0, patternEnd);

View File

@@ -1,12 +1,4 @@
<!--
~ 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" xmlns:xi="http://www.w3.org/2001/XInclude">
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
<name>IdeaVim</name>
<id>IdeaVIM</id>
<description><![CDATA[
@@ -21,7 +13,7 @@
<li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li>
</ul>
]]></description>
<version>SNAPSHOT</version>
<version>chylex</version>
<vendor>JetBrains</vendor>
<!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) -->
@@ -143,6 +135,8 @@
<editorNotificationProvider
implementation="com.maddyhome.idea.vim.troubleshooting.AccidentalInstallDetectorEditorNotificationProvider"/>
<dependencySupport coordinate="configuration" kind="vim" displayName="IdeaVim"/>
</extensions>
<xi:include href="/META-INF/includes/ApplicationServices.xml" xpointer="xpointer(/idea-plugin/*)"/>
@@ -171,5 +165,6 @@
</group>
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
</actions>
</idea-plugin>

View File

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

View File

@@ -1144,6 +1144,12 @@ $c tw${c}o
"O${c}NcE thIs ${c}TEXt wIlL n${c}Ot lOoK s${c}O rIdIcuLoUs\n",
)
assertState("O${c}nce this text will n${c}ot look s${c}o ridiculous\n")
typeTextInFile(
injector.parser.parseKeys("v2wgu"),
"O${c}NcE thIs ${c}TEXt wIlL n${c}Ot lOoK s${c}O rIdIcuLoUs\n",
)
assertState("O${c}nce this text will n${c}ot look s${c}o ridiculous\n")
}
@Test
@@ -1180,6 +1186,12 @@ $c tw${c}o
"O${c}NcE thIs ${c}TEXt wIlL N${c}Ot lOoK S${c}O rIdIcuLoUs\n",
)
assertState("O${c}NCE THIS TEXT WILL N${c}OT LOOK S${c}O RIDICULOUS\n")
typeTextInFile(
injector.parser.parseKeys("v2wgU"),
"O${c}NcE thIs ${c}TEXt wIlL N${c}Ot lOoK S${c}O rIdIcuLoUs\n",
)
assertState("O${c}NCE THIS TEXT WILL N${c}OT LOOK S${c}O RIDICULOUS\n")
}
@Test

View File

@@ -320,4 +320,61 @@ class ChangeCaseToggleCharacterActionTest : VimTestCase() {
enterCommand("set nooldundo")
}
}
@Test
fun `test toggle case line caret position`() {
configureByText(" Hello ${c}World")
typeText("g~~")
assertState(" ${c}hELLO wORLD")
typeText("u")
assertState(" Hello ${c}World")
typeText("^g~~")
assertState(" ${c}hELLO wORLD")
typeText("u")
assertState(" ${c}Hello World")
typeText("hg~~")
assertState(" $c hELLO wORLD")
typeText("u")
assertState(" $c Hello World")
}
@Test
fun `test uppercase line caret position`() {
configureByText(" Hello ${c}World")
typeText("gUU")
assertState(" ${c}HELLO WORLD")
typeText("u")
assertState(" Hello ${c}World")
typeText("^gUU")
assertState(" ${c}HELLO WORLD")
typeText("u")
assertState(" ${c}Hello World")
typeText("hgUU")
assertState(" $c HELLO WORLD")
typeText("u")
assertState(" $c Hello World")
}
@Test
fun `test lowercase line caret position`() {
configureByText(" Hello ${c}World")
typeText("guu")
assertState(" ${c}hello world")
typeText("u")
assertState(" Hello ${c}World")
typeText("^guu")
assertState(" ${c}hello world")
typeText("u")
assertState(" ${c}Hello World")
typeText("hguu")
assertState(" $c hello world")
typeText("u")
assertState(" $c Hello World")
}
}

View File

@@ -17,7 +17,7 @@ import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Test
class VisualBlockInsertActionTest : VimTestCase() {
class VisualInsertActionTest : VimTestCase() {
// VIM-1379 |CTRL-V| |j| |v_b_I|
@TestWithoutNeovim(SkipNeovimReason.VISUAL_BLOCK_MODE)
@Test
@@ -101,28 +101,70 @@ class VisualBlockInsertActionTest : VimTestCase() {
)
}
@TestWithoutNeovim(SkipNeovimReason.VISUAL_BLOCK_MODE)
@Test
fun `test insert in non block mode`() {
doTest(
listOf("vwIHello<esc>"),
"""
${c}A Discovery
fun `test insert in non-block visual within single line`() {
val before = """
| A ${c}Discovery
${c}I found it in a legendary land
all rocks and ${c}lavender and tufted grass,
where it was settled on some sodden sand
hard by the torrent of a mountain pass.
""".trimIndent(),
"""
Hell${c}oA Discovery
| I ${c}found it in a legendary land
| all rocks and lavender and tufted grass,
| where it was settled on some sodden sand
| hard by the torrent of a mountain pass.
""".trimMargin()
val after = """
|Hell${c}o A Discovery
Hell${c}oI found it in a legendary land
Hell${c}oall rocks and lavender and tufted grass,
where it was settled on some sodden sand
hard by the torrent of a mountain pass.
""".trimIndent(),
)
|Hell${c}o I found it in a legendary land
| all rocks and lavender and tufted grass,
| where it was settled on some sodden sand
| hard by the torrent of a mountain pass.
""".trimMargin()
doTest(listOf($$"v$IHello<esc>"), before, after)
doTest(listOf("VIHello<esc>"), before, after)
}
@Test
fun `test insert in non-block visual spanning multiple lines down`() {
val before = """
| A ${c}Discovery
| I ${c}found it in a legendary land
| all rocks and lavender and tufted grass,
| where it was settled on some sodden sand
| hard by the torrent of a mountain pass.
""".trimMargin()
val after = """
|Hell${c}o A Discovery
|Hell${c}o I found it in a legendary land
| all rocks and lavender and tufted grass,
| where it was settled on some sodden sand
| hard by the torrent of a mountain pass.
""".trimMargin()
doTest(listOf("vjIHello<esc>"), before, after)
doTest(listOf("VjIHello<esc>"), before, after)
}
@Test
fun `test insert in non-block visual spanning multiple lines up`() {
val before = """
| A Discovery
| I found it in a legendary land
| all rocks and lavender and tufted grass${c},
| where it was settled on some sodden sand
| hard ${c}by the torrent of a mountain pass.
""".trimMargin()
val after = """
| A Discovery
| I found it in a legendary landHell${c}o
| all rocks and lavender and tufted grass,
| whereHell${c}o it was settled on some sodden sand
| hard by the torrent of a mountain pass.
""".trimMargin()
doTest(listOf("vkIHello<esc>"), before, after)
doTest(listOf("VkIHello<esc>"), before, after)
}
@TestWithoutNeovim(SkipNeovimReason.VISUAL_BLOCK_MODE)

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
testImplementation(testFixtures(project(":"))) // The root project
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.0")
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.2")
intellijPlatform {
// Snapshots don't use installers

View File

@@ -17,7 +17,7 @@ import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimJavaTestCase
import org.junit.jupiter.api.Test
class VisualBlockInsertActionJavaTest : VimJavaTestCase() {
class VisualInsertActionJavaTest : VimJavaTestCase() {
// VIM-1110 |CTRL-V| |v_b_i| |zc|
@TestWithoutNeovim(SkipNeovimReason.FOLDING)
@Test

View File

@@ -25,7 +25,7 @@ dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
testImplementation(testFixtures(project(":"))) // The root project
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.0")
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.2")
intellijPlatform {
// Snapshots don't use installers
@@ -47,17 +47,8 @@ tasks {
// I didn't find a better way to exclude except disabling and defining a new task with a different name
// Note that useJUnitTestPlatform() is required to prevent red code
test {
enabled = false
useJUnitPlatform()
}
// The `test` task is automatically set up with IntelliJ goodness. A custom test task needs to be configured for it
val testLongRunning by intellijPlatformTesting.testIde.registering {
task {
group = "verification"
useJUnitPlatform()
}
}
}
java {

View File

@@ -25,7 +25,7 @@ dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
testImplementation(testFixtures(project(":"))) // The root project
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.0")
testImplementation("org.junit.vintage:junit-vintage-engine:5.13.2")
intellijPlatform {
// Snapshots don't use installers
@@ -48,15 +48,6 @@ tasks {
// I didn't find a better way to exclude except disabling and defining a new task with a different name
test {
useJUnitPlatform()
enabled = false
}
// The `test` task is automatically set up with IntelliJ goodness. A custom test task needs to be configured for it
val testPropertyBased by intellijPlatformTesting.testIde.registering {
task {
group = "verification"
useJUnitPlatform()
}
}
}

View File

@@ -15,7 +15,7 @@ val javaVersion: String by project
val remoteRobotVersion: String by project
dependencies {
testFixturesImplementation("org.junit.jupiter:junit-jupiter:5.13.0")
testFixturesImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
testFixturesImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
testFixturesImplementation(testFixtures(project(":"))) // The root project

View File

@@ -11,6 +11,7 @@ import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.CommonContainerFixture
import com.intellij.remoterobot.fixtures.DefaultXpath
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.fixtures.JButtonFixture
import com.intellij.remoterobot.search.locators.byXpath
import java.time.Duration
@@ -28,6 +29,6 @@ class ManageLicensesFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteCompo
/// Note: The license code is obfuscated, so we use the class `W`. But a better solution is required.
textFields(byXpath("//div[@class='W']")).first().text = System.getenv("RIDER_LICENSE")
button("Activate").click()
button("Close").click()
button(JButtonFixture.byText("Close"), timeout = Duration.ofSeconds(20)).click()
}
}

View File

@@ -10,8 +10,8 @@ plugins {
java
kotlin("jvm")
// id("org.jlleitschuh.gradle.ktlint")
id("com.google.devtools.ksp") version "2.0.21-1.0.25"
kotlin("plugin.serialization") version "2.0.21"
id("com.google.devtools.ksp") version "2.2.0-2.0.2"
kotlin("plugin.serialization") version "2.2.0"
`maven-publish`
antlr
}
@@ -45,13 +45,13 @@ afterEvaluate {
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.13.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.13.3")
// Temp workaround suggested in https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#junit5-test-framework-refers-to-junit4
// Can be removed when IJPL-159134 is fixed
// testRuntimeOnly("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.13.0")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.13.2")
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-test
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
@@ -68,7 +68,7 @@ dependencies {
compileOnly(kotlin("reflect"))
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:6.0.0")
}
tasks {
@@ -88,13 +88,13 @@ tasks {
named("compileTestKotlin") {
dependsOn("generateTestGrammarSource")
}
}
compileKotlin {
kotlinOptions {
apiVersion = "2.0"
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
}
kotlin {
compilerOptions {
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
}
// --- Linting

View File

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

View File

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

View File

@@ -18,10 +18,13 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.DuplicableOperatorAction
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
@CommandOrMotion(keys = ["gu"], modes = [Mode.NORMAL])
class ChangeCaseLowerMotionAction : ChangeEditorActionHandler.ForEachCaret(), DuplicableOperatorAction {
private val logger = vimLogger<ChangeCaseLowerMotionAction>()
override val type: Command.Type = Command.Type.CHANGE
override val argumentType: Argument.Type = Argument.Type.MOTION
@@ -35,15 +38,18 @@ class ChangeCaseLowerMotionAction : ChangeEditorActionHandler.ForEachCaret(), Du
argument: Argument?,
operatorArguments: OperatorArguments,
): Boolean {
return argument != null &&
injector.changeGroup
.changeCaseMotion(
editor,
caret,
context,
VimChangeGroup.ChangeCaseType.LOWER,
argument,
operatorArguments,
)
if (argument == null || argument !is Argument.Motion) {
logger.error("Argument is null or not Argument.Motion. argument=$argument")
return false
}
return injector.changeGroup.changeCaseMotion(
editor,
caret,
context,
VimChangeGroup.ChangeCaseType.LOWER,
argument,
operatorArguments,
)
}
}

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -22,7 +21,7 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/**
* @author vlan
*/
@CommandOrMotion(keys = ["u"], modes = [Mode.VISUAL])
@CommandOrMotion(keys = [], modes = [])
class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE

View File

@@ -18,10 +18,13 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.DuplicableOperatorAction
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
@CommandOrMotion(keys = ["g~"], modes = [Mode.NORMAL])
class ChangeCaseToggleMotionAction : ChangeEditorActionHandler.ForEachCaret(), DuplicableOperatorAction {
private val logger = vimLogger<ChangeCaseToggleMotionAction>()
override val type: Command.Type = Command.Type.CHANGE
override val argumentType: Argument.Type = Argument.Type.MOTION
@@ -35,15 +38,18 @@ class ChangeCaseToggleMotionAction : ChangeEditorActionHandler.ForEachCaret(), D
argument: Argument?,
operatorArguments: OperatorArguments,
): Boolean {
return argument != null &&
injector.changeGroup
.changeCaseMotion(
editor,
caret,
context,
VimChangeGroup.ChangeCaseType.TOGGLE,
argument,
operatorArguments,
)
if (argument == null || argument !is Argument.Motion) {
logger.error("Argument is null or not Argument.Motion. argument=$argument")
return false
}
return injector.changeGroup.changeCaseMotion(
editor,
caret,
context,
VimChangeGroup.ChangeCaseType.TOGGLE,
argument,
operatorArguments,
)
}
}

View File

@@ -18,10 +18,13 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.DuplicableOperatorAction
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
@CommandOrMotion(keys = ["gU"], modes = [Mode.NORMAL])
class ChangeCaseUpperMotionAction : ChangeEditorActionHandler.ForEachCaret(), DuplicableOperatorAction {
private val logger = vimLogger<ChangeCaseUpperMotionAction>()
override val type: Command.Type = Command.Type.CHANGE
override val argumentType: Argument.Type = Argument.Type.MOTION
@@ -35,15 +38,18 @@ class ChangeCaseUpperMotionAction : ChangeEditorActionHandler.ForEachCaret(), Du
argument: Argument?,
operatorArguments: OperatorArguments,
): Boolean {
return argument != null &&
injector.changeGroup
.changeCaseMotion(
editor,
caret,
context,
VimChangeGroup.ChangeCaseType.UPPER,
argument,
operatorArguments,
)
if (argument == null || argument !is Argument.Motion) {
logger.error("Argument is null or not Argument.Motion. argument=$argument")
return false
}
return injector.changeGroup.changeCaseMotion(
editor,
caret,
context,
VimChangeGroup.ChangeCaseType.UPPER,
argument,
operatorArguments,
)
}
}

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -22,7 +21,7 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/**
* @author vlan
*/
@CommandOrMotion(keys = ["U"], modes = [Mode.VISUAL])
@CommandOrMotion(keys = [], modes = [])
class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE

View File

@@ -27,7 +27,7 @@ class InsertAfterCursorAction : ChangeEditorActionHandler.SingleExecution() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Boolean {
injector.changeGroup.insertAfterCursor(editor, context)
injector.changeGroup.insertAfterCaret(editor, context)
return true
}
}

View File

@@ -47,5 +47,5 @@ private fun insertAtPreviousInsert(editor: VimEditor, context: ExecutionContext)
if (motion is Motion.AbsoluteOffset) {
caret.moveToOffset(motion.offset)
}
injector.changeGroup.insertBeforeCursor(editor, context)
injector.changeGroup.insertBeforeCaret(editor, context)
}

View File

@@ -29,7 +29,7 @@ class InsertBeforeCursorAction : ChangeEditorActionHandler.SingleExecution() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Boolean {
injector.changeGroup.insertBeforeCursor(editor, context)
injector.changeGroup.insertBeforeCaret(editor, context)
return true
}
}

View File

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

View File

@@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.normalizeLine
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection
@@ -20,10 +21,17 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.state.mode.SelectionType
/**
* @author vlan
* Handles the 'I' command in Visual mode.
*
* For (linewise) Visual mode, the caret positioning follows these rules (based on observation in Vim):
* - If text on multiple lines is selected AND the caret is on the first line (e.g., when selecting from bottom to top),
* the caret position remains unchanged
* - In all other cases, the caret is moved to the start of the first selected line
*
* For blockwise Visual mode, it initiates insert at the start of block on each line in the selection
*/
@CommandOrMotion(keys = ["I"], modes = [Mode.VISUAL])
class VisualBlockInsertAction : VisualOperatorActionHandler.SingleExecution() {
class VisualInsertAction : VisualOperatorActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT
override fun executeForAllCarets(
@@ -38,7 +46,17 @@ class VisualBlockInsertAction : VisualOperatorActionHandler.SingleExecution() {
return if (vimSelection.type == SelectionType.BLOCK_WISE) {
injector.changeGroup.initBlockInsert(editor, context, vimSelection.toVimTextRange(false), false)
} else {
injector.changeGroup.insertBeforeFirstNonBlank(editor, context)
// For visual selections spanning multiple lines, keep caret position if it's on the first line
// Otherwise move the caret to the start of the first selected line
for ((caret, selection) in caretsAndSelections) {
val range = selection.toVimTextRange()
val posStart = editor.offsetToBufferPosition(range.startOffset)
val nextLineStart = editor.getLineStartOffset(editor.normalizeLine(posStart.line + 1))
if (caret.offset >= nextLineStart || nextLineStart >= range.endOffset) {
caret.moveToOffset(injector.motion.moveCaretToLineStart(editor, posStart.line))
}
}
injector.changeGroup.insertBeforeCaret(editor, context)
true
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.action.file
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.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["ZQ"], modes = [Mode.NORMAL])
class FileCloseAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override fun execute(
editor: VimEditor,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
injector.file.closeFile(editor, context)
return true
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2023 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -16,9 +16,9 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["ZQ", "ZZ"], modes = [Mode.NORMAL])
@CommandOrMotion(keys = ["ZZ"], modes = [Mode.NORMAL])
class FileSaveCloseAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_WRITABLE
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override fun execute(
editor: VimEditor,

View File

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

View File

@@ -48,7 +48,7 @@ class SelectMotionArrowLeftAction : MotionActionHandler.ForEachCaret() {
editor.exitSelectModeNative(false)
if (editor.isTemplateActive()) {
logger.debug("Template is active. Activate insert mode")
injector.changeGroup.insertBeforeCursor(editor, context)
injector.changeGroup.insertBeforeCaret(editor, context)
if (caret.offset in startSelection..endSelection) {
return startSelection.toMotion()
}

View File

@@ -48,7 +48,7 @@ class SelectMotionArrowRightAction : MotionActionHandler.ForEachCaret() {
editor.exitSelectModeNative(false)
if (editor.isTemplateActive()) {
logger.debug("Template is active. Activate insert mode")
injector.changeGroup.insertBeforeCursor(editor, context)
injector.changeGroup.insertBeforeCaret(editor, context)
if (caret.offset in startSelection..endSelection) {
return endSelection.toMotion()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,13 +23,13 @@ import javax.swing.KeyStroke
interface VimChangeGroup {
fun setInsertRepeat(lines: Int, column: Int, append: Boolean)
fun insertBeforeCursor(editor: VimEditor, context: ExecutionContext)
fun insertBeforeCaret(editor: VimEditor, context: ExecutionContext)
fun insertBeforeFirstNonBlank(editor: VimEditor, context: ExecutionContext)
fun insertLineStart(editor: VimEditor, context: ExecutionContext)
fun insertAfterCursor(editor: VimEditor, context: ExecutionContext)
fun insertAfterCaret(editor: VimEditor, context: ExecutionContext)
fun insertAfterLineEnd(editor: VimEditor, context: ExecutionContext)
@@ -198,7 +198,7 @@ interface VimChangeGroup {
caret: VimCaret,
context: ExecutionContext?,
type: ChangeCaseType,
argument: Argument,
argument: Argument.Motion,
operatorArguments: OperatorArguments,
): Boolean
@@ -231,7 +231,7 @@ interface VimChangeGroup {
operatorArguments: OperatorArguments,
)
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret
fun insertText(editor: VimEditor, caret: VimCaret, str: String): VimCaret

View File

@@ -182,31 +182,41 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return false
}
}
val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE
val shouldYank = type != null && !isInsertMode && saveToRegister
if (shouldYank && !caret.registerStorage.storeText(editor, context, updatedRange, type, isDelete = true)) {
return false
}
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
val mode = editor.mode
if (type == null ||
(mode == Mode.INSERT || mode == Mode.REPLACE) ||
!saveToRegister ||
injector.registerGroup.storeText(
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
context,
caret,
updatedRange,
type,
true,
!editor.isFirstCaret,
editor.isReversingCarets
)
) {
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
editor.deleteString(TextRange(newRange.first, newRange.second))
}
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
return false
}
/**
@@ -216,7 +226,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param caret The caret to start insertion in
* @param str The text to insert
*/
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret {
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret {
injector.application.runWriteAction {
(editor as MutableVimEditor).insertText(caret, offset, str)
}
@@ -391,7 +401,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param editor The editor to insert into
* @param context The data context
*/
override fun insertBeforeCursor(editor: VimEditor, context: ExecutionContext) {
override fun insertBeforeCaret(editor: VimEditor, context: ExecutionContext) {
initInsert(editor, context, Mode.INSERT)
}
@@ -407,7 +417,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param editor The editor to insert into
* @param context The data context
*/
override fun insertAfterCursor(editor: VimEditor, context: ExecutionContext) {
override fun insertAfterCaret(editor: VimEditor, context: ExecutionContext) {
for (caret in editor.nativeCarets()) {
caret.moveToMotion(injector.motion.getHorizontalMotion(editor, caret, 1, true))
}
@@ -771,7 +781,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
lambdaEditor.exitSelectModeNative(false)
KeyHandler.getInstance().reset(lambdaEditor)
if (isPrintableChar(key.keyChar) || activeTemplateWithLeftRightMotion(lambdaEditor, key)) {
injector.changeGroup.insertBeforeCursor(lambdaEditor, lambdaContext)
injector.changeGroup.insertBeforeCaret(lambdaEditor, lambdaContext)
}
}
}
@@ -1292,7 +1302,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (type === SelectionType.LINE_WISE) {
// Please don't use `getDocument().getText().isEmpty()` because it converts CharSequence into String
if (editor.fileSize() == 0L) {
insertBeforeCursor(editor, context)
insertBeforeCaret(editor, context)
} else if (after && !editor.endsWithNewLine()) {
insertNewLineBelow(editor, updatedCaret, lp.column)
} else {
@@ -1305,7 +1315,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
editor.vimChangeActionSwitchMode = Mode.INSERT
}
} else {
insertBeforeCursor(editor, context)
insertBeforeCaret(editor, context)
}
return true
}
@@ -1857,14 +1867,22 @@ abstract class VimChangeGroupBase : VimChangeGroup {
caret: VimCaret,
context: ExecutionContext?,
type: VimChangeGroup.ChangeCaseType,
argument: Argument,
argument: Argument.Motion,
operatorArguments: OperatorArguments,
): Boolean {
val range = injector.motion.getMotionRange(
editor, caret, context!!, argument,
operatorArguments
var range = injector.motion.getMotionRange(
editor, caret, context!!, argument, operatorArguments
)
return range != null && changeCaseRange(editor, caret, range, type)
if (range == null) return false
// If the motion is linewise, we need to adjust range.startOffset to match the observed Vim behavior
if (argument.isLinewiseMotion()) {
val pos = editor.offsetToBufferPosition(range.startOffset)
// The leftmost non-whitespace character OR the current caret position, whichever is closer to the left
val start = editor.getLeadingCharacterOffset(pos.line).coerceAtMost(caret.offset)
range = TextRange(start, range.endOffset)
}
return changeCaseRange(editor, caret, range, type)
}
/**
@@ -2030,7 +2048,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column)))
setInsertRepeat(lines, column, append)
}
insertBeforeCursor(editor, context)
insertBeforeCaret(editor, context)
return true
}

View File

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

View File

@@ -111,7 +111,8 @@ interface VimEditor {
* This method should perform caret merging after the operations. This is similar to IJ runForEachCaret
* TODO review
*/
val isFirstCaret: Boolean
val isReversingCarets: Boolean
fun forEachCaret(action: (VimCaret) -> Unit)
fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false)
fun isInForEachCaretScope(): Boolean
@@ -210,6 +211,7 @@ interface VimEditor {
fun createIndentBySize(size: Int): String
fun getFoldRegionAtOffset(offset: Int): VimFoldRegion?
fun getSoftWrapStartAtOffset(offset: Int): Int?
/**
* Mostly related to Fleet. After the editor is modified, the carets are modified. You can't use the old caret

View File

@@ -17,8 +17,6 @@ import com.maddyhome.idea.vim.common.VimListenersNotifier
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.register.VimRegisterGroupBase
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.vimscript.services.VariableService
import com.maddyhome.idea.vim.vimscript.services.VimVariableServiceBase
@@ -28,7 +26,6 @@ import com.maddyhome.idea.vim.yank.YankGroupBase
abstract class VimInjectorBase : VimInjector {
companion object {
val logger: VimLogger by lazy { vimLogger<VimInjectorBase>() }
val registerGroupStub: VimRegisterGroupBase by lazy { object : VimRegisterGroupBase() {} }
}
override val vimState: VimStateMachine = VimStateMachineImpl()
@@ -38,8 +35,6 @@ abstract class VimInjectorBase : VimInjector {
override val variableService: VariableService by lazy { object : VimVariableServiceBase() {} }
override val registerGroup: VimRegisterGroup by lazy { registerGroupStub }
override val registerGroupIfCreated: VimRegisterGroup? by lazy { registerGroupStub }
override val messages: VimMessages by lazy { VimMessagesStub() }
override val processGroup: VimProcessGroup by lazy { VimProcessGroupStub() }
override val application: VimApplication by lazy { VimApplicationStub() }

View File

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

View File

@@ -33,14 +33,18 @@ abstract class VimMotionGroupBase : VimMotionGroup {
override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T
override var lastFTChar: Char = ' '
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean): Motion {
val pos = caret.getVisualPosition()
if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) {
return Motion.Error
}
val intendedColumn = caret.vimLastColumn
val line = editor.normalizeVisualLine(pos.line + count)
val line = if (bufferLines)
// TODO Does not work with folds, but I don't use those.
editor.normalizeVisualLine(editor.bufferLineToVisualLine(editor.visualLineToBufferLine(pos.line) + count))
else
editor.normalizeVisualLine(pos.line + count)
if (intendedColumn == LAST_COLUMN) {
val normalisedColumn = editor.normalizeVisualColumn(
@@ -396,12 +400,11 @@ abstract class VimMotionGroupBase : VimMotionGroup {
// If we are a linewise motion we need to normalize the start and stop then move the start to the beginning
// of the line and move the end to the end of the line.
if (argument.isLinewiseMotion()) {
if (caret.getBufferPosition().line != editor.lineCount() - 1) {
start = editor.getLineStartForOffset(start)
end = min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt()
start = editor.getLineStartForOffset(start)
end = if (caret.getBufferPosition().line != editor.lineCount() - 1) {
min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt()
} else {
start = editor.getLineStartForOffset(start)
end = editor.getLineEndForOffset(end)
editor.getLineEndForOffset(end)
}
}

View File

@@ -206,4 +206,17 @@ interface VimSearchGroup {
* Returns true if any text is selected in the visible editors, false otherwise.
*/
fun isSomeTextHighlighted(): Boolean
/**
* Sets the last search state purely for tests
*
* @param pattern The pattern to save. This is the last search pattern, not the last substitute pattern
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search
*/
fun setLastSearchState(
pattern: String,
patternOffset: String,
direction: Direction,
)
}

View File

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

View File

@@ -53,15 +53,11 @@ sealed class Argument {
fun getMotionType() = if (isLinewiseMotion()) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE
fun isLinewiseMotion(): Boolean {
return motion.let {
when (it) {
is TextObjectActionHandler -> it.visualType == TextObjectVisualType.LINE_WISE
is MotionActionHandler -> it.motionType == MotionType.LINE_WISE
is ExternalActionHandler -> it.isLinewiseMotion
else -> error("Command is not a motion: $motion")
}
}
fun isLinewiseMotion(): Boolean = when (motion) {
is TextObjectActionHandler -> motion.visualType == TextObjectVisualType.LINE_WISE
is MotionActionHandler -> motion.motionType == MotionType.LINE_WISE
is ExternalActionHandler -> motion.isLinewiseMotion
else -> error("Command is not a motion: $motion")
}
fun withArgument(argument: Argument) = Motion(motion, argument)

View File

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

View File

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

View File

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

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