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

Compare commits

..

227 Commits

Author SHA1 Message Date
57ddf2083e Set plugin version to chylex-35 2024-06-04 13:26:12 +02:00
aad2287433 Revert "Factor disposable objects on editor opening"
This reverts commit 1fa78935
2024-06-04 13:26:11 +02:00
a348428422 Fix(VIM-3364): Exception with mapped Generate action 2024-06-04 13:26:11 +02:00
c4c66c194a Apply scrolloff after executing native IDEA actions 2024-06-04 13:26:11 +02:00
e065783486 Stay on same line after reindenting 2024-06-04 13:26:11 +02:00
dae5e3a8fd Update search register when using f/t 2024-06-04 13:26:11 +02:00
bb3b67f611 Automatically add unambiguous imports after running a macro 2024-06-04 13:26:11 +02:00
bf69c8b4a6 Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2024-06-04 13:26:11 +02:00
49d1d2d270 Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2024-06-04 13:26:11 +02:00
96cdf1c26d Add support for count for visual and line motion surround 2024-06-04 13:20:29 +02:00
365bbce9a0 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2024-06-04 13:20:29 +02:00
1f8a580b7f Fix(VIM-696) Restore visual mode after undo/redo, and disable incompatible actions 2024-06-04 13:20:27 +02:00
fa9529eaa5 Respect count with <Action> mappings 2024-06-04 13:18:11 +02:00
0b29a4704b Change matchit plugin to use HTML patterns in unrecognized files 2024-06-04 13:18:11 +02:00
92b9046d6c Reset insert mode when switching active editor 2024-06-04 13:18:11 +02:00
f5b120ac01 Remove update checker 2024-06-04 13:18:11 +02:00
70ea63c0ba Set custom plugin version 2024-06-04 13:18:11 +02:00
Matt Ellis
d00bd8bb25 Fix incsearch highlights with operator count
E.g. `2"a3"b4"c5d6/foo` will now highlight the current match correctly
2024-06-03 11:54:48 +03:00
Matt Ellis
a66e44d835 Fix [count]: initial range text in ex field
Fixes regression from changes in ex field handling
2024-06-03 11:54:48 +03:00
Matt Ellis
ce05317634 Maintain Visual when cancelling search entry 2024-06-03 11:54:48 +03:00
Matt Ellis
33d88d55c9 Remove unused branch in SortCommand
Commands are executed in Normal mode, although there may be multiple carets
2024-06-03 11:54:48 +03:00
Matt Ellis
a31a4a8ca7 Share ex entry set up with filter commands 2024-06-03 11:54:48 +03:00
Matt Ellis
c34d000b91 Reintroduce ProcessGroup.startExEntry
We need to share the implementation between starting an Ex command, and starting a filter command, which is just an Ex command with initial text
2024-06-03 11:54:48 +03:00
Matt Ellis
a2bfe950fa Fix move command moving to current line
Removes a workaround that would break moving a range to the current line because it would always move the caret to the start of the range. Now positions the caret to the start of the selection if there is one. This also means we can remove the SAVE_VISUAL flag from JoinLinesCommand

Fixes VIM-2936
2024-06-03 11:54:48 +03:00
Matt Ellis
b44f40acd6 Fix incorrect output for line() in Normal mode 2024-06-03 11:54:48 +03:00
Matt Ellis
5169093bbf Clear ex output after assert and fix tests
This ensures that a failing action doesn't pass due to previous state
2024-06-03 11:54:48 +03:00
Matt Ellis
b15b1cd3f8 Fix range with missing last address
Fixes VIM-992
2024-06-03 11:54:48 +03:00
Matt Ellis
f7b6a97435 Support 0 as part of a range address
Fixes VIM-1137
2024-06-03 11:54:48 +03:00
Matt Ellis
f64c99c406 Support incsearch highlighting for global command
Fixes VIM-2891
2024-06-03 11:54:48 +03:00
Matt Ellis
f552e43c5b Refactor handling of default range
Specify a default range instead of default line for count.
2024-06-03 11:54:48 +03:00
Matt Ellis
4798198e41 Minor refactorings 2024-06-03 11:54:48 +03:00
Matt Ellis
a59de4ce05 Add tests and fixes for yank command
Handles validation for count and ensures correct behaviour for registers.
2024-06-03 11:54:48 +03:00
Matt Ellis
2dea525665 Add tests and fixes for join command
Handles validation for count and positions caret in the correct place. Also handles join with visual multicaret scenarios.
2024-06-03 11:54:48 +03:00
Matt Ellis
8ecb1f7296 Add tests and fixes for print command
Handles validation of count and correctly moves caret to end of range after execution. Also fix issue where the results of :print are accumulated and not cleared.

Fixes VIM-2570
2024-06-03 11:54:48 +03:00
Matt Ellis
9ca9530061 Add tests and fixes for shift commands
Shift left and right now work with counts, validate the counts and move the caret to the correct end position
2024-06-03 11:54:48 +03:00
Matt Ellis
54e27afc3f Simplify getCount functions
Made it explicit to get the count from argument and/or range. Default count is not passed, because it was never used. Added some tests where possible, but hard to test select file and friends
2024-06-03 11:54:48 +03:00
Matt Ellis
5e67032655 Add tests and fixes for delete lines command
Validates register before use and correctly uses register and count
2024-06-03 11:54:48 +03:00
Matt Ellis
cb37f6df63 Add tests and fixes for goto line command
Correctly handles some validation, and also allows going to line zero
2024-06-03 11:54:48 +03:00
Matt Ellis
ca3a18cf37 Add tests for goto character command
Also start to refactor handling of count
2024-06-03 11:54:48 +03:00
Matt Ellis
cac36627aa Support 0 in copy command to copy to top of file 2024-06-03 11:54:48 +03:00
Matt Ellis
6404e1127e Remove incorrect VimBehaviourDiffers annotation 2024-06-03 11:54:48 +03:00
Matt Ellis
e762a3093b Range is already normalised 2024-06-03 11:54:48 +03:00
Matt Ellis
739ac2ae5e Simplify getting address from argument 2024-06-03 11:54:48 +03:00
Matt Ellis
77c364a2d0 Move count handling out of range into command 2024-06-03 11:54:48 +03:00
Matt Ellis
103cd9f5ce Fix off-by-one error in count
Count needs to be one-based, lines must be zero-based. So store addresses as one-based until processed
2024-06-03 11:54:48 +03:00
Matt Ellis
99859fe857 Rename test classes
Also ensure that test derives from VimTestCase so that injector is correctly initialised
2024-06-03 11:54:48 +03:00
Matt Ellis
4778995f3b Remove unnecessary getFirstLine function 2024-06-03 11:54:48 +03:00
Matt Ellis
0a14150840 Extract TextRange from Ex range class 2024-06-03 11:54:48 +03:00
Matt Ellis
e8ffc0313f Remove Range overloads that don't require a caret
Provide caret when calling from Command
2024-06-03 11:54:48 +03:00
Matt Ellis
7dbd3886b1 Introduce addresses for current line and last line
Remove mutation from LineAddress
2024-06-03 11:54:48 +03:00
Matt Ellis
8c83164d76 Remove mutable state from Range
Sometimes we cache things, and other times it's relative to a passed caret. Let's always calculate it
2024-06-03 11:54:48 +03:00
Matt Ellis
076aab1ccf Rename Ranges, and ExRanges.kt 2024-06-03 11:54:48 +03:00
Matt Ellis
751f51c88f Rename Range and related classes to Address
An address evaluates to a line, and a range is a collection of addresses
2024-06-03 11:54:48 +03:00
Filipp Vakhitov
ab7359ffd3 Fix options test for Windows
Thanks @citizenmatt for this patch
2024-05-29 18:48:05 +03:00
Alex Plate
d795d70041 Disable some tests due to VIM-3376 2024-05-29 18:33:31 +03:00
aleksei.plate@jetbrains.com
1320034e7e TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Publish EAP Build from branch' build configuration were updated 2024-05-29 12:45:26 +00:00
Alex Plate
79a216043f Use a ReleasesVcsRoot for the ReleaseEapFromBranch configuration 2024-05-29 15:42:24 +03:00
Alex Plate
c508a3cc32 Add a build configuration that creates a EAP release from the release branch 2024-05-29 15:40:25 +03:00
Alex Plate
d40d8f34c5 Apply patches for TeamCity configs 2024-05-29 15:18:15 +03:00
Alex Pláte
b561a13e7c Merge pull request #893 from JetBrains/fleet
Moving away from Swing TextAction to KeyHandler
2024-05-29 14:16:44 +03:00
aleksei.plate@jetbrains.com
71bcee2a07 TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:40:30 +00:00
aleksei.plate@jetbrains.com
6c490b1baa TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:40:09 +00:00
aleksei.plate@jetbrains.com
4ab6a36aca TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:39:01 +00:00
aleksei.plate@jetbrains.com
c4355841ad TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:38:13 +00:00
aleksei.plate@jetbrains.com
f163946e38 TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:37:43 +00:00
Filipp Vakhitov
7abb1fd630 Finalizing merge 2024-05-28 23:35:31 +03:00
aleksei.plate@jetbrains.com
f78d3387a5 TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:35:10 +00:00
aleksei.plate@jetbrains.com
69e2e3c047 TeamCity change in 'Ideavim' project: 'IdeaVim Releases' VCS root was updated 2024-05-28 20:34:19 +00:00
aleksei.plate@jetbrains.com
52832cbe22 TeamCity change in 'Ideavim' project: 'IdeaVim Releases' VCS root was updated 2024-05-28 20:32:36 +00:00
aleksei.plate@jetbrains.com
82ca2be51c TeamCity change in 'Ideavim' project: 'IdeaVim Releases' VCS root was updated 2024-05-28 20:30:53 +00:00
aleksei.plate@jetbrains.com
8552bb8a17 TeamCity change in 'Ideavim' project: 'IdeaVim Releases' VCS root was updated 2024-05-28 20:30:05 +00:00
Alex Plate
b49a38b6d0 Rename ReleasesVcsRoot 2024-05-28 23:24:03 +03:00
Alex Plate
ad0751677a Add a special VCS root for releases 2024-05-28 23:18:45 +03:00
aleksei.plate@jetbrains.com
dd8bb20ba9 TeamCity change in 'Ideavim / IdeaVim releases' project: VCS roots of 'EXP: Print release branch' build configuration were updated 2024-05-28 20:13:24 +00:00
Alex Plate
03f5f27901 Experiment: print release branch in TC 2024-05-28 23:08:02 +03:00
Filipp Vakhitov
9e3ca56afd Merge branch 'refs/heads/master' into fleet
# Conflicts:
#	src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java
#	vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/LeaveCommandLineAction.kt
#	vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/search/SearchEntryFwdAction.kt
#	vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/search/SearchEntryRevAction.kt
#	vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt
2024-05-28 22:19:17 +03:00
Filipp Vakhitov
5379528e3e Minor post-review PR improvements 2024-05-28 21:52:43 +03:00
Filipp Vakhitov
12201359bc Fix unwanted triggering of IDE actions instead of Vim ones
https://github.com/JetBrains/ideavim/pull/893#pullrequestreview-2082768468
2024-05-28 21:39:14 +03:00
Alex Plate
ba50fcf85e Add ssh agent for the configuration 2024-05-28 19:19:06 +03:00
Alex Plate
24c0db563c Use releases prefix instead of release 2024-05-28 19:10:40 +03:00
Alex Plate
33c4905dcb Add testing TC job for calculating new version of the release 2024-05-28 19:05:37 +03:00
Alex Plate
9296c3f9a0 Bring back tests before the release 2024-05-28 14:50:11 +03:00
Alex Plate
7ecc566169 Temporally skip tests for this release
As this is considered as a VERY bad practice, it makes sense in this particular case. Generally, we need an updated release process so such things don’t happen
2024-05-28 14:31:46 +03:00
Alex Plate
a918da36d3 Bring back the old value of the release constant 2024-05-28 14:27:29 +03:00
Alex Plate
91ff5fdb02 Temporally change the release constant to make the last release based on 2023.3.2 2024-05-28 14:15:49 +03:00
Alex Plate
4481631547 Disable RedrawListener if the plugin is not enabled 2024-05-28 13:55:47 +03:00
Matt Ellis
7865388086 Fix regression when sub string ends with backslash
Fixes VIM-3428
2024-05-28 13:53:13 +03:00
Matt Ellis
26297bc5a9 Fix last line sometimes obscured by scrollbar
Fixes VIM-3028
2024-05-28 13:53:13 +03:00
Matt Ellis
9db5cdd8e3 Fix last line sometimes obscured by ex entry prompt
When performing a substitute command with confirmation, the height of the editor content pane should be reduced by the height of the ex entry panel. IdeaVim would do this correctly when moving a search result to the bottom of the file, but not when the result was on the last line of the file. Because the wrong height was used, IdeaVim would decide that no scrolling was necessary and the result in the last line would be obscured.

Fixes VIM-1560
2024-05-28 13:53:13 +03:00
Matt Ellis
2b17534a08 Fix infinite loop while highlighting in old regex
Fixes VIM-2510
2024-05-28 13:53:13 +03:00
Matt Ellis
4e2db68acf Update selection when searching in Visual mode 2024-05-28 13:53:13 +03:00
Matt Ellis
ddabf8df5e Fix regression finding endpos in new regex engine
Fixes VIM-3344
2024-05-28 13:53:13 +03:00
Matt Ellis
3f28e197ca Clear status bar on scrolling, add/remove lines, etc.
Also implements <C-L> to "redraw" screen and clear status line
2024-05-28 13:53:13 +03:00
Matt Ellis
3a67524e8a Show status bar message when search wraps
Fixes VIM-1043
2024-05-28 13:53:13 +03:00
Matt Ellis
33312d95b0 Remove wrapscan flag for substitute
Substitute always works on a known range
2024-05-28 13:53:13 +03:00
Matt Ellis
f554b21cd9 Support count for search
Fixes VIM-2836
2024-05-28 13:53:13 +03:00
Matt Ellis
d9fa4e4648 Fix end of file atom for old regex engine
Fixes VIM-2888
2024-05-28 13:53:13 +03:00
Matt Ellis
e98a284d40 Fix multiline regex substitution
Fixes VIM-2141
2024-05-28 13:53:13 +03:00
Matt Ellis
38ef8c1053 Fix removing search highlight that crosses newline 2024-05-28 13:53:13 +03:00
Matt Ellis
d3f560a31c Add special case to match newline with end of file 2024-05-28 13:53:13 +03:00
Matt Ellis
e2cb3ff284 Stop subst skipping too far with multiline pattern
Fixes VIM-698
2024-05-28 13:53:13 +03:00
Matt Ellis
0f5a3953e1 Don't break ex field with exceptions from incsearch 2024-05-28 13:53:13 +03:00
Matt Ellis
633667ed7f Fix regression with global not saving used patterns
This is a messy quick fix, as the search group needs a lot of tidying up right now. Perhaps a better implementation would be to move the implementation of the global command into the search group - processGlobalCommand, like we already have processSearchCommand and processSubstituteCommand

Fixes VIM-3348
2024-05-28 13:53:13 +03:00
Matt Ellis
656e975562 Fix reset of last substitution string on use
Fixes VIM-3354
2024-05-28 13:53:13 +03:00
Matt Ellis
f7fbe89de4 Use EnumSet instead of list of enums 2024-05-28 13:53:13 +03:00
Matt Ellis
509829b052 Fix force ignorecase atom in search highlights
Fixes VIM-3391
2024-05-28 13:53:13 +03:00
Matt Ellis
efd61e17f2 Reset CMD_LINE mode when losing focus
Fixes VIM-3293
2024-05-28 13:53:13 +03:00
Matt Ellis
cf2b021d02 Fix search for last search pattern with new offset
Fixes VIM-2356
2024-05-28 13:53:13 +03:00
Matt Ellis
67f10aece5 Fix search motion type when providing offset
Search motion type should be linewise if there's a line offset, or inclusive if the `e` flag is provided. Otherwise, it's exclusive.

Fixes VIM-1940
2024-05-28 13:53:13 +03:00
Matt Ellis
9b5c777fcf Update highlights when editor scheme changes 2024-05-28 13:53:13 +03:00
Matt Ellis
c65e5f8341 Fix highlighting for 'incsearch' and 'nohlsearch'
Setting 'incsearch' and 'nohlsearch' should highlight only the current match in the current editor
2024-05-28 13:53:13 +03:00
Matt Ellis
053bb603cd Fix errors with incsearch and substitute command
Fixes VIM-3325
2024-05-28 13:53:13 +03:00
Matt Ellis
93b07bbb90 Update comments and fix warnings 2024-05-28 13:53:13 +03:00
Matt Ellis
764a115601 Show search highlights in all visible editors
Fixes VIM-2174
2024-05-28 13:53:13 +03:00
Matt Ellis
1adfe53f29 Fix wrong offset returned for current search result
Fixes VIM-2779
2024-05-28 13:53:13 +03:00
Matt Ellis
bfe0f51cb1 Restore current match highlighting for :s command
Regression while migrating to the new regex engine removed the highlights shown when confirming each change
2024-05-28 13:53:13 +03:00
Matt Ellis
ffce61906a Restore search highlights when setting 'hlsearch'
Fixes VIM-3257
2024-05-28 13:53:13 +03:00
Filipp Vakhitov
3843a193cb Deprecate classes related to Swing TextActions 2024-05-26 23:56:27 +03:00
IdeaVim Bot
b2410dab0b Add Egor Nikolaevsky to contributors list 2024-05-25 09:01:49 +00:00
Matt Ellis
f382544101 Fix clearing highlighted yank in modal dialogs
Also fixes a memory leak registering a disposable for each yank, allows config variables to be numbers rather than strings and removes highlights when IdeaVim is disabled

Fixes VIM-2236
2024-05-24 18:05:51 +03:00
Filipp Vakhitov
a81d54eb90 Remove VimCommandLineHelper 2024-05-24 17:18:24 +03:00
Filipp Vakhitov
23519bbdae Refactoring to use interfaces from vim-engine 2024-05-24 16:51:38 +03:00
Alex Plate
73fd25773a Add test for VimIndentObject
For PR https://github.com/JetBrains/ideavim/pull/884
2024-05-24 16:20:36 +03:00
Egor Nikolaevsky
746d483179 Fix indent size calculation 2024-05-24 16:19:50 +03:00
Alex Plate
1a7e90c7d7 Bump the minimal version of IJ to 241
There are a lot of incompatibilities with IdeaVim on 241 if it's built on 233

Also, this time the special branch for the IJ version won't be created. Previously on bumping the version of IJ, we've created the branch to keep the reference to the moment when it happened. However, IJ version bumps are easy to trace anyway by git.
2024-05-24 15:37:05 +03:00
Filipp Vakhitov
8e2fd44f55 Remove unnecessary methods from ProcessGroup 2024-05-24 14:25:51 +03:00
Filipp Vakhitov
1c84917b29 Move LeaveCommandLineAction to its own class 2024-05-24 12:58:04 +03:00
Filipp Vakhitov
c5522ed19d Move ExEntryAction to vim-engine 2024-05-24 12:56:52 +03:00
IdeaVim Bot
33a5480456 Add Eduardo Haesbaert to contributors list 2024-05-24 09:01:57 +00:00
Alex Plate
16cd05fc76 Fix incorrect use of visual position instead of buffer position
We should calculate the line above based on logical position rather than visual position.

This issue was detected thanks to newly introduced soft wraps and proprty tests
2024-05-24 11:39:45 +03:00
Eduardo Haesbaert
4788dacc86 Fix wording
There is one additional `the` before the preview gif, and also, changing the wording from "to" to " for".
2024-05-24 10:10:46 +03:00
Filipp Vakhitov
91e54c8b0d Replace c_CTRL-R swing TextAction implementation with proper Vim engine implementation 2024-05-23 17:31:18 +03:00
Filipp Vakhitov
15ccebfe11 Cleanup 2024-05-23 15:27:18 +03:00
Filipp Vakhitov
5c849c9105 Fix ExEntryAction 2024-05-23 02:24:34 +03:00
Filipp Vakhitov
9e99506223 Move some logic to engine 2024-05-23 01:32:48 +03:00
Filipp Vakhitov
53ea90379f Moved some search related logic to commands to simplify KeyHandler and ProcessGroup 2024-05-23 00:15:46 +03:00
Filipp Vakhitov
2619d7ebb4 Moved some classes to the same file in vim-engine
They will share some logic in future commits
2024-05-22 22:14:54 +03:00
Filipp Vakhitov
8c8a7aceca Moved some classes to the same file in vim-engine
They will share some logic in future commits
2024-05-22 22:12:34 +03:00
Filipp Vakhitov
726b885b60 More obvious processing of ex-commands.
1. Now we have two parallel commandBuilders: for the editor and for the command prompt. It's done for sequence of keys like `d/foo<C-R>"<CR>` where we have two different commands that are built at the same time.
2. We simplified the CommandConsumer and made the logic more straightforward. `/`, `?` and `:` enter the command mode, while pressing final `<CR>` fires the command execution.
2024-05-22 22:04:32 +03:00
Filipp Vakhitov
a6994a09c3 Add support for commands that can be executed either once or for each caret depending on some circumstances
In a future commit, I'm going to make ProcessExEntryAction responsible for processing both search and ex commands. Search commands are motions that are executed for each caret, while ex-commands are executed once. The per-caret code is held internally by the ex commands themselves.

The current solution is definitely not the right one, and the whole ex command subsystem needs to be reviewed and refactored:

1. Some commands can be motions, which is currently not supported.
2. We need to figure out a gentle way of handling multiple carets.
2024-05-22 15:44:12 +03:00
Alex Plate
1616aff5a1 Update version of TC config 2024-05-10 16:06:28 +03:00
Alex Plate
5a82f05be8 Update version of IJ in TC tests 2024-05-10 15:46:31 +03:00
Alex Plate
3020504cdc Rename kotlin TC config to avoid compilation issues 2024-05-10 15:36:41 +03:00
Matt Ellis
d00e802674 Update options documentation 2024-05-10 15:27:24 +03:00
Matt Ellis
c46008dddc Update Vim option even when IdeaVim is disabled 2024-05-10 15:27:24 +03:00
Matt Ellis
755000c376 Reset Vim options when IDE setting changes
Options are not reset if they've been explicitly set by the user (e.g. `:set list` or _View | Active Editor | Show Whitespaces_). They are reset if they were explicitly set in `~/.ideavimrc`.

Also bumps the IDE build number to 233.11799.241 in order to use EditorSettingsExternalizable.PropNames
2024-05-10 15:27:24 +03:00
Matt Ellis
1e4a60bfe6 Fix nullability warning 2024-05-10 15:27:24 +03:00
Matt Ellis
c9ee685956 Codify assumption re global-local external setting 2024-05-10 15:27:24 +03:00
Matt Ellis
c2b1083836 Updated descriptions as per review comments 2024-05-10 15:27:24 +03:00
Matt Ellis
3518528f22 Prevent resetting options when plugin re-enabled 2024-05-10 15:27:24 +03:00
Matt Ellis
9bed8fec79 Map 'scrolloff' and 'sidescrolloff' options
Fixes VIM-3110
2024-05-10 15:27:24 +03:00
Matt Ellis
3ffbea7d42 Match Vim's behaviour for :set[local] {option}<
String and number/toggle options have different and opposite behaviour for `:set {option}<` and `:setlocal {option}<`. This change matches Vim's behaviour.
2024-05-10 15:27:24 +03:00
Matt Ellis
39b42193cb Map 'scrolljump' and 'sidescroll' options
Fixes VIM-3110
2024-05-10 15:27:24 +03:00
Matt Ellis
c69e080b05 Add 'fileencoding' option
No tests, as I don't know how to test interaction with saving to disk
2024-05-10 15:27:24 +03:00
Matt Ellis
50b42036d1 Add 'bomb' option
No tests, as I don't know how to test interaction with saving to disk
2024-05-10 15:27:24 +03:00
Matt Ellis
340c40ceff Add 'fileformat' option
No tests, as I don't know how to test interaction with saving to disk
2024-05-10 15:27:24 +03:00
Matt Ellis
6aeffe71de Map 'number' and 'relativenumber' options 2024-05-10 15:27:24 +03:00
Matt Ellis
04c33d719e Improve relative line converter for soft wraps
It now shows visual lines relative to the caret's visual line, rather than relative to the caret's logical line. This still isn't correct, and we should be showing the relative count of Vim logical lines (buffer lines + fold lines) but this matches movement so is more helpful
2024-05-10 15:27:24 +03:00
Matt Ellis
07f1d1e8e6 Move number/relativenumber options out of engine
While they are core Vim options, they are implemented by the host, not by the engine. If another host wants to support these options, they can add them in their implementation layer.
2024-05-10 15:27:24 +03:00
Matt Ellis
6c61254c50 Add 'colorcolumn' option to show visual guides
IntelliJ ties the hard wrap right margin guide with the other visual guides, and it's not possible to show one without the other. In Vim, you can show the hard wrap margin by adding "+0" to 'colorcolumn', so in IdeaVim, we automatically add this.
2024-05-10 15:27:24 +03:00
Matt Ellis
c6efea8c34 Add 'textwidth' option
Also supports overriding local-to-buffer options with IDE values, ensuring that changes to the option/IDE value are applied to all editors for the buffer.

