1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2024-11-25 07:42:59 +01:00

Compare commits

...

160 Commits

Author SHA1 Message Date
0612367c59
Set plugin version to chylex-23 2024-01-24 02:30:54 +01:00
755f05791a
Update search register when using f/t 2024-01-24 02:30:54 +01:00
2d170dd15b
Automatically add unambiguous imports after running a macro 2024-01-24 02:30:54 +01:00
943dffd43a
Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2024-01-24 02:30:54 +01:00
f17e99dd46
Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2024-01-24 02:30:54 +01:00
aa4caaa722
Fix(VIM-3166): Workaround to fix broken filtering of visual lines 2024-01-24 02:30:53 +01:00
4380b88cbd
Add support for count for visual and line motion surround 2024-01-24 02:30:53 +01:00
55ce038d51
Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2024-01-24 02:30:53 +01:00
deec7eef2e
Fix(VIM-696) Restore visual mode after undo/redo, and disable incompatible actions 2024-01-24 02:30:53 +01:00
2feffa9ff4
Revert(VIM-2884): Fix moving lines to cursor 2024-01-24 02:30:53 +01:00
f7f663f29a
Respect count with <Action> mappings 2024-01-24 02:30:53 +01:00
badbcd83d6
Add Matchit support for Java statements 2024-01-24 02:30:53 +01:00
d978901edf
Change matchit plugin to use HTML patterns in unrecognized files 2024-01-24 02:30:53 +01:00
08940fdaba
Reset insert mode when switching active editor 2024-01-24 02:30:52 +01:00
3a11fb9bd3
Remove update checker 2024-01-24 02:30:52 +01:00
fa9bb6adf4
Set custom plugin version 2024-01-24 02:30:52 +01:00
Filipp Vakhitov
fb75508258 Support widget themes 2024-01-23 23:59:57 +02:00
Filipp Vakhitov
0e69168382 Make the Apply button disabled by default 2024-01-23 18:54:47 +02:00
Filipp Vakhitov
9970ab8643 Allow to open only one widget settings window at a time 2024-01-23 18:53:41 +02:00
Filipp Vakhitov
7ff82010c3 Rename "Foreground:" field to "Text:" in mode widget settings 2024-01-23 16:47:21 +02:00
Filipp Vakhitov
1da8cd53d2 VIM-1377 Normal mode needs to be more obvious
Save mode widget colors state in XML
2024-01-23 01:27:57 +02:00
Filipp Vakhitov
9337a89eac VIM-1377 Normal mode needs to be more obvious
Redraw widget after applying new colors
2024-01-23 01:27:57 +02:00
Filipp Vakhitov
510564dd91 VIM-1377 Normal mode needs to be more obvious
Do not show widget with no files opened
2024-01-23 01:27:57 +02:00
Filipp Vakhitov
a9ededc997 VIM-1377 Normal mode needs to be more obvious
Add color customization to mode widget
2024-01-23 01:27:57 +02:00
Alex Plate
722cffbd48
[RIDER-85968] Do not format inserted code for CLion Nova
CLion Nova gets the  same problem with formatting as Rider has
2024-01-22 10:20:07 +04:00
Alex Plate
a787befd72
Add special esc processor for CLion Nova
CLion Nova has a similar architecture like Rider. So, it got the same problem like Rider has with the esc after adding the octopus handler.
2024-01-22 09:51:31 +04:00
Alex Plate
8ddd71a65a
Switch all releases to 2023.3.2 2024-01-18 10:03:07 +04:00
filipp
280e1ec16d Fix updating widget for cases when statusbar is not initialized 2024-01-17 11:15:54 +02:00
Filipp Vakhitov
52cf10cb2e Better widget 2024-01-13 23:01:01 +02:00
dependabot[bot]
c12082affc Bump io.ktor:ktor-client-content-negotiation from 2.3.6 to 2.3.7
Bumps [io.ktor:ktor-client-content-negotiation](https://github.com/ktorio/ktor) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.7/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.6...2.3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 17:59:13 +02:00
dependabot[bot]
c0d7d74dac Bump io.ktor:ktor-client-core from 2.3.6 to 2.3.7
Bumps [io.ktor:ktor-client-core](https://github.com/ktorio/ktor) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.7/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.6...2.3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 17:58:42 +02:00
Alex Plate
df72b24ad2
Wait smart mode before starting to create files 2024-01-09 17:34:03 +02:00
Alex Plate
26bdd15400
Do not try to turn off IdeaVim dialog as we don't show it anymore in UI tests 2024-01-08 18:52:09 +02:00
Alex Plate
e13310b4e0
Get rid of typing the action name 2024-01-08 18:51:30 +02:00
Alex Plate
e9d4218705
Try another way to search for the action 2024-01-08 17:13:31 +02:00
Alex Plate
56b80e4e60
Fix UI test with action search 2024-01-08 15:23:12 +02:00
Alex Plate
679f6471e6
Wait longer for the track action id action 2024-01-05 20:16:13 +02:00
Alex Plate
984179695c
Explicitly specify the save version of action cache
GitHub shows a security notification about this dependency. It's also said that setting a v2 version is enough, however the notification still persists, so let's specify the full version
2024-01-05 20:08:37 +02:00
Alex Plate
5cca484a82
Do not use sample code for this case 2024-01-05 19:51:18 +02:00
Alex Plate
d91e2296b0
Fix incorrect version of the dependency 2024-01-05 19:37:54 +02:00
Alex Plate
59768c16e2
Wait for track action id test to appear in search results 2024-01-05 19:36:03 +02:00
Alex Plate
580efeae1a
Update version of the robot 2024-01-05 19:35:47 +02:00
Alex Plate
0a3b508c8a
Update versions of actions for ui tests 2024-01-05 19:19:55 +02:00
Alex Plate
5e2f590b76
Try to use gradle-build-action in UI tests 2024-01-05 19:00:36 +02:00
Alex Plate
ee94396afa
Double escape to exit multicaret is required 2024-01-05 18:53:22 +02:00
Alex Plate
98764b6356
Change the set up of sandbox idea 2024-01-05 18:37:34 +02:00
Alex Plate
f01cc4d0d0
Add UI test for enter in insert and select modes 2024-01-05 18:31:02 +02:00
Alex Plate
4c0f17429b
Get rid of function and clean up UI test 2024-01-05 18:15:16 +02:00
Alex Plate
6a2ae1c572
Increase the expand timeout for the tree. For some reason it doesn't open quickly during tests on GH 2024-01-05 18:10:53 +02:00
Alex Plate
a2681ce6cc
Add UI test for multicaret enter
For ticket VIM-3186
2024-01-05 18:01:23 +02:00
Alex Plate
4e43606932
GH actions: always store the execution of the test 2024-01-05 17:44:44 +02:00
Alex Plate
28c0c3207a
Add UI test for mappings on A-Enter and C-Enter
For ticket VIM-3190
2024-01-05 17:40:40 +02:00
Alex Plate
ecfa0e2b49
Fix incorrect reference for the test 2024-01-05 17:24:20 +02:00
Alex Plate
ec3122f320
Upload the logs of the sandbox 2024-01-05 17:21:19 +02:00
Alex Plate
7e4b4c973c
Add UI tests for adding new line above and below via action in normal mode
For ticket VIM-3190
2024-01-05 17:05:07 +02:00
Alex Plate
64753df2dd
Always store the execution of the test 2024-01-05 16:41:22 +02:00
Alex Plate
75b36ab886
Do not show tips on startup for UI tests 2024-01-05 16:34:50 +02:00
Alex Plate
208a78c748
Get rid of testing error 2024-01-05 16:34:24 +02:00
Alex Plate
027249c575
Incorrect import was used for video 2024-01-05 16:15:00 +02:00
Alex Plate
5ceb960205
Use junit 5 version of video-recorder 2024-01-05 15:56:07 +02:00
Alex Plate
1cea156c5a
Try to update the ffmpeg downloader 2024-01-05 15:41:53 +02:00
Alex Plate
e1efa1ecbc
Update copyright template 2024-01-05 15:41:53 +02:00
IdeaVim Bot
517de5e179 Update changelog after merging PR 2024-01-04 14:00:52 +00:00
Matt Ellis
825b62a2a9 Refactor to remove lazy properties 2024-01-04 15:58:36 +02:00
Matt Ellis
5ec817776c Use "vim" prefix for option keys 2024-01-04 15:58:36 +02:00
Matt Ellis
3ad0519add Extract initialisation strategies to new class 2024-01-04 15:58:36 +02:00
Matt Ellis
9868522341 Only calculate stack trace if logging is enabled 2024-01-04 15:58:36 +02:00
Matt Ellis
5b8d8c617f Improve type handling 2024-01-04 15:58:36 +02:00
Matt Ellis
a1f66061e3 Extract option storage to separate class 2024-01-04 15:58:36 +02:00
Matt Ellis
d8811933c9 Simplify resetting options for testing 2024-01-04 15:58:36 +02:00
Matt Ellis
c9864dde8d Extract parsed values cache 2024-01-04 15:58:36 +02:00
Matt Ellis
ca849d6649 Simplify API of OptionListenersImpl 2024-01-04 15:58:36 +02:00
Matt Ellis
95a2354a86 Fix issue where global value wasn't properly set 2024-01-04 15:58:36 +02:00
Matt Ellis
538e0ac48c Extract listener notification
Refactoring - no intentional changes in behaviour
2024-01-04 15:58:36 +02:00
Matt Ellis
1c17411f04 Add test for changing number global-local options
The local value is not unset, but set to a copy of the new value, so we need to make sure that we notify editors that are not "unset"
2024-01-04 15:58:36 +02:00
Matt Ellis
cbe0f89548 Extract listener registration to separate class
Refactoring - no intentional changes in behaviour
2024-01-04 15:58:36 +02:00
Matt Ellis
615b071dcb Rename methods for clarity 2024-01-04 15:58:36 +02:00
Filipp Vakhitov
2d74f121aa Set min width for widget 2024-01-04 10:45:59 +02:00
dependabot[bot]
f65c180b8f Bump io.ktor:ktor-serialization-kotlinx-json from 2.3.6 to 2.3.7
Bumps [io.ktor:ktor-serialization-kotlinx-json](https://github.com/ktorio/ktor) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.7/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.6...2.3.7)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-serialization-kotlinx-json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 17:42:36 +02:00
dependabot[bot]
eb389c472d Bump io.ktor:ktor-client-cio from 2.3.6 to 2.3.7
Bumps [io.ktor:ktor-client-cio](https://github.com/ktorio/ktor) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.7/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.6...2.3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 17:42:06 +02:00
dependabot[bot]
befdf08035 Bump com.google.devtools.ksp:symbol-processing-api
Bumps [com.google.devtools.ksp:symbol-processing-api](https://github.com/google/ksp) from 1.9.21-1.0.15 to 1.9.22-1.0.16.
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/1.9.21-1.0.15...1.9.22-1.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 17:28:58 +02:00
dependabot[bot]
7a43ac865e Bump org.jetbrains.kotlin:kotlin-stdlib from 1.9.21 to 1.9.22
Bumps [org.jetbrains.kotlin:kotlin-stdlib](https://github.com/JetBrains/kotlin) from 1.9.21 to 1.9.22.
- [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/v1.9.21...v1.9.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 17:28:26 +02:00
dependabot[bot]
c43fcf9fbf Bump io.ktor:ktor-client-auth from 2.3.6 to 2.3.7
Bumps [io.ktor:ktor-client-auth](https://github.com/ktorio/ktor) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.7/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.6...2.3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 17:27:46 +02:00
IdeaVim Bot
472a633010 Update changelog after merging PR 2024-01-03 10:17:35 +00:00
Filipp Vakhitov
fc46acb2e4 Move to concurrent list 2024-01-03 12:15:31 +02:00
Filipp Vakhitov
7fde66eb40 Better color 2024-01-03 12:15:31 +02:00
Filipp Vakhitov
b3cea3997d Safer changes to VimPlugin
(avoid changes to old code that worked)
2024-01-03 12:15:31 +02:00
Filipp Vakhitov
2f20193086 Post-review improvements 2024-01-03 12:15:31 +02:00
filipp
601e207f04 Remove comment 2024-01-03 12:15:31 +02:00
filipp
f0d3d8b276 Add colors to showmode widget 2024-01-03 12:15:31 +02:00
Filipp Vakhitov
e02d34f023 Better ShowMode widget & Macro recording widget 2024-01-03 12:15:31 +02:00
Filipp Vakhitov
0504be84b6 Add base implementation of showmode widget 2024-01-03 12:15:31 +02:00
filipp
216f020b70 Add new listeners 2024-01-03 12:15:31 +02:00
IdeaVim Bot
66505eedfa Add Leonid Danilov to contributors list 2024-01-03 09:02:40 +00:00
Alex Plate
b307c7d88b
[VIM-2929]: Reset the key stack in case of exception during the execution 2024-01-02 13:57:12 +02:00
Alex Plate
47d4445fa8
Check the key stack at the end of every test 2024-01-02 13:57:12 +02:00
Alex Plate
7098d2633a
Add a helper function to key keystokes from string 2024-01-02 13:57:12 +02:00
IdeaVim Bot
61b5393b54 Update changelog after merging PR 2024-01-02 10:14:39 +00:00
Leonid Danilov
6fe2cf13b6 Added "Which-Key" to Plugins 2024-01-02 12:11:58 +02:00
dependabot[bot]
cc971eb2df Bump org.jetbrains.intellij from 1.16.0 to 1.16.1
Bumps org.jetbrains.intellij from 1.16.0 to 1.16.1.

---
updated-dependencies:
- dependency-name: org.jetbrains.intellij
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-28 12:45:26 +02:00
dependabot[bot]
a260987f5c Bump org.eclipse.jgit:org.eclipse.jgit.ssh.apache
Bumps org.eclipse.jgit:org.eclipse.jgit.ssh.apache from 6.7.0.202309050840-r to 6.8.0.202311291450-r.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-28 12:07:46 +02:00
dependabot[bot]
5eb8f44dfc Bump org.mockito.kotlin:mockito-kotlin from 5.1.0 to 5.2.1
Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 5.1.0 to 5.2.1.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/5.1.0...5.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-28 12:07:16 +02:00
Alex Plate
e36131b38b
[VIM-2929]: Adding logging for tracing the keyStack 2023-12-28 11:24:13 +02:00
Alex Plate
b67868afde
Extract the companion object into the top level
As the inspection says, due to eager companion object initialization, it's better to keep such things on the top level
2023-12-28 10:03:50 +02:00
Alex Plate
328fdee281
Add comment about autocompletion in macros 2023-12-22 10:55:16 +02:00
Matt Ellis
8ab43e98fe Remove unnecessary keeping visual mode flag
The value was only ever set to false.
2023-12-22 10:55:00 +02:00
Matt Ellis
4f407ccc03 Remove unused multikey-undo flag
It's uncertain what this was introduced for, and it's no longer used for any behaviour
2023-12-22 10:55:00 +02:00
Matt Ellis
5f3fddd3e4 Remove unnecessary post process method
We no longer need to post process the selection because it is up to the operator implementation to end in the correct mode
2023-12-22 10:55:00 +02:00
Matt Ellis
392f3b536d Remove unnecessary mode reset
Visual mode should already have been exited before executing the operator. The operator's implementation is responsible for handling the final mode
2023-12-22 10:55:00 +02:00
Matt Ellis
155de2b396 Remove always true check and always exit visual 2023-12-22 10:55:00 +02:00
Matt Ellis
6c9930ac2a Removes unnecessary 'exit visual' command flag
This flag is only used to modify the behaviour of visual operators, but all visual operators have the flag, which means it's unnecessary. The only behaviour for visual operators now is to exit visual mode.

Note that visual motions are implemented separately, and handle their own visual mode requirements (e.g. MotionArrowLeftAction).
2023-12-22 10:55:00 +02:00
Matt Ellis
9dddf4f4bc Minor cleanup 2023-12-22 10:55:00 +02:00
IdeaVim Bot
314506c15c Update changelog after merging PR 2023-12-22 07:34:37 +00:00
673222da6c Prevent code completion popup from appearing after running a macro 2023-12-22 09:32:33 +02:00
Alex Plate
58b97e6361
Get back to regular agents 2023-12-19 10:45:40 +02:00
Alex Plate
8bc2032b07
Do not override all artifact rules 2023-12-19 10:44:49 +02:00
Alex Plate
40d4354dfc
Avoid issue with blocked execution due to starting a coroutine in ProjectActivity 2023-12-19 10:30:14 +02:00
Alex Plate
27f2f5bb2b
Migrate KeymapChecker to ProjectActivity 2023-12-19 10:08:01 +02:00
Alex Plate
490b934269
Turn on leaks check on local development 2023-12-19 10:00:10 +02:00
Alex Plate
e2e2b4d176
Proper tear down with tests with mock 2023-12-19 09:59:54 +02:00
Alex Plate
7a1763bbee
Dispose carets of custom editor in test 2023-12-19 02:21:34 +02:00
Alex Plate
ca8904b6bb
Refactor common extension tests in order to avoid double remove of extension.
Firstly extension is removed in tearDown, then as disposable of VimPlugin.getInstance()
2023-12-19 02:21:18 +02:00
Alex Plate
6384b28689
Refactor listeners to avoid manual unregister
However, manual removal of listeners may cause "double" remove in cause the user turns off the plugin and then closes IDE: firstly listener is removed manually, and then by dispose call
2023-12-19 01:44:57 +02:00
Alex Plate
e661466558
Small refactorings on IdeaSelectionControl
They are done because if we don't set timer to null after tests, we have a leaked project
2023-12-19 00:12:54 +02:00
Alex Plate
8faf2beba4
Refactor IdeaRefactorModeHelper for splitting logic into change calculation and change apply 2023-12-19 00:12:54 +02:00
Alex Plate
fb29319ec6
Add VimPlugin.isNotEnabled function to simplify a lot of checks for !isEnabled() 2023-12-19 00:12:54 +02:00
Alex Plate
7779d7d193
This Easter egg caused a bug that a disposable balloon was leaked.
As smart people suggest, it's better not to have easter eggs at all.
2023-12-19 00:12:54 +02:00
Alex Plate
2c5246b62f
Avoid project leak via KeyEvent 2023-12-19 00:12:53 +02:00
Alex Plate
e43a3f4518
Avoid disposable leak because of widget
With the call that was removed, we initialized the widget too early, and the widget wasn't properly registered as disposable. This caused disposable leak.
Also, there is no understanding why this code was used to update the widget. The call for ShowCmd.update seems enough
2023-12-19 00:12:53 +02:00
Alex Plate
b5716f7a6d
Fix incorrect error handling
Since TestLoggerAssertionError is not available in production, we can't catch this exception in production code
2023-12-19 00:12:53 +02:00
Alex Plate
fac5a3cc6f
Remove XYZ testing configuration 2023-12-19 00:12:53 +02:00
Alex Plate
671793078a
Revert "Downgrade version of IJ plugin"
This reverts commit 258203f400.
2023-12-19 00:12:53 +02:00
Alex Plate
4f5ea1696f
Revert "Add back-to-232 branch"
This reverts commit 20832f11b6.
2023-12-19 00:12:53 +02:00
Alex Plate
b3e47e3bac
Revert "Disable some tests"
This reverts commit 95838d045d.
2023-12-19 00:12:52 +02:00
IdeaVim Bot
d67e990065 Update changelog. Action id - 7231244078 2023-12-16 10:06:23 +00:00
Alex Plate
7fb6f4b47f
Revert "Refactor key cache"
This reverts commit e159866d3b.
2023-12-15 18:49:18 +02:00
Alex Plate
df3b435a1f
Revert "Clean swing timer"
This reverts commit 5b65f1b544.
2023-12-15 18:49:18 +02:00
Alex Plate
5b65f1b544
Clean swing timer 2023-12-15 18:43:49 +02:00
Alex Plate
e159866d3b
Refactor key cache 2023-12-15 18:40:04 +02:00
Alex Plate
aa0ce71612
Temporally switch to larger agents 2023-12-15 18:24:31 +02:00
Alex Plate
522e547f99
Clean up patch 2023-12-15 17:45:01 +02:00
Alex Plate
9430341d4e
Add artifact rules for all builds 2023-12-15 17:42:08 +02:00
Alex Plate
95838d045d
Disable some tests 2023-12-15 17:17:01 +02:00
Alex Plate
20832f11b6
Add back-to-232 branch 2023-12-15 17:01:08 +02:00
Alex Plate
258203f400
Downgrade version of IJ plugin 2023-12-15 16:52:59 +02:00
Alex Plate
3b1768fa4e
Update new build configuration name 2023-12-15 16:41:48 +02:00
Alex Plate
23a3085bad
Add xyz branch for testing 2023-12-15 16:34:38 +02:00
Alex Plate
78c12e0ea6
Switch to stable version of IDEA 2023-12-15 16:34:38 +02:00
Alex Plate
997cb85663
Do not log LOG.error during test execution 2023-12-15 16:34:37 +02:00
aleksei.plate@jetbrains.com
968d5eabfa TeamCity change in 'Ideavim' project: general settings of 'Tests for IntelliJ Latest EAP' build configuration were updated 2023-12-15 14:26:47 +00:00
aleksei.plate@jetbrains.com
590ce1f7ed TeamCity change in 'Ideavim' project: runners of 'Tests for IntelliJ Latest EAP' build configuration were updated 2023-12-15 14:19:03 +00:00
aleksei.plate@jetbrains.com
416a8838e4 TeamCity change in 'Ideavim' project: runners of 'Tests for IntelliJ Latest EAP' build configuration were updated 2023-12-15 14:16:39 +00:00
aleksei.plate@jetbrains.com
f6c349ac31 TeamCity change in 'Ideavim' project: runners of 'Tests for IntelliJ Latest EAP' build configuration were updated 2023-12-15 14:15:47 +00:00
Alex Plate
517c6b40b5
Fix issue with disposed editor
If we process a focus change event, there is a chance that the editor is already disposed
2023-12-15 14:51:27 +02:00
Alex Plate
1fa78935a6
Factor disposable objects on editor opening 2023-12-15 14:28:18 +02:00
Alex Plate
4ddcd56740
Fix(VIM-3085): Open access to VimTypedActionHandler and VimShortcutKeyAction 2023-12-15 12:46:35 +02:00
aleksei.plate@jetbrains.com
e5a2f33584 TeamCity change in 'Ideavim' project: general settings of 'Tests for IntelliJ Latest EAP' build configuration were updated 2023-12-08 16:58:34 +00:00
aleksei.plate@jetbrains.com
c17cf3256a TeamCity change in 'Ideavim' project: project settings were updated 2023-12-08 16:50:30 +00:00
aleksei.plate@jetbrains.com
5415bda02d TeamCity change in 'Ideavim' project: general settings of 'Tests for IntelliJ Latest EAP' build configuration were updated 2023-12-08 16:45:24 +00:00
Alex Plate
07cbaeb7aa
Add 2023.3 test on TeamCity dashboard 2023-12-08 18:23:23 +02:00
159 changed files with 3333 additions and 1203 deletions

1
.gitattributes vendored Normal file
View File

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

View File

@ -8,18 +8,20 @@ jobs:
if: github.repository == 'JetBrains/ideavim' if: github.repository == 'JetBrains/ideavim'
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v2.1.0 uses: actions/setup-java@v4
with: with:
distribution: zulu distribution: zulu
java-version: 11 java-version: 11
- name: Setup FFmpeg - name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v1 uses: FedericoCarboni/setup-ffmpeg@v3
with: with:
# Not strictly necessary, but it may prevent rate limit # Not strictly necessary, but it may prevent rate limit
# errors especially on GitHub-hosted macos machines. # errors especially on GitHub-hosted macos machines.
token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.4.2
- name: Build Plugin - name: Build Plugin
run: gradle :buildPlugin run: gradle :buildPlugin
- name: Run Idea - name: Run Idea
@ -27,7 +29,7 @@ jobs:
mkdir -p build/reports mkdir -p build/reports
gradle :runIdeForUiTests > build/reports/idea.log & gradle :runIdeForUiTests > build/reports/idea.log &
- name: Wait for Idea started - name: Wait for Idea started
uses: jtalk/url-health-check-action@1.5 uses: jtalk/url-health-check-action@v3
with: with:
url: http://127.0.0.1:8082 url: http://127.0.0.1:8082
max-attempts: 20 max-attempts: 20
@ -35,15 +37,19 @@ jobs:
- name: Tests - name: Tests
run: gradle :testUi run: gradle :testUi
- name: Move video - name: Move video
if: ${{ failure() }} if: always()
run: mv video build/reports run: mv video build/reports
- name: Save fails report - name: Move sandbox logs
if: ${{ failure() }} if: always()
uses: actions/upload-artifact@v2 run: mv build/idea-sandbox/system/log sandbox-idea-log
- name: Save report
if: always()
uses: actions/upload-artifact@v4
with: with:
name: ui-test-fails-report-mac name: ui-test-fails-report-mac
path: | path: |
build/reports build/reports
sandbox-idea-log
# build-for-ui-test-linux: # build-for-ui-test-linux:
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# steps: # steps:

View File

@ -6,6 +6,7 @@
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</value> </value>
</option> </option>
<option name="LINE_SEPARATOR" value="&#10;" />
<JavaCodeStyleSettings> <JavaCodeStyleSettings>
<option name="FIELD_NAME_PREFIX" value="my" /> <option name="FIELD_NAME_PREFIX" value="my" />
<option name="STATIC_FIELD_NAME_PREFIX" value="our" /> <option name="STATIC_FIELD_NAME_PREFIX" value="our" />

View File

@ -1,6 +1,6 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<copyright> <copyright>
<option name="notice" value="Copyright 2003-2023 The IdeaVim authors&#10;&#10;Use of this source code is governed by an MIT-style&#10;license that can be found in the LICENSE.txt file or at&#10;https://opensource.org/licenses/MIT." /> <option name="notice" value="Copyright 2003-&amp;#36;today.year The IdeaVim authors&#10;&#10;Use of this source code is governed by an MIT-style&#10;license that can be found in the LICENSE.txt file or at&#10;https://opensource.org/licenses/MIT." />
<option name="myName" value="IdeaVim" /> <option name="myName" value="IdeaVim" />
</copyright> </copyright>
</component> </component>

View File

@ -5,14 +5,13 @@ object Constants {
const val EAP_CHANNEL = "eap" const val EAP_CHANNEL = "eap"
const val DEV_CHANNEL = "Dev" const val DEV_CHANNEL = "Dev"
// TODO it should be 2023.3 as soon as it releases const val GITHUB_TESTS = "2023.3.2"
const val GITHUB_TESTS = "LATEST-EAP-SNAPSHOT" const val NVIM_TESTS = "2023.3.2"
const val NVIM_TESTS = "LATEST-EAP-SNAPSHOT" const val PROPERTY_TESTS = "2023.3.2"
const val PROPERTY_TESTS = "LATEST-EAP-SNAPSHOT" const val LONG_RUNNING_TESTS = "2023.3.2"
const val LONG_RUNNING_TESTS = "LATEST-EAP-SNAPSHOT" const val QODANA_TESTS = "2023.3.2"
const val QODANA_TESTS = "LATEST-EAP-SNAPSHOT" const val RELEASE = "2023.3.2"
const val RELEASE = "LATEST-EAP-SNAPSHOT"
const val RELEASE_DEV = "LATEST-EAP-SNAPSHOT" const val RELEASE_DEV = "2023.3.2"
const val RELEASE_EAP = "LATEST-EAP-SNAPSHOT" const val RELEASE_EAP = "2023.3.2"
} }

View File

@ -24,6 +24,7 @@ object Project : Project({
// Active tests // Active tests
buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT")) buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(TestingBuildType("2023.3", "<default>", version = "2023.3"))
buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT")) buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(PropertyBased) buildType(PropertyBased)
@ -38,6 +39,11 @@ object Project : Project({
// Common build type for all configurations // Common build type for all configurations
abstract class IdeaVimBuildType(init: BuildType.() -> Unit) : BuildType({ abstract class IdeaVimBuildType(init: BuildType.() -> Unit) : BuildType({
artifactRules = """
+:build/reports => build/reports
+:/mnt/agent/temp/buildTmp/ => /mnt/agent/temp/buildTmp/
""".trimIndent()
init() init()
requirements { requirements {

View File

@ -0,0 +1,29 @@
package patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2019_2.RelativeId
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.GradleBuildStep
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
import jetbrains.buildServer.configs.kotlin.v2019_2.ui.changeBuildType
import jetbrains.buildServer.configs.kotlin.v2019_2.ui.expectSteps
import jetbrains.buildServer.configs.kotlin.v2019_2.ui.update
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with id = 'IdeaVimTests_Latest_EAP'
accordingly, and delete the patch script.
*/
changeBuildType(RelativeId("IdeaVimTests_Latest_EAP")) {
expectSteps {
gradle {
tasks = "clean test"
buildFile = ""
enableStacktrace = true
}
}
steps {
update<GradleBuildStep>(0) {
clearConditions()
jdkHome = "/usr/lib/jvm/java-17-amazon-corretto"
}
}
}

17
.teamcity/patches/projects/_Self.kts vendored Normal file
View File

@ -0,0 +1,17 @@
package patches.projects
import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.Project
import jetbrains.buildServer.configs.kotlin.v2019_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the root project
accordingly, and delete the patch script.
*/
changeProject(DslContext.projectId) {
check(description == "Vim engine for IDEs based on the IntelliJ platform") {
"Unexpected description: '$description'"
}
description = "Vim engine for JetBrains IDEs"
}

View File

@ -487,6 +487,10 @@ Contributors:
[![icon][github]](https://github.com/pWydmuch) [![icon][github]](https://github.com/pWydmuch)
&nbsp; &nbsp;
pWydmuch pWydmuch
* [![icon][mail]](mailto:leonid989@gmail.com)
[![icon][github]](https://github.com/Infonautica)
&nbsp;
Leonid Danilov
Previous contributors: Previous contributors:

View File

@ -43,9 +43,14 @@ usual beta standards.
* [VIM-3176](https://youtrack.jetbrains.com/issue/VIM-3176) Reselecting visual selection after pasting above it select wrong lines * [VIM-3176](https://youtrack.jetbrains.com/issue/VIM-3176) Reselecting visual selection after pasting above it select wrong lines
* [VIM-3206](https://youtrack.jetbrains.com/issue/VIM-3206) Disable both copilot suggestion and insert mode on a single escape * [VIM-3206](https://youtrack.jetbrains.com/issue/VIM-3206) Disable both copilot suggestion and insert mode on a single escape
* [VIM-3090](https://youtrack.jetbrains.com/issue/VIM-3090) Cmd line mode saves the visual mode * [VIM-3090](https://youtrack.jetbrains.com/issue/VIM-3090) Cmd line mode saves the visual mode
* [VIM-3085](https://youtrack.jetbrains.com/issue/VIM-3085) Open access to VimTypedActionHandler and VimShortcutKeyAction
### Merged PRs: ### Merged PRs:
* [763](https://github.com/JetBrains/ideavim/pull/763) by [Sam Ng](https://github.com/samabcde): Fix(VIM-3176) add test for restore selection after pasting in/below s… * [763](https://github.com/JetBrains/ideavim/pull/763) by [Sam Ng](https://github.com/samabcde): Fix(VIM-3176) add test for restore selection after pasting in/below s…
* [772](https://github.com/JetBrains/ideavim/pull/772) by [chylex](https://github.com/chylex): Prevent code completion popup from appearing after running a macro
* [787](https://github.com/JetBrains/ideavim/pull/787) by [Leonid Danilov](https://github.com/Infonautica): Added "Which-Key" to Plugins
* [778](https://github.com/JetBrains/ideavim/pull/778) by [lippfi](https://github.com/lippfi): Showmode
* [788](https://github.com/JetBrains/ideavim/pull/788) by [Matt Ellis](https://github.com/citizenmatt): Refactor VimOptionGroupBase
## 2.7.0, 2023-11-07 ## 2.7.0, 2023-11-07

View File

@ -21,7 +21,7 @@ repositories {
} }
dependencies { dependencies {
compileOnly("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15") compileOnly("com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.16")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion") { 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 // kotlin stdlib is provided by IJ, so there is no need to include it into the distribution
exclude("org.jetbrains.kotlin", "kotlin-stdlib") exclude("org.jetbrains.kotlin", "kotlin-stdlib")

View File

@ -49,14 +49,14 @@ buildscript {
classpath("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r") classpath("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
// This is needed for jgit to connect to ssh // This is needed for jgit to connect to ssh
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.7.0.202309050840-r") classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.8.0.202311291450-r")
classpath("org.kohsuke:github-api:1.305") classpath("org.kohsuke:github-api:1.305")
classpath("io.ktor:ktor-client-core:2.3.6") classpath("io.ktor:ktor-client-core:2.3.7")
classpath("io.ktor:ktor-client-cio:2.3.6") classpath("io.ktor:ktor-client-cio:2.3.7")
classpath("io.ktor:ktor-client-auth:2.3.6") classpath("io.ktor:ktor-client-auth:2.3.7")
classpath("io.ktor:ktor-client-content-negotiation:2.3.6") classpath("io.ktor:ktor-client-content-negotiation:2.3.7")
classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.6") classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
// This comes from the changelog plugin // This comes from the changelog plugin
// classpath("org.jetbrains:markdown:0.3.1") // classpath("org.jetbrains:markdown:0.3.1")
@ -69,7 +69,7 @@ plugins {
kotlin("jvm") version "1.8.21" kotlin("jvm") version "1.8.21"
application application
id("org.jetbrains.intellij") version "1.16.0" id("org.jetbrains.intellij") version "1.16.1"
id("org.jetbrains.changelog") version "2.2.0" id("org.jetbrains.changelog") version "2.2.0"
// ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
@ -126,11 +126,11 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
// https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin // https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion")
testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion")
testImplementation("com.automation-remarks:video-recorder-junit:2.0") testImplementation("com.automation-remarks:video-recorder-junit5:2.0")
runtimeOnly("org.antlr:antlr4-runtime:$antlrVersion") runtimeOnly("org.antlr:antlr4-runtime:$antlrVersion")
antlr("org.antlr:antlr4:$antlrVersion") antlr("org.antlr:antlr4:$antlrVersion")
@ -184,6 +184,14 @@ tasks {
include("**/*test.class") include("**/*test.class")
include("**/*Tests.class") include("**/*Tests.class")
exclude("**/ParserTest.class") exclude("**/ParserTest.class")
// Set teamcity env variable locally to run additional tests for leaks.
// By default, this test runs on TC only, but this test doesn't take a lot of time,
// so we can turn it on for local development
if (environment["TEAMCITY_VERSION"] == null) {
println("Set env TEAMCITY_VERSION to X")
environment("TEAMCITY_VERSION" to "X")
}
} }
val testWithNeovim by getting(Test::class) { val testWithNeovim by getting(Test::class) {
@ -294,6 +302,7 @@ tasks {
systemProperty("ide.mac.message.dialogs.as.sheets", "false") systemProperty("ide.mac.message.dialogs.as.sheets", "false")
systemProperty("jb.privacy.policy.text", "<!--999.999-->") systemProperty("jb.privacy.policy.text", "<!--999.999-->")
systemProperty("jb.consents.confirmation.enabled", "false") systemProperty("jb.consents.confirmation.enabled", "false")
systemProperty("ide.show.tips.on.startup.default.value", "false")
} }
runPluginVerifier { runPluginVerifier {
@ -344,8 +353,6 @@ tasks {
val pluginVersion = version val pluginVersion = version
// Don't forget to update plugin.xml // Don't forget to update plugin.xml
patchPluginXml { patchPluginXml {
sinceBuild.set("233.11799.30")
// Get the latest available change notes from the changelog file // Get the latest available change notes from the changelog file
changeNotes.set( changeNotes.set(
provider { provider {

View File

@ -396,3 +396,19 @@ Original plugin: [quick-scope](https://github.com/unblevable/quick-scope).
https://plugins.jetbrains.com/plugin/19417-ideavim-quickscope https://plugins.jetbrains.com/plugin/19417-ideavim-quickscope
</details> </details>
<details>
<summary><h2>Which-Key</h2></summary>
Original plugin: [vim-which-key](https://github.com/liuchengxu/vim-which-key).
### Setup:
- Install [Which-Key](https://plugins.jetbrains.com/plugin/15976-which-key) plugin.
- Add the following command to `~/.ideavimrc`: `set which-key`
### Instructions
https://github.com/TheBlob42/idea-which-key?tab=readme-ov-file#installation
</details>

View File

@ -8,14 +8,15 @@
# suppress inspection "UnusedProperty" for whole file # suppress inspection "UnusedProperty" for whole file
ideaVersion=LATEST-EAP-SNAPSHOT ideaVersion=2023.3.2
downloadIdeaSources=true downloadIdeaSources=true
instrumentPluginCode=true instrumentPluginCode=true
version=SNAPSHOT version=chylex-23
javaVersion=17 javaVersion=17
remoteRobotVersion=0.11.17 remoteRobotVersion=0.11.21
antlrVersion=4.10.1 antlrVersion=4.10.1
kotlin.incremental.useClasspathSnapshot=false
# Please don't forget to update kotlin version in buildscript section # Please don't forget to update kotlin version in buildscript section
# Also update kotlinxSerializationVersion version # Also update kotlinxSerializationVersion version

View File

@ -20,17 +20,17 @@ repositories {
} }
dependencies { dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:1.9.21") compileOnly("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
implementation("io.ktor:ktor-client-core:2.3.6") implementation("io.ktor:ktor-client-core:2.3.7")
implementation("io.ktor:ktor-client-cio:2.3.6") implementation("io.ktor:ktor-client-cio:2.3.7")
implementation("io.ktor:ktor-client-content-negotiation:2.3.6") implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.6") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
implementation("io.ktor:ktor-client-auth:2.3.6") implementation("io.ktor:ktor-client-auth:2.3.7")
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r") implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
// This is needed for jgit to connect to ssh // This is needed for jgit to connect to ssh
implementation("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.7.0.202309050840-r") implementation("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.8.0.202311291450-r")
implementation("com.vdurmont:semver4j:3.1.0") implementation("com.vdurmont:semver4j:3.1.0")
} }

View File

@ -219,6 +219,10 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
return getInstance().enabled; return getInstance().enabled;
} }
public static boolean isNotEnabled() {
return !isEnabled();
}
public static void setEnabled(final boolean enabled) { public static void setEnabled(final boolean enabled) {
if (isEnabled() == enabled) return; if (isEnabled() == enabled) return;
@ -232,6 +236,12 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
getInstance().turnOnPlugin(); getInstance().turnOnPlugin();
} }
if (enabled) {
VimInjectorKt.getInjector().getListenersNotifier().notifyPluginTurnedOn();
} else {
VimInjectorKt.getInjector().getListenersNotifier().notifyPluginTurnedOff();
}
StatusBarIconFactory.Util.INSTANCE.updateIcon(); StatusBarIconFactory.Util.INSTANCE.updateIcon();
} }

View File

@ -28,8 +28,11 @@ import javax.swing.KeyStroke
* Accepts all regular keystrokes and passes them on to the Vim key handler. * Accepts all regular keystrokes and passes them on to the Vim key handler.
* *
* IDE shortcut keys used by Vim commands are handled by [com.maddyhome.idea.vim.action.VimShortcutKeyAction]. * IDE shortcut keys used by Vim commands are handled by [com.maddyhome.idea.vim.action.VimShortcutKeyAction].
*
* This class is used in Which-Key plugin, so don't make it internal. Generally, we should provide a proper
* way to get ideavim keys for this plugin. See VIM-3085
*/ */
internal class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedActionHandlerEx { public class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedActionHandlerEx {
private val handler = KeyHandler.getInstance() private val handler = KeyHandler.getInstance()
private val traceTime = injector.globalOptions().ideatracetime private val traceTime = injector.globalOptions().ideatracetime
@ -86,7 +89,7 @@ internal class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedAct
} }
} }
companion object { internal companion object {
private val LOG = logger<VimTypedActionHandler>() private val LOG = logger<VimTypedActionHandler>()
} }
} }

View File

@ -54,8 +54,11 @@ import javax.swing.KeyStroke
* *
* *
* These keys are not passed to [com.maddyhome.idea.vim.VimTypedActionHandler] and should be handled by actions. * These keys are not passed to [com.maddyhome.idea.vim.VimTypedActionHandler] and should be handled by actions.
*
* This class is used in Which-Key plugin, so don't make it internal. Generally, we should provide a proper
* way to get ideavim keys for this plugin. See VIM-3085
*/ */
internal class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ { public class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
private val traceTime: Boolean private val traceTime: Boolean
get() { get() {
// Make sure the injector is initialized // Make sure the injector is initialized
@ -95,7 +98,7 @@ internal class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatib
// There is a chance that we can use BGT, but we call for isCell inside the update. // There is a chance that we can use BGT, but we call for isCell inside the update.
// Not sure if can can use BGT with this call. Let's use EDT for now. // Not sure if can can use BGT with this call. Let's use EDT for now.
override fun getActionUpdateThread() = ActionUpdateThread.EDT override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
val start = if (traceTime) System.currentTimeMillis() else null val start = if (traceTime) System.currentTimeMillis() else null
@ -110,7 +113,7 @@ internal class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatib
} }
private fun isEnabled(e: AnActionEvent, keyStroke: KeyStroke?): ActionEnableStatus { private fun isEnabled(e: AnActionEvent, keyStroke: KeyStroke?): ActionEnableStatus {
if (!VimPlugin.isEnabled()) return ActionEnableStatus.no("IdeaVim is disabled", LogLevel.DEBUG) if (VimPlugin.isNotEnabled()) return ActionEnableStatus.no("IdeaVim is disabled", LogLevel.DEBUG)
val editor = getEditor(e) val editor = getEditor(e)
if (editor != null && keyStroke != null) { if (editor != null && keyStroke != null) {
if (isOctopusEnabled(keyStroke, editor)) { if (isOctopusEnabled(keyStroke, editor)) {
@ -229,9 +232,9 @@ internal class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatib
/** /**
* getDefaultKeyStroke is needed for NEO layout keyboard VIM-987 * getDefaultKeyStroke is needed for NEO layout keyboard VIM-987
* but we should cache the value because on the second call (isEnabled -> actionPerformed) * but we should cache the value because on the second call (isEnabled -> actionPerformed)
* the event is already consumed * the event is already consumed and getDefaultKeyStroke returns null
*/ */
private var keyStrokeCache: Pair<KeyEvent?, KeyStroke?> = null to null private var keyStrokeCache: Pair<Long?, KeyStroke?> = null to null
private fun getKeyStroke(e: AnActionEvent): KeyStroke? { private fun getKeyStroke(e: AnActionEvent): KeyStroke? {
val inputEvent = e.inputEvent val inputEvent = e.inputEvent
@ -239,9 +242,9 @@ internal class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatib
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent) val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
val strokeCache = keyStrokeCache val strokeCache = keyStrokeCache
if (defaultKeyStroke != null) { if (defaultKeyStroke != null) {
keyStrokeCache = inputEvent to defaultKeyStroke keyStrokeCache = inputEvent.`when` to defaultKeyStroke
return defaultKeyStroke return defaultKeyStroke
} else if (strokeCache.first === inputEvent) { } else if (strokeCache.first == inputEvent.`when`) {
keyStrokeCache = null to null keyStrokeCache = null to null
return strokeCache.second return strokeCache.second
} }
@ -274,7 +277,7 @@ internal class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatib
.toSet() .toSet()
} }
companion object { internal companion object {
@JvmField @JvmField
val VIM_ONLY_EDITOR_KEYS: Set<KeyStroke> = val VIM_ONLY_EDITOR_KEYS: Set<KeyStroke> =
ImmutableSet.builder<KeyStroke>().addAll(getKeyStrokes(KeyEvent.VK_ENTER, 0)) ImmutableSet.builder<KeyStroke>().addAll(getKeyStrokes(KeyEvent.VK_ENTER, 0))

View File

@ -18,9 +18,7 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.setChangeMarks import com.maddyhome.idea.vim.api.setChangeMarks
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.argumentCaptured import com.maddyhome.idea.vim.common.argumentCaptured
import com.maddyhome.idea.vim.group.MotionGroup import com.maddyhome.idea.vim.group.MotionGroup
@ -28,10 +26,9 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.helper.vimStateMachine import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import java.util.* import com.maddyhome.idea.vim.state.mode.SelectionType
// todo make it multicaret // todo make it multicaret
private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: SelectionType): Boolean { private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: SelectionType): Boolean {
@ -104,8 +101,6 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
internal class VisualOperatorAction : VisualOperatorActionHandler.ForEachCaret() { internal class VisualOperatorAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_EXIT_VISUAL)
override fun executeAction( override fun executeAction(
editor: VimEditor, editor: VimEditor,
caret: VimCaret, caret: VimCaret,

View File

@ -14,13 +14,10 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.newapi.ijOptions import com.maddyhome.idea.vim.newapi.ijOptions
import java.util.*
/** /**
* @author vlan * @author vlan
@ -29,8 +26,6 @@ import java.util.*
public class DeleteJoinVisualLinesAction : VisualOperatorActionHandler.SingleExecution() { public class DeleteJoinVisualLinesAction : VisualOperatorActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.DELETE override val type: Command.Type = Command.Type.DELETE
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_EXIT_VISUAL)
override fun executeForAllCarets( override fun executeForAllCarets(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,

View File

@ -14,13 +14,10 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.newapi.ijOptions import com.maddyhome.idea.vim.newapi.ijOptions
import java.util.*
/** /**
* @author vlan * @author vlan
@ -29,8 +26,6 @@ import java.util.*
public class DeleteJoinVisualLinesSpacesAction : VisualOperatorActionHandler.SingleExecution() { public class DeleteJoinVisualLinesSpacesAction : VisualOperatorActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.DELETE override val type: Command.Type = Command.Type.DELETE
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_EXIT_VISUAL)
override fun executeForAllCarets( override fun executeForAllCarets(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,

View File

@ -9,6 +9,7 @@ package com.maddyhome.idea.vim.extension
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
@ -17,7 +18,6 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.common.CommandAlias import com.maddyhome.idea.vim.common.CommandAlias
import com.maddyhome.idea.vim.common.CommandAliasHandler import com.maddyhome.idea.vim.common.CommandAliasHandler
import com.maddyhome.idea.vim.helper.CommandLineHelper import com.maddyhome.idea.vim.helper.CommandLineHelper
@ -26,6 +26,7 @@ import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.MappingOwner import com.maddyhome.idea.vim.key.MappingOwner
import com.maddyhome.idea.vim.key.OperatorFunction import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.ui.ModalEntry import com.maddyhome.idea.vim.ui.ModalEntry
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@ -38,6 +39,9 @@ import javax.swing.KeyStroke
* @author vlan * @author vlan
*/ */
public object VimExtensionFacade { public object VimExtensionFacade {
private val LOG = logger<VimExtensionFacade>()
/** The 'map' command for mapping keys to handlers defined in extensions. */ /** The 'map' command for mapping keys to handlers defined in extensions. */
@JvmStatic @JvmStatic
public fun putExtensionHandlerMapping( public fun putExtensionHandlerMapping(
@ -140,10 +144,12 @@ public object VimExtensionFacade {
public fun inputKeyStroke(editor: Editor): KeyStroke { public fun inputKeyStroke(editor: Editor): KeyStroke {
if (editor.vim.vimStateMachine.isDotRepeatInProgress) { if (editor.vim.vimStateMachine.isDotRepeatInProgress) {
val input = Extension.consumeKeystroke() val input = Extension.consumeKeystroke()
LOG.trace("inputKeyStroke: dot repeat in progress. Input: $input")
return input ?: error("Not enough keystrokes saved: ${Extension.lastExtensionHandler}") return input ?: error("Not enough keystrokes saved: ${Extension.lastExtensionHandler}")
} }
val key: KeyStroke? = if (ApplicationManager.getApplication().isUnitTestMode) { val key: KeyStroke? = if (ApplicationManager.getApplication().isUnitTestMode) {
LOG.trace("Unit test mode is active")
val mappingStack = KeyHandler.getInstance().keyStack val mappingStack = KeyHandler.getInstance().keyStack
mappingStack.feedSomeStroke() ?: TestInputModel.getInstance(editor).nextKeyStroke()?.also { mappingStack.feedSomeStroke() ?: TestInputModel.getInstance(editor).nextKeyStroke()?.also {
if (editor.vim.vimStateMachine.isRecording) { if (editor.vim.vimStateMachine.isRecording) {
@ -151,11 +157,13 @@ public object VimExtensionFacade {
} }
} }
} else { } else {
LOG.trace("Getting char from the modal entry...")
var ref: KeyStroke? = null var ref: KeyStroke? = null
ModalEntry.activate(editor.vim) { stroke: KeyStroke? -> ModalEntry.activate(editor.vim) { stroke: KeyStroke? ->
ref = stroke ref = stroke
false false
} }
LOG.trace("Got char $ref")
ref ref
} }
val result = key ?: KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE.toChar()) val result = key ?: KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE.toChar())

View File

@ -156,11 +156,6 @@ internal class CommentaryExtension : VimExtension {
private class CommentaryOperatorHandler : OperatorFunction, ExtensionHandler { private class CommentaryOperatorHandler : OperatorFunction, ExtensionHandler {
override val isRepeatable = true override val isRepeatable = true
// In this operator we process selection by ourselves. This is necessary for rider, VIM-1758
override fun postProcessSelection(): Boolean {
return false
}
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
setOperatorFunction(this) setOperatorFunction(this)
executeNormalWithoutMapping(injector.parser.parseKeys("g@"), editor.ij) executeNormalWithoutMapping(injector.parser.parseKeys("g@"), editor.ij)

View File

@ -217,6 +217,8 @@ private object FileTypePatterns {
return if (fileTypeName in htmlLikeFileTypes) { return if (fileTypeName in htmlLikeFileTypes) {
this.htmlPatterns this.htmlPatterns
} else if (fileTypeName == "JAVA" || fileExtension == "java") {
this.javaPatterns
} else if (fileTypeName == "Ruby" || fileExtension == "rb") { } else if (fileTypeName == "Ruby" || fileExtension == "rb") {
this.rubyPatterns this.rubyPatterns
} else if (fileTypeName == "RHTML" || fileExtension == "erb") { } else if (fileTypeName == "RHTML" || fileExtension == "erb") {
@ -231,7 +233,7 @@ private object FileTypePatterns {
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") { } else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
this.cMakePatterns this.cMakePatterns
} else { } else {
return null this.htmlPatterns
} }
} }
@ -242,6 +244,7 @@ private object FileTypePatterns {
) )
private val htmlPatterns = createHtmlPatterns() private val htmlPatterns = createHtmlPatterns()
private val javaPatterns = createJavaPatterns()
private val rubyPatterns = createRubyPatterns() private val rubyPatterns = createRubyPatterns()
private val rubyAndHtmlPatterns = rubyPatterns + htmlPatterns private val rubyAndHtmlPatterns = rubyPatterns + htmlPatterns
private val phpPatterns = createPhpPatterns() private val phpPatterns = createPhpPatterns()
@ -271,6 +274,14 @@ private object FileTypePatterns {
) )
} }
private fun createJavaPatterns(): LanguagePatterns {
return (
LanguagePatterns("\\b(?<!else\\s+)if\\b", "\\belse\\s+if\\b", "\\belse(?!\\s+if)\\b") +
LanguagePatterns("\\bdo\\b", "\\bwhile\\b") +
LanguagePatterns("\\btry\\b", "\\bcatch\\b", "\\bfinally\\b")
)
}
private fun createRubyPatterns(): LanguagePatterns { private fun createRubyPatterns(): LanguagePatterns {
// Original patterns: https://github.com/vim/vim/blob/master/runtime/ftplugin/ruby.vim // Original patterns: https://github.com/vim/vim/blob/master/runtime/ftplugin/ruby.vim
// We use non-capturing groups (?:) since we don't need any back refs. The \\b marker takes care of word boundaries. // We use non-capturing groups (?:) since we don't need any back refs. The \\b marker takes care of word boundaries.

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

@ -8,20 +8,19 @@
package com.maddyhome.idea.vim.extension.surround package com.maddyhome.idea.vim.extension.surround
import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.endsWithNewLine import com.maddyhome.idea.vim.api.endsWithNewLine
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.setChangeMarks import com.maddyhome.idea.vim.api.setChangeMarks
import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.extension.VimExtension import com.maddyhome.idea.vim.extension.VimExtension
@ -33,12 +32,18 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMa
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing
import com.maddyhome.idea.vim.extension.VimExtensionFacade.setOperatorFunction import com.maddyhome.idea.vim.extension.VimExtensionFacade.setOperatorFunction
import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
import com.maddyhome.idea.vim.state.mode.mode import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
import com.maddyhome.idea.vim.key.OperatorFunction import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
import com.maddyhome.idea.vim.put.PutData import com.maddyhome.idea.vim.put.PutData
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.selectionType
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@ -79,7 +84,7 @@ internal class VimSurroundExtension : VimExtension {
override val isRepeatable = true override val isRepeatable = true
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
setOperatorFunction(Operator()) setOperatorFunction(Operator(supportsMultipleCursors = false, count = 1)) // TODO
executeNormalWithoutMapping(injector.parser.parseKeys("g@"), editor.ij) executeNormalWithoutMapping(injector.parser.parseKeys("g@"), editor.ij)
} }
} }
@ -100,7 +105,7 @@ internal class VimSurroundExtension : VimExtension {
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset) val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
if (lastNonWhiteSpaceOffset != null) { if (lastNonWhiteSpaceOffset != null) {
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1) val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
performSurround(pair, range, it) performSurround(pair, range, it, count = operatorArguments.count1)
} }
// it.moveToOffset(lineStartOffset) // it.moveToOffset(lineStartOffset)
} }
@ -120,15 +125,13 @@ internal class VimSurroundExtension : VimExtension {
private class VSurroundHandler : ExtensionHandler { private class VSurroundHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// NB: Operator ignores SelectionType anyway // NB: Operator ignores SelectionType anyway
if (!Operator().apply(editor, context, editor.mode.selectionType)) { if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
return return
} }
runWriteAction { runWriteAction {
// Leave visual mode // Leave visual mode
executeNormalWithoutMapping(injector.parser.parseKeys("<Esc>"), editor.ij) executeNormalWithoutMapping(injector.parser.parseKeys("<Esc>"), editor.ij)
editor.ij.caretModel.moveToOffset(selectionStart)
} }
} }
} }
@ -149,6 +152,10 @@ internal class VimSurroundExtension : VimExtension {
companion object { companion object {
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: Pair<String, String>?) { fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: Pair<String, String>?) {
editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) }
}
fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: Pair<String, String>?) {
// Save old register values for carets // Save old register values for carets
val surroundings = editor.sortedCarets() val surroundings = editor.sortedCarets()
.map { .map {
@ -249,27 +256,49 @@ internal class VimSurroundExtension : VimExtension {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
// Deleting surround is just changing the surrounding to "nothing" // Deleting surround is just changing the surrounding to "nothing"
val charFrom = getChar(editor.ij) val charFrom = getChar(editor.ij)
LOG.debug("DSurroundHandler: charFrom = $charFrom")
if (charFrom.code == 0) return if (charFrom.code == 0) return
runWriteAction { CSurroundHandler.change(editor, context, charFrom, null) } runWriteAction { CSurroundHandler.change(editor, context, charFrom, null) }
} }
} }
private class Operator : OperatorFunction { private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean { override fun apply(vimEditor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val ijEditor = editor.ij val editor = vimEditor.ij
val c = getChar(ijEditor) val c = getChar(editor)
if (c.code == 0) return true if (c.code == 0) return true
val pair = getOrInputPair(c, ijEditor) ?: return false val pair = getOrInputPair(c, editor) ?: return false
// XXX: Will it work with line-wise or block-wise selections?
val range = getSurroundRange(editor.currentCaret()) ?: return false runWriteAction {
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE) val change = VimPlugin.getChange()
if (supportsMultipleCursors) {
editor.runWithEveryCaretAndRestore {
applyOnce(editor, change, pair, count)
}
}
else {
applyOnce(editor, change, pair, count)
// Jump back to start // Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor) executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor)
}
}
return true return true
} }
private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: Pair<String, String>, count: Int) {
// XXX: Will it work with line-wise or block-wise selections?
val primaryCaret = editor.caretModel.primaryCaret
val range = getSurroundRange(primaryCaret.vim)
if (range != null) {
val start = RepeatedCharSequence.of(pair.first, count)
val end = RepeatedCharSequence.of(pair.second, count)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end)
}
}
private fun getSurroundRange(caret: VimCaret): TextRange? { private fun getSurroundRange(caret: VimCaret): TextRange? {
val editor = caret.editor val editor = caret.editor
val ijEditor = editor.ij val ijEditor = editor.ij
@ -280,13 +309,15 @@ internal class VimSurroundExtension : VimExtension {
} }
} }
} }
}
companion object { private val LOG = logger<VimSurroundExtension>()
private const val REGISTER = '"'
private val tagNameAndAttributesCapturePattern = "(\\S+)([^>]*)>".toPattern() private const val REGISTER = '"'
private val SURROUND_PAIRS = mapOf( private val tagNameAndAttributesCapturePattern = "(\\S+)([^>]*)>".toPattern()
private val SURROUND_PAIRS = mapOf(
'b' to ("(" to ")"), 'b' to ("(" to ")"),
'(' to ("( " to " )"), '(' to ("( " to " )"),
')' to ("(" to ")"), ')' to ("(" to ")"),
@ -299,18 +330,18 @@ internal class VimSurroundExtension : VimExtension {
'a' to ("<" to ">"), 'a' to ("<" to ">"),
'>' to ("<" to ">"), '>' to ("<" to ">"),
's' to (" " to ""), 's' to (" " to ""),
) )
private fun getSurroundPair(c: Char): Pair<String, String>? = if (c in SURROUND_PAIRS) { private fun getSurroundPair(c: Char): Pair<String, String>? = if (c in SURROUND_PAIRS) {
SURROUND_PAIRS[c] SURROUND_PAIRS[c]
} else if (!c.isLetter()) { } else if (!c.isLetter()) {
val s = c.toString() val s = c.toString()
s to s s to s
} else { } else {
null null
} }
private fun inputTagPair(editor: Editor): Pair<String, String>? { private fun inputTagPair(editor: Editor): Pair<String, String>? {
val tagInput = inputString(editor, "<", '>') val tagInput = inputString(editor, "<", '>')
val matcher = tagNameAndAttributesCapturePattern.matcher(tagInput) val matcher = tagNameAndAttributesCapturePattern.matcher(tagInput)
return if (matcher.find()) { return if (matcher.find()) {
@ -320,43 +351,45 @@ internal class VimSurroundExtension : VimExtension {
} else { } else {
null null
} }
} }
private fun inputFunctionName( private fun inputFunctionName(
editor: Editor, editor: Editor,
withInternalSpaces: Boolean, withInternalSpaces: Boolean,
): Pair<String, String>? { ): Pair<String, String>? {
val functionNameInput = inputString(editor, "function: ", null) val functionNameInput = inputString(editor, "function: ", null)
if (functionNameInput.isEmpty()) return null if (functionNameInput.isEmpty()) return null
return if (withInternalSpaces) "$functionNameInput( " to " )" else "$functionNameInput(" to ")" return if (withInternalSpaces) "$functionNameInput( " to " )" else "$functionNameInput(" to ")"
} }
private fun getOrInputPair(c: Char, editor: Editor): Pair<String, String>? = when (c) { private fun getOrInputPair(c: Char, editor: Editor): Pair<String, String>? = when (c) {
'<', 't' -> inputTagPair(editor) '<', 't' -> inputTagPair(editor)
'f' -> inputFunctionName(editor, false) 'f' -> inputFunctionName(editor, false)
'F' -> inputFunctionName(editor, true) 'F' -> inputFunctionName(editor, true)
else -> getSurroundPair(c) else -> getSurroundPair(c)
} }
private fun getChar(editor: Editor): Char { private fun getChar(editor: Editor): Char {
val key = inputKeyStroke(editor) val key = inputKeyStroke(editor)
val keyChar = key.keyChar val keyChar = key.keyChar
return if (keyChar == KeyEvent.CHAR_UNDEFINED || keyChar.code == KeyEvent.VK_ESCAPE) { val res = if (keyChar == KeyEvent.CHAR_UNDEFINED || keyChar.code == KeyEvent.VK_ESCAPE) {
0.toChar() 0.toChar()
} else { } else {
keyChar keyChar
} }
} LOG.trace("getChar: $res")
return res
}
private fun performSurround(pair: Pair<String, String>, range: TextRange, caret: VimCaret, tagsOnNewLines: Boolean = false) { private fun performSurround(pair: Pair<String, String>, range: TextRange, caret: VimCaret, count: Int, tagsOnNewLines: Boolean = false) {
runWriteAction { runWriteAction {
val editor = caret.editor val editor = caret.editor
val change = VimPlugin.getChange() val change = VimPlugin.getChange()
val leftSurround = pair.first + if (tagsOnNewLines) "\n" else "" val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
val isEOF = range.endOffset == editor.text().length val isEOF = range.endOffset == editor.text().length
val hasNewLine = editor.endsWithNewLine() val hasNewLine = editor.endsWithNewLine()
val rightSurround = if (tagsOnNewLines) { val rightSurround = (if (tagsOnNewLines) {
if (isEOF && !hasNewLine) { if (isEOF && !hasNewLine) {
"\n" + pair.second "\n" + pair.second
} else { } else {
@ -364,12 +397,13 @@ internal class VimSurroundExtension : VimExtension {
} }
} else { } else {
pair.second pair.second
} }).let { RepeatedCharSequence.of(it, count) }
change.insertText(editor, caret, range.startOffset, leftSurround) change.insertText(editor, caret, range.startOffset, leftSurround)
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround) change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)
injector.markService.setChangeMarks(caret, TextRange(range.startOffset, range.endOffset + leftSurround.length + rightSurround.length)) injector.markService.setChangeMarks(
} caret,
} TextRange(range.startOffset, range.endOffset + leftSurround.length + rightSurround.length)
)
} }
} }

View File

@ -20,9 +20,6 @@ import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.event.EditorMouseEvent import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.event.EditorMouseListener import com.intellij.openapi.editor.event.EditorMouseListener
import com.intellij.openapi.editor.impl.TextRangeInterval import com.intellij.openapi.editor.impl.TextRangeInterval
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.codeStyle.CodeStyleManager
@ -65,7 +62,6 @@ import com.maddyhome.idea.vim.helper.endOffsetInclusive
import com.maddyhome.idea.vim.helper.inInsertMode import com.maddyhome.idea.vim.helper.inInsertMode
import com.maddyhome.idea.vim.helper.moveToInlayAwareLogicalPosition import com.maddyhome.idea.vim.helper.moveToInlayAwareLogicalPosition
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
import com.maddyhome.idea.vim.icons.VimIcons
import com.maddyhome.idea.vim.key.KeyHandlerKeeper.Companion.getInstance import com.maddyhome.idea.vim.key.KeyHandlerKeeper.Companion.getInstance
import com.maddyhome.idea.vim.listener.VimInsertListener import com.maddyhome.idea.vim.listener.VimInsertListener
import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext
@ -89,7 +85,6 @@ import kotlin.math.min
*/ */
public class ChangeGroup : VimChangeGroupBase() { public class ChangeGroup : VimChangeGroupBase() {
private val insertListeners = ContainerUtil.createLockFreeCopyOnWriteList<VimInsertListener>() private val insertListeners = ContainerUtil.createLockFreeCopyOnWriteList<VimInsertListener>()
private var lastShownTime = 0L
private val listener: EditorMouseListener = object : EditorMouseListener { private val listener: EditorMouseListener = object : EditorMouseListener {
override fun mouseClicked(event: EditorMouseEvent) { override fun mouseClicked(event: EditorMouseEvent) {
val editor = event.editor val editor = event.editor
@ -103,10 +98,6 @@ public class ChangeGroup : VimChangeGroupBase() {
EventFacade.getInstance().addEditorMouseListener(editor!!, listener, disposable) EventFacade.getInstance().addEditorMouseListener(editor!!, listener, disposable)
} }
public fun editorReleased(editor: Editor?) {
EventFacade.getInstance().removeEditorMouseListener(editor!!, listener)
}
override fun type(vimEditor: VimEditor, context: ExecutionContext, key: Char) { override fun type(vimEditor: VimEditor, context: ExecutionContext, key: Char) {
val editor = (vimEditor as IjVimEditor).editor val editor = (vimEditor as IjVimEditor).editor
val ijContext = context.ij val ijContext = context.ij
@ -645,25 +636,6 @@ public class ChangeGroup : VimChangeGroupBase() {
avalanche: Boolean, avalanche: Boolean,
): Boolean { ): Boolean {
// Just an easter egg
if (avalanche) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastShownTime > 60000) {
lastShownTime = currentTime
ApplicationManager.getApplication().invokeLater {
val balloon = JBPopupFactory.getInstance()
.createHtmlTextBalloonBuilder(
"Wow, nice vim skills!", VimIcons.IDEAVIM,
MessageType.INFO.titleForeground, MessageType.INFO.popupBackground,
null
).createBalloon()
balloon.show(
JBPopupFactory.getInstance().guessBestPopupLocation((editor as IjVimEditor).editor),
Balloon.Position.below
)
}
}
}
val nf: List<String> = injector.options(editor).nrformats val nf: List<String> = injector.options(editor).nrformats
val alpha = nf.contains("alpha") val alpha = nf.contains("alpha")
val hex = nf.contains("hex") val hex = nf.contains("hex")

View File

@ -29,6 +29,8 @@ public open class GlobalIjOptions(scope: OptionAccessScope) : OptionsPropertiesB
public val lookupkeys: StringListOptionValue by optionProperty(IjOptions.lookupkeys) public val lookupkeys: StringListOptionValue by optionProperty(IjOptions.lookupkeys)
public var trackactionids: Boolean by optionProperty(IjOptions.trackactionids) public var trackactionids: Boolean by optionProperty(IjOptions.trackactionids)
public var visualdelay: Int by optionProperty(IjOptions.visualdelay) public var visualdelay: Int by optionProperty(IjOptions.visualdelay)
public var showmodewidget: Boolean by optionProperty(IjOptions.showmodewidget)
public var colorfulmodewidget: Boolean by optionProperty(IjOptions.colorfulmodewidget)
// Temporary options to control work-in-progress behaviour // Temporary options to control work-in-progress behaviour
public var oldundo: Boolean by optionProperty(IjOptions.oldundo) public var oldundo: Boolean by optionProperty(IjOptions.oldundo)

View File

@ -83,9 +83,11 @@ public object IjOptions {
public val trackactionids: ToggleOption = addOption(ToggleOption("trackactionids", GLOBAL, "tai", false)) public val trackactionids: ToggleOption = addOption(ToggleOption("trackactionids", GLOBAL, "tai", false))
public val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true)) public val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true))
public val visualdelay: UnsignedNumberOption = addOption(UnsignedNumberOption("visualdelay", GLOBAL, "visualdelay", 100)) public val visualdelay: UnsignedNumberOption = addOption(UnsignedNumberOption("visualdelay", GLOBAL, "visualdelay", 100))
public val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isTemporary = true)) public val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isTemporary = true))
public val vimscriptFunctionAnnotation: ToggleOption = addOption(ToggleOption("vimscriptfunctionannotation", GLOBAL, "vimscriptfunctionannotation", true, isTemporary = true)) public val vimscriptFunctionAnnotation: ToggleOption = addOption(ToggleOption("vimscriptfunctionannotation", GLOBAL, "vimscriptfunctionannotation", true, isTemporary = true))
public val commandOrMotionAnnotation: ToggleOption = addOption(ToggleOption("commandormotionannotation", GLOBAL, "commandormotionannotation", true, isTemporary = true)) public val commandOrMotionAnnotation: ToggleOption = addOption(ToggleOption("commandormotionannotation", GLOBAL, "commandormotionannotation", true, isTemporary = true))
public val showmodewidget: ToggleOption = addOption(ToggleOption("showmodewidget", GLOBAL, "showmodewidget", false, isTemporary = true))
public val colorfulmodewidget: ToggleOption = addOption(ToggleOption("colorfulmodewidget", GLOBAL, "colorfulmodewidget", false, isTemporary = true))
// This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which // This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which
// derives from Option<VimInt> // derives from Option<VimInt>

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

@ -7,6 +7,8 @@
*/ */
package com.maddyhome.idea.vim.group package com.maddyhome.idea.vim.group
import com.intellij.codeInsight.completion.CompletionPhase
import com.intellij.codeInsight.completion.impl.CompletionServiceImpl
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProcessCanceledException
@ -19,6 +21,7 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.MessageHelper.message import com.maddyhome.idea.vim.helper.MessageHelper.message
import com.maddyhome.idea.vim.macro.VimMacroBase import com.maddyhome.idea.vim.macro.VimMacroBase
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
/** /**
* Used to handle playback of macros * Used to handle playback of macros
@ -61,8 +64,10 @@ internal class MacroGroup : VimMacroBase() {
try { try {
myPotemkinProgress.text2 = if (isInternalMacro) "Executing internal macro" else "" myPotemkinProgress.text2 = if (isInternalMacro) "Executing internal macro" else ""
val runnable = runnable@{ val runnable = runnable@{
try {
// Handle one keystroke then queue up the next key // Handle one keystroke then queue up the next key
for (i in 0 until total) { for (i in 0 until total) {
try {
myPotemkinProgress.fraction = (i + 1).toDouble() / total myPotemkinProgress.fraction = (i + 1).toDouble() / total
while (keyStack.hasStroke()) { while (keyStack.hasStroke()) {
val key = keyStack.feedStroke() val key = keyStack.feedStroke()
@ -71,13 +76,25 @@ internal class MacroGroup : VimMacroBase() {
} catch (e: ProcessCanceledException) { } catch (e: ProcessCanceledException) {
return@runnable return@runnable
} }
ProgressManager.getInstance().executeNonCancelableSection { getInstance().handleKey(editor, key, context) } ProgressManager.getInstance().executeNonCancelableSection {
// Prevent autocompletion during macros.
// See https://github.com/JetBrains/ideavim/pull/772 for details
CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion)
getInstance().handleKey(editor, key, context)
}
if (injector.messages.isError()) return@runnable if (injector.messages.isError()) return@runnable
} }
} finally {
keyStack.resetFirst() keyStack.resetFirst()
} }
}
} finally {
keyStack.removeFirst() keyStack.removeFirst()
} }
if (!isInternalMacro) {
MacroAutoImport.run(editor.ij, context.ij)
}
}
if (isInternalMacro) { if (isInternalMacro) {
runnable() runnable()

View File

@ -48,9 +48,7 @@ import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.api.visualLineToBufferLine import com.maddyhome.idea.vim.api.visualLineToBufferLine
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.ExOutputModel import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion
@ -74,6 +72,8 @@ import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import org.jetbrains.annotations.Range import org.jetbrains.annotations.Range
import java.io.File import java.io.File
@ -461,6 +461,7 @@ internal class MotionGroup : VimMotionGroupBase() {
val fileEditor = event.oldEditor val fileEditor = event.oldEditor
if (fileEditor is TextEditor) { if (fileEditor is TextEditor) {
val editor = fileEditor.editor val editor = fileEditor.editor
if (!editor.isDisposed) {
ExOutputModel.getInstance(editor).clear() ExOutputModel.getInstance(editor).clear()
editor.vim.let { vimEditor -> editor.vim.let { vimEditor ->
if (VimStateMachine.getInstance(vimEditor).mode is Mode.VISUAL) { if (VimStateMachine.getInstance(vimEditor).mode is Mode.VISUAL) {
@ -471,4 +472,5 @@ internal class MotionGroup : VimMotionGroupBase() {
} }
} }
} }
}
} }

View File

@ -40,10 +40,6 @@ internal class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup {
override fun getGlobalIjOptions() = GlobalIjOptions(OptionAccessScope.GLOBAL(null)) override fun getGlobalIjOptions() = GlobalIjOptions(OptionAccessScope.GLOBAL(null))
override fun getEffectiveIjOptions(editor: VimEditor) = EffectiveIjOptions(OptionAccessScope.EFFECTIVE(editor)) override fun getEffectiveIjOptions(editor: VimEditor) = EffectiveIjOptions(OptionAccessScope.EFFECTIVE(editor))
private fun updateFallbackWindow(fallbackWindow: VimEditor, targetEditor: VimEditor) {
copyPerWindowGlobalValues(fallbackWindow, targetEditor)
}
companion object { companion object {
fun fileEditorManagerSelectionChangedCallback(event: FileEditorManagerEvent) { fun fileEditorManagerSelectionChangedCallback(event: FileEditorManagerEvent) {
// Vim only has one window, and it's not possible to close it. This means that editing a new file will always // Vim only has one window, and it's not possible to close it. This means that editing a new file will always
@ -58,6 +54,8 @@ internal class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup {
// Unfortunately, we can't reliably know if a closing editor is the selected editor. Instead, we rely on selection // Unfortunately, we can't reliably know if a closing editor is the selected editor. Instead, we rely on selection
// change events. If an editor is losing selection and there is no new selection, we can assume this means that // change events. If an editor is losing selection and there is no new selection, we can assume this means that
// the last editor has been closed, and use the closed editor to update the fallback window // the last editor has been closed, and use the closed editor to update the fallback window
//
// XXX: event.oldEditor will must probably return a disposed editor. So, it should be treated with care
if (event.newEditor == null) { if (event.newEditor == null) {
(event.oldEditor as? TextEditor)?.editor?.let { (event.oldEditor as? TextEditor)?.editor?.let {
(VimPlugin.getOptionGroup() as OptionGroup).updateFallbackWindow(injector.fallbackWindow, it.vim) (VimPlugin.getOptionGroup() as OptionGroup).updateFallbackWindow(injector.fallbackWindow, it.vim)
@ -68,7 +66,7 @@ internal class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup {
} }
internal class IjOptionConstants { internal class IjOptionConstants {
@Suppress("SpellCheckingInspection", "MemberVisibilityCanBePrivate") @Suppress("SpellCheckingInspection", "MemberVisibilityCanBePrivate", "ConstPropertyName")
companion object { companion object {
const val idearefactormode_keep = "keep" const val idearefactormode_keep = "keep"

View File

@ -192,8 +192,8 @@ public class SearchGroup extends VimSearchGroupBase implements PersistentStateCo
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}` * @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search * @param direction The direction to search
*/ */
@TestOnly @Override
public void setLastSearchState(@SuppressWarnings("unused") @NotNull Editor editor, @NotNull String pattern, public void setLastSearchState(@SuppressWarnings("unused") @NotNull VimEditor editor, @NotNull String pattern,
@NotNull String patternOffset, Direction direction) { @NotNull String patternOffset, Direction direction) {
setLastUsedPattern(pattern, RE_SEARCH, true); setLastUsedPattern(pattern, RE_SEARCH, true);
lastIgnoreSmartCase = false; lastIgnoreSmartCase = false;

View File

@ -205,7 +205,7 @@ internal class VimMarkServiceImpl : VimMarkServiceBase(), PersistentStateCompone
* @param event The change event * @param event The change event
*/ */
override fun beforeDocumentChange(event: DocumentEvent) { override fun beforeDocumentChange(event: DocumentEvent) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
if (logger.isDebugEnabled) logger.debug("MarkUpdater before, event = $event") if (logger.isDebugEnabled) logger.debug("MarkUpdater before, event = $event")
if (event.oldLength == 0) return if (event.oldLength == 0) return
val doc = event.document val doc = event.document
@ -221,7 +221,7 @@ internal class VimMarkServiceImpl : VimMarkServiceBase(), PersistentStateCompone
* @param event The change event * @param event The change event
*/ */
override fun documentChanged(event: DocumentEvent) { override fun documentChanged(event: DocumentEvent) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
if (logger.isDebugEnabled) logger.debug("MarkUpdater after, event = $event") if (logger.isDebugEnabled) logger.debug("MarkUpdater after, event = $event")
if (event.newLength == 0 || event.newLength == 1 && event.newFragment[0] != '\n') return if (event.newLength == 0 || event.newLength == 1 && event.newFragment[0] != '\n') return
val doc = event.document val doc = event.document
@ -242,7 +242,7 @@ internal class VimMarkServiceImpl : VimMarkServiceBase(), PersistentStateCompone
class VimBookmarksListener(private val myProject: Project) : BookmarksListener { class VimBookmarksListener(private val myProject: Project) : BookmarksListener {
override fun bookmarkAdded(group: BookmarkGroup, bookmark: Bookmark) { override fun bookmarkAdded(group: BookmarkGroup, bookmark: Bookmark) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
if (!injector.globalIjOptions().ideamarks) { if (!injector.globalIjOptions().ideamarks) {
return return
} }
@ -255,7 +255,7 @@ internal class VimMarkServiceImpl : VimMarkServiceBase(), PersistentStateCompone
} }
override fun bookmarkRemoved(group: BookmarkGroup, bookmark: Bookmark) { override fun bookmarkRemoved(group: BookmarkGroup, bookmark: Bookmark) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
if (!injector.globalIjOptions().ideamarks) { if (!injector.globalIjOptions().ideamarks) {
return return
} }

View File

@ -27,15 +27,12 @@ import com.maddyhome.idea.vim.api.getLineEndOffset
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.setChangeMarks import com.maddyhome.idea.vim.api.setChangeMarks
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.isBlock
import com.maddyhome.idea.vim.state.mode.isChar
import com.maddyhome.idea.vim.state.mode.isLine
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.RWLockLabel import com.maddyhome.idea.vim.helper.RWLockLabel
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import com.maddyhome.idea.vim.newapi.IjVimCaret import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
@ -48,6 +45,10 @@ import com.maddyhome.idea.vim.put.PutData
import com.maddyhome.idea.vim.put.VimPasteProvider import com.maddyhome.idea.vim.put.VimPasteProvider
import com.maddyhome.idea.vim.put.VimPutBase import com.maddyhome.idea.vim.put.VimPutBase
import com.maddyhome.idea.vim.register.RegisterConstants import com.maddyhome.idea.vim.register.RegisterConstants
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.isBlock
import com.maddyhome.idea.vim.state.mode.isChar
import com.maddyhome.idea.vim.state.mode.isLine
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
internal class PutGroup : VimPutBase() { internal class PutGroup : VimPutBase() {
@ -189,7 +190,7 @@ internal class PutGroup : VimPutBase() {
endOffset: Int, endOffset: Int,
): Int { ): Int {
// Temp fix for VIM-2808. Should be removed after rider will fix it's issues // Temp fix for VIM-2808. Should be removed after rider will fix it's issues
if (PlatformUtils.isRider()) return endOffset if (PlatformUtils.isRider() || isClionNova()) return endOffset
val startLine = editor.offsetToBufferPosition(startOffset).line val startLine = editor.offsetToBufferPosition(startOffset).line
val endLine = editor.offsetToBufferPosition(endOffset - 1).line val endLine = editor.offsetToBufferPosition(endOffset - 1).line

View File

@ -40,9 +40,15 @@ internal object IdeaSelectionControl {
* This method should be in sync with [predictMode] * This method should be in sync with [predictMode]
* *
* Control unexpected (non vim) selection change and adjust a mode to it. The new mode is not enabled immediately, * Control unexpected (non vim) selection change and adjust a mode to it. The new mode is not enabled immediately,
* but with some delay (using [VimVisualTimer]) * but with some delay (using [VimVisualTimer]). The delay is used because some platform functionality
* makes features by using selection. E.g. PyCharm unindent firstly select the indenting then applies delete action.
* Such "quick" selection breaks IdeaVim behaviour.
* *
* See [VimVisualTimer] to more info. * See [VimVisualTimer] to more info.
*
* XXX: This method can be split into "change calculation" and "change apply". In this way, we would be able
* to calculate if we need to make a change or not and reduce the number of these calls.
* If this refactoring ever is applied, please add `assertNull(VimVisualTimer.timer)` to `tearDown` of VimTestCase.
*/ */
fun controlNonVimSelectionChange( fun controlNonVimSelectionChange(
editor: Editor, editor: Editor,
@ -50,6 +56,7 @@ internal object IdeaSelectionControl {
) { ) {
VimVisualTimer.singleTask(editor.vim.mode) { initialMode -> VimVisualTimer.singleTask(editor.vim.mode) { initialMode ->
if (VimPlugin.isNotEnabled()) return@singleTask
if (editor.isIdeaVimDisabledHere) return@singleTask if (editor.isIdeaVimDisabledHere) return@singleTask
logger.debug("Adjust non-vim selection. Source: $selectionSource, initialMode: $initialMode") logger.debug("Adjust non-vim selection. Source: $selectionSource, initialMode: $initialMode")
@ -121,8 +128,9 @@ internal object IdeaSelectionControl {
} }
} }
private fun dontChangeMode(editor: Editor): Boolean = private fun dontChangeMode(editor: Editor): Boolean {
editor.isTemplateActive() && (editor.vim.isIdeaRefactorModeKeep || editor.vim.mode.hasVisualSelection) return editor.isTemplateActive() && (editor.vim.isIdeaRefactorModeKeep || editor.vim.mode.hasVisualSelection)
}
private fun chooseNonSelectionMode(editor: Editor): Mode { private fun chooseNonSelectionMode(editor: Editor): Mode {
val templateActive = editor.isTemplateActive() val templateActive = editor.isTemplateActive()

View File

@ -9,10 +9,10 @@
package com.maddyhome.idea.vim.group.visual package com.maddyhome.idea.vim.group.visual
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.group.visual.VimVisualTimer.mode import com.maddyhome.idea.vim.group.visual.VimVisualTimer.mode
import com.maddyhome.idea.vim.group.visual.VimVisualTimer.singleTask import com.maddyhome.idea.vim.group.visual.VimVisualTimer.singleTask
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.state.mode.Mode
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import javax.swing.Timer import javax.swing.Timer
@ -79,6 +79,11 @@ internal object VimVisualTimer {
} }
} }
fun drop() {
swingTimer?.stop()
swingTimer = null
}
inline fun timerAction(task: (initialMode: Mode?) -> Unit) { inline fun timerAction(task: (initialMode: Mode?) -> Unit) {
task(mode) task(mode)
swingTimer = null swingTimer = null

View File

@ -11,41 +11,68 @@ package com.maddyhome.idea.vim.handler
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.actionSystem.KeyboardShortcut
import com.intellij.openapi.actionSystem.Shortcut import com.intellij.openapi.actionSystem.Shortcut
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.keymap.Keymap import com.intellij.openapi.keymap.Keymap
import com.intellij.openapi.keymap.KeymapManagerListener import com.intellij.openapi.keymap.KeymapManagerListener
import com.intellij.openapi.keymap.ex.KeymapManagerEx import com.intellij.openapi.keymap.ex.KeymapManagerEx
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity import com.intellij.openapi.startup.ProjectActivity
import com.intellij.util.SingleAlarm
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.key import com.maddyhome.idea.vim.api.key
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import javax.swing.KeyStroke import javax.swing.KeyStroke
// We use alarm with delay to avoid many notifications in case many events are fired at the same time // We use alarm with delay to avoid many notifications in case many events are fired at the same time
// [VERSION UPDATE] 2023.3+ Replace SingleAlarm with coroutine flows https://youtrack.jetbrains.com/articles/IJPL-A-8/Alarm-Alternative internal val keyCheckRequests = MutableSharedFlow<Unit>(replay=1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
internal val keymapCheckRequester = SingleAlarm({ verifyKeymap() }, 5_000)
/** /**
* This checker verifies that the keymap has a correct configuration that is required for IdeaVim plugin * This checker verifies that the keymap has a correct configuration that is required for IdeaVim plugin
*/ */
internal class KeymapChecker : StartupActivity { internal class KeymapChecker : ProjectActivity {
override fun runActivity(project: Project) { override suspend fun execute(project: Project) {
keymapCheckRequester.request() project.service<KeymapCheckerService>().start()
keyCheckRequests.emit(Unit)
}
}
/**
* At the moment of release 2023.3 there is a problem that starting a coroutine like this
* right in the project activity will block this project activity in tests.
* To avoid that, there is an intermediate service that will allow to avoid this issue.
*
* However, in general we should start this coroutine right in the [KeymapChecker]
*/
@OptIn(FlowPreview::class)
@Service(Service.Level.PROJECT)
internal class KeymapCheckerService(private val cs: CoroutineScope) {
fun start() {
cs.launch {
keyCheckRequests
.debounce(5_000)
.collectLatest { verifyKeymap() }
}
} }
} }
internal class IdeaVimKeymapChangedListener : KeymapManagerListener { internal class IdeaVimKeymapChangedListener : KeymapManagerListener {
override fun activeKeymapChanged(keymap: Keymap?) { override fun activeKeymapChanged(keymap: Keymap?) {
keymapCheckRequester.request() check(keyCheckRequests.tryEmit(Unit))
} }
override fun shortcutChanged(keymap: Keymap, actionId: String) { override fun shortcutChanged(keymap: Keymap, actionId: String) {
keymapCheckRequester.request() check(keyCheckRequests.tryEmit(Unit))
} }
override fun shortcutChanged(keymap: Keymap, actionId: String, fromSettings: Boolean) { override fun shortcutChanged(keymap: Keymap, actionId: String, fromSettings: Boolean) {
keymapCheckRequester.request() check(keyCheckRequests.tryEmit(Unit))
} }
} }

View File

@ -125,7 +125,7 @@ internal abstract class OctopusHandler(private val nextHandler: EditorActionHand
} }
private fun isThisHandlerEnabled(editor: Editor, caret: Caret?, dataContext: DataContext?): Boolean { private fun isThisHandlerEnabled(editor: Editor, caret: Caret?, dataContext: DataContext?): Boolean {
if (!VimPlugin.isEnabled()) return false if (VimPlugin.isNotEnabled()) return false
if (!isHandlerEnabled(editor, dataContext)) return false if (!isHandlerEnabled(editor, dataContext)) return false
if (isNotActualKeyPress(dataContext)) return false if (isNotActualKeyPress(dataContext)) return false
return true return true
@ -229,7 +229,7 @@ internal class VimEscHandler(nextHandler: EditorActionHandler) : VimKeyHandler(n
} }
/** /**
* Rider uses a separate handler for esc to close the completion. IdeaOnlyEscapeHandlerAction is especially * Rider (and CLion Nova) uses a separate handler for esc to close the completion. IdeaOnlyEscapeHandlerAction is especially
* designer to get all the esc presses, and if there is a completion close it and do not pass the execution further. * designer to get all the esc presses, and if there is a completion close it and do not pass the execution further.
* This doesn't work the same as in IJ. * This doesn't work the same as in IJ.
* In IdeaVim, we'd like to exit insert mode on closing completion. This is a requirement as the change of this * In IdeaVim, we'd like to exit insert mode on closing completion. This is a requirement as the change of this

View File

@ -81,7 +81,7 @@ private fun Editor.guicursorMode(): GuiCursorMode {
private fun isBlockCursorOverride() = EditorSettingsExternalizable.getInstance().isBlockCursor private fun isBlockCursorOverride() = EditorSettingsExternalizable.getInstance().isBlockCursor
private fun Editor.updatePrimaryCaretVisualAttributes() { private fun Editor.updatePrimaryCaretVisualAttributes() {
if (!VimPlugin.isEnabled()) thisLogger().error("The caret attributes should not be updated if the IdeaVim is disabled") if (VimPlugin.isNotEnabled()) thisLogger().error("The caret attributes should not be updated if the IdeaVim is disabled")
caretModel.primaryCaret.visualAttributes = AttributesCache.getCaretVisualAttributes(this) caretModel.primaryCaret.visualAttributes = AttributesCache.getCaretVisualAttributes(this)
// Make sure the caret is visible as soon as it's set. It might be invisible while blinking // Make sure the caret is visible as soon as it's set. It might be invisible while blinking
@ -89,7 +89,7 @@ private fun Editor.updatePrimaryCaretVisualAttributes() {
} }
private fun Editor.updateSecondaryCaretsVisualAttributes() { private fun Editor.updateSecondaryCaretsVisualAttributes() {
if (!VimPlugin.isEnabled()) thisLogger().error("The caret attributes should not be updated if the IdeaVim is disabled") if (VimPlugin.isNotEnabled()) thisLogger().error("The caret attributes should not be updated if the IdeaVim is disabled")
// IntelliJ simulates visual block with multiple carets with selections. Do our best to hide them // IntelliJ simulates visual block with multiple carets with selections. Do our best to hide them
val attributes = if (this.vim.inBlockSelection) HIDDEN else AttributesCache.getCaretVisualAttributes(this) val attributes = if (this.vim.inBlockSelection) HIDDEN else AttributesCache.getCaretVisualAttributes(this)
this.caretModel.allCarets.forEach { this.caretModel.allCarets.forEach {

View File

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

View File

@ -12,6 +12,7 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
@ -19,6 +20,8 @@ import com.intellij.util.ui.table.JBTableRowEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import java.awt.Component import java.awt.Component
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JTable import javax.swing.JTable
@ -93,3 +96,41 @@ internal val Caret.vimLine: Int
*/ */
internal val Editor.vimLine: Int internal val Editor.vimLine: Int
get() = this.caretModel.currentCaret.vimLine get() = this.caretModel.currentCaret.vimLine
internal inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) {
val caretModel = this.caretModel
val carets = if (this.vim.inBlockSelection) null else caretModel.allCarets
if (carets == null || carets.size == 1) {
action()
}
else {
var initialDocumentSize = this.document.textLength
var documentSizeDifference = 0
val caretOffsets = carets.map { it.selectionStart to it.selectionEnd }
val restoredCarets = mutableListOf<CaretState>()
caretModel.removeSecondaryCarets()
for ((selectionStart, selectionEnd) in caretOffsets) {
if (selectionStart == selectionEnd) {
caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference)
}
else {
caretModel.primaryCaret.setSelection(
selectionStart + documentSizeDifference,
selectionEnd + documentSizeDifference
)
}
action()
restoredCarets.add(caretModel.caretsAndSelections.single())
val documentLength = this.document.textLength
documentSizeDifference += documentLength - initialDocumentSize
initialDocumentSize = documentLength
}
caretModel.caretsAndSelections = restoredCarets
}
}

View File

@ -110,7 +110,7 @@ internal fun Editor.isTemplateActive(): Boolean {
} }
private fun vimEnabled(editor: Editor?): Boolean { private fun vimEnabled(editor: Editor?): Boolean {
if (!VimPlugin.isEnabled()) return false if (VimPlugin.isNotEnabled()) return false
if (editor != null && editor.isIdeaVimDisabledHere) return false if (editor != null && editor.isIdeaVimDisabledHere) return false
return true return true
} }

View File

@ -14,7 +14,6 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.normalizeVisualColumn import com.maddyhome.idea.vim.api.normalizeVisualColumn
import com.maddyhome.idea.vim.api.options import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.helper.EditorHelper.getApproximateScreenHeight import com.maddyhome.idea.vim.helper.EditorHelper.getApproximateScreenHeight
import com.maddyhome.idea.vim.helper.EditorHelper.getApproximateScreenWidth import com.maddyhome.idea.vim.helper.EditorHelper.getApproximateScreenWidth
import com.maddyhome.idea.vim.helper.EditorHelper.getNonNormalizedVisualLineAtBottomOfScreen import com.maddyhome.idea.vim.helper.EditorHelper.getNonNormalizedVisualLineAtBottomOfScreen
@ -29,6 +28,7 @@ import com.maddyhome.idea.vim.helper.EditorHelper.scrollVisualLineToBottomOfScre
import com.maddyhome.idea.vim.helper.EditorHelper.scrollVisualLineToMiddleOfScreen import com.maddyhome.idea.vim.helper.EditorHelper.scrollVisualLineToMiddleOfScreen
import com.maddyhome.idea.vim.helper.EditorHelper.scrollVisualLineToTopOfScreen import com.maddyhome.idea.vim.helper.EditorHelper.scrollVisualLineToTopOfScreen
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.VimStateMachine
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -56,7 +56,7 @@ internal object ScrollViewHelper {
// that this needs to be replaced as a more or less dumb line for line rewrite. // that this needs to be replaced as a more or less dumb line for line rewrite.
val topLine = getVisualLineAtTopOfScreen(editor) val topLine = getVisualLineAtTopOfScreen(editor)
val bottomLine = getVisualLineAtBottomOfScreen(editor) val bottomLine = getVisualLineAtBottomOfScreen(editor)
val lastLine = vimEditor.getVisualLineCount() - 1 val lastLine = vimEditor.getVisualLineCount() + editor.settings.additionalLinesCount
// We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred // We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred
val scrollOffset = injector.options(vimEditor).scrolloff val scrollOffset = injector.options(vimEditor).scrolloff

View File

@ -14,6 +14,7 @@ import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
@ -21,6 +22,8 @@ import com.maddyhome.idea.vim.common.ChangesListener
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.undo.UndoRedoBase import com.maddyhome.idea.vim.undo.UndoRedoBase
/** /**
@ -39,6 +42,7 @@ internal class UndoRedoHelper : UndoRedoBase() {
if (injector.globalIjOptions().oldundo) { if (injector.globalIjOptions().oldundo) {
SelectionVimListenerSuppressor.lock().use { undoManager.undo(fileEditor) } SelectionVimListenerSuppressor.lock().use { undoManager.undo(fileEditor) }
restoreVisualMode(editor)
} else { } else {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking { editor.runWithChangeTracking {
@ -74,6 +78,7 @@ internal class UndoRedoHelper : UndoRedoBase() {
if (undoManager.isRedoAvailable(fileEditor)) { if (undoManager.isRedoAvailable(fileEditor)) {
if (injector.globalIjOptions().oldundo) { if (injector.globalIjOptions().oldundo) {
SelectionVimListenerSuppressor.lock().use { undoManager.redo(fileEditor) } SelectionVimListenerSuppressor.lock().use { undoManager.redo(fileEditor) }
restoreVisualMode(editor)
} else { } else {
undoManager.redo(fileEditor) undoManager.redo(fileEditor)
CommandProcessor.getInstance().runUndoTransparentAction { CommandProcessor.getInstance().runUndoTransparentAction {
@ -131,4 +136,21 @@ internal class UndoRedoHelper : UndoRedoBase() {
val hasChanges: Boolean val hasChanges: Boolean
get() = changeListener.hasChanged || initialPath != editor.getPath() get() = changeListener.hasChanged || initialPath != editor.getPath()
} }
private fun restoreVisualMode(editor: VimEditor) {
if (!editor.inVisualMode && editor.getSelectionModel().hasSelection()) {
val detectedMode = VimPlugin.getVisualMotion().autodetectVisualSubmode(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

@ -124,10 +124,6 @@ internal var Editor.vimMorePanel: ExOutputPanel? by userData()
internal var Editor.vimExOutput: ExOutputModel? by userData() internal var Editor.vimExOutput: ExOutputModel? by userData()
internal var Editor.vimTestInputModel: TestInputModel? by userData() internal var Editor.vimTestInputModel: TestInputModel? by userData()
/**
* Checks whether a keeping visual mode visual operator action is performed on editor.
*/
internal var Editor.vimKeepingVisualOperatorAction: Boolean by userDataOr { false }
internal var Editor.vimChangeActionSwitchMode: Mode? by userData() internal var Editor.vimChangeActionSwitchMode: Mode? by userData()
/** /**

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.isEnabled() || "dev" in VimPlugin.getVersion()
companion object {
private const val PROPERTY_NAME = "ideavim.statistics.timestamp"
val instance: VimStandalonePluginUpdateChecker = service()
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2003-2024 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.ide
import com.intellij.openapi.extensions.ExtensionPointName
internal val clionEP = ExtensionPointName.create<ClionNovaProvider>("IdeaVIM.clionNovaProvider")
internal interface ClionNovaProvider {
fun isClionNova(): Boolean
}
internal class ClionNovaProviderImpl : ClionNovaProvider {
override fun isClionNova(): Boolean = true
}
internal fun isClionNova(): Boolean {
return clionEP.extensions.any { it.isClionNova() }
}

View File

@ -40,7 +40,7 @@ internal object AppCodeTemplates {
private var editor: Editor? = null private var editor: Editor? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) { override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR) val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) { if (hostEditor != null) {
@ -49,7 +49,7 @@ internal object AppCodeTemplates {
} }
override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) { override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
if (ActionManager.getInstance().getId(action) == IdeActions.ACTION_CHOOSE_LOOKUP_ITEM) { if (ActionManager.getInstance().getId(action) == IdeActions.ACTION_CHOOSE_LOOKUP_ITEM) {
val myEditor = editor val myEditor = editor

View File

@ -59,7 +59,7 @@ internal object IdeaSpecifics {
private var completionPrevDocumentLength: Int? = null private var completionPrevDocumentLength: Int? = null
private var completionPrevDocumentOffset: Int? = null private var completionPrevDocumentOffset: Int? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) { override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR) val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) { if (hostEditor != null) {
@ -92,7 +92,7 @@ internal object IdeaSpecifics {
} }
override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) { override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
val editor = editor val editor = editor
if (editor != null && action is ChooseItemAction && editor.vimStateMachine?.isRecording == true) { if (editor != null && action is ChooseItemAction && editor.vimStateMachine?.isRecording == true) {
@ -138,7 +138,7 @@ internal object IdeaSpecifics {
//region Enter insert mode for surround templates without selection //region Enter insert mode for surround templates without selection
class VimTemplateManagerListener : TemplateManagerListener { class VimTemplateManagerListener : TemplateManagerListener {
override fun templateStarted(state: TemplateState) { override fun templateStarted(state: TemplateState) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
val editor = state.editor ?: return val editor = state.editor ?: return
state.addTemplateStateListener(object : TemplateEditingAdapter() { state.addTemplateStateListener(object : TemplateEditingAdapter() {
@ -176,7 +176,7 @@ internal object IdeaSpecifics {
//region Register shortcuts for lookup and perform partial reset //region Register shortcuts for lookup and perform partial reset
class LookupTopicListener : LookupManagerListener { class LookupTopicListener : LookupManagerListener {
override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) { override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
// Lookup opened // Lookup opened
if (oldLookup == null && newLookup is LookupImpl) { if (oldLookup == null && newLookup is LookupImpl) {
@ -199,7 +199,7 @@ internal object IdeaSpecifics {
//region Hide Vim search highlights when showing IntelliJ search results //region Hide Vim search highlights when showing IntelliJ search results
class VimFindModelListener : FindModelListener { class VimFindModelListener : FindModelListener {
override fun findNextModelChanged() { override fun findNextModelChanged() {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
VimPlugin.getSearch().clearSearchHighlight() VimPlugin.getSearch().clearSearchHighlight()
} }
} }

View File

@ -27,7 +27,7 @@ internal class RiderActionListener : AnActionListener {
private var editor: Editor? = null private var editor: Editor? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) { override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR) val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) { if (hostEditor != null) {
@ -36,7 +36,7 @@ internal class RiderActionListener : AnActionListener {
} }
override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) { override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
//region Extend Selection for Rider //region Extend Selection for Rider
when (ActionManager.getInstance().getId(action)) { when (ActionManager.getInstance().getId(action)) {

View File

@ -9,6 +9,7 @@
package com.maddyhome.idea.vim.listener package com.maddyhome.idea.vim.listener
import com.intellij.ide.ui.UISettings import com.intellij.ide.ui.UISettings
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.trace import com.intellij.openapi.diagnostic.trace
@ -28,8 +29,9 @@ import com.intellij.openapi.editor.event.EditorMouseMotionListener
import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionEvent
import com.intellij.openapi.editor.event.SelectionListener import com.intellij.openapi.editor.event.SelectionListener
import com.intellij.openapi.editor.ex.DocumentEx import com.intellij.openapi.editor.ex.DocumentEx
import com.intellij.openapi.editor.ex.EditorEventMulticasterEx
import com.intellij.openapi.editor.ex.FocusChangeListener
import com.intellij.openapi.editor.impl.EditorComponentImpl import com.intellij.openapi.editor.impl.EditorComponentImpl
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.FileEditorManagerListener
@ -40,14 +42,11 @@ import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
import com.intellij.openapi.fileEditor.impl.EditorComposite import com.intellij.openapi.fileEditor.impl.EditorComposite
import com.intellij.openapi.fileEditor.impl.EditorWindow import com.intellij.openapi.fileEditor.impl.EditorWindow
import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.rd.createLifetime
import com.intellij.openapi.rd.createNestedDisposable
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.removeUserData import com.intellij.openapi.util.removeUserData
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.ExceptionUtil import com.intellij.util.ExceptionUtil
import com.jetbrains.rd.util.lifetime.Lifetime
import com.maddyhome.idea.vim.EventFacade import com.maddyhome.idea.vim.EventFacade
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimKeyListener import com.maddyhome.idea.vim.VimKeyListener
@ -62,6 +61,7 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.ex.ExOutputModel import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.group.EditorGroup import com.maddyhome.idea.vim.group.EditorGroup
import com.maddyhome.idea.vim.group.FileGroup import com.maddyhome.idea.vim.group.FileGroup
import com.maddyhome.idea.vim.group.IjOptions
import com.maddyhome.idea.vim.group.MotionGroup import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.group.OptionGroup import com.maddyhome.idea.vim.group.OptionGroup
import com.maddyhome.idea.vim.group.ScrollGroup import com.maddyhome.idea.vim.group.ScrollGroup
@ -71,10 +71,9 @@ import com.maddyhome.idea.vim.group.visual.VimVisualTimer
import com.maddyhome.idea.vim.group.visual.moveCaretOneCharLeftFromSelectionEnd import com.maddyhome.idea.vim.group.visual.moveCaretOneCharLeftFromSelectionEnd
import com.maddyhome.idea.vim.group.visual.vimSetSystemSelectionSilently import com.maddyhome.idea.vim.group.visual.vimSetSystemSelectionSilently
import com.maddyhome.idea.vim.handler.correctorRequester import com.maddyhome.idea.vim.handler.correctorRequester
import com.maddyhome.idea.vim.handler.keymapCheckRequester import com.maddyhome.idea.vim.handler.keyCheckRequests
import com.maddyhome.idea.vim.helper.GuicursorChangeListener import com.maddyhome.idea.vim.helper.GuicursorChangeListener
import com.maddyhome.idea.vim.helper.StrictMode 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.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.forceBarCursor import com.maddyhome.idea.vim.helper.forceBarCursor
@ -91,11 +90,16 @@ import com.maddyhome.idea.vim.listener.MouseEventsDataHolder.skipNDragEvents
import com.maddyhome.idea.vim.listener.VimListenerManager.EditorListeners.add import com.maddyhome.idea.vim.listener.VimListenerManager.EditorListeners.add
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.mode import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.selectionType import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import com.maddyhome.idea.vim.ui.widgets.macro.macroWidgetOptionListener
import com.maddyhome.idea.vim.ui.widgets.mode.modeWidgetOptionListener
import com.maddyhome.idea.vim.vimDisposable
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@ -131,7 +135,7 @@ internal object VimListenerManager {
GlobalListeners.enable() GlobalListeners.enable()
EditorListeners.addAll() EditorListeners.addAll()
correctorRequester.request() correctorRequester.request()
keymapCheckRequester.request() check(keyCheckRequests.tryEmit(Unit))
} }
fun turnOff() { fun turnOff() {
@ -155,6 +159,13 @@ internal object VimListenerManager {
optionGroup.addEffectiveOptionValueChangeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE) optionGroup.addEffectiveOptionValueChangeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addEffectiveOptionValueChangeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener) optionGroup.addEffectiveOptionValueChangeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener)
optionGroup.addGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener) optionGroup.addGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
// This code is executed after ideavimrc execution, so we trigger onGlobalOptionChanged just in case
optionGroup.addGlobalOptionChangeListener(IjOptions.showmodewidget, modeWidgetOptionListener)
optionGroup.addGlobalOptionChangeListener(IjOptions.showmodewidget, macroWidgetOptionListener)
modeWidgetOptionListener.onGlobalOptionChanged()
macroWidgetOptionListener.onGlobalOptionChanged()
optionGroup.addEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener) optionGroup.addEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
EventFacade.getInstance().addEditorFactoryListener(VimEditorFactoryListener, VimPlugin.getInstance().onOffDisposable) EventFacade.getInstance().addEditorFactoryListener(VimEditorFactoryListener, VimPlugin.getInstance().onOffDisposable)
@ -162,6 +173,8 @@ internal object VimListenerManager {
busConnection.subscribe(FileOpenedSyncListener.TOPIC, VimEditorFactoryListener) busConnection.subscribe(FileOpenedSyncListener.TOPIC, VimEditorFactoryListener)
EditorFactory.getInstance().eventMulticaster.addCaretListener(VimCaretListener, VimPlugin.getInstance().onOffDisposable) EditorFactory.getInstance().eventMulticaster.addCaretListener(VimCaretListener, VimPlugin.getInstance().onOffDisposable)
val eventMulticaster = EditorFactory.getInstance().eventMulticaster as? EditorEventMulticasterEx
eventMulticaster?.addFocusChangeListener(VimFocusListener, VimPlugin.getInstance().onOffDisposable)
} }
fun disable() { fun disable() {
@ -172,6 +185,8 @@ internal object VimListenerManager {
optionGroup.removeEffectiveOptionValueChangeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE) optionGroup.removeEffectiveOptionValueChangeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeEffectiveOptionValueChangeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener) optionGroup.removeEffectiveOptionValueChangeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener)
optionGroup.removeGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener) optionGroup.removeGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.removeGlobalOptionChangeListener(IjOptions.showmodewidget, modeWidgetOptionListener)
optionGroup.removeGlobalOptionChangeListener(IjOptions.showmodewidget, macroWidgetOptionListener)
optionGroup.removeEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener) optionGroup.removeEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
} }
} }
@ -214,49 +229,67 @@ internal object VimListenerManager {
} }
fun add(editor: Editor, openingEditor: VimEditor, scenario: LocalOptionInitialisationScenario) { fun add(editor: Editor, openingEditor: VimEditor, scenario: LocalOptionInitialisationScenario) {
val pluginLifetime = VimPlugin.getInstance().createLifetime() // As I understand, there is no need to pass a disposable that also disposes on editor close
val editorLifetime = (editor as EditorImpl).disposable.createLifetime() // because all editor resources will be garbage collected anyway on editor close
val disposable = val disposable = editor.project?.vimDisposable ?: return
Lifetime.intersect(pluginLifetime, editorLifetime).createNestedDisposable("MyLifetimedDisposable")
val listenersDisposable = Disposer.newDisposable(disposable)
editor.putUserData(editorListenersDisposable, listenersDisposable)
Disposer.register(listenersDisposable) {
if (VimListenerTestObject.enabled) {
VimListenerTestObject.disposedCounter += 1
}
}
editor.contentComponent.addKeyListener(VimKeyListener) editor.contentComponent.addKeyListener(VimKeyListener)
Disposer.register(disposable) { editor.contentComponent.removeKeyListener(VimKeyListener) } Disposer.register(listenersDisposable) { editor.contentComponent.removeKeyListener(VimKeyListener) }
// Initialise the local options. We MUST do this before anything has the chance to query options // Initialise the local options. We MUST do this before anything has the chance to query options
VimPlugin.getOptionGroup().initialiseLocalOptions(editor.vim, openingEditor, scenario) val vimEditor = editor.vim
VimPlugin.getOptionGroup().initialiseLocalOptions(vimEditor, openingEditor, scenario)
val eventFacade = EventFacade.getInstance() val eventFacade = EventFacade.getInstance()
eventFacade.addEditorMouseListener(editor, EditorMouseHandler, disposable) eventFacade.addEditorMouseListener(editor, EditorMouseHandler, listenersDisposable)
eventFacade.addEditorMouseMotionListener(editor, EditorMouseHandler, disposable) eventFacade.addEditorMouseMotionListener(editor, EditorMouseHandler, listenersDisposable)
eventFacade.addEditorSelectionListener(editor, EditorSelectionHandler, disposable) eventFacade.addEditorSelectionListener(editor, EditorSelectionHandler, listenersDisposable)
eventFacade.addComponentMouseListener(editor.contentComponent, ComponentMouseListener, disposable) eventFacade.addComponentMouseListener(editor.contentComponent, ComponentMouseListener, listenersDisposable)
eventFacade.addCaretListener(editor, EditorCaretHandler, disposable) eventFacade.addCaretListener(editor, EditorCaretHandler, listenersDisposable)
VimPlugin.getEditor().editorCreated(editor) VimPlugin.getEditor().editorCreated(editor)
VimPlugin.getChange().editorCreated(editor, disposable) VimPlugin.getChange().editorCreated(editor, listenersDisposable)
Disposer.register(disposable) { injector.listenersNotifier.notifyEditorCreated(vimEditor)
Disposer.register(listenersDisposable) {
VimPlugin.getEditorIfCreated()?.editorDeinit(editor, true) VimPlugin.getEditorIfCreated()?.editorDeinit(editor, true)
} }
} }
fun remove(editor: Editor, isReleased: Boolean) { fun remove(editor: Editor, isReleased: Boolean) {
editor.contentComponent.removeKeyListener(VimKeyListener) val editorDisposable = editor.getUserData(editorListenersDisposable)
val eventFacade = EventFacade.getInstance() if (editorDisposable != null) {
eventFacade.removeEditorMouseListener(editor, EditorMouseHandler) Disposer.dispose(editorDisposable)
eventFacade.removeEditorMouseMotionListener(editor, EditorMouseHandler) }
eventFacade.removeEditorSelectionListener(editor, EditorSelectionHandler) else StrictMode.fail("Editor doesn't have disposable attached. $editor")
eventFacade.removeComponentMouseListener(editor.contentComponent, ComponentMouseListener)
eventFacade.removeCaretListener(editor, EditorCaretHandler)
VimPlugin.getEditorIfCreated()?.editorDeinit(editor, isReleased) VimPlugin.getEditorIfCreated()?.editorDeinit(editor, isReleased)
}
VimPlugin.getChange().editorReleased(editor)
} }
private object VimFocusListener : FocusChangeListener {
override fun focusGained(editor: Editor) {
injector.listenersNotifier.notifyEditorFocusGained(editor.vim)
} }
override fun focusLost(editor: Editor) {
injector.listenersNotifier.notifyEditorFocusLost(editor.vim)
}
}
val editorListenersDisposable = Key.create<Disposable>("IdeaVim listeners disposable")
object VimCaretListener : CaretListener { object VimCaretListener : CaretListener {
override fun caretAdded(event: CaretEvent) { override fun caretAdded(event: CaretEvent) {
if (vimDisabled(event.editor)) return if (vimDisabled(event.editor)) return
@ -271,7 +304,17 @@ internal object VimListenerManager {
class VimFileEditorManagerListener : FileEditorManagerListener { class VimFileEditorManagerListener : FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) { override fun selectionChanged(event: FileEditorManagerEvent) {
if (!VimPlugin.isEnabled()) return if (VimPlugin.isNotEnabled()) return
val newEditor = event.newEditor
if (newEditor is TextEditor) {
val editor = newEditor.editor
if (editor.isInsertMode) {
VimStateMachine.getInstance(editor).mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim)
}
}
MotionGroup.fileEditorManagerSelectionChangedCallback(event) MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroup.fileEditorManagerSelectionChangedCallback(event) FileGroup.fileEditorManagerSelectionChangedCallback(event)
SearchGroup.fileEditorManagerSelectionChangedCallback(event) SearchGroup.fileEditorManagerSelectionChangedCallback(event)
@ -336,18 +379,18 @@ internal object VimListenerManager {
event.editor.putUserData(openingEditorKey, OpeningEditor(openingEditor, owningEditorWindow, isPreview, canBeReused)) event.editor.putUserData(openingEditorKey, OpeningEditor(openingEditor, owningEditorWindow, isPreview, canBeReused))
} }
VimStandalonePluginUpdateChecker.instance.pluginUsed()
} }
override fun editorReleased(event: EditorFactoryEvent) { override fun editorReleased(event: EditorFactoryEvent) {
injector.markService.editorReleased(event.editor.vim) val vimEditor = event.editor.vim
injector.listenersNotifier.notifyEditorReleased(vimEditor)
injector.markService.editorReleased(vimEditor)
} }
override fun fileOpenedSync( override fun fileOpenedSync(
source: FileEditorManager, source: FileEditorManager,
file: VirtualFile, file: VirtualFile,
editorsWithProviders: List<FileEditorWithProvider> editorsWithProviders: List<FileEditorWithProvider>,
) { ) {
// This callback is called once all editors are created for a file being opened. The EditorComposite has been // This callback is called once all editors are created for a file being opened. The EditorComposite has been
// created (and the list of editors and providers is passed here) and added to an EditorWindow tab, inside a // created (and the list of editors and providers is passed here) and added to an EditorWindow tab, inside a
@ -405,6 +448,7 @@ internal object VimListenerManager {
*/ */
override fun selectionChanged(selectionEvent: SelectionEvent) { override fun selectionChanged(selectionEvent: SelectionEvent) {
if (selectionEvent.editor.isIdeaVimDisabledHere) return if (selectionEvent.editor.isIdeaVimDisabledHere) return
VimVisualTimer.drop()
val editor = selectionEvent.editor val editor = selectionEvent.editor
val document = editor.document val document = editor.document
val ijVimEditor = IjVimEditor(editor) val ijVimEditor = IjVimEditor(editor)
@ -698,6 +742,11 @@ internal object VimListenerManager {
} }
} }
internal object VimListenerTestObject {
var enabled: Boolean = false
var disposedCounter = 0
}
private object MouseEventsDataHolder { private object MouseEventsDataHolder {
const val skipEvents = 3 const val skipEvents = 3
var skipNDragEvents = skipEvents var skipNDragEvents = skipEvents

View File

@ -54,7 +54,6 @@ import com.maddyhome.idea.vim.helper.isTemplateActive
import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes
import com.maddyhome.idea.vim.helper.updateCaretsVisualPosition import com.maddyhome.idea.vim.helper.updateCaretsVisualPosition
import com.maddyhome.idea.vim.helper.vimChangeActionSwitchMode import com.maddyhome.idea.vim.helper.vimChangeActionSwitchMode
import com.maddyhome.idea.vim.helper.vimKeepingVisualOperatorAction
import com.maddyhome.idea.vim.helper.vimLastSelectionType import com.maddyhome.idea.vim.helper.vimLastSelectionType
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
@ -82,11 +81,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
set(value) { set(value) {
editor.vimChangeActionSwitchMode = value editor.vimChangeActionSwitchMode = value
} }
override var vimKeepingVisualOperatorAction: Boolean
get() = editor.vimKeepingVisualOperatorAction
set(value) {
editor.vimKeepingVisualOperatorAction = value
}
override fun fileSize(): Long = editor.fileSize.toLong() override fun fileSize(): Long = editor.fileSize.toLong()

View File

@ -18,6 +18,7 @@ import com.intellij.util.IJSwingUtilities;
import com.maddyhome.idea.vim.KeyHandler; import com.maddyhome.idea.vim.KeyHandler;
import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.ExecutionContext; import com.maddyhome.idea.vim.api.ExecutionContext;
import com.maddyhome.idea.vim.diagnostic.VimLogger;
import com.maddyhome.idea.vim.helper.HelperKt; import com.maddyhome.idea.vim.helper.HelperKt;
import com.maddyhome.idea.vim.helper.MessageHelper; import com.maddyhome.idea.vim.helper.MessageHelper;
import com.maddyhome.idea.vim.helper.UiHelper; import com.maddyhome.idea.vim.helper.UiHelper;
@ -59,6 +60,8 @@ public class ExOutputPanel extends JPanel {
private boolean myActive = false; private boolean myActive = false;
private static final VimLogger LOG = injector.getLogger(ExOutputPanel.class);
private ExOutputPanel(@NotNull Editor editor) { private ExOutputPanel(@NotNull Editor editor) {
myEditor = editor; myEditor = editor;
@ -299,6 +302,10 @@ public class ExOutputPanel extends JPanel {
final KeyStroke key = KeyStroke.getKeyStrokeForEvent(e); final KeyStroke key = KeyStroke.getKeyStrokeForEvent(e);
final List<KeyStroke> keys = new ArrayList<>(1); final List<KeyStroke> keys = new ArrayList<>(1);
keys.add(key); keys.add(key);
if (LOG.isTrace()) {
LOG.trace("Adding new keys to keyStack as part of playback. State before adding keys: " +
KeyHandler.getInstance().getKeyStack().dump());
}
KeyHandler.getInstance().getKeyStack().addKeys(keys); KeyHandler.getInstance().getKeyStack().addKeys(keys);
ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(myEditor), null); ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(myEditor), null);
VimPlugin.getMacro().playbackKeys(new IjVimEditor(myEditor), context, 1); VimPlugin.getMacro().playbackKeys(new IjVimEditor(myEditor), context, 1);
@ -358,7 +365,7 @@ public class ExOutputPanel extends JPanel {
public static class LafListener implements LafManagerListener { public static class LafListener implements LafManagerListener {
@Override @Override
public void lookAndFeelChanged(@NotNull LafManager source) { public void lookAndFeelChanged(@NotNull LafManager source) {
if (!VimPlugin.isEnabled()) return; if (VimPlugin.isNotEnabled()) return;
// Calls updateUI on this and child components // Calls updateUI on this and child components
for (Editor editor : HelperKt.localEditors()) { for (Editor editor : HelperKt.localEditors()) {
if (!ExOutputPanel.isPanelActive(editor)) continue; if (!ExOutputPanel.isPanelActive(editor)) continue;

View File

@ -8,6 +8,9 @@
package com.maddyhome.idea.vim.ui package com.maddyhome.idea.vim.ui
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.trace
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.helper.isCloseKeyStroke import com.maddyhome.idea.vim.helper.isCloseKeyStroke
@ -22,13 +25,19 @@ import javax.swing.KeyStroke
* @author dhleong * @author dhleong
*/ */
public object ModalEntry { public object ModalEntry {
public val LOG: Logger = logger<ModalEntry>()
public inline fun activate(editor: VimEditor, crossinline processor: (KeyStroke) -> Boolean) { public inline fun activate(editor: VimEditor, crossinline processor: (KeyStroke) -> Boolean) {
// Firstly we pull the unfinished keys of the current mapping // Firstly we pull the unfinished keys of the current mapping
val mappingStack = KeyHandler.getInstance().keyStack val mappingStack = KeyHandler.getInstance().keyStack
LOG.trace("Dumping key stack:")
LOG.trace { mappingStack.dump() }
var stroke = mappingStack.feedSomeStroke() var stroke = mappingStack.feedSomeStroke()
while (stroke != null) { while (stroke != null) {
val result = processor(stroke) val result = processor(stroke)
if (!result) { if (!result) {
LOG.trace("Got char from mapping stack")
return return
} }
stroke = mappingStack.feedSomeStroke() stroke = mappingStack.feedSomeStroke()
@ -55,6 +64,7 @@ public object ModalEntry {
KeyHandler.getInstance().modalEntryKeys += stroke KeyHandler.getInstance().modalEntryKeys += stroke
} }
if (!processor(stroke)) { if (!processor(stroke)) {
LOG.trace("Got char from keyboard input: $stroke. Event: $e")
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(this) KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(this)
loop.exit() loop.exit()
} }

View File

@ -19,7 +19,6 @@ import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.openapi.wm.WindowManager import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.impl.status.EditorBasedWidget import com.intellij.openapi.wm.impl.status.EditorBasedWidget
import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.intellij.util.Consumer import com.intellij.util.Consumer
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
@ -68,13 +67,6 @@ internal object ShowCmd {
internal object ShowCmdOptionChangeListener : GlobalOptionChangeListener { internal object ShowCmdOptionChangeListener : GlobalOptionChangeListener {
override fun onGlobalOptionChanged() { override fun onGlobalOptionChanged() {
ShowCmd.update() ShowCmd.update()
val extension = StatusBarWidgetFactory.EP_NAME.findExtension(ShowCmdStatusBarWidgetFactory::class.java) ?: return
val projectManager = ProjectManager.getInstanceIfCreated() ?: return
for (project in projectManager.openProjects) {
val statusBarWidgetsManager = project.getService(StatusBarWidgetsManager::class.java) ?: continue
statusBarWidgetsManager.updateWidget(extension)
}
} }
} }

View File

@ -453,7 +453,7 @@ public class ExEntryPanel extends JPanel {
public static class LafListener implements LafManagerListener { public static class LafListener implements LafManagerListener {
@Override @Override
public void lookAndFeelChanged(@NotNull LafManager source) { public void lookAndFeelChanged(@NotNull LafManager source) {
if (!VimPlugin.isEnabled()) return; if (VimPlugin.isNotEnabled()) return;
// Calls updateUI on this and child components // Calls updateUI on this and child components
if (ExEntryPanel.isInstanceWithShortcutsActive()) { if (ExEntryPanel.isInstanceWithShortcutsActive()) {
IJSwingUtilities.updateComponentTreeUI(ExEntryPanel.getInstance()); IJSwingUtilities.updateComponentTreeUI(ExEntryPanel.getInstance());

View File

@ -0,0 +1,31 @@
/*
* Copyright 2003-2024 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.ui.widgets
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.VimPluginListener
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
public class VimWidgetListener(private val updateWidget: Runnable) : GlobalOptionChangeListener, VimPluginListener {
init {
injector.listenersNotifier.vimPluginListeners.add(this)
}
override fun onGlobalOptionChanged() {
updateWidget.run()
}
override fun turnedOn() {
updateWidget.run()
}
override fun turnedOff() {
updateWidget.run()
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.ui.widgets.macro
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.MacroRecordingListener
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.ui.widgets.VimWidgetListener
import com.maddyhome.idea.vim.ui.widgets.mode.VimStatusBarWidget
import java.awt.Component
private const val ID = "IdeaVim::Macro"
internal class MacroWidgetFactory : StatusBarWidgetFactory, VimStatusBarWidget {
private var content: String = ""
private val macroRecordingListener = object : MacroRecordingListener {
override fun recordingStarted(editor: VimEditor, register: Char) {
content = "recording @$register"
updateWidgetInStatusBar(ID, editor.ij.project)
}
override fun recordingFinished(editor: VimEditor, register: Char) {
content = ""
updateWidgetInStatusBar(ID, editor.ij.project)
}
}
override fun getId(): String {
return ID
}
override fun getDisplayName(): String {
return "IdeaVim Macro Recording Widget"
}
override fun createWidget(project: Project): StatusBarWidget {
injector.listenersNotifier.macroRecordingListeners.add(macroRecordingListener)
return VimMacroWidget()
}
override fun isAvailable(project: Project): Boolean {
return VimPlugin.isEnabled() && injector.globalIjOptions().showmodewidget
}
private inner class VimMacroWidget : StatusBarWidget {
override fun ID(): String {
return ID
}
override fun getPresentation(): StatusBarWidget.WidgetPresentation {
return VimModeWidgetPresentation()
}
}
private inner class VimModeWidgetPresentation : StatusBarWidget.TextPresentation {
override fun getAlignment(): Float = Component.CENTER_ALIGNMENT
override fun getText(): String {
return content
}
override fun getTooltipText(): String {
return content.ifEmpty {
"No macro recording in progress"
}
}
}
}
public fun updateMacroWidget() {
val factory = StatusBarWidgetFactory.EP_NAME.findExtension(MacroWidgetFactory::class.java) ?: return
for (project in ProjectManager.getInstance().openProjects) {
val statusBarWidgetsManager = project.service<StatusBarWidgetsManager>()
statusBarWidgetsManager.updateWidget(factory)
}
}
public val macroWidgetOptionListener: VimWidgetListener = VimWidgetListener { updateMacroWidget() }

View File

@ -0,0 +1,45 @@
/*
* 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.ui.widgets.mode
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.ui.widgets.VimWidgetListener
public class ModeWidgetFactory : StatusBarWidgetFactory {
public companion object {
public const val ID: String = "IdeaVim::Mode"
}
override fun getId(): String {
return ID
}
override fun getDisplayName(): String {
return "IdeaVim Mode Widget"
}
override fun createWidget(project: Project): StatusBarWidget {
return VimModeWidget(project)
}
override fun isAvailable(project: Project): Boolean {
return VimPlugin.isEnabled()
&& injector.globalIjOptions().showmodewidget
&& !project.isDisposed
&& FileEditorManager.getInstance(project).hasOpenFiles()
}
}
public val modeWidgetOptionListener: VimWidgetListener = VimWidgetListener { updateModeWidget() }

View File

@ -0,0 +1,368 @@
/*
* Copyright 2003-2024 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.ui.widgets.mode
import com.intellij.ide.ui.LafManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTabbedPane
import com.intellij.ui.content.ContentFactory
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RowLayout
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.selected
import com.intellij.ui.dsl.builder.toNullableProperty
import com.intellij.ui.layout.not
import com.intellij.util.Alarm
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.datatypes.asVimInt
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.FlowLayout
import javax.swing.BorderFactory
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
public class ModeWidgetPopup : AnAction() {
public override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val popup = createPopup() ?: return
popup.showCenteredInCurrentWindow(project)
}
public companion object {
@Volatile
private var currentPopup: JBPopup? = null
public fun createPopup(): JBPopup? {
synchronized(this) {
if (currentPopup?.isDisposed == false) return null
val mainPanel = JPanel(BorderLayout())
val buttonPanel = JPanel(FlowLayout(FlowLayout.RIGHT))
val applyButton = JButton("Apply").apply { isEnabled = false }
val cancelButton = JButton("Close")
buttonPanel.add(applyButton)
buttonPanel.add(cancelButton)
mainPanel.add(buttonPanel, BorderLayout.SOUTH)
val tabbedPane = JBTabbedPane()
val lightThemeSettings = createPanel(getWidgetThemeColors(true))
val darkThemeSettings = createPanel(getWidgetThemeColors(false))
tabbedPane.addTab(MessageHelper.getMessage("widget.mode.popup.tab.light"), lightThemeSettings.addScrollPane())
tabbedPane.addTab(MessageHelper.getMessage("widget.mode.popup.tab.dark"), darkThemeSettings.addScrollPane())
tabbedPane.preferredSize = Dimension(300, 300)
for (i in 0 until tabbedPane.tabCount) {
val label = JLabel(tabbedPane.getTitleAt(i), JLabel.CENTER)
label.preferredSize = Dimension(126, tabbedPane.getTabComponentAt(i).preferredSize.height)
tabbedPane.setTabComponentAt(i, label)
}
tabbedPane.selectedIndex = if (LafManager.getInstance().currentUIThemeLookAndFeel.isDark) 1 else 0
mainPanel.add(tabbedPane, BorderLayout.CENTER)
val popupContent = ContentFactory.getInstance().createContent(mainPanel, "", false).component
val popup = JBPopupFactory.getInstance()
.createComponentPopupBuilder(popupContent, popupContent)
.setTitle(MessageHelper.getMessage("widget.mode.popup.title"))
.setResizable(true)
.setMovable(true)
.setRequestFocus(true)
.setCancelOnClickOutside(false)
.setCancelKeyEnabled(false)
.createPopup()
applyButton.addActionListener {
lightThemeSettings.apply()
darkThemeSettings.apply()
repaintModeWidget()
}
cancelButton.addActionListener {
popup.cancel()
}
val alarm = Alarm(popup)
fun updateApplyButtonVisibility() {
alarm.addRequest({
applyButton.isEnabled = lightThemeSettings.isModified() || darkThemeSettings.isModified()
updateApplyButtonVisibility()
}, 500L)
}
updateApplyButtonVisibility()
currentPopup = popup
return currentPopup
}
}
private fun getWidgetThemeColors(isLight: Boolean): ModeColors {
val keyPostfix = if (isLight) "_light" else "_dark"
return ModeColors(
"widget_mode_is_full_customization$keyPostfix",
"widget_mode_theme$keyPostfix",
"widget_mode_normal_background$keyPostfix",
"widget_mode_normal_foreground$keyPostfix",
"widget_mode_insert_background$keyPostfix",
"widget_mode_insert_foreground$keyPostfix",
"widget_mode_replace_background$keyPostfix",
"widget_mode_replace_foreground$keyPostfix",
"widget_mode_command_background$keyPostfix",
"widget_mode_command_foreground$keyPostfix",
"widget_mode_visual_background$keyPostfix",
"widget_mode_visual_foreground$keyPostfix",
"widget_mode_visual_line_background$keyPostfix",
"widget_mode_visual_line_foreground$keyPostfix",
"widget_mode_visual_block_background$keyPostfix",
"widget_mode_visual_block_foreground$keyPostfix",
"widget_mode_select_background$keyPostfix",
"widget_mode_select_foreground$keyPostfix",
"widget_mode_select_line_background$keyPostfix",
"widget_mode_select_line_foreground$keyPostfix",
"widget_mode_select_block_background$keyPostfix",
"widget_mode_select_block_foreground$keyPostfix",
)
}
private fun createPanel(modeColors: ModeColors): DialogPanel {
val panel = panel {
lateinit var advancedSettings: Cell<JBCheckBox>
row {
advancedSettings = checkBox(MessageHelper.getMessage("widget.mode.popup.field.advanced.settings")).bindSelected(modeColors::isFullCustomization)
}
group {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.theme"))
comboBox(ModeWidgetTheme.values().toList()).bindItem(modeColors::theme.toNullableProperty())
}
row { browserLink("Suggest your theme", "https://youtrack.jetbrains.com/issue/VIM-1377/Normal-mode-needs-to-be-more-obvious") }
}.topGap(TopGap.NONE).visibleIf(!advancedSettings.selected)
group(MessageHelper.getMessage("widget.mode.popup.group.title.full.customization")) {
row { text(MessageHelper.getMessage("widget.mode.popup.color.instruction")) }
group(MessageHelper.getMessage("widget.mode.popup.group.normal.title")) {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::normalBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::normalFg)
}.layout(RowLayout.PARENT_GRID)
}
group(MessageHelper.getMessage("widget.mode.popup.group.insert.title")) {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::insertBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::insertFg)
}.layout(RowLayout.PARENT_GRID)
}
group(MessageHelper.getMessage("widget.mode.popup.group.replace.title")) {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::replaceBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::replaceFg)
}.layout(RowLayout.PARENT_GRID)
}
group(MessageHelper.getMessage("widget.mode.popup.group.command.title")) {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::commandBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::commandFg)
}.layout(RowLayout.PARENT_GRID)
}
group(MessageHelper.getMessage("widget.mode.popup.group.visual.title")) {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::visualBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::visualFg)
}.layout(RowLayout.PARENT_GRID)
collapsibleGroup(MessageHelper.getMessage("widget.mode.popup.group.visual.subgroup.line.title")) {
row { text(MessageHelper.getMessage("widget.mode.popup.group.visual.subgroup.instruction")) }
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::visualLineBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::visualLineFg)
}.layout(RowLayout.PARENT_GRID)
}
collapsibleGroup(MessageHelper.getMessage("widget.mode.popup.group.visual.subgroup.block.title")) {
row { text(MessageHelper.getMessage("widget.mode.popup.group.visual.subgroup.instruction")) }
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::visualBlockBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::visualBlockFg)
}.layout(RowLayout.PARENT_GRID)
}
}
group(MessageHelper.getMessage("widget.mode.popup.group.select.title")) {
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::selectBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::selectFg)
}.layout(RowLayout.PARENT_GRID)
collapsibleGroup(MessageHelper.getMessage("widget.mode.popup.group.select.subgroup.line.title")) {
row { text(MessageHelper.getMessage("widget.mode.popup.group.select.subgroup.instruction")) }
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::selectLineBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::selectLineFg)
}.layout(RowLayout.PARENT_GRID)
}
collapsibleGroup(MessageHelper.getMessage("widget.mode.popup.group.select.subgroup.block.title")) {
row { text(MessageHelper.getMessage("widget.mode.popup.group.select.subgroup.instruction")) }
row {
label(MessageHelper.getMessage("widget.mode.popup.field.background"))
textField().bindText(modeColors::selectBlockBg)
}.layout(RowLayout.PARENT_GRID)
row {
label(MessageHelper.getMessage("widget.mode.popup.field.foreground"))
textField().bindText(modeColors::selectBlockFg)
}.layout(RowLayout.PARENT_GRID)
}
}
}.topGap(TopGap.NONE).visibleIf(advancedSettings.selected)
}
return panel
}
private fun JComponent.addScrollPane(): JComponent {
val scrollPane = JBScrollPane(this, JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
scrollPane.border = BorderFactory.createEmptyBorder()
return scrollPane
}
}
private class ModeColors(
isFullCustomizationKey: String, themeKey: String,
normalBgKey: String, normalFgKey: String,
insertBgKey: String, insertFgKey: String,
replaceBgKey: String, replaceFgKey: String,
commandBgKey: String, commandFgKey: String,
visualBgKey: String, visualFgKey: String, visualLineBgKey: String, visualLineFgKey: String, visualBlockBgKey: String, visualBlockFgKey: String,
selectBgKey: String, selectFgKey: String, selectLineBgKey: String, selectLineFgKey: String, selectBlockBgKey: String, selectBlockFgKey: String
) {
var isFullCustomization: Boolean by VimScopeBooleanVariable(isFullCustomizationKey)
var theme: ModeWidgetTheme by VimScopeThemeVariable(themeKey)
var normalBg: String by VimScopeStringVariable(normalBgKey)
var normalFg: String by VimScopeStringVariable(normalFgKey)
var insertBg: String by VimScopeStringVariable(insertBgKey)
var insertFg: String by VimScopeStringVariable(insertFgKey)
var replaceBg: String by VimScopeStringVariable(replaceBgKey)
var replaceFg: String by VimScopeStringVariable(replaceFgKey)
var commandBg: String by VimScopeStringVariable(commandBgKey)
var commandFg: String by VimScopeStringVariable(commandFgKey)
var visualBg: String by VimScopeStringVariable(visualBgKey)
var visualFg: String by VimScopeStringVariable(visualFgKey)
var visualLineBg: String by VimScopeStringVariable(visualLineBgKey)
var visualLineFg: String by VimScopeStringVariable(visualLineFgKey)
var visualBlockBg: String by VimScopeStringVariable(visualBlockBgKey)
var visualBlockFg: String by VimScopeStringVariable(visualBlockFgKey)
var selectBg: String by VimScopeStringVariable(selectBgKey)
var selectFg: String by VimScopeStringVariable(selectFgKey)
var selectLineBg: String by VimScopeStringVariable(selectLineBgKey)
var selectLineFg: String by VimScopeStringVariable(selectLineFgKey)
var selectBlockBg: String by VimScopeStringVariable(selectBlockBgKey)
var selectBlockFg: String by VimScopeStringVariable(selectBlockFgKey)
private class VimScopeBooleanVariable(private var key: String): ReadWriteProperty<ModeColors, Boolean> {
override fun getValue(thisRef: ModeColors, property: KProperty<*>): Boolean {
return injector.variableService.getVimVariable(key)?.asBoolean() ?: false
}
override fun setValue(thisRef: ModeColors, property: KProperty<*>, value: Boolean) {
injector.variableService.storeVimVariable(key, value.asVimInt())
}
}
private class VimScopeStringVariable(private var key: String): ReadWriteProperty<ModeColors, String> {
override fun getValue(thisRef: ModeColors, property: KProperty<*>): String {
return injector.variableService.getVimVariable(key)?.asString() ?: ""
}
override fun setValue(thisRef: ModeColors, property: KProperty<*>, value: String) {
injector.variableService.storeVimVariable(key, VimString(value))
}
}
private class VimScopeThemeVariable(private var key: String): ReadWriteProperty<ModeColors, ModeWidgetTheme> {
override fun getValue(thisRef: ModeColors, property: KProperty<*>): ModeWidgetTheme {
val themeString = injector.variableService.getVimVariable(key)?.asString() ?: return ModeWidgetTheme.getDefaultTheme()
return ModeWidgetTheme.parseString(themeString) ?: ModeWidgetTheme.getDefaultTheme()
}
override fun setValue(thisRef: ModeColors, property: KProperty<*>, value: ModeWidgetTheme) {
injector.variableService.storeVimVariable(key, VimString(value.toString()))
}
}
}
}
public enum class ModeWidgetTheme(private var value: String) {
TEST("Nord-Aurora (testing, will be removed)"),
COLORLESS("Colorless");
override fun toString(): String {
return value
}
public companion object {
public fun parseString(string: String): ModeWidgetTheme? {
return ModeWidgetTheme.values().firstOrNull { it.value == string }
}
public fun getDefaultTheme(): ModeWidgetTheme = TEST
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright 2003-2024 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.ui.widgets.mode
import com.intellij.ide.ui.LafManager
import com.intellij.util.ui.UIUtil
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import java.awt.Color
public fun getModeBackground(mode: Mode?): Color {
val isLight = !LafManager.getInstance().currentUIThemeLookAndFeel.isDark
val keyPostfix = if (isLight) "_light" else "_dark"
if (injector.variableService.getVimVariable("widget_mode_is_full_customization$keyPostfix")?.asBoolean() != true) {
val themeString = injector.variableService.getVimVariable("widget_mode_theme$keyPostfix")?.asString() ?: ""
val theme = ModeWidgetTheme.parseString(themeString) ?: ModeWidgetTheme.getDefaultTheme()
when (theme) {
ModeWidgetTheme.TEST -> {
return when (mode) {
Mode.INSERT -> Color.decode("#D08770")
Mode.REPLACE -> Color.decode("#EBCB8B")
is Mode.NORMAL -> Color.decode("#BF616A")
is Mode.CMD_LINE -> Color.decode("#A3BE8C")
is Mode.VISUAL -> Color.decode("#B48EAD")
is Mode.SELECT -> Color.decode("#B48EAD")
is Mode.OP_PENDING, null -> UIUtil.getPanelBackground()
}
}
ModeWidgetTheme.COLORLESS -> {
return UIUtil.getPanelBackground()
}
}
} else {
val colorString = when (mode) {
Mode.INSERT -> injector.variableService.getVimVariable("widget_mode_insert_background$keyPostfix")
Mode.REPLACE -> injector.variableService.getVimVariable("widget_mode_replace_background$keyPostfix")
is Mode.NORMAL -> injector.variableService.getVimVariable("widget_mode_normal_background$keyPostfix")
is Mode.CMD_LINE -> injector.variableService.getVimVariable("widget_mode_command_background$keyPostfix")
is Mode.VISUAL -> {
val visualModeBackground = injector.variableService.getVimVariable("widget_mode_visual_background$keyPostfix")
when (mode.selectionType) {
SelectionType.CHARACTER_WISE -> visualModeBackground
SelectionType.LINE_WISE -> injector.variableService.getVimVariable("widget_mode_visual_line_background$keyPostfix") ?: visualModeBackground
SelectionType.BLOCK_WISE -> injector.variableService.getVimVariable("widget_mode_visual_block_background$keyPostfix") ?: visualModeBackground
}
}
is Mode.SELECT -> {
val selectModeBackground = injector.variableService.getVimVariable("widget_mode_select_background$keyPostfix")
when (mode.selectionType) {
SelectionType.CHARACTER_WISE -> selectModeBackground
SelectionType.LINE_WISE -> injector.variableService.getVimVariable("widget_mode_select_line_background$keyPostfix") ?: selectModeBackground
SelectionType.BLOCK_WISE -> injector.variableService.getVimVariable("widget_mode_select_block_background$keyPostfix") ?: selectModeBackground
}
}
is Mode.OP_PENDING, null -> null
}?.asString()
val defaultColor = UIUtil.getPanelBackground()
val color = when (colorString) {
"v:status_bar_bg" -> UIUtil.getPanelBackground()
"v:status_bar_fg" -> UIUtil.getLabelForeground()
else -> {
if (colorString == null) {
defaultColor
} else {
try { Color.decode(colorString) } catch (e: Exception) { defaultColor }
}
}
}
return color
}
}
public fun getModeForeground(mode: Mode?): Color {
val isLight = !LafManager.getInstance().currentUIThemeLookAndFeel.isDark
val keyPostfix = if (isLight) "_light" else "_dark"
if (injector.variableService.getVimVariable("widget_mode_is_full_customization$keyPostfix")?.asBoolean() != true) {
val themeString = injector.variableService.getVimVariable("widget_mode_theme$keyPostfix")?.asString() ?: ""
val theme = ModeWidgetTheme.parseString(themeString) ?: ModeWidgetTheme.getDefaultTheme()
return when (theme) {
ModeWidgetTheme.TEST -> Color.decode("#2E3440")
ModeWidgetTheme.COLORLESS -> UIUtil.getLabelForeground()
}
} else {
val colorString = when (mode) {
Mode.INSERT -> injector.variableService.getVimVariable("widget_mode_insert_foreground$keyPostfix")
Mode.REPLACE -> injector.variableService.getVimVariable("widget_mode_replace_foreground$keyPostfix")
is Mode.NORMAL -> injector.variableService.getVimVariable("widget_mode_normal_foreground$keyPostfix")
is Mode.CMD_LINE -> injector.variableService.getVimVariable("widget_mode_command_foreground$keyPostfix")
is Mode.VISUAL -> {
val visualModeBackground = injector.variableService.getVimVariable("widget_mode_visual_foreground$keyPostfix")
when (mode.selectionType) {
SelectionType.CHARACTER_WISE -> visualModeBackground
SelectionType.LINE_WISE -> injector.variableService.getVimVariable("widget_mode_visual_line_foreground$keyPostfix") ?: visualModeBackground
SelectionType.BLOCK_WISE -> injector.variableService.getVimVariable("widget_mode_visual_block_foreground$keyPostfix") ?: visualModeBackground
}
}
is Mode.SELECT -> {
val selectModeBackground = injector.variableService.getVimVariable("widget_mode_select_foreground$keyPostfix")
when (mode.selectionType) {
SelectionType.CHARACTER_WISE -> selectModeBackground
SelectionType.LINE_WISE -> injector.variableService.getVimVariable("widget_mode_select_line_foreground$keyPostfix") ?: selectModeBackground
SelectionType.BLOCK_WISE -> injector.variableService.getVimVariable("widget_mode_select_block_foreground$keyPostfix") ?: selectModeBackground
}
}
is Mode.OP_PENDING, null -> null
}?.asString()
val defaultColor = UIUtil.getLabelForeground()
val color = when (colorString) {
"v:status_bar_bg" -> UIUtil.getPanelBackground()
"v:status_bar_fg" -> UIUtil.getLabelForeground()
else -> {
if (colorString == null) {
defaultColor
} else {
try { Color.decode(colorString) } catch (e: Exception) { defaultColor }
}
}
}
return color
}
}

View File

@ -0,0 +1,176 @@
/*
* 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.ui.widgets.mode
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.CustomStatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBLabel
import com.intellij.ui.util.width
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetFocusListener
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetModeListener
import java.awt.Dimension
import java.awt.Point
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
import kotlin.math.max
public class VimModeWidget(public val project: Project) : CustomStatusBarWidget, VimStatusBarWidget {
private companion object {
private const val INSERT = "INSERT"
private const val NORMAL = "NORMAL"
private const val REPLACE = "REPLACE"
private const val COMMAND = "COMMAND"
private const val VISUAL = "VISUAL"
private const val VISUAL_LINE = "V-LINE"
private const val VISUAL_BLOCK = "V-BLOCK"
private const val SELECT = "SELECT"
private const val SELECT_LINE = "S-LINE"
private const val SELECT_BLOCK = "S-BLOCK"
}
private val useColors = injector.globalIjOptions().colorfulmodewidget
private val label = JBLabelWiderThan(setOf(REPLACE)).apply {
isOpaque = useColors
}
init {
val mode = getFocusedEditor(project)?.vim?.mode
updateLabel(mode)
injector.listenersNotifier.apply {
modeChangeListeners.add(ModeWidgetModeListener(this@VimModeWidget))
myEditorListeners.add(ModeWidgetFocusListener(this@VimModeWidget))
}
label.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
val popup = ModeWidgetPopup.createPopup() ?: return
val dimension = popup.content.preferredSize
val widgetLocation = e.component.locationOnScreen
popup.show(RelativePoint(Point(
widgetLocation.x + e.component.width - dimension.width,
widgetLocation.y - dimension.height,
)))
}
})
}
override fun ID(): String {
return ModeWidgetFactory.ID
}
override fun getComponent(): JComponent {
return label
}
public fun updateWidget() {
val mode = getFocusedEditor(project)?.vim?.mode
updateWidget(mode)
}
public fun updateWidget(mode: Mode?) {
updateLabel(mode)
updateWidgetInStatusBar(ModeWidgetFactory.ID, project)
}
private fun updateLabel(mode: Mode?) {
label.text = getModeText(mode)
if (useColors) {
label.foreground = getModeForeground(mode)
label.background = getModeBackground(mode)
}
}
private fun getFocusedEditor(project: Project): Editor? {
val fileEditorManager = FileEditorManager.getInstance(project)
return fileEditorManager.selectedTextEditor
}
private fun getModeText(mode: Mode?): String? {
return when (mode) {
Mode.INSERT -> INSERT
Mode.REPLACE -> REPLACE
is Mode.NORMAL -> NORMAL
is Mode.CMD_LINE -> COMMAND
is Mode.VISUAL -> getVisualModeText(mode)
is Mode.SELECT -> getSelectModeText(mode)
is Mode.OP_PENDING, null -> null
}
}
private fun getVisualModeText(mode: Mode.VISUAL) = when (mode.selectionType) {
SelectionType.CHARACTER_WISE -> VISUAL
SelectionType.LINE_WISE -> VISUAL_LINE
SelectionType.BLOCK_WISE -> VISUAL_BLOCK
}
private fun getSelectModeText(mode: Mode.SELECT) = when (mode.selectionType) {
SelectionType.CHARACTER_WISE -> SELECT
SelectionType.LINE_WISE -> SELECT_LINE
SelectionType.BLOCK_WISE -> SELECT_BLOCK
}
private class JBLabelWiderThan(private val words: Collection<String>): JBLabel("", CENTER) {
private val wordWidth: Int
get() {
val fontMetrics = getFontMetrics(font)
return words.maxOfOrNull { fontMetrics.stringWidth(it) } ?: 0
}
override fun getMinimumSize(): Dimension {
val minimumSize = super.getMinimumSize()
return Dimension(max(minimumSize.width, wordWidth + insets.width), minimumSize.height)
}
override fun getPreferredSize(): Dimension {
val preferredSize = super.getPreferredSize()
return Dimension(max(preferredSize.width, wordWidth + insets.width), preferredSize.height)
}
override fun getMaximumSize(): Dimension {
val maximumSize = super.getMaximumSize()
return Dimension(max(maximumSize.width, wordWidth + insets.width), maximumSize.height)
}
}
}
public fun updateModeWidget() {
val factory = StatusBarWidgetFactory.EP_NAME.findExtension(ModeWidgetFactory::class.java) ?: return
for (project in ProjectManager.getInstance().openProjects) {
val statusBarWidgetsManager = project.service<StatusBarWidgetsManager>()
statusBarWidgetsManager.updateWidget(factory)
}
}
public fun repaintModeWidget() {
for (project in ProjectManager.getInstance().openProjects) {
val widgets = WindowManager.getInstance()?.getStatusBar(project)?.allWidgets ?: continue
for (widget in widgets) {
if (widget is VimModeWidget) {
widget.updateWidget()
}
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2003-2024 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.ui.widgets.mode
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.WindowManager
import java.util.*
public interface VimStatusBarWidget {
public fun updateWidgetInStatusBar(widgetID: String, project: Project?) {
if (project == null) return
val windowManager = WindowManager.getInstance()
windowManager.getStatusBar(project)?.updateWidget(widgetID) ?: run {
TimerWithRetriesTask(500L, 50) {
val statusBar = windowManager.getStatusBar(project) ?: return@TimerWithRetriesTask false
statusBar.updateWidget(widgetID)
return@TimerWithRetriesTask true
}.execute()
}
}
}
/**
* A task that may be used to address issues with initialization in the Platform, executing code with a reasonable number of retries and a reasonable period.
* Clearly, this is a workaround and its use should be avoided when possible.
*
* Why is it needed for widgets?
* In a single project environment, it is not necessary since the status bar is initialized before the editor opens.
* However, in multi-project setups, the editor window is opened before the status bar initialization.
* And this tasks tries to loops until status bar creation in order to notify it about opened editor.
*/
private class TimerWithRetriesTask(
private val period: Long,
private val retriesLimit: Int,
private val block: () -> Boolean,
) {
private val timer = Timer()
fun execute() {
timer.schedule(object : TimerTask() {
private var counter = 0
override fun run() {
if (counter >= retriesLimit) {
timer.cancel()
} else {
if (this@TimerWithRetriesTask.block()) timer.cancel()
counter++
}
}
}, 0L, period)
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.ui.widgets.mode.listeners
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.common.EditorListener
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.ui.widgets.mode.VimModeWidget
import com.maddyhome.idea.vim.ui.widgets.mode.updateModeWidget
internal class ModeWidgetFocusListener(private val modeWidget: VimModeWidget): EditorListener {
override fun created(editor: VimEditor) {
updateModeWidget()
val mode = getFocusedEditorForProject(editor.ij.project)?.vim?.mode
modeWidget.updateWidget(mode)
}
override fun released(editor: VimEditor) {
updateModeWidget()
val focusedEditor = getFocusedEditorForProject(editor.ij.project)
if (focusedEditor == null || focusedEditor == editor.ij) {
modeWidget.updateWidget(null)
}
}
override fun focusGained(editor: VimEditor) {
if (editor.ij.project != modeWidget.project) return
val mode = editor.mode
modeWidget.updateWidget(mode)
}
override fun focusLost(editor: VimEditor) {
val mode = getFocusedEditorForProject(editor.ij.project)?.vim?.mode
modeWidget.updateWidget(mode)
}
private fun getFocusedEditorForProject(editorProject: Project?): Editor? {
if (editorProject != modeWidget.project) return null
val fileEditorManager = FileEditorManager.getInstance(editorProject)
return fileEditorManager.selectedTextEditor
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.ui.widgets.mode.listeners
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.ui.widgets.mode.VimModeWidget
internal class ModeWidgetModeListener(private val modeWidget: VimModeWidget): ModeChangeListener {
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
val editorMode = editor.mode
if (editorMode !is Mode.OP_PENDING && editor.ij.project == modeWidget.project) {
modeWidget.updateWidget(editorMode)
}
}
}

View File

@ -36,31 +36,28 @@ public val VimEditor.isIdeaRefactorModeSelect: Boolean
internal object IdeaRefactorModeHelper { internal object IdeaRefactorModeHelper {
fun correctSelection(editor: Editor) { sealed interface Action {
val action: () -> Unit = { object RemoveSelection : Action
val mode = editor.vim.mode class SetMode(val newMode: Mode) : Action
if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) { class MoveToOffset(val newOffset: Int) : Action
}
fun applyCorrections(corrections: List<Action>, editor: Editor) {
val correctionsApplier = {
corrections.forEach { correction ->
when (correction) {
is Action.MoveToOffset -> {
editor.caretModel.moveToOffset(correction.newOffset)
}
Action.RemoveSelection -> {
SelectionVimListenerSuppressor.lock().use { SelectionVimListenerSuppressor.lock().use {
editor.selectionModel.removeSelection() editor.selectionModel.removeSelection()
} }
} }
if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
val autodetectedSubmode = VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)
if (mode.selectionType != autodetectedSubmode) {
// Update the submode
val newMode = when (mode) {
is Mode.SELECT -> mode.copy(selectionType = autodetectedSubmode)
is Mode.VISUAL -> mode.copy(selectionType = autodetectedSubmode)
else -> error("IdeaVim should be either in visual or select modes")
}
editor.vim.vimStateMachine.mode = newMode
}
}
if (editor.hasBlockOrUnderscoreCaret()) { is Action.SetMode -> {
TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange -> editor.vim.vimStateMachine.mode = correction.newMode
if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) {
editor.caretModel.moveToOffset(editor.caretModel.offset - 1)
} }
} }
} }
@ -70,7 +67,9 @@ internal object IdeaRefactorModeHelper {
if (lookup != null) { if (lookup != null) {
val selStart = editor.selectionModel.selectionStart val selStart = editor.selectionModel.selectionStart
val selEnd = editor.selectionModel.selectionEnd val selEnd = editor.selectionModel.selectionEnd
lookup.performGuardedChange(action) lookup.performGuardedChange {
correctionsApplier()
}
lookup.addLookupListener(object : LookupListener { lookup.addLookupListener(object : LookupListener {
override fun beforeItemSelected(event: LookupEvent): Boolean { override fun beforeItemSelected(event: LookupEvent): Boolean {
// FIXME: 01.11.2019 Nasty workaround because of problems in IJ platform // FIXME: 01.11.2019 Nasty workaround because of problems in IJ platform
@ -82,7 +81,41 @@ internal object IdeaRefactorModeHelper {
} }
}) })
} else { } else {
action() correctionsApplier()
} }
} }
fun calculateCorrections(editor: Editor): List<Action> {
val corrections = mutableListOf<Action>()
val mode = editor.vim.mode
if (!mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
corrections.add(Action.RemoveSelection)
}
if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) {
val autodetectedSubmode = VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)
if (mode.selectionType != autodetectedSubmode) {
// Update the submode
val newMode = when (mode) {
is Mode.SELECT -> mode.copy(selectionType = autodetectedSubmode)
is Mode.VISUAL -> mode.copy(selectionType = autodetectedSubmode)
else -> error("IdeaVim should be either in visual or select modes")
}
corrections.add(Action.SetMode(newMode))
}
}
if (editor.hasBlockOrUnderscoreCaret()) {
TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange ->
if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) {
corrections.add(Action.MoveToOffset(editor.caretModel.offset - 1))
}
}
}
return corrections
}
fun correctSelection(editor: Editor) {
val corrections = calculateCorrections(editor)
applyCorrections(corrections, editor)
}
} }

View File

@ -8,6 +8,10 @@
package com.maddyhome.idea.vim.vimscript.services package com.maddyhome.idea.vim.vimscript.services
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.ex.vimscript.VimScriptGlobalEnvironment import com.maddyhome.idea.vim.ex.vimscript.VimScriptGlobalEnvironment
@ -22,8 +26,10 @@ import com.maddyhome.idea.vim.vimscript.model.datatypes.VimList
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.expressions.Scope import com.maddyhome.idea.vim.vimscript.model.expressions.Scope
import com.maddyhome.idea.vim.vimscript.model.expressions.Variable import com.maddyhome.idea.vim.vimscript.model.expressions.Variable
import org.jdom.Element
internal class IjVariableService : VimVariableServiceBase() { @State(name = "VimVariables", storages = [Storage(value = "\$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)])
internal class IjVariableService : VimVariableServiceBase(), PersistentStateComponent<Element?> {
override fun storeVariable(variable: Variable, value: VimDataType, editor: VimEditor, context: ExecutionContext, vimContext: VimLContext) { override fun storeVariable(variable: Variable, value: VimDataType, editor: VimEditor, context: ExecutionContext, vimContext: VimLContext) {
super.storeVariable(variable, value, editor, context, vimContext) super.storeVariable(variable, value, editor, context, vimContext)
@ -47,4 +53,49 @@ internal class IjVariableService : VimVariableServiceBase() {
else -> error("Unexpected") else -> error("Unexpected")
} }
} }
override fun getState(): Element {
val element = Element("variables")
saveData(element)
return element
}
override fun loadState(state: Element) {
readData(state)
}
private fun saveData(element: Element) {
val vimVariablesElement = Element("vim-variables")
for ((key, value) in vimVariables.entries) {
if (value is VimString) {
val variableElement = Element("variable")
variableElement.setAttribute("key", key)
variableElement.setAttribute("value", value.value)
variableElement.setAttribute("type", "string")
vimVariablesElement.addContent(variableElement)
} else if (value is VimInt) {
val variableElement = Element("variable")
variableElement.setAttribute("key", key)
variableElement.setAttribute("value", value.value.toString())
variableElement.setAttribute("type", "int")
vimVariablesElement.addContent(variableElement)
}
}
element.addContent(vimVariablesElement)
}
private fun readData(element: Element) {
val vimVariablesElement = element.getChild("vim-variables")
val variableElements = vimVariablesElement.getChildren("variable")
for (variableElement in variableElements) {
when (variableElement.getAttributeValue("type")) {
"string" -> {
vimVariables[variableElement.getAttributeValue("key")] = VimString(variableElement.getAttributeValue("value"))
}
"int" -> {
vimVariables[variableElement.getAttributeValue("key")] = VimInt(variableElement.getAttributeValue("value"))
}
}
}
}
} }

View File

@ -0,0 +1,19 @@
<!--
~ Copyright 2003-2024 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>
<extensions defaultExtensionNs="com.intellij">
<editorActionHandler action="EditorEscape"
implementationClass="com.maddyhome.idea.vim.handler.VimEscForRiderHandler"
id="ideavim-clion-nova-esc"
order="first, before idea.only.escape"/>
</extensions>
<extensions defaultExtensionNs="IdeaVIM">
<clionNovaProvider implementation="com.maddyhome.idea.vim.ide.ClionNovaProviderImpl"/>
</extensions>
</idea-plugin>

View File

@ -226,12 +226,12 @@
<!-- Change --> <!-- Change -->
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerMotionAction" mappingModes="N" keys="gu"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerMotionAction" mappingModes="N" keys="gu"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction" mappingModes="X" keys="u"/> <!-- <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction" mappingModes="X" keys="u"/>-->
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseToggleCharacterAction" mappingModes="N" keys="~"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseToggleCharacterAction" mappingModes="N" keys="~"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseToggleMotionAction" mappingModes="N" keys="g~"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseToggleMotionAction" mappingModes="N" keys="g~"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseToggleVisualAction" mappingModes="X" keys="~"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseToggleVisualAction" mappingModes="X" keys="~"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperMotionAction" mappingModes="N" keys="gU"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperMotionAction" mappingModes="N" keys="gU"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction" mappingModes="X" keys="U"/> <!-- <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction" mappingModes="X" keys="U"/>-->
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCharacterAction" mappingModes="N" keys="r"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCharacterAction" mappingModes="N" keys="r"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCharactersAction" mappingModes="N" keys="s"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeCharactersAction" mappingModes="N" keys="s"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeEndOfLineAction" mappingModes="N" keys="C"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.change.ChangeEndOfLineAction" mappingModes="N" keys="C"/>
@ -329,8 +329,8 @@
<vimAction implementation="com.maddyhome.idea.vim.action.change.RepeatChangeAction" mappingModes="N" keys="."/> <vimAction implementation="com.maddyhome.idea.vim.action.change.RepeatChangeAction" mappingModes="N" keys="."/>
<vimAction implementation="com.maddyhome.idea.vim.action.ExEntryAction" mappingModes="NXO" keys=":"/> <vimAction implementation="com.maddyhome.idea.vim.action.ExEntryAction" mappingModes="NXO" keys=":"/>
<vimAction implementation="com.maddyhome.idea.vim.action.ResetModeAction" mappingModes="ALL" keys="«C-\»«C-N»"/> <vimAction implementation="com.maddyhome.idea.vim.action.ResetModeAction" mappingModes="ALL" keys="«C-\»«C-N»"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.RedoAction" mappingModes="N" keys="«C-R»"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.RedoAction" mappingModes="NX" keys="U,«C-R»"/>
<vimAction implementation="com.maddyhome.idea.vim.action.change.UndoAction" mappingModes="N"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.UndoAction" mappingModes="NX"/>
<!-- Keys --> <!-- Keys -->
<vimAction implementation="com.maddyhome.idea.vim.action.change.OperatorAction" mappingModes="N" keys="g@"/> <vimAction implementation="com.maddyhome.idea.vim.action.change.OperatorAction" mappingModes="N" keys="g@"/>

View File

@ -1,12 +1,4 @@
<!-- <idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
~ Copyright 2003-2023 The IdeaVim authors
~
~ Use of this source code is governed by an MIT-style
~ license that can be found in the LICENSE.txt file or at
~ https://opensource.org/licenses/MIT.
-->
<idea-plugin url="https://plugins.jetbrains.com/plugin/164" xmlns:xi="http://www.w3.org/2001/XInclude">
<name>IdeaVim</name> <name>IdeaVim</name>
<id>IdeaVIM</id> <id>IdeaVIM</id>
<description><![CDATA[ <description><![CDATA[
@ -21,13 +13,13 @@
<li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li> <li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li>
</ul> </ul>
]]></description> ]]></description>
<version>SNAPSHOT</version> <version>chylex</version>
<vendor>JetBrains</vendor> <vendor>JetBrains</vendor>
<!-- Please search for "[VERSION UPDATE]" in project in case you update the since-build version --> <!-- Please search for "[VERSION UPDATE]" in project in case you update the since-build version -->
<!-- Check for [Version Update] tag in YouTrack as well --> <!-- Check for [Version Update] tag in YouTrack as well -->
<!-- Also, please update the value in build.gradle.kts file--> <!-- Also, please update the value in build.gradle.kts file-->
<idea-version since-build="233.11799.30"/> <idea-version since-build="232"/>
<!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) --> <!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) -->
<depends>com.intellij.modules.platform</depends> <depends>com.intellij.modules.platform</depends>
@ -37,6 +29,8 @@
<!--suppress PluginXmlValidity --> <!--suppress PluginXmlValidity -->
<depends optional="true" config-file="ides/ideavim-withRider.xml">com.intellij.modules.rider</depends> <depends optional="true" config-file="ides/ideavim-withRider.xml">com.intellij.modules.rider</depends>
<!--suppress PluginXmlValidity --> <!--suppress PluginXmlValidity -->
<depends optional="true" config-file="ides/ideavim-withClionNova.xml">org.jetbrains.plugins.clion.radler</depends>
<!--suppress PluginXmlValidity -->
<depends optional="true" config-file="ides/ideavim-withAppCode.xml">com.intellij.modules.appcode</depends> <depends optional="true" config-file="ides/ideavim-withAppCode.xml">com.intellij.modules.appcode</depends>
<depends optional="true" config-file="ideavim-withAceJump.xml">AceJump</depends> <depends optional="true" config-file="ideavim-withAceJump.xml">AceJump</depends>
@ -63,6 +57,8 @@
<extensionPoint name="vimAction" beanClass="com.maddyhome.idea.vim.handler.ActionBeanClass" dynamic="true"> <extensionPoint name="vimAction" beanClass="com.maddyhome.idea.vim.handler.ActionBeanClass" dynamic="true">
<with attribute="implementation" implements="com.maddyhome.idea.vim.handler.EditorActionHandlerBase"/> <with attribute="implementation" implements="com.maddyhome.idea.vim.handler.EditorActionHandlerBase"/>
</extensionPoint> </extensionPoint>
<extensionPoint interface="com.maddyhome.idea.vim.ide.ClionNovaProvider" dynamic="true" name="clionNovaProvider"/>
</extensionPoints> </extensionPoints>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
@ -70,7 +66,9 @@
<projectService serviceImplementation="com.maddyhome.idea.vim.group.NotificationService"/> <projectService serviceImplementation="com.maddyhome.idea.vim.group.NotificationService"/>
<projectService serviceImplementation="com.maddyhome.idea.vim.group.LastTabService"/> <projectService serviceImplementation="com.maddyhome.idea.vim.group.LastTabService"/>
<statusBarWidgetFactory id="IdeaVim-Icon" implementation="com.maddyhome.idea.vim.ui.StatusBarIconFactory"/> <statusBarWidgetFactory id="IdeaVim-Icon" implementation="com.maddyhome.idea.vim.ui.StatusBarIconFactory"/>
<statusBarWidgetFactory id="IdeaVim::Mode" implementation="com.maddyhome.idea.vim.ui.widgets.mode.ModeWidgetFactory" order="last"/>
<statusBarWidgetFactory id="IdeaVim::ShowCmd" implementation="com.maddyhome.idea.vim.ui.ShowCmdStatusBarWidgetFactory" order="first"/> <statusBarWidgetFactory id="IdeaVim::ShowCmd" implementation="com.maddyhome.idea.vim.ui.ShowCmdStatusBarWidgetFactory" order="first"/>
<statusBarWidgetFactory id="IdeaVim::Macro" implementation="com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetFactory"/>
<applicationService serviceImplementation="com.maddyhome.idea.vim.VimPlugin"/> <applicationService serviceImplementation="com.maddyhome.idea.vim.VimPlugin"/>
@ -154,6 +152,7 @@
<action id="VimShortcutKeyAction" class="com.maddyhome.idea.vim.action.VimShortcutKeyAction"/> <action id="VimShortcutKeyAction" class="com.maddyhome.idea.vim.action.VimShortcutKeyAction"/>
<action id="VimActions" class="com.maddyhome.idea.vim.ui.VimActions"/> <action id="VimActions" class="com.maddyhome.idea.vim.ui.VimActions"/>
<action id="CustomizeModeWidget" class="com.maddyhome.idea.vim.ui.widgets.mode.ModeWidgetPopup"/>
<group id="IdeaVim.ReloadVimRc.group" class="com.maddyhome.idea.vim.ui.ReloadFloatingToolbarActionGroup"> <group id="IdeaVim.ReloadVimRc.group" class="com.maddyhome.idea.vim.ui.ReloadFloatingToolbarActionGroup">
<action id="IdeaVim.ReloadVimRc.reload" class="com.maddyhome.idea.vim.ui.ReloadVimRc" <action id="IdeaVim.ReloadVimRc.reload" class="com.maddyhome.idea.vim.ui.ReloadVimRc"
@ -162,5 +161,6 @@
</group> </group>
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/> <action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
</actions> </actions>
</idea-plugin> </idea-plugin>

View File

@ -84,6 +84,8 @@ action.VimShortcutKeyAction.text=Shortcuts
action.VimActions.text=Vim Actions action.VimActions.text=Vim Actions
action.not.found.0=Action not found: {0} action.not.found.0=Action not found: {0}
action.CustomizeModeWidget.text=Mode Widget Settings
action.VimFindActionIdAction.text=IdeaVim: Track Action Ids action.VimFindActionIdAction.text=IdeaVim: Track Action Ids
action.VimFindActionIdAction.description=Starts tracking ids of executed actions action.VimFindActionIdAction.description=Starts tracking ids of executed actions
@ -129,6 +131,28 @@ action.finish.eap.text=Finish EAP
# Don't forget to update README if you modify this entry # Don't forget to update README if you modify this entry
action.subscribe.to.eap.text=Subscribe to EAP action.subscribe.to.eap.text=Subscribe to EAP
widget.mode.popup.title=Mode Widget Colors
widget.mode.popup.tab.light=Light Theme
widget.mode.popup.tab.dark=Dark Theme
widget.mode.popup.color.instruction=Use HEX color values for exact colors; use v:status_bar_bg to use your IDE's status bar background color and v:status_bar_fg for the foreground
widget.mode.popup.field.theme=Widget theme:
widget.mode.popup.field.advanced.settings=Full color customization (advanced)
widget.mode.popup.group.title.full.customization=Full customization
widget.mode.popup.group.normal.title=Normal Mode
widget.mode.popup.group.insert.title=Insert Mode
widget.mode.popup.group.replace.title=Replace Mode
widget.mode.popup.group.command.title=Command Mode
widget.mode.popup.group.visual.title=Visual Mode
widget.mode.popup.group.visual.subgroup.instruction=Leave fields empty to inherit colors from Visual Mode
widget.mode.popup.group.visual.subgroup.line.title=Visual Line
widget.mode.popup.group.visual.subgroup.block.title=Visual Block
widget.mode.popup.group.select.title=Select Mode
widget.mode.popup.group.select.subgroup.instruction=Leave fields empty to inherit colors from Select Mode
widget.mode.popup.group.select.subgroup.line.title=Select Line
widget.mode.popup.group.select.subgroup.block.title=Select Block
widget.mode.popup.field.background=Background:
widget.mode.popup.field.foreground=Text:
configurable.name.vim.emulation=Vim configurable.name.vim.emulation=Vim
configurable.keyhandler.link=<html>Use <a>sethandler</a> command to configure handlers from the .ideavimrc file</html> configurable.keyhandler.link=<html>Use <a>sethandler</a> command to configure handlers from the .ideavimrc file</html>
configurable.noneditablehandler.helper.text.with.example=Non-editable handlers are defined in .ideavimrc file. E.g. ''{0}'' for {1}. configurable.noneditablehandler.helper.text.with.example=Non-editable handlers are defined in .ideavimrc file. E.g. ''{0}'' for {1}.

View File

@ -0,0 +1,37 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.ideavim
import com.intellij.testFramework.LoggedErrorProcessor
import com.intellij.testFramework.TestLoggerFactory.TestLoggerAssertionError
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.fail
/**
* By default, LOG.error does three things in tests:
* - rethrows the exception
* - logs error
* - prints to stderr
*
* The problem is that if we catch exception in tests, such an approach will print the exception to stderr and it will
* look like the exception is not processed.
* I don't see a need for printing these caught exceptions, so we can use this processor to only rethrow them.
*/
internal object OnlyThrowLoggedErrorProcessor : LoggedErrorProcessor() {
override fun processError(category: String, message: String, details: Array<out String>, t: Throwable?): Set<Action> {
return setOf(Action.RETHROW)
}
}
/**
* Asserts that [T] was thrown via `LOG.error("message", e)` call where `e` has a type of [T].
*/
internal inline fun <reified T: Throwable> assertThrowsLogError(crossinline action: () -> Unit): T {
val exception = assertThrows<TestLoggerAssertionError> {
LoggedErrorProcessor.executeWith<Throwable>(OnlyThrowLoggedErrorProcessor) {
action()
}
}
val cause = exception.cause
if (cause !is T) fail("Expected ${T::class.java} exception in LOG.error, but got $cause")
return cause
}

View File

@ -14,12 +14,17 @@ import com.intellij.openapi.editor.LogicalPosition
import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.EditorTestUtil
import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.util.containers.toArray import com.intellij.util.containers.toArray
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.state.mode.mode import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.key.MappingOwner
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.mode
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import kotlin.test.fail import kotlin.test.fail
@ -129,3 +134,15 @@ internal fun <T> product(vararg elements: List<T>): List<List<T>> {
} }
return res return res
} }
internal class ExceptionHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
error(exceptionMessage)
}
companion object {
internal const val exceptionMessage = "Exception here"
}
}
internal val exceptionMappingOwner = MappingOwner.Plugin.get("Exception mapping owner")

View File

@ -102,7 +102,7 @@ import kotlin.test.assertTrue
* This is done as we have no mechanism to guarantee compatibility as we update this test case. * This is done as we have no mechanism to guarantee compatibility as we update this test case.
* Feel free to copy this class into your plugin, or copy just needed functions. * Feel free to copy this class into your plugin, or copy just needed functions.
*/ */
@RunInEdt(writeIntent = true) @RunInEdt
@ApiStatus.Internal @ApiStatus.Internal
abstract class VimTestCase { abstract class VimTestCase {
protected lateinit var fixture: CodeInsightTestFixture protected lateinit var fixture: CodeInsightTestFixture
@ -123,7 +123,7 @@ abstract class VimTestCase {
VimPlugin.getOptionGroup().resetAllOptionsForTesting() VimPlugin.getOptionGroup().resetAllOptionsForTesting()
VimPlugin.getKey().resetKeyMappings() VimPlugin.getKey().resetKeyMappings()
VimPlugin.getSearch().resetState() VimPlugin.getSearch().resetState()
if (!VimPlugin.isEnabled()) VimPlugin.setEnabled(true) if (VimPlugin.isNotEnabled()) VimPlugin.setEnabled(true)
injector.globalOptions().ideastrictmode = true injector.globalOptions().ideastrictmode = true
Checks.reset() Checks.reset()
clearClipboard() clearClipboard()
@ -152,8 +152,8 @@ abstract class VimTestCase {
@AfterEach @AfterEach
open fun tearDown(testInfo: TestInfo) { open fun tearDown(testInfo: TestInfo) {
val swingTimer = swingTimer
swingTimer?.stop() swingTimer?.stop()
swingTimer = null
val bookmarksManager = BookmarksManager.getInstance(fixture.project) val bookmarksManager = BookmarksManager.getInstance(fixture.project)
bookmarksManager?.bookmarks?.forEach { bookmark -> bookmarksManager?.bookmarks?.forEach { bookmark ->
bookmarksManager.remove(bookmark) bookmarksManager.remove(bookmark)
@ -170,6 +170,7 @@ abstract class VimTestCase {
injector.jumpService.resetJumps() injector.jumpService.resetJumps()
VimPlugin.getChange().resetRepeat() VimPlugin.getChange().resetRepeat()
VimPlugin.getKey().savedShortcutConflicts.clear() VimPlugin.getKey().savedShortcutConflicts.clear()
assertTrue(KeyHandler.getInstance().keyStack.isEmpty())
// Tear down neovim // Tear down neovim
NeovimTesting.tearDown(testInfo) NeovimTesting.tearDown(testInfo)

View File

@ -7,22 +7,40 @@
*/ */
package org.jetbrains.plugins.ideavim.action package org.jetbrains.plugins.ideavim.action
import com.intellij.idea.TestFor
import com.intellij.testFramework.LoggedErrorProcessor
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.keys
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.helper.vimStateMachine import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.plugins.ideavim.ExceptionHandler
import org.jetbrains.plugins.ideavim.OnlyThrowLoggedErrorProcessor
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
import org.jetbrains.plugins.ideavim.exceptionMappingOwner
import org.jetbrains.plugins.ideavim.rangeOf import org.jetbrains.plugins.ideavim.rangeOf
import org.jetbrains.plugins.ideavim.waitAndAssert import org.jetbrains.plugins.ideavim.waitAndAssert
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/** /**
* @author vlan * @author vlan
*/ */
class MacroActionTest : VimTestCase() { class MacroActionTest : VimTestCase() {
@AfterEach
fun tearDown() {
injector.keyGroup.removeKeyMapping(exceptionMappingOwner)
}
// |q| // |q|
@Test @Test
fun testRecordMacro() { fun testRecordMacro() {
@ -178,4 +196,33 @@ class MacroActionTest : VimTestCase() {
""".trimIndent(), """.trimIndent(),
) )
} }
@TestFor(issues = ["VIM-2929"])
@TestWithoutNeovim(reason = SkipNeovimReason.ACTION_COMMAND)
@Test
fun `macro to handler with exception`() {
configureByText(
"""
Lorem Ipsum
Lorem ipsum dolor sit amet,
${c}consectetur adipiscing elit
Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
""".trimIndent()
)
injector.keyGroup.putKeyMapping(MappingMode.NXO, keys("abc"), exceptionMappingOwner, ExceptionHandler(), false)
injector.registerGroup.storeText('k', "abc")
injector.registerGroup.storeText('q', "x@ky")
val exception = assertThrows<Throwable> {
LoggedErrorProcessor.executeWith<Throwable>(OnlyThrowLoggedErrorProcessor) {
typeText("@q")
}
}
assertEquals(ExceptionHandler.exceptionMessage, exception.cause!!.cause!!.message)
assertTrue(KeyHandler.getInstance().keyStack.isEmpty())
}
} }

View File

@ -1025,10 +1025,10 @@ $c tw${c}o
) )
assertState( assertState(
""" """
<selection>one two ${s}one two
three four three four
five six five six
</selection> $se
""".trimIndent(), """.trimIndent(),
) )
} }

View File

@ -12,8 +12,9 @@ package org.jetbrains.plugins.ideavim.action.motion.gn
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.common.Direction import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
@ -85,7 +86,7 @@ class GnNextTextObjectTest : VimTestCase() {
private fun doTestWithSearch(keys: List<KeyStroke>, before: String, after: String) { private fun doTestWithSearch(keys: List<KeyStroke>, before: String, after: String) {
configureByText(before) configureByText(before)
VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
typeText(keys) typeText(keys)
assertState(after) assertState(after)
assertState(Mode.NORMAL()) assertState(Mode.NORMAL())

View File

@ -12,8 +12,9 @@ package org.jetbrains.plugins.ideavim.action.motion.gn
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.common.Direction import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
@ -63,7 +64,7 @@ class GnPreviousTextObjectTest : VimTestCase() {
private fun doTestWithSearch(keys: List<KeyStroke>, before: String, after: String) { private fun doTestWithSearch(keys: List<KeyStroke>, before: String, after: String) {
configureByText(before) configureByText(before)
VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
typeText(keys) typeText(keys)
assertState(after) assertState(after)
assertState(Mode.NORMAL()) assertState(Mode.NORMAL())

View File

@ -11,9 +11,10 @@ import com.intellij.idea.TestFor
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.action.motion.search.SearchWholeWordForwardAction import com.maddyhome.idea.vim.action.motion.search.SearchWholeWordForwardAction
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.common.Direction
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
@ -57,7 +58,7 @@ class VisualSelectNextSearchTest : VimTestCase() {
@Test @Test
fun testWithoutSpaces() { fun testWithoutSpaces() {
configureByText("test<caret>test") configureByText("test<caret>test")
VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
typeText(injector.parser.parseKeys("gn")) typeText(injector.parser.parseKeys("gn"))
assertOffset(7) assertOffset(7)
assertSelection("test") assertSelection("test")

View File

@ -11,9 +11,10 @@ import com.intellij.idea.TestFor
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.action.motion.search.SearchWholeWordForwardAction import com.maddyhome.idea.vim.action.motion.search.SearchWholeWordForwardAction
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.common.Direction
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
@ -54,7 +55,7 @@ class VisualSelectPreviousSearchTest : VimTestCase() {
@Test @Test
fun testWithoutSpaces() { fun testWithoutSpaces() {
configureByText("tes<caret>ttest") configureByText("tes<caret>ttest")
VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
typeText(injector.parser.parseKeys("gN")) typeText(injector.parser.parseKeys("gN"))
assertOffset(0) assertOffset(0)
assertSelection("test") assertSelection("test")

View File

@ -10,6 +10,7 @@ package org.jetbrains.plugins.ideavim.action.motion.search
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.common.Direction import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
@ -167,7 +168,7 @@ class SearchAgainPreviousActionTest : VimTestCase() {
private fun doTestWithSearch(keys: String, before: String, after: String) { private fun doTestWithSearch(keys: String, before: String, after: String) {
doTest(keys, before, after) { doTest(keys, before, after) {
VimPlugin.getSearch().setLastSearchState(it, "all", "", Direction.FORWARDS) VimPlugin.getSearch().setLastSearchState(it.vim, "all", "", Direction.FORWARDS)
} }
} }
} }

View File

@ -10,26 +10,45 @@ package org.jetbrains.plugins.ideavim.ex.implementation.commands
import com.intellij.idea.TestFor import com.intellij.idea.TestFor
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.textarea.TextComponentEditorImpl import com.intellij.openapi.editor.textarea.TextComponentEditorImpl
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.LoggedErrorProcessor
import com.intellij.testFramework.TestLoggerFactory.TestLoggerAssertionError
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.keys
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.helper.StringHelper.parseKeys import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import com.maddyhome.idea.vim.history.HistoryConstants import com.maddyhome.idea.vim.history.HistoryConstants
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import org.jetbrains.plugins.ideavim.ExceptionHandler
import org.jetbrains.plugins.ideavim.OnlyThrowLoggedErrorProcessor
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
import org.jetbrains.plugins.ideavim.assertThrowsLogError
import org.jetbrains.plugins.ideavim.exceptionMappingOwner
import org.jetbrains.plugins.ideavim.waitAndAssert import org.jetbrains.plugins.ideavim.waitAndAssert
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import javax.swing.JTextArea import javax.swing.JTextArea
import kotlin.test.assertEquals
import kotlin.test.assertIs import kotlin.test.assertIs
import kotlin.test.assertTrue
/** /**
* @author vlan * @author vlan
*/ */
class MapCommandTest : VimTestCase() { class MapCommandTest : VimTestCase() {
@AfterEach
fun tearDown() {
injector.keyGroup.removeKeyMapping(exceptionMappingOwner)
}
@TestWithoutNeovim(reason = SkipNeovimReason.UNCLEAR) @TestWithoutNeovim(reason = SkipNeovimReason.UNCLEAR)
@Test @Test
fun testMapKtoJ() { fun testMapKtoJ() {
@ -876,13 +895,14 @@ n ,f <Plug>Foo
indicateErrors = true, indicateErrors = true,
null, null,
) )
val exception = assertThrows<Throwable> { val exception = assertThrowsLogError<TestLoggerAssertionError> {
typeText(injector.parser.parseKeys("t")) typeText(injector.parser.parseKeys("t"))
} }
assertIs<ExException>(exception.cause) // The original exception comes from the LOG.error, so we check the cause assertIs<ExException>(exception.cause) // Exception is wrapped into LOG.error twice
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessageContains("E121: Undefined variable: s:mapping") assertPluginErrorMessageContains("E121: Undefined variable: s:mapping")
editor.caretModel.allCarets.forEach { Disposer.dispose(it) }
} }
// todo keyPresses invoked inside a script should have access to the script context // todo keyPresses invoked inside a script should have access to the script context
@ -923,11 +943,10 @@ n ,f <Plug>Foo
""".trimIndent() """.trimIndent()
configureByJavaText(text) configureByJavaText(text)
val exception = assertThrows<Throwable> { assertThrowsLogError<ExException> {
typeText(commandToKeys("inoremap <expr> <cr> unknownFunction() ? '\\<C-y>' : '\\<C-g>u\\<CR>'")) typeText(commandToKeys("inoremap <expr> <cr> unknownFunction() ? '\\<C-y>' : '\\<C-g>u\\<CR>'"))
typeText(injector.parser.parseKeys("i<CR>")) typeText(injector.parser.parseKeys("i<CR>"))
} }
assertIs<ExException>(exception.cause) // The original exception comes from the LOG.error, so we check the cause
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessageContains("E117: Unknown function: unknownFunction") assertPluginErrorMessageContains("E117: Unknown function: unknownFunction")
@ -1097,4 +1116,32 @@ n ,i <Action>(Back)
Cras id tellus in ex imperdiet egestas. Cras id tellus in ex imperdiet egestas.
""".trimIndent()) """.trimIndent())
} }
@TestFor(issues = ["VIM-2929"])
@TestWithoutNeovim(reason = SkipNeovimReason.ACTION_COMMAND)
@Test
fun `mapping to handler with exception`() {
configureByText(
"""
Lorem Ipsum
Lorem ipsum dolor sit amet,
${c}consectetur adipiscing elit
Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
""".trimIndent()
)
injector.keyGroup.putKeyMapping(MappingMode.NXO, keys("abc"), exceptionMappingOwner, ExceptionHandler(), false)
typeText(commandToKeys("map k abcx"))
val exception = assertThrows<Throwable> {
LoggedErrorProcessor.executeWith<Throwable>(OnlyThrowLoggedErrorProcessor) {
typeText("k")
}
}
assertEquals(ExceptionHandler.exceptionMessage, exception.cause!!.cause!!.message)
assertTrue(KeyHandler.getInstance().keyStack.isEmpty())
}
} }

View File

@ -9,7 +9,9 @@
package org.jetbrains.plugins.ideavim.extension package org.jetbrains.plugins.ideavim.extension
import com.intellij.ide.plugins.PluginManagerCore import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.PlatformTestUtil
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
@ -41,28 +43,23 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class OpMappingTest : VimTestCase() { class OpMappingTest : VimTestCase() {
private var initialized = false
private lateinit var extension: ExtensionBeanClass private lateinit var extension: ExtensionBeanClass
private var disposable: Disposable = Disposer.newDisposable()
@BeforeEach @BeforeEach
override fun setUp(testInfo: TestInfo) { override fun setUp(testInfo: TestInfo) {
super.setUp(testInfo) super.setUp(testInfo)
if (!initialized) {
initialized = true
extension = TestExtension.createBean() extension = TestExtension.createBean()
VimExtension.EP_NAME.point.registerExtension(extension, VimPlugin.getInstance()) VimExtension.EP_NAME.point.registerExtension(extension, disposable)
enableExtensions("TestExtension") enableExtensions("TestExtension")
} }
}
@AfterEach @AfterEach
override fun tearDown(testInfo: TestInfo) { override fun tearDown(testInfo: TestInfo) {
@Suppress("DEPRECATION") Disposer.dispose(disposable)
VimExtension.EP_NAME.point.unregisterExtension(extension) super.tearDown(testInfo)
super.tearDown(super.testInfo)
} }
@Test @Test
@ -138,13 +135,13 @@ class OpMappingTest : VimTestCase() {
typeText(injector.parser.parseKeys("Q")) typeText(injector.parser.parseKeys("Q"))
assertState("I$c found it in a legendary land") assertState("I$c found it in a legendary land")
@Suppress("DEPRECATION") Disposer.dispose(disposable)
VimExtension.EP_NAME.point.unregisterExtension(extension) disposable = Disposer.newDisposable()
assertEmpty(VimPlugin.getKey().getKeyMappingByOwner(extension.instance.owner)) assertEmpty(VimPlugin.getKey().getKeyMappingByOwner(extension.instance.owner))
typeText(injector.parser.parseKeys("Q")) typeText(injector.parser.parseKeys("Q"))
assertState("I$c found it in a legendary land") assertState("I$c found it in a legendary land")
VimExtension.EP_NAME.point.registerExtension(extension, VimPlugin.getInstance()) VimExtension.EP_NAME.point.registerExtension(extension, disposable)
assertEmpty(VimPlugin.getKey().getKeyMappingByOwner(extension.instance.owner)) assertEmpty(VimPlugin.getKey().getKeyMappingByOwner(extension.instance.owner))
enableExtensions("TestExtension") enableExtensions("TestExtension")
typeText(injector.parser.parseKeys("Q")) typeText(injector.parser.parseKeys("Q"))
@ -158,12 +155,12 @@ class OpMappingTest : VimTestCase() {
assertState("I$c found it in a legendary land") assertState("I$c found it in a legendary land")
enterCommand("set noTestExtension") enterCommand("set noTestExtension")
@Suppress("DEPRECATION") Disposer.dispose(disposable)
VimExtension.EP_NAME.point.unregisterExtension(extension) disposable = Disposer.newDisposable()
typeText(injector.parser.parseKeys("Q")) typeText(injector.parser.parseKeys("Q"))
assertState("I$c found it in a legendary land") assertState("I$c found it in a legendary land")
VimExtension.EP_NAME.point.registerExtension(extension, VimPlugin.getInstance()) VimExtension.EP_NAME.point.registerExtension(extension, disposable)
enableExtensions("TestExtension") enableExtensions("TestExtension")
typeText(injector.parser.parseKeys("Q")) typeText(injector.parser.parseKeys("Q"))
assertState("I ${c}found it in a legendary land") assertState("I ${c}found it in a legendary land")
@ -201,19 +198,20 @@ class PlugExtensionsTest : VimTestCase() {
private lateinit var extension: ExtensionBeanClass private lateinit var extension: ExtensionBeanClass
private var disposable: Disposable = Disposer.newDisposable()
@BeforeEach @BeforeEach
override fun setUp(testInfo: TestInfo) { override fun setUp(testInfo: TestInfo) {
super.setUp(testInfo) super.setUp(testInfo)
configureByText("\n") configureByText("\n")
extension = TestExtension.createBean() extension = TestExtension.createBean()
VimExtension.EP_NAME.point.registerExtension(extension, VimPlugin.getInstance()) VimExtension.EP_NAME.point.registerExtension(extension, disposable)
} }
@AfterEach @AfterEach
override fun tearDown(testInfo: TestInfo) { override fun tearDown(testInfo: TestInfo) {
@Suppress("DEPRECATION") Disposer.dispose(disposable)
VimExtension.EP_NAME.point.unregisterExtension(extension)
super.tearDown(super.testInfo) super.tearDown(super.testInfo)
} }
@ -244,19 +242,20 @@ class PlugMissingKeysTest : VimTestCase() {
private lateinit var extension: ExtensionBeanClass private lateinit var extension: ExtensionBeanClass
private var disposable: Disposable = Disposer.newDisposable()
@BeforeEach @BeforeEach
override fun setUp(testInfo: TestInfo) { override fun setUp(testInfo: TestInfo) {
super.setUp(testInfo) super.setUp(testInfo)
configureByText("\n") configureByText("\n")
extension = TestExtension.createBean() extension = TestExtension.createBean()
VimExtension.EP_NAME.point.registerExtension(extension, VimPlugin.getInstance()) VimExtension.EP_NAME.point.registerExtension(extension, disposable)
} }
@AfterEach @AfterEach
override fun tearDown(testInfo: TestInfo) { override fun tearDown(testInfo: TestInfo) {
@Suppress("DEPRECATION") Disposer.dispose(disposable)
VimExtension.EP_NAME.point.unregisterExtension(extension)
super.tearDown(super.testInfo) super.tearDown(super.testInfo)
} }

View File

@ -14,16 +14,16 @@ import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.ConstantNode import com.intellij.codeInsight.template.impl.ConstantNode
import com.intellij.codeInsight.template.impl.TemplateManagerImpl import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
import com.maddyhome.idea.vim.group.visual.VimVisualTimer import com.maddyhome.idea.vim.group.visual.VimVisualTimer
import com.maddyhome.idea.vim.helper.VimBehaviorDiffers import com.maddyhome.idea.vim.helper.VimBehaviorDiffers
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.listener.VimListenerManager import com.maddyhome.idea.vim.listener.VimListenerManager
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.OptionConstants import com.maddyhome.idea.vim.options.OptionConstants
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.selectionType
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestOptionConstants import org.jetbrains.plugins.ideavim.TestOptionConstants
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
@ -33,6 +33,7 @@ import org.jetbrains.plugins.ideavim.impl.TraceOptions
import org.jetbrains.plugins.ideavim.impl.VimOption import org.jetbrains.plugins.ideavim.impl.VimOption
import org.jetbrains.plugins.ideavim.waitAndAssert import org.jetbrains.plugins.ideavim.waitAndAssert
import org.jetbrains.plugins.ideavim.waitAndAssertMode import org.jetbrains.plugins.ideavim.waitAndAssertMode
import kotlin.test.assertNull
@TraceOptions(TestOptionConstants.selectmode) @TraceOptions(TestOptionConstants.selectmode)
class IdeaVisualControlTest : VimTestCase() { class IdeaVisualControlTest : VimTestCase() {
@ -764,6 +765,23 @@ class IdeaVisualControlTest : VimTestCase() {
assertCaretsVisualAttributes() assertCaretsVisualAttributes()
} }
@OptionTest(VimOption(TestOptionConstants.selectmode, limitedValues = [""]))
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
fun `test control selection interruption`() {
configureByText(
"""
Lorem Ipsum
I $s${c}found$se it in a legendary land
consectetur adipiscing elit
""".trimIndent(),
)
IdeaSelectionControl.controlNonVimSelectionChange(fixture.editor)
typeText(injector.parser.parseKeys("V"))
assertNull(VimVisualTimer.swingTimer)
}
private fun startDummyTemplate() { private fun startDummyTemplate() {
TemplateManagerImpl.setTemplateTesting(fixture.testRootDisposable) TemplateManagerImpl.setTemplateTesting(fixture.testRootDisposable)
val templateManager = TemplateManager.getInstance(fixture.project) val templateManager = TemplateManager.getInstance(fixture.project)

View File

@ -0,0 +1,37 @@
/*
* 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 org.jetbrains.plugins.ideavim.listener
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.listener.VimListenerTestObject
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class VimListenersTest : VimTestCase() {
@AfterEach
fun tearDown() {
VimListenerTestObject.disposedCounter = 0
VimListenerTestObject.enabled = false
}
@Test
fun `disposable is called on plugin disable`() {
configureByText("XYZ")
VimListenerTestObject.disposedCounter = 0
VimListenerTestObject.enabled = true
VimPlugin.setEnabled(false)
assertEquals(1, VimListenerTestObject.disposedCounter)
VimPlugin.setEnabled(true)
}
}

View File

@ -12,11 +12,14 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.textarea.TextComponentEditorImpl import com.intellij.openapi.editor.textarea.TextComponentEditorImpl
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.junit5.TestDisposable import com.intellij.testFramework.junit5.TestDisposable
import com.intellij.testFramework.replaceService import com.intellij.testFramework.replaceService
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.AfterEach
import org.mockito.Mockito import org.mockito.Mockito
import javax.swing.JTextArea import javax.swing.JTextArea
@ -28,6 +31,11 @@ open class MockTestCase : VimTestCase() {
val editorStub = TextComponentEditorImpl(null, JTextArea()).vim val editorStub = TextComponentEditorImpl(null, JTextArea()).vim
val contextStub: ExecutionContext = DataContext.EMPTY_CONTEXT.vim val contextStub: ExecutionContext = DataContext.EMPTY_CONTEXT.vim
@AfterEach
fun tearDown() {
editorStub.carets().forEach { Disposer.dispose(it.ij) }
}
fun <T : Any> mockService(service: Class<T>): T { fun <T : Any> mockService(service: Class<T>): T {
val mock = Mockito.mock(service) val mock = Mockito.mock(service)
val applicationManager = ApplicationManager.getApplication() val applicationManager = ApplicationManager.getApplication()

View File

@ -22,9 +22,11 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.EffectiveOptionValueChangeListener import com.maddyhome.idea.vim.options.EffectiveOptionValueChangeListener
import com.maddyhome.idea.vim.options.NumberOption
import com.maddyhome.idea.vim.options.OptionAccessScope import com.maddyhome.idea.vim.options.OptionAccessScope
import com.maddyhome.idea.vim.options.OptionDeclaredScope import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.StringOption import com.maddyhome.idea.vim.options.StringOption
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.TestWithoutNeovim
@ -34,9 +36,10 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInfo import org.junit.jupiter.api.TestInfo
import javax.swing.SwingConstants import javax.swing.SwingConstants
import kotlin.test.assertContentEquals import kotlin.test.assertEquals
private const val defaultValue = "defaultValue" private const val defaultValue = "defaultValue"
private const val defaultNumberValue = 10
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION) @TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
class EffectiveOptionChangeListenerTest : VimTestCase() { class EffectiveOptionChangeListenerTest : VimTestCase() {
@ -109,10 +112,17 @@ class EffectiveOptionChangeListenerTest : VimTestCase() {
return option return option
} }
private fun addNumberOption(scope: OptionDeclaredScope): NumberOption {
val option = NumberOption(optionName, scope, optionName, defaultNumberValue)
injector.optionGroup.addOption(option)
injector.optionGroup.addEffectiveOptionValueChangeListener(option, Listener)
return option
}
private fun assertNotifiedEditors(vararg editors: Editor) { private fun assertNotifiedEditors(vararg editors: Editor) {
val sortedExpected = editors.sortedBy { it.virtualFile!!.path }.toTypedArray() val expected = editors.toSet()
val sortedActual = Listener.notifiedEditors.sortedBy { it.virtualFile!!.path }.toTypedArray() val actual = Listener.notifiedEditors.toSet()
assertContentEquals(sortedExpected, sortedActual) assertEquals(expected, actual)
} }
private fun assertNoNotifications() = assertNotifiedEditors() private fun assertNoNotifications() = assertNotifiedEditors()
@ -272,10 +282,23 @@ class EffectiveOptionChangeListenerTest : VimTestCase() {
@Test @Test
fun `test listener called for all editors when locally modified global-local local-to-buffer option changes at effective scope`() { fun `test listener called for all editors when locally modified global-local local-to-buffer option changes at effective scope`() {
val option = addOption(OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_BUFFER) val option = addOption(OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_BUFFER)
injector.optionGroup.setOptionValue(option, OptionAccessScope.LOCAL(otherBufferWindow.vim), VimString("localValue")) injector.optionGroup.setOptionValue(option, OptionAccessScope.LOCAL(originalEditor.vim), VimString("localValue"))
Listener.notifiedEditors.clear() Listener.notifiedEditors.clear()
injector.optionGroup.setOptionValue(option, OptionAccessScope.EFFECTIVE(otherBufferWindow.vim), VimString("newValue")) injector.optionGroup.setOptionValue(option, OptionAccessScope.EFFECTIVE(originalEditor.vim), VimString("newValue"))
assertNotifiedEditors(originalEditor, splitWindow, otherBufferWindow)
}
@Test
fun `test listener called for all editors when locally modified number global-local local-to-buffer option changes at effective scope`() {
// When a number (and therefore also toggle) global-local option is set at effective scope, the local value is not
// reset to [Option.unsetValue] but to a copy of the new value. The local editor(s) should still be notified.
val option = addNumberOption(OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_BUFFER)
injector.optionGroup.setOptionValue(option, OptionAccessScope.LOCAL(originalEditor.vim), VimInt(100))
Listener.notifiedEditors.clear()
injector.optionGroup.setOptionValue(option, OptionAccessScope.EFFECTIVE(originalEditor.vim), VimInt(200))
assertNotifiedEditors(originalEditor, splitWindow, otherBufferWindow) assertNotifiedEditors(originalEditor, splitWindow, otherBufferWindow)
} }
@ -327,6 +350,19 @@ class EffectiveOptionChangeListenerTest : VimTestCase() {
assertNotifiedEditors(originalEditor, splitWindow, otherBufferWindow) assertNotifiedEditors(originalEditor, splitWindow, otherBufferWindow)
} }
@Test
fun `test listener called for all editors when locally modified number global-local local-to-window option changes at effective scope`() {
// When a number (and therefore also toggle) global-local option is set at effective scope, the local value is not
// reset to [Option.unsetValue] but to a copy of the new value. The local editor(s) should still be notified.
val option = addNumberOption(OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_WINDOW)
injector.optionGroup.setOptionValue(option, OptionAccessScope.LOCAL(originalEditor.vim), VimInt(100))
Listener.notifiedEditors.clear()
injector.optionGroup.setOptionValue(option, OptionAccessScope.EFFECTIVE(originalEditor.vim), VimInt(200))
assertNotifiedEditors(originalEditor, splitWindow, otherBufferWindow)
}
@Test @Test
fun `test listener not called for locally modified editor when global-local local-to-window option changes at global scope`() { fun `test listener not called for locally modified editor when global-local local-to-window option changes at global scope`() {
val option = addOption(OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_WINDOW) val option = addOption(OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_WINDOW)

View File

@ -129,6 +129,23 @@ class OptionAccessScopeTest: VimTestCase() {
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim))) assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim)))
} }
@Test
fun `test set local-to-buffer option at effective scope to current value changes global value`() {
val defaultValue = VimInt(10)
val option = NumberOption(OPTION_NAME, OptionDeclaredScope.LOCAL_TO_BUFFER, OPTION_NAME, defaultValue)
injector.optionGroup.addOption(option)
val effectiveValue = VimInt(100)
injector.optionGroup.setOptionValue(option, OptionAccessScope.LOCAL(fixture.editor.vim), effectiveValue)
assertEquals(defaultValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.GLOBAL(fixture.editor.vim)))
injector.optionGroup.setOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim), effectiveValue)
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.GLOBAL(fixture.editor.vim)))
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.LOCAL(fixture.editor.vim)))
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim)))
}
// LOCAL_TO_WINDOW // LOCAL_TO_WINDOW
@Test @Test
@ -173,6 +190,23 @@ class OptionAccessScopeTest: VimTestCase() {
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim))) assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim)))
} }
@Test
fun `test set local-to-window option at effective scope to current value changes global value`() {
val defaultValue = VimInt(10)
val option = NumberOption(OPTION_NAME, OptionDeclaredScope.LOCAL_TO_WINDOW, OPTION_NAME, defaultValue)
injector.optionGroup.addOption(option)
val effectiveValue = VimInt(100)
injector.optionGroup.setOptionValue(option, OptionAccessScope.LOCAL(fixture.editor.vim), effectiveValue)
assertEquals(defaultValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.GLOBAL(fixture.editor.vim)))
injector.optionGroup.setOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim), effectiveValue)
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.GLOBAL(fixture.editor.vim)))
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.LOCAL(fixture.editor.vim)))
assertEquals(effectiveValue, injector.optionGroup.getOptionValue(option, OptionAccessScope.EFFECTIVE(fixture.editor.vim)))
}
// Global-local is tricky. The local value is initially not set, and the global value is used until it is. For string // Global-local is tricky. The local value is initially not set, and the global value is used until it is. For string
// options, this is represented by the local value showing as an empty string. Number options usually use the value -1 // options, this is represented by the local value showing as an empty string. Number options usually use the value -1

View File

@ -8,14 +8,13 @@
package ui package ui
import com.automation.remarks.junit.VideoRule import com.automation.remarks.junit5.Video
import com.automation.remarks.video.annotations.Video
import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.fixtures.ContainerFixture import com.intellij.remoterobot.fixtures.ContainerFixture
import com.intellij.remoterobot.steps.CommonSteps
import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.keyboard import com.intellij.remoterobot.utils.keyboard
import org.assertj.swing.core.MouseButton import org.assertj.swing.core.MouseButton
import org.junit.Rule
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import ui.pages.Editor import ui.pages.Editor
import ui.pages.IdeaFrame import ui.pages.IdeaFrame
@ -25,6 +24,7 @@ import ui.pages.dialog
import ui.pages.editor import ui.pages.editor
import ui.pages.gutter import ui.pages.gutter
import ui.pages.idea import ui.pages.idea
import ui.pages.searchEverywhere
import ui.pages.welcomeFrame import ui.pages.welcomeFrame
import ui.utils.JavaExampleSteps import ui.utils.JavaExampleSteps
import ui.utils.StepsLogger import ui.utils.StepsLogger
@ -38,6 +38,7 @@ import ui.utils.tripleClickOnRight
import ui.utils.uiTest import ui.utils.uiTest
import ui.utils.vimExit import ui.utils.vimExit
import java.awt.Point import java.awt.Point
import java.awt.event.KeyEvent
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -47,14 +48,19 @@ class UiTests {
StepsLogger.init() StepsLogger.init()
} }
@Rule private lateinit var commonSteps: CommonSteps
@JvmField
var videoRule = VideoRule() private val testTextForEditor = """
|One Two
|Three Four
|Five
""".trimMargin()
@Test @Test
@Video @Video
fun ideaVimTest() = uiTest("ideaVimTest") { fun ideaVimTest() = uiTest("ideaVimTest") {
val sharedSteps = JavaExampleSteps(this) val sharedSteps = JavaExampleSteps(this)
commonSteps = CommonSteps(this)
startNewProject() startNewProject()
Thread.sleep(1000) Thread.sleep(1000)
@ -63,16 +69,11 @@ class UiTests {
Thread.sleep(1000) Thread.sleep(1000)
idea { idea {
waitSmartMode()
createFile("MyDoc.txt", this@uiTest) createFile("MyDoc.txt", this@uiTest)
val editor = editor("MyDoc.txt") { val editor = editor("MyDoc.txt") {
step("Write a text") { step("Write a text") {
injectText( injectText(testTextForEditor)
"""
|One Two
|Three Four
|Five
""".trimMargin(),
)
} }
} }
testSelectTextWithDelay(editor) testSelectTextWithDelay(editor)
@ -89,6 +90,11 @@ class UiTests {
testClickRightFromLineEnd(editor) testClickRightFromLineEnd(editor)
testClickOnWord(editor) testClickOnWord(editor)
testGutterClick(editor) testGutterClick(editor)
testAddNewLineInNormalMode(editor)
testMappingToCtrlOrAltEnter(editor)
`simple enter in insert mode`(editor)
testMilticaretEnter(editor)
`simple enter in select mode`(editor)
reenableIdeaVim(editor) reenableIdeaVim(editor)
createFile("MyTest.java", this@uiTest) createFile("MyTest.java", this@uiTest)
@ -96,7 +102,7 @@ class UiTests {
step("Write a text") { step("Write a text") {
injectText( injectText(
""" """
|class Main { |class MyTest {
| public static void main() { | public static void main() {
| System.out.println("Hello"); | System.out.println("Hello");
| } | }
@ -115,7 +121,6 @@ class UiTests {
private fun closeUnrelated(sharedSteps: JavaExampleSteps) { private fun closeUnrelated(sharedSteps: JavaExampleSteps) {
with(sharedSteps) { with(sharedSteps) {
closeIdeaVimDialog()
closeTipOfTheDay() closeTipOfTheDay()
closeAllTabs() closeAllTabs()
} }
@ -126,7 +131,7 @@ class UiTests {
createNewProjectLink.click() createNewProjectLink.click()
dialog("New Project") { dialog("New Project") {
findText("Java").click() findText("Java").click()
checkBox("Add sample code").select() checkBox("Add sample code").unselect()
button("Create").click() button("Create").click()
} }
} }
@ -195,9 +200,11 @@ class UiTests {
private fun IdeaFrame.testTrackActionId(editor: Editor) { private fun IdeaFrame.testTrackActionId(editor: Editor) {
remoteRobot.invokeActionJs("GotoAction") remoteRobot.invokeActionJs("GotoAction")
editor.keyboard {
enterText("IdeaVim: Track Action Ids") val searchEverywhere = this@testTrackActionId.searchEverywhere()
enter()
commonSteps.invokeAction("VimFindActionIdAction")
keyboard {
escape() escape()
} }
@ -211,7 +218,7 @@ class UiTests {
assertEquals( assertEquals(
""" """
|EditorEscapeclass Main { |EditorEscapeclass MyTest {
| public static void main() { | public static void main() {
| if (true) { | if (true) {
| System.out.println("Hello"); | System.out.println("Hello");
@ -231,7 +238,7 @@ class UiTests {
private fun IdeaFrame.createFile(fileName: String, remoteRobot: RemoteRobot) { private fun IdeaFrame.createFile(fileName: String, remoteRobot: RemoteRobot) {
step("Create $fileName file") { step("Create $fileName file") {
with(projectViewTree) { with(projectViewTree) {
setExpandTimeout(15_000) setExpandTimeout(30_000)
expand(projectName, "src") expand(projectName, "src")
findText("src").click(MouseButton.RIGHT_BUTTON) findText("src").click(MouseButton.RIGHT_BUTTON)
} }
@ -510,4 +517,187 @@ class UiTests {
vimExit() vimExit()
} }
// For VIM-3159
private fun ContainerFixture.testAddNewLineInNormalMode(editor: Editor) {
println("Run testAddNewLineInNormalMode...")
commonSteps.invokeAction("EditorStartNewLineBefore")
assertEquals(
"""
|
|One Two
|Three Four
|Five
""".trimMargin(), editor.text
)
editor.injectText(testTextForEditor)
commonSteps.invokeAction("EditorStartNewLine")
assertEquals(
"""
|One Two
|
|Three Four
|Five
""".trimMargin(), editor.text
)
editor.injectText(testTextForEditor)
vimExit()
}
// For VIM-3190
private fun ContainerFixture.testMappingToCtrlOrAltEnter(editor: Editor) {
println("Run testMappingToCtrlOrAltEnter...")
keyboard {
enterText(":nmap <C-Enter> k")
enter()
enterText(":nmap <A-Enter> G")
enter()
}
// Set up initial position
keyboard {
enterText("jll")
}
assertEquals(10, editor.caretOffset)
// Checking C-ENTER
keyboard {
pressing(KeyEvent.VK_CONTROL) { enter() }
}
assertEquals(2, editor.caretOffset)
// Checking A-ENTER
keyboard {
pressing(KeyEvent.VK_ALT) { enter() }
}
assertEquals(19, editor.caretOffset)
vimExit()
}
// For VIM-3186
private fun ContainerFixture.testMilticaretEnter(editor: Editor) {
println("Run testMilticaretEnter...")
keyboard {
pressing(KeyEvent.VK_ALT) {
pressing(KeyEvent.VK_SHIFT) {
findText("One").click()
findText("Three").click()
findText("Five").click()
}
}
enterText("A")
enter()
}
assertEquals(3, editor.caretCount)
assertEquals(
"""
|One Two
|
|Three Four
|
|Five
|
""".trimMargin(), editor.text
)
// Reset state
keyboard {
escape()
escape()
}
assertEquals(1, editor.caretCount)
editor.injectText(testTextForEditor)
vimExit()
}
private fun ContainerFixture.`simple enter in insert mode`(editor: Editor) {
println("Run test 'simple enter in insert mode'...")
// Start of file
keyboard {
enterText("i")
enter()
}
assertEquals(
"""
|
|One Two
|Three Four
|Five
""".trimMargin(),
editor.text
)
// Middle of file
findText("Four").click()
keyboard { enter() }
assertEquals(
"""
|
|One Two
|Three
|Four
|Five
""".trimMargin(),
editor.text
)
// End of file
val fivePoint = findText("Five").point
val endOfLine = Point(fivePoint.x + 50, fivePoint.y)
click(endOfLine)
keyboard { enter() }
assertEquals(
"""
|
|One Two
|Three
|Four
|Five
|
""".trimMargin(),
editor.text
)
editor.injectText(testTextForEditor)
vimExit()
}
private fun ContainerFixture.`simple enter in select mode`(editor: Editor) {
println("Run test 'simple enter in select mode'...")
findText("Four").doubleClick()
keyboard {
pressing(KeyEvent.VK_CONTROL) { enterText("g") }
enter()
}
assertEquals(
"""
|One Two
|Three
|
|Five
""".trimMargin(),
editor.text
)
editor.injectText(testTextForEditor)
vimExit()
}
} }

View File

@ -44,6 +44,9 @@ class Editor(
val caretOffset: Int val caretOffset: Int
get() = callJs("component.getEditor().getCaretModel().getOffset()", runInEdt = true) get() = callJs("component.getEditor().getCaretModel().getOffset()", runInEdt = true)
val caretCount: Int
get() = callJs("component.getEditor().getCaretModel().getCaretCount()", runInEdt = true)
val isBlockCursor: Boolean val isBlockCursor: Boolean
// get() = callJs("component.getEditor().getSettings().isBlockCursor()", true) // get() = callJs("component.getEditor().getSettings().isBlockCursor()", true)
// Doesn't work at the moment because remote robot can't resolve classes from a plugin classloader // Doesn't work at the moment because remote robot can't resolve classes from a plugin classloader

View File

@ -51,6 +51,14 @@ class IdeaFrame(
} }
} }
fun waitSmartMode(timeout: Duration = Duration.ofMinutes(5)) {
step("Wait for smart mode") {
waitFor(duration = timeout, interval = Duration.ofSeconds(5)) {
isDumbMode().not()
}
}
}
private fun isDumbMode(): Boolean { private fun isDumbMode(): Boolean {
return callJs("com.intellij.openapi. project.DumbService.isDumb(component.project);", true) return callJs("com.intellij.openapi. project.DumbService.isDumb(component.project);", true)
} }

View File

@ -0,0 +1,31 @@
/*
* Copyright 2003-2024 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 ui.pages
import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.CommonContainerFixture
import com.intellij.remoterobot.fixtures.ContainerFixture
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.search.locators.byXpath
@JvmOverloads
fun ContainerFixture.searchEverywhere(function: SearchEverywhere.() -> Unit = {}): SearchEverywhere {
return find<SearchEverywhere>(
byXpath("Search Everywhere", "//div[@accessiblename='Search everywhere' and @class='SearchEverywhereUI']"),
)
.apply { runJs("robot.moveMouse(component);") }
.apply(function)
}
@FixtureName("SearchEverywhere")
class SearchEverywhere(
remoteRobot: RemoteRobot,
remoteComponent: RemoteComponent,
) : CommonContainerFixture(remoteRobot, remoteComponent)

View File

@ -17,19 +17,11 @@ import com.intellij.remoterobot.utils.Keyboard
import ui.pages.DialogFixture import ui.pages.DialogFixture
import ui.pages.DialogFixture.Companion.byTitle import ui.pages.DialogFixture.Companion.byTitle
import ui.pages.IdeaFrame import ui.pages.IdeaFrame
import ui.pages.dialog
import ui.pages.idea
class JavaExampleSteps(private val remoteRobot: RemoteRobot) { class JavaExampleSteps(private val remoteRobot: RemoteRobot) {
@Suppress("unused") @Suppress("unused")
private val keyboard: Keyboard = Keyboard(remoteRobot) private val keyboard: Keyboard = Keyboard(remoteRobot)
fun closeIdeaVimDialog() = optionalStep("Close Idea Vim dialog if it appears") {
remoteRobot.idea {
dialog("IdeaVim") { button("Yes").click() }
}
}
fun closeTipOfTheDay() = optionalStep("Close Tip of the Day if it appears") { fun closeTipOfTheDay() = optionalStep("Close Tip of the Day if it appears") {
val idea: IdeaFrame = remoteRobot.find(IdeaFrame::class.java) val idea: IdeaFrame = remoteRobot.find(IdeaFrame::class.java)
idea.dumbAware { idea.dumbAware {

View File

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

View File

@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.handler.VimActionHandler
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL, Mode.VISUAL])
public class UndoAction : VimActionHandler.SingleExecution(), ComplicatedKeysAction { public class UndoAction : VimActionHandler.SingleExecution(), ComplicatedKeysAction {
override val keyStrokesSet: Set<List<KeyStroke>> = setOf( override val keyStrokesSet: Set<List<KeyStroke>> = setOf(
injector.parser.parseKeys("u"), injector.parser.parseKeys("u"),

View File

@ -28,7 +28,7 @@ import java.util.*
public class AutoIndentLinesVisualAction : VisualOperatorActionHandler.ForEachCaret() { public class AutoIndentLinesVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_MOT_LINEWISE, CommandFlags.FLAG_EXIT_VISUAL) override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_MOT_LINEWISE)
override fun executeAction( override fun executeAction(
editor: VimEditor, editor: VimEditor,

View File

@ -8,29 +8,23 @@
package com.maddyhome.idea.vim.action.change.change package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.CharacterHelper import com.maddyhome.idea.vim.helper.CharacterHelper
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
/** /**
* @author vlan * @author vlan
*/ */
@CommandOrMotion(keys = ["u"], modes = [Mode.VISUAL]) @CommandOrMotion(keys = [], modes = [])
public class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() { public class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_EXIT_VISUAL)
override fun executeAction( override fun executeAction(
editor: VimEditor, editor: VimEditor,
caret: VimCaret, caret: VimCaret,

View File

@ -14,13 +14,10 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.CharacterHelper import com.maddyhome.idea.vim.helper.CharacterHelper
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
/** /**
* @author vlan * @author vlan
@ -29,8 +26,6 @@ import java.util.*
public class ChangeCaseToggleVisualAction : VisualOperatorActionHandler.ForEachCaret() { public class ChangeCaseToggleVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_EXIT_VISUAL)
override fun executeAction( override fun executeAction(
editor: VimEditor, editor: VimEditor,
caret: VimCaret, caret: VimCaret,

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