Fixes VIM-1310
2024-05-10 15:27:24 +03:00
Matt Ellis
80f43a7c66 Add 'cursorline' option 2024-05-10 15:27:24 +03:00
Matt Ellis
9f5d3a9a28 Add 'list' option to show whitespace
Fixes VIM-267
2024-05-10 15:27:24 +03:00
Matt Ellis
8800a4f70b Add 'breakindent' option
Fixes VIM-2748
2024-05-10 15:27:24 +03:00
Matt Ellis
990254dcac Sort ideavim.dic to make it easier to modify 2024-05-10 15:27:24 +03:00
Matt Ellis
73a1118e78 Extract base implementation for IDE backed options 2024-05-10 15:27:24 +03:00
Matt Ellis
d3bedf26cf Treat IDE value as default for 'wrap' option 2024-05-10 15:27:24 +03:00
Matt Ellis
25b29f429a Track how option value is set 2024-05-10 15:27:24 +03:00
Matt Ellis
363f821962 Introduce 'wrap' option based on IntelliJ setting
Fixes VIM-1265
2024-05-10 15:27:24 +03:00
Alex Plate
8d873c0bf1 Fix(VIM-3418): Restore mappings after plugin disable/enable
There is no need to clean up the requiredShortcuts after turning off the plugin. Also, previously this set was not populated properly and keys that were added by ideavimrc or by user were not restored
2024-05-09 08:57:12 +03:00
Alex Plate
955676ed9e Add comments about structures for testing
Also, there is no need to clear these structures during plugin disabling
2024-05-09 08:30:47 +03:00
Alex Plate
a2439a37e4 Make RequiredShortcut as a data class
This will simplify reading the data during the debug
2024-05-09 08:21:26 +03:00
dependabot[bot]
7b6daa8e4e Bump io.ktor:ktor-serialization-kotlinx-json from 2.3.10 to 2.3.11
Bumps [io.ktor:ktor-serialization-kotlinx-json](https://github.com/ktorio/ktor) from 2.3.10 to 2.3.11.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/commits)

---
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-05-08 19:15:51 +03:00
dependabot[bot]
fcf782296a Bump org.jetbrains.kotlin:kotlin-stdlib from 1.9.23 to 1.9.24
Bumps [org.jetbrains.kotlin:kotlin-stdlib](https://github.com/JetBrains/kotlin) from 1.9.23 to 1.9.24.
- [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.23...v1.9.24)

---
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-05-08 19:15:37 +03:00
dependabot[bot]
44889b635c Bump com.google.devtools.ksp:symbol-processing-api
Bumps [com.google.devtools.ksp:symbol-processing-api](https://github.com/google/ksp) from 1.9.23-1.0.20 to 1.9.24-1.0.20.
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/1.9.23-1.0.20...1.9.24-1.0.20)

---
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-05-08 19:03:20 +03:00
dependabot[bot]
bc12ba11cd Bump io.ktor:ktor-client-core from 2.3.10 to 2.3.11
Bumps [io.ktor:ktor-client-core](https://github.com/ktorio/ktor) from 2.3.10 to 2.3.11.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/commits)

---
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-05-08 19:01:14 +03:00
dependabot[bot]
9b08db75e3 Bump io.ktor:ktor-client-auth from 2.3.10 to 2.3.11
Bumps [io.ktor:ktor-client-auth](https://github.com/ktorio/ktor) from 2.3.10 to 2.3.11.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/commits)

---
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-05-08 19:00:51 +03:00
4c3ad3f263 Add NERDTree action to toggle excluded files 2024-05-08 10:02:10 +03:00
Alex Plate
598bcc7edd Do not notify slack about the new release 2024-05-07 14:18:31 +03:00
Alex Plate
b86ec03dc4 Update UI tests for python. Open tool window by calling API 2024-04-26 19:13:53 +03:00
Alex Plate
ae75498f8a Update the UI test to search for the new name of the copy dialog 2024-04-26 18:44:31 +03:00
Alex Plate
9d0b68b0f8 Use the correct context after executing the ex command
With the incorrect context the action EditorSelectWord didn't make any effect because it worked on the ex-entry panel editor
2024-04-26 18:22:53 +03:00
Alex Plate
eeb5939e59 Use brew to install ffmpeg on GitHub actions
After GitHub updated macos from version 12 to version 14, the existing action stopped working
2024-04-26 17:37:21 +03:00
Alex Plate
ef235a47bf Try to install ffmpeg on GitHub actions using homebrew
After GitHub updated macos from version 12 to version 14, the existing action stopped working
2024-04-26 17:25:54 +03:00
Alex Plate
b66da76880 VIM-3376: Remove usages of the EditorDataContext
EditorDataContext cannot be used because the platform cannot convert it to the async data context. Taking the fact that it's not clear why this custom context exists, I decided to get rid of it at all.

```
Cannot convert to AsyncDataContext at 'keyboard shortcut' DataContextWrapper(CaretSpecificDataContext(com.maddyhome.idea.vim.helper.EditorDataContext)). Please use CustomizedDataContext or its inheritors like SimpleDataContext
```

Class EditorDataContext cannot be removed because it's used in github.zgqq.intellij-enhance plugin
2024-04-26 14:52:47 +03:00
Alex Plate
54d6119784 VIM-3376: Working on removing EditorDataContext. Remove it from ReplaceWithRegister
EditorDataContext cannot be used because the platform cannot convert it to the async data context. Taking the fact that it's not clear why this custom context exists, I decided to get rid of it at all.

```
Cannot convert to AsyncDataContext at 'keyboard shortcut' DataContextWrapper(CaretSpecificDataContext(com.maddyhome.idea.vim.helper.EditorDataContext)). Please use CustomizedDataContext or its inheritors like SimpleDataContext
```
2024-04-26 14:19:47 +03:00
Alex Plate
0b8c081425 VIM-3376: Working on removing EditorDataContext. Remove it from multiple places
EditorDataContext cannot be used because the platform cannot convert it to the async data context. Taking the fact that it's not clear why this custom context exists, I decided to get rid of it at all.

```
Cannot convert to AsyncDataContext at 'keyboard shortcut' DataContextWrapper(CaretSpecificDataContext(com.maddyhome.idea.vim.helper.EditorDataContext)). Please use CustomizedDataContext or its inheritors like SimpleDataContext
```
2024-04-26 14:16:52 +03:00
Alex Plate
209052ffa6 Create a function to get the execution context from the editor
This is a part of VIM-3376. This context will not be a custom EditorDataContext, but some context created by the platform.
In some places we don't have any kind of "current context", but we have to use it for the function. However, such context can be simply retrieved from the editor.
2024-04-26 14:03:37 +03:00
Alex Plate
fe9a6b5cfb Remove context argument when creating a pad for the string
It's unclear why it was needed to get the project from the context, but it's easy to get the project from the existing editor
2024-04-26 13:56:10 +03:00
Alex Plate
9c0f74369f VIM-3376: Working on removing EditorDataContext. Remove from ExEditorKit
This one was added after the implementation of cmap in 5c9faba7f4

EditorDataContext cannot be used because the platform cannot convert it to the async data context. Taking the fact that it's not clear why this custom context exists, I decided to get rid of it at all.

```
Cannot convert to AsyncDataContext at 'keyboard shortcut' DataContextWrapper(CaretSpecificDataContext(com.maddyhome.idea.vim.helper.EditorDataContext)). Please use CustomizedDataContext or its inheritors like SimpleDataContext
```
2024-04-26 13:49:59 +03:00
Alex Plate
cd27e5229b VIM-3376: Working on removing EditorDataContext. Remove from CommandLineHelper
EditorDataContext cannot be used because the platform cannot convert it to the async data context. Taking the fact that it's not clear why this custom context exists, I decided to get rid of it at all.

```
Cannot convert to AsyncDataContext at 'keyboard shortcut' DataContextWrapper(CaretSpecificDataContext(com.maddyhome.idea.vim.helper.EditorDataContext)). Please use CustomizedDataContext or its inheritors like SimpleDataContext
```
2024-04-26 13:40:18 +03:00
Alex Plate
472732905c VIM-3376: Get rid of IjCaretAndEditorExecutionContext
This context was added long ago, but I wasn't able to find specific reasons for that. Currently, such custom contexts cannot work with the intellij platform and should be refactored or removed. The issues with this context are that it cannot be converted to the async data context by the platform.
Taking the fact that the reason for this context was not found, I decided to get rid of it.

The issue from the platform looks like this

```
Cannot convert to AsyncDataContext at 'keyboard shortcut' DataContextWrapper(CaretSpecificDataContext(com.maddyhome.idea.vim.helper.EditorDataContext)). Please use CustomizedDataContext or its inheritors like SimpleDataContext
```
Here the EditorDataContext is mentioned instead of CaretAndEditorData context, however, I'll clean up both contexts during this refactoring

It was used in the action system for mapping to the `<Action>` keyword and in commit 256f5fcd0e it's mentioned that the EditorActionHandler was not working without this context. However, currently both cases work fine without addition wrapping.
2024-04-26 13:27:56 +03:00
Alex Plate
485d9f81cd VIM-3376: Use SimpleDataContext in tests 2024-04-26 12:25:56 +03:00
Alex Plate
8cf136ce4c Add toString representations for IjNativeAction and ActionEnableStatus 2024-04-26 10:22:50 +03:00
Alex Plate
116a8ac9d2 Reformat test code 2024-04-26 09:58:27 +03:00
Alex Plate
fda310bda6 Create a configuration for 2024.1 tests 2024-04-26 09:44:46 +03:00
dependabot[bot]
e55619ea33 Bump io.ktor:ktor-client-auth from 2.3.9 to 2.3.10
Bumps [io.ktor:ktor-client-auth](https://github.com/ktorio/ktor) from 2.3.9 to 2.3.10.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.10/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.9...2.3.10)

---
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-04-17 18:57:24 +03:00
dependabot[bot]
b952b20128 Bump io.ktor:ktor-client-content-negotiation from 2.3.9 to 2.3.10
Bumps [io.ktor:ktor-client-content-negotiation](https://github.com/ktorio/ktor) from 2.3.9 to 2.3.10.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.10/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.9...2.3.10)

---
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-04-17 18:43:04 +03:00
dependabot[bot]
62d1f85648 Bump io.ktor:ktor-client-core from 2.3.9 to 2.3.10
Bumps [io.ktor:ktor-client-core](https://github.com/ktorio/ktor) from 2.3.9 to 2.3.10.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.10/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.9...2.3.10)

---
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-04-10 19:01:40 +03:00
dependabot[bot]
5e3c8c0e92 Bump io.ktor:ktor-client-cio from 2.3.9 to 2.3.10
Bumps [io.ktor:ktor-client-cio](https://github.com/ktorio/ktor) from 2.3.9 to 2.3.10.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.10/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.9...2.3.10)

---
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-04-10 18:46:53 +03:00
dependabot[bot]
b58dddf2ff Bump com.google.devtools.ksp:symbol-processing-api
Bumps [com.google.devtools.ksp:symbol-processing-api](https://github.com/google/ksp) from 1.9.23-1.0.19 to 1.9.23-1.0.20.
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/1.9.23-1.0.19...1.9.23-1.0.20)

---
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-04-10 18:34:54 +03:00
dependabot[bot]
78d351a0b0 Bump org.mockito.kotlin:mockito-kotlin from 5.2.1 to 5.3.1
Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 5.2.1 to 5.3.1.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/5.2.1...5.3.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>
2024-04-10 18:33:14 +03:00
dependabot[bot]
61dbc948cc Bump io.ktor:ktor-serialization-kotlinx-json from 2.3.9 to 2.3.10
Bumps [io.ktor:ktor-serialization-kotlinx-json](https://github.com/ktorio/ktor) from 2.3.9 to 2.3.10.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/2.3.10/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/2.3.9...2.3.10)

---
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-04-10 18:33:02 +03:00
Alex Plate
c4d92ebe73 VIM-308 In intellij 2024.1+ the caret movement won't be detected as a separate undo action 2024-04-05 17:50:42 +03:00
dependabot[bot]
d0cf827638 Bump org.jetbrains.intellij from 1.17.2 to 1.17.3
Bumps org.jetbrains.intellij from 1.17.2 to 1.17.3.

---
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>
2024-04-03 16:38:41 +00:00
dependabot[bot]
6a6a92b6b9 Bump com.dorongold.task-tree from 2.1.1 to 3.0.0
Bumps com.dorongold.task-tree from 2.1.1 to 3.0.0.

---
updated-dependencies:
- dependency-name: com.dorongold.task-tree
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 16:36:28 +00:00
Alex Plate
9869b8a34e Add new UI tests for the actions
NewElementSamePlace
CopyReferencePopupGroup
2024-04-03 18:01:45 +03:00
Alex Plate
60fbf88322 Add UI test for generate action 2024-04-03 17:36:22 +03:00
Alex Plate
fae3924062 Update a version of IJ for tests 2024-04-02 11:56:59 +03:00
Alex Plate
dc2ce64823 Revert changes in action processing to fix VIM-3351 2024-04-02 11:52:56 +03:00
Alex Plate
d0d86d9178 Print the exceptin in logger.warn instead of just a message 2024-03-29 16:53:22 +02:00
Alex Plate
f417af6148 Specify ActionUpdateThread for FindActionIdAction 2024-03-29 15:38:05 +02:00
Alex Plate
2fe2860a09 Remove Offset and Pointer and switch to the regular Ints
Details on that can be found here: VIM-3368
2024-03-29 15:38:05 +02:00
filipp
cb40426976 Fix(FL-25338): Vim plugin stopped working in 1.31.107 2024-03-29 14:52:52 +02:00
Filipp Vakhitov
423ed390a2 Fix(FL-25087): p in vim mode
Pasting was broken with immutable carets because the old caret was not updated during execution
2024-03-29 14:52:52 +02:00
Filipp Vakhitov
7652b16ca6 Move more MotionGroup methods to its base class 2024-03-29 14:52:52 +02:00
Filipp Vakhitov
618a010c15 Move some MotionGroup methods to its base class 2024-03-29 14:52:52 +02:00
Filipp Vakhitov
d44a34ed9b Remove unnecessary abstract method 2024-03-29 14:52:52 +02:00
Filipp Vakhitov
c84fc996db Move some methods to vim-engine
The more methods we have in the engine, the fewer number of methods we will need to implement in the Fleet
2024-03-29 14:52:52 +02:00
Filipp Vakhitov
43f232543b Replace findBlockRange with newer implementation
The newer implementation is a part of the vim-engine library and uses new methods from the SearchGroup.kt, but it is not fully refactored yet
2024-03-29 14:52:52 +02:00
filipp
3f65d1d99a Revert "Revert changes to SearchGroup"
This reverts commit 00ccddf8cf.
2024-03-29 14:52:52 +02:00
Alex Plate
bfcf706ca7 Change the logic for detecting new dependencies on IdeaVim plugin 2024-03-29 09:27:09 +02:00
Alex Plate
8c1103c461 Add comment about the fix for project leak in tests 2024-03-28 10:30:19 +02:00
Alex Plate
ab75ace8db Fix(VIM-3331): Support custom registers in replaceWithRegister plugin 2024-03-25 09:40:45 +02:00
Alex Plate
4a58e6a282 Add test with custom register for textObjEntire extension 2024-03-25 09:34:58 +02:00
Alex Plate
ac9e4f69b4 Remove affectedRate related automation 2024-03-22 20:04:13 +02:00
Alex Plate
581edba7fd Remove the specification of the plugin verifier
The latest version of the verified was broken at some moment, so I specified the static version. Now these issues are fixed.
2024-03-22 13:53:55 +02:00
385 changed files with 16833 additions and 4682 deletions

View File

@@ -15,11 +15,7 @@ jobs:
distribution: zulu
java-version: 17
- name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
with:
# Not strictly necessary, but it may prevent rate limit
# errors especially on GitHub-hosted macos machines.
github-token: ${{ secrets.GITHUB_TOKEN }}
run: brew install ffmpeg
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.4.2
- name: Build Plugin

View File

@@ -18,11 +18,7 @@ jobs:
with:
python-version: '3.10'
- name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
with:
# Not strictly necessary, but it may prevent rate limit
# errors especially on GitHub-hosted macos machines.
github-token: ${{ secrets.GITHUB_TOKEN }}
run: brew install ffmpeg
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.4.2
- name: Build Plugin

View File

@@ -15,11 +15,7 @@ jobs:
distribution: zulu
java-version: 17
- name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
with:
# Not strictly necessary, but it may prevent rate limit
# errors especially on GitHub-hosted macos machines.
github-token: ${{ secrets.GITHUB_TOKEN }}
run: brew install ffmpeg
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.4.2
- name: Build Plugin

View File

@@ -1,34 +0,0 @@
# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
# This workflow syncs changes from the docs folder of IdeaVim to the IdeaVim.wiki repository
name: Update Affected Rate field on YouTrack
on:
workflow_dispatch:
schedule:
- cron: '0 8 * * *'
jobs:
build:
runs-on: ubuntu-latest
if: github.repository == 'JetBrains/ideavim'
steps:
- name: Fetch origin repo
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Update YouTrack
run: ./gradlew scripts:updateAffectedRates
env:
YOUTRACK_TOKEN: ${{ secrets.YOUTRACK_TOKEN }}

2
.idea/vcs.xml generated
View File

@@ -11,6 +11,6 @@
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -5,13 +5,13 @@ object Constants {
const val EAP_CHANNEL = "eap"
const val DEV_CHANNEL = "Dev"
const val GITHUB_TESTS = "2023.3.2"
const val NVIM_TESTS = "2023.3.2"
const val PROPERTY_TESTS = "2023.3.2"
const val LONG_RUNNING_TESTS = "2023.3.2"
const val QODANA_TESTS = "2023.3.2"
const val RELEASE = "2023.3.2"
const val GITHUB_TESTS = "2024.1.1"
const val NVIM_TESTS = "2024.1.1"
const val PROPERTY_TESTS = "2024.1.1"
const val LONG_RUNNING_TESTS = "2024.1.1"
const val QODANA_TESTS = "2024.1.1"
const val RELEASE = "2024.1.1"
const val RELEASE_DEV = "2023.3.2"
const val RELEASE_EAP = "2023.3.2"
const val RELEASE_DEV = "2024.1.1"
const val RELEASE_EAP = "2024.1.1"
}

View File

@@ -11,6 +11,7 @@ import _Self.subprojects.GitHub
import _Self.subprojects.OldTests
import _Self.subprojects.Releases
import _Self.vcsRoots.GitHubPullRequest
import _Self.vcsRoots.ReleasesVcsRoot
import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType
import jetbrains.buildServer.configs.kotlin.v2019_2.Project
@@ -21,10 +22,11 @@ object Project : Project({
// VCS roots
vcsRoot(GitHubPullRequest)
vcsRoot(ReleasesVcsRoot)
// Active tests
buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(TestingBuildType("2023.3", "<default>", version = "2023.3"))
buildType(TestingBuildType("2024.1.1", "<default>"))
buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(PropertyBased)

View File

@@ -0,0 +1,72 @@
/*
* 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 _Self.buildTypes
import _Self.IdeaVimBuildType
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.sshAgent
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
object CreateNewReleaseBranchFromMaster : IdeaVimBuildType({
name = "EXP: Create new release branch from master"
vcs {
root(DslContext.settingsRoot)
branchFilter = "+:<default>"
checkoutMode = CheckoutMode.AUTO
}
steps {
script {
name = "Calculate next potential release version"
scriptContent = """
#!/bin/bash
# Fetch all remote branches
git fetch --all
# Get a list of all branches matching the pattern releases/x.y.z
branches=${'$'}(git branch -r | grep -oE 'releases/[0-9]+\.[0-9]+\.x')
# If no matching branches are found, print a message and exit
if [[ -z "${'$'}branches" ]]; then
echo "No release branches found"
exit 1
fi
# Find the largest release version
largest_release=${'$'}(echo "${'$'}branches" | sort -V | tail -n 1)
# Print the largest release
echo "Largest release branch: ${'$'}largest_release"
echo "##teamcity[setParameter name='env.POTENTIAL_VERSION' value='${'$'}largest_release']"
""".trimIndent()
}
script {
name = "Show potential release version"
scriptContent = """
#!/bin/bash
echo "Calculated or user-provided parameter value is: %env.POTENTIAL_VERSION%"
""".trimIndent()
}
}
params {
param("env.POTENTIAL_VERSION", "")
}
features {
sshAgent {
teamcitySshKey = "IdeaVim ssh keys"
}
}
})

View File

@@ -0,0 +1,42 @@
/*
* 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 _Self.buildTypes
import _Self.IdeaVimBuildType
import _Self.vcsRoots.ReleasesVcsRoot
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.sshAgent
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
object PrintReleaseBranch : IdeaVimBuildType({
name = "EXP: Print release branch"
vcs {
root(ReleasesVcsRoot)
branchFilter = "+:heads/releases/*"
checkoutMode = CheckoutMode.AUTO
}
steps {
script {
name = "Print current branch"
scriptContent = """
echo "Current branch is: %teamcity.build.branch%"
""".trimIndent()
}
}
features {
sshAgent {
teamcitySshKey = "IdeaVim ssh keys"
}
}
})

View File

@@ -26,7 +26,7 @@ object PublishVimEngine : IdeaVimBuildType({
vcs {
root(DslContext.settingsRoot)
branchFilter = "+:<default>"
branchFilter = "+:fleet"
checkoutMode = CheckoutMode.AUTO
}

View File

@@ -0,0 +1,106 @@
package _Self.buildTypes
import _Self.Constants.EAP_CHANNEL
import _Self.Constants.RELEASE_EAP
import _Self.IdeaVimBuildType
import _Self.vcsRoots.ReleasesVcsRoot
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
import jetbrains.buildServer.configs.kotlin.v2019_2.ParameterDisplay
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.sshAgent
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnMetric
import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnMetricChange
object ReleaseEapFromBranch : IdeaVimBuildType({
name = "EXP: Publish EAP Build from branch"
description = "Build and publish EAP of IdeaVim plugin"
artifactRules = "build/distributions/*"
params {
param("env.ORG_GRADLE_PROJECT_ideaVersion", RELEASE_EAP)
password(
"env.ORG_GRADLE_PROJECT_publishToken",
"credentialsJSON:61a36031-4da1-4226-a876-b8148bf32bde",
label = "Password"
)
param("env.ORG_GRADLE_PROJECT_publishChannels", EAP_CHANNEL)
password(
"env.ORG_GRADLE_PROJECT_slackUrl",
"credentialsJSON:a8ab8150-e6f8-4eaf-987c-bcd65eac50b5",
label = "Slack Token"
)
password(
"env.YOUTRACK_TOKEN",
"credentialsJSON:2479995b-7b60-4fbb-b095-f0bafae7f622",
display = ParameterDisplay.HIDDEN
)
}
vcs {
root(ReleasesVcsRoot)
branchFilter = """
+:heads/(releases/*)
""".trimIndent()
checkoutMode = CheckoutMode.AUTO
}
steps {
script {
name = "Pull git tags"
scriptContent = "git fetch --tags origin"
}
script {
name = "Pull git history"
scriptContent = "git fetch --unshallow"
}
gradle {
name = "Calculate new eap version from branch"
tasks = "scripts:calculateNewEapVersionFromBranch"
}
gradle {
name = "Set TeamCity build number"
tasks = "scripts:setTeamCityBuildNumber"
}
gradle {
name = "Add release tag"
tasks = "scripts:addReleaseTag"
}
gradle {
name = "Publish plugin"
tasks = "publishPlugin"
}
script {
name = "Push changes to the repo"
scriptContent = """
branch=$(git branch --show-current)
echo current branch is ${'$'}branch
git push origin %build.number%
""".trimIndent()
}
gradle {
name = "YouTrack post release actions"
tasks = "scripts:eapReleaseActions"
}
}
features {
sshAgent {
teamcitySshKey = "IdeaVim ssh keys"
}
}
failureConditions {
failOnMetricChange {
metric = BuildFailureOnMetric.MetricType.ARTIFACT_SIZE
threshold = 5
units = BuildFailureOnMetric.MetricUnit.PERCENTS
comparison = BuildFailureOnMetric.MetricComparison.DIFF
compareTo = build {
buildRule = lastSuccessful()
}
}
}
})

View File

@@ -147,10 +147,10 @@ sealed class ReleasePlugin(private val releaseType: String) : IdeaVimBuildType({
name = "Run Integrations"
tasks = "releaseActions"
}
gradle {
name = "Slack Notification"
tasks = "slackNotification"
}
// gradle {
// name = "Slack Notification"
// tasks = "slackNotification"
// }
}
features {

View File

@@ -16,10 +16,10 @@ object GitHub : Project({
name = "Pull Requests checks"
description = "Automatic checking of GitHub Pull Requests"
buildType(Github("clean test", "Tests"))
buildType(GithubBuildType("clean test", "Tests"))
})
class Github(command: String, desc: String) : IdeaVimBuildType({
class GithubBuildType(command: String, desc: String) : IdeaVimBuildType({
name = "GitHub Pull Requests $desc"
description = "Test GitHub pull requests $desc"

View File

@@ -1,8 +1,11 @@
package _Self.subprojects
import _Self.buildTypes.CreateNewReleaseBranchFromMaster
import _Self.buildTypes.PrintReleaseBranch
import _Self.buildTypes.PublishVimEngine
import _Self.buildTypes.ReleaseDev
import _Self.buildTypes.ReleaseEap
import _Self.buildTypes.ReleaseEapFromBranch
import _Self.buildTypes.ReleaseMajor
import _Self.buildTypes.ReleaseMinor
import _Self.buildTypes.ReleasePatch
@@ -38,4 +41,8 @@ object Releases : Project({
buildType(ReleaseEap)
buildType(ReleaseDev)
buildType(PublishVimEngine)
buildType(CreateNewReleaseBranchFromMaster)
buildType(PrintReleaseBranch)
buildType(ReleaseEapFromBranch)
})

13
.teamcity/_Self/vcsRoots/Releases.kt vendored Normal file
View File

@@ -0,0 +1,13 @@
package _Self.vcsRoots
import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot
object ReleasesVcsRoot : GitVcsRoot({
name = "IdeaVim Releases"
url = "git@github.com:JetBrains/ideavim.git"
branch = "refs/heads/master"
branchSpec = "+:refs/(*)"
authMethod = uploadedKey {
uploadedKey = "IdeaVim ssh keys"
}
})

View File

@@ -5,15 +5,15 @@ 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 buildType with id = 'PublishVimEngine'
To apply the patch, change the buildType with id = 'ReleaseEapFromBranch'
accordingly, and delete the patch script.
*/
changeBuildType(RelativeId("PublishVimEngine")) {
changeBuildType(RelativeId("ReleaseEapFromBranch")) {
vcs {
check(branchFilter == "+:<default>") {
check(branchFilter == "+:heads/(releases/*)") {
"Unexpected option value: branchFilter = $branchFilter"
}
branchFilter = "+:fleet"
branchFilter = "heads/releases/*"
}
}

View File

@@ -30,5 +30,5 @@ node (Plugins -> teamcity-configs -> teamcity-configs:generate),
the 'Debug' option is available in the context menu for the task.
*/
version = "2023.11"
version = "2024.03"
project(_Self.Project)

View File

@@ -503,6 +503,14 @@ Contributors:
[![icon][github]](https://github.com/Parker7123)
&nbsp;
FilipParker
* [![icon][mail]](mailto:7138209+duhaesbaert@users.noreply.github.com)
[![icon][github]](https://github.com/duhaesbaert)
&nbsp;
Eduardo Haesbaert
* [![icon][mail]](mailto:nikolaevsky.egor@gmail.com)
[![icon][github]](https://github.com/Aisper)
&nbsp;
Egor Nikolaevsky
Previous contributors:

View File

@@ -21,7 +21,7 @@ repositories {
}
dependencies {
compileOnly("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.19")
compileOnly("com.google.devtools.ksp:symbol-processing-api:1.9.24-1.0.20")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion") {
// kotlin stdlib is provided by IJ, so there is no need to include it into the distribution
exclude("org.jetbrains.kotlin", "kotlin-stdlib")

View File

@@ -51,11 +51,11 @@ buildscript {
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.9.0.202403050737-r")
classpath("org.kohsuke:github-api:1.305")
classpath("io.ktor:ktor-client-core:2.3.9")
classpath("io.ktor:ktor-client-cio:2.3.9")
classpath("io.ktor:ktor-client-auth:2.3.9")
classpath("io.ktor:ktor-client-content-negotiation:2.3.9")
classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
classpath("io.ktor:ktor-client-core:2.3.11")
classpath("io.ktor:ktor-client-cio:2.3.10")
classpath("io.ktor:ktor-client-auth:2.3.11")
classpath("io.ktor:ktor-client-content-negotiation:2.3.10")
classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.11")
// This comes from the changelog plugin
// classpath("org.jetbrains:markdown:0.3.1")
@@ -69,11 +69,11 @@ plugins {
application
id("java-test-fixtures")
id("org.jetbrains.intellij") version "1.17.2"
id("org.jetbrains.intellij") version "1.17.3"
id("org.jetbrains.changelog") version "2.2.0"
id("org.jetbrains.kotlinx.kover") version "0.6.1"
id("com.dorongold.task-tree") version "2.1.1"
id("com.dorongold.task-tree") version "3.0.0"
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}
@@ -141,7 +141,7 @@ dependencies {
testFixturesImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
// https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2")
@@ -264,9 +264,6 @@ tasks {
runPluginVerifier {
downloadDir.set("${project.buildDir}/pluginVerifier/ides")
teamCityOutputFormat.set(true)
// The latest version of the plugin verifier is broken, so temporally use the stable version
verifierVersion = "1.307"
}
generateGrammarSource {
@@ -313,7 +310,7 @@ tasks {
patchPluginXml {
// Don't forget to update plugin.xml
sinceBuild.set("233.11799.67")
sinceBuild.set("241.15989.150")
changeNotes.set(
"""<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>"""

View File

@@ -27,7 +27,7 @@ Plug 'nerdtree'
### Preview
<details>
<summary>Click to the the preview</summary>
<summary>Click for the preview</summary>
<img src="images/nerdtree.gif" alt="NERDTree example"/>
</details>

View File

@@ -1,140 +1,268 @@
List of Supported Set Commands
==============================
List of Supported Options
=========================
The following `:set` commands can appear in `~/.ideavimrc` or be set manually in the command mode:
The following options can be set with the `:set`, `:setglobal` and `:setlocal` commands.
They can be added to the `~/.ideavimrc` file, or set manually in Command-line mode.
For more details of each option, please see the Vim documentation.
Every effort is made to make these options compatible with Vim behaviour.
However, some differences are inevitable.
'clipboard' 'cb' clipboard options
Standard clipboard options plus
```
'clipboard' 'cb' Defines clipboard behavioue
A comma-separated list of words to control clipboard behaviour:
unnamed The clipboard register '*' is used instead of the
unnamed register
unnamedplus The clipboard register '+' is used instead of the
unnamed register
ideaput Uses the IDEs own paste implementation for put
operations rather than simply inserting the text
'digraph' 'dg' Enable using <BS> to enter digraphs in Insert mode
'gdefault' 'gd' The ":substitute" flag 'g' is by default
'guicursor' 'gcr' Controls the shape of the cursor for different modes
'history' 'hi' Number of command-lines that are remembered
'hlsearch' 'hls' Highlight matches with the last search pattern
'ignorecase' 'ic' Ignore case in search patterns
'incsearch' 'is' Show where search pattern typed so far matches
'iskeyword' 'isk' Defines keywords for commands like 'w', '*', etc.
'keymodel' 'km' Controls selection behaviour with special keys
List of comma separated words, which enable special things that keys
can do. These values can be used:
startsel Using a shifted special key starts selection (either
Select mode or Visual mode, depending on "key" being
present in 'selectmode')
stopsel Using a NOT-shifted special key stops selection.
Automatically enables `stopselect` and `stopvisual`
stopselect Using a NOT-shifted special key stops select mode
and removes selection - IdeaVim ONLY
stopvisual Using a NOT-shifted special key stops visual mode
and removes selection - IdeaVim ONLY
continueselect Using a shifted arrow key doesn't start selection,
but in select mode acts like startsel is enabled
- IdeaVim ONLY
continuevisual Using a shifted arrow key doesn't start selection,
but in visual mode acts like startsel is enabled
- IdeaVim ONLY
`ideaput` (default on) - IdeaVim ONLY
enable native idea paste action for put operations
'digraph' 'dg' enable the entering of digraphs in Insert mode
'gdefault' 'gd' the ":substitute" flag 'g' is by default
'history' 'hi' number of command-lines that are remembered
'hlsearch' 'hls' highlight matches with the last search pattern
'ignorecase' 'ic' ignore case in search patterns
'iskeyword' 'isk' defines keywords for commands like 'w', '*', etc.
'incsearch' 'is' show where search pattern typed so far matches
`keymodel` `km` String (default "continueselect,stopselect")
Special keys in this context are the cursor keys, <End>, <Home>,
<PageUp> and <PageDown>.
List of comma separated words, which enable special things that keys
can do. These values can be used:
startsel Using a shifted special[1] key starts selection (either
Select mode or Visual mode, depending on "key" being
present in 'selectmode').
stopsel Using a NOT-shifted special[1] key stops selection.
Automatically enables `stopselect` and `stopvisual`
stopselect Using a NOT-shifted special[1] key stops - IdeaVim ONLY
select mode and removes selection.
stopvisual Using a NOT-shifted special[1] key stops - IdeaVim ONLY
visual mode and removes selection.
continueselect Using a shifted arrow key doesn't - IdeaVim ONLY
start selection, but in select mode
acts like startsel is enabled
continuevisual Using a shifted arrow key doesn't - IdeaVim ONLY
start selection, but in visual mode
acts like startsel is enabled
'matchpairs' 'mps' pairs of characters that "%" can match
'maxmapdepth' 'mmd' Maximum depth of mappings
'more' 'more' When on, listings pause when the whole screen is filled.
'nrformats' 'nf' number formats recognized for CTRL-A command
'number' 'nu' print the line number in front of each line
'relativenumber' 'rnu' show the line number relative to the line with
the cursor
'scroll' 'scr' lines to scroll with CTRL-U and CTRL-D
'scrolljump' 'sj' minimum number of lines to scroll
'scrolloff' 'so' minimum number of lines above and below the cursor
'selection' 'sel' what type of selection to use
'matchpairs' 'mps' Pairs of characters that "%" can match
'maxmapdepth' 'mmd' Maximum depth of mappings
'more' 'more' When on, listings pause when the whole screen is filled
'nrformats' 'nf' Number formats recognized for CTRL-A command
'operatorfunc' 'opfunc' Name of a function to call with the g@ operator
'scroll' 'scr' Number of lines to scroll with CTRL-U and CTRL-D
'selection' 'sel' What type of selection to use
'selectmode' 'slm' Controls when to start Select mode instead of Visual
This is a comma-separated list of words:
mouse When using the mouse
key When using shifted special[1] keys
cmd When using "v", "V", or <C-V>
ideaselection When IDE sets a selection - IdeaVim ONLY
(e.g.: extend selection, wrap with while, etc.)
`selectmode` `slm` String (default "")
'shell' 'sh' The shell to use to execute commands with ! and :!
'shellcmdflag' 'shcf' The command flag passed to the shell
'shellxescape' 'sxe' The characters to be escaped when calling a shell
'shellxquote' 'sxq' The quote character to use in a shell command
'showcmd' 'sc' Show (partial) command in the status bar
'showmode' 'smd' Show the current mode in the status bar
'smartcase' 'scs' Use case sensitive search if any character in the
pattern is uppercase
'startofline' 'sol' When on, some commands move the cursor to the first
non-blank of the line
When off, the cursor is kept in the same column
(if possible)
'timeout' 'to' Use timeout for mapped key sequences
'timeoutlen' 'tm' Timeout duration for a mapped key sequence
'viminfo' 'vi' Information to remember after restart
'virtualedit' 've' Placement of the cursor where there is no actual text
A comma-separated list of these words:
block Allow virtual editing in Visual mode (not supported)
insert Allow virtual editing in Insert mode (not supported)
all Allow virtual editing in all modes (not supported)
onemore Allow the cursor to move just past the end of the line
This is a comma-separated list of words, which specify when to start
Select mode instead of Visual mode, when a selection is started.
Possible values:
mouse when using the mouse
key when using shifted special[1] keys
cmd when using "v", "V", or <C-V>
ideaselection when IDE sets a selection - IdeaVim ONLY
(examples: extend selection, wrap with while, etc.)
'visualbell' 'vb' When on, prevents beeping on error
'whichwrap' 'ww' Which keys that move the cursor left/right can wrap to
other lines
A comma-separated list of these flags:
char key modes
b <BS> Normal and Visual
s <Space> Normal and Visual
h "h" Normal and Visual
l "l" Normal and Visual
< <Left> Normal and Visual
> <Right> Normal and Visual
~ "~" Normal
[ <Left> Insert and Replace
] <Right> Insert and Replace
`startofline` `sol` When "on" some commands move the cursor to the first non-blank of the line.
When off the cursor is kept in the same column (if possible).
'wrapscan' 'ws' Search will wrap around the end of file
```
'showmode' 'smd' message on the status line to show current mode
'showcmd' 'sc' show (partial) command in the status bar
'sidescroll' 'ss' minimum number of columns to scroll horizontally
'sidescrolloff' 'siso' min. number of columns to left and right of cursor
'smartcase' 'scs' no ignore case when pattern is uppercase
'timeout' 'to' use timeout for mapped key sequences
'timeoutlen' 'tm' timeout duration for a mapped key sequence
'undolevels' 'ul' maximum number of changes that can be undone
'viminfo' 'vi' information to remember after restart
'visualbell' 'vb' use visual bell instead of beeping
'wrapscan' 'ws' searches wrap around the end of file
IdeaVim only commands:
## IdeaVim options mapped to IntelliJ-based IDE settings
`ideamarks` `ideamarks` Boolean (default true)
If true, creation of global mark will trigger creation of IDE's bookmark
and vice versa.
`idearefactormode` `idearefactormode` String(default "select")
Define the mode that would be enabled during
the refactoring (renaming, live template, introduce variable, etc)
Use one of the following values:
- keep - keep the mode that was enabled before starting a refactoring
- select - start refactoring in select mode
- visual - start refactoring in visual mode
This option has effect if you are in normal, insert or replace mode before refactoring start.
Visual or select mode are not changed.
`ideajoin` `ideajoin` Boolean (default false)
If true, join command will be performed via IDE
See wiki/`ideajoin` examples
`ideastatusicon` `ideastatusicon` String(default "enabled")
Define the behavior of IdeaVim icon in the status bar.
Use one of the following values:
- enabled - icon is shown in the status bar
- gray - use the gray version of the icon
- disabled - hide the icon
IdeaVim provides its own implementation for handling scroll jump and offset, even though IntelliJ-based IDEs have similar functionality (there are differences in behaviour).
When IdeaVim is hosted in an IntelliJ-based IDE (but not JetBrains Fleet), the following options map to the equivalent IDE settings:
`ideawrite` `ideawrite` String (default "all")
"file" or "all". Defines the behaviour of ":w" command.
Value "all" enables execution of ":wa" (save all) command on ":w" (save).
This feature exists because some IJ options like "Prettier on save" or "ESlint on save"
work only with "save all" action. If this option is set to "all", these actions work
also with ":w" command.
`lookupkeys` `lookupkeys` List of strings
List of keys that should be processed by the IDE during the active lookup (autocompletion).
For example, <Tab> and <Enter> are used by the IDE to finish the lookup,
but <C-W> should be passed to IdeaVim.
Default value:
"<Tab>", "<Down>", "<Up>", "<Enter>", "<Left>", "<Right>",
"<C-Down>", "<C-Up>", "<PageUp>", "<PageDown>",
"<C-J>", "<C-Q>"
`ideavimsupport` `ideavimsupport` List of strings (default "dialog")
Define the list of additional buffers where IdeaVim is enabled.
- dialog - enable IdeaVim in dialogs
- singleline - enable IdeaVim in single line editors (not suggested)
```
'scrolljump' 'sj' Minimal number of lines to scroll
'scrolloff' 'so' Minimal number of lines above and below the cursor
'sidescroll' 'ss' Minimal number of columns to scroll horizontally
'sidescrolloff' 'siso' Minimal number of columns to left and right of cursor
```
----------
[1] - cursor keys, <End>, <Home>, <PageUp> and <PageDown>
## IdeaVim options for IntelliJ-based IDE features
Some Vim features cannot be implemented by IdeaVim, and must be implemented by the host IDE, such as showing whitespace and line numbers, and enabling soft-wrap.
The following options modify equivalent settings and features implemented by IntelliJ-based IDEs.
There is some mismatch when trying to map Vim options, most of which are local options, to IDE settings, which are mostly global-local.
The Vim option will always reflect the effective value of the IDE setting for the current editor, and modifying the Vim option will update the local value of the IDE setting.
The default value of the Vim option set during startup is not passed to the IDE setting.
If the IDE setting has a way to modify the local value, such as entries in the _View | Active Editor_ menu, then changing this will update the current editor and be reflected in the Vim option value.
If the IDE setting can only modify its global setting in the main _Settings_ dialog, this change does not always update the current editor (because the local IDE setting has been modified and takes precedence).
IdeaVim tries to make this work more naturally by updating the editor and local Vim option when a global value changes unless the Vim option has been explicitly set in Command-line mode.
In other words, if the local Vim value is explicitly set for a window or buffer, interactively, then it should not be reset.
If the Vim option was explicitly set in `~/.ideavimrc` however, then the value will be reset, because this can be viewed as a "global" value - set once and applied to subsequently opened windows.
(This should not be confused with Vim's concept of global options, which are mainly used to initialise new windows.)
The local Vim option can always be reset to the global IDE setting value by resetting the Vim option to default with the `:set {option}&` syntax.
```
'bomb' 'bomb' Add or remove a byte order mark (BOM) to the
current file. Unlike Vim, the file is modified
immediately, and not when saved
'breakindent' 'bri' Indent soft wrapped lines to match the first
line's indent
'colorcolumn' 'cc' Maps to IntelliJ's visual guide columns
'cursorline' 'cul' Highlight the line containing the cursor
'fileencoding' 'fenc' Change the encoding of the current file. The file
is modified and written immediately, rather than
waiting to be saved
Note that the names of the encoding might not
match Vim's known names
'fileformat' 'ff' Change the file format - dos, unix or mac
The file is modified immediately, rather than
when saved
'list' 'list' Show whitespace. Maps to the editor's local
setting in the View | Active Editor menu
'number' 'nu' Show line numbers. Maps to the editor's local
setting in the View | Active Editor menu
'relativenumber' 'rnu' Show line numbers relative to the current line
'textwidth' 'tw' Set the column at which text is automatically
wrapped
'wrap' 'wrap' Enable soft-wraps. Maps to the editor's local
setting in the View | Active Editor menu
```
## IdeaVim only options
These options are IdeaVim only, and not supported by Vim.
They control integration with the host IDE.
Unless otherwise stated, these options do not have abbreviations.
```
'ideacopypreprocess' boolean (default off)
global or local to buffer
When enabled, the IDE will run custom copy pre-processors over text
copied to registers. These pre-processors can perform transformations
on the text, such as converting escape characters in a string literal
into the actual control characters in a Java file.
This is not usually the expected behaviour, so this option's default
value is off. The equivalent processing for paste is controlled by the
"ideaput" value to the 'clipboard' option.
'ideaglobalmode' boolean (default off)
global
This option will cause IdeaVim to share a single mode across all open
windows. In other words, entering Insert mode in one window will
enable Insert mode in all windows.
'ideajoin' boolean (default off)
global or local to buffer
When enabled, join commands will be handled by the IDE's "smart join"
feature. The IDE can change syntax when joining lines, such as merging
string literals or if statements. See the wiki for more examples. Not
all languages support smart join functionality.
'ideamarks' boolean (default on)
global
Maps Vim's global marks to IDE bookmarks.
'idearefactormode' string (default "select")
global or local to buffer
Specifies the mode to be used when a refactoring selects text to be
edited (e.g. renaming, live template fields, introduce variable, etc):
keep Keep the current mode
select Switch to Select mode
visual Switch to Visual mode
This option is only used when the refactoring is started in Normal,
Insert or Replace mode. Visual or Select modes are not changed.
'ideastatusicon' string (default "enabled")
global
This option controls the behaviour and appearance of the IdeaVim icon
in the status bar:
enabled Show the icon in the status bar
gray Show the gray version of the icon
disabled Hide the icon
'ideavimsupport' string (default "dialog")
global
A comma-separated list of additional buffers or locations where
IdeaVim should be enabled:
dialog Enable IdeaVim in editors hosted in dialogs
singleline Enable IdeaVim in single line editors (not recommended)
The IDE's editor component can be used in many places, such as VCS
commit tool window, or inside dialogs, and even as single line fields.
'ideawrite' string (default "all")
global
This option defines the behaviour of the :w command:
file Save the current file only
all The :w command works like :wa and invokes the Save All
IDE action. This allows options such as "Prettier on
save" or "ESlint on save" to work with the :w command,
but means all files are saved.
'lookupkeys' string (default "<Tab>,<Down>,<Up>,<Enter>,
<Left>,<Right>,<C-Down>,<C-Up>,
<PageUp>,<PageDown>, <C-J>,<C-Q>")
global
Comma-separated list of keys that should be processed by the IDE while
a code completion lookup popup is active. For example, <Tab> and
<Enter> are used by the IDE to complete the lookup and insert text,
but <C-W> should be passed IdeaVim to continue editing the text.
'trackactionids' boolean (default off)
global
When on, IdeaVim will try to track the current IDE action and display
the action name in a notification. This action ID can then be used in
a mapping to the action in the form <Action>(...).
'visualdelay' number (default 100)
global
This option specifies the delay, in milliseconds before converting an
IDE selection into Visual mode.
Some IDE features make a selection to help modify text (e.g. backspace
in Python or Yaml selects an indent and invokes the "remove selection"
action). IdeaVim listens for changes in selection to switch to Visual
mode, and will return to Normal mode when the selection is removed,
even if originally in Insert mode.
By waiting before converting to Visual mode, temporary selections can
be ignored and the current Vim mode maintained.
It is not expected that this value will need to be changed.
```

View File

@@ -9,12 +9,12 @@
# suppress inspection "UnusedProperty" for whole file
#ideaVersion=LATEST-EAP-SNAPSHOT
ideaVersion=2023.3.3
ideaVersion=2024.1.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IC
downloadIdeaSources=true
instrumentPluginCode=true
version=chylex-30
version=chylex-35
javaVersion=17
remoteRobotVersion=0.11.22
antlrVersion=4.10.1
@@ -42,4 +42,3 @@ kotlin.stdlib.default.dependency=false
# Disable incremental annotation processing
ksp.incremental=false

View File

@@ -20,13 +20,13 @@ repositories {
}
dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:1.9.23")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:1.9.24")
implementation("io.ktor:ktor-client-core:2.3.9")
implementation("io.ktor:ktor-client-cio:2.3.9")
implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
implementation("io.ktor:ktor-client-auth:2.3.9")
implementation("io.ktor:ktor-client-core:2.3.11")
implementation("io.ktor:ktor-client-cio:2.3.10")
implementation("io.ktor:ktor-client-content-negotiation:2.3.10")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11")
implementation("io.ktor:ktor-client-auth:2.3.11")
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
// This is needed for jgit to connect to ssh
@@ -58,13 +58,6 @@ tasks.register("checkNewPluginDependencies", JavaExec::class) {
classpath = sourceSets["main"].runtimeClasspath
}
tasks.register("updateAffectedRates", JavaExec::class) {
group = "verification"
description = "This job updates Affected Rate field on YouTrack"
mainClass.set("scripts.YouTrackUsersAffectedKt")
classpath = sourceSets["main"].runtimeClasspath
}
tasks.register("calculateNewVersion", JavaExec::class) {
group = "release"
mainClass.set("scripts.release.CalculateNewVersionKt")
@@ -114,6 +107,13 @@ tasks.register("calculateNewEapVersion", JavaExec::class) {
args = listOf("${rootProject.rootDir}")
}
tasks.register("calculateNewEapVersionFromBranch", JavaExec::class) {
group = "release"
mainClass.set("scripts.release.CalculateNewEapVersionFromBranchKt")
classpath = sourceSets["main"].runtimeClasspath
args = listOf("${rootProject.rootDir}")
}
tasks.register("calculateNewDevVersion", JavaExec::class) {
group = "release"
mainClass.set("scripts.release.CalculateNewDevVersionKt")

View File

@@ -49,17 +49,13 @@ suspend fun main() {
}
val output = response.body<List<String>>().toSet()
println(output)
if (knownPlugins != output) {
val newPlugins = (output - knownPlugins).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
val removedPlugins = (knownPlugins - output.toSet()).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
val newPlugins = (output - knownPlugins).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
if (newPlugins.isNotEmpty()) {
// val removedPlugins = (knownPlugins - output.toSet()).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
error(
"""
Unregistered plugins:
${if (newPlugins.isNotEmpty()) newPlugins.joinToString(separator = "\n") { it.first + "(" + it.second + ")" } else "No unregistered plugins"}
Removed plugins:
${if (removedPlugins.isNotEmpty()) removedPlugins.joinToString(separator = "\n") { it.first + "(" + it.second + ")" } else "No removed plugins"}
${newPlugins.joinToString(separator = "\n") { it.first + "(" + it.second + ")" }}
""".trimIndent()
)
}

View File

@@ -0,0 +1,49 @@
/*
* 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 scripts.release
fun main(args: Array<String> ) {
val projectDir = args[0]
println("Working directory: $projectDir")
val branch = withRepo(projectDir) { it.branch }
val (majorBranchVersion, minorBranchVersion) = versions(branch)
val versions = getVersionsExistingVersionsFor(majorBranchVersion, minorBranchVersion, projectDir)
val maxExistingVersion = versions.keys.maxOrNull()
val nextVersion = if (maxExistingVersion != null) {
if (maxExistingVersion.suffixTokens.isEmpty()) {
maxExistingVersion.nextPatch().withSuffix("eap.1").value
}
else {
check(maxExistingVersion.suffixTokens.size == 2) {
"We should have exactly two suffix tokens. Current tokens: ${maxExistingVersion.suffixTokens.toList()}"
}
check(maxExistingVersion.suffixTokens[0] == "eap") {
"First suffix token must be eap. Current tokens: ${maxExistingVersion.suffixTokens.toList()}"
}
val newEapNumber = maxExistingVersion.suffixTokens[1].toInt().inc()
maxExistingVersion.withSuffix("eap.$newEapNumber").value
}
} else {
"$majorBranchVersion.$minorBranchVersion.0-eap.1"
}
println("Next eap version: $nextVersion")
println("##teamcity[setParameter name='env.ORG_GRADLE_PROJECT_version' value='$nextVersion']")
}
private val regex = "releases/(\\d+)\\.(\\d+)\\.x".toRegex()
private fun versions(branchName: String): Pair<Int, Int> {
val match = regex.matchEntire(branchName) ?: error("Cannot match branch: $branchName")
val major = match.groups[1]
val minor = match.groups[2]
return major!!.value.toInt() to minor!!.value.toInt()
}

View File

@@ -64,6 +64,31 @@ enum class ReleaseType {
STABLE_NO_PATCH, // Version that ends on 0. Like 2.5.0
}
internal fun getVersionsExistingVersionsFor(
majorVersion: Int,
minorVersion: Int,
projectDir: String,
): Map<Semver, ObjectId> {
val repository = RepositoryBuilder().setGitDir(File("$projectDir/.git")).build()
val git = Git(repository)
println(git.log().call().first())
println(git.tagList().call().first())
return git.tagList().call().mapNotNull { ref ->
runCatching {
// Git has two types of tags: light and annotated. This code detect hash of the commit for both types of tags
val revWalk = RevWalk(repository)
val tag = revWalk.parseAny(ref.objectId)
val commitHash = revWalk.peel(tag).id
val semver = Semver(ref.name.removePrefix("refs/tags/"))
if (semver.major == majorVersion && semver.minor == minorVersion) {
semver to commitHash
} else null
}.getOrNull()
}
.toMap()
}
internal fun getVersion(projectDir: String, releaseType: ReleaseType): Pair<Semver, ObjectId> {
val repository = RepositoryBuilder().setGitDir(File("$projectDir/.git")).build()
val git = Git(repository)

View File

@@ -1,62 +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 scripts
import io.ktor.client.call.*
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
val areaWeights = setOf(
Triple("118-53212", "Plugins", 50),
Triple("118-53220", "Vim Script", 30),
Triple("118-54084", "Esc", 100),
)
suspend fun updateRates() {
println("Updating rates of the issues")
areaWeights.forEach { (id, name, weight) ->
val unmappedIssues = unmappedIssues(name)
println("Got ${unmappedIssues.size} for $name area")
unmappedIssues.forEach { issueId ->
print("Trying to update issue $issueId: ")
val response = updateCustomField(issueId) {
put("name", "Affected Rate")
put("\$type", "SimpleIssueCustomField")
put("value", weight)
}
println(response)
}
}
}
private suspend fun unmappedIssues(area: String): List<String> {
val areaProcessed = if (" " in area) "{$area}" else area
val res = issuesQuery(
query = "project: VIM Affected Rate: {No affected rate} Area: $areaProcessed #Unresolved",
fields = "id,idReadable"
)
return res.body<JsonArray>().map { it.jsonObject }.map { it["idReadable"]!!.jsonPrimitive.content }
}
suspend fun getAreasWithoutWeight(): Set<Pair<String, String>> {
val allAreas = getAreaValues()
return allAreas
.filterNot { it.key in areaWeights.map { it.first }.toSet() }
.entries
.map { it.key to it.value }
.toSet()
}
suspend fun main() {
updateRates()
}

View File

@@ -101,10 +101,14 @@ command:
(WS | COLON)* range? (WS | COLON)* EXECUTE WS* (expr WS*)* (NEW_LINE | BAR)+
#ExecuteCommand|
(WS | COLON)* range? (WS | COLON)* lShift (WS* commandArgument = ~(NEW_LINE | BAR)+)? ((inline_comment NEW_LINE+) | (NEW_LINE | BAR)+)
(WS | COLON)* range? (WS | COLON)* lShift
WS* ((commandArgumentWithoutBars? inline_comment NEW_LINE) | (commandArgumentWithoutBars? NEW_LINE) | (commandArgumentWithoutBars? BAR))
(NEW_LINE | BAR)*
#ShiftLeftCommand|
(WS | COLON)* range? (WS | COLON)* rShift (WS* commandArgument = ~(NEW_LINE | BAR)+)? ((inline_comment NEW_LINE+) | (NEW_LINE | BAR)+)
(WS | COLON)* range? (WS | COLON)* rShift
WS* ((commandArgumentWithoutBars? inline_comment NEW_LINE) | (commandArgumentWithoutBars? NEW_LINE) | (commandArgumentWithoutBars? BAR))
(NEW_LINE | BAR)*
#ShiftRightCommand|
(WS | COLON)* range? (WS | COLON)*
@@ -824,4 +828,4 @@ AUGROUP_SKIP: NEW_LINE (WS|COLON)* AUGROUP .*? AUGROUP WS+ END -> skip
// All the other symbols
UNICODE_CHAR: '\u0000'..'\uFFFE';
EMOJI: [\p{Emoji}];
EMOJI: [\p{Emoji}];

View File

@@ -282,7 +282,13 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
ideavimrcRegistered = true;
if (!ApplicationManager.getApplication().isUnitTestMode()) {
executeIdeaVimRc(editor);
try {
VimInjectorKt.injector.getOptionGroup().startInitVimRc();
executeIdeaVimRc(editor);
}
finally {
VimInjectorKt.injector.getOptionGroup().endInitVimRc();
}
}
}

View File

@@ -77,7 +77,7 @@ public class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedActio
val modifiers = if (charTyped == ' ' && VimKeyListener.isSpaceShift) KeyEvent.SHIFT_DOWN_MASK else 0
val keyStroke = KeyStroke.getKeyStroke(charTyped, modifiers)
val startTime = if (traceTime) System.currentTimeMillis() else null
handler.handleKey(editor.vim, keyStroke, injector.executionContextManager.onEditor(editor.vim, context.vim), handler.keyHandlerState)
handler.handleKey(editor.vim, keyStroke, context.vim, handler.keyHandlerState)
if (startTime != null) {
val duration = System.currentTimeMillis() - startTime
LOG.info("VimTypedAction '$charTyped': $duration ms")

View File

@@ -0,0 +1,39 @@
/*
* 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.action
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
/**
* Clear and redraw the screen
*
* Typically, an IDE does not need to redraw the screen - it's editor handles scrolling, etc. However, it can be
* necessary to clear the status line.
*/
@CommandOrMotion(keys = ["<C-L>"], modes = [Mode.NORMAL])
internal class RedrawAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_READONLY
override fun execute(
editor: VimEditor,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
injector.redrawService.redraw()
return true
}
}

View File

@@ -26,6 +26,7 @@ import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.EditorHolderService
import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.group.IjOptions
import com.maddyhome.idea.vim.handler.enableOctopus
@@ -44,6 +45,7 @@ import com.maddyhome.idea.vim.listener.AceJumpService
import com.maddyhome.idea.vim.listener.AppCodeTemplates.appCodeTemplateCaptured
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.ui.ex.ExTextField
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
@@ -79,12 +81,7 @@ public class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible
try {
val start = if (traceTime) System.currentTimeMillis() else null
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor.vim,
keyStroke,
injector.executionContextManager.onEditor(editor.vim, e.dataContext.vim),
keyHandler.keyHandlerState,
)
keyHandler.handleKey(editor.vim, keyStroke, e.dataContext.vim, keyHandler.keyHandlerState)
if (start != null) {
val duration = System.currentTimeMillis() - start
LOG.info("VimShortcut update '$keyStroke': $duration ms")
@@ -257,7 +254,14 @@ public class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible
return null
}
private fun getEditor(e: AnActionEvent): Editor? = e.getData(PlatformDataKeys.EDITOR)
private fun getEditor(e: AnActionEvent): Editor? {
return e.getData(PlatformDataKeys.EDITOR)
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
EditorHolderService.getInstance().editor
} else {
null
}
}
/**
* Every time the key pressed with an active lookup, there is a decision:
@@ -381,6 +385,10 @@ private class ActionEnableStatus(
}
}
override fun toString(): String {
return "ActionEnableStatus(isEnabled=$isEnabled, message='$message', logLevel=$logLevel)"
}
companion object {
private val LOG = logger<ActionEnableStatus>()

View File

@@ -58,11 +58,11 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
)
} else if (!repeatHandler && lastCommand != null) {
if (cmd.rawCount > 0) {
lastCommand.count = cmd.count
lastCommand.rawCount = cmd.count
val arg = lastCommand.argument
if (arg != null) {
val mot = arg.motion
mot.count = 0
mot.rawCount = 0
}
}
state.executingCommand = lastCommand

View File

@@ -34,7 +34,7 @@ public class DeleteJoinLinesSpacesAction : ChangeEditorActionHandler.SingleExecu
}
injector.editorGroup.notifyIdeaJoin(editor)
var res = true
editor.nativeCarets().sortedByDescending { it.offset.point }.forEach { caret ->
editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret ->
if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true, operatorArguments)) {
res = false
}

View File

@@ -39,7 +39,7 @@ public class DeleteJoinVisualLinesAction : VisualOperatorActionHandler.SingleExe
return true
}
var res = true
editor.nativeCarets().sortedByDescending { it.offset.point }.forEach { caret ->
editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret ->
if (!caret.isValid) return@forEach
val range = caretsAndSelections[caret] ?: return@forEach
if (!injector.changeGroup.deleteJoinRange(

View File

@@ -39,7 +39,7 @@ public class DeleteJoinVisualLinesSpacesAction : VisualOperatorActionHandler.Sin
return true
}
var res = true
editor.carets().sortedByDescending { it.offset.point }.forEach { caret ->
editor.carets().sortedByDescending { it.offset }.forEach { caret ->
if (!caret.isValid) return@forEach
val range = caretsAndSelections[caret] ?: return@forEach
if (!injector.changeGroup.deleteJoinRange(

View File

@@ -1,35 +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.action.ex
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
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.handler.VimActionHandler
import java.util.*
/**
* Called by KeyHandler to process the contents of the ex entry panel
*
* The mapping for this action means that the ex command is executed as a write action
*/
@CommandOrMotion(keys = ["<CR>", "<C-M>", "<C-J>"], modes = [Mode.CMD_LINE])
public class ProcessExEntryAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override val flags: EnumSet<CommandFlags> = EnumSet.of(CommandFlags.FLAG_COMPLETE_EX)
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
return VimPlugin.getProcess().processExEntry(editor, context)
}
}

View File

@@ -9,8 +9,6 @@
package com.maddyhome.idea.vim.common
import com.intellij.application.options.CodeStyle
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions
@@ -39,13 +37,12 @@ internal class IndentConfig private constructor(indentOptions: IndentOptions) {
companion object {
@JvmStatic
fun create(editor: Editor, context: DataContext): IndentConfig {
return create(editor, PlatformDataKeys.PROJECT.getData(context))
fun create(editor: Editor): IndentConfig {
return create(editor, editor.project)
}
@JvmStatic
@JvmOverloads
fun create(editor: Editor, project: Project? = editor.project): IndentConfig {
fun create(editor: Editor, project: Project?): IndentConfig {
val indentOptions = if (project != null) {
CodeStyle.getIndentOptions(project, editor.document)
} else {

View File

@@ -13,24 +13,46 @@ import com.maddyhome.idea.vim.api.VimExOutputPanel
import com.maddyhome.idea.vim.helper.vimExOutput
import com.maddyhome.idea.vim.ui.ExOutputPanel
/**
* @author vlan
*/
// TODO: We need a nicer way to handle output, especially wrt testing, appending + clearing
public class ExOutputModel private constructor(private val myEditor: Editor) : VimExOutputPanel {
private var isActiveInTestMode = false
override val isActive: Boolean
get() = if (!ApplicationManager.getApplication().isUnitTestMode) {
ExOutputPanel.isPanelActive(myEditor)
} else {
isActiveInTestMode
}
override var text: String? = null
private set
get() = if (!ApplicationManager.getApplication().isUnitTestMode) {
ExOutputPanel.getInstance(myEditor).text
} else {
field
}
set(value) {
if (!ApplicationManager.getApplication().isUnitTestMode) {
ExOutputPanel.getInstance(myEditor).setText(value ?: "")
} else {
field = value
isActiveInTestMode = !value.isNullOrEmpty()
}
}
override fun output(text: String) {
this.text = text
if (!ApplicationManager.getApplication().isUnitTestMode) {
ExOutputPanel.getInstance(myEditor).setText(text)
}
}
override fun clear() {
text = null
}
override fun close() {
if (!ApplicationManager.getApplication().isUnitTestMode) {
ExOutputPanel.getInstance(myEditor).deactivate(false)
ExOutputPanel.getInstance(myEditor).close()
}
else {
isActiveInTestMode = false
}
}

View File

@@ -7,6 +7,7 @@
*/
package com.maddyhome.idea.vim.extension
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
@@ -23,7 +24,6 @@ import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.common.CommandAlias
import com.maddyhome.idea.vim.common.CommandAliasHandler
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.CommandLineHelper
import com.maddyhome.idea.vim.helper.TestInputModel
import com.maddyhome.idea.vim.helper.noneOfEnum
import com.maddyhome.idea.vim.helper.vimStateMachine
@@ -142,7 +142,7 @@ public object VimExtensionFacade {
*/
@JvmStatic
public fun executeNormalWithoutMapping(keys: List<KeyStroke>, editor: Editor) {
val context = injector.executionContextManager.onEditor(editor.vim)
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
val keyHandler = KeyHandler.getInstance()
keys.forEach { keyHandler.handleKey(editor.vim, it, context, false, false, keyHandler.keyHandlerState) }
}
@@ -181,8 +181,8 @@ public object VimExtensionFacade {
/** Returns a string typed in the input box similar to 'input()'. */
@JvmStatic
public fun inputString(editor: Editor, prompt: String, finishOn: Char?): String {
return service<CommandLineHelper>().inputString(editor.vim, prompt, finishOn) ?: ""
public fun inputString(editor: Editor, context: DataContext, prompt: String, finishOn: Char?): String {
return injector.commandLine.inputString(editor.vim, context.vim, prompt, finishOn) ?: ""
}
/** Get the current contents of the given register similar to 'getreg()'. */

View File

@@ -32,7 +32,8 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.command.TextObjectVisualType
import com.maddyhome.idea.vim.common.CommandAliasHandler
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.ex.ranges.toTextRange
import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.extension.VimExtension
import com.maddyhome.idea.vim.extension.VimExtensionFacade
@@ -248,8 +249,14 @@ internal class CommentaryExtension : VimExtension {
* Used like `:1,3Commentary` or `g/fun/Commentary`
*/
private class CommentaryCommandAliasHandler : CommandAliasHandler {
override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) {
Util.doCommentary(editor, context, ranges.getTextRange(editor, -1), SelectionType.LINE_WISE, false)
override fun execute(command: String, range: Range, editor: VimEditor, context: ExecutionContext) {
Util.doCommentary(
editor,
context,
range.getLineRange(editor, editor.primaryCaret()).toTextRange(editor),
SelectionType.LINE_WISE,
false
)
}
}
}

View File

@@ -10,7 +10,7 @@ package com.maddyhome.idea.vim.extension.highlightedyank
import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.markup.EffectType
@@ -19,25 +19,24 @@ import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.Disposer
import com.intellij.util.Alarm
import com.intellij.util.Alarm.ThreadToUse
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.VimProjectService
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.extension.VimExtension
import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.VimNlsSafe
import com.maddyhome.idea.vim.listener.VimInsertListener
import com.maddyhome.idea.vim.listener.VimYankListener
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import org.jetbrains.annotations.NonNls
import java.awt.Color
import java.awt.Font
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
internal const val DEFAULT_HIGHLIGHT_DURATION: Long = 300
internal const val DEFAULT_HIGHLIGHT_DURATION: Int = 300
@NonNls
private val HIGHLIGHT_DURATION_VARIABLE_NAME = "highlightedyank_highlight_duration"
@@ -78,111 +77,128 @@ internal class HighlightColorResetter : LafManagerListener {
*/
internal class VimHighlightedYank : VimExtension, VimYankListener, VimInsertListener {
private val highlightHandler = HighlightHandler()
private var initialised = false
override fun getName() = "highlightedyank"
override fun init() {
// Note that these listeners will still be registered when IdeaVim is disabled. However, they'll never get called
VimPlugin.getYank().addListener(this)
VimPlugin.getChange().addInsertListener(this)
// Register our own disposable to remove highlights when IdeaVim is disabled. Somehow, we need to re-register when
// IdeaVim is re-enabled. We don't get a call back for that, but because the listeners are active until the
// _extension_ is disabled, make sure we're properly initialised each time we're called.
registerIdeaVimDisabledCallback()
initialised = true
}
private fun registerIdeaVimDisabledCallback() {
// TODO: IdeaVim should help with lifecycle management here - VIM-3419
// IdeaVim should possibly unregister extensions, but it would also need to re-register them. We have to do this
// state management manually for now
Disposer.register(VimPlugin.getInstance().onOffDisposable) {
highlightHandler.clearYankHighlighters()
initialised = false
}
}
override fun dispose() {
// Called when the extension is disabled with `:set no{extension}` or if the plugin owning the extension unloads
VimPlugin.getYank().removeListener(this)
VimPlugin.getChange().removeInsertListener(this)
highlightHandler.clearYankHighlighters()
initialised = false
}
override fun yankPerformed(editor: VimEditor, range: TextRange) {
ensureInitialised()
highlightHandler.highlightYankRange(editor.ij, range)
}
override fun insertModeStarted(editor: Editor) {
highlightHandler.clearAllYankHighlighters()
ensureInitialised()
highlightHandler.clearYankHighlighters()
}
private fun ensureInitialised() {
if (!initialised) {
registerIdeaVimDisabledCallback()
initialised = true
}
}
private class HighlightHandler {
private var editor: Editor? = null
private val yankHighlighters: MutableSet<RangeHighlighter> = mutableSetOf()
private val alarm = Alarm(ThreadToUse.SWING_THREAD)
private var lastEditor: Editor? = null
private val highlighters = mutableSetOf<RangeHighlighter>()
fun highlightYankRange(editor: Editor, range: TextRange) {
// from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted
clearAllYankHighlighters()
clearYankHighlighters()
this.editor = editor
val project = editor.project
if (project != null) {
Disposer.register(
VimProjectService.getInstance(project),
) {
this.editor = null
yankHighlighters.clear()
}
lastEditor = editor
val attributes = getHighlightTextAttributes(editor)
for (i in 0 until range.size()) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.startOffsets[i],
range.endOffsets[i],
HighlighterLayer.SELECTION,
attributes,
HighlighterTargetArea.EXACT_RANGE,
)
highlighters.add(highlighter)
}
if (range.isMultiple) {
for (i in 0 until range.size()) {
highlightSingleRange(editor, range.startOffsets[i]..range.endOffsets[i])
}
} else {
highlightSingleRange(editor, range.startOffset..range.endOffset)
}
}
fun clearAllYankHighlighters() {
yankHighlighters.forEach { highlighter ->
editor?.markupModel?.removeHighlighter(highlighter) ?: StrictMode.fail("Highlighters without an editor")
}
yankHighlighters.clear()
}
private fun highlightSingleRange(editor: Editor, range: ClosedRange<Int>) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.start,
range.endInclusive,
HighlighterLayer.SELECTION,
getHighlightTextAttributes(),
HighlighterTargetArea.EXACT_RANGE,
)
yankHighlighters.add(highlighter)
setClearHighlightRangeTimer(highlighter)
}
private fun setClearHighlightRangeTimer(highlighter: RangeHighlighter) {
val timeout = extractUsersHighlightDuration()
// from vim-highlightedyank docs: A negative number makes the highlight persistent.
val timeout = extractUsersHighlightDuration()
if (timeout >= 0) {
Executors.newSingleThreadScheduledExecutor().schedule(
{
ApplicationManager.getApplication().invokeLater {
editor?.markupModel?.removeHighlighter(highlighter) ?: StrictMode.fail("Highlighters without an editor")
}
},
// Note modality. This is important when highlighting an editor when a modal dialog is open, such as the resolve
// conflict diff view
alarm.addRequest(
{ clearYankHighlighters() },
timeout,
TimeUnit.MILLISECONDS,
ModalityState.any()
)
}
}
private fun getHighlightTextAttributes() = TextAttributes(
fun clearYankHighlighters() {
alarm.cancelAllRequests()
// Make sure the last editor we saved is still alive before we use it. We can't just use the list of open editors
// because this list is empty when IdeaVim is disabled, so we're unable to clean up
lastEditor?.let { editor ->
if (!editor.isDisposed) {
highlighters.forEach { highlighter -> editor.markupModel.removeHighlighter(highlighter) }
}
}
lastEditor = null
highlighters.clear()
}
private fun getHighlightTextAttributes(editor: Editor) = TextAttributes(
null,
extractUsersHighlightColor(),
editor?.colorsScheme?.getColor(EditorColors.CARET_COLOR),
editor.colorsScheme.getColor(EditorColors.CARET_COLOR),
EffectType.SEARCH_MATCH,
Font.PLAIN,
)
private fun extractUsersHighlightDuration(): Long {
private fun extractUsersHighlightDuration(): Int {
return extractVariable(HIGHLIGHT_DURATION_VARIABLE_NAME, DEFAULT_HIGHLIGHT_DURATION) {
it.toLong()
// toVimNumber will return 0 for an invalid string. Let's force it to throw
when (it) {
is VimString -> it.value.toInt()
else -> it.toVimNumber().value
}
}
}
private fun extractUsersHighlightColor(): Color {
return extractVariable(HIGHLIGHT_COLOR_VARIABLE_NAME, getDefaultHighlightTextColor()) { value ->
val rgba = value
val rgba = value.asString()
.substring(4)
.filter { it != '(' && it != ')' && !it.isWhitespace() }
.split(',')
@@ -192,12 +208,11 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, VimInsertList
}
}
private fun <T> extractVariable(variable: String, default: T, extractFun: (value: String) -> T): T {
private fun <T> extractVariable(variable: String, default: T, extractFun: (value: VimDataType) -> T): T {
val value = VimPlugin.getVariableService().getGlobalVariableValue(variable)
if (value is VimString) {
if (value != null) {
return try {
extractFun(value.value)
extractFun(value)
} catch (e: Exception) {
@VimNlsSafe val message = MessageHelper.message(
"highlightedyank.invalid.value.of.0.1",

View File

@@ -37,7 +37,7 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.CommandAlias
import com.maddyhome.idea.vim.common.CommandAliasHandler
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.extension.VimExtension
import com.maddyhome.idea.vim.group.KeyGroup
import com.maddyhome.idea.vim.helper.MessageHelper
@@ -137,13 +137,13 @@ internal class NerdTree : VimExtension {
}
class IjCommandHandler(private val actionId: String) : CommandAliasHandler {
override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) {
override fun execute(command: String, range: Range, editor: VimEditor, context: ExecutionContext) {
Util.callAction(editor, actionId, context)
}
}
class ToggleHandler : CommandAliasHandler {
override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) {
override fun execute(command: String, range: Range, editor: VimEditor, context: ExecutionContext) {
val project = editor.ij.project ?: return
val toolWindow = ToolWindowManagerEx.getInstanceEx(project).getToolWindow(ToolWindowId.PROJECT_VIEW) ?: return
if (toolWindow.isVisible) {
@@ -155,7 +155,7 @@ internal class NerdTree : VimExtension {
}
class CloseHandler : CommandAliasHandler {
override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) {
override fun execute(command: String, range: Range, editor: VimEditor, context: ExecutionContext) {
val project = editor.ij.project ?: return
val toolWindow = ToolWindowManagerEx.getInstanceEx(project).getToolWindow(ToolWindowId.PROJECT_VIEW) ?: return
if (toolWindow.isVisible) {
@@ -477,6 +477,7 @@ internal class NerdTree : VimExtension {
"r",
NerdAction.ToIj("SynchronizeCurrentFile"),
)
registerCommand("NERDTreeMapToggleHidden", "I", NerdAction.ToIj("ProjectView.ShowExcludedFiles"))
registerCommand("NERDTreeMapRefreshRoot", "R", NerdAction.ToIj("Synchronize"))
registerCommand("NERDTreeMapMenu", "m", NerdAction.ToIj("ShowPopupMenu"))
registerCommand("NERDTreeMapQuit", "q", NerdAction.ToIj("HideActiveWindow"))
@@ -502,13 +503,6 @@ internal class NerdTree : VimExtension {
}
},
)
for (c in ('a'..'z') + ('A'..'Z')) {
val ks = KeyStroke.getKeyStroke(c)
if (ks !in actionsRoot) {
registerCommand(c.toString(), NerdAction.Code { _, _, _ -> })
}
}
}
object Util {

View File

@@ -8,6 +8,7 @@
package com.maddyhome.idea.vim.extension.replacewithregister
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
@@ -64,7 +65,7 @@ internal class ReplaceWithRegister : VimExtension {
val selectionEnd = caret.selectionEnd
val visualSelection = caret to VimSelection.create(selectionStart, selectionEnd - 1, typeInEditor, editor)
doReplace(editor.ij, caret, PutData.VisualSelection(mapOf(visualSelection), typeInEditor))
doReplace(editor.ij, context.ij, caret, PutData.VisualSelection(mapOf(visualSelection), typeInEditor))
}
editor.exitVisualMode()
}
@@ -92,7 +93,7 @@ internal class ReplaceWithRegister : VimExtension {
val visualSelection = caret to VimSelection.create(lineStart, lineEnd, SelectionType.LINE_WISE, editor)
caretsAndSelections += visualSelection
doReplace(editor.ij, caret, PutData.VisualSelection(mapOf(visualSelection), SelectionType.LINE_WISE))
doReplace(editor.ij, context.ij, caret, PutData.VisualSelection(mapOf(visualSelection), SelectionType.LINE_WISE))
}
editor.sortedCarets().forEach { caret ->
@@ -120,7 +121,7 @@ internal class ReplaceWithRegister : VimExtension {
selectionType ?: SelectionType.CHARACTER_WISE,
)
// todo multicaret
doReplace(ijEditor, editor.primaryCaret(), visualSelection)
doReplace(ijEditor, context.ij, editor.primaryCaret(), visualSelection)
return true
}
@@ -140,7 +141,7 @@ internal class ReplaceWithRegister : VimExtension {
}
}
private fun doReplace(editor: Editor, caret: ImmutableVimCaret, visualSelection: PutData.VisualSelection) {
private fun doReplace(editor: Editor, context: DataContext, caret: ImmutableVimCaret, visualSelection: PutData.VisualSelection) {
val registerGroup = injector.registerGroup
val lastRegisterChar = if (editor.caretModel.caretCount == 1) registerGroup.currentRegister else registerGroup.getCurrentRegisterForMulticaret()
val savedRegister = caret.registerStorage.getRegister(lastRegisterChar) ?: return
@@ -168,7 +169,7 @@ private fun doReplace(editor: Editor, caret: ImmutableVimCaret, visualSelection:
ClipboardOptionHelper.IdeaputDisabler().use {
VimPlugin.getPut().putText(
vimEditor,
injector.executionContextManager.onEditor(editor.vim),
context.vim,
putData,
operatorArguments = OperatorArguments(
editor.vimStateMachine?.isOperatorPending(vimEditor.mode) ?: false,

View File

@@ -118,7 +118,7 @@ internal class IdeaVimSneakExtension : VimExtension {
var lastSymbols: String = ""
fun jumpTo(editor: VimEditor, charone: Char, chartwo: Char, sneakDirection: Direction): TextRange? {
val caret = editor.primaryCaret()
val position = caret.offset.point
val position = caret.offset
val chars = editor.text()
val foundPosition = sneakDirection.findBiChar(editor, chars, position, charone, chartwo)
if (foundPosition != null) {

View File

@@ -7,6 +7,7 @@
*/
package com.maddyhome.idea.vim.extension.surround
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Editor
@@ -34,6 +35,7 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMa
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing
import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
import com.maddyhome.idea.vim.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.findBlockRange
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCaret
@@ -44,7 +46,6 @@ import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
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 java.awt.event.KeyEvent
@@ -100,7 +101,7 @@ internal class VimSurroundExtension : VimExtension {
val ijEditor = editor.ij
val c = getChar(ijEditor)
if (c.code == 0) return
val pair = getOrInputPair(c, ijEditor) ?: return
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return
editor.forEachCaret {
val line = it.getBufferPosition().line
@@ -150,7 +151,7 @@ internal class VimSurroundExtension : VimExtension {
val charTo = getChar(editor.ij)
if (charTo.code == 0) return
val newSurround = getOrInputPair(charTo, editor.ij) ?: return
val newSurround = getOrInputPair(charTo, editor.ij, context.ij) ?: return
runWriteAction { change(editor, context, charFrom, newSurround) }
}
@@ -227,12 +228,12 @@ internal class VimSurroundExtension : VimExtension {
val searchHelper = injector.searchHelper
return when (char) {
't' -> searchHelper.findBlockTagRange(editor, caret, 1, true)
'(', ')', 'b' -> searchHelper.findBlockRange(editor, caret, '(', 1, true)
'[', ']' -> searchHelper.findBlockRange(editor, caret, '[', 1, true)
'{', '}', 'B' -> searchHelper.findBlockRange(editor, caret, '{', 1, true)
'<', '>' -> searchHelper.findBlockRange(editor, caret, '<', 1, true)
'(', ')', 'b' -> findBlockRange(editor, caret, '(', 1, true)
'[', ']' -> findBlockRange(editor, caret, '[', 1, true)
'{', '}', 'B' -> findBlockRange(editor, caret, '{', 1, true)
'<', '>' -> findBlockRange(editor, caret, '<', 1, true)
'`', '\'', '"' -> {
val caretOffset = caret.offset.point
val caretOffset = caret.offset
val text = editor.text()
if (text.getOrNull(caretOffset - 1) == char && text.getOrNull(caretOffset) == char) {
TextRange(caretOffset - 1, caretOffset + 1)
@@ -269,23 +270,23 @@ internal class VimSurroundExtension : VimExtension {
private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
override fun apply(vimEditor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val editor = vimEditor.ij
val c = getChar(editor)
val ijEditor = vimEditor.ij
val c = getChar(ijEditor)
if (c.code == 0) return true
val pair = getOrInputPair(c, editor) ?: return false
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
runWriteAction {
val change = VimPlugin.getChange()
if (supportsMultipleCursors) {
editor.runWithEveryCaretAndRestore {
applyOnce(editor, change, pair, count)
ijEditor.runWithEveryCaretAndRestore {
applyOnce(ijEditor, change, pair, count)
}
}
else {
applyOnce(editor, change, pair, count)
applyOnce(ijEditor, change, pair, count)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor)
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
}
}
return true
@@ -347,8 +348,8 @@ private fun getSurroundPair(c: Char): Pair<String, String>? = if (c in SURROUND_
null
}
private fun inputTagPair(editor: Editor): Pair<String, String>? {
val tagInput = inputString(editor, "<", '>')
private fun inputTagPair(editor: Editor, context: DataContext): Pair<String, String>? {
val tagInput = inputString(editor, context, "<", '>')
val matcher = tagNameAndAttributesCapturePattern.matcher(tagInput)
return if (matcher.find()) {
val tagName = matcher.group(1)
@@ -361,17 +362,18 @@ private fun inputTagPair(editor: Editor): Pair<String, String>? {
private fun inputFunctionName(
editor: Editor,
context: DataContext,
withInternalSpaces: Boolean,
): Pair<String, String>? {
val functionNameInput = inputString(editor, "function: ", null)
val functionNameInput = inputString(editor, context, "function: ", null)
if (functionNameInput.isEmpty()) return null
return if (withInternalSpaces) "$functionNameInput( " to " )" else "$functionNameInput(" to ")"
}
private fun getOrInputPair(c: Char, editor: Editor): Pair<String, String>? = when (c) {
'<', 't' -> inputTagPair(editor)
'f' -> inputFunctionName(editor, false)
'F' -> inputFunctionName(editor, true)
private fun getOrInputPair(c: Char, editor: Editor, context: DataContext): Pair<String, String>? = when (c) {
'<', 't' -> inputTagPair(editor, context)
'f' -> inputFunctionName(editor, context, false)
'F' -> inputFunctionName(editor, context, true)
else -> getSurroundPair(c)
}

View File

@@ -131,10 +131,11 @@ public class VimIndentObject implements VimExtension {
// This is done as a separate step so that it works even when the caret is inside the indentation.
int offset = caretLineStartOffset;
int indentSize = 0;
while (++offset < charSequence.length()) {
while (offset < charSequence.length()) {
final char ch = charSequence.charAt(offset);
if (ch == ' ' || ch == '\t') {
++indentSize;
++offset;
} else {
break;
}

View File

@@ -197,7 +197,7 @@ public class ChangeGroup : VimChangeGroupBase() {
val allowWrap = injector.options(editor).whichwrap.contains("~")
var motion = injector.motion.getHorizontalMotion(editor, caret, count, true, allowWrap)
if (motion is Motion.Error) return false
changeCase(editor, caret, caret.offset.point, (motion as AbsoluteOffset).offset, CharacterHelper.CASE_TOGGLE)
changeCase(editor, caret, caret.offset, (motion as AbsoluteOffset).offset, CharacterHelper.CASE_TOGGLE)
motion = injector.motion.getHorizontalMotion(
editor,
caret,
@@ -235,8 +235,7 @@ public class ChangeGroup : VimChangeGroupBase() {
}
val lineLength = editor.lineLength(line)
if (column < VimMotionGroupBase.LAST_COLUMN && lineLength < column) {
val pad =
EditorHelper.pad((editor as IjVimEditor).editor, (context as IjEditorExecutionContext).context, line, column)
val pad = EditorHelper.pad((editor as IjVimEditor).editor, line, column)
val offset = editor.getLineEndOffset(line)
insertText(editor, caret, offset, pad)
}
@@ -395,7 +394,7 @@ public class ChangeGroup : VimChangeGroupBase() {
context: ExecutionContext,
range: TextRange,
) {
val startPos = editor.offsetToBufferPosition(caret.offset.point)
val startPos = editor.offsetToBufferPosition(caret.offset)
val startOffset = editor.getLineStartForOffset(range.startOffset)
val endOffset = editor.getLineEndForOffset(range.endOffset)
val ijEditor = (editor as IjVimEditor).editor
@@ -451,7 +450,7 @@ public class ChangeGroup : VimChangeGroupBase() {
dir: Int,
operatorArguments: OperatorArguments,
) {
val start = caret.offset.point
val start = caret.offset
val end = injector.motion.moveCaretToRelativeLineEnd(editor, caret, lines - 1, true)
indentRange(editor, caret, context, TextRange(start, end), 1, dir, operatorArguments)
}
@@ -485,7 +484,7 @@ public class ChangeGroup : VimChangeGroupBase() {
// Remember the current caret column
val intendedColumn = caret.vimLastColumn
val indentConfig = create((editor as IjVimEditor).editor, (context as IjEditorExecutionContext).context)
val indentConfig = create((editor as IjVimEditor).editor)
val sline = editor.offsetToBufferPosition(range.startOffset).line
val endLogicalPosition = editor.offsetToBufferPosition(range.endOffset)
val eline = if (endLogicalPosition.column == 0) max((endLogicalPosition.line - 1).toDouble(), 0.0)
@@ -568,7 +567,7 @@ public class ChangeGroup : VimChangeGroupBase() {
): Boolean {
val startLine = range.startLine
val endLine = range.endLine
val count = endLine - startLine + 1
val count = range.size
if (count < 2) {
return false
}

View File

@@ -39,11 +39,12 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.intellij.openapi.editor.EditorSettings.*;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
import static com.maddyhome.idea.vim.api.VimInjectorKt.options;
import static com.maddyhome.idea.vim.newapi.IjVimInjectorKt.ijOptions;
/**
@@ -55,12 +56,31 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
private Boolean isKeyRepeat = null;
// TODO: Get rid of this custom line converter once we support soft wraps properly
// The builtin relative line converter looks like it's using Vim's logical lines for counting, where a Vim logical
// line is a buffer line, or a single line representing a fold of several buffer lines. This converter is counting
// screen lines (but badly - if you're on the second line of a wrapped line, it still counts like you're on the first.
// We really want to use Vim logical lines, but we don't currently support them for movement - we move by screen line.
private final CaretListener myLineNumbersCaretListener = new CaretListener() {
@Override
public void caretPositionChanged(@NotNull CaretEvent e) {
final boolean requiresRepaint = e.getNewPosition().line != e.getOldPosition().line;
if (requiresRepaint && options(injector, new IjVimEditor(e.getEditor())).getRelativenumber()) {
repaintRelativeLineNumbers(e.getEditor());
// We don't get notified when the IDE's settings change, so make sure we're up-to-date when the caret moves
final Editor editor = e.getEditor();
boolean relativenumber = ijOptions(injector, new IjVimEditor(editor)).getRelativenumber();
if (relativenumber) {
if (!hasRelativeLineNumbersInstalled(editor)) {
installRelativeLineNumbers(editor);
}
else {
// We must repaint on each caret move, so we update when caret's visual line doesn't match logical line
repaintRelativeLineNumbers(editor);
}
}
else {
if (hasRelativeLineNumbersInstalled(editor)) {
removeRelativeLineNumbers(editor);
}
}
}
};
@@ -73,11 +93,10 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
editor.getCaretModel().addCaretListener(myLineNumbersCaretListener);
UserDataManager.setVimEditorGroup(editor, true);
UserDataManager.setVimLineNumbersInitialState(editor, editor.getSettings().isLineNumbersShown());
updateLineNumbers(editor);
}
private void deinitLineNumbers(@NotNull Editor editor, boolean isReleasing) {
private void deinitLineNumbers(@NotNull Editor editor) {
if (isProjectDisposed(editor) || !supportsVimLineNumbers(editor) || !UserDataManager.getVimEditorGroup(editor)) {
return;
}
@@ -86,14 +105,6 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
UserDataManager.setVimEditorGroup(editor, false);
removeRelativeLineNumbers(editor);
// Don't reset the built in line numbers if we're releasing the editor. If we do, EditorSettings.setLineNumbersShown
// can cause the editor to refresh settings and can call into FileManagerImpl.getCachedPsiFile AFTER FileManagerImpl
// has been disposed (Closing the project with a Find Usages result showing a preview panel is a good repro case).
// See IDEA-184351 and VIM-1671
if (!isReleasing) {
setBuiltinLineNumbers(editor, UserDataManager.getVimLineNumbersInitialState(editor));
}
}
private static boolean supportsVimLineNumbers(final @NotNull Editor editor) {
@@ -107,41 +118,22 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
}
private static void updateLineNumbers(final @NotNull Editor editor) {
final EffectiveOptions options = options(injector, new IjVimEditor(editor));
final boolean relativeNumber = options.getRelativenumber();
final boolean number = options.getNumber();
final boolean showBuiltinEditorLineNumbers = shouldShowBuiltinLineNumbers(editor, number, relativeNumber);
final EditorSettings settings = editor.getSettings();
if (settings.isLineNumbersShown() ^ showBuiltinEditorLineNumbers) {
// Update line numbers later since it may be called from a caret listener
// on the caret move and it may move the caret internally
ApplicationManager.getApplication().invokeLater(() -> {
if (editor.isDisposed()) return;
setBuiltinLineNumbers(editor, showBuiltinEditorLineNumbers);
});
final boolean isLineNumbersShown = editor.getSettings().isLineNumbersShown();
if (!isLineNumbersShown) {
return;
}
if (relativeNumber) {
final LineNumerationType lineNumerationType = editor.getSettings().getLineNumerationType();
if (lineNumerationType == LineNumerationType.RELATIVE || lineNumerationType == LineNumerationType.HYBRID) {
if (!hasRelativeLineNumbersInstalled(editor)) {
installRelativeLineNumbers(editor);
}
}
else if (hasRelativeLineNumbersInstalled(editor)) {
else {
removeRelativeLineNumbers(editor);
}
}
private static boolean shouldShowBuiltinLineNumbers(final @NotNull Editor editor, boolean number, boolean relativeNumber) {
final boolean initialState = UserDataManager.getVimLineNumbersInitialState(editor);
return initialState || number || relativeNumber;
}
private static void setBuiltinLineNumbers(final @NotNull Editor editor, boolean show) {
editor.getSettings().setLineNumbersShown(show);
}
private static boolean hasRelativeLineNumbersInstalled(final @NotNull Editor editor) {
return UserDataManager.getVimHasRelativeLineNumbersInstalled(editor);
}
@@ -235,7 +227,7 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
// Note that we need a similar check in `VimEditor.isWritable` to allow Escape to work to exit insert mode. We need
// to know that a read-only editor that is hosting a console view with a running process can be treated as writable.
Runnable switchToInsertMode = () -> {
ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(editor), null);
ExecutionContext context = injector.getExecutionContextManager().getEditorExecutionContext(new IjVimEditor(editor));
VimPlugin.getChange().insertBeforeCursor(new IjVimEditor(editor), context);
KeyHandler.getInstance().reset(new IjVimEditor(editor));
};
@@ -256,8 +248,8 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
updateCaretsVisualAttributes(new IjVimEditor(editor));
}
public void editorDeinit(@NotNull Editor editor, boolean isReleased) {
deinitLineNumbers(editor, isReleased);
public void editorDeinit(@NotNull Editor editor) {
deinitLineNumbers(editor);
UserDataManager.unInitializeEditor(editor);
VimPlugin.getKey().unregisterShortcutKeys(new IjVimEditor(editor));
CaretVisualAttributesHelperKt.removeCaretsVisualAttributes(editor);
@@ -322,17 +314,18 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
private static class RelativeLineNumberConverter implements LineNumberConverter {
@Override
public Integer convert(@NotNull Editor editor, int lineNumber) {
final boolean number = options(injector, new IjVimEditor(editor)).getNumber();
final IjVimEditor ijVimEditor = new IjVimEditor(editor);
final boolean number = ijOptions(injector, ijVimEditor).getNumber();
final int caretLine = editor.getCaretModel().getLogicalPosition().line;
// lineNumber is 1 based
if (number && (lineNumber - 1) == caretLine) {
return lineNumber;
if ((lineNumber - 1) == caretLine) {
return number ? lineNumber : 0;
}
else {
final int visualLine = new IjVimEditor(editor).bufferLineToVisualLine(lineNumber - 1);
final int currentVisualLine = new IjVimEditor(editor).bufferLineToVisualLine(caretLine);
return Math.abs(currentVisualLine - visualLine);
final int visualLine = ijVimEditor.bufferLineToVisualLine(lineNumber - 1);
final int caretVisualLine = editor.getCaretModel().getVisualPosition().line;
return Math.abs(caretVisualLine - visualLine);
}
}
@@ -371,16 +364,22 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
private Stream<Editor> getLocalEditors() {
// Always fetch local editors. If we're hosting a Code With Me session, any connected guests will create hidden
// editors to handle syntax highlighting, completion requests, etc. We need to make sure that IdeaVim only makes
// changes (e.g. adding search highlights) to local editors, so things don't incorrectly flow through to any Clients.
// changes (e.g., adding search highlights) to local editors so things don't incorrectly flow through to any Clients.
// In non-CWM scenarios, or if IdeaVim is installed on the Client, there are only ever local editors, so this will
// also work there. In Gateway remote development scenarios, IdeaVim should not be installed on the host, only the
// Client, so all should work there too.
// Note that most IdeaVim operations are in response to interactive keystrokes, which would mean that
// ClientEditorManager.getCurrentInstance would return local editors. However, some operations are in response to
// events such as document change (to update search highlights) and these can come from CWM guests, and we'd get the
// remote editors.
// This invocation will always get local editors, regardless of current context.
final ClientAppSession localSession = ClientSessionsManager.getAppSessions(ClientKind.LOCAL).get(0);
return localSession.getService(ClientEditorManager.class).editors();
// events such as document change (to update search highlights), and these can come from CWM guests, and we'd get
// the remote editors.
// This invocation will always get local editors, regardless of the current context.
List<ClientAppSession> appSessions = ClientSessionsManager.getAppSessions(ClientKind.ALL);
if (!appSessions.isEmpty()) {
ClientAppSession localSession = appSessions.get(0);
return localSession.getService(ClientEditorManager.class).editors();
}
else {
return Stream.empty();
}
}
}

View File

@@ -22,9 +22,12 @@ import com.intellij.openapi.fileEditor.impl.EditorsSplitters;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.VirtualFileSystem;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.ProjectScope;
@@ -44,6 +47,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
@@ -445,4 +449,28 @@ public class FileGroup extends VimFileBase {
LastTabService.getInstance(event.getManager().getProject()).setLastTab(event.getOldFile());
}
}
@Nullable
@Override
public VimEditor selectEditor(@NotNull String projectId, @NotNull String documentPath, @Nullable String protocol) {
VirtualFileSystem fileSystem = VirtualFileManager.getInstance().getFileSystem(protocol);
if (fileSystem == null) return null;
VirtualFile virtualFile = fileSystem.findFileByPath(documentPath);
if (virtualFile == null) return null;
Project project = Arrays.stream(ProjectManager.getInstance().getOpenProjects())
.filter(p -> injector.getFile().getProjectId(p).equals(projectId))
.findFirst().orElseThrow();
Editor editor = selectEditor(project, virtualFile);
if (editor == null) return null;
return new IjVimEditor(editor);
}
@NotNull
@Override
public String getProjectId(@NotNull Object project) {
if (!(project instanceof Project)) throw new IllegalArgumentException();
return ((Project) project).getName();
}
}

View File

@@ -45,6 +45,18 @@ public open class GlobalIjOptions(scope: OptionAccessScope) : OptionsPropertiesB
* As a convenience, this class also provides access to the IntelliJ specific global options, via inheritance.
*/
public class EffectiveIjOptions(scope: OptionAccessScope.EFFECTIVE): GlobalIjOptions(scope) {
// Vim options that are implemented purely by existing IntelliJ features and not used by vim-engine
public var breakindent: Boolean by optionProperty(IjOptions.breakindent)
public val colorcolumn: StringListOptionValue by optionProperty(IjOptions.colorcolumn)
public var cursorline: Boolean by optionProperty(IjOptions.cursorline)
public var fileformat: String by optionProperty(IjOptions.fileformat)
public var list: Boolean by optionProperty(IjOptions.list)
public var number: Boolean by optionProperty(IjOptions.number)
public var relativenumber: Boolean by optionProperty(IjOptions.relativenumber)
public var textwidth: Int by optionProperty(IjOptions.textwidth)
public var wrap: Boolean by optionProperty(IjOptions.wrap)
// IntelliJ specific options
public var ideacopypreprocess: Boolean by optionProperty(IjOptions.ideacopypreprocess)
public var ideajoin: Boolean by optionProperty(IjOptions.ideajoin)
public var idearefactormode: String by optionProperty(IjOptions.idearefactormode)

View File

@@ -10,9 +10,14 @@ package com.maddyhome.idea.vim.group
import com.intellij.openapi.application.ApplicationNamesInfo
import com.maddyhome.idea.vim.api.Options
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.ex.exExceptionMessage
import com.maddyhome.idea.vim.options.NumberOption
import com.maddyhome.idea.vim.options.Option
import com.maddyhome.idea.vim.options.OptionDeclaredScope.GLOBAL
import com.maddyhome.idea.vim.options.OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_BUFFER
import com.maddyhome.idea.vim.options.OptionDeclaredScope.LOCAL_TO_BUFFER
import com.maddyhome.idea.vim.options.OptionDeclaredScope.LOCAL_TO_WINDOW
import com.maddyhome.idea.vim.options.StringListOption
import com.maddyhome.idea.vim.options.StringOption
import com.maddyhome.idea.vim.options.ToggleOption
@@ -33,6 +38,56 @@ public object IjOptions {
Options.overrideDefaultValue(Options.clipboard, VimString("ideaput,autoselect,exclude:cons\\|linux"))
}
// Vim options that are implemented purely by existing IntelliJ features and not used by vim-engine
public val breakindent: ToggleOption = addOption(ToggleOption("breakindent", LOCAL_TO_WINDOW, "bri", false))
public val colorcolumn: StringListOption = addOption(object : StringListOption("colorcolumn", LOCAL_TO_WINDOW, "cc", "") {
override fun checkIfValueValid(value: VimDataType, token: String) {
super.checkIfValueValid(value, token)
if (value != VimString.EMPTY) {
// Each element in the comma-separated string list needs to be a number. No spaces. Vim supports numbers
// beginning "+" or "-" to draw a highlight column relative to the 'textwidth' value. We don't fully support
// that, but we do automatically add "+0" because IntelliJ always displays the right margin
split((value as VimString).asString()).forEach {
if (!it.matches(Regex("[+-]?[0-9]+"))) {
throw exExceptionMessage("E474", token)
}
}
}
}
})
public val cursorline: ToggleOption = addOption(ToggleOption("cursorline", LOCAL_TO_WINDOW, "cul", false))
public val list: ToggleOption = addOption(ToggleOption("list", LOCAL_TO_WINDOW, "list", false))
public val number: ToggleOption = addOption(ToggleOption("number", LOCAL_TO_WINDOW, "nu", false))
public val relativenumber: ToggleOption = addOption(ToggleOption("relativenumber", LOCAL_TO_WINDOW, "rnu", false))
public val textwidth: NumberOption = addOption(UnsignedNumberOption("textwidth", LOCAL_TO_BUFFER, "tw", 0))
public val wrap: ToggleOption = addOption(ToggleOption("wrap", LOCAL_TO_WINDOW, "wrap", true))
// These options are not explicitly listed as local-noglobal in Vim's help, but are set when a new buffer is edited,
// based on the value of 'fileformats' or 'fileencodings'. To prevent unexpected file cnversion, we treat them as
// local-noglobal. See `:help local-noglobal`, `:help 'fileformats'` and `:help 'fileencodings'`
public val bomb: ToggleOption =
addOption(ToggleOption("bomb", LOCAL_TO_BUFFER, "bomb", false, isLocalNoGlobal = true))
public val fileencoding: StringOption = addOption(
StringOption(
"fileencoding",
LOCAL_TO_BUFFER,
"fenc",
VimString.EMPTY,
isLocalNoGlobal = true
)
)
public val fileformat: StringOption = addOption(
StringOption(
"fileformat",
LOCAL_TO_BUFFER,
"ff",
if (injector.systemInfoService.isWindows) "dos" else "unix",
boundedValues = setOf("dos", "unix", "mac"),
isLocalNoGlobal = true
)
)
// IntelliJ specific functionality - custom options
public val ide: StringOption = addOption(
StringOption("ide", GLOBAL, "ide", ApplicationNamesInfo.getInstance().fullProductNameWithEdition)
)

View File

@@ -0,0 +1,61 @@
/*
* 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.group
import com.intellij.lang.CodeDocumentationAwareCommenter
import com.intellij.lang.LanguageCommenters
import com.intellij.openapi.components.Service
import com.intellij.psi.PsiComment
import com.intellij.psi.util.PsiTreeUtil
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimPsiService
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.PsiHelper
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
@Service
public class IjVimPsiService: VimPsiService {
override fun getCommentAtPos(editor: VimEditor, pos: Int): Pair<TextRange, Pair<String, String>?>? {
val psiFile = PsiHelper.getFile(editor.ij) ?: return null
val psiElement = psiFile.findElementAt(pos) ?: return null
val language = psiElement.language
val commenter = LanguageCommenters.INSTANCE.forLanguage(language)
val psiComment = PsiTreeUtil.getParentOfType(psiElement, PsiComment::class.java, false) ?: return null
val commentText = psiComment.text
val blockCommentPrefix = commenter.blockCommentPrefix
val blockCommentSuffix = commenter.blockCommentSuffix
val docCommentPrefix = (commenter as? CodeDocumentationAwareCommenter)?.documentationCommentPrefix
val docCommentSuffix = (commenter as? CodeDocumentationAwareCommenter)?.documentationCommentSuffix
val prefixToSuffix: Pair<String, String>? =
if (docCommentPrefix != null && docCommentSuffix != null && commentText.startsWith(docCommentPrefix) && commentText.endsWith(docCommentSuffix)) {
docCommentPrefix to docCommentSuffix
}
else if (blockCommentPrefix != null && blockCommentSuffix != null && commentText.startsWith(blockCommentPrefix) && commentText.endsWith(blockCommentSuffix)) {
blockCommentPrefix to blockCommentSuffix
}
else {
null
}
return Pair(psiComment.textRange.vim, prefixToSuffix)
}
override fun getDoubleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
// TODO[ideavim] It wasn't implemented before, but implementing it will significantly improve % motion
return getDoubleQuotesRangeNoPSI(editor.text(), pos, isInner)
}
override fun getSingleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
// TODO[ideavim] It wasn't implemented before, but implementing it will significantly improve % motion
return getSingleQuotesRangeNoPSI(editor.text(), pos, isInner)
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.group
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimRedrawService
import com.maddyhome.idea.vim.api.injector
public class IjVimRedrawService : VimRedrawService {
override fun redraw() {
// The only thing IntelliJ needs to redraw is the status line. Everything else is handled automatically.
redrawStatusLine()
}
override fun redrawStatusLine() {
injector.messages.clearStatusBarMessage()
}
public companion object {
/**
* Simulate Vim's redraw when the current editor changes
*/
public fun fileEditorManagerSelectionChangedCallback(event: FileEditorManagerEvent) {
injector.redrawService.redraw()
}
}
/**
* Simulates Vim's redraw when the document changes
*
* Only redraw if lines are added/removed.
*/
public object RedrawListener : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
if (VimPlugin.isNotEnabled()) return
if (event.newFragment.contains("\n") || event.oldFragment.contains("\n")) {
injector.redrawService.redraw()
}
}
}
}

View File

@@ -11,41 +11,25 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.impl.EditorWindow
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.VirtualFileSystem
import com.intellij.util.MathUtil.clamp
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroupBase
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.addJump
import com.maddyhome.idea.vim.api.anyNonWhitespace
import com.maddyhome.idea.vim.api.getJump
import com.maddyhome.idea.vim.api.getJumpSpot
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
import com.maddyhome.idea.vim.api.getVisualLineCount
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.lineLength
import com.maddyhome.idea.vim.api.normalizeColumn
import com.maddyhome.idea.vim.api.normalizeLine
import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.api.normalizeVisualColumn
import com.maddyhome.idea.vim.api.normalizeVisualLine
import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.api.visualLineToBufferLine
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType
@@ -54,30 +38,22 @@ import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
import com.maddyhome.idea.vim.handler.Motion.AdjustedOffset
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import com.maddyhome.idea.vim.handler.toMotionOrError
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.SearchHelper
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.fileSize
import com.maddyhome.idea.vim.helper.getNormalizedScrollOffset
import com.maddyhome.idea.vim.helper.getNormalizedSideScrollOffset
import com.maddyhome.idea.vim.helper.isEndAllowed
import com.maddyhome.idea.vim.helper.vimLastColumn
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.listener.AppCodeTemplates
import com.maddyhome.idea.vim.mark.Mark
import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import org.jetbrains.annotations.Range
import java.io.File
import kotlin.math.max
import kotlin.math.min
@@ -90,24 +66,6 @@ internal class MotionGroup : VimMotionGroupBase() {
AppCodeTemplates.onMovement(editor.ij, caret.ij, oldOffset < offset)
}
private fun selectEditor(project: Project, mark: Mark): Editor? {
val virtualFile = markToVirtualFile(mark) ?: return null
return selectEditor(project, virtualFile)
}
private fun markToVirtualFile(mark: Mark): VirtualFile? {
val protocol = mark.protocol
val fileSystem: VirtualFileSystem? = VirtualFileManager.getInstance().getFileSystem(protocol)
return fileSystem?.findFileByPath(mark.filepath)
}
private fun selectEditor(project: Project?, file: VirtualFile) =
VimPlugin.getFile().selectEditor(project, file)
override fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion {
return SearchHelper.findMatchingPairOnCurrentLine(editor.ij, caret.ij).toMotionOrError()
}
override fun moveCaretToFirstDisplayLine(
editor: VimEditor,
caret: ImmutableVimCaret,
@@ -130,85 +88,12 @@ internal class MotionGroup : VimMotionGroupBase() {
return moveCaretToScreenLocation(editor.ij, caret.ij, ScreenLocation.MIDDLE, 0, false)
}
override fun moveCaretToMark(caret: ImmutableVimCaret, ch: Char, toLineStart: Boolean): Motion {
val markService = injector.markService
val mark = markService.getMark(caret, ch) ?: return Motion.Error
val caretEditor = caret.editor
val caretVirtualFile = EditorHelper.getVirtualFile((caretEditor as IjVimEditor).editor)
val line = mark.line
if (caretVirtualFile!!.path == mark.filepath) {
val offset = if (toLineStart) {
moveCaretToLineStartSkipLeading(caretEditor, line)
} else {
caretEditor.bufferPositionToOffset(BufferPosition(line, mark.col, false))
}
return offset.toMotionOrError()
}
val project = caretEditor.editor.project
val markEditor = selectEditor(project!!, mark)
if (markEditor != null) {
// todo should we move all the carets or only one?
for (carett in markEditor.caretModel.allCarets) {
val offset = if (toLineStart) {
moveCaretToLineStartSkipLeading(IjVimEditor(markEditor), line)
} else {
// todo should it be the same as getting offset above?
markEditor.logicalPositionToOffset(LogicalPosition(line, mark.col))
}
IjVimCaret(carett!!).moveToOffset(offset)
}
}
return Motion.Error
}
override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
val jumpService = injector.jumpService
val spot = jumpService.getJumpSpot(editor)
val (line, col, fileName) = jumpService.getJump(editor, count) ?: return Motion.Error
val vf = EditorHelper.getVirtualFile(editor.ij) ?: return Motion.Error
val lp = BufferPosition(line, col, false)
val lpNative = LogicalPosition(line, col, false)
return if (vf.path != fileName) {
val newFile = LocalFileSystem.getInstance().findFileByPath(fileName.replace(File.separatorChar, '/'))
?: return Motion.Error
selectEditor(editor.ij.project, newFile)?.let { newEditor ->
if (spot == -1) {
jumpService.addJump(editor, false)
}
newEditor.vim.let {
it.currentCaret().moveToOffset(it.normalizeOffset(newEditor.logicalPositionToOffset(lpNative), false))
}
}
Motion.Error
} else {
if (spot == -1) {
jumpService.addJump(editor, false)
}
editor.bufferPositionToOffset(lp).toMotionOrError()
}
}
override fun moveCaretToCurrentDisplayLineMiddle(editor: VimEditor, caret: ImmutableVimCaret): Motion {
val width = EditorHelper.getApproximateScreenWidth(editor.ij) / 2
val len = editor.lineLength(editor.currentCaret().getBufferPosition().line)
return moveCaretToColumn(editor, caret, max(0, min(len - 1, width)), false)
}
override fun moveCaretToColumn(editor: VimEditor, caret: ImmutableVimCaret, count: Int, allowEnd: Boolean): Motion {
val line = caret.getLine().line
val column = editor.normalizeColumn(line, count, allowEnd)
val offset = editor.bufferPositionToOffset(BufferPosition(line, column, false))
return if (column != count) {
AdjustedOffset(offset, count)
} else {
AbsoluteOffset(offset)
}
}
override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, false)
@@ -219,7 +104,7 @@ internal class MotionGroup : VimMotionGroupBase() {
caret: ImmutableVimCaret,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
val bufferLine = caret.getLine().line
val bufferLine = caret.getLine()
return editor.getLeadingCharacterOffset(bufferLine, col)
}
@@ -232,36 +117,6 @@ internal class MotionGroup : VimMotionGroupBase() {
return moveCaretToColumn(editor, caret, col, allowEnd)
}
override fun moveCaretToLineWithSameColumn(
editor: VimEditor,
line: Int,
caret: ImmutableVimCaret,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
var c = caret.vimLastColumn
var l = line
if (l < 0) {
l = 0
c = 0
} else if (l >= editor.lineCount()) {
l = editor.normalizeLine(editor.lineCount() - 1)
c = editor.lineLength(l)
}
val newPos = BufferPosition(l, editor.normalizeColumn(l, c, false))
return editor.bufferPositionToOffset(newPos)
}
override fun moveCaretToLineWithStartOfLineOption(
editor: VimEditor,
line: Int,
caret: ImmutableVimCaret,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
return if (injector.options(editor).startofline) {
moveCaretToLineStartSkipLeading(editor, line)
} else {
moveCaretToLineWithSameColumn(editor, line, caret)
}
}
/**
* If 'absolute' is true, then set tab index to 'value', otherwise add 'value' to tab index with wraparound.
*/
@@ -279,30 +134,18 @@ internal class MotionGroup : VimMotionGroupBase() {
}
override fun moveCaretGotoPreviousTab(editor: VimEditor, context: ExecutionContext, rawCount: Int): Int {
val project = editor.ij.project ?: return editor.currentCaret().offset.point
val project = editor.ij.project ?: return editor.currentCaret().offset
val currentWindow = FileEditorManagerEx.getInstanceEx(project).splitters.currentWindow
switchEditorTab(currentWindow, if (rawCount >= 1) -rawCount else -1, false)
return editor.currentCaret().offset.point
return editor.currentCaret().offset
}
override fun moveCaretGotoNextTab(editor: VimEditor, context: ExecutionContext, rawCount: Int): Int {
val absolute = rawCount >= 1
val project = editor.ij.project ?: return editor.currentCaret().offset.point
val project = editor.ij.project ?: return editor.currentCaret().offset
val currentWindow = FileEditorManagerEx.getInstanceEx(project).splitters.currentWindow
switchEditorTab(currentWindow, if (absolute) rawCount - 1 else 1, absolute)
return editor.currentCaret().offset.point
}
override fun moveCaretToLinePercent(
editor: VimEditor,
caret: ImmutableVimCaret,
count: Int,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
return moveCaretToLineWithStartOfLineOption(
editor,
editor.normalizeLine((editor.lineCount() * clamp(count, 0, 100) + 99) / 100 - 1),
caret,
)
return editor.currentCaret().offset
}
private enum class ScreenLocation {
@@ -459,16 +302,21 @@ internal class MotionGroup : VimMotionGroupBase() {
}
fun fileEditorManagerSelectionChangedCallback(event: FileEditorManagerEvent) {
ExEntryPanel.deactivateAll()
val fileEditor = event.oldEditor
if (fileEditor is TextEditor) {
val editor = fileEditor.editor
if (!editor.isDisposed) {
ExOutputModel.getInstance(editor).clear()
editor.vim.let { vimEditor ->
if (VimStateMachine.getInstance(vimEditor).mode is Mode.VISUAL) {
vimEditor.exitVisualMode()
KeyHandler.getInstance().reset(vimEditor)
when (vimEditor.vimStateMachine.mode) {
is Mode.VISUAL -> {
vimEditor.exitVisualMode()
KeyHandler.getInstance().reset(vimEditor)
}
is Mode.CMD_LINE -> {
injector.processGroup.cancelExEntry(vimEditor, false)
ExOutputModel.getInstance(editor).clear()
}
else -> {}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicatorProvider
@@ -28,25 +27,20 @@ import com.maddyhome.idea.vim.api.VimProcessGroupBase
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.ex.InvalidCommandException
import com.maddyhome.idea.vim.helper.requestFocus
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.VimStateMachine.Companion.getInstance
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.Mode.NORMAL
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.returnTo
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext
import java.io.BufferedWriter
import java.io.IOException
import java.io.OutputStreamWriter
import java.io.Reader
import java.io.Writer
import javax.swing.KeyStroke
import javax.swing.SwingUtilities
public class ProcessGroup : VimProcessGroupBase() {
override var lastCommand: String? = null
@@ -54,25 +48,13 @@ public class ProcessGroup : VimProcessGroupBase() {
override var isCommandProcessing: Boolean = false
override var modeBeforeCommandProcessing: Mode? = null
public override fun startSearchCommand(editor: VimEditor, context: ExecutionContext, count: Int, leader: Char) {
// Don't allow searching in one line editors
if (editor.isOneLineMode()) return
val initText = ""
val label = leader.toString()
val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, label, initText, count)
}
public override fun endSearchCommand(): String {
val panel = ExEntryPanel.getInstance()
panel.deactivate(true)
return panel.text
}
public override fun startExCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) {
public override fun startExEntry(
editor: VimEditor,
context: ExecutionContext,
command: Command,
label: String,
initialCommandText: String,
) {
// Don't allow ex commands in one line editors
if (editor.isOneLineMode()) return
@@ -83,11 +65,24 @@ public class ProcessGroup : VimProcessGroupBase() {
isCommandProcessing = true
modeBeforeCommandProcessing = currentMode
val initText = getRange(editor, cmd)
// Make sure the Visual selection marks are up to date before we use them.
injector.markService.setVisualSelectionMarks(editor)
val rangeText = getRange(editor, command)
// Note that we should remove selection and reset caret offset before we switch back to Normal mode and then enter
// Command-line mode. However, some IdeaVim commands can handle multiple carets, including multiple carets with
// selection (which might or might not be a block selection). Unfortunately, because we're just entering
// Command-line mode, we don't know which command is going to be entered, so we can't remove selection here.
// Therefore, we switch to Normal and then Command-line even though we might still have a Visual selection...
// On the plus side, it means we still show selection while editing the command line, which Vim also does
// (Normal then Command-line is not strictly necessary, but done for completeness and autocmd)
// Caret selection is finally handled in Command.execute
editor.mode = Mode.NORMAL()
editor.mode = Mode.CMD_LINE(currentMode)
val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, ":", initText, 1)
injector.commandLine.create(editor, context, ":", rangeText + initialCommandText, 1)
}
public override fun processExKey(editor: VimEditor, stroke: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean {
@@ -103,86 +98,27 @@ public class ProcessGroup : VimProcessGroupBase() {
return true
} else {
processResultBuilder.addExecutionStep { _, lambdaEditor, _ ->
lambdaEditor.mode = NORMAL()
lambdaEditor.mode = Mode.NORMAL()
getInstance().reset(lambdaEditor)
}
return false
}
}
public override fun processExEntry(editor: VimEditor, context: ExecutionContext): Boolean {
val panel = ExEntryPanel.getInstance()
panel.deactivate(true)
var res = true
try {
editor.mode = NORMAL()
logger.debug("processing command")
val text = panel.text
if (panel.label != ":") {
// Search is handled via Argument.Type.EX_STRING. Although ProcessExEntryAction is registered as the handler for
// <CR> in both command and search modes, it's only invoked for command mode (see KeyHandler.handleCommandNode).
// We should never be invoked for anything other than an actual ex command.
throw InvalidCommandException("Expected ':' command. Got '" + panel.label + "'", text)
}
logger.debug {
"swing=" + SwingUtilities.isEventDispatchThread()
}
injector.vimscriptExecutor.execute(text, editor, context, skipHistory(editor), true, CommandLineVimLContext)
} catch (e: ExException) {
VimPlugin.showMessage(e.message)
VimPlugin.indicateError()
res = false
} catch (bad: Exception) {
logger.error(bad)
VimPlugin.indicateError()
res = false
} finally {
isCommandProcessing = false
modeBeforeCommandProcessing = null
}
return res
}
// commands executed from map command / macro should not be added to history
private fun skipHistory(editor: VimEditor): Boolean {
return getInstance(editor).mappingState.isExecutingMap() || injector.macro.isExecutingMacro
}
public override fun cancelExEntry(editor: VimEditor, resetCaret: Boolean) {
editor.mode = NORMAL()
// If 'cpoptions' contains 'x', then Escape should execute the command line. This is the default for Vi but not Vim.
// IdeaVim does not (currently?) support 'cpoptions', so sticks with Vim's default behaviour. Escape cancels.
editor.mode = editor.mode.returnTo()
getInstance().reset(editor)
val panel = ExEntryPanel.getInstance()
panel.deactivate(true, resetCaret)
}
public override fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) {
val initText = getRange(editor, cmd) + "!"
val currentMode = editor.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable cmd mode from $currentMode" }
editor.mode = Mode.CMD_LINE(currentMode)
val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, ":", initText, 1)
}
private fun getRange(editor: VimEditor, cmd: Command): String {
var initText = ""
if (editor.vimStateMachine.mode is VISUAL) {
initText = "'<,'>"
} else if (cmd.rawCount > 0) {
initText = if (cmd.count == 1) {
"."
} else {
".,.+" + (cmd.count - 1)
}
}
return initText
private fun getRange(editor: VimEditor, cmd: Command) = when {
editor.inVisualMode -> "'<,'>"
cmd.rawCount == 1 -> "."
cmd.rawCount > 1 -> ".,.+" + (cmd.count - 1)
else -> ""
}
@Throws(ExecutionException::class, ProcessCanceledException::class)

View File

@@ -24,7 +24,6 @@ import com.maddyhome.idea.vim.helper.ScrollViewHelper
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.getNormalizedScrollOffset
import com.maddyhome.idea.vim.helper.getNormalizedSideScrollOffset
import com.maddyhome.idea.vim.helper.vimEditorGroup
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.EffectiveOptionValueChangeListener
@@ -258,11 +257,7 @@ internal class ScrollGroup : VimScrollGroup {
object ScrollOptionsChangeListener : EffectiveOptionValueChangeListener {
override fun onEffectiveValueChanged(editor: VimEditor) {
editor.ij.apply {
if (vimEditorGroup) {
ScrollViewHelper.scrollCaretIntoView(this)
}
}
editor.ij.apply { ScrollViewHelper.scrollCaretIntoView(this) }
}
}

View File

@@ -24,6 +24,7 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.util.Ref;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.*;
import com.maddyhome.idea.vim.command.MotionType;
import com.maddyhome.idea.vim.common.CharacterPosition;
import com.maddyhome.idea.vim.common.Direction;
import com.maddyhome.idea.vim.common.TextRange;
@@ -60,23 +61,19 @@ import static com.maddyhome.idea.vim.helper.SearchHelperKtKt.shouldIgnoreCase;
import static com.maddyhome.idea.vim.newapi.IjVimInjectorKt.globalIjOptions;
import static com.maddyhome.idea.vim.register.RegisterConstants.LAST_SEARCH_REGISTER;
/**
* @deprecated Replace with IjVimSearchGroup
*/
@State(name = "VimSearchSettings", storages = {
@Storage(value = "$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)
})
@Deprecated
/**
* @deprecated Replace with IjVimSearchGroup
*/
public class SearchGroup extends IjVimSearchGroup implements PersistentStateComponent<Element> {
public SearchGroup() {
super();
if (!globalIjOptions(injector).getUseNewRegex()) {
// TODO: Investigate migrating these listeners to use the effective value change listener
// This would allow us to update the editor we're told to update, rather than looping over all projects and updating
// the highlights in that project's current document's open editors (see VIM-2779).
// However, we probably only want to update the editors associated with the current document, so maybe the whole
// code needs to be reworked. We're currently using the same update code for changes in the search term as well as
// changes in the search options.
// We use the global option listener instead of the effective listener that gets called for each affected editor
// because we handle updating the affected editors ourselves (e.g., we can filter for visible windows).
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.hlsearch, () -> {
resetShowSearchHighlight();
forceUpdateSearchHighlights();
@@ -150,11 +147,11 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
@Override
protected @Nullable String getLastUsedPattern() {
if (globalIjOptions(injector).getUseNewRegex()) return super.getLastUsedPattern();
switch (lastPatternIdx) {
case RE_SEARCH: return lastSearch;
case RE_SUBST: return lastSubstitute;
}
return null;
return switch (lastPatternIdx) {
case RE_SEARCH -> lastSearch;
case RE_SUBST -> lastSubstitute;
default -> null;
};
}
/**
@@ -272,11 +269,18 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
* Can include a trailing offset, e.g. /{pattern}/{offset}, or multiple commands separated by a semicolon.
* If the pattern is empty, the last used (search? substitute?) pattern (and offset?) is used.
* @param dir The direction to search
* @return Offset to the next occurrence of the pattern or -1 if not found
* @return Pair containing the offset to the next occurrence of the pattern, and the [MotionType] based on
* the search offset. The value will be `null` if no result is found.
*/
@Nullable
@Override
public int processSearchCommand(@NotNull VimEditor editor, @NotNull String command, int startOffset, @NotNull Direction dir) {
if (globalIjOptions(injector).getUseNewRegex()) return super.processSearchCommand(editor, command, startOffset, dir);
public Pair<Integer, MotionType> processSearchCommand(@NotNull VimEditor editor,
@NotNull String command,
int startOffset,
int count1,
@NotNull Direction dir) {
if (globalIjOptions(injector).getUseNewRegex()) return super.processSearchCommand(editor, command, startOffset, count1, dir);
boolean isNewPattern = false;
String pattern = null;
@@ -284,7 +288,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
final char type = dir == Direction.FORWARDS ? '/' : '?';
if (command.length() > 0) {
if (!command.isEmpty()) {
if (command.charAt(0) != type) {
CharPointer p = new CharPointer(command);
CharPointer end = RegExp.skip_regexp(p.ref(0), type, true);
@@ -336,17 +340,19 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
// Direction is saved in do_search
// IgnoreSmartCase is only ever set for searching words (`*`, `#`, `g*`, etc.) and is reset for all other operations
if (pattern == null || pattern.length() == 0) {
if (pattern == null || pattern.isEmpty()) {
pattern = getLastSearchPattern();
patternOffset = lastPatternOffset;
if (pattern == null || pattern.length() == 0) {
if (pattern == null || pattern.isEmpty()) {
isNewPattern = true;
pattern = getLastSubstitutePattern();
if (pattern == null || pattern.length() == 0) {
if (pattern == null || pattern.isEmpty()) {
VimPlugin.showMessage(MessageHelper.message("e_noprevre"));
return -1;
return null;
}
}
if (patternOffset == null || patternOffset.isEmpty()) {
patternOffset = lastPatternOffset;
}
}
// Save the pattern. If it's explicitly entered, or comes from RE_SUBST, isNewPattern is true, and this becomes
@@ -365,7 +371,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
resetShowSearchHighlight();
forceUpdateSearchHighlights();
return findItOffset(((IjVimEditor)editor).getEditor(), startOffset, 1, lastDir);
return findItOffset(((IjVimEditor)editor).getEditor(), startOffset, count1, lastDir);
}
/**
@@ -407,7 +413,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
resetShowSearchHighlight();
forceUpdateSearchHighlights();
final int result = findItOffset(editor, startOffset, 1, lastDir);
final Pair<Integer, MotionType> result = findItOffset(editor, startOffset, 1, lastDir);
// Set lastPatternOffset AFTER searching so it doesn't affect the result
lastPatternOffset = patternOffset != 0 ? Integer.toString(patternOffset) : "";
@@ -418,7 +424,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
logger.debug("lastDir=" + lastDir);
}
return result;
return result != null ? result.getFirst() : -1;
}
/**
@@ -463,8 +469,9 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
resetShowSearchHighlight();
forceUpdateSearchHighlights();
final int offset = findItOffset(((IjVimEditor)editor).getEditor(), range.getStartOffset(), count, lastDir);
return offset == -1 ? range.getStartOffset() : offset;
final Pair<Integer, MotionType> offsetAndMotion =
findItOffset(((IjVimEditor)editor).getEditor(), range.getStartOffset(), count, lastDir);
return offsetAndMotion == null ? range.getStartOffset() : offsetAndMotion.getFirst();
}
/**
@@ -507,14 +514,14 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
resetShowSearchHighlight();
updateSearchHighlights();
final int startOffset = caret.getOffset();
int offset = findItOffset(editor, startOffset, count, dir);
if (offset == startOffset) {
Pair<Integer, MotionType> offsetAndMotion = findItOffset(editor, startOffset, count, dir);
if (offsetAndMotion != null && offsetAndMotion.getFirst() == startOffset) {
/* Avoid getting stuck on the current cursor position, which can
* happen when an offset is given and the cursor is on the last char
* in the buffer: Repeat with count + 1. */
offset = findItOffset(editor, startOffset, count + 1, dir);
offsetAndMotion = findItOffset(editor, startOffset, count + 1, dir);
}
return offset;
return offsetAndMotion != null ? offsetAndMotion.getFirst() : -1;
}
@@ -538,20 +545,24 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
*
* @param editor The editor to search in
* @param caret The caret to use for initial search offset, and to move for interactive substitution
* @param context
* @param range Only search and substitute within the given line range. Must be valid
* @param excmd The command part of the ex command line, e.g. `s` or `substitute`, or `~`
* @param exarg The argument to the substitute command, such as `/{pattern}/{string}/[flags]`
* @return True if the substitution succeeds, false on error. Will succeed even if nothing is modified
* @return True if the substitution succeeds, false on error. Will succeed even if nothing is modified
*/
@Override
@RWLockLabel.SelfSynchronized
public boolean processSubstituteCommand(@NotNull VimEditor editor,
@NotNull VimCaret caret,
@NotNull ExecutionContext context,
@NotNull LineRange range,
@NotNull @NonNls String excmd,
@NotNull @NonNls String exarg,
@NotNull VimLContext parent) {
if (globalIjOptions(injector).getUseNewRegex()) return super.processSubstituteCommand(editor, caret, range, excmd, exarg, parent);
if (globalIjOptions(injector).getUseNewRegex()) {
return super.processSubstituteCommand(editor, caret, context, range, excmd, exarg, parent);
}
// Explicitly exit visual mode here, so that visual mode marks don't change when we move the cursor to a match.
List<ExException> exceptions = new ArrayList<>();
@@ -800,7 +811,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
CharacterPosition startpos = new CharacterPosition(lnum + regmatch.startpos[0].lnum, regmatch.startpos[0].col);
CharacterPosition endpos = new CharacterPosition(lnum + regmatch.endpos[0].lnum, regmatch.endpos[0].col);
int startoff = startpos.toOffset(((IjVimEditor)editor).getEditor());
int endoff = endpos.toOffset(((IjVimEditor)editor).getEditor());
int endoff = (endpos.line >= lcount) ? (int) editor.fileSize() : endpos.toOffset(((IjVimEditor)editor).getEditor());
if (do_all || line != lastLine) {
boolean doReplace = true;
@@ -808,7 +819,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
RangeHighlighter hl =
SearchHighlightsHelper.addSubstitutionConfirmationHighlight(((IjVimEditor)editor).getEditor(), startoff,
endoff);
final ReplaceConfirmationChoice choice = confirmChoice(((IjVimEditor)editor).getEditor(), match, ((IjVimCaret)caret).getCaret(), startoff);
final ReplaceConfirmationChoice choice = confirmChoice(((IjVimEditor)editor).getEditor(), context, match, ((IjVimCaret)caret).getCaret(), startoff);
((IjVimEditor)editor).getEditor().getMarkupModel().removeHighlighter(hl);
switch (choice) {
case SUBSTITUTE_THIS:
@@ -837,8 +848,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
caret.moveToOffset(startoff);
if (expression != null) {
try {
match =
expression.evaluate(editor, injector.getExecutionContextManager().onEditor(editor, null), parent).toInsertableString();
match = expression.evaluate(editor, context, parent).toInsertableString();
}
catch (Exception e) {
exceptions.add((ExException)e);
@@ -860,23 +870,23 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
lastLine = line;
lnum += nmatch - 1;
if (do_all && startoff != endoff) {
if (newpos != null) {
lnum = newpos.line;
searchcol = newpos.column;
}
else {
lnum += Math.max(1, nmatch - 1);
searchcol = endpos.column;
}
}
else {
lnum += Math.max(1, nmatch - 1);
searchcol = 0;
lnum++;
}
}
else {
lnum++;
lnum += Math.max(1, nmatch - 1);
searchcol = 0;
}
}
@@ -887,7 +897,8 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, editor.offsetToBufferPosition(lastMatch).getLine()));
}
else {
VimPlugin.showMessage(MessageHelper.message(Msg.e_patnotf2, pattern));
// E486: Pattern not found: {0}
VimPlugin.showMessage(MessageHelper.message("E486", pattern));
}
}
@@ -949,17 +960,17 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
if (which_pat == RE_LAST) {
which_pat = lastPatternIdx;
}
String errorMessage = null;
switch (which_pat) {
case RE_SEARCH:
String errorMessage = switch (which_pat) {
case RE_SEARCH -> {
pattern = lastSearch;
errorMessage = MessageHelper.message("e_nopresub");
break;
case RE_SUBST:
yield MessageHelper.message("e_nopresub");
}
case RE_SUBST -> {
pattern = lastSubstitute;
errorMessage = MessageHelper.message("e_noprevre");
break;
}
yield MessageHelper.message("e_noprevre");
}
default -> null;
};
// Pattern was never defined
if (pattern == null) {
@@ -989,7 +1000,9 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
return new Pair<>(true, new Triple<>(regmatch, pattern, sp));
}
private static @NotNull ReplaceConfirmationChoice confirmChoice(@NotNull Editor editor, @NotNull String match, @NotNull Caret caret, int startoff) {
private static @NotNull ReplaceConfirmationChoice confirmChoice(@NotNull Editor editor,
@NotNull ExecutionContext context,
@NotNull String match, @NotNull Caret caret, int startoff) {
final Ref<ReplaceConfirmationChoice> result = Ref.create(ReplaceConfirmationChoice.QUIT);
final Function1<KeyStroke, Boolean> keyStrokeProcessor = key -> {
final ReplaceConfirmationChoice choice;
@@ -1023,7 +1036,6 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
else {
// XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for this method
final ExEntryPanel exEntryPanel = ExEntryPanel.getInstanceWithoutShortcuts();
ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(editor), null);
exEntryPanel.activate(editor, ((IjEditorExecutionContext)context).getContext(), MessageHelper.message("replace.with.0", match), "", 1);
new IjVimCaret(caret).moveToOffset(startoff);
ModalEntry.INSTANCE.activate(new IjVimEditor(editor), keyStrokeProcessor);
@@ -1081,9 +1093,9 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
private @Nullable TextRange findNextSearchForGn(@NotNull VimEditor editor, int count, boolean forwards) {
if (forwards) {
final EnumSet<SearchOptions> searchOptions = EnumSet.of(SearchOptions.WRAP, SearchOptions.WHOLE_FILE);
return VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), editor.primaryCaret().getOffset().getPoint(), count, searchOptions);
return VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), editor.primaryCaret().getOffset(), count, searchOptions);
} else {
return searchBackward(editor, editor.primaryCaret().getOffset().getPoint(), count);
return searchBackward(editor, editor.primaryCaret().getOffset(), count);
}
}
@@ -1208,27 +1220,33 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
final Document document = event.getDocument();
for (VimEditor vimEditor : injector.getEditorGroup().getEditors(new IjVimDocument(document))) {
final Editor editor = ((IjVimEditor)vimEditor).getEditor();
Collection<RangeHighlighter> hls = UserDataManager.getVimLastHighlighters(editor);
if (hls == null) {
Collection<RangeHighlighter> existingHighlighters = UserDataManager.getVimLastHighlighters(editor);
if (existingHighlighters == null) {
continue;
}
if (logger.isDebugEnabled()) {
logger.debug("hls=" + hls);
logger.debug("hls=" + existingHighlighters);
logger.debug("event=" + event);
}
// We can only re-highlight whole lines, so clear any highlights in the affected lines
// We can only re-highlight whole lines, so clear any highlights in the affected lines.
// If we're deleting lines, this will clear + re-highlight the new current line, which hasn't been modified.
// However, we still want to re-highlight this line in case any highlights cross the line boundaries.
// If we're adding lines, this will clear + re-highlight all new lines.
final LogicalPosition startPosition = editor.offsetToLogicalPosition(event.getOffset());
final LogicalPosition endPosition = editor.offsetToLogicalPosition(event.getOffset() + event.getNewLength());
final int startLineOffset = document.getLineStartOffset(startPosition.line);
final int endLineOffset = document.getLineEndOffset(endPosition.line);
final Iterator<RangeHighlighter> iter = hls.iterator();
// Remove any highlights that have already been deleted, and remove + clear those that intersect with the change
final Iterator<RangeHighlighter> iter = existingHighlighters.iterator();
while (iter.hasNext()) {
final RangeHighlighter highlighter = iter.next();
if (!highlighter.isValid() ||
(highlighter.getStartOffset() >= startLineOffset && highlighter.getEndOffset() <= endLineOffset)) {
if (!highlighter.isValid()) {
iter.remove();
}
else if (highlighter.getTextRange().intersects(startLineOffset, endLineOffset)) {
iter.remove();
editor.getMarkupModel().removeHighlighter(highlighter);
}
@@ -1237,9 +1255,9 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
VimPlugin.getSearch().highlightSearchLines(editor, startPosition.line, endPosition.line);
if (logger.isDebugEnabled()) {
hls = UserDataManager.getVimLastHighlighters(editor);
existingHighlighters = UserDataManager.getVimLastHighlighters(editor);
logger.debug("sl=" + startPosition.line + ", el=" + endPosition.line);
logger.debug("hls=" + hls);
logger.debug("hls=" + existingHighlighters);
}
}
}
@@ -1265,9 +1283,11 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
* @param startOffset The offset to search from
* @param count Find the nth occurrence
* @param dir The direction to search in
* @return The offset to the occurrence or -1 if not found
* @return Pair containing the offset to the next occurrence of the pattern, and the [MotionType] based
* on the search offset. The value will be `null` if no result is found.
*/
private int findItOffset(@NotNull Editor editor, int startOffset, int count, Direction dir) {
@Nullable
private Pair<Integer, MotionType> findItOffset(@NotNull Editor editor, int startOffset, int count, Direction dir) {
boolean wrap = globalOptions(injector).getWrapscan();
logger.debug("Perform search. Direction: " + dir + " wrap: " + wrap);
@@ -1277,7 +1297,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
ParsePosition pp = new ParsePosition(0);
if (lastPatternOffset.length() > 0) {
if (!lastPatternOffset.isEmpty()) {
if (Character.isDigit(lastPatternOffset.charAt(0)) || lastPatternOffset.charAt(0) == '+' || lastPatternOffset.charAt(0) == '-') {
offsetIsLineOffset = true;
@@ -1313,6 +1333,18 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
}
}
// `/{pattern}/{offset}` is inclusive if offset contains `e`, and linewise if there's a line offset
final MotionType motionType;
if (offset != 0 && !hasEndOffset) {
motionType = MotionType.LINE_WISE;
}
else if (hasEndOffset) {
motionType = MotionType.INCLUSIVE;
}
else {
motionType = MotionType.EXCLUSIVE;
}
/*
* If there is a character offset, subtract it from the current
* position, so we don't get stuck at "?pat?e+2" or "/pat/s-2".
@@ -1334,7 +1366,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
TextRange range = injector.getSearchHelper().findPattern(new IjVimEditor(editor), getLastUsedPattern(), startOffset, count, searchOptions);
if (range == null) {
logger.warn("No range is found");
return -1;
return null;
}
int res = range.getStartOffset();
@@ -1342,8 +1374,6 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
if (offsetIsLineOffset) {
int line = editor.offsetToLogicalPosition(range.getStartOffset()).line;
int newLine = EngineEditorHelperKt.normalizeLine(new IjVimEditor(editor), line + offset);
// TODO: Don't move the caret!
res = VimPlugin.getMotion().moveCaretToLineStart(new IjVimEditor(editor), newLine);
}
else if (hasEndOffset || offset != 0) {
@@ -1361,16 +1391,19 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
nextDir = Direction.BACKWARDS;
}
else {
return res;
return new Pair(res, motionType);
}
if (lastPatternOffset.length() - ppos > 2) {
ppos++;
}
res = processSearchCommand(new IjVimEditor(editor), lastPatternOffset.substring(ppos + 1), res, nextDir);
Pair<Integer, MotionType> offsetAndMotion =
processSearchCommand(new IjVimEditor(editor), lastPatternOffset.substring(ppos + 1), res, 1, nextDir);
res = offsetAndMotion != null ? offsetAndMotion.getFirst() : -1;
}
return res;
return new Pair<Integer, MotionType>(res, motionType);
}
@@ -1386,7 +1419,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
addOptionalTextElement(search, "last-search", lastSearch);
addOptionalTextElement(search, "last-substitute", lastSubstitute);
addOptionalTextElement(search, "last-offset", lastPatternOffset.length() > 0 ? lastPatternOffset : null);
addOptionalTextElement(search, "last-offset", !lastPatternOffset.isEmpty() ? lastPatternOffset : null);
addOptionalTextElement(search, "last-replace", lastReplace);
addOptionalTextElement(search, "last-pattern", lastPatternIdx == RE_SEARCH ? lastSearch : lastSubstitute);
addOptionalTextElement(search, "last-dir", Integer.toString(lastDir.toInt()));

View File

@@ -26,9 +26,11 @@ import com.maddyhome.idea.vim.listener.VimListenerManager
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.OptionConstants
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inCommandLineMode
import com.maddyhome.idea.vim.state.mode.inNormalMode
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.returnTo
import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
@@ -67,6 +69,11 @@ internal object IdeaSelectionControl {
}
if (editor.selectionModel.hasSelection(true)) {
if (editor.vim.inCommandLineMode && editor.vim.mode.returnTo().hasVisualSelection) {
logger.trace { "Modifying selection while in Command-line mode, most likely incsearch" }
return@singleTask
}
if (dontChangeMode(editor)) {
IdeaRefactorModeHelper.correctSelection(editor)
logger.trace { "Selection corrected for refactoring" }
@@ -117,7 +124,7 @@ internal object IdeaSelectionControl {
is Mode.VISUAL -> VimPlugin.getVisualMotion().enterVisualMode(editor.vim, mode.selectionType)
is Mode.SELECT -> VimPlugin.getVisualMotion().enterSelectMode(editor.vim, mode.selectionType)
is Mode.INSERT -> VimPlugin.getChange()
.insertBeforeCursor(editor.vim, injector.executionContextManager.onEditor(editor.vim))
.insertBeforeCursor(editor.vim, injector.executionContextManager.getEditorExecutionContext(editor.vim))
is Mode.NORMAL -> Unit
else -> error("Unexpected mode: $mode")

View File

@@ -337,7 +337,7 @@ internal abstract class VimKeyHandler(nextHandler: EditorActionHandler?) : Octop
override fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val enterKey = key(key)
val context = injector.executionContextManager.onEditor(editor.vim, dataContext?.vim)
val context = dataContext?.vim ?: injector.executionContextManager.getEditorExecutionContext(editor.vim)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor.vim, enterKey, context, keyHandler.keyHandlerState)
}

View File

@@ -90,6 +90,8 @@ private fun Editor.updatePrimaryCaretVisualAttributes() {
// Make sure the caret is visible as soon as it's set. It might be invisible while blinking
// NOTE: At the moment, this causes project leak in tests
// IJPL-928 - this will be fixed in 2024.2
// [VERSION UPDATE] 2024.2 - remove if wrapping
if (!ApplicationManager.getApplication().isUnitTestMode) {
(this as? EditorEx)?.setCaretVisible(true)
}

View File

@@ -14,6 +14,7 @@ import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
@Deprecated("Do not use context wrappers, use existing provided contexts. If no context available, use `injector.getExecutionContextManager().getEditorExecutionContext(editor)`")
internal class EditorDataContext @Deprecated("Please use `init` method") constructor(
private val editor: Editor,
private val editorContext: DataContext,

View File

@@ -8,10 +8,12 @@
package com.maddyhome.idea.vim.helper;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.LightVirtualFile;
import com.maddyhome.idea.vim.api.EngineEditorHelperKt;
@@ -52,6 +54,10 @@ public class EditorHelper {
final ScrollingModel scrollingModel = editor.getScrollingModel();
final Rectangle area = scrollingModel.getVisibleAreaOnScrollingFinished();
scrollingModel.scroll(area.x, verticalOffset);
// Simulate Vim's redraw (essentially to clear messages) if the screen is scrolled
if (area.y != verticalOffset && area.y >= 0) {
injector.getRedrawService().redraw();
}
return scrollingModel.getVisibleAreaOnScrollingFinished().y != area.y;
}
@@ -59,6 +65,10 @@ public class EditorHelper {
final ScrollingModel scrollingModel = editor.getScrollingModel();
final Rectangle area = scrollingModel.getVisibleAreaOnScrollingFinished();
scrollingModel.scroll(horizontalOffset, area.y);
// Simulate Vim's redraw (essentially to clear messages) if the screen is scrolled
if (area.x != horizontalOffset && area.x >= 0) {
injector.getRedrawService().redraw();
}
}
public static int getVisualLineAtTopOfScreen(final @NotNull Editor editor) {
@@ -77,10 +87,12 @@ public class EditorHelper {
public static int getNonNormalizedVisualLineAtBottomOfScreen(final @NotNull Editor editor) {
// The editor will return line numbers of virtual space if the text doesn't reach the end of the visible area
// (either because it's too short, or it's been scrolled up)
// (either because it's too short, or it's been scrolled up).
// Adjust available height if the ex entry text field is visible
final Rectangle visibleArea = getVisibleArea(editor);
return getFullVisualLine(editor, visibleArea.y + visibleArea.height, visibleArea.y,
visibleArea.y + visibleArea.height);
final int height = visibleArea.height - getExEntryHeight() - getHorizontalScrollbarHeight(editor);
return getFullVisualLine(editor, visibleArea.y + height, visibleArea.y,
visibleArea.y + height);
}
public static int getVisualLineAtBottomOfScreen(final @NotNull Editor editor) {
@@ -211,15 +223,12 @@ public class EditorHelper {
return injector.getEditorGroup().getEditors(new IjVimDocument(doc)).stream().findFirst().orElse(null);
}
public static @NotNull String pad(final @NotNull Editor editor,
@NotNull DataContext context,
int line,
final int to) {
public static @NotNull String pad(final @NotNull Editor editor, int line, final int to) {
final int len = EngineEditorHelperKt.lineLength(new IjVimEditor(editor), line);
if (len >= to) return "";
final int limit = to - len;
return IndentConfig.create(editor, context).createIndentBySize(limit);
return IndentConfig.create(editor).createIndentBySize(limit);
}
/**
@@ -358,24 +367,40 @@ public class EditorHelper {
}
private static int getOffsetToScrollVisualLineToBottomOfScreen(@NotNull Editor editor, int nonNormalisedVisualLine) {
int exPanelHeight = 0;
if (ExEntryPanel.getInstance().isActive()) {
exPanelHeight = ExEntryPanel.getInstance().getHeight();
}
if (ExEntryPanel.getInstanceWithoutShortcuts().isActive()) {
exPanelHeight += ExEntryPanel.getInstanceWithoutShortcuts().getHeight();
}
// Note that we explicitly do not normalise the visual line, as we might be trying to scroll a virtual line, at the
// end of the file
// end of the file.
// Adjust available height if the ex entry text field is visible
final int lineHeight = editor.getLineHeight();
final int screenHeight = getVisibleArea(editor).height - exPanelHeight;
final int screenHeight = getVisibleArea(editor).height - getExEntryHeight() - getHorizontalScrollbarHeight(editor);
final int inlayHeight = EditorUtil.getInlaysHeight(editor, nonNormalisedVisualLine, false);
final int maxInlayHeight = BLOCK_INLAY_MAX_LINE_HEIGHT * lineHeight;
final int y = editor.visualLineToY(nonNormalisedVisualLine) + lineHeight + min(inlayHeight, maxInlayHeight);
return max(0, y - screenHeight);
}
private static int getExEntryHeight() {
if (ExEntryPanel.getInstance().isActive()) {
return ExEntryPanel.getInstance().getHeight();
}
if (ExEntryPanel.getInstanceWithoutShortcuts().isActive()) {
return ExEntryPanel.getInstanceWithoutShortcuts().getHeight();
}
return 0;
}
private static int getHorizontalScrollbarHeight(@NotNull final Editor editor) {
// Horizontal scrollbars on macOS are either transparent AND auto-hide, so we don't need to worry about obscured
// text, or always visible, opaque and outside the content area, so we don't need to adjust for them
// Transparent scrollbars on Windows and Linux are overlays on the editor content area, and always visible. That
// means they can obscure text, so we want to adjust by the scrollbar height. If they are not transparent, then they
// are not overlays and are outside the content area. We don't need to adjust
if (!SystemInfo.isMac && editor instanceof EditorImpl editorImpl && Registry.is("editor.transparent.scrollbar")) {
return editorImpl.getScrollPane().getHorizontalScrollBar().getHeight();
}
return 0;
}
public static void scrollColumnToLeftOfScreen(@NotNull Editor editor, int visualLine, int visualColumn) {
int targetVisualColumn = visualColumn;

View File

@@ -21,6 +21,7 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.actionSystem.impl.Utils
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.components.Service
@@ -34,7 +35,6 @@ import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction
import com.maddyhome.idea.vim.api.VimActionExecutor
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.newapi.IjNativeAction
@@ -78,6 +78,7 @@ internal class IjActionExecutor : VimActionExecutor {
val dataContext = DataContextWrapper(context.ij)
dataContext.putUserData(runFromVimKey, true)
val actionId = ActionManager.getInstance().getId(ijAction)
val event = AnActionEvent(
null,
dataContext,
@@ -86,12 +87,20 @@ internal class IjActionExecutor : VimActionExecutor {
ActionManager.getInstance(),
0,
)
Utils.initUpdateSession(event)
// beforeActionPerformedUpdate should be called to update the action. It fixes some rider-specific problems.
// because rider use async update method. See VIM-1819.
// This method executes inside of lastUpdateAndCheckDumb
// Another related issue: VIM-2604
ijAction.beforeActionPerformedUpdate(event)
if (!event.presentation.isEnabled) return false
// This is a hack to fix the tests and fix VIM-3332
// We should get rid of it in VIM-3376
if (actionId == "RunClass" || actionId == IdeActions.ACTION_COMMENT_LINE || actionId == IdeActions.ACTION_COMMENT_BLOCK) {
ijAction.beforeActionPerformedUpdate(event)
if (!event.presentation.isEnabled) return false
} else {
if (!ActionUtil.lastUpdateAndCheckDumb(ijAction, event, false)) return false
}
if (ijAction is ActionGroup && !event.presentation.isPerformGroup) {
// Some ActionGroups should not be performed, but shown as a popup
val popup = JBPopupFactory.getInstance()
@@ -215,7 +224,7 @@ internal class IjActionExecutor : VimActionExecutor {
CommandProcessor.getInstance()
.executeCommand(
editor.ij.project,
{ cmd.execute(editor, injector.executionContextManager.onEditor(editor, context), operatorArguments) },
{ cmd.execute(editor, context, operatorArguments) },
cmd.id,
DocCommandGroupId.noneGroupId(editor.ij.document),
UndoConfirmationPolicy.DEFAULT,

View File

@@ -14,7 +14,6 @@ import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.actionSystem.EditorActionManager
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.maddyhome.idea.vim.api.EngineEditorHelper
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.newapi.IjVimEditor
@@ -51,8 +50,8 @@ internal class IjEditorHelper : EngineEditorHelper {
return EditorHelper.getVisualLineAtBottomOfScreen(editor.ij)
}
override fun pad(editor: VimEditor, context: ExecutionContext, line: Int, to: Int): String {
return EditorHelper.pad(editor.ij, context.ij, line, to)
override fun pad(editor: VimEditor, line: Int, to: Int): String {
return EditorHelper.pad(editor.ij, line, to)
}
override fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition {

View File

@@ -8,6 +8,7 @@ package com.maddyhome.idea.vim.helper
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.textarea.TextComponentEditor
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getVisualLineCount
import com.maddyhome.idea.vim.api.injector
@@ -39,6 +40,9 @@ import kotlin.math.roundToInt
internal object ScrollViewHelper {
@JvmStatic
fun scrollCaretIntoView(editor: Editor) {
// TextComponentEditor doesn't support scrolling. We only support TextComponentEditor for the fallback window
if (editor is TextComponentEditor) return
val position = editor.caretModel.visualPosition
scrollCaretIntoViewVertically(editor, position.line)
scrollCaretIntoViewHorizontally(editor, position)

View File

@@ -10,18 +10,10 @@ package com.maddyhome.idea.vim.helper;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx;
import com.intellij.lang.CodeDocumentationAwareCommenter;
import com.intellij.lang.Commenter;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageCommenters;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.spellchecker.SpellCheckerSeveritiesProvider;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.EngineEditorHelperKt;
@@ -318,11 +310,17 @@ public class SearchHelper {
lnum = lineCount - 1;
//if (!shortmess(SHM_SEARCH) && (options & SEARCH_MSG))
// give_warning((char_u *)_(top_bot_msg), TRUE);
if (searchOptions.contains(SearchOptions.SHOW_MESSAGES)) {
VimPlugin.showMessage(MessageHelper.message("message.search.hit.top"));
}
}
else {
lnum = 0;
//if (!shortmess(SHM_SEARCH) && (options & SEARCH_MSG))
// give_warning((char_u *)_(bot_top_msg), TRUE);
if (searchOptions.contains(SearchOptions.SHOW_MESSAGES)) {
VimPlugin.showMessage(MessageHelper.message("message.search.hit.bottom"));
}
}
}
//if (got_int || called_emsg || break_loop)
@@ -334,12 +332,15 @@ public class SearchHelper {
//if ((options & SEARCH_MSG) == SEARCH_MSG)
if (searchOptions.contains(SearchOptions.SHOW_MESSAGES)) {
if (searchOptions.contains(SearchOptions.WRAP)) {
VimPlugin.showMessage(MessageHelper.message(Msg.e_patnotf2, pattern));
// E486: Pattern not found: {0}
VimPlugin.showMessage(MessageHelper.message("E486", pattern));
}
else if (lnum <= 0) {
// E384: Search hit TOP without match for: {0}
VimPlugin.showMessage(MessageHelper.message(Msg.E384, pattern));
}
else {
// E385: Search hit BOTTOM without match for: {0}
VimPlugin.showMessage(MessageHelper.message(Msg.E385, pattern));
}
}
@@ -375,7 +376,7 @@ public class SearchHelper {
final List<TextRange> results = Lists.newArrayList();
if (globalIjOptions(injector).getUseNewRegex()) {
final List<VimRegexOptions> options = new ArrayList<>();
final EnumSet<VimRegexOptions> options = EnumSet.noneOf(VimRegexOptions.class);
if (globalOptions(injector).getSmartcase()) options.add(VimRegexOptions.SMART_CASE);
if (globalOptions(injector).getIgnorecase()) options.add(VimRegexOptions.IGNORE_CASE);
VimEditor vimEditor = new IjVimEditor(editor);
@@ -414,7 +415,7 @@ public class SearchHelper {
final CharacterPosition endPos = new CharacterPosition(line + regMatch.endpos[0].lnum,
regMatch.endpos[0].col);
int start = startPos.toOffset(editor);
int end = endPos.toOffset(editor);
int end = endPos.line >= lineCount ? editor.getDocument().getTextLength() : endPos.toOffset(editor);
results.add(new TextRange(start, end));
if (start != end) {
@@ -632,113 +633,6 @@ public class SearchHelper {
return new TextRange(bstart, bend + 1);
}
private static int findMatchingBlockCommentPair(@NotNull PsiComment comment,
int pos,
@Nullable String prefix,
@Nullable String suffix) {
if (prefix != null && suffix != null) {
// TODO: Try to get rid of `getText()` because it takes a lot of time to calculate the string
final String commentText = comment.getText();
if (commentText.startsWith(prefix) && commentText.endsWith(suffix)) {
final int endOffset = comment.getTextOffset() + comment.getTextLength();
if (pos < comment.getTextOffset() + prefix.length()) {
return endOffset;
}
else if (pos >= endOffset - suffix.length()) {
return comment.getTextOffset();
}
}
}
return -1;
}
private static int findMatchingBlockCommentPair(@NotNull PsiElement element, int pos) {
final Language language = element.getLanguage();
final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language);
final PsiComment comment = PsiTreeUtil.getParentOfType(element, PsiComment.class, false);
if (comment != null) {
final int ret = findMatchingBlockCommentPair(comment, pos, commenter.getBlockCommentPrefix(),
commenter.getBlockCommentSuffix());
if (ret >= 0) {
return ret;
}
if (commenter instanceof CodeDocumentationAwareCommenter docCommenter) {
return findMatchingBlockCommentPair(comment, pos, docCommenter.getDocumentationCommentPrefix(),
docCommenter.getDocumentationCommentSuffix());
}
}
return -1;
}
/**
* This looks on the current line, starting at the cursor position for one of {, }, (, ), [, or ]. It then searches
* forward or backward, as appropriate for the associated match pair. String in double quotes are skipped over.
* Single characters in single quotes are skipped too.
*
* @param editor The editor to search in
* @return The offset within the editor of the found character or -1 if no match was found or none of the characters
* were found on the remainder of the current line.
*/
public static int findMatchingPairOnCurrentLine(@NotNull Editor editor, @NotNull Caret caret) {
int pos = caret.getOffset();
final int commentPos = findMatchingComment(editor, pos);
if (commentPos >= 0) {
return commentPos;
}
int line = caret.getLogicalPosition().line;
final IjVimEditor vimEditor = new IjVimEditor(editor);
int end = EngineEditorHelperKt.getLineEndOffset(vimEditor, line, true);
// To handle the case where visual mode allows the user to go past the end of the line,
// which will prevent loc from finding a pairable character below
if (pos > 0 && pos == end) {
pos = end - 1;
}
final String pairChars = parseMatchPairsOption(vimEditor);
CharSequence chars = editor.getDocument().getCharsSequence();
int loc = -1;
// Search the remainder of the current line for one of the candidate characters
while (pos < end) {
loc = pairChars.indexOf(chars.charAt(pos));
if (loc >= 0) {
break;
}
pos++;
}
int res = -1;
// If we found one ...
if (loc >= 0) {
// What direction should we go now (-1 is backward, 1 is forward)
Direction dir = loc % 2 == 0 ? Direction.FORWARDS : Direction.BACKWARDS;
// Which character did we find and which should we now search for
char found = pairChars.charAt(loc);
char match = pairChars.charAt(loc + dir.toInt());
res = findBlockLocation(chars, found, match, dir, pos, 1, true);
}
return res;
}
/**
* If on the start/end of a block comment, jump to the matching of that comment, or vice versa.
*/
private static int findMatchingComment(@NotNull Editor editor, int pos) {
final PsiFile psiFile = PsiHelper.getFile(editor);
if (psiFile != null) {
final PsiElement element = psiFile.findElementAt(pos);
if (element != null) {
return findMatchingBlockCommentPair(element, pos);
}
}
return -1;
}
private static int findBlockLocation(@NotNull CharSequence chars,
char found,
char match,

View File

@@ -12,25 +12,20 @@ package com.maddyhome.idea.vim.helper
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.ProjectManager
import com.intellij.ui.ColorUtil
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.ranges.LineRange
import com.maddyhome.idea.vim.newapi.IjVimDocument
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.annotations.Contract
import java.awt.Color
import java.awt.Font
import java.util.*
@@ -40,20 +35,35 @@ internal fun updateSearchHighlights(
showHighlights: Boolean,
forceUpdate: Boolean,
) {
updateSearchHighlights(pattern, shouldIgnoreSmartCase, showHighlights, -1, null, true, forceUpdate)
updateSearchHighlights(null, pattern, 1, shouldIgnoreSmartCase, showHighlights, -1, null, true, forceUpdate)
}
internal fun updateIncsearchHighlights(
editor: Editor,
pattern: String,
count1: Int,
forwards: Boolean,
caretOffset: Int,
searchRange: LineRange?,
): Int {
val searchStartOffset =
if (searchRange != null) editor.vim.getLineStartOffset(searchRange.startLine) else caretOffset
val searchStartOffset = if (searchRange != null && searchRange.startLine < editor.document.lineCount) {
editor.vim.getLineStartOffset(searchRange.startLine)
}
else {
caretOffset
}
val showHighlights = injector.options(editor.vim).hlsearch
return updateSearchHighlights(pattern, false, showHighlights, searchStartOffset, searchRange, forwards, false)
return updateSearchHighlights(
editor.vim,
pattern,
count1,
false,
showHighlights,
searchStartOffset,
searchRange,
forwards,
false
)
}
internal fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int): RangeHighlighter {
@@ -74,10 +84,12 @@ internal fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, en
}
/**
* Refreshes current search highlights for all editors of currently active text editor/document
* Refreshes current search highlights for all visible editors
*/
private fun updateSearchHighlights(
currentEditor: VimEditor?,
pattern: String?,
count1: Int,
shouldIgnoreSmartCase: Boolean,
showHighlights: Boolean,
initialOffset: Int,
@@ -85,72 +97,82 @@ private fun updateSearchHighlights(
forwards: Boolean,
forceUpdate: Boolean,
): Int {
var currentMatchOffset = -1
val projectManager = ProjectManager.getInstanceIfCreated() ?: return currentMatchOffset
var currentEditorCurrentMatchOffset = -1
// TODO: This implementation needs rethinking
// It's a bit weird that we update search highlights across all open projects. It would make more sense to treat top
// level project frame windows as separate applications, but we can't do this because IdeaVim does not maintain state
// per-project.
// So, to be clear, this will loop over each project, and therefore, for each project top-level frame, will update
// search highlights in all editors for the document of the currently selected editor. It does not update highlights
// for editors for the document that are in other projects.
// Update highlights in all visible editors. We update non-visible editors when they get focus.
// Note that this now includes all editors - main, diff windows, even toolwindows like the Commit editor and consoles
val editors = injector.editorGroup.getEditors().filter {
injector.application.isUnitTest() || it.ij.component.isShowing
}
for (project in projectManager.openProjects) {
val current = FileEditorManager.getInstance(project).selectedTextEditor ?: continue
val editors = injector.editorGroup.getEditors(IjVimDocument(current.document))
for (vimEditor in editors) {
val editor = (vimEditor as IjVimEditor).editor
if (editor.project != project) {
continue
}
editors.forEach {
val editor = it.ij
var currentMatchOffset = -1
// Try to keep existing highlights if possible. Update if hlsearch has changed or if the pattern has changed.
// Force update for the situations where the text is the same, but the ignore case values have changed.
// E.g. Use `*` to search for a word (which ignores smartcase), then use `/<Up>` to search for the same pattern,
// which will match smartcase. Or changing the smartcase/ignorecase settings
if (shouldRemoveSearchHighlights(editor, pattern, showHighlights) || forceUpdate) {
removeSearchHighlights(editor)
}
// Try to keep existing highlights if possible. Update if hlsearch has changed or if the pattern has changed.
// Force update for the situations where the text is the same, but the ignore case values have changed.
// E.g., Use `*` to search for a word (which ignores smartcase), then use `/<Up>` to search for the same pattern,
// which will match smartcase. Or changing the smartcase/ignorecase settings
if (shouldRemoveSearchHighlights(editor, pattern, showHighlights) || forceUpdate) {
removeSearchHighlights(editor)
}
if (pattern == null) continue
if (pattern == null) return@forEach
if (shouldAddAllSearchHighlights(editor, pattern, showHighlights)) {
// hlsearch (+ incsearch/noincsearch)
val startLine = searchRange?.startLine ?: 0
val endLine = searchRange?.endLine ?: -1
if (shouldAddAllSearchHighlights(editor, pattern, showHighlights)) {
// hlsearch (+ incsearch/noincsearch)
// Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given
// `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows
val vimEditor = editor.vim
val editorLastLine = vimEditor.lineCount() - 1
val searchStartLine = searchRange?.startLine ?: 0
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
if (searchStartLine <= editorLastLine) {
val results =
injector.searchHelper.findAll(IjVimEditor(editor), pattern, startLine, endLine, shouldIgnoreCase(pattern, shouldIgnoreSmartCase))
injector.searchHelper.findAll(
vimEditor,
pattern,
searchStartLine,
searchEndLine,
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
)
if (results.isNotEmpty()) {
currentMatchOffset = findClosestMatch(editor, results, initialOffset, forwards)
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
}
highlightSearchResults(editor, pattern, results, currentMatchOffset)
}
editor.vimLastSearch = pattern
} else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
// nohlsearch + incsearch
}
editor.vimLastSearch = pattern
} else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
// nohlsearch + incsearch. Only highlight the current editor
if (editor === currentEditor?.ij) {
val searchOptions = EnumSet.of(SearchOptions.WHOLE_FILE)
if (injector.globalOptions().wrapscan) {
searchOptions.add(SearchOptions.WRAP)
}
if (injector.globalOptions().wrapscan) searchOptions.add(SearchOptions.WRAP)
if (shouldIgnoreSmartCase) searchOptions.add(SearchOptions.IGNORE_SMARTCASE)
if (!forwards) searchOptions.add(SearchOptions.BACKWARDS)
val result = injector.searchHelper.findPattern(IjVimEditor(editor), pattern, initialOffset, 1, searchOptions)
val result = injector.searchHelper.findPattern(it, pattern, initialOffset, count1, searchOptions)
if (result != null) {
currentMatchOffset = result.startOffset
val results = listOf(result)
highlightSearchResults(editor, pattern, results, currentMatchOffset)
}
} else if (shouldMaintainCurrentMatchOffset(pattern, initialOffset)) {
// incsearch. If nothing has changed (e.g. we've edited offset values in `/foo/e+2`) make sure we return the
// current match offset so the caret remains at the current incsarch match
val offset = editor.vimIncsearchCurrentMatchOffset
if (offset != null) {
currentMatchOffset = offset
highlightSearchResults(editor, pattern, results, result.startOffset)
currentMatchOffset = result.startOffset
}
}
} else if (shouldMaintainCurrentMatchOffset(pattern, initialOffset)) {
// incsearch. If nothing has changed (e.g., we've edited offset values in `/foo/e+2`) make sure we return the
// current match offset so the caret remains at the current incsarch match
val offset = editor.vimIncsearchCurrentMatchOffset
if (offset != null && editor === currentEditor?.ij) {
currentMatchOffset = offset
}
}
if (editor === currentEditor?.ij) {
currentEditorCurrentMatchOffset = currentMatchOffset
}
}
return currentMatchOffset
return currentEditorCurrentMatchOffset
}
/**
@@ -178,37 +200,22 @@ private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hl
return hlSearch && newPattern != null && newPattern != editor.vimLastSearch && newPattern != ""
}
private fun findClosestMatch(editor: Editor, results: List<TextRange>, initialOffset: Int, forwards: Boolean): Int {
private fun findClosestMatch(
results: List<TextRange>,
initialOffset: Int,
count: Int,
forwards: Boolean,
): Int {
if (results.isEmpty() || initialOffset == -1) {
return -1
}
val size = editor.fileSize
val max = Collections.max(results) { r1: TextRange, r2: TextRange ->
val d1 = distance(r1, initialOffset, forwards, size)
val d2 = distance(r2, initialOffset, forwards, size)
if (d1 < 0 && d2 >= 0) {
return@max Int.MAX_VALUE
}
d2 - d1
}
if (!injector.globalOptions().wrapscan) {
val start = max.startOffset
if (forwards && start < initialOffset) {
return -1
} else if (start >= initialOffset) {
return -1
}
}
return max.startOffset
}
private fun distance(range: TextRange, pos: Int, forwards: Boolean, size: Int): Int {
val start = range.startOffset
return if (start <= pos) {
if (forwards) size - pos + start else pos - start
} else {
if (forwards) start - pos else pos + size - start
val sortedResults = results.sortedBy { it.startOffset }.let { if (!forwards) it.reversed() else it }
val nextIndex = sortedResults.indexOfFirst {
if (forwards) it.startOffset > initialOffset else it.startOffset < initialOffset
}
val toDrop = (nextIndex + count - 1).let { if (injector.globalOptions().wrapscan) it % results.size else it }
return sortedResults.drop(toDrop).firstOrNull()?.startOffset ?: -1
}
internal fun highlightSearchResults(editor: Editor, pattern: String, results: List<TextRange>, currentMatchOffset: Int) {
@@ -226,57 +233,46 @@ internal fun highlightSearchResults(editor: Editor, pattern: String, results: Li
}
private fun highlightMatch(editor: Editor, start: Int, end: Int, current: Boolean, tooltip: String): RangeHighlighter {
var attributes = editor.colorsScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES)
if (current) {
// This mimics what IntelliJ does with the Find live preview
attributes = attributes.clone()
attributes.effectType = EffectType.ROUNDED_BOX
attributes.effectColor = editor.colorsScheme.getColor(EditorColors.CARET_COLOR)
val layer = HighlighterLayer.SELECTION - 1
val targetArea = HighlighterTargetArea.EXACT_RANGE
if (!current) {
// If we use a text attribute key, it will update automatically when the editor's colour scheme changes
val highlighter =
editor.markupModel.addRangeHighlighter(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES, start, end, layer, targetArea)
highlighter.errorStripeTooltip = tooltip
return highlighter
}
if (attributes.errorStripeColor == null) {
attributes.errorStripeColor = getFallbackErrorStripeColor(attributes, editor.colorsScheme)
// There isn't a text attribute key for current selection. This means we won't update automatically when the editor's
// colour scheme changes. However, this is only used during incsearch, so it should be replaced pretty quickly. It's a
// small visual glitch that will fix itself quickly. Let's not bother implementing an editor colour scheme listener
// just for this.
// These are the same modifications that the Find live preview does. We could look at using LivePreviewPresentation,
// which might also be useful for text attributes in selection (if we supported that)
val attributes = editor.colorsScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES).clone().apply {
effectType = EffectType.ROUNDED_BOX
effectColor = editor.colorsScheme.getColor(EditorColors.CARET_COLOR)
}
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea).apply {
errorStripeTooltip = tooltip
}
val highlighter = editor.markupModel.addRangeHighlighter(
start,
end,
HighlighterLayer.SELECTION - 1,
attributes,
HighlighterTargetArea.EXACT_RANGE,
)
highlighter.errorStripeTooltip = tooltip
return highlighter
}
/**
* Return a valid error stripe colour based on editor background
*
*
* Based on HighlightManager#addRangeHighlight behaviour, which we can't use because it will hide highlights
* when hitting Escape.
*/
private fun getFallbackErrorStripeColor(attributes: TextAttributes, colorsScheme: EditorColorsScheme): Color? {
if (attributes.backgroundColor != null) {
val isDark = ColorUtil.isDark(colorsScheme.defaultBackground)
return if (isDark) attributes.backgroundColor.brighter() else attributes.backgroundColor.darker()
}
return null
}
/**
* Add search highlight for current match if hlsearch is false and we're performing incsearch highlights
* Add search highlight for current match if hlsearch is false, and we're performing incsearch highlights
*/
@Contract("_, true, _ -> false")
private fun shouldAddCurrentMatchSearchHighlight(pattern: String?, hlSearch: Boolean, initialOffset: Int): Boolean {
return !hlSearch && isIncrementalSearchHighlights(initialOffset) && pattern != null && pattern.isNotEmpty()
return !hlSearch && isIncrementalSearchHighlights(initialOffset) && !pattern.isNullOrEmpty()
}
/**
* Keep the current match offset if the pattern is still valid and we're performing incremental search highlights
* Keep the current match offset if the pattern is still valid, and we're performing incremental search highlights
* This will keep the caret position when editing the offset in e.g. `/foo/e+1`
*/
@Contract("null, _ -> false")
private fun shouldMaintainCurrentMatchOffset(pattern: String?, initialOffset: Int): Boolean {
return pattern != null && pattern.isNotEmpty() && isIncrementalSearchHighlights(initialOffset)
return !pattern.isNullOrEmpty() && isIncrementalSearchHighlights(initialOffset)
}
/**

View File

@@ -13,13 +13,14 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.util.registry.Registry
import com.maddyhome.idea.vim.VimPlugin
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.common.ChangesListener
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
@@ -40,24 +41,7 @@ internal class UndoRedoHelper : UndoRedoBase() {
val scrollingModel = editor.getScrollingModel()
scrollingModel.accumulateViewportChanges()
if (injector.globalIjOptions().oldundo) {
SelectionVimListenerSuppressor.lock().use { undoManager.undo(fileEditor) }
restoreVisualMode(editor)
} else {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking {
undoManager.undo(fileEditor)
// We execute undo one more time if the previous one just restored selection
if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
undoManager.undo(fileEditor)
}
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
}
}
performUndo(editor, undoManager, fileEditor)
scrollingModel.flushViewportChanges()
@@ -66,43 +50,79 @@ internal class UndoRedoHelper : UndoRedoBase() {
return false
}
private fun performUndo(
editor: VimEditor,
undoManager: UndoManager,
fileEditor: TextEditor,
) {
if (injector.globalIjOptions().oldundo) {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking {
undoManager.undo(fileEditor)
restoreVisualMode(editor)
}
} else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
undoManager.undo(fileEditor)
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
}
}
}
private fun hasSelection(editor: VimEditor): Boolean {
return editor.primaryCaret().ij.hasSelection()
}
override fun redo(editor: VimEditor, context: ExecutionContext): Boolean {
val ijContext = context.context as DataContext
val project = PlatformDataKeys.PROJECT.getData(ijContext) ?: return false
val fileEditor = TextEditorProvider.getInstance().getTextEditor(editor.ij)
val undoManager = UndoManager.getInstance(project)
if (undoManager.isRedoAvailable(fileEditor)) {
if (injector.globalIjOptions().oldundo) {
SelectionVimListenerSuppressor.lock().use { undoManager.redo(fileEditor) }
restoreVisualMode(editor)
} else {
undoManager.redo(fileEditor)
CommandProcessor.getInstance().runUndoTransparentAction {
editor.carets().forEach { it.ij.removeSelection() }
}
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking {
undoManager.redo(fileEditor)
performRedo(undoManager, fileEditor, editor)
// We execute undo one more time if the previous one just restored selection
if (!hasChanges && hasSelection(editor) && undoManager.isRedoAvailable(fileEditor)) {
undoManager.redo(fileEditor)
}
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
}
}
return true
}
return false
}
private fun performRedo(
undoManager: UndoManager,
fileEditor: TextEditor,
editor: VimEditor,
) {
if (injector.globalIjOptions().oldundo) {
undoManager.redo(fileEditor)
CommandProcessor.getInstance().runUndoTransparentAction {
editor.carets().forEach { it.ij.removeSelection() }
}
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking {
undoManager.redo(fileEditor)
// We execute undo one more time if the previous one just restored selection
if (!hasChanges && hasSelection(editor) && undoManager.isRedoAvailable(fileEditor)) {
undoManager.redo(fileEditor)
}
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
}
} else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
undoManager.redo(fileEditor)
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
}
}
}
private fun removeSelections(editor: VimEditor) {
editor.carets().forEach {
val ijCaret = it.ij
@@ -114,6 +134,17 @@ internal class UndoRedoHelper : UndoRedoBase() {
}
}
private fun runWithBooleanRegistryOption(option: String, value: Boolean, block: () -> Unit) {
val registry = Registry.get(option)
val oldValue = registry.asBoolean()
registry.setValue(value)
try {
block()
} finally {
registry.setValue(oldValue)
}
}
private fun VimEditor.runWithChangeTracking(block: ChangeTracker.() -> Unit) {
val tracker = ChangeTracker(this)
tracker.block()
@@ -140,7 +171,7 @@ internal class UndoRedoHelper : UndoRedoBase() {
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
@@ -149,7 +180,7 @@ internal class UndoRedoHelper : UndoRedoBase() {
SelectionType.CHARACTER_WISE
else
detectedMode
VimPlugin.getVisualMotion().enterVisualMode(editor, wantedMode)
}
}

View File

@@ -121,7 +121,6 @@ internal var Editor.vimIncsearchCurrentMatchOffset: Int? by userData()
internal var Editor.vimLastSelectionType: SelectionType? by userData()
internal var Editor.vimStateMachine: VimStateMachine? by userData()
internal var Editor.vimEditorGroup: Boolean by userDataOr { false }
internal var Editor.vimLineNumbersInitialState: Boolean by userDataOr { false }
internal var Editor.vimHasRelativeLineNumbersInstalled: Boolean by userDataOr { false }
internal var Editor.vimMorePanel: ExOutputPanel? by userData()
internal var Editor.vimExOutput: ExOutputModel? by userData()

View File

@@ -19,6 +19,7 @@ import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.find.FindModelListener
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.AnActionResult
@@ -181,7 +182,7 @@ internal object IdeaSpecifics {
if (editor.vim.inNormalMode) {
VimPlugin.getChange().insertBeforeCursor(
editor.vim,
injector.executionContextManager.onEditor(editor.vim),
injector.executionContextManager.getEditorExecutionContext(editor.vim),
)
KeyHandler.getInstance().reset(editor.vim)
}
@@ -231,5 +232,7 @@ internal class FindActionIdAction : DumbAwareToggleAction() {
override fun setSelected(e: AnActionEvent, state: Boolean) {
injector.globalIjOptions().trackactionids = !injector.globalIjOptions().trackactionids
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
}
//endregion

View File

@@ -35,6 +35,7 @@ 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.EditorImpl
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
@@ -45,11 +46,14 @@ import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
import com.intellij.openapi.fileEditor.impl.EditorComposite
import com.intellij.openapi.fileEditor.impl.EditorWindow
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.Key
import com.intellij.openapi.util.removeUserData
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.ExceptionUtil
import com.jetbrains.rd.util.lifetime.Lifetime
import com.maddyhome.idea.vim.EventFacade
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyHandlerStateResetter
@@ -66,6 +70,8 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.group.EditorGroup
import com.maddyhome.idea.vim.group.FileGroup
import com.maddyhome.idea.vim.group.IjOptions
import com.maddyhome.idea.vim.group.IjVimRedrawService
import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.group.OptionGroup
import com.maddyhome.idea.vim.group.ScrollGroup
@@ -101,7 +107,6 @@ import com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetListener
import com.maddyhome.idea.vim.ui.widgets.macro.macroWidgetOptionListener
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetListener
import com.maddyhome.idea.vim.ui.widgets.mode.modeWidgetOptionListener
import com.maddyhome.idea.vim.vimDisposable
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.SwingUtilities
@@ -177,8 +182,8 @@ internal object VimListenerManager {
}
val optionGroup = VimPlugin.getOptionGroup()
optionGroup.addEffectiveOptionValueChangeListener(Options.number, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addEffectiveOptionValueChangeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addEffectiveOptionValueChangeListener(IjOptions.number, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addEffectiveOptionValueChangeListener(IjOptions.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addEffectiveOptionValueChangeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener)
optionGroup.addEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
optionGroup.addGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
@@ -209,8 +214,8 @@ internal object VimListenerManager {
EventFacade.getInstance().restoreTypedActionHandler()
val optionGroup = VimPlugin.getOptionGroup()
optionGroup.removeEffectiveOptionValueChangeListener(Options.number, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeEffectiveOptionValueChangeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeEffectiveOptionValueChangeListener(IjOptions.number, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeEffectiveOptionValueChangeListener(IjOptions.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeEffectiveOptionValueChangeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener)
optionGroup.removeGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.removeGlobalOptionChangeListener(Options.showmode, modeWidgetOptionListener)
@@ -264,12 +269,10 @@ internal object VimListenerManager {
// TODO: If the user changes the 'ideavimsupport' option, existing editors won't be initialised
if (vimDisabled(editor)) return
// As I understand, there is no need to pass a disposable that also disposes on editor close
// because all editor resources will be garbage collected anyway on editor close
// Note that this uses the plugin's main disposable, rather than VimPlugin.onOffDisposable, because we don't need
// to - we explicitly call VimListenerManager.removeAll from VimPlugin.turnOffPlugin, and this disposes each
// editor's disposable individually.
val disposable = editor.project?.vimDisposable ?: return
val pluginLifetime = VimPlugin.getInstance().createLifetime()
val editorLifetime = (editor as EditorImpl).disposable.createLifetime()
val disposable =
Lifetime.intersect(pluginLifetime, editorLifetime).createNestedDisposable("MyLifetimedDisposable")
val listenersDisposable = Disposer.newDisposable(disposable)
editor.putUserData(editorListenersDisposableKey, listenersDisposable)
@@ -300,7 +303,7 @@ internal object VimListenerManager {
injector.listenersNotifier.notifyEditorCreated(vimEditor)
Disposer.register(listenersDisposable) {
VimPlugin.getEditor().editorDeinit(editor, true)
VimPlugin.getEditor().editorDeinit(editor)
}
}
@@ -341,11 +344,13 @@ internal object VimListenerManager {
override fun beforeDocumentChange(event: DocumentEvent) {
VimMarkServiceImpl.MarkUpdater.beforeDocumentChange(event)
SearchGroup.DocumentSearchListener.INSTANCE.beforeDocumentChange(event)
IjVimRedrawService.RedrawListener.beforeDocumentChange(event)
}
override fun documentChanged(event: DocumentEvent) {
VimMarkServiceImpl.MarkUpdater.documentChanged(event)
SearchGroup.DocumentSearchListener.INSTANCE.documentChanged(event)
IjVimRedrawService.RedrawListener.documentChanged(event)
}
}
@@ -372,6 +377,7 @@ internal object VimListenerManager {
FileGroup.fileEditorManagerSelectionChangedCallback(event)
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
OptionGroup.fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
}
}
@@ -538,15 +544,15 @@ internal object VimListenerManager {
// When starting on an empty line and dragging vertically upwards onto
// another line, the selection should include the entirety of the empty line
caret.setSelection(
ijVimEditor.coerceOffset(endOffset + 1).point,
ijVimEditor.coerceOffset(startOffset).point,
ijVimEditor.coerceOffset(endOffset + 1),
ijVimEditor.coerceOffset(startOffset),
)
} else if (lineEnd == startOffset + 1 && startOffset == endOffset) {
// When dragging left from EOL on a non-empty line, the selection
// should include the last character on the line
caret.setSelection(
ijVimEditor.coerceOffset(lineEnd).point,
ijVimEditor.coerceOffset(lineEnd - 1).point,
ijVimEditor.coerceOffset(lineEnd),
ijVimEditor.coerceOffset(lineEnd - 1),
)
}
}

View File

@@ -12,16 +12,8 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
internal open class IjEditorExecutionContext(override val context: DataContext) : ExecutionContext.Editor {
override fun updateEditor(editor: VimEditor): ExecutionContext {
return IjEditorExecutionContext(injector.executionContextManager.onEditor(editor, context.vim).ij)
}
}
internal class IjCaretAndEditorExecutionContext(override val context: DataContext) : IjEditorExecutionContext(context), ExecutionContext.CaretAndEditor
internal open class IjEditorExecutionContext(override val context: DataContext) : ExecutionContext
// This key is stored in data context when the action is started from vim
internal val runFromVimKey = Key.create<Boolean>("RunFromVim")

View File

@@ -9,23 +9,15 @@
package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ExecutionContextManagerBase
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.helper.EditorDataContext
@Service
internal class IjExecutionContextManager : ExecutionContextManagerBase() {
override fun onEditor(editor: VimEditor, prevContext: ExecutionContext?): ExecutionContext.Editor {
if (prevContext is ExecutionContext.CaretAndEditor) {
return prevContext
}
return IjEditorExecutionContext(EditorDataContext.init((editor as IjVimEditor).editor, prevContext?.ij))
}
override fun onCaret(caret: VimCaret, prevContext: ExecutionContext.Editor): ExecutionContext.CaretAndEditor {
return IjCaretAndEditorExecutionContext(CaretSpecificDataContext.create(prevContext.ij, caret.ij))
override fun getEditorExecutionContext(editor: VimEditor): ExecutionContext {
return EditorUtil.getEditorDataContext(editor.ij).vim
}
}

View File

@@ -10,12 +10,10 @@ package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.editor.RangeMarker
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.Offset
import com.maddyhome.idea.vim.common.offset
internal class IjLiveRange(val marker: RangeMarker) : LiveRange {
override val startOffset: Offset
get() = marker.startOffset.offset
override val startOffset: Int
get() = marker.startOffset
}
public val RangeMarker.vim: LiveRange

View File

@@ -34,4 +34,8 @@ internal class IjNativeActionManager : NativeActionManager {
public val AnAction.vim: IjNativeAction
get() = IjNativeAction(this)
public class IjNativeAction(override val action: AnAction) : NativeAction
public class IjNativeAction(override val action: AnAction) : NativeAction {
override fun toString(): String {
return "IjNativeAction(action=$action)"
}
}

View File

@@ -21,10 +21,7 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretBase
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.common.EditorLine
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.Offset
import com.maddyhome.idea.vim.common.offset
import com.maddyhome.idea.vim.group.visual.VisualChange
import com.maddyhome.idea.vim.helper.lastSelectionInfo
import com.maddyhome.idea.vim.helper.markStorage
@@ -78,8 +75,8 @@ internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
}
override val editor: VimEditor
get() = IjVimEditor(caret.editor)
override val offset: Offset
get() = caret.offset.offset
override val offset: Int
get() = caret.offset
override var vimLastColumn: Int
get() = caret.vimLastColumn
set(value) {
@@ -118,8 +115,8 @@ internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
this.caret.moveToLogicalPosition(LogicalPosition(position.line, position.column, position.leansForward))
}
override fun getLine(): EditorLine.Pointer {
return EditorLine.Pointer.init(caret.logicalPosition.line, editor)
override fun getLine(): Int {
return caret.logicalPosition.line
}
override fun hasSelection(): Boolean {
@@ -164,8 +161,8 @@ internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
return this
}
override fun setSelection(start: Offset, end: Offset) {
caret.setSelection(start.point, end.point)
override fun setSelection(start: Int, end: Int) {
caret.setSelection(start, end)
}
override fun removeSelection() {

View File

@@ -14,7 +14,6 @@ import com.intellij.openapi.editor.event.DocumentListener
import com.maddyhome.idea.vim.api.VimDocument
import com.maddyhome.idea.vim.common.ChangesListener
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.Offset
internal class IjVimDocument(val document: Document) : VimDocument {
@@ -41,7 +40,7 @@ internal class IjVimDocument(val document: Document) : VimDocument {
document.removeDocumentListener(nativeListener)
}
override fun getOffsetGuard(offset: Offset): LiveRange? {
return document.getOffsetGuard(offset.point)?.vim
override fun getOffsetGuard(offset: Int): LiveRange? {
return document.getOffsetGuard(offset)?.vim
}
}

View File

@@ -35,13 +35,11 @@ import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.VirtualFile
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.EditorLine
import com.maddyhome.idea.vim.common.IndentConfig
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.Offset
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.offset
import com.maddyhome.idea.vim.group.visual.vimSetSystemBlockSelectionSilently
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.StrictMode
@@ -90,18 +88,18 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
return editor.document.lineCount
}
override fun deleteRange(leftOffset: Offset, rightOffset: Offset) {
editor.document.deleteString(leftOffset.point, rightOffset.point)
override fun deleteRange(leftOffset: Int, rightOffset: Int) {
editor.document.deleteString(leftOffset, rightOffset)
}
override fun addLine(atPosition: EditorLine.Offset): EditorLine.Pointer {
val offset: Int = if (atPosition.line < lineCount()) {
override fun addLine(atPosition: Int): Int {
val offset: Int = if (atPosition < lineCount()) {
// The new line character is inserted before the new line char of the previous line. So it works line an enter
// on a line end. I believe that the correct implementation would be to insert the new line char after the
// \n of the previous line, however at the moment this won't update the mark on this line.
// https://youtrack.jetbrains.com/issue/IDEA-286587
val lineStart = (editor.document.getLineStartOffset(atPosition.line) - 1).coerceAtLeast(0)
val lineStart = (editor.document.getLineStartOffset(atPosition) - 1).coerceAtLeast(0)
val guard = editor.document.getOffsetGuard(lineStart)
if (guard != null && guard.endOffset == lineStart + 1) {
// Dancing around guarded blocks. It may happen that this concrete position is locked, but the next
@@ -116,11 +114,11 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
fileSize().toInt()
}
editor.document.insertString(offset, "\n")
return EditorLine.Pointer.init(atPosition.line, this)
return atPosition
}
override fun insertText(atPosition: Offset, text: CharSequence) {
editor.document.insertString(atPosition.point, text)
override fun insertText(atPosition: Int, text: CharSequence) {
editor.document.insertString(atPosition, text)
}
override fun replaceString(start: Int, end: Int, newString: String) {
@@ -128,13 +126,13 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
}
// TODO: 30.12.2021 Is end offset inclusive?
override fun getLineRange(line: EditorLine.Pointer): Pair<Offset, Offset> {
override fun getLineRange(line: Int): Pair<Int, Int> {
// TODO: 30.12.2021 getLineEndOffset returns the same value for "xyz" and "xyz\n"
return editor.document.getLineStartOffset(line.line).offset to editor.document.getLineEndOffset(line.line).offset
return editor.document.getLineStartOffset(line) to editor.document.getLineEndOffset(line)
}
override fun getLine(offset: Offset): EditorLine.Pointer {
return EditorLine.Pointer.init(editor.offsetToLogicalPosition(offset.point).line, this)
override fun getLine(offset: Int): Int {
return editor.offsetToLogicalPosition(offset).line
}
override fun carets(): List<VimCaret> {
@@ -203,15 +201,15 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
return editor.isOneLineMode
}
override fun getText(left: Offset, right: Offset): CharSequence {
return editor.document.charsSequence.subSequence(left.point, right.point)
override fun getText(left: Int, right: Int): CharSequence {
return editor.document.charsSequence.subSequence(left, right)
}
override fun search(
pair: Pair<Offset, Offset>,
pair: Pair<Int, Int>,
editor: VimEditor,
shiftType: LineDeleteShift,
): Pair<Pair<Offset, Offset>, LineDeleteShift>? {
): Pair<Pair<Int, Int>, LineDeleteShift>? {
val ijEditor = (editor as IjVimEditor).editor
return when (shiftType) {
LineDeleteShift.NO_NL -> if (pair.noGuard(ijEditor)) return pair to shiftType else null
@@ -358,10 +356,10 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
return EditorHelper.getVirtualFile(editor)?.getUrl()?.let { VirtualFileManager.extractProtocol(it) }
}
override val projectId = editor.project?.basePath ?: DEFAULT_PROJECT_ID
override val projectId = editor.project?.let { injector.file.getProjectId(it) } ?: DEFAULT_PROJECT_ID
override fun visualPositionToOffset(position: VimVisualPosition): Offset {
return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)).offset
override fun visualPositionToOffset(position: VimVisualPosition): Int {
return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight))
}
override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) {
@@ -417,8 +415,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
return visualPosition.run { VimVisualPosition(line, column, leansRight) }
}
override fun createLiveMarker(start: Offset, end: Offset): LiveRange {
return editor.document.createRangeMarker(start.point, end.point).vim
override fun createLiveMarker(start: Int, end: Int): LiveRange {
return editor.document.createRangeMarker(start, end).vim
}
/**
@@ -456,10 +454,10 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
ijFoldRegion.isExpanded = value
}
}
override val startOffset: Offset
get() = Offset(ijFoldRegion.startOffset)
override val endOffset: Offset
get() = Offset(ijFoldRegion.endOffset)
override val startOffset: Int
get() = ijFoldRegion.startOffset
override val endOffset: Int
get() = ijFoldRegion.endOffset
}
}
@@ -468,17 +466,17 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
return caret
}
private fun Pair<Offset, Offset>.noGuard(editor: Editor): Boolean {
return editor.document.getRangeGuard(this.first.point, this.second.point) == null
private fun Pair<Int, Int>.noGuard(editor: Editor): Boolean {
return editor.document.getRangeGuard(this.first, this.second) == null
}
private inline fun Pair<Offset, Offset>.shift(
private inline fun Pair<Int, Int>.shift(
shiftStart: Int = 0,
shiftEnd: Int = 0,
action: Pair<Offset, Offset>.() -> Unit,
action: Pair<Int, Int>.() -> Unit,
) {
val data =
(this.first.point + shiftStart).coerceAtLeast(0).offset to (this.second.point + shiftEnd).coerceAtLeast(0).offset
(this.first + shiftStart).coerceAtLeast(0) to (this.second + shiftEnd).coerceAtLeast(0)
data.action()
}
@@ -500,3 +498,6 @@ public val Editor.vim: VimEditor
get() = IjVimEditor(this)
public val VimEditor.ij: Editor
get() = (this as IjVimEditor).editor
public val com.intellij.openapi.util.TextRange.vim: TextRange
get() = TextRange(this.startOffset, this.endOffset)

View File

@@ -24,6 +24,7 @@ import com.maddyhome.idea.vim.api.VimApplication
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimClipboardManager
import com.maddyhome.idea.vim.api.VimCommandGroup
import com.maddyhome.idea.vim.api.VimCommandLineService
import com.maddyhome.idea.vim.api.VimDigraphGroup
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimEditorGroup
@@ -42,6 +43,8 @@ import com.maddyhome.idea.vim.api.VimMessages
import com.maddyhome.idea.vim.api.VimMotionGroup
import com.maddyhome.idea.vim.api.VimOptionGroup
import com.maddyhome.idea.vim.api.VimProcessGroup
import com.maddyhome.idea.vim.api.VimPsiService
import com.maddyhome.idea.vim.api.VimRedrawService
import com.maddyhome.idea.vim.api.VimRegexpService
import com.maddyhome.idea.vim.api.VimScrollGroup
import com.maddyhome.idea.vim.api.VimSearchGroup
@@ -65,6 +68,7 @@ import com.maddyhome.idea.vim.group.FileGroup
import com.maddyhome.idea.vim.group.GlobalIjOptions
import com.maddyhome.idea.vim.group.HistoryGroup
import com.maddyhome.idea.vim.group.IjVimOptionGroup
import com.maddyhome.idea.vim.group.IjVimPsiService
import com.maddyhome.idea.vim.group.MacroGroup
import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.group.SearchGroup
@@ -72,12 +76,10 @@ import com.maddyhome.idea.vim.group.TabService
import com.maddyhome.idea.vim.group.VimWindowGroup
import com.maddyhome.idea.vim.group.WindowGroup
import com.maddyhome.idea.vim.group.copy.PutGroup
import com.maddyhome.idea.vim.helper.CommandLineHelper
import com.maddyhome.idea.vim.helper.IjActionExecutor
import com.maddyhome.idea.vim.helper.IjEditorHelper
import com.maddyhome.idea.vim.helper.IjVimStringParser
import com.maddyhome.idea.vim.helper.UndoRedoHelper
import com.maddyhome.idea.vim.helper.VimCommandLineHelper
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.history.VimHistory
import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl
@@ -147,8 +149,8 @@ internal class IjVimInjector : VimInjectorBase() {
get() = service<MacroGroup>()
override val undo: VimUndoRedo
get() = service<UndoRedoHelper>()
override val commandLineHelper: VimCommandLineHelper
get() = service<CommandLineHelper>()
override val psiService: VimPsiService
get() = service<IjVimPsiService>()
override val nativeActionManager: NativeActionManager
get() = service<IjNativeActionManager>()
override val messages: VimMessages
@@ -193,6 +195,8 @@ internal class IjVimInjector : VimInjectorBase() {
get() = service<Executor>()
override val vimscriptParser: VimscriptParser
get() = com.maddyhome.idea.vim.vimscript.parser.VimscriptParser
override val commandLine: VimCommandLineService
get() = service()
override val optionGroup: VimOptionGroup
get() = service()
@@ -203,6 +207,8 @@ internal class IjVimInjector : VimInjectorBase() {
get() = service()
override val vimStorageService: VimStorageService
get() = service()
override val redrawService: VimRedrawService
get() = service()
override fun commandStateFor(editor: VimEditor): VimStateMachine {
var res = editor.ij.vimStateMachine

View File

@@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.api.VimEditor
@@ -26,27 +27,59 @@ internal class IjVimMessages : VimMessagesBase() {
private var message: String? = null
private var error = false
private var lastBeepTimeMillis = 0L
private var allowClearStatusBarMessage = true
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
if (ApplicationManager.getApplication().isUnitTestMode) {
this.message = message
}
val pm = ProjectManager.getInstance()
val projects = pm.openProjects
for (project in projects) {
val bar = WindowManager.getInstance().getStatusBar(project)
if (bar != null) {
if (message.isNullOrEmpty()) {
bar.info = ""
} else {
bar.info = "VIM - $message"
}
fun setStatusBarMessage(project: Project, message: String?) {
WindowManager.getInstance().getStatusBar(project)?.let {
it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
}
}
this.message = message
val project = editor?.ij?.project
if (project != null) {
setStatusBarMessage(project, message)
}
else {
// TODO: We really shouldn't set the status bar text for other projects. That's rude.
ProjectManager.getInstance().openProjects.forEach {
setStatusBarMessage(it, message)
}
}
// Redraw happens automatically based on changes or scrolling. If we've just set the message (e.g., searching for a
// string, hitting the bottom and scrolling to the top), make sure we don't immediately clear it when scrolling.
allowClearStatusBarMessage = false
ApplicationManager.getApplication().invokeLater {
allowClearStatusBarMessage = true
}
}
override fun getStatusBarMessage(): String? = message
// Vim doesn't appear to have a policy about clearing the status bar, other than on "redraw". This can be forced with
// <C-L> or the `:redraw` command, but also happens as the screen changes, e.g., when inserting or deleting lines,
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
// things happen.
override fun clearStatusBarMessage() {
if (message.isNullOrEmpty()) return
// Don't clear the status bar message if we've only just set it
if (!allowClearStatusBarMessage) return
ProjectManager.getInstance().openProjects.forEach { project ->
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
// Only clear the status bar if it's showing our last message
if (statusBar.info?.contains(message.toString()) == true) {
statusBar.info = ""
}
}
}
message = null
}
override fun indicateError() {
error = true
if (!ApplicationManager.getApplication().isUnitTestMode) {
@@ -68,6 +101,7 @@ internal class IjVimMessages : VimMessagesBase() {
override fun isError(): Boolean = error
override fun message(key: String, vararg params: Any): String = MessageHelper.message(key, *params)
override fun updateStatusBar(editor: VimEditor) {
ShowCmd.update()
}

View File

@@ -9,8 +9,11 @@
package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Ref
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.Options
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
@@ -26,23 +29,17 @@ import com.maddyhome.idea.vim.helper.shouldIgnoreCase
import com.maddyhome.idea.vim.helper.updateSearchHighlights
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import com.maddyhome.idea.vim.ui.ModalEntry
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser.parseExpression
import org.jetbrains.annotations.TestOnly
import javax.swing.KeyStroke
public abstract class IjVimSearchGroup : VimSearchGroupBase() {
init {
// TODO: Investigate migrating these listeners to use the effective value change listener
// This would allow us to update the editor we're told to update, rather than looping over all projects and updating
// the highlights in that project's current document's open editors (see VIM-2779).
// However, we probably only want to update the editors associated with the current document, so maybe the whole
// code needs to be reworked. We're currently using the same update code for changes in the search term as well as
// changes in the search options.
// We use the global option listener instead of the effective listener that gets called for each affected editor
// because we handle updating the affected editors ourselves (e.g., we can filter for visible windows).
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.hlsearch) {
resetSearchHighlight()
setShouldShowSearchHighlights()
updateSearchHighlights(true)
}
@@ -82,6 +79,7 @@ public abstract class IjVimSearchGroup : VimSearchGroupBase() {
override fun confirmChoice(
editor: VimEditor,
context: ExecutionContext,
match: String,
caret: VimCaret,
startOffset: Int,
@@ -121,7 +119,6 @@ public abstract class IjVimSearchGroup : VimSearchGroupBase() {
// XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for this method
val exEntryPanel: com.maddyhome.idea.vim.ui.ex.ExEntryPanel =
com.maddyhome.idea.vim.ui.ex.ExEntryPanel.getInstanceWithoutShortcuts()
val context = injector.executionContextManager.onEditor(editor, null)
exEntryPanel.activate(
editor.ij,
(context as IjEditorExecutionContext).context,
@@ -136,17 +133,19 @@ public abstract class IjVimSearchGroup : VimSearchGroupBase() {
return result.get()
}
override fun parseVimScriptExpression(expressionString: String): Expression? {
return parseExpression(expressionString)
}
override fun addSubstitutionConfirmationHighlight(
editor: VimEditor,
startOffset: Int,
endOffset: Int,
): SearchHighlight {
override fun addSubstitutionConfirmationHighlight(editor: VimEditor, startOffset: Int, endOffset: Int) {
val hl = addSubstitutionConfirmationHighlight(
(editor as IjVimEditor).editor,
val ijEditor = (editor as IjVimEditor).editor
val highlighter = addSubstitutionConfirmationHighlight(
ijEditor,
startOffset,
endOffset
)
editor.editor.markupModel.removeHighlighter(hl)
return IjSearchHighlight(ijEditor, highlighter)
}
override fun setLatestMatch(match: String) {
@@ -170,7 +169,7 @@ public abstract class IjVimSearchGroup : VimSearchGroupBase() {
showSearchHighlight = injector.globalOptions().hlsearch
}
override fun resetSearchHighlight() {
override fun setShouldShowSearchHighlights() {
showSearchHighlight = injector.globalOptions().hlsearch
}
@@ -178,4 +177,12 @@ public abstract class IjVimSearchGroup : VimSearchGroupBase() {
showSearchHighlight = false
updateSearchHighlights(false)
}
private class IjSearchHighlight(private val editor: Editor, private val highlighter: RangeHighlighter) :
SearchHighlight() {
override fun remove() {
editor.markupModel.removeHighlighter(highlighter)
}
}
}

View File

@@ -13,144 +13,31 @@ import com.intellij.openapi.diagnostic.Logger
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimSearchHelperBase
import com.maddyhome.idea.vim.api.anyNonWhitespace
import com.maddyhome.idea.vim.api.getLineEndOffset
import com.maddyhome.idea.vim.api.getLineStartForOffset
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.CharacterHelper
import com.maddyhome.idea.vim.helper.CharacterHelper.charType
import com.maddyhome.idea.vim.helper.PsiHelper
import com.maddyhome.idea.vim.helper.SearchHelper
import com.maddyhome.idea.vim.helper.SearchOptions
import com.maddyhome.idea.vim.helper.checkInString
import com.maddyhome.idea.vim.helper.fileSize
import com.maddyhome.idea.vim.state.VimStateMachine.Companion.getInstance
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
import it.unimi.dsi.fastutil.ints.IntComparator
import it.unimi.dsi.fastutil.ints.IntComparators
import java.util.*
import java.util.function.Function
import java.util.regex.Pattern
import kotlin.math.abs
import kotlin.math.max
@Service
internal class IjVimSearchHelper : VimSearchHelperBase() {
companion object {
private const val BLOCK_CHARS = "{}()[]<>"
private val logger = Logger.getInstance(IjVimSearchHelper::class.java.name)
}
override fun findSection(
editor: VimEditor,
caret: ImmutableVimCaret,
type: Char,
direction: Int,
count: Int,
)
: Int {
val documentText: CharSequence = editor.ij.document.charsSequence
var currentLine: Int = caret.ij.logicalPosition.line + direction
var resultOffset = -1
var remainingTargets = count
while (currentLine in 1 until editor.lineCount() && remainingTargets > 0) {
val lineStartOffset = editor.getLineStartOffset(currentLine)
if (lineStartOffset < documentText.length) {
val currentChar = documentText[lineStartOffset]
if (currentChar == type || currentChar == '\u000C') {
resultOffset = lineStartOffset
remainingTargets--
}
}
currentLine += direction
}
if (resultOffset == -1) {
resultOffset = if (direction < 0) 0 else documentText.length - 1
}
return resultOffset
}
override fun findMethodEnd(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int {
// TODO add it to PsiService
return PsiHelper.findMethodEnd(editor.ij, caret.ij.offset, count)
}
override fun findMethodStart(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int {
// TODO add it to PsiService
return PsiHelper.findMethodStart(editor.ij, caret.ij.offset, count)
}
override fun findUnmatchedBlock(editor: VimEditor, caret: ImmutableVimCaret, type: Char, count: Int): Int {
val chars: CharSequence = editor.ij.document.charsSequence
var pos: Int = caret.ij.offset
val loc = BLOCK_CHARS.indexOf(type)
// What direction should we go now (-1 is backward, 1 is forward)
val dir = if (loc % 2 == 0) Direction.BACKWARDS else Direction.FORWARDS
// Which character did we find and which should we now search for
val match = BLOCK_CHARS[loc]
val found = BLOCK_CHARS[loc - dir.toInt()]
if (pos < chars.length && chars[pos] == type) {
pos += dir.toInt()
}
return findBlockLocation(chars, found, match, dir, pos, count)
}
private fun findBlockLocation(
chars: CharSequence,
found: Char,
match: Char,
dir: Direction,
pos: Int,
cnt: Int,
): Int {
var position = pos
var count = cnt
var res = -1
val initialPos = position
val initialInString = checkInString(chars, position, true)
val inCheckPosF =
Function { x: Int -> if (dir === Direction.BACKWARDS && x > 0) x - 1 else x + 1 }
val inCheckPos = inCheckPosF.apply(position)
var inString = checkInString(chars, inCheckPos, true)
var inChar = checkInString(chars, inCheckPos, false)
var stack = 0
// Search to start or end of file, as appropriate
val charsToSearch: Set<Char> = HashSet(listOf('\'', '"', '\n', match, found))
while (position >= 0 && position < chars.length && count > 0) {
val (c, second) = SearchHelper.findPositionOfFirstCharacter(chars, position, charsToSearch, true, dir) ?: return -1
position = second
// If we found a match and we're not in a string...
if (c == match && (!inString) && !inChar) {
// We found our match
if (stack == 0) {
res = position
count--
} else {
stack--
}
} else if (c == '\n') {
inString = false
inChar = false
} else if (position != initialPos) {
// We found another character like our original - belongs to another pair
if (!inString && !inChar && c == found) {
stack++
} else if (!inChar) {
inString = checkInString(chars, inCheckPosF.apply(position), true)
} else if (!inString) {
inChar = checkInString(chars, inCheckPosF.apply(position), false)
}
}
position += dir.toInt()
}
return res
}
override fun findPattern(
editor: VimEditor,
pattern: String?,
@@ -173,525 +60,6 @@ internal class IjVimSearchHelper : VimSearchHelperBase() {
else SearchHelper.findAll(editor.ij, pattern, startLine, endLine, ignoreCase)
}
override fun findNextCharacterOnLine(editor: VimEditor, caret: ImmutableVimCaret, count: Int, ch: Char): Int {
val line: Int = caret.ij.logicalPosition.line
val start = editor.getLineStartOffset(line)
val end = editor.getLineEndOffset(line, true)
val chars: CharSequence = editor.ij.document.charsSequence
var found = 0
val step = if (count >= 0) 1 else -1
var pos: Int = caret.ij.offset + step
while (pos in start until end && pos < chars.length) {
if (chars[pos] == ch) {
found++
if (found == abs(count)) {
break
}
}
pos += step
}
return if (found == abs(count)) {
pos
} else {
-1
}
}
override fun findWordUnderCursor(
editor: VimEditor,
caret: ImmutableVimCaret,
count: Int,
dir: Int,
isOuter: Boolean,
isBig: Boolean,
hasSelection: Boolean,
): TextRange {
if (logger.isDebugEnabled) {
logger.debug("count=$count")
logger.debug("dir=$dir")
logger.debug("isOuter=$isOuter")
logger.debug("isBig=$isBig")
logger.debug("hasSelection=$hasSelection")
}
val chars: CharSequence = editor.ij.document.charsSequence
//int min = EditorHelper.getLineStartOffset(editor, EditorHelper.getCurrentLogicalLine(editor));
//int max = EditorHelper.getLineEndOffset(editor, EditorHelper.getCurrentLogicalLine(editor), true);
val min = 0
val max: Int = editor.ij.fileSize
if (max == 0) return TextRange(0, 0)
if (logger.isDebugEnabled) {
logger.debug("min=$min")
logger.debug("max=$max")
}
val pos: Int = caret.ij.offset
if (chars.length <= pos) return TextRange(chars.length - 1, chars.length - 1)
val startSpace = charType(editor, chars[pos], isBig) === CharacterHelper.CharacterType.WHITESPACE
// Find word start
val onWordStart = pos == min ||
charType(editor, chars[pos - 1], isBig) !==
charType(editor, chars[pos], isBig)
var start = pos
if (logger.isDebugEnabled) {
logger.debug("pos=$pos")
logger.debug("onWordStart=$onWordStart")
}
if (!onWordStart && !(startSpace && isOuter) || hasSelection || count > 1 && dir == -1) {
start = if (dir == 1) {
findNextWord(editor, pos, -1, isBig, !isOuter)
} else {
findNextWord(
editor,
pos,
-(count - if (onWordStart && !hasSelection) 1 else 0),
isBig,
!isOuter
)
}
start = editor.normalizeOffset(start, false)
}
if (logger.isDebugEnabled) logger.debug("start=$start")
// Find word end
// Find word end
val onWordEnd = pos >= max - 1 ||
charType(editor, chars[pos + 1], isBig) !==
charType(editor, chars[pos], isBig)
if (logger.isDebugEnabled) logger.debug("onWordEnd=$onWordEnd")
var end = pos
if (!onWordEnd || hasSelection || count > 1 && dir == 1 || startSpace && isOuter) {
end = if (dir == 1) {
val c = count - if (onWordEnd && !hasSelection && (!(startSpace && isOuter) || startSpace && !isOuter)) 1 else 0
findNextWordEnd(editor, pos, c, isBig, !isOuter)
} else {
findNextWordEnd(editor, pos, 1, isBig, !isOuter)
}
}
if (logger.isDebugEnabled) logger.debug("end=$end")
var goBack = startSpace && !hasSelection || !startSpace && hasSelection && !onWordStart
if (dir == 1 && isOuter) {
var firstEnd = end
if (count > 1) {
firstEnd = findNextWordEnd(editor, pos, 1, isBig, false)
}
if (firstEnd < max - 1) {
if (charType(editor, chars[firstEnd + 1], false) !== CharacterHelper.CharacterType.WHITESPACE) {
goBack = true
}
}
}
if (dir == -1 && isOuter && startSpace) {
if (pos > min) {
if (charType(editor, chars[pos - 1], false) !== CharacterHelper.CharacterType.WHITESPACE) {
goBack = true
}
}
}
var goForward = dir == 1 && isOuter && (!startSpace && !onWordEnd || startSpace && onWordEnd && hasSelection)
if (!goForward && dir == 1 && isOuter) {
var firstEnd = end
if (count > 1) {
firstEnd = findNextWordEnd(editor, pos, 1, isBig, false)
}
if (firstEnd < max - 1) {
if (charType(editor, chars[firstEnd + 1], false) !== CharacterHelper.CharacterType.WHITESPACE) {
goForward = true
}
}
}
if (!goForward && dir == 1 && isOuter && !startSpace && !hasSelection) {
if (end < max - 1) {
if (charType(editor, chars[end + 1], !isBig) !==
charType(editor, chars[end], !isBig)
) {
goForward = true
}
}
}
if (logger.isDebugEnabled) {
logger.debug("goBack=$goBack")
logger.debug("goForward=$goForward")
}
if (goForward) {
if (editor.anyNonWhitespace(end, 1)) {
while (end + 1 < max &&
charType(editor, chars[end + 1], false) === CharacterHelper.CharacterType.WHITESPACE
) {
end++
}
}
}
if (goBack) {
if (editor.anyNonWhitespace(start, -1)) {
while (start > min &&
charType(editor, chars[start - 1], false) === CharacterHelper.CharacterType.WHITESPACE
) {
start--
}
}
}
if (logger.isDebugEnabled) {
logger.debug("start=$start")
logger.debug("end=$end")
}
// End offset is exclusive
return TextRange(start, end + 1)
}
override fun findBlockTagRange(editor: VimEditor, caret: ImmutableVimCaret, count: Int, isOuter: Boolean): TextRange? {
var counter = count
var isOuterVariable = isOuter
val position: Int = caret.ij.offset
val sequence: CharSequence = editor.ij.document.charsSequence
val selectionStart: Int = caret.ij.selectionStart
val selectionEnd: Int = caret.ij.selectionEnd
val isRangeSelection = selectionEnd - selectionStart > 1
var searchStartPosition: Int
searchStartPosition = if (!isRangeSelection) {
val line: Int = caret.ij.logicalPosition.line
val lineBegin: Int = editor.ij.document.getLineStartOffset(line)
ignoreWhitespaceAtLineStart(sequence, lineBegin, position)
} else {
selectionEnd
}
if (isInHTMLTag(sequence, searchStartPosition, false)) {
// caret is inside opening tag. Move to closing '>'.
while (searchStartPosition < sequence.length && sequence[searchStartPosition] != '>') {
searchStartPosition++
}
} else if (isInHTMLTag(sequence, searchStartPosition, true)) {
// caret is inside closing tag. Move to starting '<'.
while (searchStartPosition > 0 && sequence[searchStartPosition] != '<') {
searchStartPosition--
}
}
while (true) {
val (closingTagTextRange, tagName) = findUnmatchedClosingTag(sequence, searchStartPosition, counter)
?: return null
val openingTag = findUnmatchedOpeningTag(sequence, closingTagTextRange.startOffset, tagName)
?: return null
if (isRangeSelection && openingTag.endOffset - 1 >= selectionStart) {
// If there was already some text selected and the new selection would not extend further, we try again
searchStartPosition = closingTagTextRange.endOffset
counter = 1
continue
}
var selectionEndWithoutNewline = selectionEnd
while (selectionEndWithoutNewline < sequence.length && sequence[selectionEndWithoutNewline] == '\n') {
selectionEndWithoutNewline++
}
val mode = getInstance(editor).mode
if (mode is VISUAL) {
if (closingTagTextRange.startOffset == selectionEndWithoutNewline &&
openingTag.endOffset == selectionStart
) {
// Special case: if the inner tag is already selected we should like isOuter is active
// Note that we need to ignore newlines, because their selection is lost between multiple "it" invocations
isOuterVariable = true
} else if (openingTag.endOffset == closingTagTextRange.startOffset &&
selectionStart == openingTag.endOffset
) {
// Special case: for an empty tag pair (e.g. <a></a>) the whole tag is selected if the caret is in the middle.
isOuterVariable = true
}
}
return if (isOuterVariable) {
TextRange(openingTag.startOffset, closingTagTextRange.endOffset)
} else {
TextRange(openingTag.endOffset, closingTagTextRange.startOffset)
}
}
}
/**
* returns new position which ignore whitespaces at beginning of the line
*/
private fun ignoreWhitespaceAtLineStart(seq: CharSequence, lineStart: Int, pos: Int): Int {
var position = pos
if (seq.subSequence(lineStart, position).chars().allMatch { codePoint: Int ->
Character.isWhitespace(
codePoint
)
}) {
while (position < seq.length && seq[position] != '\n' && Character.isWhitespace(seq[position])) {
position++
}
}
return position
}
/**
* Returns true if there is a html at the given position. Ignores tags with a trailing slash like <aaa></aaa>.
*/
private fun isInHTMLTag(sequence: CharSequence, position: Int, isEndtag: Boolean): Boolean {
var openingBracket = -1
run {
var i = position
while (i >= 0 && i < sequence.length) {
if (sequence[i] == '<') {
openingBracket = i
break
}
if (sequence[i] == '>' && i != position) {
return false
}
i--
}
}
if (openingBracket == -1) {
return false
}
val hasSlashAfterOpening = openingBracket + 1 < sequence.length && sequence[openingBracket + 1] == '/'
if (isEndtag && !hasSlashAfterOpening || !isEndtag && hasSlashAfterOpening) {
return false
}
var closingBracket = -1
for (i in openingBracket until sequence.length) {
if (sequence[i] == '>') {
closingBracket = i
break
}
}
return closingBracket != -1 && sequence[closingBracket - 1] != '/'
}
private fun findUnmatchedOpeningTag(
sequence: CharSequence,
position: Int,
tagName: String,
): TextRange? {
val quotedTagName = Pattern.quote(tagName)
val patternString = ("(</%s>)" // match closing tags
+
"|(<%s" // or opening tags starting with tagName
+
"(\\s([^>]*" // After at least one whitespace there might be additional text in the tag. E.g. <html lang="en">
+
"[^/])?)?>)") // Slash is not allowed as last character (this would be a self closing tag).
val tagPattern =
Pattern.compile(String.format(patternString, quotedTagName, quotedTagName), Pattern.CASE_INSENSITIVE)
val matcher = tagPattern.matcher(sequence.subSequence(0, position + 1))
val openTags: Deque<TextRange> = ArrayDeque()
while (matcher.find()) {
val match = TextRange(matcher.start(), matcher.end())
if (sequence[matcher.start() + 1] == '/') {
if (!openTags.isEmpty()) {
openTags.pop()
}
} else {
openTags.push(match)
}
}
return if (openTags.isEmpty()) {
null
} else {
openTags.pop()
}
}
private fun findUnmatchedClosingTag(
sequence: CharSequence,
position: Int,
count: Int,
): Pair<TextRange, String>? {
// The tag name may contain any characters except slashes, whitespace and '>'
var counter = count
val tagNamePattern = "([^/\\s>]+)"
// An opening tag consists of '<' followed by a tag name, optionally some additional text after whitespace and a '>'
val openingTagPattern = String.format("<%s(?:\\s[^>]*)?>", tagNamePattern)
val closingTagPattern = String.format("</%s>", tagNamePattern)
val tagPattern = Pattern.compile(String.format("(?:%s)|(?:%s)", openingTagPattern, closingTagPattern))
val matcher = tagPattern.matcher(sequence.subSequence(position, sequence.length))
val openTags: Deque<String> = ArrayDeque()
while (matcher.find()) {
val isClosingTag = matcher.group(1) == null
if (isClosingTag) {
val tagName = matcher.group(2)
// Ignore unmatched open tags. Either the file is malformed or it might be a tag like <br> that does not need to be closed.
while (!openTags.isEmpty() && !openTags.peek().equals(tagName, ignoreCase = true)) {
openTags.pop()
}
if (openTags.isEmpty()) {
if (counter <= 1) {
return Pair(TextRange(position + matcher.start(), position + matcher.end()), tagName)
} else {
counter--
}
} else {
openTags.pop()
}
} else {
val tagName = matcher.group(1)
openTags.push(tagName)
}
}
return null
}
override fun findBlockRange(
editor: VimEditor,
caret: ImmutableVimCaret,
type: Char,
count: Int,
isOuter: Boolean,
): TextRange? {
val chars: CharSequence = editor.ij.document.charsSequence
var pos: Int = caret.ij.offset
var start: Int = caret.ij.selectionStart
var end: Int = caret.ij.selectionEnd
val loc = BLOCK_CHARS.indexOf(type)
val close = BLOCK_CHARS[loc + 1]
// extend the range for blank line after type and before close, as they are excluded when inner match
if (!isOuter) {
if (start > 1 && chars[start - 2] == type && chars[start - 1] == '\n') {
start--
}
if (end < chars.length && chars[end] == '\n') {
var isSingleLineAllWhiteSpaceUntilClose = false
var countWhiteSpaceCharacter = 1
while (end + countWhiteSpaceCharacter < chars.length) {
if (Character.isWhitespace(chars[end + countWhiteSpaceCharacter]) &&
chars[end + countWhiteSpaceCharacter] != '\n'
) {
countWhiteSpaceCharacter++
continue
}
if (chars[end + countWhiteSpaceCharacter] == close) {
isSingleLineAllWhiteSpaceUntilClose = true
}
break
}
if (isSingleLineAllWhiteSpaceUntilClose) {
end += countWhiteSpaceCharacter
}
}
}
var rangeSelection = end - start > 1
if (rangeSelection && start == 0) // early return not only for optimization
{
return null // but also not to break the interval semantic on this edge case (see below)
}
/* In case of successive inner selection. We want to break out of
* the block delimiter of the current inner selection.
* In other terms, for the rest of the algorithm, a previous inner selection of a block
* if equivalent to an outer one. */
/* In case of successive inner selection. We want to break out of
* the block delimiter of the current inner selection.
* In other terms, for the rest of the algorithm, a previous inner selection of a block
* if equivalent to an outer one. */if (!isOuter && start - 1 >= 0 && type == chars[start - 1] && end < chars.length && close == chars[end]) {
start -= 1
pos = start
rangeSelection = true
}
/* when one char is selected, we want to find the enclosing block of (start,end]
* although when a range of characters is selected, we want the enclosing block of [start, end]
* shifting the position allow to express which kind of interval we work on */
/* when one char is selected, we want to find the enclosing block of (start,end]
* although when a range of characters is selected, we want the enclosing block of [start, end]
* shifting the position allow to express which kind of interval we work on */if (rangeSelection) pos =
max(0.0, (start - 1).toDouble()).toInt()
val initialPosIsInString = checkInString(chars, pos, true)
var bstart = -1
var bend = -1
var startPosInStringFound = false
if (initialPosIsInString) {
val quoteRange = injector.searchHelper
.findBlockQuoteInLineRange(editor, caret, '"', false)
if (quoteRange != null) {
val startOffset = quoteRange.startOffset
val endOffset = quoteRange.endOffset
val subSequence = chars.subSequence(startOffset, endOffset)
val inQuotePos = pos - startOffset
var inQuoteStart =
findBlockLocation(subSequence, close, type, Direction.BACKWARDS, inQuotePos, count)
if (inQuoteStart == -1) {
inQuoteStart =
findBlockLocation(subSequence, close, type, Direction.FORWARDS, inQuotePos, count)
}
if (inQuoteStart != -1) {
startPosInStringFound = true
val inQuoteEnd =
findBlockLocation(subSequence, type, close, Direction.FORWARDS, inQuoteStart, 1)
if (inQuoteEnd != -1) {
bstart = inQuoteStart + startOffset
bend = inQuoteEnd + startOffset
}
}
}
}
if (!startPosInStringFound) {
bstart = findBlockLocation(chars, close, type, Direction.BACKWARDS, pos, count)
if (bstart == -1) {
bstart = findBlockLocation(chars, close, type, Direction.FORWARDS, pos, count)
}
if (bstart != -1) {
bend = findBlockLocation(chars, type, close, Direction.FORWARDS, bstart, 1)
}
}
if (bstart == -1 || bend == -1) {
return null
}
if (!isOuter) {
bstart++
// exclude first line break after start for inner match
if (chars[bstart] == '\n') {
bstart++
}
val o = editor.getLineStartForOffset(bend)
var allWhite = true
for (i in o until bend) {
if (!Character.isWhitespace(chars[i])) {
allWhite = false
break
}
}
if (allWhite) {
bend = o - 2
} else {
bend--
}
}
// End offset exclusive
return TextRange(bstart, bend + 1)
}
override fun findMisspelledWord(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int {
val startOffset: Int
val endOffset: Int
@@ -700,17 +68,18 @@ internal class IjVimSearchHelper : VimSearchHelperBase() {
if (count < 0) {
startOffset = 0
endOffset = caret.offset.point - 1
endOffset = caret.offset - 1
skipCount = -count - 1
offsetOrdering = IntComparators.OPPOSITE_COMPARATOR
}
else {
startOffset = caret.offset.point + 1
startOffset = caret.offset + 1
endOffset = editor.ij.document.textLength
skipCount = count - 1
offsetOrdering = IntComparators.NATURAL_COMPARATOR
}
// TODO add it to PsiService
return SearchHelper.findMisspelledWords(editor.ij, startOffset, endOffset, skipCount, offsetOrdering)
}
}

View File

@@ -136,7 +136,7 @@
* |-| {@link com.maddyhome.idea.vim.action.motion.updown.MotionUpFirstNonSpaceAction}
* |.| {@link com.maddyhome.idea.vim.action.change.RepeatChangeAction}
* |/| {@link com.maddyhome.idea.vim.action.motion.search.SearchEntryFwdAction}
* |:| {@link com.maddyhome.idea.vim.action.ExEntryAction}
* |:| {@link com.maddyhome.idea.vim.action.ex.ExEntryAction}
* |;| {@link com.maddyhome.idea.vim.action.motion.leftright.MotionLastMatchCharAction}
* |<| {@link com.maddyhome.idea.vim.action.change.shift.ShiftLeftMotionAction}
* |<<| translated to <_
@@ -618,7 +618,6 @@
* |c_CTRL-N| {@link com.maddyhome.idea.vim.ui.ex.HistoryDownAction}
* |c_CTRL-P| {@link com.maddyhome.idea.vim.ui.ex.HistoryUpAction}
* |c_CTRL-Q| Handled by KeyHandler
* |c_CTRL-R| {@link com.maddyhome.idea.vim.ui.ex.InsertRegisterAction}
* |c_CTRL-R_CTRL-A| TO BE IMPLEMENTED
* |c_CTRL-R_CTRL-F| TO BE IMPLEMENTED
* |c_CTRL-R_CTRL-L| TO BE IMPLEMENTED

View File

@@ -1460,6 +1460,9 @@ internal class RegExp {
* can't go before line 1 */
return if (reg_firstlnum + lnum < 0) {
null
} else if (reg_firstlnum + lnum >= reg_buf!!.lineCount()) {
// Must have matched the "\n" in the last line.
CharPointer("")
} else {
CharPointer(
reg_buf!!.getLineBuffer(reg_firstlnum + lnum),
@@ -1507,7 +1510,7 @@ internal class RegExp {
reg_buf = buf
// reg_win = win;
reg_firstlnum = lnum
reg_maxline = lcount - lnum
reg_maxline = lcount - lnum - 1 // Remember, lnum is 0-based, while in Vim, it's 1-based
ireg_ic = rmp.rmm_ic
/* Need to switch to buffer "buf" to make vim_iswordc() work. */
@@ -1874,7 +1877,9 @@ internal class RegExp {
reginput!!.inc()
}
IDENT -> {
if (!Character.isJavaIdentifierPart(c)) {
// Character.isJavaIdentifier treats '\0' as a valid identifier!
// Also, this should really be using 'isident' instead of Character.isJavaIdentifierPart
if (c == '\u0000' || !Character.isJavaIdentifierPart(c)) {
return false
}
reginput!!.inc()
@@ -2488,7 +2493,7 @@ internal class RegExp {
return false
}
NEWL -> {
if (c != '\u0000' || reglnum == reg_maxline) {
if (c != '\u0000' || reglnum > reg_maxline) {
return false
}
reg_nextline()
@@ -2530,7 +2535,7 @@ internal class RegExp {
++count
scan.inc()
}
if (!WITH_NL(p.OP()) || reglnum == reg_maxline || count == maxcount) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline || count == maxcount) {
break
}
++count /* count the line-break */
@@ -2548,7 +2553,7 @@ internal class RegExp {
) {
scan.inc()
} else if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2568,7 +2573,7 @@ internal class RegExp {
) {
scan.inc()
} else if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2587,7 +2592,7 @@ internal class RegExp {
if (CharacterClasses.isWord(scan.charAt()) && (testval == 1 || !Character.isDigit(scan.charAt()))) {
scan.inc()
} else if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2605,7 +2610,7 @@ internal class RegExp {
if (CharacterClasses.isWord(scan.charAt()) && (testval == 1 || !Character.isDigit(scan.charAt()))) {
scan.inc()
} else if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2624,7 +2629,7 @@ internal class RegExp {
if (CharacterClasses.isFile(scan.charAt()) && (testval == 1 || !Character.isDigit(scan.charAt()))) {
scan.inc()
} else if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2642,7 +2647,7 @@ internal class RegExp {
if (CharacterClasses.isFile(scan.charAt()) && (testval == 1 || !Character.isDigit(scan.charAt()))) {
scan.inc()
} else if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2659,7 +2664,7 @@ internal class RegExp {
testval = 1
while (count < maxcount) {
if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2679,7 +2684,7 @@ internal class RegExp {
}
SPRINT, SPRINT + ADD_NL -> while (count < maxcount) {
if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2765,7 +2770,7 @@ internal class RegExp {
testval = 1
while (count < maxcount) {
if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2784,7 +2789,7 @@ internal class RegExp {
}
ANYBUT, ANYBUT + ADD_NL -> while (count < maxcount) {
if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum > reg_maxline) {
break
}
reg_nextline()
@@ -2800,7 +2805,7 @@ internal class RegExp {
}
++count
}
NEWL -> while (count < maxcount && scan.isNul && reglnum < reg_maxline) {
NEWL -> while (count < maxcount && scan.isNul && reglnum <= reg_maxline) {
count++
reg_nextline()
scan = reginput!!.ref(0)
@@ -2813,7 +2818,7 @@ internal class RegExp {
if (mask != 0) {
while (count < maxcount) {
if (scan.isNul) {
if (!WITH_NL(p.OP()) || reglnum == reg_maxline) {
if (!WITH_NL(p.OP()) || reglnum < reg_maxline) {
break
}
reg_nextline()
@@ -3165,7 +3170,7 @@ internal class RegExp {
reg_mmatch = rmp
// reg_buf = curbuf; /* always works on the current buffer! */
reg_firstlnum = lnum
reg_maxline = reg_buf!!.lineCount() - lnum
reg_maxline = reg_buf!!.lineCount() - lnum - 1 // lnum is 0-based, so make sure maxline is too
return vim_regsub_both(source, magic, backslash)
}
@@ -3307,7 +3312,7 @@ internal class RegExp {
if (reg_mmatch!!.endpos[no]!!.lnum == clnum) {
break
}
dst.append('\r')
dst.append('\n')
s = reg_getline(++clnum)
len = if (reg_mmatch!!.endpos[no]!!.lnum == clnum) {
reg_mmatch!!.endpos[no]!!.col

View File

@@ -151,6 +151,10 @@ public class ExOutputPanel extends JPanel {
}
}
public String getText() {
return myText.getText();
}
@SuppressWarnings("ConstantConditions")
@Override
public Color getForeground() {
@@ -288,7 +292,7 @@ public class ExOutputPanel extends JPanel {
}
}
private void close() {
public void close() {
close(null);
}
@@ -307,7 +311,7 @@ public class ExOutputPanel extends JPanel {
KeyHandler.getInstance().getKeyStack().dump());
}
KeyHandler.getInstance().getKeyStack().addKeys(keys);
ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(myEditor), null);
ExecutionContext context = injector.getExecutionContextManager().getEditorExecutionContext(new IjVimEditor(myEditor));
VimPlugin.getMacro().playbackKeys(new IjVimEditor(myEditor), context, 1);
}
});

View File

@@ -26,10 +26,12 @@ import javax.swing.text.TextAction
import kotlin.math.abs
import kotlin.math.min
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal interface MultiStepAction : Action {
fun reset()
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class HistoryUpAction : TextAction(ExEditorKit.HistoryUp) {
override fun actionPerformed(actionEvent: ActionEvent) {
val target = getTextComponent(actionEvent) as ExTextField
@@ -37,6 +39,7 @@ internal class HistoryUpAction : TextAction(ExEditorKit.HistoryUp) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class HistoryDownAction : TextAction(ExEditorKit.HistoryDown) {
override fun actionPerformed(actionEvent: ActionEvent) {
val target = getTextComponent(actionEvent) as ExTextField
@@ -44,6 +47,7 @@ internal class HistoryDownAction : TextAction(ExEditorKit.HistoryDown) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class HistoryUpFilterAction : TextAction(ExEditorKit.HistoryUpFilter) {
override fun actionPerformed(actionEvent: ActionEvent) {
val target = getTextComponent(actionEvent) as ExTextField
@@ -51,6 +55,7 @@ internal class HistoryUpFilterAction : TextAction(ExEditorKit.HistoryUpFilter) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class HistoryDownFilterAction : TextAction(ExEditorKit.HistoryDownFilter) {
override fun actionPerformed(actionEvent: ActionEvent) {
val target = getTextComponent(actionEvent) as ExTextField
@@ -58,51 +63,7 @@ internal class HistoryDownFilterAction : TextAction(ExEditorKit.HistoryDownFilte
}
}
internal class InsertRegisterAction : TextAction(ExEditorKit.InsertRegister), MultiStepAction {
private enum class State {
SKIP_CTRL_R, WAIT_REGISTER
}
private var state = State.SKIP_CTRL_R
override fun actionPerformed(e: ActionEvent) {
val target = getTextComponent(e) as ExTextField
val key = ExEditorKit.convert(e)
if (key != null) {
when (state) {
State.SKIP_CTRL_R -> {
state = State.WAIT_REGISTER
target.setCurrentAction(this, '\"')
}
State.WAIT_REGISTER -> {
state = State.SKIP_CTRL_R
target.clearCurrentAction()
val c = key.keyChar
if (c != KeyEvent.CHAR_UNDEFINED) {
val register = VimPlugin.getRegister().getRegister(c)
if (register != null) {
val oldText = target.actualText
val text = register.text
if (text != null) {
val offset = target.caretPosition
target.text = oldText.substring(0, offset) + text + oldText.substring(offset)
target.caretPosition = offset + text.length
}
}
} else if (key.modifiers and KeyEvent.CTRL_DOWN_MASK != 0 && key.keyCode == KeyEvent.VK_C) {
// Eat any unused keys, unless it's <C-C>, in which case forward on and cancel entry
target.handleKey(key)
}
}
}
}
}
override fun reset() {
state = State.SKIP_CTRL_R
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class CompleteEntryAction : TextAction(ExEditorKit.CompleteEntry) {
override fun actionPerformed(actionEvent: ActionEvent) {
logger.debug("complete entry")
@@ -123,6 +84,7 @@ internal class CompleteEntryAction : TextAction(ExEditorKit.CompleteEntry) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class CancelEntryAction : TextAction(ExEditorKit.CancelEntry) {
override fun actionPerformed(e: ActionEvent) {
val target = getTextComponent(e) as ExTextField
@@ -130,6 +92,7 @@ internal class CancelEntryAction : TextAction(ExEditorKit.CancelEntry) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class EscapeCharAction : TextAction(ExEditorKit.EscapeChar) {
override fun actionPerformed(e: ActionEvent) {
val target = getTextComponent(e) as ExTextField
@@ -137,6 +100,7 @@ internal class EscapeCharAction : TextAction(ExEditorKit.EscapeChar) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal abstract class DeleteCharAction internal constructor(name: String?) : TextAction(name) {
@kotlin.jvm.Throws(BadLocationException::class)
fun deleteSelection(doc: Document, dot: Int, mark: Int): Boolean {
@@ -184,6 +148,7 @@ internal abstract class DeleteCharAction internal constructor(name: String?) : T
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class DeleteNextCharAction : DeleteCharAction(DefaultEditorKit.deleteNextCharAction) {
override fun actionPerformed(e: ActionEvent) {
val target = getTextComponent(e) as ExTextField
@@ -202,6 +167,7 @@ internal class DeleteNextCharAction : DeleteCharAction(DefaultEditorKit.deleteNe
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class DeletePreviousCharAction : DeleteCharAction(DefaultEditorKit.deletePrevCharAction) {
override fun actionPerformed(e: ActionEvent) {
val target = getTextComponent(e) as ExTextField
@@ -222,6 +188,7 @@ internal class DeletePreviousCharAction : DeleteCharAction(DefaultEditorKit.dele
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class DeletePreviousWordAction : TextAction(DefaultEditorKit.deletePrevWordAction) {
/**
* Invoked when an action occurs.
@@ -258,6 +225,7 @@ internal class DeletePreviousWordAction : TextAction(DefaultEditorKit.deletePrev
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class DeleteToCursorAction : TextAction(ExEditorKit.DeleteToCursor) {
/**
* Invoked when an action occurs.
@@ -275,6 +243,7 @@ internal class DeleteToCursorAction : TextAction(ExEditorKit.DeleteToCursor) {
}
}
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class ToggleInsertReplaceAction : TextAction(ExEditorKit.ToggleInsertReplace) {
/**
* Invoked when an action occurs.

View File

@@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.ui.ex
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.annotations.NonNls
import java.awt.event.ActionEvent
@@ -21,6 +20,7 @@ import javax.swing.text.DefaultEditorKit
import javax.swing.text.Document
import javax.swing.text.TextAction
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal object ExEditorKit : DefaultEditorKit() {
@NonNls
@@ -38,9 +38,6 @@ internal object ExEditorKit : DefaultEditorKit() {
@NonNls
val ToggleInsertReplace: String = "toggle-insert"
@NonNls
val InsertRegister: String = "insert-register"
@NonNls
val HistoryUp: String = "history-up"
@@ -108,7 +105,6 @@ internal object ExEditorKit : DefaultEditorKit() {
HistoryUpFilterAction(),
HistoryDownFilterAction(),
ToggleInsertReplaceAction(),
InsertRegisterAction(),
)
class DefaultExKeyHandler : DefaultKeyTypedAction() {
@@ -126,12 +122,7 @@ internal object ExEditorKit : DefaultEditorKit() {
val entry = ExEntryPanel.getInstance().entry
val editor = entry.editor
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor.vim,
key,
injector.executionContextManager.onEditor(editor.vim, entry.context.vim),
keyHandler.keyHandlerState,
)
keyHandler.handleKey(editor.vim, key, entry.context.vim, keyHandler.keyHandlerState)
} else {
val event = ActionEvent(e.source, e.id, c.toString(), e.getWhen(), e.modifiers)
super.actionPerformed(event)

View File

@@ -18,7 +18,13 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollingModel;
import com.intellij.ui.DocumentAdapter;
import com.intellij.util.IJSwingUtilities;
import com.maddyhome.idea.vim.EventFacade;
import com.maddyhome.idea.vim.KeyHandler;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.action.VimShortcutKeyAction;
import com.maddyhome.idea.vim.api.VimCommandLine;
import com.maddyhome.idea.vim.api.VimCommandLineCaret;
import com.maddyhome.idea.vim.api.VimKeyGroupBase;
import com.maddyhome.idea.vim.ex.ranges.LineRange;
import com.maddyhome.idea.vim.helper.SearchHighlightsHelper;
import com.maddyhome.idea.vim.helper.UiHelper;
@@ -28,6 +34,7 @@ import com.maddyhome.idea.vim.regexp.CharPointer;
import com.maddyhome.idea.vim.regexp.RegExp;
import com.maddyhome.idea.vim.ui.ExPanelBorder;
import com.maddyhome.idea.vim.vimscript.model.commands.Command;
import com.maddyhome.idea.vim.vimscript.model.commands.GlobalCommand;
import com.maddyhome.idea.vim.vimscript.model.commands.SubstituteCommand;
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser;
import org.jetbrains.annotations.Contract;
@@ -44,12 +51,13 @@ import java.awt.event.ComponentListener;
import static com.maddyhome.idea.vim.api.VimInjectorKt.globalOptions;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
import static com.maddyhome.idea.vim.group.KeyGroup.toShortcutSet;
/**
* This is used to enter ex commands such as searches and "colon" commands
*/
public class ExEntryPanel extends JPanel {
private static ExEntryPanel instance;
public class ExEntryPanel extends JPanel implements VimCommandLine {
public static ExEntryPanel instance;
private static ExEntryPanel instanceWithoutShortcuts;
private ExEntryPanel(boolean enableShortcuts) {
@@ -71,6 +79,11 @@ public class ExEntryPanel extends JPanel {
if (enableShortcuts) {
// This does not need to be unregistered, it's registered as a custom UI property on this
EventFacade.getInstance().registerCustomShortcutSet(
VimShortcutKeyAction.getInstance(),
toShortcutSet(((VimKeyGroupBase) injector.getKeyGroup()).getRequiredShortcutKeys()),
entry
);
new ExShortcutKeyAction(this).registerCustomShortcutSet();
}
@@ -174,6 +187,7 @@ public class ExEntryPanel extends JPanel {
/**
* Turns off the ex entry field and optionally puts the focus back to the original component
*/
@Override
public void deactivate(boolean refocusOwningEditor, boolean resetCaret) {
logger.info("Deactivate ex entry panel");
if (!active) return;
@@ -254,51 +268,72 @@ public class ExEntryPanel extends JPanel {
private final @NotNull DocumentListener incSearchDocumentListener = new DocumentAdapter() {
@Override
protected void textChanged(@NotNull DocumentEvent e) {
final Editor editor = entry.getEditor();
try {
final Editor editor = entry.getEditor();
boolean searchCommand = false;
LineRange searchRange = null;
char separator = label.getText().charAt(0);
String searchText = entry.getActualText();
if (label.getText().equals(":")) {
if (searchText.isEmpty()) return;
final Command command = getIncsearchCommand(searchText);
if (command == null) {
return;
final String labelText = label.getText(); // Either '/', '?' or ':'boolean searchCommand = false;
boolean searchCommand = false;
LineRange searchRange = null;
char separator = labelText.charAt(0);
String searchText = entry.getActualText();
if (labelText.equals(":")) {
if (searchText.isEmpty()) return;
final Command command = getIncsearchCommand(searchText);
if (command == null) {
return;
}
searchCommand = true;
searchText = "";
final String argument = command.getCommandArgument();
if (argument.length() > 1) { // E.g. skip '/' in `:%s/`. `%` is range, `s` is command, `/` is argument
separator = argument.charAt(0);
searchText = argument.substring(1);
}
if (!searchText.isEmpty()) {
searchRange = command.getLineRangeSafe(new IjVimEditor(editor));
}
searchCommand = true;
searchText = "";
final String argument = command.getCommandArgument();
if (argument.length() > 1) { // E.g. skip '/' in `:%s/`. `%` is range, `s` is command, `/` is argument
separator = argument.charAt(0);
searchText = argument.substring(1);
if (searchText.isEmpty() || searchRange == null) {
// Reset back to the original search highlights after deleting a search from a substitution command.Or if
// there is no search range (because the user entered an invalid range, e.g. mark not set).
// E.g. Highlight `whatever`, type `:%s/foo` + highlight `foo`, delete back to `:%s/` and reset highlights
// back to `whatever`
VimPlugin.getSearch().resetIncsearchHighlights();
resetCaretOffset(editor);
return;
}
}
if (searchText.length() == 0) {
// Reset back to the original search highlights after deleting a search from a substitution command.
// E.g. Highlight `whatever`, type `:%s/foo` + highlight `foo`, delete back to `:%s/` and reset highlights
// back to `whatever`
VimPlugin.getSearch().resetIncsearchHighlights();
return;
// Get the current count from the command builder. This value is coerced to at least 1, so will always be valid.
// The aggregated value includes any counts for operator and register selections, and the uncommitted count for
// the search command (`/` or `?`). E.g., `2"a3"b4"c5d6/` would return 720.
// If we're showing highlights for an ex command like `:s`, there won't be a command, but the value is already
// coerced to at least 1.
int count1 = KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getAggregatedUncommittedCount();
if (labelText.equals("/") || labelText.equals("?") || searchCommand) {
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
final String pattern;
final CharPointer p = new CharPointer(searchText);
final CharPointer end = RegExp.skip_regexp(new CharPointer(searchText), separator, true);
pattern = p.substring(end.pointer() - p.pointer());
VimPlugin.getEditor().closeEditorSearchSession(editor);
final int matchOffset =
SearchHighlightsHelper.updateIncsearchHighlights(editor, pattern, count1, forwards, caretOffset,
searchRange);
if (matchOffset != -1) {
new IjVimCaret(editor.getCaretModel().getPrimaryCaret()).moveToOffset(matchOffset);
}
else {
resetCaretOffset(editor);
}
}
searchRange = command.getLineRange(new IjVimEditor(editor));
}
final String labelText = label.getText();
if (labelText.equals("/") || labelText.equals("?") || searchCommand) {
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
final String pattern;
final CharPointer p = new CharPointer(searchText);
final CharPointer end = RegExp.skip_regexp(new CharPointer(searchText), separator, true);
pattern = p.substring(end.pointer() - p.pointer());
VimPlugin.getEditor().closeEditorSearchSession(editor);
final int matchOffset = SearchHighlightsHelper.updateIncsearchHighlights(editor, pattern, forwards, caretOffset, searchRange);
if (matchOffset != -1) {
new IjVimCaret(editor.getCaretModel().getPrimaryCaret()).moveToOffset(matchOffset);
}
else {
resetCaretOffset(editor);
}
catch (Throwable ex) {
// Make sure the exception doesn't leak out of the handler, because it can break the text entry field and
// require the editor to be closed/reopened. The worst that will happen is no incsearch highlights
logger.warn("Error while trying to show incsearch highlights", ex);
}
}
@@ -307,8 +342,8 @@ public class ExEntryPanel extends JPanel {
if (commandText == null) return null;
try {
final Command exCommand = VimscriptParser.INSTANCE.parseCommand(commandText);
// TODO: Add global, vglobal, smagic and snomagic here when the commands are supported
if (exCommand instanceof SubstituteCommand) {
// TODO: Add smagic and snomagic here if/when the commands are supported
if (exCommand instanceof SubstituteCommand || exCommand instanceof GlobalCommand) {
return exCommand;
}
}
@@ -325,6 +360,7 @@ public class ExEntryPanel extends JPanel {
*
* @return The ex entry label
*/
@Override
public String getLabel() {
return label.getText();
}
@@ -352,10 +388,16 @@ public class ExEntryPanel extends JPanel {
*
* @return The user entered text
*/
@Override
public @NotNull String getText() {
return entry.getActualText();
}
@Override
public void setText(@NotNull String s) {
entry.setText(s);
}
public @NotNull ExTextField getEntry() {
return entry;
}
@@ -365,6 +407,7 @@ public class ExEntryPanel extends JPanel {
*
* @param stroke The keystroke
*/
@Override
public void handleKey(@NotNull KeyStroke stroke) {
entry.handleKey(stroke);
}
@@ -450,6 +493,12 @@ public class ExEntryPanel extends JPanel {
private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName());
@NotNull
@Override
public VimCommandLineCaret getCaret() {
return (VimCommandLineCaret) entry.getCaret();
}
public static class LafListener implements LafManagerListener {
@Override
public void lookAndFeelChanged(@NotNull LafManager source) {

View File

@@ -1,29 +1,34 @@
/*
* Copyright 2003-2023 The IdeaVim authors
* 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.helper
package com.maddyhome.idea.vim.ui.ex
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.maddyhome.idea.vim.action.change.Extension
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCommandLine
import com.maddyhome.idea.vim.api.VimCommandLineService
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.TestInputModel
import com.maddyhome.idea.vim.helper.isCloseKeyStroke
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.ui.ModalEntry
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@Service
internal class CommandLineHelper : VimCommandLineHelper {
public class ExEntryPanelService : VimCommandLineService {
public override fun getActiveCommandLine(): VimCommandLine? {
return ExEntryPanel.instance
}
override fun inputString(vimEditor: VimEditor, prompt: String, finishOn: Char?): String? {
override fun inputString(vimEditor: VimEditor, context: ExecutionContext, prompt: String, finishOn: Char?): String? {
val editor = vimEditor.ij
if (vimEditor.vimStateMachine.isDotRepeatInProgress) {
val input = Extension.consumeString()
@@ -52,27 +57,26 @@ internal class CommandLineHelper : VimCommandLineHelper {
} else {
var text: String? = null
// XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for input()
val exEntryPanel = ExEntryPanel.getInstanceWithoutShortcuts()
exEntryPanel.activate(editor, injector.executionContextManager.onEditor(editor.vim).ij, prompt.ifEmpty { " " }, "", 1)
val commandLine = injector.commandLine.create(vimEditor, context, prompt.ifEmpty { " " }, "", 1)
ModalEntry.activate(editor.vim) { key: KeyStroke ->
return@activate when {
key.isCloseKeyStroke() -> {
exEntryPanel.deactivate(true)
commandLine.deactivate(true)
false
}
key.keyCode == KeyEvent.VK_ENTER -> {
text = exEntryPanel.text
exEntryPanel.deactivate(true)
text = commandLine.text
commandLine.deactivate(true)
false
}
finishOn != null && key.keyChar == finishOn -> {
exEntryPanel.handleKey(key)
text = exEntryPanel.text
exEntryPanel.deactivate(true)
commandLine.handleKey(key)
text = commandLine.text
commandLine.deactivate(true)
false
}
else -> {
exEntryPanel.handleKey(key)
commandLine.handleKey(key)
true
}
}
@@ -83,4 +87,10 @@ internal class CommandLineHelper : VimCommandLineHelper {
return text
}
}
public override fun create(editor: VimEditor, context: ExecutionContext, label: String, initText: String, count: Int): VimCommandLine {
val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, label, initText, count)
return panel
}
}

View File

@@ -13,6 +13,7 @@ import javax.swing.KeyStroke
import javax.swing.text.DefaultEditorKit
import javax.swing.text.JTextComponent.KeyBinding
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal object ExKeyBindings {
// TODO - add the following keys:
@@ -71,8 +72,6 @@ internal object ExKeyBindings {
KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.CTRL_DOWN_MASK), ExEditorKit.StartLiteral),
KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK), ExEditorKit.StartLiteral),
KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK), ExEditorKit.InsertRegister),
// These appear to be non-Vim shortcuts
KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.META_DOWN_MASK), DefaultEditorKit.pasteAction),
KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.pasteAction),

View File

@@ -13,7 +13,6 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.KeyboardShortcut
import com.intellij.openapi.project.DumbAwareAction
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.vim
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@@ -30,6 +29,7 @@ import javax.swing.KeyStroke
* component has focus. It registers all shortcuts used by the Swing actions and forwards them directly to the key
* handler.
*/
@Deprecated("ExCommands should be migrated to KeyHandler like commands for other modes")
internal class ExShortcutKeyAction(private val exEntryPanel: ExEntryPanel) : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
@@ -37,12 +37,12 @@ internal class ExShortcutKeyAction(private val exEntryPanel: ExEntryPanel) : Dum
if (keyStroke != null) {
val editor = exEntryPanel.entry.editor
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor.vim,
keyStroke,
injector.executionContextManager.onEditor(editor.vim, e.dataContext.vim),
keyHandler.keyHandlerState
)
// About the context: we use the context of the main editor to execute actions on it.
// e.dataContext will refer to the ex-entry editor and commands will be executed on it,
// thus it should not be used. For example, `:action EditorSelectWord` will not work with this context
val mainEditorContext = exEntryPanel.entry.context.vim
keyHandler.handleKey(editor.vim, keyStroke, mainEditorContext, keyHandler.keyHandlerState)
}
}

View File

@@ -14,6 +14,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.ui.paint.PaintUtil;
import com.intellij.util.ui.JBUI;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.VimCommandLineCaret;
import com.maddyhome.idea.vim.group.EditorHolderService;
import com.maddyhome.idea.vim.helper.UiHelper;
import com.maddyhome.idea.vim.history.HistoryConstants;
@@ -408,7 +409,7 @@ public class ExTextField extends JTextField {
caret.setAttributes(GuiCursorOptionHelper.INSTANCE.getAttributes(GuiCursorMode.CMD_LINE_REPLACE));
}
private static class CommandLineCaret extends DefaultCaret {
private static class CommandLineCaret extends DefaultCaret implements VimCommandLineCaret {
private GuiCursorType mode;
private int thickness = 100;
@@ -557,6 +558,16 @@ public class ExTextField extends JTextField {
}
return fullHeight;
}
@Override
public int getOffset() {
return getDot();
}
@Override
public void setOffset(int i) {
setDot(i);
}
}
@TestOnly

View File

@@ -74,8 +74,7 @@ internal class Executor : VimScriptExecutorBase() {
VimPlugin.indicateError()
}
} catch (e: Exception) {
logger.warn("Caught: ${e.message}")
logger.warn(e.stackTrace.toString())
logger.warn(e)
if (injector.application.isUnitTest()) {
throw e
}

View File

@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
@@ -26,7 +26,7 @@ import java.util.*
* @author smartbomb
*/
@ExCommand(command = "actionl[ist]")
internal data class ActionListCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges) {
internal data class ActionListCommand(val range: Range, val argument: String) : Command.SingleExecution(range) {
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {

View File

@@ -15,7 +15,7 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.newapi.ij
@@ -27,7 +27,7 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
* @author John Weigel
*/
@ExCommand(command = "b[uffer]")
internal data class BufferCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges) {
internal data class BufferCommand(val range: Range, val argument: String) : Command.SingleExecution(range) {
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {

View File

@@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.vimLine
import com.maddyhome.idea.vim.newapi.ij
@@ -32,7 +32,7 @@ import org.jetbrains.annotations.NonNls
* @author John Weigel
*/
@ExCommand(command = "ls,files,buffers")
internal data class BufferListCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges) {
internal data class BufferListCommand(val range: Range, val argument: String) : Command.SingleExecution(range) {
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
companion object {

View File

@@ -18,7 +18,8 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.ex.ranges.toTextRange
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.newapi.ij
@@ -28,7 +29,7 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
* see "h :!"
*/
@ExCommand(command = "!")
internal data class CmdFilterCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges) {
internal data class CmdFilterCommand(val range: Range, val argument: String) : Command.SingleExecution(range) {
override val argFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_OPTIONAL, Access.SELF_SYNCHRONIZED)
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {
@@ -70,7 +71,7 @@ internal data class CmdFilterCommand(val ranges: Ranges, val argument: String) :
val workingDirectory = editor.ij.project?.basePath
return try {
if (ranges.size() == 0) {
if (range.size() == 0) {
// Show command output in a window
VimPlugin.getProcess().executeCommand(editor, command, null, workingDirectory)?.let {
ExOutputModel.getInstance(editor.ij).output(it)
@@ -78,7 +79,7 @@ internal data class CmdFilterCommand(val ranges: Ranges, val argument: String) :
ExecutionResult.Success
} else {
// Filter
val range = this.getTextRange(editor, false)
val range = getLineRange(editor).toTextRange(editor)
val input = editor.ij.document.charsSequence.subSequence(range.startOffset, range.endOffset)
VimPlugin.getProcess().executeCommand(editor, command, input, workingDirectory)?.let {
ApplicationManager.getApplication().runWriteAction {

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