1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2025-09-15 14:32:08 +02:00

80 Commits

Author SHA1 Message Date
568128f474 Set version to chylex-26 2024-10-31 03:57:33 +01:00
018d29007c Add option to invert behavior of holding Shift when searching, and toggle with Enter 2024-10-31 03:57:33 +01:00
cb814e0999 Set version to chylex-25 2024-09-05 09:11:05 +02:00
1fc06e8120 Update for latest IdeaVim 2024-09-05 09:10:21 +02:00
841f2fd125 Set version to chylex-24 2024-09-05 06:44:02 +02:00
6c8f19e311 Fix edge case where generated tags may have a double character tag that is the only tag starting with its first character 2024-09-05 06:43:41 +02:00
ce4f3b5f03 Fix IntelliJ SDK warning about services in class initializer 2024-09-05 02:44:39 +02:00
46f42c88eb Optimize screen visibility checks during search
Search results are tested for visibility on the screen. There is already an optimization that only checks results on visible lines, but the number of offset-to-XY conversions still scales linearly with the number of results, which can be very large in files with long lines.

A simple observation is that every line has a first and last offset that is visible on the screen (which may be different for each line due to proportional fonts).

This commit caches the visible offset range for every line involved in one search query, so testing visibility of a search result becomes a check if its offset is inside its line's visible offset range. Finding the visible offset range requires two XY-to-offset conversions per line, so the total number of conversions is bounded by the number of lines that can fit on the screen.

The worst case for this optimization is when every line has exactly one search result; before, this would lead to one offset-to-XY conversion per line, whereas now it leads to two XY-to-offset conversions per line. However, the maximum number of conversions is twice the number of visible lines, which will generally be very small.
2024-09-05 02:12:48 +02:00
43dfec940e Set version to chylex-23 2024-09-04 11:40:16 +02:00
abe06ec7be Increase width of editor fade opacity slider 2024-09-04 11:39:59 +02:00
3fc3cbc7f8 Optimize tagging 2024-09-04 11:33:26 +02:00
5979579042 Set version to chylex-22 2024-09-04 08:50:33 +02:00
ea61d49aa6 Refactor TagMarker to reduce allocations during rendering 2024-09-04 08:50:13 +02:00
dbc6db108d Redo tag generation (eliminate explicit prefix chars) 2024-09-04 08:44:52 +02:00
01c38df82a Search in the middle of words again unless pressing an uppercase letter, and rebind Space to cycling search boundaries 2024-09-03 21:50:03 +02:00
a3a86cf447 Update for IDEA 2024.2 2024-09-03 20:58:08 +02:00
59fbd4e19c Set version to chylex-21 2024-07-15 13:46:02 +02:00
6e08d56cdf Update for IDEA 2024.1 & latest IdeaVim 2024-07-15 13:45:39 +02:00
9a14fb87e3 Set version to chylex-20 2024-03-27 13:53:41 +01:00
d22bcc220e Update IdeaVim integration 2024-03-27 13:53:25 +01:00
1f76a8bd25 Set version to chylex-19 2023-12-29 21:45:12 +01:00
2e31ddd504 Use top row remapping only for tags 2023-12-29 21:45:12 +01:00
47a34f6f14 Disable searching in the middle of a word unless Space is pressed when typing tag 2023-12-29 21:45:12 +01:00
575283b2be Set version to chylex-18 2023-12-18 05:25:09 +01:00
8bb34d7f75 Make editor fade opacity configurable 2023-12-18 05:24:57 +01:00
8d1ef031d2 Set version to chylex-17 2023-12-18 03:44:20 +01:00
a9375ec414 Remove unused code 2023-12-18 03:44:01 +01:00
b42112cc9e Do not try to tag folded regions 2023-12-18 02:36:41 +01:00
8e42c82821 Use distance from current caret for tag order 2023-12-18 02:32:54 +01:00
48bcf9f284 Add QWERTZ (CZ) layout that remaps top row to digits 2023-12-18 00:58:37 +01:00
2681d9901f Change priority order of two-character tags 2023-12-18 00:45:28 +01:00
b13d629046 Add option to set different colors for two-character tags 2023-12-17 22:41:36 +01:00
92dcd033fb Make priority of unknown characters lower than known characters 2023-12-17 21:49:24 +01:00
fa3505b850 Set version to chylex-16 2023-12-14 00:09:49 +01:00
dacac684f0 Make two-character tag prefixes customizable 2023-12-14 00:09:27 +01:00
e627db3a24 Fix Shift mode not working when accepting a tag 2023-12-13 22:52:18 +01:00
084d729baa Set version to chylex-15 2023-12-13 20:16:51 +01:00
e01edccb5b Redesign tags to look like easymotion vim plugin 2023-12-13 20:16:13 +01:00
655ccde60e Work around IntelliJ terminal plugin stealing Enter keys 2023-12-13 17:48:23 +01:00
eb2ea55fb8 Rework tagging to match easymotion (no search query refinement, no double letter tags) 2023-12-13 15:19:57 +01:00
a6381a2a34 Update Gradle to 8.5 and IntelliJ to 2023.3 2023-12-12 23:19:03 +01:00
e005983d4c Set version to chylex-14 2023-11-17 08:56:13 +01:00
8f4d9748ad Scroll after jumping in vim mode 2023-11-17 08:55:49 +01:00
76c6458ef4 Re-add action to go to declaration after jump 2023-11-17 08:52:46 +01:00
2f53e9da6d Update for IdeaVIM chylex-20 2023-11-17 08:52:23 +01:00
100001ffca Set version to chylex-13 2023-10-04 02:41:33 +02:00
184896a6cb Update for IdeaVIM chylex-16 2023-10-04 02:41:05 +02:00
8563400946 Set plugin version to chylex-12 2023-10-04 02:40:51 +02:00
a07c61a384 Fully depend on IdeaVIM and remake actions 2023-07-28 07:50:39 +02:00
e072003c5c Update dependency versions and gitignore 2023-07-27 22:07:17 +02:00
8062cf5cab Update dependency versions 2023-01-09 07:19:07 +01:00
9151ee376c Set plugin version to chylex-11 2022-07-06 15:46:45 +02:00
19ce1c69fd Improve tag order for non-QWERTY layouts 2022-07-06 15:46:35 +02:00
f2a053505c Remove no longer necessary actions 2022-07-06 15:46:35 +02:00
647cfb14f1 Remove unused code 2022-07-06 15:46:35 +02:00
c31ba60909 Implement a customized Vim easymotion plugin 2022-07-06 15:46:35 +02:00
9c60a8a4ba Update build.gradle IDEA & plugin versions 2022-06-18 20:59:52 +02:00
1e8b7d7963 Remove Kotlin stdlib from distribution 2022-06-18 20:54:58 +02:00
9157ce19b0 Remove all special modes introduced in the rework 2021-11-14 14:35:54 +01:00
9a435feccc Fix broken special actions in Rider 2021-09-29 08:56:15 +02:00
c1feb891e4 Update Gradle wrapper to 7.1 2021-09-29 08:25:09 +02:00
0b6ba62cda Simplify existing modes and add new vim shortcuts 2021-05-15 20:58:58 +02:00
dfd5b122a0 Optimizations 2021-05-15 20:30:18 +02:00
f2400d134d Update build.gradle 2021-05-15 19:46:51 +02:00
98997b4d86 [WIP] Fix tests 2021-05-08 03:54:27 +02:00
1e172e9c49 [WIP] Implement jumping across splitters 2021-05-08 02:39:27 +02:00
01b37c878e [WIP] Add more vim-friendly jump actions (declaration, usages) 2021-04-11 08:34:52 +02:00
1630c706a9 [WIP] Fix easymotion-like actions in visual mode 2021-04-08 13:57:11 +02:00
041a130c5f [WIP] Add a few easymotion-like actions 2021-04-07 03:05:11 +02:00
7561bfde36 [WIP] Make quick jump the default mode and rename non-quick jump to advanced mode 2021-04-07 00:16:50 +02:00
724e469f21 [WIP] Remove 'From Caret' mode 2021-04-06 02:09:29 +02:00
bce9a5f636 [WIP] Add quick jump mode 2021-03-29 18:43:00 +02:00
bfe0aa536e [WIP] Change plugin version 2021-03-29 18:42:40 +02:00
2ca21b8423 [WIP] Work around weird highlight offset bug 2021-03-29 18:42:00 +02:00
430bcf6883 [WIP] Remove word start jump to simplify 2021-03-29 18:41:59 +02:00
092650af81 [WIP] Pressing Enter before typing query starts jump mode for character at caret 2021-03-29 18:41:57 +02:00
2009123114 [WIP] Interactive modes 2021-03-29 18:41:54 +02:00
de19c0e0b8 Make Enter immediately tag search occurrences 2021-03-29 18:39:15 +02:00
5021a07fb9 Remove tag visiting functionality 2021-03-29 18:38:32 +02:00
1180547b06 Remove whole file search 2021-03-29 18:36:53 +02:00
61 changed files with 1964 additions and 3418 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: breandan

24
.gitignore vendored
View File

@@ -1,21 +1,5 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
/.idea/*
!/.idea/runConfigurations
## Directory-based project format:
.idea
*.iml
## Plugin-specific files:
# IntelliJ
/out/
### Gradle template
.gradle
build/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
gradle.properties
# Mac OS
.DS_Store
/.gradle/
/build/

View File

@@ -1,108 +1,7 @@
# Changelog
## Unreleased
### 3.7
## 3.8.19
- Enable support for 2024.1, fixing ([#457](https://github.com/acejump/AceJump/issues/457))
## 3.8.18
- Disable tagging and jumping to folded regions ([#453](https://github.com/acejump/AceJump/issues/453)), thanks to [@chylex](https://github.com/chylex)
- Update hint styling and show mode ([#394](https://github.com/acejump/AceJump/issues/394)) when "Show hint with search text" is enabled
- Fixes "Char sequence is empty" ([#404](https://github.com/acejump/AceJump/issues/404)) when "Map Unicode to ASCII" is enabled
## 3.8.17
- Add buttons to reset colors to default values in Settings, [#411](https://github.com/acejump/AceJump/issues/411), thanks to [@chylex](https://github.com/chylex)
- Unbundle conflicting Kotlin Standard Library version, [#449](https://github.com/acejump/AceJump/issues/449), thanks to [@chylex](https://github.com/chylex)
- Fix some instances of "Read access not allowed", [#447](https://github.com/acejump/AceJump/issues/447), thanks to [@h0tk3y](https://github.com/h0tk3y)
## 3.8.16
- Fix issue with unselectable tags, [#446](https://github.com/acejump/AceJump/issues/446)
## 3.8.15
- Forbid jumping to offscreen tags, [#442](https://github.com/acejump/AceJump/issues/442)
## 3.8.14
- Fixes NoSuchFieldError: Companion on older platform versions, [#432](https://github.com/acejump/AceJump/issues/432), [#434](https://github.com/acejump/AceJump/issues/434), [#435](https://github.com/acejump/AceJump/issues/432), [#437](https://github.com/acejump/AceJump/issues/437), [#438](https://github.com/acejump/AceJump/issues/438), thanks to [@wuruofan](https://github.com/wuruofan)
## 3.8.13
- Fixes color settings not being persisted, [#431](https://github.com/acejump/AceJump/issues/431)
## 3.8.12
- Fixes tag cycling issue with Enter/Shift+Enter, [#429](https://github.com/acejump/AceJump/issues/429)
## 3.8.11
- Fixes UI issue affecting mode cycling order, [#426](https://github.com/acejump/AceJump/issues/426)
## 3.8.10
- Fixes regression in 3.8.9 breaking cross-tab selection, [#417](https://github.com/acejump/AceJump/issues/417)
## 3.8.9
- Add ids to editor action handlers, [#410](https://github.com/acejump/AceJump/pull/410), thanks to [@AlexPl292](https://github.com/AlexPl292)
- Update API to IJ-2022.3 and JDK to 17
## 3.8.8
- Add AZERTY keyboard layout, [#398](https://github.com/acejump/AceJump/pull/398), thanks to [@delphinaubin](https://github.com/delphinaubin)
- Add bounded toggle mode to start jump mode before or after the caret, [#401](https://github.com/acejump/AceJump/pull/401), thanks to [@colossatr0n](https://github.com/colossatr0n)
- Remove only the highlighters added by AceJump when jump session ends, [#407](https://github.com/acejump/AceJump/pull/407), thanks to [@huoguangjin](https://github.com/huoguangjin)
## 3.8.7
- Fixes Unicode-ASCII regression, [#399](https://github.com/acejump/AceJump/issues/399)
## 3.8.6
- Adds AZERTY keyboard layout, [#398](https://github.com/acejump/AceJump/pull/398), thanks to [@delphinaubin](https://github.com/delphinaubin)
## 3.8.5
- Improves tag order for non-QWERTY layouts, [#385](https://github.com/acejump/AceJump/issues/385)
- Restores <kbd>Tab</kbd>/<kbd>Shift</kbd>+<kbd>Tab</kbd> functionality, [#356](https://github.com/acejump/AceJump/issues/356)
- Fixes tag cycling with <kbd>Enter</kbd>/<kbd>Shift</kbd>+<kbd>Enter</kbd>, [#380](https://github.com/acejump/AceJump/issues/380), thanks [@AlexPl292](https://github.com/AlexPl292)
## 3.8.4
- Fixes Declaration Mode in Rider, [#379](https://github.com/acejump/AceJump/issues/379), thanks to [@igor-akhmetov](https://github.com/igor-akhmetov) for helping diagnose!
- Fixes highlight offset on high-DPI screens, [#362](https://github.com/acejump/AceJump/issues/362), thanks to [@chylex](https://github.com/chylex) for [the PR](https://github.com/acejump/AceJump/pull/384)!
## 3.8.3
- Displays regular expression for regex-based queries
- Fixes a bug when current search text was enabled causing word and line mode tags to not be displayed, [#376](https://github.com/acejump/AceJump/issues/376)
## 3.8.2
- Add option to display current search text, [#375](https://github.com/acejump/AceJump/issues/375)
- Fixes a bug where editor was not focused, [#374](https://github.com/acejump/AceJump/issues/374)
- Thanks to [@SaiKai](https://github.com/SaiKai) for the PRs!
## 3.8.1
- Hotfix for stale cache, [#373](https://github.com/acejump/AceJump/issues/373)
## 3.8.0
- Allow jumping between splitters in the editor, [#371](https://github.com/acejump/AceJump/pull/371)
- Adds support for unicode search and selection, [#368](https://github.com/acejump/AceJump/issues/368)
## 3.7.1
- Fix settings display issue, [#363](https://github.com/acejump/AceJump/issues/363)
- Update AceJump extension API to include tag information, [#357](https://github.com/acejump/AceJump/pull/357)
- Allow defining jump mode with boundaries, [#358](https://github.com/acejump/AceJump/pull/358)
- Use Kotlin classes for actions, [#359](https://github.com/acejump/AceJump/pull/359)
- Thanks to [@AlexPl292](https://github.com/AlexPl292) for the PRs!
## 3.7.0
- Improvements to tag latency
- Redesign settings panel
- Add missing configuration for definition mode color
@@ -112,225 +11,223 @@
- Jump-to-End mode jumps to the end of a word
- Fixes toggle keys not resetting mode when pressed twice
- Increase limit for what is considered a large file
- Major refactoring, [#350](https://github.com/acejump/AceJump/pull/353)
- [Many bug fixes](https://github.com/acejump/AceJump/issues/348#issuecomment-739454920): [#338](https://github.com/acejump/AceJump/issues/338), [#336](https://github.com/acejump/AceJump/issues/336), [#329](https://github.com/acejump/AceJump/issues/329), [#327](https://github.com/acejump/AceJump/issues/327), [#310](https://github.com/acejump/AceJump/issues/310), [#233](https://github.com/acejump/AceJump/issues/233), [#228](https://github.com/acejump/AceJump/issues/228), [#187](https://github.com/acejump/AceJump/issues/187), [#147](https://github.com/acejump/AceJump/issues/147), [#132](https://github.com/acejump/AceJump/issues/132), [#71](https://github.com/acejump/AceJump/issues/71)
- Huge thanks to [@chylex](https://github.com/chylex) for [all the PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex)!
- Thanks to @chylex for [all the PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex)!
## 3.6.3
### 3.6.3
- Vote for your favorite <a href="https://twitter.com/breandan/status/1274169810411274241">AceJump logo</a>!
- Fixes potential bug.
- Increases test coverage.
## 3.6.2
### 3.6.2
- Fixes [#226](https://github.com/acejump/AceJump/issues/226). Thanks [@AlexPl292](https://github.com/AlexPl292)!
- Fixes [#226](https://github.com/acejump/AceJump/issues/226). Thanks @AlexPl292!
- Update Pinyin engine.
## 3.6.1
### 3.6.1
- Fixes [#324](https://github.com/acejump/AceJump/issues/324). Thanks [@AlexPl292](https://github.com/AlexPl292)!
- Fixes [#324](https://github.com/acejump/AceJump/issues/324). Thanks @AlexPl292!
- Fixes [#325](https://github.com/acejump/AceJump/issues/325).
- Fixes Pinyin support.
## 3.6.0
### 3.6.0
- Adds support for Chinese [#314](https://github.com/acejump/AceJump/issues/314).
- Fixes constantly loading settings page [#303](https://github.com/acejump/AceJump/issues/303).
- Honor camel humps [#315](https://github.com/acejump/AceJump/issues/315). Thanks to [@clojj](https://github.com/clojj).
- Honor camel humps [#315](https://github.com/acejump/AceJump/issues/315). Thanks to @clojj.
- Support dynamic application reloading [#322](https://github.com/acejump/AceJump/issues/322).
## 3.5.9
### 3.5.9
- Fix a build configuration error affecting plugins which depend on AceJump. Fixes [#305](https://github.com/acejump/AceJump/issues/305).
## 3.5.8
### 3.5.8
- Tagging improvements
- Support for external plugin integration
- Fixes [#304](https://github.com/acejump/AceJump/issues/304), [#255](https://github.com/acejump/AceJump/issues/255)
## 3.5.7
### 3.5.7
- <kbd>Tab</kbd>/<kbd>Enter</kbd> will now scroll horizontally if results are not visible.
- Fixes [#294](https://github.com/acejump/AceJump/issues/294) "Access is allowed from event dispatch thread only" error
## 3.5.6
### 3.5.6
- Key prioritization for most common keyboard layouts and fixes for a number of minor issues.
- Fixes: Index OOB [#242](https://github.com/acejump/AceJump/issues/242), Missing editor [#249](https://github.com/acejump/AceJump/issues/249), [#275](https://github.com/acejump/AceJump/issues/275), Forgotten block caret [#278](https://github.com/acejump/AceJump/issues/278), QWERTZ layout [#273](https://github.com/acejump/AceJump/issues/273)
## 3.5.5
### 3.5.5
- <kbd>Enter</kbd> will now escape exit from AceJump when there is a single visible tag. [#274](https://github.com/acejump/AceJump/issues/274)
- <kbd>Shift</kbd>+<kbd>Tab</kbd> to scroll to previous occurrences now works properly. [#179](https://github.com/acejump/AceJump/issues/179)
- Fixes an error with sticky block caret mode. [#269](https://github.com/acejump/AceJump/issues/269)
## 3.5.4
### 3.5.4
- Introduces cyclical selection: press Enter or Shift + Enter to cycle through tags on the screen. Press Escape to return to the editor.
## 3.5.3
### 3.5.3
- Fixes for two regressions affecting caret color and shift-selection.
## 3.5.2
### 3.5.2
- Various improvements to settings page, including a keyboard layout selector.
- Shorter tags on average, AceJump tries to use a single-character tag more often.
- Tag characters are now prioritized by user-defined order from the settings page.
- Fixes an issue when running the plugin on platform version 2018.3 and above.
## 3.5.1
### 3.5.1
- Now supports searching for CaPiTaLiZeD letters (typing capital letters in the query will force a case-sensitive search).
- **Declaration Mode**: Press the AceJump shortcut a second time to activate Declaration Mode, which will jump to the declaration of a variable in the editor.
- Keep hitting the AceJump shortcut to cycle between modes (default, declaration, target, disabled).
- Bug fix: AceJump settings should now properly persist after restarting the IDE.
## 3.5.0
### 3.5.0
- Adds two new features. "**Word-Mode**" and quick tag selection.
- **Word Mode** removes search and addresses latency issues raised in [#161](https://github.com/acejump/AceJump/issues/161). To learn more about **Word Mode**, see the [readme](https://github.com/johnlindquist/AceJump#tips).
- Pressing <kbd>Enter</kbd> during a search will jump to the next visible match (or closest match, if next is not visible), as per [#133](https://github.com/acejump/AceJump/issues/133).
## 3.4.3
### 3.4.3
- Stability improvements and tagging optimizations. Fixes [#206](https://github.com/acejump/AceJump/issues/206), [#202](https://github.com/acejump/AceJump/issues/202).
## 3.4.2
### 3.4.2
- Fixes [a regression](https://github.com/johnlindquist/AceJump/issues/197) affecting older platform versions.
## 3.4.1
### 3.4.1
- Fixes a regression affecting tag alignment when line spacing is greater than 1.0. Minor speed improvements.
## 3.4.0
### 3.4.0
- Restores original scroll position if tab search cancelled. Minor improvements to latency and tag painting.
## 3.3.6
### 3.3.6
- Fix for [#129](https://github.com/acejump/AceJump/issues/129).
## 3.3.5
### 3.3.5
- Minor bugfix release. Improve handling of window resizing.
## 3.3.4
### 3.3.4
- Add a settings page. (Settings > Tools > AceJump)
## 3.3.3
### 3.3.3
- Improve latency and fix a bug in line selection mode.
## 3.3.2
### 3.3.2
- AceJump now persists target mode state when scrolling or tabbing.
## 3.3.1
### 3.3.1
- Fixes a minor regression where tags are not displaying correctly.
## 3.3.0
### 3.3.0
- AceJump now searches the entire document. Press TAB to get the next set of results!
## 3.2.7
### 3.2.7
- Minor fixes and stability improvements.
## 3.2.6
### 3.2.6
- Fixes an error affecting older versions of the IntelliJ Platform.
## 3.2.5
### 3.2.5
- AceJump 3 now supports older IntelliJ Platform and Kotlin versions.
## 3.2.4
### 3.2.4
- Tagging improvements (tags now shorter on average) and visual updates.
## 3.2.3
### 3.2.3
- Fixes a critical issue affecting users with multiple editor windows open.
## 3.2.2
### 3.2.2
- Adds scrolling support and fixes some line spacing issues.
## 3.2.1
### 3.2.1
- AceJump now synchronizes font style changes in real-time.
## 3.2.0
### 3.2.0
- Support Back/Forward navigation in the IntelliJ Platform.
## 3.1.8
### 3.1.8
- Fixes some errors that occur when the user closes an editor prematurely.
## 3.1.6
### 3.1.6
- Fixes a rare tag collision scenario and UninitializedPropertyAccess exception
## 3.1.5
### 3.1.5
- Allow users to enter target mode directly by pressing Ctrl+Alt+;
## 3.1.4
### 3.1.4
- Fixes the "Assertion Failed" exception popup
## 3.1.3
### 3.1.3
- Fixes an error affecting some users during startup.
## 3.1.2
### 3.1.2
- Fixes an Android Studio regression.
## 3.1.1
### 3.1.1
- Hotfix for broken target mode.
## 3.1.0
### 3.1.0
- Removes the search box, lots of small usability improvements.
## 3.0.7
### 3.0.7
- No longer tags "folded" regions and minor alignment adjustments.
## 3.0.6
### 3.0.6
- Fixes alignment issues, removes top and bottom alignments until there is a better way to visually differentiate adjacent tags.
## 3.0.5
### 3.0.5
- Hotfix for target mode.
## 3.0.4
### 3.0.4
- Adds *Line Mode* - press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>;</kbd> to activate.
## 3.0.3
### 3.0.3
- Updates to tag placement and performance improvements.
## 3.0.2
### 3.0.2
- Fixes target mode and default shortcut activation for Mac users.
## 3.0.1
### 3.0.1
- Fixes target-mode issues affecting users with non-default shortcuts and adds support for Home/End.
## 3.0.0
### 3.0.0
- Major rewrite of AceJump. Introducing:
* Realtime search: Just type the word where you want to jump and AceJump will do the rest.
@@ -338,82 +235,82 @@
* Keyboard-aware tagging: Tries to minimize finger travel distance on QWERTY keyboards.
* Colorful highlighting: AceJump will now highlight the editor text, as you type.
## 2.0.13
### 2.0.13
- Fix a regression affecting *Target Mode* and line-based navigation.
## 2.0.12
### 2.0.12
- Fix ClassCastException when input letter not present: [#73](https://github.com/acejump/AceJump/issues/73)
## 2.0.11
### 2.0.11
- One hundred percent all natural Kotlin.
## 2.0.10
### 2.0.10
- Support 2016.2, remove upper version limit, update internal Kotlin version
## 2.0.9
### 2.0.9
- Compile on Java 7 to address: [#61](https://github.com/acejump/AceJump/issues/61)
## 2.0.8
### 2.0.8
- Compile on Java 6 to address: [#59](https://github.com/acejump/AceJump/issues/59)
## 2.0.7
### 2.0.7
- Language update for Kotlin 1.0 release.
## 2.0.6
### 2.0.6
- Fixing "lost focus" bugs mentioned here: [#41](https://github.com/acejump/AceJump/issues/41)
## 2.0.5
### 2.0.5
- Fixing "backspace" bugs mentioned here: [#20](https://github.com/acejump/AceJump/issues/20)
## 2.0.4
### 2.0.4
- Fixing "code folding" bugs mentioned here: [#24](https://github.com/acejump/AceJump/issues/24)
## 2.0.3
### 2.0.3
- More work on Ubuntu focus bug
## 2.0.2
### 2.0.2
- Fixed bug when there's only 1 search result
## 2.0.1
### 2.0.1
- Fixing Ubuntu focus bug
## 2.0.0
### 2.0.0
- Major release: Added "target mode", many speed increases, multi-char search implemented
## 1.1.0
### 1.1.0
- Switching to Kotlin for the code base
## 1.0.4
### 1.0.4
- Fixing [#9](https://github.com/acejump/AceJump/issues/9) and [#6](https://github.com/acejump/AceJump/issues/6)
## 1.0.3
### 1.0.3
- Fixed minor visual lag when removing the "jumpers" from the editor
## 1.0.2
### 1.0.2
- Cleaning up minor bugs (npe when editor not in focus, not removing layers)
## 1.0.1
### 1.0.1
- Adding a new jump: "Enter" will take you to the first non-whitespace char in a new line (compare to "Home" which takes you to a new line)
## 1.0.0
### 1.0.0
- Cleaned up code base for release

114
README.md
View File

@@ -1,7 +1,7 @@
<p align="center"><a href="https://plugins.jetbrains.com/plugin/7086"> <img src="logo.png" alt="AceJumpLogo"></a></p>
<p align="center">
<a href="https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub" title="JetBrains on GitHub"><img src="https://jb.gg/badges/team.svg"></a>
<a href="https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub" title="JetBrains on GitHub"><img src="http://jb.gg/badges/team.svg"></a>
<a href="https://teamcity.jetbrains.com/viewType.html?buildTypeId=acejump_buildplugin&guest=1" title="Build Plugin"><img src="https://teamcity.jetbrains.com/app/rest/builds/buildType:acejump_buildplugin/statusIcon.svg"></a>
<a href="https://plugins.jetbrains.com/plugin/7086-acejump" title="Jetbrains Plugin"><img src="https://img.shields.io/jetbrains/plugin/v/7086-acejump.svg"></a>
<a href="LICENSE" title="License"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
@@ -24,6 +24,8 @@ Press the AceJump shortcut, followed by <kbd>→</kbd> to target the last, <kbd>
![](https://cloud.githubusercontent.com/assets/175716/20177472/4f0ba956-a74d-11e6-97ba-b296eacdd396.png)
AceJump search is [smart case](http://ideavim.sourceforge.net/vim/usr_27.html#vim.27%2E1) sensitive, however tag selection is *not* case sensitive. Holding down <kbd>Shift</kbd> when typing the last tag character will select all text from the current cursor position to that destination.
## Tips
- Press <kbd>Tab</kbd> when searching to jump to the next group of matches in the editor.
@@ -32,10 +34,6 @@ Press the AceJump shortcut, followed by <kbd>→</kbd> to target the last, <kbd>
- If no matches can be found on-screen, AceJump will scroll to the next match it can find.
- Note that search is [smart case](http://ideavim.sourceforge.net/vim/usr_27.html#vim.27%2E1) sensitive, however tag selection is *not* case sensitive.
- Holding down <kbd>Shift</kbd> when typing the last tag character will select all text from the current cursor position to that destination.
- Pressing <kbd>Enter</kbd> or <kbd>Shift</kbd>+<kbd>Enter</kbd> during a search will cycle through tagged results on screen.
- To select a location and continue editing, just press <kbd>Esc</kbd>.
@@ -56,6 +54,8 @@ Press the AceJump shortcut, followed by <kbd>→</kbd> to target the last, <kbd>
AceJump can be [installed directly from the IDE](https://www.jetbrains.com/help/idea/managing-plugins.html#install), via **Settings | Plugins | Browse Repositories... | 🔍 "AceJump"**.
[Canary builds](https://teamcity.jetbrains.com/repository/download/acejump_buildplugin/.lastSuccessful/AceJump.zip?guest=1) are provided courtesy of [TeamCity](https://www.jetbrains.com/teamcity/). These can be downloaded and [installed from disk](https://www.jetbrains.com/help/idea/managing-plugins.html#install_plugin_from_disk).
## Configuring
[IdeaVim](https://plugins.jetbrains.com/plugin/164) users can choose to activate AceJump with a single keystroke (<kbd>f</kbd>, <kbd>F</kbd> and <kbd>g</kbd> are arbitrary) by running:
@@ -64,25 +64,15 @@ AceJump can be [installed directly from the IDE](https://www.jetbrains.com/help/
echo -e '
" Press `f` to activate AceJump
map f <Action>(AceAction)
map f :action AceAction<CR>
" Press `F` to activate Target Mode
map F <Action>(AceTargetAction)
map F :action AceTargetAction<CR>
" Press `g` to activate Line Mode
map g <Action>(AceLineAction)
map g :action AceLineAction<CR>
' >> ~/.ideavimrc
```
To customize AceJump's behavior further with additional actions, see the `<action>` tags in [plugin.xml](src/main/resources/META-INF/plugin.xml). The following example shows how to activate AceJump before or after the caret.
```
" Press `S` in normal mode to activate AceJump mode before the caret
nmap S <Action>(AceBackwardAction)
" Press `s` in normal mode to activate AceJump mode after the caret
nmap s <Action>(AceForwardAction)
```
To change the default keyboard shortcuts, open **File \| Settings \| Keymap \| 🔍 "AceJump" \| AceJump \|** <kbd>Enter⏎</kbd>.
![Keymap](https://cloud.githubusercontent.com/assets/175716/11760350/911aed4c-a065-11e5-8f17-49bc97ad1dad.png)
@@ -101,56 +91,21 @@ The build artifact will be placed in `build/distributions/`.
*Miscellaneous: AceJump is built using [Gradle](https://gradle.com/) with the [Gradle Kotlin DSL](https://docs.gradle.org/5.1/userguide/kotlin_dsl.html) and the [gradle-intellij-plugin](https://github.com/JetBrains/gradle-intellij-plugin).*
## Extending
AceJump can be used by other [IntelliJ Platform](https://plugins.jetbrains.com/docs/intellij/welcome.html) plugins. To do so, add the following snippet to your `build.gradle.kts` file:
```kotlin
intellij {
plugins.set("AceJump:<LATEST_VERSION>")
}
```
Callers who pass an instance of [`Editor`](https://github.com/JetBrains/intellij-community/blob/master/platform/editor-ui-api/src/com/intellij/openapi/editor/Editor.java) into `SessionManager.start(editor)` will receive a [`Session`](src/main/kotlin/org/acejump/session/Session.kt) instance in return. Sessions are disposed after use.
To use AceJump externally, please see the following example:
```kotlin
import org.acejump.session.SessionManager
import org.acejump.session.AceJumpListener
import org.acejump.boundaries.StandardBoundaries.*
import org.acejump.search.Pattern.*
val aceJumpSession = SessionManager.start(editorInstance)
aceJumpSession.addAceJumpListener(object: AceJumpListener {
override fun finished() {
// ...
}
})
// Sessions provide these endpoints for external consumers:
/*1.*/ aceJumpSession.markResults(sortedSetOf(/*...*/)) // Pass a set of offsets
/*2.*/ aceJumpSession.startRegexSearch("[aeiou]+", WHOLE_FILE) // Search for regex
/*3.*/ aceJumpSession.startRegexSearch(ALL_WORDS, VISIBLE_ON_SCREEN) // Search for Pattern
```
Custom boundaries for search (i.e. current line before caret etc.) can also be defined using the [Boundaries](src/main/kotlin/org/acejump/boundaries/Boundaries.kt) interface.
## Contributing
AceJump is supported by community members like you. Contributions are highly welcome!
If you would like to [contribute](https://github.com/acejump/AceJump/pulls?q=is%3Apr), here are a few of the ways you can help improve AceJump:
If you would like to [contribute](https://github.com/acejump/AceJump/pulls), here are a few of the ways you can help improve AceJump:
* [Improve test coverage](https://github.com/acejump/AceJump/issues/139)
* [Add action to repeat last search](https://github.com/acejump/AceJump/issues/316)
* [Add configurable RegEx modes](https://github.com/acejump/AceJump/issues/215)
* [Add font family and size options](https://github.com/acejump/AceJump/issues/192)
* [Tag placement and visibility improvements](https://github.com/acejump/AceJump/issues/323)
* [Add option to place the caret after the search text](https://github.com/acejump/AceJump/issues/225)
* [Support user-configurable keyboard layouts](https://github.com/acejump/AceJump/issues/172)
* [Speed up tagging on large files](https://github.com/acejump/AceJump/issues/217)
* [Animated documentation](https://github.com/acejump/AceJump/issues/145)
* [Display current search text](https://github.com/acejump/AceJump/issues/227)
* [Support for full screen tagging](https://github.com/acejump/AceJump/issues/144)
* [Fold text between matches](https://github.com/acejump/AceJump/issues/255)
* [Multi-platform support](https://github.com/acejump/AceJump/issues/229)
To start [IntelliJ IDEA CE](https://github.com/JetBrains/intellij-community) with AceJump installed, run `./gradlew runIde -PluginDev [-x test]`.
@@ -173,43 +128,33 @@ AceJump is inspired by prior work, but adds several improvements, including:
* **Line Mode**: Jump to the first, last, or first non-whitespace character of any line on-screen (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>;</kbd>).
* **Word Mode**: Jump to the first character of any visible word on-screen in two keystrokes or less.
* **Declaration Mode**: Jump to the declaration of a token (if it is available) rather than the token itself.
* **Unicode support**: Unicode search and selection, e.g. to search for "拼音", activate AceJump and type: <kbd>p</kbd><kbd>y</kbd>
* **Pinyin support**: Pinyin search and selection, e.g. to search for "拼音", activate AceJump and type: <kbd>p</kbd><kbd>y</kbd>
The following plugins have a similar UI for navigating text and web browsing:
| Source Code | Download | Application | Actively Maintained | Language |
|:----------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------:|:-------------------:|:------------------------------------------------------------------------:|
|:----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|:-------------------:|:------------------------------------------------------------------------:|
| AceJump | [](https://plugins.jetbrains.com/plugin/7086-acejump) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
| [IdeaVim-EasyMotion](https://github.com/AlexPl292/IdeaVim-EasyMotion) | [](https://github.com/AlexPl292/IdeaVim-EasyMotion) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
| [KJump](https://github.com/a690700752/KJump) | [](https://plugins.jetbrains.com/plugin/10149-kjump) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
| [AceJump-Lite](https://github.com/EeeMt/AceJump-Lite) | [](https://plugins.jetbrains.com/plugin/9803-acejump-lite) | [IntelliJ Platform](https://jetbrains.com) | :x: | [Java](https://www.java.com) |
| [emacsIDEAs](https://github.com/whunmr/emacsIDEAs) | [](https://plugins.jetbrains.com/plugin/7163-emacsideas) | [IntelliJ Platform](https://jetbrains.com) | :x: | [Java](https://www.java.com) |
| [TraceJump](https://github.com/acejump/tracejump) | [](https://github.com/acejump/tracejump) | Desktop | :x: | [Kotlin](http://kotlinlang.org/) |
| [AceJump-Lite](https://github.com/EeeMt/AceJump-Lite) | [](https://plugins.jetbrains.com/plugin/9803-acejump-lite) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Java](https://www.java.com) |
| [KJump](https://github.com/a690700752/KJump) | [](https://plugins.jetbrains.com/plugin/10149-kjump) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Java](https://www.java.com) |
| [emacsIDEAs](https://github.com/whunmr/emacsIDEAs) | [](https://plugins.jetbrains.com/plugin/7163-emacsideas) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Java](https://www.java.com) |
| [TraceJump](https://github.com/acejump/tracejump) | [](https://github.com/acejump/tracejump) | Desktop | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
| [ace-jump-mode](https://github.com/winterTTr/ace-jump-mode) | [](https://melpa.org/#/ace-jump-mode) | [emacs](https://www.gnu.org/software/emacs/) | :x: | [Emacs Lisp](https://www.gnu.org/software/emacs/manual/eintr.html) |
| [avy](https://github.com/abo-abo/avy) | [](https://melpa.org/#/avy) | [emacs](https://www.gnu.org/software/emacs/) | :heavy_check_mark: | [Emacs Lisp](https://www.gnu.org/software/emacs/manual/eintr.html) |
| [EasyMotion](https://github.com/easymotion/vim-easymotion) | [](https://vimawesome.com/plugin/easymotion) | [Vim](http://www.vim.org/) | :x: | [Vimscript](http://learnvimscriptthehardway.stevelosh.com/) |
| [eyeliner.nvim](https://github.com/jinh0/eyeliner.nvim) | [](https://github.com/jinh0/eyeliner.nvim?tab=readme-ov-file#-installation) | [NeoVim](https://neovim.io/) | :heavy_check_mark: | [Lua](https://www.lua.org/) |
| [Hop](https://github.com/phaazon/hop.nvim) | [](https://github.com/phaazon/hop.nvim#installation) | [NeoVim](https://neovim.io/) | :heavy_check_mark: | [Lua](https://www.lua.org/) |
| [leap.nvim](https://github.com/ggandor/leap.nvim) | [](https://github.com/ggandor/leap.nvim#installation) | [NeoVim](https://neovim.io/) | :heavy_check_mark: | [Fennel](https://fennel-lang.org) |
| [lightspeed.nvim](https://github.com/ggandor/lightspeed.nvim) | [](https://github.com/ggandor/lightspeed.nvim#installation) | [NeoVim](https://neovim.io/) | :x: | [Fennel](https://fennel-lang.org) |
| [Sublime EasyMotion](https://github.com/tednaleid/sublime-EasyMotion) | [](https://packagecontrol.io/packages/EasyMotion) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.org/) |
| [AceJump](https://github.com/ice9js/ace-jump-sublime) | [](https://packagecontrol.io/packages/AceJump) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.org/) |
| [Jumpy](https://github.com/DavidLGoldberg/jumpy) | [](https://atom.io/packages/jumpy) | [Atom](https://atom.io/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [Jumpy2](https://github.com/DavidLGoldberg/jumpy2) | [](https://marketplace.visualstudio.com/items?itemName=DavidLGoldberg.jumpy2) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [AceJump](https://github.com/ice9js/ace-jump-sublime) | [](https://packagecontrol.io/packages/AceJump) | [Sublime](https://www.sublimetext.com/) | :heavy_check_mark: | [Python](https://www.python.org/) |
| [Jumpy](https://github.com/DavidLGoldberg/jumpy) | [](https://atom.io/packages/jumpy) | [Atom](https://atom.io/) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.org/) |
| [Find-Jump](https://github.com/msafi/xvsc/tree/master/findJump) | [](https://marketplace.visualstudio.com/items?itemName=mksafi.find-jump) | [Visual Studio Code](https://code.visualstudio.com/) | :x: | [TypeScript](https://www.typescriptlang.org/) |
| [MetaGo](https://github.com/metaseed/metaGo) | [](https://marketplace.visualstudio.com/items?itemName=metaseed.metago) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [VSCodeVim](https://github.com/VSCodeVim/Vim) | [](https://marketplace.visualstudio.com/items?itemName=vscodevim.vim) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [CodeAceJumper](https://github.com/lucax88x/CodeAceJumper) | [](https://marketplace.visualstudio.com/items?itemName=lucax88x.codeacejumper) | [Visual Studio Code](https://code.visualstudio.com/) | :x: | [TypeScript](https://www.typescriptlang.org/) |
| [CodeAceJumper](https://github.com/lucax88x/CodeAceJumper) | [](https://marketplace.visualstudio.com/items?itemName=lucax88x.codeacejumper) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [AceJump](https://github.com/jsturtevant/ace-jump) | [](https://marketplace.visualstudio.com/items?itemName=jsturtevant.AceJump) | [Visual Studio](https://www.visualstudio.com/) | :x: | [C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/) |
| [EasyMotion](https://github.com/jaredpar/EasyMotion) | [](https://marketplace.visualstudio.com/items?itemName=JaredParMSFT.EasyMotion) | [Visual Studio](https://www.visualstudio.com/) | :x: | [C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/) |
| [tmux-fingers](https://github.com/Morantron/tmux-fingers) | [](https://github.com/Morantron/tmux-fingers#using-tmux-plugin-manager) | [tmux](https://github.com/tmux/tmux) | :heavy_check_mark: | [Crystal](https://crystal-lang.org/) |
| [tmux-thumb](https://github.com/Morantron/tmux-fingers) | [](https://github.com/fcsonline/tmux-thumbs#using-tmux-plugin-manager) | [tmux](https://github.com/tmux/tmux) | :heavy_check_mark: | [Rust](https://www.rust-lang.org/) |
| [tmux-jump](https://github.com/schasse/tmux-jump) | [](https://github.com/schasse/tmux-jump#installation-via-tpm) | [tmux](https://github.com/tmux/tmux) | :heavy_check_mark: | [Ruby](https://www.ruby-lang.org) |
| [tmux-copycat](https://github.com/tmux-plugins/tmux-copycat) | [](https://github.com/tmux-plugins/tmux-copycat?tab=readme-ov-file#installation-with-tmux-plugin-manager-recommended) | [tmux](https://github.com/tmux/tmux) | :x: | [Shell](https://www.shellscript.sh/) |
| [cVim](https://github.com/1995eaton/chromium-vim) | [](https://chrome.google.com/webstore/detail/cvim/ihlenndgcmojhcghmfjfneahoeklbjjh) | [Chrome](https://www.google.com/chrome) | :x: | [JavaScript](https://www.javascript.com/) |
| [SurfingKeys](https://github.com/brookhong/Surfingkeys) | [](https://chrome.google.com/webstore/detail/surfingkeys/gfbliohnnapiefjpjlpjnehglfpaknnc) | [Chrome](https://www.google.com/chrome)/[Firefox](https://www.mozilla.org/firefox)/[Edge](https://microsoftedge.microsoft.com/) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
| [Vimium](https://github.com/philc/vimium) | [](https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb) | [Chrome](https://www.google.com/chrome)/[Firefox](https://www.mozilla.org/firefox)/[Edge](https://microsoftedge.microsoft.com/) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
| [Vimium-C](https://github.com/gdh1995/vimium-c) | [](https://microsoftedge.microsoft.com/addons/detail/vimium-c-all-by-keyboar/aibcglbfblnogfjhbcmmpobjhnomhcdo) | [Chrome](https://www.google.com/chrome)/[Firefox](https://www.mozilla.org/firefox)/[Edge](https://microsoftedge.microsoft.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [cVim](https://github.com/1995eaton/chromium-vim) | [](https://chrome.google.com/webstore/detail/cvim/ihlenndgcmojhcghmfjfneahoeklbjjh) | [Chrome](https://www.google.com/chrome) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
| [SurfingKeys](https://github.com/brookhong/Surfingkeys) | [](https://chrome.google.com/webstore/detail/surfingkeys/gfbliohnnapiefjpjlpjnehglfpaknnc) | [Chrome](https://www.google.com/chrome) / [Firefox](https://www.mozilla.org/firefox) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
| [Vimium](https://github.com/philc/vimium) | [](https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb) | [Chrome](https://www.google.com/chrome) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.org/) |
| [Vrome](https://github.com/jinzhu/vrome) | [](https://chrome.google.com/webstore/detail/vrome/godjoomfiimiddapohpmfklhgmbfffjj) | [Chrome](https://www.google.com/chrome) | :x: | [CoffeeScript](http://coffeescript.org/) |
| [ViChrome](https://github.com/k2nr/ViChrome) | [](https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi) | [Chrome](https://www.google.com/chrome) | :x: | [CoffeeScript](http://coffeescript.org/) |
| [VimFx](https://github.com/akhodakivskiy/VimFx) | [](https://github.com/akhodakivskiy/VimFx/releases) | [Firefox](https://www.mozilla.org/firefox) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.org/) |
@@ -217,8 +162,7 @@ The following plugins have a similar UI for navigating text and web browsing:
| [Pentadactyl](https://github.com/5digits/dactyl) | [](http://bug.5digits.org/pentadactyl/#sect-download) | [Firefox](https://www.mozilla.org/firefox) | :x: | [JavaScript](https://www.javascript.com/) |
| [Vim Vixen](https://github.com/ueokande/vim-vixen) | [](https://addons.mozilla.org/firefox/addon/vim-vixen/) | [Firefox 57+](https://blog.mozilla.org/addons/2017/09/28/webextensions-in-firefox-57/) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
| [Tridactyl](https://github.com/tridactyl/tridactyl) | [](https://addons.mozilla.org/firefox/addon/tridactyl-vim/) | [Firefox 57+](https://blog.mozilla.org/addons/2017/09/28/webextensions-in-firefox-57/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [Vimari](https://github.com/guyht/vimari) | [](https://github.com/guyht/vimari/releases) | [Safari](https://www.apple.com/safari/) | :x: | [JavaScript](https://www.javascript.com/) |
| [Jump To Link](https://github.com/mrjackphil/obsidian-jump-to-link) | [](https://obsidian.md/plugins?id=mrj-jump-to-link) | [Obsidian](https://obsidian.md/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
| [Vimari](https://github.com/guyht/vimari) | [](https://github.com/guyht/vimari/releases) | [Safari](https://www.apple.com/safari/) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
## Acknowledgements
@@ -226,8 +170,8 @@ The following individuals have significantly improved AceJump through their cont
* [John Lindquist](https://github.com/johnlindquist) for creating AceJump and supporting it for many years.
* [Breandan Considine](https://github.com/breandan) for maintaining the project and adding some new features.
* [chylex](https://github.com/chylex) for numerous [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex), [bug fixes](https://github.com/acejump/AceJump/issues/348#issuecomment-739454920) and [refactoring](https://github.com/acejump/AceJump/pull/353).
* [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292).
* [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex).
* [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency.
* [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years.
* [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design.

View File

@@ -1,100 +1,54 @@
import org.jetbrains.changelog.Changelog.OutputType.HTML
import org.jetbrains.changelog.date
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
@file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
idea
alias(libs.plugins.kotlin) // Kotlin support
alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin
alias(libs.plugins.changelog) // Gradle Changelog Plugin
alias(libs.plugins.kover) // Gradle Kover Plugin
id("com.github.ben-manes.versions") version "0.51.0"
kotlin("jvm") version "1.9.10"
id("org.jetbrains.intellij") version "1.17.3"
}
tasks {
named<Zip>("buildPlugin") {
dependsOn("test")
archiveFileName = "AceJump.zip"
group = "org.acejump"
version = "chylex-26"
repositories {
mavenCentral()
}
runIde {
findProperty("luginDev")?.let { args = listOf(projectDir.absolutePath) }
}
intellij {
version.set("2024.2")
updateSinceUntilBuild.set(false)
publishPlugin {
val intellijPublishToken: String? by project
token = intellijPublishToken
}
plugins.add("IdeaVIM:chylex-41")
plugins.add("com.intellij.classic.ui:242.20224.159")
patchPluginXml {
sinceBuild = "223.7571.182"
changeNotes = provider {
changelog.renderItem(changelog.getAll().values.take(2).last(), HTML)
}
}
// Remove pending: https://youtrack.jetbrains.com/issue/IDEA-278926
val test by getting(Test::class) {
isScanForTestClasses = false
// Only run tests from classes that end with "Test"
include("**/AceTest.class")
include("**/ExternalUsageTest.class")
include("**/LatencyTest.class")
afterTest(
KotlinClosure2({ desc: TestDescriptor, result: TestResult ->
println("Completed `${desc.displayName}` in ${result.endTime - result.startTime}ms")
})
)
pluginsRepositories {
custom("https://intellij.chylex.com")
marketplace()
}
}
kotlin {
jvmToolchain(17)
sourceSets.all {
languageSettings.apply {
languageVersion = "2.0"
}
}
}
val acejumpVersion = "3.8.19"
changelog {
version = acejumpVersion
path = "${project.projectDir}/CHANGES.md"
header = provider { "[${project.version}] - ${date()}" }
itemPrefix = "-"
unreleasedTerm = "Unreleased"
}
repositories {
mavenCentral()
intellijPlatform.defaultRepositories()
// intellijPlatform.localPlatformArtifacts()
}
dependencies {
// https://github.com/anyascii/anyascii
implementation("com.anyascii:anyascii:0.3.2")
intellijPlatform{
testImplementation(libs.junit)
bundledPlugins("com.intellij.java")
create("IC", "2024.1.4")
pluginVerifier()
instrumentationTools()
testFramework(TestFrameworkType.Platform)
}
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
intellijPlatform {
pluginConfiguration {
version = acejumpVersion
name = "AceJump"
tasks.patchPluginXml {
sinceBuild.set("242")
}
pluginVerification.ides.recommended()
tasks.buildSearchableOptions {
enabled = false
}
group = "org.acejump"
version = acejumpVersion
tasks.test {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}

View File

@@ -1,4 +1 @@
kotlin.stdlib.default.dependency=false
kotlin.incremental.useClasspathSnapshot=false
org.gradle.jvmargs=-Xmx2048m

View File

@@ -1,18 +0,0 @@
[versions]
# libraries
junit = "4.13.2"
# plugins
changelog = "2.2.1"
intelliJPlatform = "2.0.0"
kotlin = "2.0.0"
kover = "0.8.3"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
[plugins]
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }

Binary file not shown.

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

294
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,99 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

11
gradlew.bat vendored
View File

@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -0,0 +1,13 @@
package org.acejump.action;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.VimEditor;
import com.maddyhome.idea.vim.state.mode.SelectionType;
final class AceVimUtil {
private AceVimUtil() {}
public static void enterVisualMode(final VimEditor vim, final SelectionType mode) {
VimPlugin.getVisualMotion().enterVisualMode(vim, mode);
}
}

View File

@@ -1,57 +1,42 @@
package org.acejump
import com.anyascii.AnyAscii
import com.intellij.diff.util.DiffUtil.getLineCount
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.*
import com.intellij.openapi.util.Computable
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.Project
import com.intellij.util.IncorrectOperationException
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.config.AceConfig
import java.awt.Point
import kotlin.math.*
/**
* This annotation is a marker which means that the annotated function is
* used in external plugins.
*/
@Retention(AnnotationRetention.SOURCE)
annotation class ExternalUsage
/**
* Returns an immutable version of the currently edited document.
*/
val Editor.immutableText get() = EditorsCache.getText(this)
val Editor.immutableText
get() = this.document.immutableCharSequence
object EditorsCache {
private var stale = true
fun invalidate() {
stale = true
editorTexts.clear()
/**
* Returns all open editors in the project.
*/
val Project.openEditors: List<Editor>
get() {
return try {
FileEditorManagerEx.getInstanceEx(this)
.splitters
.getSelectedEditors()
.mapNotNull { (it as? TextEditor)?.editor }
} catch (e: IncorrectOperationException) {
emptyList()
}
private val editorTexts = mutableMapOf<Editor, CharSequence>()
fun getText(editor: Editor) =
if (stale || editor !in editorTexts)
editor.document.immutableCharSequence
.let { if (AceConfig.mapToASCII) it.mapToASCII() else it }
.also { editorTexts[editor] = it; stale = false }
else editorTexts[editor]!!
}
fun CharSequence.mapToASCII() =
map { AnyAscii.transliterate("$it").firstOrNull() ?: it }.joinToString("")
/**
* Returns true if [this] contains [otherText] at the specified offset.
*/
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean) =
regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean): Boolean {
return this.regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
}
/**
* Calculates the length of a common prefix in [this] starting
* at index [selfOffset], and [otherText] starting at index 0.
* Calculates the length of a common prefix in [this] starting at index [selfOffset], and [otherText] starting at index 0.
*/
fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): Int {
var i = 0
@@ -65,54 +50,6 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
return i
}
/**
* Determines which characters form a "word" for the purposes of functions below.
*/
val Char.isWordPart
get() = this.isJavaIdentifierPart()
/**
* Finds index of the first character in a word.
*/
inline fun CharSequence.wordStart(
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var start = pos
while (start > 0 && isPartOfWord(this[start - 1])) --start
return start
}
/**
* Finds index of the last character in a word.
*/
inline fun CharSequence.wordEnd(
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var end = pos
while (end < length - 1 && isPartOfWord(this[end + 1])) ++end
return end
}
/**
* Finds index of the first word character following a sequence of non-word
* characters following the end of a word.
*/
inline fun CharSequence.wordEndPlus(
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var end = this.wordEnd(pos, isPartOfWord)
while (end < length - 1 && !isPartOfWord(this[end + 1])) ++end
if (end < length - 1 && isPartOfWord(this[end + 1])) ++end
return end
}
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
val clone = HashMap<Editor, IntArrayList>(size)
@@ -122,140 +59,3 @@ fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
return clone
}
fun Editor.offsetCenter(first: Int, second: Int): LogicalPosition {
val firstIndexLine = offsetToLogicalPosition(first).line
val lastIndexLine = offsetToLogicalPosition(second).line
val center = (firstIndexLine + lastIndexLine) / 2
return offsetToLogicalPosition(getLineStartOffset(center))
}
// Borrowed from Editor.calculateVisibleRange() but only available after 232.6095.10
fun Editor.getView(): IntRange {
ApplicationManager.getApplication().assertIsDispatchThread()
val rect = scrollingModel.visibleArea
val startPosition = xyToLogicalPosition(Point(rect.x, rect.y))
val visibleStart = logicalPositionToOffset(startPosition)
val endPosition = xyToLogicalPosition(Point(rect.x + rect.width, rect.y + rect.height))
val visibleEnd = logicalPositionToOffset(LogicalPosition(endPosition.line + 1, 0))
return visibleStart..visibleEnd
}
/**
* Returns the offset of the start of the requested line.
*
* @param line The logical line to get the start offset for.
*
* @return 0 if line is &lt 0, file size of line is bigger than file, else the
* start offset for the line
*/
fun Editor.getLineStartOffset(line: Int) =
when {
line < 0 -> 0
line >= getLineCount(document) -> getFileSize()
else -> document.getLineStartOffset(line)
}
/**
* Returns the offset of the end of the requested line.
*
* @param line The logical line to get the end offset for
*
* @param allowEnd True include newline
*
* @return 0 if line is &lt 0, file size of line is bigger than file, else the
* end offset for the line
*/
fun Editor.getLineEndOffset(line: Int, allowEnd: Boolean = true) =
when {
line < 0 -> 0
line >= getLineCount(document) -> getFileSize(allowEnd)
else -> document.getLineEndOffset(line) - if (allowEnd) 0 else 1
}
/**
* Gets the number of lines than can be displayed on the screen at one time.
* This is rounded down to the nearest whole line if there is a partial line
* visible at the bottom of the screen.
*
* @return The number of screen lines
*/
fun Editor.getScreenHeight() =
(scrollingModel.visibleArea.y + scrollingModel.visibleArea.height -
getVisualLineAtTopOfScreen() * lineHeight) / lineHeight
/**
* This is a set of helper methods for working with editors.
* All line and column values are zero based.
*/
fun Editor.getVisualLineAtTopOfScreen() =
(scrollingModel.verticalScrollOffset + lineHeight - 1) / lineHeight
/**
* Gets the actual number of characters in the file
*
* @param countNewLines True include newline
*
* @return The file's character count
*/
fun Editor.getFileSize(countNewLines: Boolean = false): Int {
val len = document.textLength
val doc = document.charsSequence
return if (countNewLines || len == 0 || doc[len - 1] != '\n') len else len - 1
}
/**
* Ensures that the supplied logical line is within the range 0 (incl) and the
* number of logical lines in the file (excl).
*
* @param line The logical line number to normalize
*
* @return The normalized logical line number
*/
fun Editor.normalizeLine(line: Int) = max(0, min(line, getLineCount(document) - 1))
/**
* Converts a visual line number to a logical line number.
*
* @param line The visual line number to convert
*
* @return The logical line number
*/
fun Editor.visualLineToLogicalLine(line: Int) =
normalizeLine(visualToLogicalPosition(
VisualPosition(line.coerceAtLeast(0), 0)).line)
/**
* Ensures that the supplied offset for the given logical line is within the
* range for the line. If allowEnd is true, the range will allow for the offset
* to be one past the last character on the line.
*
* @param line The logical line number
*
* @param offset The offset to normalize
*
* @param allowEnd true if the offset can be one past the last character on the
* line, false if not
*
* @return The normalized column number
*/
fun Editor.normalizeOffset(line: Int, offset: Int, allowEnd: Boolean = true) =
if (getFileSize(allowEnd) == 0) 0 else
max(min(offset, getLineEndOffset(line, allowEnd)), getLineStartOffset(line))
// https://plugins.jetbrains.com/docs/intellij/general-threading-rules.html#read-access
fun <T> read(action: () -> T): T =
ApplicationManager.getApplication().runReadAction(Computable { action() })
fun <T> write(action: () -> T): T =
ApplicationManager.getApplication().runWriteAction(Computable { action() })

View File

@@ -1,120 +0,0 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.actionSystem.PlatformDataKeys.LAST_ACTIVE_FILE_EDITOR
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.util.IncorrectOperationException
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
import org.acejump.input.JumpMode
import org.acejump.input.JumpMode.DECLARATION
import org.acejump.input.JumpMode.JUMP
import org.acejump.input.JumpMode.JUMP_END
import org.acejump.input.JumpMode.TARGET
import org.acejump.search.Pattern
import org.acejump.search.Pattern.ALL_WORDS
import org.acejump.search.Pattern.LINE_ALL_MARKS
import org.acejump.search.Pattern.LINE_ENDS
import org.acejump.search.Pattern.LINE_INDENTS
import org.acejump.search.Pattern.LINE_STARTS
import org.acejump.session.Session
import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated actions that create or update an AceJump [Session].
*/
sealed class AceAction: DumbAwareAction() {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled =
(action.getData(EDITOR) ?: (action.getData(LAST_ACTIVE_FILE_EDITOR) as? TextEditor)?.editor) != null
}
final override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(EDITOR) ?: (e.getData(LAST_ACTIVE_FILE_EDITOR) as? TextEditor)?.editor ?: return
val project = e.project
if (project != null) {
try {
val fem = FileEditorManager.getInstance(project) as FileEditorManagerEx
val openEditors = fem.splitters.getSelectedEditors()
.mapNotNull { (it as? TextEditor)?.editor }
.sortedBy { if (it === editor) 0 else 1 }
invoke(SessionManager.start(editor, openEditors))
} catch (e: IncorrectOperationException) {
invoke(SessionManager.start(editor))
}
} else {
invoke(SessionManager.start(editor))
}
}
abstract operator fun invoke(session: Session)
/**
* Generic action type that toggles a specific [JumpMode].
*/
abstract class BaseToggleJumpModeAction(private val mode: JumpMode): AceAction() {
final override fun invoke(session: Session) = session.toggleJumpMode(mode)
}
/**
* Generic action type that toggles a specific [JumpMode] with [Boundaries].
*/
abstract class BaseToggleBoundedJumpModeAction(private val mode: JumpMode, private val boundaries: Boundaries): AceAction() {
final override fun invoke(session: Session) = session.toggleJumpMode(mode, boundaries)
}
/**
* Generic action type that starts a regex search.
*/
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries): AceAction() {
override fun invoke(session: Session) = session.startRegexSearch(pattern, boundaries)
}
/**
* Initiates an AceJump session in the first [JumpMode], or cycles to the next [JumpMode] as defined in configuration.
*/
class ActivateOrCycleMode: AceAction() {
override fun invoke(session: Session) = session.cycleNextJumpMode()
}
/**
* Initiates an AceJump session in the last [JumpMode], or cycles to the previous [JumpMode] as defined in configuration.
*/
class ActivateOrReverseCycleMode: AceAction() {
override fun invoke(session: Session) = session.cyclePreviousJumpMode()
}
// @formatter:off
// Unbounded Toggle Modes
class ToggleJumpMode : BaseToggleJumpModeAction(JUMP)
class ToggleJumpEndMode : BaseToggleJumpModeAction(JUMP_END)
class ToggleTargetMode : BaseToggleJumpModeAction(TARGET)
class ToggleDeclarationMode : BaseToggleJumpModeAction(DECLARATION)
// Bounded Toggle Modes
class ToggleBackwardJumpMode : BaseToggleBoundedJumpModeAction(JUMP, BEFORE_CARET)
class ToggleForwardJumpMode : BaseToggleBoundedJumpModeAction(JUMP, AFTER_CARET)
// Regex Modes
class StartAllWordsMode : BaseRegexSearchAction(ALL_WORDS, WHOLE_FILE)
class StartAllWordsBackwardsMode : BaseRegexSearchAction(ALL_WORDS, BEFORE_CARET)
class StartAllWordsForwardMode : BaseRegexSearchAction(ALL_WORDS, AFTER_CARET)
class StartAllLineStartsMode : BaseRegexSearchAction(LINE_STARTS, WHOLE_FILE)
class StartAllLineEndsMode : BaseRegexSearchAction(LINE_ENDS, WHOLE_FILE)
class StartAllLineIndentsMode : BaseRegexSearchAction(LINE_INDENTS, WHOLE_FILE)
class StartAllLineMarksMode : BaseRegexSearchAction(LINE_ALL_MARKS, WHOLE_FILE)
// @formatter:on
}

View File

@@ -4,25 +4,27 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries.*
import org.acejump.search.Pattern.*
import org.acejump.session.Session
import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
*/
sealed class AceEditorAction(private val originalHandler: EditorActionHandler): EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean =
SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
abstract class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
}
final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val session = SessionManager[editor]
if (session != null) run(session)
else if (originalHandler.isEnabled(editor, caret, dataContext))
if (session != null) {
run(session)
}
else if (originalHandler.isEnabled(editor, caret, dataContext)) {
originalHandler.execute(editor, caret, dataContext)
}
}
protected abstract fun run(session: Session)
@@ -36,31 +38,7 @@ sealed class AceEditorAction(private val originalHandler: EditorActionHandler):
override fun run(session: Session) = session.restart()
}
class SelectBackward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitPreviousTag()
}
class SelectForward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitNextTag()
}
class ScrollToNextScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) { session.scrollToNextScreenful() }
}
class ScrollToPreviousScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) { session.scrollToPreviousScreenful() }
}
class SearchLineStarts(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(LINE_STARTS, WHOLE_FILE)
}
class SearchLineEnds(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(LINE_ENDS, WHOLE_FILE)
}
class SearchLineIndents(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(LINE_INDENTS, WHOLE_FILE)
class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.tagImmediately()
}
}

View File

@@ -0,0 +1,99 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import org.acejump.search.Tag
/**
* Base class for actions available after typing a tag.
*/
sealed class AceTagAction {
abstract operator fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean)
abstract class BaseJumpAction : AceTagAction() {
override fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) {
val editor = tag.editor
val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
editor.project?.let { addCurrentPositionToHistory(it, editor.document) }
if (isFinal) {
ensureEditorFocused(editor)
}
moveCaretTo(editor, tag.offset)
if (shiftMode) {
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
}
}
private companion object {
fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
selectionModel.removeSelection(true)
caretModel.removeSecondaryCarets()
caretModel.moveToOffset(offset)
}
fun performAction(actionName: String) {
val actionManager = ActionManager.getInstance()
val action = actionManager.getAction(actionName)
if (action != null) {
actionManager.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
}
fun ensureEditorFocused(editor: Editor) {
val project = editor.project ?: return
val fem = FileEditorManagerEx.getInstanceEx(project)
val window = fem.windows.firstOrNull { (it.selectedComposite?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
if (window != null && window !== fem.currentWindow) {
fem.currentWindow = window
}
}
private fun addCurrentPositionToHistory(project: Project, document: Document) {
CommandProcessor.getInstance().executeCommand(project, {
with(IdeDocumentHistory.getInstance(project)) {
setCurrentCommandHasMoves()
includeCurrentCommandAsNavigation()
includeCurrentPlaceAsChangePlace()
}
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document), UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document)
}
}
// Actions
/**
* On default action, places the caret at the first character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpToSearchStart : BaseJumpAction()
/**
* On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
* On shift action, performs the Go To Type Declaration action, available via `Navigate | Type Declaration`.
* Always places the caret at the start of the word.
*/
object GoToDeclaration : AceTagAction() {
override fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) {
JumpToSearchStart.invoke(tag, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_GOTO_TYPE_DECLARATION else IdeActions.ACTION_GOTO_DECLARATION) }
}
}
}

View File

@@ -0,0 +1,189 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.project.DumbAwareAction
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.action.change.change.ChangeVisualAction
import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction
import com.maddyhome.idea.vim.action.copy.YankVisualAction
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.vimSetSelection
import com.maddyhome.idea.vim.helper.inVisualMode
import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode.OP_PENDING
import com.maddyhome.idea.vim.state.mode.SelectionType
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
import org.acejump.boundaries.StandardBoundaries.CARET_LINE
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.modes.JumpMode
import org.acejump.search.Pattern
import org.acejump.search.Tag
import org.acejump.session.SessionManager
import org.acejump.session.SessionState
sealed class AceVimAction : DumbAwareAction() {
protected abstract val mode: AceVimMode
final override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val context = e.dataContext.vim
val caret = editor.caretModel.currentCaret
val initialOffset = caret.offset
val selectionStart = if (editor.inVisualMode) caret.vimSelectionStart else null
val session = SessionManager.start(editor, mode.getJumpEditors(editor))
session.defaultBoundary = mode.boundaries
session.startJumpMode {
object : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag) {
AceTagAction.JumpToSearchStart.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true)
if (selectionStart != null) {
caret.vim.vimSetSelection(selectionStart, caret.offset, moveCaretToSelectionEnd = true)
}
else {
val vim = editor.vim
if (vim.mode is OP_PENDING) {
val keyHandler = KeyHandler.getInstance()
val key = keyHandler.keyHandlerState.commandBuilder.keys.singleOrNull()?.keyChar
keyHandler.fullReset(vim)
AceVimUtil.enterVisualMode(vim, SelectionType.CHARACTER_WISE)
caret.vim.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true)
val action = when (key) {
'd' -> DeleteVisualAction()
'c' -> ChangeVisualAction()
'y' -> YankVisualAction()
else -> null
}
if (action != null) {
ApplicationManager.getApplication().invokeLater {
WriteAction.run<Nothing> {
keyHandler.keyHandlerState.commandBuilder.addAction(action)
val cmd = keyHandler.keyHandlerState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(vim.mode is OP_PENDING, cmd.rawCount, injector.vimState.mode)
injector.vimState.executingCommand = cmd
injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments)
}
keyHandler.reset(vim)
}
}
}
}
injector.scroll.scrollCaretIntoView(editor.vim)
mode.finishSession(editor, session)
}
}
}
mode.setupSession(editor, session)
}
class JumpAllEditors : AceVimAction() {
override val mode = AceVimMode.JumpAllEditors
}
class JumpForward : AceVimAction() {
override val mode = AceVimMode.Jump(AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpBackward : AceVimAction() {
override val mode = AceVimMode.Jump(BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpTillForward : AceVimAction() {
override val mode = AceVimMode.JumpTillForward(AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpTillBackward : AceVimAction() {
override val mode = AceVimMode.JumpTillBackward(BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpOnLineForward : AceVimAction() {
override val mode = AceVimMode.Jump(AFTER_CARET.intersection(CARET_LINE))
}
class JumpOnLineBackward : AceVimAction() {
override val mode = AceVimMode.Jump(BEFORE_CARET.intersection(CARET_LINE))
}
class JumpLineIndentsForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLineIndentsBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordEndForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordEndForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordEndBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordEndBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpAllEditorsGoToDeclaration : DumbAwareAction() {
override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(CommonDataKeys.EDITOR) != null
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val session = SessionManager.start(editor, AceVimMode.JumpAllEditors.getJumpEditors(editor))
session.defaultBoundary = VISIBLE_ON_SCREEN
session.startJumpMode {
object : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag) {
AceTagAction.GoToDeclaration.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true)
}
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
package org.acejump.action
import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.openEditors
import org.acejump.search.Pattern
import org.acejump.session.Session
sealed class AceVimMode {
abstract val boundaries: Boundaries
open fun getJumpEditors(mainEditor: Editor): List<Editor> {
return listOf(mainEditor)
}
open fun setupSession(editor: Editor, session: Session) {}
open fun finishSession(editor: Editor, session: Session) {}
class Jump(override val boundaries: Boundaries) : AceVimMode()
object JumpAllEditors : AceVimMode() {
override val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
override fun getJumpEditors(mainEditor: Editor): List<Editor> {
val project = mainEditor.project ?: return super.getJumpEditors(mainEditor)
return project.openEditors
.sortedBy { if (it === mainEditor) 0 else 1 }
.ifEmpty { listOf(mainEditor) }
}
}
class JumpTillForward(override val boundaries: Boundaries) : AceVimMode() {
override fun finishSession(editor: Editor, session: Session) {
val document = editor.document
for (caret in editor.caretModel.allCarets) {
val offset = caret.offset
if (offset > document.getLineStartOffset(document.getLineNumber(offset))) {
caret.moveToOffset(offset - 1, false)
}
}
}
}
class JumpTillBackward(override val boundaries: Boundaries) : AceVimMode() {
override fun finishSession(editor: Editor, session: Session) {
val document = editor.document
for (caret in editor.caretModel.allCarets) {
val offset = caret.offset
if (offset < document.getLineEndOffset(document.getLineNumber(offset))) {
caret.moveToOffset(offset + 1, false)
}
}
}
}
class JumpToPattern(private val pattern: Pattern, override val boundaries: Boundaries) : AceVimMode() {
override fun setupSession(editor: Editor, session: Session) {
session.startRegexSearch(pattern)
}
}
}

View File

@@ -1,134 +0,0 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions.ACTION_GOTO_DECLARATION
import com.intellij.openapi.actionSystem.IdeActions.ACTION_GOTO_TYPE_DECLARATION
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import org.acejump.input.JumpMode
import org.acejump.input.JumpMode.DECLARATION
import org.acejump.input.JumpMode.JUMP_END
import org.acejump.input.JumpMode.TARGET
import org.acejump.isWordPart
import org.acejump.search.SearchProcessor
import org.acejump.search.Tag
import org.acejump.wordEnd
import org.acejump.wordStart
/**
* Performs [JumpMode] navigation and actions.
*/
internal class TagJumper(private val mode: JumpMode, private val searchProcessor: SearchProcessor?) {
/**
* Moves caret to a specific offset in the editor according to the positioning and selection rules of the current [JumpMode].
*/
fun visit(tag: Tag) {
val editor = tag.editor
val offset = tag.offset
if (mode === JUMP_END || mode === TARGET) {
val chars = editor.immutableText
val matchingChars = searchProcessor?.let { chars.countMatchingCharacters(offset, it.query.rawText) } ?: 0
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && chars[targetOffset - 1].isWordPart && chars[targetOffset].isWordPart
val finalTargetOffset = if (isInsideWord) chars.wordEnd(targetOffset) + 1 else targetOffset
if (mode === JUMP_END) {
moveCaretTo(editor, finalTargetOffset)
} else if (mode === TARGET) {
if (isInsideWord) {
selectRange(editor, chars.wordStart(targetOffset), finalTargetOffset)
} else {
selectRange(editor, offset, finalTargetOffset)
}
}
} else {
moveCaretTo(editor, offset)
}
}
/**
* Updates caret and selection by [visit]ing a specific offset in the editor, and applying session-finalizing [JumpMode] actions such as
* using the Go To Declaration action, or selecting text between caret and target offset/word if Shift was held during the jump.
*/
fun jump(tag: Tag, shiftMode: Boolean, isCrossEditor: Boolean) {
val editor = tag.editor
val oldOffset = editor.caretModel.offset
visit(tag)
if (mode === DECLARATION) {
performAction(ActionManager.getInstance().getAction(if (shiftMode) ACTION_GOTO_TYPE_DECLARATION else ACTION_GOTO_DECLARATION))
return
}
if (shiftMode && !isCrossEditor) {
val newOffset = editor.caretModel.offset
if (mode === TARGET) {
selectRange(
editor, oldOffset, when {
newOffset < oldOffset -> editor.selectionModel.selectionStart
else -> editor.selectionModel.selectionEnd
}
)
} else {
selectRange(editor, oldOffset, newOffset)
}
}
}
private companion object {
private fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
ensureEditorFocused(this)
project?.let { addCurrentPositionToHistory(it, document) }
selectionModel.removeSelection(true)
caretModel.moveToOffset(offset)
}
private fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int) = with(editor) {
ensureEditorFocused(this)
selectionModel.removeSelection(true)
selectionModel.setSelection(fromOffset, toOffset)
caretModel.moveToOffset(toOffset)
}
private fun ensureEditorFocused(editor: Editor) {
val project = editor.project ?: return
val fem = FileEditorManager.getInstance(project) as FileEditorManagerEx
val window = fem.windows.firstOrNull { (it.getSelectedComposite(false)?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
if (window != null && window !== fem.currentWindow) {
fem.currentWindow = window
}
}
private fun addCurrentPositionToHistory(project: Project, document: Document) {
CommandProcessor.getInstance().executeCommand(
project, {
with(IdeDocumentHistory.getInstance(project)) {
setCurrentCommandHasMoves()
includeCurrentCommandAsNavigation()
includeCurrentPlaceAsChangePlace()
}
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document),
UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document
)
}
private fun performAction(action: AnAction) = ActionManager.getInstance()
.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
}

View File

@@ -1,64 +0,0 @@
package org.acejump.action
import com.intellij.openapi.editor.*
import org.acejump.*
import org.acejump.search.SearchProcessor
internal class TagScroller(private val editor: Editor, private val searchProcessor: SearchProcessor) {
fun scroll(
forward: Boolean = true,
position: LogicalPosition? = if (forward) findNextPosition() else findPreviousPosition()
) = if (position != null) true.also { scrollTo(position) } else false
private fun scrollTo(position: LogicalPosition) = editor.run {
scrollingModel.disableAnimation()
scrollingModel.scrollTo(position, ScrollType.CENTER)
val firstInView = textMatches.first { it in editor.getView() }
val horizontalOffset = offsetToLogicalPosition(firstInView).column
if (horizontalOffset > scrollingModel.visibleArea.width)
scrollingModel.scrollHorizontally(horizontalOffset)
}
val textMatches by lazy { searchProcessor.results[editor]!! }
private fun findPreviousPosition(): LogicalPosition? {
val prevIndex = textMatches.toList().dropLastWhile { it > editor.getView().first }
.lastOrNull() ?: textMatches.lastOrNull() ?: return null
val prevLineNum = editor.offsetToLogicalPosition(prevIndex).line
// Try to capture as many previous results as will fit in a screenful
fun maximizeCoverageOfPreviousOccurrence(): LogicalPosition {
val minVisibleLine = prevLineNum - editor.getScreenHeight()
val firstVisibleIndex = editor.getLineStartOffset(minVisibleLine)
val firstIndex = textMatches.dropWhile { it < firstVisibleIndex }.first()
return editor.offsetCenter(firstIndex, prevIndex)
}
return maximizeCoverageOfPreviousOccurrence()
}
/**
* Returns the center of the next set of results that will fit in the editor.
* [textMatches] must be sorted prior to using Scroller. If [textMatches] have
* not previously been sorted, the result of calling this method is undefined.
*/
private fun findNextPosition(): LogicalPosition? {
val nextIndex = textMatches.dropWhile { it <= editor.getView().last }
.firstOrNull() ?: textMatches.firstOrNull() ?: return null
val nextLineNum = editor.offsetToLogicalPosition(nextIndex).line
// Try to capture as many subsequent results as will fit in a screenful
fun maximizeCoverageOfNextOccurrence(): LogicalPosition {
val maxVisibleLine = nextLineNum + editor.getScreenHeight()
val lastVisibleIndex = editor.getLineEndOffset(maxVisibleLine, true)
val lastIndex = textMatches.toList().dropLastWhile { it > lastVisibleIndex }.last()
return editor.offsetCenter(nextIndex, lastIndex)
}
return maximizeCoverageOfNextOccurrence()
}
}

View File

@@ -1,69 +0,0 @@
package org.acejump.action
import com.intellij.openapi.editor.*
import com.intellij.openapi.editor.ScrollType.*
import org.acejump.search.SearchProcessor
import org.acejump.search.Tag
import kotlin.math.abs
/**
* Enables navigation between currently active tags.
*/
internal class TagVisitor(private val editor: Editor, private val searchProcessor: SearchProcessor, private val tagJumper: TagJumper) {
/**
* Places caret at the closest tag following the caret position, according
* to the rules of the current jump mode (see [TagJumper.visit]).
* If the caret is at or past the last tag, it moves to the first tag instead.
* If there is only one tag, it immediately performs the jump action as described in [TagJumper.jump].
*/
fun visitNext(): Boolean =
visit(SelectionModel::getSelectionEnd) { if (it < 0) -it - 1 else it + 1 }
/**
* Places caret at the closest tag preceding the caret position, according to
* the rules of the current jump mode (see [TagJumper.visit]).
* If the caret is at or before the first tag, it moves to the last tag instead.
* If there is only one tag, it immediately performs the jump action as described in [TagJumper.jump].
*/
fun visitPrevious(): Boolean =
visit(SelectionModel::getSelectionStart) { if (it < 0) -it - 2 else it - 1 }
/**
* Scrolls to the closest result to the caret.
*/
fun scrollToClosest() {
val caret = editor.caretModel.offset
val results = searchProcessor.results[editor].takeUnless { it.isNullOrEmpty() } ?: return
val index = results.binarySearch(caret).let { if (it < 0) -it - 1 else it }
val targetOffset = listOfNotNull(
results.getOrNull(index - 1),
results.getOrNull(index)
).minByOrNull { abs(it - caret) }
if (targetOffset != null)
editor.scrollingModel.scrollTo(editor.offsetToLogicalPosition(targetOffset), RELATIVE)
}
private inline fun visit(caretPosition: SelectionModel.() -> Int, indexModifier: (Int) -> Int): Boolean {
val results = searchProcessor.results[editor].takeUnless { it.isNullOrEmpty() } ?: return false
val nextIndex = indexModifier(results.binarySearch(caretPosition(editor.selectionModel)))
val targetOffset = results.getInt(
when {
nextIndex < 0 -> results.lastIndex
nextIndex > results.lastIndex -> 0
else -> nextIndex
}
)
val onlyResult = results.size == 1
if (onlyResult) tagJumper.jump(Tag(editor, targetOffset), shiftMode = false, isCrossEditor = false)
else tagJumper.visit(Tag(editor, targetOffset))
editor.scrollingModel.scrollToCaret(RELATIVE)
return onlyResult
}
}

View File

@@ -5,40 +5,40 @@ import kotlin.math.max
import kotlin.math.min
/**
* Defines a (possibly) disjoint set of editor offsets that partitions
* the whole editor into two groups - offsets inside the range, and
* Defines a (possibly) disjoint set of editor offsets that partitions the whole editor into two groups - offsets inside the range, and
* offsets outside the range.
*/
interface Boundaries {
/**
* Returns a range of editor offsets, starting at the first offset in the
* boundary, and ending at the last offset in the boundary. May include
* offsets outside the boundary, for ex. when the boundary is rectangular
* and the file has long lines which are only partially visible.
* Returns a range of editor offsets, starting at the first offset in the boundary, and ending at the last offset in the boundary.
* May include offsets outside the boundary, for ex. when the boundary is rectangular and the file has long lines which are only
* partially visible.
*/
fun getOffsetRange(editor: Editor, cache: EditorOffsetCache = EditorOffsetCache.Uncached): IntRange =
StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
fun getOffsetRange(editor: Editor, cache: EditorOffsetCache = EditorOffsetCache.Uncached): IntRange
/**
* Returns whether the editor offset is included within the boundary.
*/
fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache = EditorOffsetCache.Uncached): Boolean =
StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, offset, cache)
fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache = EditorOffsetCache.Uncached): Boolean
/**
* Creates a boundary so that an offset/range is within the boundary
* iff it is within both original boundaries.
* Creates a boundary so that an offset/range is within the boundary iff it is within both original boundaries.
*/
fun intersection(other: Boundaries): Boundaries =
if (this === other) this
else object: Boundaries {
fun intersection(other: Boundaries): Boundaries {
if (this === other) {
return this
}
return object : Boundaries {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val b1 = this@Boundaries.getOffsetRange(editor, cache)
val b2 = other.getOffsetRange(editor, cache)
return max(b1.first, b2.first)..min(b1.last, b2.last)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
this@Boundaries.isOffsetInside(editor, offset, cache) && other.isOffsetInside(editor, offset, cache)
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return this@Boundaries.isOffsetInside(editor, offset, cache) && other.isOffsetInside(editor, offset, cache)
}
}
}
}

View File

@@ -3,17 +3,14 @@ package org.acejump.boundaries
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.acejump.read
import java.awt.Point
/**
* Optionally caches slow operations of (1) retrieving the currently
* visible editor area, and (2) converting between editor offsets and
* Optionally caches slow operations of (1) retrieving the currently visible editor area, and (2) converting between editor offsets and
* pixel coordinates.
*
* To avoid unnecessary overhead, there is no automatic detection of when
* the editor, its contents, or its visible area has changed, so the cache
* must only be used for a single rendered frame of a single [Editor].
* To avoid unnecessary overhead, there is no automatic detection of when the editor, its contents, or its visible area has changed, so the
* cache must only be used for a single rendered frame of a single [Editor].
*/
sealed class EditorOffsetCache {
/**
@@ -36,17 +33,21 @@ sealed class EditorOffsetCache {
*/
abstract fun offsetToXY(editor: Editor, offset: Int): Point
companion object { fun new(): EditorOffsetCache = Cache() }
companion object {
fun new(): EditorOffsetCache {
return Cache()
}
}
private class Cache : EditorOffsetCache() {
private var visibleArea: Pair<Point, Point>? = null
private val lineToVisibleOffsetRange = Int2ObjectOpenHashMap<IntRange>()
private val pointToOffset =
Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
override fun visibleArea(editor: Editor): Pair<Point, Point> =
visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
override fun visibleArea(editor: Editor): Pair<Point, Point> {
return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
}
override fun isVisible(editor: Editor, offset: Int): Boolean {
val visualLine = editor.offsetToVisualLine(offset, false)
@@ -66,25 +67,38 @@ sealed class EditorOffsetCache {
return offset in visibleRange
}
override fun xyToOffset(editor: Editor, pos: Point): Int =
pointToOffset.getInt(pos).let { offset ->
if (offset != -1) offset
else Uncached.xyToOffset(editor, pos)
.also { pointToOffset.put(pos, it) }
override fun xyToOffset(editor: Editor, pos: Point): Int {
val offset = pointToOffset.getInt(pos)
if (offset != -1) {
return offset
}
override fun offsetToXY(editor: Editor, offset: Int) =
offsetToPoint.get(offset) ?: Uncached.offsetToXY(editor, offset)
.also { offsetToPoint.put(offset, it) }
return Uncached.xyToOffset(editor, pos).also {
pointToOffset.put(pos, it)
}
}
override fun offsetToXY(editor: Editor, offset: Int): Point {
val pos = offsetToPoint.get(offset)
if (pos != null) {
return pos
}
return Uncached.offsetToXY(editor, offset).also {
offsetToPoint.put(offset, it)
}
}
}
object Uncached : EditorOffsetCache() {
override fun visibleArea(editor: Editor): Pair<Point, Point> =
editor.scrollingModel.visibleArea.let { visibleRect ->
Pair(
visibleRect.location, visibleRect.location.apply {
translate(visibleRect.width, visibleRect.height)
}
override fun visibleArea(editor: Editor): Pair<Point, Point> {
val visibleRect = editor.scrollingModel.visibleArea
return Pair(
visibleRect.location,
visibleRect.location.apply { translate(visibleRect.width, visibleRect.height) }
)
}
@@ -97,10 +111,12 @@ sealed class EditorOffsetCache {
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
}
override fun xyToOffset(editor: Editor, pos: Point): Int =
read { editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos)) }
override fun xyToOffset(editor: Editor, pos: Point): Int {
return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
}
override fun offsetToXY(editor: Editor, offset: Int): Point =
editor.offsetToXY(offset, true, false)
override fun offsetToXY(editor: Editor, offset: Int): Point {
return editor.offsetToXY(offset, true, false)
}
}
}

View File

@@ -3,14 +3,6 @@ package org.acejump.boundaries
import com.intellij.openapi.editor.Editor
enum class StandardBoundaries : Boundaries {
WHOLE_FILE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
0..editor.document.textLength
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache) =
offset in (0..editor.document.textLength)
},
VISIBLE_ON_SCREEN {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val (topLeft, bottomRight) = cache.visibleArea(editor)
@@ -26,18 +18,35 @@ enum class StandardBoundaries : Boundaries {
},
BEFORE_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
0..(editor.caretModel.offset)
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0 until editor.caretModel.offset
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
offset <= editor.caretModel.offset
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset < editor.caretModel.offset
}
},
AFTER_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
editor.caretModel.offset until editor.document.textLength
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return (editor.caretModel.offset + 1) until editor.document.textLength
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
offset >= editor.caretModel.offset
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset > editor.caretModel.offset
}
},
CARET_LINE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val document = editor.document
val offset = editor.caretModel.offset
val line = document.getLineNumber(offset)
return document.getLineStartOffset(line)..document.getLineEndOffset(line)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in getOffsetRange(editor, cache)
}
}
}

View File

@@ -1,14 +1,13 @@
package org.acejump.config
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.application
import org.acejump.input.KeyLayoutCache
/**
* Ensures consistency between [AceSettings] and [AceSettingsPanel].
* Persists the state of the AceJump IDE settings across IDE restarts.
* Ensures consistiency between [AceSettings] and [AceSettingsPanel]. Persists the state of the AceJump IDE settings across IDE restarts.
* [https://www.jetbrains.org/intellij/sdk/docs/basics/persisting_state_of_components.html]
*/
@State(name = "AceConfig", storages = [(Storage("\$APP_CONFIG\$/AceJump.xml"))])
@@ -16,26 +15,22 @@ class AceConfig: PersistentStateComponent<AceSettings> {
private var aceSettings = AceSettings()
companion object {
val settings get() = application.getService(AceConfig::class.java).aceSettings
val settings
get() = ServiceManager.getService(AceConfig::class.java).aceSettings
// @formatter:off
val layout get() = settings.layout
val cycleModes get() = settings.let { arrayOf(it.cycleMode1, it.cycleMode2, it.cycleMode3, it.cycleMode4) }
val minQueryLength get() = settings.minQueryLength
val jumpModeColor get() = settings.getJumpModeJBC()
val jumpEndModeColor get() = settings.getJumpEndModeJBC()
val targetModeColor get() = settings.getTargetModeJBC()
val definitionModeColor get() = settings.getDefinitionModeJBC()
val textHighlightColor get() = settings.getTextHighlightJBC()
val tagForegroundColor get() = settings.getTagForegroundJBC()
val tagBackgroundColor get() = settings.getTagBackgroundJBC()
val searchWholeFile get() = settings.searchWholeFile
val mapToASCII get() = settings.mapToASCII
val showSearchNotification get() = settings.showSearchNotification
// @formatter:on
val invertUppercaseMode get() = settings.invertUppercaseMode
val editorFadeOpacity get() = settings.editorFadeOpacity
val jumpModeColor get() = settings.jumpModeColor
val tagForegroundColor1 get() = settings.tagForegroundColor1
val tagForegroundColor2 get() = settings.tagForegroundColor2
val searchHighlightColor get() = settings.searchHighlightColor
}
override fun getState() = aceSettings
override fun getState(): AceSettings {
return aceSettings
}
override fun loadState(state: AceSettings) {
aceSettings = state

View File

@@ -14,40 +14,24 @@ class AceConfigurable: Configurable {
override fun isModified() =
panel.allowedChars != settings.allowedChars ||
panel.keyboardLayout != settings.layout ||
panel.cycleMode1 != settings.cycleMode1 ||
panel.cycleMode2 != settings.cycleMode2 ||
panel.cycleMode3 != settings.cycleMode3 ||
panel.cycleMode4 != settings.cycleMode4 ||
panel.minQueryLengthInt != settings.minQueryLength ||
panel.jumpModeColor?.rgb != settings.jumpModeColor ||
panel.jumpEndModeColor?.rgb != settings.jumpEndModeColor ||
panel.targetModeColor?.rgb != settings.targetModeColor ||
panel.definitionModeColor?.rgb != settings.definitionModeColor ||
panel.textHighlightColor?.rgb != settings.textHighlightColor ||
panel.tagForegroundColor?.rgb != settings.tagForegroundColor ||
panel.tagBackgroundColor?.rgb != settings.tagBackgroundColor ||
panel.searchWholeFile != settings.searchWholeFile ||
panel.mapToASCII != settings.mapToASCII ||
panel.showSearchNotification != settings.showSearchNotification
panel.invertUppercaseMode != settings.invertUppercaseMode ||
panel.editorFadeOpacityPercent != settings.editorFadeOpacity ||
panel.jumpModeColor != settings.jumpModeColor ||
panel.tagForegroundColor1 != settings.tagForegroundColor1 ||
panel.tagForegroundColor2 != settings.tagForegroundColor2 ||
panel.searchHighlightColor != settings.searchHighlightColor
override fun apply() {
settings.allowedChars = panel.allowedChars
settings.layout = panel.keyboardLayout
settings.cycleMode1 = panel.cycleMode1
settings.cycleMode2 = panel.cycleMode2
settings.cycleMode3 = panel.cycleMode3
settings.cycleMode4 = panel.cycleMode4
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
panel.jumpModeColor?.let { settings.jumpModeColor = it.rgb }
panel.jumpEndModeColor?.let { settings.jumpEndModeColor = it.rgb }
panel.targetModeColor?.let { settings.targetModeColor = it.rgb }
panel.definitionModeColor?.let { settings.definitionModeColor = it.rgb }
panel.textHighlightColor?.let { settings.textHighlightColor = it.rgb }
panel.tagForegroundColor?.let { settings.tagForegroundColor = it.rgb }
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it.rgb }
settings.searchWholeFile = panel.searchWholeFile
settings.mapToASCII = panel.mapToASCII
settings.showSearchNotification = panel.showSearchNotification
settings.invertUppercaseMode = panel.invertUppercaseMode
settings.editorFadeOpacity = panel.editorFadeOpacityPercent
panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.tagForegroundColor1?.let { settings.tagForegroundColor1 = it }
panel.tagForegroundColor2?.let { settings.tagForegroundColor2 = it }
panel.searchHighlightColor?.let { settings.searchHighlightColor = it }
KeyLayoutCache.reset(settings)
}

View File

@@ -1,43 +1,26 @@
package org.acejump.config
import com.intellij.ui.JBColor
import org.acejump.input.*
import com.intellij.util.xmlb.annotations.OptionTag
import org.acejump.input.KeyLayout
import org.acejump.input.KeyLayout.QWERTY
import java.awt.Color
data class AceSettings(
var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars,
var cycleMode1: JumpMode = JumpMode.JUMP,
var cycleMode2: JumpMode = JumpMode.DECLARATION,
var cycleMode3: JumpMode = JumpMode.TARGET,
var cycleMode4: JumpMode = JumpMode.JUMP_END,
var invertUppercaseMode: Boolean = false,
var minQueryLength: Int = 1,
var editorFadeOpacity: Int = 70,
var jumpModeColor: Int = 0xFFFFFF,
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF),
var jumpEndModeColor: Int = 0x33E78A,
@OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var tagForegroundColor1: Color = Color(0xFFFFFF),
var targetModeColor: Int = 0xFFB700,
@OptionTag("tagForeground2RGB", converter = ColorConverter::class)
var tagForegroundColor2: Color = Color(0xFFFFFF),
var definitionModeColor: Int = 0x6FC5FF,
var textHighlightColor: Int = 0x394B58,
var tagForegroundColor: Int = 0xFFFFFF,
var tagBackgroundColor: Int = 0x008299,
var searchWholeFile: Boolean = true,
var mapToASCII: Boolean = false,
var showSearchNotification: Boolean = false
) {
fun getJumpModeJBC() = JBColor.namedColor("jumpModeRGB", jumpModeColor)
fun getJumpEndModeJBC() = JBColor.namedColor("jumpEndModeRGB", jumpEndModeColor)
fun getTargetModeJBC() = JBColor.namedColor("targetModeRGB", targetModeColor)
fun getDefinitionModeJBC() = JBColor.namedColor("definitionModeRGB", definitionModeColor)
fun getTextHighlightJBC() = JBColor.namedColor("textHighlightRGB", textHighlightColor)
fun getTagForegroundJBC() = JBColor.namedColor("tagForegroundRGB", tagForegroundColor)
fun getTagBackgroundJBC() = JBColor.namedColor("tagBackgroundRGB", tagBackgroundColor)
}
@OptionTag("searchHighlightRGB", converter = ColorConverter::class)
var searchHighlightColor: Color = Color(0x008299),
)

View File

@@ -1,12 +1,24 @@
package org.acejump.config
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.*
import com.intellij.ui.dsl.builder.*
import org.acejump.input.JumpMode
import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBSlider
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.COLUMNS_LARGE
import com.intellij.ui.dsl.builder.COLUMNS_SHORT
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.panel
import org.acejump.input.KeyLayout
import java.awt.*
import javax.swing.*
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.util.Hashtable
import javax.swing.JCheckBox
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JSlider
import javax.swing.text.JTextComponent
import kotlin.reflect.KProperty
@@ -15,119 +27,90 @@ import kotlin.reflect.KProperty
*/
@Suppress("UsePropertyAccessSyntax")
internal class AceSettingsPanel {
private val defaults = AceSettings()
private val tagCharsField = JBTextField()
private val tagAllowedCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val cycleModeCombo1 = ComboBox<JumpMode>()
private val cycleModeCombo2 = ComboBox<JumpMode>()
private val cycleModeCombo3 = ComboBox<JumpMode>()
private val cycleModeCombo4 = ComboBox<JumpMode>()
private val minQueryLengthField = JBTextField()
private val jumpModeColorWheel = ResettableColorPicker(defaults.getJumpModeJBC())
private val jumpEndModeColorWheel = ResettableColorPicker(defaults.getJumpEndModeJBC())
private val targetModeColorWheel = ResettableColorPicker(defaults.getTargetModeJBC())
private val definitionModeColorWheel = ResettableColorPicker(defaults.getDefinitionModeJBC())
private val textHighlightColorWheel = ResettableColorPicker(defaults.getTextHighlightJBC())
private val tagForegroundColorWheel = ResettableColorPicker(defaults.getTagForegroundJBC())
private val tagBackgroundColorWheel = ResettableColorPicker(defaults.getTagBackgroundJBC())
private val searchWholeFileCheckBox = JBCheckBox()
private val mapToASCIICheckBox = JBCheckBox()
private val showSearchNotificationCheckBox = JBCheckBox()
private val invertUppercaseModeCheckBox = JBCheckBox("Invert uppercase mode")
private val editorFadeOpacitySlider = JBSlider(0, 10).apply {
labelTable = Hashtable((0..10).associateWith { JLabel("${it * 10}") })
paintTrack = true
paintLabels = true
paintTicks = true
minorTickSpacing = 1
majorTickSpacing = 1
minimumSize = Dimension(275, minimumSize.height)
}
private val jumpModeColorWheel = ColorPanel()
private val tagForeground1ColorWheel = ColorPanel()
private val tagForeground2ColorWheel = ColorPanel()
private val searchHighlightColorWheel = ColorPanel()
init {
tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
tagAllowedCharsField.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") }
cycleModeCombo1.setupEnumItems { cycleMode1 = it }
cycleModeCombo2.setupEnumItems { cycleMode2 = it }
cycleModeCombo3.setupEnumItems { cycleMode3 = it }
cycleModeCombo4.setupEnumItems { cycleMode4 = it }
}
internal val rootPanel: JPanel = panel {
group("Characters and Layout") {
row("Allowed characters in tags:") { cell(tagCharsField).columns(COLUMNS_LARGE) }
row("Allowed characters in tags:") { cell(tagAllowedCharsField).columns(COLUMNS_LARGE) }
row("Keyboard layout:") { cell(keyboardLayoutCombo).columns(COLUMNS_SHORT) }
row("Keyboard design:") { cell(keyboardLayoutArea).columns(COLUMNS_SHORT) }
}
group("Modes") {
row("Cycle order:") {
cell(cycleModeCombo1).columns(10)
cell(cycleModeCombo2).columns(10)
cell(cycleModeCombo3).columns(10)
cell(cycleModeCombo4).columns(10)
}
group("Behavior") {
row("Minimum typed characters (1-10):") { cell(minQueryLengthField).columns(COLUMNS_SHORT) }
row { cell(invertUppercaseModeCheckBox) }
}
group("Colors") {
row("Jump mode caret background:") { cell(jumpModeColorWheel) }
row("Jump to End mode caret background:") { cell(jumpEndModeColorWheel) }
row("Target mode caret background:") { cell(targetModeColorWheel) }
row("Definition mode caret background:") { cell(definitionModeColorWheel) }
row("Searched text background:") { cell(textHighlightColorWheel) }
row("Tag foreground:") { cell(tagForegroundColorWheel) }
row("Tag background:") { cell(tagBackgroundColorWheel) }
row("Caret background:") {
cell(jumpModeColorWheel)
}
group("Behavior") {
row { cell(searchWholeFileCheckBox.apply { text = "Search whole file" }) }
row("Minimum typed characters (1-10):") { cell(minQueryLengthField) }
row("Tag foreground:") {
cell(tagForeground1ColorWheel)
cell(tagForeground2ColorWheel)
}
group("Language Settings") {
row { cell(mapToASCIICheckBox.apply { text = "Map unicode to ASCII" }) }
row("Search highlight:") {
cell(searchHighlightColorWheel)
}
row("Editor fade opacity (%):") {
cell(editorFadeOpacitySlider)
}
group("Visual") {
row { cell(showSearchNotificationCheckBox.apply { text = "Show hint with search text" }) }
}
}
// Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342
internal var allowedChars by tagCharsField
internal var allowedChars by tagAllowedCharsField
internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea
internal var cycleMode1 by cycleModeCombo1
internal var cycleMode2 by cycleModeCombo2
internal var cycleMode3 by cycleModeCombo3
internal var cycleMode4 by cycleModeCombo4
internal var minQueryLength by minQueryLengthField
internal var invertUppercaseMode by invertUppercaseModeCheckBox
internal var editorFadeOpacity by editorFadeOpacitySlider
internal var jumpModeColor by jumpModeColorWheel
internal var jumpEndModeColor by jumpEndModeColorWheel
internal var targetModeColor by targetModeColorWheel
internal var definitionModeColor by definitionModeColorWheel
internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel
internal var searchWholeFile by searchWholeFileCheckBox
internal var mapToASCII by mapToASCIICheckBox
internal var showSearchNotification by showSearchNotificationCheckBox
internal var tagForegroundColor1 by tagForeground1ColorWheel
internal var tagForegroundColor2 by tagForeground2ColorWheel
internal var searchHighlightColor by searchHighlightColorWheel
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) {
minQueryLength = value.toString()
}
set(value) { minQueryLength = value.toString() }
internal var editorFadeOpacityPercent
get() = editorFadeOpacity * 10
set(value) { editorFadeOpacity = value / 10 }
fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars
keyboardLayout = settings.layout
cycleMode1 = settings.cycleMode1
cycleMode2 = settings.cycleMode2
cycleMode3 = settings.cycleMode3
cycleMode4 = settings.cycleMode4
minQueryLength = settings.minQueryLength.toString()
jumpModeColor = settings.getJumpModeJBC()
jumpEndModeColor = settings.getJumpEndModeJBC()
targetModeColor = settings.getTargetModeJBC()
definitionModeColor = settings.getDefinitionModeJBC()
textHighlightColor = settings.getTextHighlightJBC()
tagForegroundColor = settings.getTagForegroundJBC()
tagBackgroundColor = settings.getTagBackgroundJBC()
searchWholeFile = settings.searchWholeFile
mapToASCII = settings.mapToASCII
showSearchNotification = settings.showSearchNotification
invertUppercaseMode = settings.invertUppercaseMode
editorFadeOpacityPercent = settings.editorFadeOpacity
jumpModeColor = settings.jumpModeColor
tagForegroundColor1 = settings.tagForegroundColor1
tagForegroundColor2 = settings.tagForegroundColor2
searchHighlightColor = settings.searchHighlightColor
}
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
@@ -135,13 +118,16 @@ internal class AceSettingsPanel {
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.lowercase()
private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s)
private operator fun ResettableColorPicker.getValue(a: AceSettingsPanel, p: KProperty<*>) = getSelectedColor()
private operator fun ResettableColorPicker.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c)
private operator fun ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor
private operator fun ColorPanel.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c)
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
private operator fun JCheckBox.setValue(a: AceSettingsPanel, p: KProperty<*>, selected: Boolean) = setSelected(selected)
private inline operator fun <reified T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private operator fun JSlider.getValue(a: AceSettingsPanel, p: KProperty<*>) = value
private operator fun JSlider.setValue(a: AceSettingsPanel, p: KProperty<*>, value: Int) = setValue(value)
private operator fun <T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)
private inline fun <reified T : Enum<T>> ComboBox<T>.setupEnumItems(crossinline onChanged: (T) -> Unit) {

View File

@@ -0,0 +1,14 @@
package org.acejump.config
import com.intellij.util.xmlb.Converter
import java.awt.Color
internal class ColorConverter : Converter<Color>() {
override fun toString(value: Color): String {
return value.rgb.toString()
}
override fun fromString(value: String): Color? {
return value.toIntOrNull()?.let(::Color)
}
}

View File

@@ -1,47 +0,0 @@
package org.acejump.config
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.ui.ColorPanel
import com.intellij.ui.JBColor
import java.awt.*
import javax.swing.*
internal class ResettableColorPicker(private val defaultColor: JBColor) : JPanel(FlowLayout()) {
private val resetAction = object : AnAction({ "Reset to Default" }, AllIcons.General.Reset) {
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = colorPanel.selectedColor != defaultColor
}
override fun actionPerformed(e: AnActionEvent) {
setSelectedColor(defaultColor)
}
}
private val colorPanel = ColorPanel()
private val resetButton = ActionButton(resetAction, null, ActionPlaces.UNKNOWN, ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE)
init {
add(colorPanel)
add(resetButton)
setSelectedColor(defaultColor)
colorPanel.addActionListener {
resetButton.update()
}
}
fun getSelectedColor(): Color? {
return colorPanel.selectedColor
}
fun setSelectedColor(color: Color?) {
colorPanel.selectedColor = color
resetButton.update()
}
}

View File

@@ -6,12 +6,10 @@ import com.intellij.openapi.editor.actionSystem.TypedAction
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
/**
* If at least one session exists, this listener redirects all characters
* typed in [Editor]s with attached sessions to the appropriate sessions'
* own handlers.
* If at least one session exists, this listener redirects all characters, typed in [Editor]s with attached sessions, to the appropriate
* sessions' own handlers.
*/
internal object EditorKeyListener : TypedActionHandler {
private val action = TypedAction.getInstance()
private val attached = mutableMapOf<Editor, TypedActionHandler>()
private var originalHandler: TypedActionHandler? = null
@@ -21,8 +19,9 @@ internal object EditorKeyListener: TypedActionHandler {
fun attach(editor: Editor, callback: TypedActionHandler) {
if (attached.isEmpty()) {
originalHandler = action.rawHandler
action.setupRawHandler(this)
val typedAction = TypedAction.getInstance()
originalHandler = typedAction.rawHandler
typedAction.setupRawHandler(this)
}
attached[editor] = callback
@@ -32,7 +31,7 @@ internal object EditorKeyListener: TypedActionHandler {
attached.remove(editor)
if (attached.isEmpty()) {
originalHandler?.let(action::setupRawHandler)
originalHandler?.let(TypedAction.getInstance()::setupRawHandler)
originalHandler = null
}
}

View File

@@ -1,79 +0,0 @@
package org.acejump.input
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import org.acejump.config.AceConfig
import java.awt.Color
/**
* Describes modes that determine the behavior of a "jump" to a tag.
* Most modes have two variations:
* - **Default jump** happens when jumping without holding the Shift key
* - **Shift jump** happens when jumping while holding the Shift key
*/
enum class JumpMode {
/**
* Default value at the start of a session. If the session does not get
* assigned a proper [JumpMode] by the time the user requests a jump, the
* results of the jump are undefined.
*/
DISABLED,
/**
* On default jump, places the caret at the first character of the search
* query. On shift jump, does the above but also selects all text between
* the original and new caret positions.
*/
JUMP,
/**
* On default jump, places the caret at the end of a word. Word detection
* uses [Character.isJavaIdentifierPart] to count some special characters,
* such as underscores, as part of a word. If there is no word at the first
* character of the search query, then the caret is placed after the last
* character of the search query.
*
* On shift jump, does the above but also selects all text between
* the original and new caret positions.
*/
JUMP_END,
/**
* On default jump, places the caret at the end of a word, and also selects
* the entire word. Word detection uses [Character.isJavaIdentifierPart] to
* count some special characters, such as underscores, as part of a word.
* If there is no word at the first character of the search query, then the
* caret is placed after the last character of the search query, and all
* text between the start and end of the search query is selected.
*
* On shift jump, does the above but also selects all text between the
* start of the original caret position and the end of the new selection.
*/
TARGET,
/**
* On default jump, performs the Go To Declaration action, available
* via `Navigate | Declaration or Usages`.
*
* On shift jump, performs the Go To Type Declaration action, available
* via `Navigate | Type Declaration`.
*
* Always places the caret at the first character of the search query.
*/
DECLARATION;
val caretColor: Color get() = when (this) {
DISABLED -> AbstractColorsScheme.INHERITED_COLOR_MARKER
JUMP -> AceConfig.jumpModeColor
JUMP_END -> AceConfig.jumpEndModeColor
TARGET -> AceConfig.targetModeColor
DECLARATION -> AceConfig.definitionModeColor
}
override fun toString() = when (this) {
DISABLED -> "(Skip)"
JUMP -> "Jump"
JUMP_END -> "Jump to End"
TARGET -> "Target"
DECLARATION -> "Definition"
}
}

View File

@@ -1,56 +0,0 @@
package org.acejump.input
import org.acejump.config.AceConfig
/**
* Remembers the current [JumpMode] for a session. Allows cycling
* [JumpMode]s according to the order defined in configuration, or
* toggling one specific [JumpMode] on or off.
*/
internal class JumpModeTracker {
private var currentMode = JumpMode.DISABLED
private var currentIndex = 0
/**
* Switches to the next/previous [JumpMode] defined in configuration,
* skipping any [JumpMode]s that are not assigned. If at least two
* [JumpMode]s are assigned in the cycle order, then cycling will
* wrap around. If only one [JumpMode] is assigned, then cycling will
* toggle that one mode.
*/
fun cycle(forward: Boolean): JumpMode {
val cycleModes = AceConfig.cycleModes
val direction = if (forward) 1 else -1
val start = if (currentIndex == 0 && !forward) 0 else currentIndex - 1
for (offset in 1 until cycleModes.size) {
val index = (start + cycleModes.size + (offset * direction)) % cycleModes.size
if (cycleModes[index] != JumpMode.DISABLED) {
currentMode = cycleModes[index]
currentIndex = index + 1
return currentMode
}
}
currentMode = JumpMode.DISABLED
currentIndex = 0
return currentMode
}
/**
* Switches to the specified [JumpMode]. If the current mode already
* equals the specified one, it resets to [JumpMode.DISABLED].
*/
fun toggle(newMode: JumpMode): JumpMode {
if (currentMode == newMode) {
currentMode = JumpMode.DISABLED
currentIndex = 0
} else {
currentMode = newMode
currentIndex = AceConfig.cycleModes.indexOfFirst { it == newMode } + 1
}
return currentMode
}
}

View File

@@ -1,64 +1,52 @@
package org.acejump.input
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import java.awt.geom.Point2D
import kotlin.math.floor
/**
* Defines common keyboard layouts. Each layout has a key priority order,
* based on each key's distance from the home row and how ergonomically
* difficult they are to press.
* Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how
* ergonomically difficult they are to press.
*/
@Suppress("unused")
enum class KeyLayout(internal val rows: Array<String>, priority: String) {
@Suppress("unused", "SpellCheckingInspection")
enum class KeyLayout(
internal val rows: Array<String>,
priority: String,
private val characterSides: Pair<Set<Char>, Set<Char>> = Pair(emptySet(), emptySet()),
internal val characterRemapping: Map<Char, Char> = emptyMap(),
) {
COLEMK(arrayOf("1234567890", "qwfpgjluy", "arstdhneio", "zxcvbkm"), priority = "tndhseriaovkcmbxzgjplfuwyq5849673210"),
WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"),
DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"),
QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210"),
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "zxcvb"), listOf("7890", "yuiop", "hjkl", "nm"))),
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "yxcvb"), listOf("7890", "zuiop", "hjkl", "nm"))),
QWERTZ_CZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "yxcvb"), listOf("7890", "zuiop", "hjkl", "nm")), characterRemapping = mapOf(
'+' to '1',
'ě' to '2',
'š' to '3',
'č' to '4',
'ř' to '5',
'ž' to '6',
'ý' to '7',
'á' to '8',
'í' to '9',
'é' to '0'
)),
QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"),
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210"),
AZERTY(arrayOf("1234567890", "azertyuiop", "qsdfghjklm", "wxcvbn"), priority = "fjghdkslqvncmbxwrutyeizoap5849673210"),
CANARY(arrayOf("1234567890", "wlypbzfou", "crstgmneia", "qjvdkxh"), priority = "tngmseracidxvhkjqpfbzyoluw5849673210"),
ENGRAM(arrayOf("1234567890", "byouldwvz", "cieahtsnq", "gxjkrmfp"), priority = "ahetiscnkrjmodulywxfgpbvqz3847295610");
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210");
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
private val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
private val keyDistances: Map<Char, Object2IntMap<Char>> by lazy {
val keyDistanceMap = mutableMapOf<Char, Object2IntMap<Char>>()
val keyLocations = mutableMapOf<Char, Point2D>()
fun priority(char: Char): Int {
return allPriorities[char] ?: allChars.length
}
for ((rowIndex, rowChars) in rows.withIndex()) {
val keyY = rowIndex * 1.2F // Slightly increase cost of traveling between rows.
for ((columnIndex, char) in rowChars.withIndex()) {
val keyX = columnIndex + (0.25F * rowIndex) // Assume a 1/4-key uniform stagger.
keyLocations[char] = Point2D.Float(keyX, keyY)
fun areOnSameSide(c1: Char, c2: Char): Boolean {
return (c1 in characterSides.first && c2 in characterSides.first) || (c1 in characterSides.second && c2 in characterSides.second)
}
}
for (fromChar in allChars) {
val distances = Object2IntOpenHashMap<Char>()
val fromLocation = keyLocations.getValue(fromChar)
for (toChar in allChars) {
distances[toChar] = floor(2F * fromLocation.distanceSq(keyLocations.getValue(toChar))).toInt()
}
keyDistanceMap[fromChar] = distances
}
keyDistanceMap
}
internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int? {
return { allPriorities[tagToChar(it)] }
}
internal fun distanceBetweenKeys(char1: Char, char2: Char): Int {
return keyDistances.getValue(char1).getValue(char2)
}
private fun sides(left: List<String>, right: List<String>): Pair<Set<Char>, Set<Char>> {
return Pair(
left.flatMapTo(mutableSetOf()) { it.toCharArray().toSet() },
right.flatMapTo(mutableSetOf()) { it.toCharArray().toSet() }
)
}

View File

@@ -1,50 +1,62 @@
package org.acejump.input
import org.acejump.config.AceSettings
import kotlin.math.pow
import kotlin.math.roundToInt
/**
* Stores data specific to the selected keyboard layout. We want to assign tags with easily reachable keys first, and ideally have tags
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/
internal object KeyLayoutCache {
/**
* Sorts tags according to current keyboard layout settings, and some predefined rules that force tags with digits, and tags with two
* keys far apart, to be sorted after other (easier to type) tags.
*/
lateinit var tagOrder: Comparator<String>
private set
/**
* Returns all possible two key tags, pre-sorted according to [tagOrder].
*/
lateinit var allPossibleTags: List<String>
lateinit var allowedTagsSorted: List<String>
private set
/**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/
fun ensureInitialized(settings: AceSettings) =
if (!::tagOrder.isInitialized) reset(settings) else Unit
fun ensureInitialized(settings: AceSettings) {
if (!::allowedTagsSorted.isInitialized) {
reset(settings)
}
}
/**
* Re-initializes cached data according to updated settings.
*/
fun reset(settings: AceSettings) {
tagOrder = compareBy(
{ it[0].isDigit() || it[1].isDigit() },
{ settings.layout.distanceBetweenKeys(it[0], it[1]) },
settings.layout.priority { it[0] }
)
val allowedChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars) }
val allowedTags = mutableSetOf<String>()
val allPossibleChars = settings.allowedChars
.toCharArray()
.filter(Char::isLetterOrDigit)
.distinct()
.joinToString("")
.ifEmpty(settings.layout::allChars)
for (c1 in allowedChars) {
allowedTags.add("$c1")
allPossibleTags = allPossibleChars.flatMap { a ->
allPossibleChars.map { b -> "$a$b".intern() }
}.sortedWith(tagOrder)
for (c2 in allowedChars) {
if (c1 != c2) {
allowedTags.add("$c1$c2")
}
}
}
allowedTagsSorted = allowedTags.sortedBy { rankPriority(settings.layout, it) }
}
private fun processCharList(charList: String): List<Char> {
return charList.toCharArray().map(Char::lowercaseChar).distinct()
}
private fun rankPriority(layout: KeyLayout, tag: String): Int {
val c1 = tag.first()
val p1 = (1.0 + layout.priority(c1)).pow(3)
if (tag.length == 1) {
return p1.roundToInt()
}
val c2 = tag.last()
val p2 = (1.0 + layout.priority(c2)).pow(3)
val multiplier = if (layout.areOnSameSide(c1, c2)) 2 else 1
return (((p1 * 50) + p2 + 1000) * multiplier).roundToInt()
}
}

View File

@@ -0,0 +1,24 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
open class JumpMode : SessionMode {
override val caretColor
get() = AceConfig.jumpModeColor
protected var wasUpperCase = false
private set
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
wasUpperCase = charTyped.isUpperCase()
return state.type(charTyped)
}
override fun accept(state: SessionState, acceptedTag: Tag) {
AceTagAction.JumpToSearchStart.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true)
}
}

View File

@@ -0,0 +1,13 @@
package org.acejump.modes
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
import java.awt.Color
interface SessionMode {
val caretColor: Color
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult
fun accept(state: SessionState, acceptedTag: Tag)
}

View File

@@ -1,9 +1,12 @@
package org.acejump.search
enum class Pattern(val regex: String) {
LINE_STARTS("^.|^\\n|(?<!.)\\Z"),
LINE_STARTS("^.|^\\n"),
LINE_ENDS("\\n|\\Z"),
LINE_INDENTS("[^\\s].*|^\\n|(?<!.)\\Z"),
LINE_ALL_MARKS(listOf(LINE_ENDS, LINE_STARTS, LINE_INDENTS).flatMap { it.regex.split("|") }.distinct().joinToString("|")),
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]");
LINE_INDENTS("[^\\s].*|^\\n"),
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
VIM_LWORD("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
VIM_UWORD("(?<=\\s|\\A)[^\\s]"),
VIM_LWORD_END("[a-zA-Z0-9_](?=[^a-zA-Z0-9_]|\\Z)"),
VIM_UWORD_END("[^\\s](?=\\s|\\Z)")
}

View File

@@ -4,28 +4,17 @@ import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.clone
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.matchesAt
/**
* Searches editor text for matches of a [SearchQuery], and updates
* previous results when the user [type]s a character.
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [refineQuery]s a character.
*/
internal class SearchProcessor private constructor(
query: SearchQuery,
results: MutableMap<Editor, IntArrayList>
) {
companion object {
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries) =
SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries) =
SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
}
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
val regex = query.toRegex()
class SearchProcessor private constructor(query: SearchQuery, val boundaries: Boundaries, val invertUppercaseMode: Boolean, private val results: MutableMap<Editor, IntArrayList>) {
internal constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries, invertUppercaseMode: Boolean) : this(query, boundaries, invertUppercaseMode, mutableMapOf()) {
val regex = query.toRegex(invertUppercaseMode)
if (regex != null) {
for (editor in editors) {
@@ -36,15 +25,13 @@ internal class SearchProcessor private constructor(
var result = regex.find(editor.immutableText, offsetRange.first)
while (result != null) {
// For some reason regex matches can be out of bounds, but
// boundary check prevents an exception.
val index = result.range.first
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception.
val highlightEnd = index + query.getHighlightLength("", index)
if (highlightEnd > offsetRange.last) {
break
}
else if (boundaries.isOffsetInside(editor, index, cache)) {
else if (boundaries.isOffsetInside(editor, index, cache) && !editor.foldingModel.isOffsetCollapsed(index)) {
offsets.add(index)
}
@@ -56,107 +43,43 @@ internal class SearchProcessor private constructor(
}
}
var query: SearchQuery = query
internal var query = query
private set
var results: MutableMap<Editor, IntArrayList> = results
private set
val resultsCopy
get() = results.clone()
/**
* Appends a character to the search query and removes all search results
* that no longer match the query. If the last typed character transitioned
* the search query from a non-word to a word, it notifies the [Tagger] to
* reassign all tags. If the new query is invalid because it would remove
* every result, the change is reverted and this function returns false.
*/
fun type(char: Char, tagger: Tagger): Boolean {
val newQuery = query.rawText + char
val canMatchTag = tagger.canQueryMatchAnyVisibleTag(newQuery)
val isQueryFinished
get() = query.rawText.length >= AceConfig.minQueryLength
// If the typed character is not compatible with any existing tag or as
// a continuation of any previous occurrence, reject the query change
// and return false to indicate that nothing else should happen.
if (newQuery.length > 1 && !canMatchTag && !isContinuation(newQuery)) {
return false
}
// If the typed character transitioned the search query from a non-word
// to a word, and the typed character does not belong to an existing tag,
// we basically restart the search at the beginning of every new word,
// and unmark existing results so that all tags get regenerated immediately
// afterwards. Although this causes tags to change, it is one solution for
// conflicts between tag characters and search query characters, and moving
// searches across word boundaries during search should be fairly uncommon.
if (!canMatchTag && newQuery.length >= 2 && !newQuery[newQuery.length - 2].isWordPart && char.isWordPart) {
query = SearchQuery.Literal(char.toString())
tagger.unmark()
for ((editor, offsets) in results) {
val chars = editor.immutableText
val iter = offsets.iterator()
while (iter.hasNext()) {
val movedOffset = iter.nextInt() + newQuery.length - 1
if (movedOffset < chars.length && chars[movedOffset].equals(char, ignoreCase = true)) {
iter.set(movedOffset)
fun refineQuery(char: Char): Boolean {
if (char == '\n') {
return true
}
else {
iter.remove()
query = query.refine(char)
removeObsoleteResults()
return isQueryFinished
}
}
}
} else {
removeObsoleteResults(newQuery, tagger)
query = SearchQuery.Literal(newQuery)
}
return true
}
/**
* Returns true if the new query is a continuation of any remaining search query.
* After updating the query, removes all results that no longer match the search query.
*/
private fun isContinuation(newQuery: String): Boolean {
for ((editor, offsets) in results) {
val chars = editor.immutableText
if (offsets.any { chars.matchesAt(it, newQuery, ignoreCase = true) }) {
return true
}
}
private fun removeObsoleteResults() {
val query = query.rawText
return false
}
for (entry in results) {
val editor = entry.key
val offsetIter = entry.value.iterator()
/**
* After updating the query, removes all results that no longer match
* the search query.
*/
private fun removeObsoleteResults(newQuery: String, tagger: Tagger) {
val lastCharOffset = newQuery.lastIndex
val lastChar = newQuery[lastCharOffset]
val ignoreCase = newQuery[0].isLowerCase()
while (offsetIter.hasNext()) {
val offset = offsetIter.nextInt()
for ((editor, offsets) in results.entries.toList()) {
val chars = editor.immutableText
val remaining = IntArrayList()
val iter = offsets.iterator()
while (iter.hasNext()) {
val offset = iter.nextInt()
val endOffset = offset + lastCharOffset
val lastTypedCharMatches = endOffset < chars.length &&
chars[endOffset].equals(lastChar, ignoreCase)
if (lastTypedCharMatches ||
tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
remaining.add(offset)
}
}
results[editor] = remaining
if (!editor.immutableText.matchesAt(offset, query, ignoreCase = true)) {
offsetIter.remove()
}
}
}
}
}

View File

@@ -8,6 +8,11 @@ import org.acejump.countMatchingCharacters
internal sealed class SearchQuery {
abstract val rawText: String
/**
* Returns a new query with the given character appended.
*/
abstract fun refine(char: Char): SearchQuery
/**
* Returns how many characters the search occurrence highlight should cover.
*/
@@ -16,44 +21,59 @@ internal sealed class SearchQuery {
/**
* Converts the query into a regular expression to find the initial matches.
*/
abstract fun toRegex(): Regex?
abstract fun toRegex(invertUppercaseMode: Boolean): Regex?
/**
* Searches for all occurrences of a literal text query. If the first
* character of the query is lowercase, then the entire query will be
* case-insensitive.
*
* Each occurrence must either match the entire query, or match the query
* up to a point so that the rest of the query matches the beginning of
* a tag at the location of the occurrence.
* Searches for all occurrences of a literal text query.
* If the first character of the query is lowercase, then the entire query will be case-insensitive,
* and only beginnings of words and camel humps will be matched.
*/
class Literal(override var rawText: String): SearchQuery() {
class Literal(override val rawText: String) : SearchQuery() {
init {
require(rawText.isNotEmpty())
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int =
text.countMatchingCharacters(offset, rawText)
override fun refine(char: Char): SearchQuery {
return Literal(rawText + char)
}
override fun toRegex(): Regex {
val options = mutableSetOf(RegexOption.MULTILINE)
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return text.countMatchingCharacters(offset, rawText)
}
if (rawText.first().isLowerCase())
options.add(RegexOption.IGNORE_CASE)
override fun toRegex(invertUppercaseMode: Boolean): Regex {
val firstChar = rawText.first()
val pattern = if (firstChar.isLowerCase() xor invertUppercaseMode) {
val fullPattern = Regex.escape(rawText)
"(?i)$fullPattern"
}
else {
val firstCharUppercasePattern = Regex.escape(firstChar.uppercase())
val firstCharLowercasePattern = Regex.escape(firstChar.lowercase())
val remainingPattern = if (rawText.length > 1) Regex.escape(rawText.drop(1)) else ""
"(?:$firstCharUppercasePattern|(?<![a-zA-Z])$firstCharLowercasePattern)$remainingPattern"
}
return Regex(Regex.escape(rawText), options)
return Regex(pattern, setOf(RegexOption.MULTILINE))
}
}
/**
* Searches for all matches of a regular expression.
*/
class RegularExpression(private var pattern: String): SearchQuery() {
class RegularExpression(private val pattern: String) : SearchQuery() {
override val rawText = ""
override fun getHighlightLength(text: CharSequence, offset: Int) = 0
override fun refine(char: Char): SearchQuery {
return Literal(char.toString())
}
override fun toRegex(): Regex =
Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return 1
}
override fun toRegex(invertUppercaseMode: Boolean): Regex {
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
}
}
}

View File

@@ -1,205 +0,0 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache
import org.acejump.isWordPart
import org.acejump.wordEndPlus
import java.util.IdentityHashMap
import kotlin.math.max
/*
* Solves the Tag Assignment Problem. The tag assignment problem can be stated
* thusly: Given a set of indices I in document d, and a set of tags T, find a
* bijection f: T*⊂T → I*⊂I s.t. d[i..k] + t ∉ d[i'..(k + |t|)], ∀ i' ∈ I\{i},
* ∀ k ∈ (i, |d|-|t|], where t ∈ T, i ∈ I. Maximize |I*|. This can be relaxed
* to t=t[0] and ∀ k ∈ (i, i+K] for some fixed K, in most natural documents.
*
* More concretely, tags are typically two-character strings containing alpha-
* numeric symbols. Documents are plaintext files. Indices are produced by a
* search query of length N, i.e. the preceding N characters of every index i in
* document d are identical. For characters proceeding d[i], all bets are off.
* We can assume that P(d[i]|d[i-1]) has some structure for d~D. Ultimately, we
* want a fast algorithm which maximizes the number of tagged document indices.
*
* Tags are used by the typist to select indices within a document. To select an
* index, the typist starts by activating AceJump and searching for a character.
* As soon as the first character is received, we begin to scan the document for
* matching locations and assign as many valid tags as possible. When subsequent
* characters are received, we refine the search results to match either:
*
* 1.) The plaintext query alone, or
* 2.) The concatenation of plaintext query and partial tag
*
* The constraint in paragraph no. 1 tries to impose the following criteria:
*
* 1.) All valid key sequences will lead to a unique location in the document
* 2.) All indices in the document will be reachable by a short key sequence
*
* If there is an insufficient number of two-character tags to cover every index
* (which typically occurs when the user searches for a common character within
* a long document), then we attempt to maximize the number of tags assigned to
* document indices. The key is, all tags must be assigned as soon as possible,
* i.e. as soon as the first character is received or whenever the user ceases
* typing (at the very latest). Once assigned, a visible tag must never change
* at any time during the selection process, so as not to confuse the user.
*/
internal class Solver private constructor(
private val editorPriority: List<Editor>,
private val queryLength: Int,
private val newResults: Map<Editor, IntList>,
private val allResults: Map<Editor, IntList>
) {
companion object {
fun solve(
editorPriority: List<Editor>,
query: SearchQuery,
newResults: Map<Editor, IntList>,
allResults: Map<Editor, IntList>,
tags: List<String>,
caches: Map<Editor, EditorOffsetCache>
): Map<String, Tag> =
Solver(editorPriority, max(1, query.rawText.length), newResults, allResults)
.map(tags, caches)
}
private var newTags = HashMap<String, Tag>(KeyLayoutCache.allPossibleTags.size)
private val newTagIndices = newResults.keys.associateWith { IntOpenHashSet() }
private var allWordFragments =
HashSet<String>(allResults.values.sumOf(IntList::size)).apply {
for ((editor, offsets) in allResults) {
val iter = offsets.iterator()
while (iter.hasNext()) forEachWordFragment(editor, iter.nextInt()) { add(it) }
}
}
fun map(availableTags: List<String>, caches: Map<Editor, EditorOffsetCache>): Map<String, Tag> {
val eligibleSitesByTag = HashMap<String, MutableList<Tag>>(100)
val tagsByFirstLetter = availableTags.groupBy { it[0] }
for ((editor, offsets) in newResults) {
val iter = offsets.iterator()
while (iter.hasNext()) {
val site = iter.nextInt()
if (editor.foldingModel.isOffsetCollapsed(site)) {
continue
}
for ((firstLetter, tags) in tagsByFirstLetter.entries) {
if (canTagBeginWithChar(editor, site, firstLetter)) {
for (tag in tags) {
eligibleSitesByTag.getOrPut(tag, ::mutableListOf).add(Tag(editor, site))
}
}
}
}
}
val matchingSites = HashMap<MutableList<Tag>, MutableList<Tag>>()
// Keys are guaranteed to be from a single collection.
val matchingSitesAsArrays = IdentityHashMap<String, MutableList<Tag>>()
val siteOrder = siteOrder(caches)
val tagOrder = KeyLayoutCache.tagOrder
.thenComparingInt { eligibleSitesByTag.getValue(it).size }
.thenBy(AceConfig.layout.priority(String::last))
val sortedTags = eligibleSitesByTag.keys.toMutableList().apply {
sortWith(tagOrder)
}
for ((mark, tags) in eligibleSitesByTag.entries) {
matchingSitesAsArrays[mark] = matchingSites.getOrPut(tags) {
tags.toMutableList().apply { sortWith(siteOrder) }
}
}
var totalAssigned = 0
val totalResults = newResults.values.sumOf(IntList::size)
for (tag in sortedTags) {
if (totalAssigned == totalResults) {
break
}
if (tryToAssignTag(tag, matchingSitesAsArrays.getValue(tag))) {
totalAssigned++
}
}
return newTags
}
private fun tryToAssignTag(mark: String, tags: List<Tag>): Boolean {
if (newTags.containsKey(mark)) return false
val tag = tags.firstOrNull { it.offset !in newTagIndices.getValue(it.editor) } ?: return false
@Suppress("ReplacePutWithAssignment")
newTags.put(mark, tag)
newTagIndices.getValue(tag.editor).add(tag.offset)
return true
}
private fun siteOrder(caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b ->
val aEditor = a.editor
val bEditor = b.editor
if (aEditor !== bEditor) {
val aEditorIndex = editorPriority.indexOf(aEditor)
val bEditorIndex = editorPriority.indexOf(bEditor)
// For multiple editors, prioritize them based on the provided order.
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
}
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, caches.getValue(aEditor))
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, caches.getValue(bEditor))
if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first.
return@Comparator if (aIsVisible) -1 else 1
}
val aIsNotWordStart = aEditor.immutableText[max(0, a.offset - 1)].isWordPart
val bIsNotWordStart = bEditor.immutableText[max(0, b.offset - 1)].isWordPart
if (aIsNotWordStart != bIsNotWordStart) {
// Ensure that the first letter of a word is prioritized for tagging.
return@Comparator if (bIsNotWordStart) -1 else 1
}
when {
a.offset < b.offset -> -1
a.offset > b.offset -> 1
else -> 0
}
}
private fun canTagBeginWithChar(editor: Editor, site: Int, char: Char): Boolean {
if (char.toString() in allWordFragments) return false
forEachWordFragment(editor, site) { if (it + char in allWordFragments) return false }
return true
}
private inline fun forEachWordFragment(editor: Editor, site: Int, callback: (String) -> Unit) {
val chars = editor.immutableText
val left = max(0, site + queryLength - 1)
val right = chars.wordEndPlus(site)
if (right >= chars.length) {
return
}
val builder = StringBuilder(1 + right - left)
for (i in left..right) {
builder.append(chars[i].lowercase())
callback(builder.toString())
}
}
}

View File

@@ -1,8 +1,13 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
import org.acejump.getView
data class Tag(val editor: Editor, val offset: Int) {
fun isVisible() = offset in editor.getView() && !editor.foldingModel.isOffsetCollapsed(offset)
override fun equals(other: Any?): Boolean {
return other is Tag && other.offset == offset && other.editor === editor
}
override fun hashCode(): Int {
return (offset * 31) + editor.hashCode()
}
}

View File

@@ -1,87 +1,130 @@
package org.acejump.search
import com.google.common.collect.ArrayListMultimap
import com.google.common.collect.HashBiMap
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.*
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.input.KeyLayoutCache.allPossibleTags
import org.acejump.input.KeyLayoutCache
import org.acejump.view.TagMarker
import java.util.AbstractMap.SimpleImmutableEntry
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.min
/**
* Assigns tags to search occurrences, updates them when the search query changes, and requests a jump if the search query matches a tag.
* Assigns tags to search occurrences.
* The ordering of [editors] may be used to prioritize tagging editors earlier in the list in case of conflicts.
*/
internal class Tagger(private val editors: List<Editor>) {
private var tagMap = HashBiMap.create<String, Tag>()
val hasTags
get() = tagMap.isNotEmpty()
class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
private var tagMap: Map<String, Tag>
private var typedTag = ""
@ExternalUsage
val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value.offset }
internal val markers: Map<Editor, Collection<TagMarker>>
get() {
val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tagMap.values.size, 40))
/**
* Removes all markers, allowing them to be regenerated from scratch.
*/
fun unmark() {
tagMap = HashBiMap.create()
for ((mark, tag) in tagMap) {
val marker = TagMarker.create(mark, tag.offset, typedTag)
markers.put(tag.editor, marker)
}
/**
* Assigns tags to as many results as possible, keeping previously assigned
* tags. Returns a [TaggingResult.Jump] if the current search query matches
* any existing tag and we should jump to it and end the session, or
* [TaggingResult.Mark] to continue the session with updated tag markers.
*
* Note that the [results] collection will be mutated.
*/
fun markOrJump(query: SearchQuery, results: Map<Editor, IntList>): TaggingResult {
val isRegex = query is SearchQuery.RegularExpression
val queryText = if (isRegex) " ${query.rawText}" else query.rawText[0] + query.rawText.drop(1).lowercase()
val availableTags = allPossibleTags.filter { !queryText.endsWith(it[0]) && it !in tagMap }
if (!isRegex) {
for (entry in tagMap.entries) {
if (entry solves queryText) {
return TaggingResult.Jump(query = queryText.substringBefore(entry.key), mark = entry.key, tag = entry.value)
}
return markers.asMap()
}
if (queryText.length == 1) {
for ((editor, offsets) in results) {
removeResultsWithOverlappingTags(editor, offsets)
}
}
}
if (!isRegex || tagMap.isEmpty())
tagMap = assignTagsAndMerge(results, availableTags, query, queryText)
val resultTags = results.flatMap { (editor, offsets) -> offsets.map { Tag(editor, it) } }
return TaggingResult.Mark(createTagMarkers(resultTags, query.rawText.ifEmpty { null }))
}
/**
* Assigns as many unassigned tags as possible, and merges them with
* the existing compatible tags.
*/
private fun assignTagsAndMerge(
results: Map<Editor, IntList>,
availableTags: List<String>,
query: SearchQuery,
queryText: String
): HashBiMap<String, Tag> {
init {
val caches = results.keys.associateWith { EditorOffsetCache.new() }
sortResults(results, caches)
val tagSites = results
.flatMap { (editor, sites) -> sites.map { site -> Tag(editor, site) } }
.sortedWith(siteOrder(editors, caches))
tagMap = generateTags(tagSites.size).zip(tagSites).toMap()
}
internal fun type(char: Char): TaggingResult {
val newTypedTag = typedTag + char.lowercaseChar()
val matchingTag = tagMap[newTypedTag]
if (matchingTag != null) {
return TaggingResult.Accept(matchingTag)
}
val newTagMap = tagMap.filter { it.key.startsWith(newTypedTag) }
if (newTagMap.isEmpty()) {
return TaggingResult.Nothing
}
typedTag = newTypedTag
tagMap = newTagMap
return TaggingResult.Mark(markers)
}
private companion object {
private fun generateTags(tagCount: Int): List<String> {
val allowedTagsSorted = KeyLayoutCache.allowedTagsSorted
val tags = mutableListOf<String>()
val containedSingleCharTags = mutableSetOf<Char>()
val blockedSingleCharTags = mutableSetOf<Char>()
val doubleCharTagCountsByFirstChar = Object2IntOpenHashMap<Char>()
for (tag in allowedTagsSorted) {
val firstChar = tag.first()
if (tag.length == 1) {
if (firstChar in blockedSingleCharTags) {
continue
}
containedSingleCharTags.add(firstChar)
}
else {
if (containedSingleCharTags.remove(firstChar)) {
tags.remove(firstChar.toString())
}
blockedSingleCharTags.add(firstChar)
doubleCharTagCountsByFirstChar.addTo(firstChar, 1)
}
tags.add(tag)
if (tags.size >= tagCount) {
break
}
}
// In rare cases, the final tag list may contain a double character tag that is the only tag starting with its first character,
// so we replace it with the single character tag.
for (entry in doubleCharTagCountsByFirstChar.object2IntEntrySet()) {
if (entry.intValue != 1) {
continue
}
tags.removeAt(tags.indexOfFirst { it.first() == entry.key })
val tag = entry.key.toString()
var previousTagIndex = -1
// The implementation of searching where to place the single character tag is theoretically slow,
// but getting here is so rare it doesn't matter.
for (i in allowedTagsSorted.indexOf(tag) - 1 downTo 0) {
previousTagIndex = tags.indexOf(allowedTagsSorted[i])
if (previousTagIndex != -1) {
break
}
}
tags.add(previousTagIndex + 1, tag)
}
return tags
}
private fun sortResults(results: Map<Editor, IntList>, caches: Map<Editor, EditorOffsetCache>) {
for ((editor, offsets) in results) {
val cache = caches.getValue(editor)
@@ -92,133 +135,40 @@ internal class Tagger(private val editors: List<Editor>) {
when {
aIsVisible && !bIsVisible -> -1
bIsVisible && !aIsVisible -> 1
else -> 0}
else -> 0
}
}
val allAssignedTags = mutableMapOf<String, Tag>()
val oldCompatibleTags = tagMap.filter { (mark, tag) ->
isTagCompatibleWithQuery(mark, tag, queryText) || results[tag.editor]?.contains(tag.offset) == true
}
val vacantResults: Map<Editor, IntList>
if (oldCompatibleTags.isEmpty()) {
vacantResults = results
} else {
val vacant = mutableMapOf<Editor, IntList>()
for ((editor, offsets) in results) {
val list = IntArrayList()
val iter = offsets.iterator()
while (iter.hasNext()) {
val tag = Tag(editor, iter.nextInt())
if (tag !in oldCompatibleTags.values) {
list.add(tag.offset)
}
}
vacant[editor] = list
}
vacantResults = vacant
}
allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editors, query, vacantResults, results, availableTags, caches))
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
// Avoid matching query - will trigger a jump.
// TODO: lift this constraint.
val queryEndsWith = queryText.endsWith(tag[0]) || queryText.endsWith(tag)
if (!queryEndsWith && canShortenTag(tag, allAssignedTags.keys))
tag[0].toString()
else
tag
}
}
private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean =
query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
private fun isTagCompatibleWithQuery(marker: String, tag: Tag, query: String): Boolean =
tag.editor.immutableText.matchesAt(tag.offset, getPlaintextPortion(query, marker), ignoreCase = true)
fun isQueryCompatibleWithTagAt(query: String, tag: Tag): Boolean =
tagMap.inverse()[tag].let { it != null && isTagCompatibleWithQuery(it, tag, query) }
fun canQueryMatchAnyVisibleTag(query: String): Boolean =
tagMap.any { (label, tag) ->
val tagPortion = getTagPortion(query, label)
tagPortion.isNotEmpty()
&& label.startsWith(tagPortion, ignoreCase = true)
&& isTagCompatibleWithQuery(label, tag, query)
&& tag.isVisible()
}
private fun removeResultsWithOverlappingTags(editor: Editor, offsets: IntList) {
val iter = offsets.iterator()
val chars = editor.immutableText
while (iter.hasNext()) {
if (!chars.canTagWithoutOverlap(iter.nextInt())) {
iter.remove() // Very uncommon, so slow removal is fine.
}
}
}
private fun createTagMarkers(tags: Collection<Tag>, literalQueryText: String?): MutableMap<Editor, Collection<TagMarker>> {
val tagMapInv = tagMap.inverse()
val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tags.size, 50))
private fun siteOrder(editorPriority: List<Editor>, caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b ->
val aEditor = a.editor
val bEditor = b.editor
for (tag in tags) {
val mark = tagMapInv[tag] ?: continue
val editor = tag.editor
val marker = TagMarker.create(editor, mark, tag.offset, literalQueryText)
markers.put(editor, marker)
if (aEditor !== bEditor) {
val aEditorIndex = editorPriority.indexOf(aEditor)
val bEditorIndex = editorPriority.indexOf(bEditor)
// For multiple editors, prioritize them based on the provided order.
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
}
return markers.asMap()
val aCaches = caches.getValue(aEditor)
val bCaches = caches.getValue(bEditor)
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, aCaches)
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, bCaches)
if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first.
return@Comparator if (aIsVisible) -1 else 1
}
private companion object {
private fun CharSequence.canTagWithoutOverlap(loc: Int) = when {
loc - 1 < 0 -> true
loc + 1 >= length -> true
this[loc] isUnlike this[loc - 1] -> true
this[loc] isUnlike this[loc + 1] -> true
this[loc] != this[loc - 1] -> true
this[loc] != this[loc + 1] -> true
this[loc + 1] == '\r' || this[loc + 1] == '\n' -> true
this[loc - 1] == this[loc] && this[loc] == this[loc + 1] -> false
this[loc + 1].isWhitespace() && this[(loc + 2).coerceAtMost(length - 1)].isWhitespace() -> true
else -> false
}
val firstEditor = editorPriority[0]
val caretPosition = caches.getValue(firstEditor).offsetToXY(firstEditor, firstEditor.caretModel.offset)
private infix fun Char.isUnlike(other: Char) =
this.isWordPart xor other.isWordPart ||
this.isWhitespace() xor other.isWhitespace()
val aDistance = aCaches.offsetToXY(aEditor, a.offset).distanceSq(caretPosition)
val bDistance = bCaches.offsetToXY(bEditor, b.offset).distanceSq(caretPosition)
private fun getPlaintextPortion(query: String, marker: String) = when {
query.endsWith(marker, true) -> query.dropLast(marker.length)
query.endsWith(marker.first(), true) -> query.dropLast(1)
else -> query
}
private fun getTagPortion(query: String, marker: String) = when {
query.endsWith(marker, true) -> query.takeLast(marker.length)
query.endsWith(marker.first(), true) -> query.takeLast(1)
else -> ""
}
private fun canShortenTag(marker: String, markers: Collection<String>): Boolean {
for (other in markers)
if (marker != other && marker[0] == other[0])
return false
return true
return@Comparator aDistance.compareTo(bDistance)
}
}
}

View File

@@ -3,7 +3,8 @@ package org.acejump.search
import com.intellij.openapi.editor.Editor
import org.acejump.view.TagMarker
sealed class TaggingResult {
class Jump(val query: String, val mark: String, val tag: Tag): TaggingResult()
class Mark(val markers: MutableMap<Editor, Collection<TagMarker>>): TaggingResult()
internal sealed class TaggingResult {
object Nothing : TaggingResult()
class Accept(val tag: Tag) : TaggingResult()
class Mark(val markers: Map<Editor, Collection<TagMarker>>) : TaggingResult()
}

View File

@@ -1,5 +0,0 @@
package org.acejump.session
interface AceJumpListener {
fun finished(mark: String?, query: String?)
}

View File

@@ -3,14 +3,10 @@ package org.acejump.session
import com.intellij.openapi.editor.Editor
/**
* Holds [Editor] caret settings. The settings are saved the
* moment a [Session] starts, modified to indicate AceJump
* states, and restored once the [Session] ends.
* Holds [Editor] caret settings. The settings are saved the moment a [Session] starts, modified to indicate AceJump states, and restored
* once the [Session] ends.
*/
internal data class EditorSettings(
private val isBlockCursor: Boolean,
private val isBlinkCaret: Boolean,
) {
internal data class EditorSettings(private val isBlockCursor: Boolean, private val isBlinkCaret: Boolean, private val isReadOnly: Boolean) {
companion object {
fun setup(editor: Editor): EditorSettings {
val settings = editor.settings
@@ -19,20 +15,39 @@ internal data class EditorSettings(
val original = EditorSettings(
isBlockCursor = settings.isBlockCursor,
isBlinkCaret = settings.isBlinkCaret,
isReadOnly = !document.isWritable
)
settings.isBlockCursor = true
settings.isBlinkCaret = false
document.setReadOnly(true)
return original
}
}
fun startEditing(editor: Editor) {
editor.document.setReadOnly(isReadOnly)
}
fun stopEditing(editor: Editor) {
editor.document.setReadOnly(true)
}
fun onTagAccepted(editor: Editor) = editor.let {
it.settings.isBlockCursor = isBlockCursor
}
fun onTagUnaccepted(editor: Editor) = editor.let {
it.settings.isBlockCursor = true
}
fun restore(editor: Editor) {
val settings = editor.settings
val document = editor.document
settings.isBlockCursor = isBlockCursor
settings.isBlinkCaret = isBlinkCaret
document.setReadOnly(isReadOnly)
}
}

View File

@@ -1,306 +1,180 @@
package org.acejump.session
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.openapi.editor.colors.EditorColors.CARET_COLOR
import com.intellij.util.containers.ContainerUtil
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.*
import org.acejump.action.TagScroller
import org.acejump.action.TagJumper
import org.acejump.action.TagVisitor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.input.EditorKeyListener
import org.acejump.input.JumpMode
import org.acejump.input.JumpModeTracker
import org.acejump.input.KeyLayoutCache
import org.acejump.modes.JumpMode
import org.acejump.modes.SessionMode
import org.acejump.search.Pattern
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
import org.acejump.search.TaggingResult
import org.acejump.search.SearchQuery
import org.acejump.search.Tag
import org.acejump.session.TypeResult.AcceptTag
import org.acejump.session.TypeResult.ChangeMode
import org.acejump.session.TypeResult.ChangeState
import org.acejump.session.TypeResult.EndSession
import org.acejump.session.TypeResult.Nothing
import org.acejump.view.TagCanvas
import org.acejump.view.TagMarker
import org.acejump.view.TextHighlighter
import java.util.*
/**
* Manages an AceJump session for one or more [Editor]s.
* Manages an AceJump session for a single [Editor].
*/
class Session(private val mainEditor: Editor, private val jumpEditors: List<Editor>) {
private val listeners: MutableList<AceJumpListener> =
ContainerUtil.createLockFreeCopyOnWriteList()
private val editorSettings = EditorSettings.setup(mainEditor)
private lateinit var mode: SessionMode
private var boundaries: Boundaries = defaultBoundaries
private var state: SessionState? = null
private companion object {
private val defaultBoundaries
get() = if (AceConfig.searchWholeFile) WHOLE_FILE else VISIBLE_ON_SCREEN
}
private val originalSettings = EditorSettings.setup(mainEditor)
private val jumpModeTracker = JumpModeTracker()
private var jumpMode = JumpMode.DISABLED
private var acceptedTag: Tag? = null
set(value) {
field = value
if (value === JumpMode.DISABLED) {
end()
} else {
searchProcessor?.let { textHighlighter.render(it.results, it.query, jumpMode) }
mainEditor.colorsScheme.setColor(CARET_COLOR, value.caretColor)
mainEditor.contentComponent.repaint()
if (value != null) {
tagCanvases.values.forEach(TagCanvas::removeMarkers)
editorSettings.onTagAccepted(mainEditor)
}
}
private var searchProcessor: SearchProcessor? = null
private var tagger = Tagger(jumpEditors)
private val tagJumper
get() = TagJumper(jumpMode, searchProcessor)
private val tagVisitor
get() = searchProcessor?.let { TagVisitor(mainEditor, it, tagJumper) }
private val tagScroller
get() = searchProcessor?.let { TagScroller(mainEditor, it) }
private val textHighlighter = TextHighlighter()
private val tagCanvases = jumpEditors.associateWith(::TagCanvas)
@ExternalUsage
val tags
get() = tagger.tags
var defaultBoundary: Boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
init {
KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(mainEditor, object: TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
var processor = searchProcessor
val hadTags = tagger.hasTags
if (processor == null) {
processor = SearchProcessor.fromChar(
jumpEditors, charTyped, boundaries
).also { searchProcessor = it }
} else if (!processor.type(charTyped, tagger)) {
return
private val actions = object : SessionActions {
override fun showHighlights(results: Map<Editor, IntList>, query: SearchQuery) {
textHighlighter.renderOccurrences(results, query)
}
updateSearch(
processor, markImmediately = hadTags,
shiftMode = charTyped.isUpperCase()
)
}
})
override fun hideHighlights() {
textHighlighter.reset()
}
/**
* Updates text highlights and tag markers according to the current
* search state. Dispatches jumps if the search query matches a tag.
* If all tags are outside view, scrolls to the closest one.
*/
private fun updateSearch(
processor: SearchProcessor,
markImmediately: Boolean,
shiftMode: Boolean = false
) {
val query = processor.query
val results = processor.results
textHighlighter.render(results, query, jumpMode)
if (!markImmediately &&
query.rawText.let {
it.length < AceConfig.minQueryLength &&
it.all(Char::isLetterOrDigit)
}
) {
return
}
when (val result = tagger.markOrJump(query, results.clone())) {
is TaggingResult.Jump -> {
tagJumper.jump(result.tag, shiftMode, isCrossEditor = mainEditor !== result.tag.editor)
tagCanvases.values.forEach(TagCanvas::removeMarkers)
end(result)
}
is TaggingResult.Mark -> {
val markers = result.markers
override fun setTagMarkers(markers: Map<Editor, Collection<TagMarker>>) {
for ((editor, canvas) in tagCanvases) {
canvas.setMarkers(markers[editor].orEmpty())
}
if (jumpEditors.all { editor ->
val cache = EditorOffsetCache.new()
markers[editor].let { it == null || it.none { marker ->
VISIBLE_ON_SCREEN.isOffsetInside(editor, marker.offsetL, cache) ||
VISIBLE_ON_SCREEN.isOffsetInside(editor, marker.offsetR, cache) } }
}) {
tagVisitor?.scrollToClosest()
}
}
}
}
@ExternalUsage
fun markResults(resultsToMark: SortedSet<Int>) {
val jumpEditor = jumpEditors.singleOrNull() ?: return
markResults(mapOf(jumpEditor to resultsToMark))
init {
KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(mainEditor) { editor, charTyped, _ -> typeCharacter(editor, charTyped) }
}
@ExternalUsage
fun markResults(resultsToMark: Map<Editor, Collection<Int>>) {
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
private fun typeCharacter(editor: Editor, charTyped: Char) {
val state = state ?: return
val processor = SearchProcessor.fromRegex(jumpEditors, "", defaultBoundaries)
.apply {
results.clear()
for ((editor, offsets) in resultsToMark) {
if (editor in jumpEditors) {
results[editor] = IntArrayList(offsets)
editorSettings.startEditing(editor)
val result = mode.type(state, charTyped, acceptedTag)
editorSettings.stopEditing(editor)
when (result) {
Nothing -> return
is ChangeState -> this.state = result.state
is ChangeMode -> setMode(result.mode)
is AcceptTag -> {
acceptedTag = result.tag
mode.accept(state, result.tag)
end()
}
EndSession -> end()
}
}
updateSearch(processor, markImmediately = true)
private fun setMode(mode: SessionMode) {
this.mode = mode
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
}
fun startJumpMode() {
startJumpMode(::JumpMode)
}
fun startJumpMode(mode: () -> JumpMode) {
if (this::mode.isInitialized && mode is JumpMode) {
end()
return
}
if (this::mode.isInitialized) {
restart()
}
setMode(mode())
state = SessionState.WaitForKey(actions, jumpEditors, defaultBoundary, AceConfig.invertUppercaseMode)
}
/**
* Starts a regular expression search. If a search was already active,
* it will be reset alongside its tags and highlights.
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/
@ExternalUsage
fun startRegexSearch(pattern: String, boundaries: Boundaries) {
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(
jumpEditors, pattern,
boundaries.intersection(defaultBoundaries)
).also { searchProcessor = it }
updateSearch(processor, markImmediately = true)
fun startRegexSearch(pattern: Pattern) {
if (!this::mode.isInitialized) {
setMode(JumpMode())
}
/**
* Starts a regular expression search. If a search was already active,
* it will be reset alongside its tags and highlights.
*/
@ExternalUsage
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) =
startRegexSearch(pattern.regex, boundaries)
/**
* See [JumpModeTracker.cycle].
*/
fun cycleNextJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = true)
for (canvas in tagCanvases.values) {
canvas.setMarkers(emptyList())
}
/**
* See [JumpModeTracker.cycle].
*/
fun cyclePreviousJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = false)
val processor = SearchProcessor(jumpEditors, SearchQuery.RegularExpression(pattern.regex), defaultBoundary, AceConfig.invertUppercaseMode)
textHighlighter.renderOccurrences(processor.resultsCopy, processor.query)
state = SessionState.SelectTag(actions, jumpEditors, processor)
}
/**
* See [JumpModeTracker.toggle]
*/
fun toggleJumpMode(newMode: JumpMode) {
jumpMode = jumpModeTracker.toggle(newMode)
fun tagImmediately() {
typeCharacter(mainEditor, '\n')
}
@ExternalUsage
fun toggleJumpMode(newMode: JumpMode, boundaries: Boundaries) {
this.boundaries = this.boundaries.intersection(boundaries)
jumpMode = jumpModeTracker.toggle(newMode)
}
/**
* See [TagVisitor.visitPrevious]. If there are no tags, nothing happens.
*/
fun visitPreviousTag() =
if (tagVisitor?.visitPrevious() == true) end() else Unit
/**
* See [TagVisitor.visitNext]. If there are no tags, nothing happens.
*/
fun visitNextTag() =
if (tagVisitor?.visitNext() == true) end() else Unit
/**
* See [TagVisitor.visitPrevious]. If there are no tags, nothing happens.
*/
fun scrollToNextScreenful() = tagScroller?.scroll(true)
/**
* See [TagVisitor.visitNext]. If there are no tags, nothing happens.
*/
fun scrollToPreviousScreenful() = tagScroller?.scroll(false)
/**
* Ends this session.
*/
fun end(taggingResult: TaggingResult? = null) =
SessionManager.end(mainEditor, taggingResult)
fun end() {
SessionManager.end(mainEditor)
}
/**
* Clears any currently active search, tags, and highlights.
* Does not reset [JumpMode].
*/
fun restart() {
tagger = Tagger(jumpEditors)
searchProcessor = null
state = null
acceptedTag = null
tagCanvases.values.forEach(TagCanvas::removeMarkers)
textHighlighter.reset()
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.onTagUnaccepted(mainEditor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
jumpEditors.forEach { it.contentComponent.repaint() }
}
/**
* Should only be used from [SessionManager] to dispose a
* successfully ended session.
* Should only be used from [SessionManager] to dispose a successfully ended session.
*/
internal fun dispose(taggingResult: TaggingResult?) {
tagger = Tagger(jumpEditors)
EditorKeyListener.detach(mainEditor)
internal fun dispose() {
tagCanvases.values.forEach(TagCanvas::unbind)
textHighlighter.reset()
EditorsCache.invalidate()
val jumpResult = taggingResult as? TaggingResult.Jump
val mark = jumpResult?.mark
val query = jumpResult?.query
listeners.forEach { it.finished(mark, query) }
EditorKeyListener.detach(mainEditor)
if (!mainEditor.isDisposed) {
originalSettings.restore(mainEditor)
mainEditor.colorsScheme.setColor(CARET_COLOR, JumpMode.DISABLED.caretColor)
}
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.restore(mainEditor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
val focusedEditor = jumpResult?.tag?.editor ?: mainEditor
if (!focusedEditor.isDisposed) {
val focusedEditor = acceptedTag?.editor ?: mainEditor
focusedEditor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
}
@ExternalUsage
fun addAceJumpListener(listener: AceJumpListener) {
listeners += listener
}
@ExternalUsage
fun removeAceJumpListener(listener: AceJumpListener) {
listeners -= listener
}
}

View File

@@ -0,0 +1,13 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.search.SearchQuery
import org.acejump.view.TagMarker
internal interface SessionActions {
fun showHighlights(results: Map<Editor, IntList>, query: SearchQuery)
fun hideHighlights()
fun setTagMarkers(markers: Map<Editor, Collection<TagMarker>>)
}

View File

@@ -1,53 +1,50 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.ExternalUsage
import org.acejump.search.TaggingResult
/**
* Manages active [Session]s in [Editor]s. There may only be
* one [Session] per [Editor], but multiple [Session]s across
* multiple [Editor]s may be active at once.
* Manages active [Session]s in [Editor]s. There may only be one [Session] per [Editor], but multiple [Session]s across multiple [Editor]s
* may be active at once.
*
* It is possible for an [Editor] to be disposed with an active
* [Session]. In such case, the reference to both will remain
* until a new [Session] starts, at which point the
* [SessionManager.cleanup] method will purge disposed [Editor]s.
* It is possible for an [Editor] to be disposed with an active [Session]. In such case, the reference to both will remain until a new
* [Session] starts, at which point the [SessionManager.cleanup] method will purge disposed [Editor]s.
*/
@ExternalUsage
object SessionManager {
private val sessions = HashMap<Editor, Session>(4)
/**
* Starts a new [Session], or returns an existing [Session]
* if the specified [Editor] already has one.
* Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
*/
fun start(editor: Editor): Session = start(editor, listOf(editor))
fun start(editor: Editor): Session {
return start(editor, listOf(editor))
}
/**
* Starts a new multi-editor [Session], or returns an existing [Session] if the specified main [Editor] already has one.
* The [mainEditor] is used for typing the search query and tag.
* The [jumpEditors] are all editors that will be searched and tagged. The list is ordered so that editors earlier in the list will be
* prioritized for tagging in case of conflicts.
* The [jumpEditors] are all editors that will be searched and tagged.
*/
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
}
/**
* Returns the active [Session] in the specified [Editor],
* or null if the [Editor] has no active session.
* Returns the active [Session] in the specified [Editor], or null if the [Editor] has no active session.
*/
operator fun get(editor: Editor): Session? = sessions[editor]
operator fun get(editor: Editor): Session? {
return sessions[editor]
}
/**
* Ends the active [Session] in the specified [Editor],
* or does nothing if the [Editor] has no active session.
* Ends the active [Session] in the specified [Editor], or does nothing if the [Editor] has no active session.
*/
fun end(editor: Editor, taggingResult: TaggingResult?) =
sessions.remove(editor)?.dispose(taggingResult) ?: Unit
private fun cleanup() = sessions.keys.filter { it.isDisposed }
.forEach { disposedEditor -> sessions.remove(disposedEditor)?.dispose(null) }
fun end(editor: Editor) {
sessions.remove(editor)?.dispose()
}
private fun cleanup() {
for (disposedEditor in sessions.keys.filter { it.isDisposed }) {
sessions.remove(disposedEditor)?.dispose()
}
}
}

View File

@@ -0,0 +1,95 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.search.SearchProcessor
import org.acejump.search.SearchQuery
import org.acejump.search.Tagger
import org.acejump.search.TaggingResult
sealed interface SessionState {
fun type(char: Char): TypeResult
class WaitForKey internal constructor(
private val actions: SessionActions,
private val jumpEditors: List<Editor>,
private val defaultBoundary: Boundaries,
private val invertUppercaseMode: Boolean,
) : SessionState {
override fun type(char: Char): TypeResult {
val searchProcessor = SearchProcessor(jumpEditors, SearchQuery.Literal(char.toString()), defaultBoundary, invertUppercaseMode)
return if (searchProcessor.isQueryFinished) {
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
}
else {
TypeResult.ChangeState(RefineSearchQuery(actions, jumpEditors, searchProcessor))
}
}
}
class RefineSearchQuery internal constructor(
private val actions: SessionActions,
private val jumpEditors: List<Editor>,
private val searchProcessor: SearchProcessor,
) : SessionState {
init {
actions.showHighlights(searchProcessor.resultsCopy, searchProcessor.query)
}
override fun type(char: Char): TypeResult {
return if (searchProcessor.refineQuery(char)) {
actions.hideHighlights()
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
}
else {
actions.showHighlights(searchProcessor.resultsCopy, searchProcessor.query)
TypeResult.Nothing
}
}
}
class SelectTag internal constructor(
private val actions: SessionActions,
private val jumpEditors: List<Editor>,
private val searchProcessor: SearchProcessor,
) : SessionState {
private val tagger = Tagger(jumpEditors, searchProcessor.resultsCopy)
init {
actions.setTagMarkers(tagger.markers)
}
override fun type(char: Char): TypeResult {
if (char == ' ') {
val query = searchProcessor.query
if (query is SearchQuery.Literal) {
val newBoundaries = when (searchProcessor.boundaries) {
StandardBoundaries.VISIBLE_ON_SCREEN -> StandardBoundaries.AFTER_CARET
StandardBoundaries.AFTER_CARET -> StandardBoundaries.BEFORE_CARET
StandardBoundaries.BEFORE_CARET -> StandardBoundaries.VISIBLE_ON_SCREEN
else -> searchProcessor.boundaries
}
val newSearchProcessor = SearchProcessor(jumpEditors, query, newBoundaries, searchProcessor.invertUppercaseMode)
return TypeResult.ChangeState(SelectTag(actions, jumpEditors, newSearchProcessor))
}
}
else if (char == '\n') {
val newSearchProcessor = SearchProcessor(jumpEditors, searchProcessor.query, searchProcessor.boundaries, !searchProcessor.invertUppercaseMode)
return TypeResult.ChangeState(SelectTag(actions, jumpEditors, newSearchProcessor))
}
return when (val result = tagger.type(AceConfig.layout.characterRemapping.getOrDefault(char, char))) {
is TaggingResult.Nothing -> TypeResult.Nothing
is TaggingResult.Accept -> TypeResult.AcceptTag(result.tag)
is TaggingResult.Mark -> {
actions.setTagMarkers(result.markers)
TypeResult.Nothing
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
package org.acejump.session
import org.acejump.modes.SessionMode
import org.acejump.search.Tag
sealed class TypeResult {
object Nothing : TypeResult()
class ChangeState(val state: SessionState) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
class AcceptTag(val tag: Tag) : TypeResult()
object EndSession : TypeResult()
}

View File

@@ -2,11 +2,10 @@ package org.acejump.view
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.ui.ColorUtil
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.read
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Rectangle
@@ -17,75 +16,63 @@ import javax.swing.SwingUtilities
/**
* Holds all active tag markers and renders them on top of the editor.
*/
internal class TagCanvas(private val editor: Editor): JComponent(), CaretListener {
private var markers: Collection<TagMarker>? = null
internal class TagCanvas(private val editor: Editor) : JComponent() {
private var markers: Collection<TagMarker> = emptyList()
private var visible = false
init {
val contentComponent = editor.contentComponent
contentComponent.add(this)
setBounds(0, 0, contentComponent.width, contentComponent.height)
if (ApplicationInfo.getInstance().build.components.first() < 173)
SwingUtilities.convertPoint(this, location, editor.component.rootPane)
.let { setLocation(-it.x, -it.y) }
editor.caretModel.addCaretListener(this)
if (ApplicationInfo.getInstance().build.components.first() < 173) {
SwingUtilities.convertPoint(this, location, editor.component.rootPane).let { setLocation(-it.x, -it.y) }
}
}
fun unbind() {
markers = null
editor.contentComponent.remove(this)
editor.caretModel.removeCaretListener(this)
editor.contentComponent.repaint()
}
/**
* Ensures that all tags and the outline around the selected tag are
* repainted. It should not be necessary to repaint the entire tag
* canvas, but the cost of repainting visible tags is negligible.
*/
override fun caretPositionChanged(event: CaretEvent) = repaint()
fun setMarkers(markers: Collection<TagMarker>) {
this.markers = markers
this.visible = true
repaint()
}
fun removeMarkers() {
markers = emptyList()
this.markers = emptyList()
this.visible = false
repaint()
}
override fun paint(g: Graphics) =
read { if (!markers.isNullOrEmpty()) super.paint(g) else Unit }
override fun paintChildren(g: Graphics) {
super.paintChildren(g)
val markers = markers ?: return
if (!visible) {
return
}
g.color = ColorUtil.withAlpha(editor.colorsScheme.defaultBackground, (AceConfig.editorFadeOpacity * 0.01).coerceIn(0.0, 1.0))
g.fillRect(0, 0, width - 1, height - 1)
if (markers.isEmpty()) {
return
}
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val font = TagFont(editor)
val cache = EditorOffsetCache.new()
val viewRange = VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val foldingModel = editor.foldingModel
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val occupied = mutableListOf<Rectangle>()
// If there is a tag at the caret location, prioritize its rendering over
// all other tags. This is helpful for seeing which tag is currently
// selected while navigating highly clustered tags, although it does end
// up rearranging nearby tags which can be confusing.
// TODO: instead of immediately painting, we could calculate the layout
// of everything first, and then remove tags that interfere with
// the caret tag to avoid changing the alignment of the caret tag
val caretOffset = editor.caretModel.offset
val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset }
caretMarker?.paint(g, editor, cache, font, occupied)
for (marker in markers) {
if (marker.isOffsetInRange(viewRange) && !foldingModel.isOffsetCollapsed(marker.offsetL) && marker !== caretMarker)
if (marker.offset in viewRange) {
marker.paint(g, editor, cache, font, occupied)
}
}
}
}

View File

@@ -1,20 +1,20 @@
package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorFontType.BOLD
import com.intellij.openapi.editor.colors.EditorFontType.PLAIN
import com.intellij.openapi.editor.colors.EditorFontType
import org.acejump.config.AceConfig
import java.awt.Font
import java.awt.FontMetrics
/**
* Stores font metrics for aligning and rendering [TagMarker]s.
*/
class TagFont(editor: Editor) {
val tagFont: Font = editor.colorsScheme.getFont(BOLD)
internal class TagFont(editor: Editor) {
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W')
val editorFontMetrics: FontMetrics =
editor.component.getFontMetrics(editor.colorsScheme.getFont(PLAIN))
val foregroundColor1 = AceConfig.tagForegroundColor1
val foregroundColor2 = AceConfig.tagForegroundColor2
val lineHeight = editor.lineHeight
val baselineDistance = editor.ascent
}

View File

@@ -1,98 +1,40 @@
package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil
import com.intellij.ui.JreHiDpiUtil
import com.intellij.ui.scale.JBUIScale
import com.intellij.openapi.util.SystemInfo
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.config.AceConfig
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import kotlin.math.max
/**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/
class TagMarker(
private val tag: String,
val offsetL: Int,
val offsetR: Int,
private val shiftR: Int,
private val hasSpaceRight: Boolean
internal class TagMarker(
private val firstChar: String,
private val secondChar: String,
val offset: Int
) {
private val length = tag.length
private constructor(tag: String, offset: Int) : this(tag.first().toString(), tag.drop(1), offset)
private val length = firstChar.length + secondChar.length
companion object {
private const val ARC = 1
/**
* TODO This might be due to DPI settings.
*/
private val HIGHLIGHT_OFFSET = if (SystemInfo.isMac) -0.5 else 0.0
/**
* Creates a new tag, precomputing some information about the nearby characters to reduce rendering overhead. If the last typed
* character ([literalQueryText]) matches the first [tag] character, only the second [tag] character is displayed.
* character ([typedTag]) matches the first [tag] character, only the second [tag] character is displayed.
*/
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker {
val chars = editor.immutableText
val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
val displayedTag = if (literalQueryText != null && literalQueryText.last().equals(tag.first(), ignoreCase = true))
tag.drop(1).uppercase()
else
tag.uppercase()
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
// Workaround for misalignment on high DPI screens.
if (JreHiDpiUtil.isJreHiDPI(g)) {
g.translate(0.0, -0.5)
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
g.translate(0.0, 0.5)
}
else {
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
fun create(tag: String, offset: Int, typedTag: String): TagMarker {
return TagMarker(tag.drop(typedTag.length), offset)
}
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) {
val x = point.x + 2
val y = point.y + font.baselineDistance
g.font = font.tagFont
if (!ColorUtil.isDark(AceConfig.tagForegroundColor)) {
g.color = Color(0F, 0F, 0F, 0.35F)
g.drawString(text, x + 1, y + 1)
}
g.color = AceConfig.tagForegroundColor
g.drawString(text, x, y)
}
private fun isLineEnding(char: Char): Boolean {
return char == '\n' || char == '\r'
}
}
/**
* Returns true if the left-aligned offset is in the range. Use to cull tags outside visible range.
* Only the left offset is checked, because if the tag was right-aligned on the last index of the range, it would not be visible anyway.
*/
fun isOffsetInRange(range: IntRange): Boolean = offsetL in range
/**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
* Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap.
@@ -100,49 +42,43 @@ class TagMarker(
fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? {
val rect = alignTag(editor, cache, font, occupied) ?: return null
drawHighlight(g, rect, AceConfig.tagBackgroundColor)
drawForeground(g, font, rect.location, tag)
drawHighlight(g, rect, editor.colorsScheme.defaultBackground)
drawForeground(g, font, rect.location)
occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) })
occupied.add(rect)
return rect
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRect(rect.x, rect.y, rect.width, rect.height + 1)
g.translate(0.0, -HIGHLIGHT_OFFSET)
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point) {
val x = point.x
val y = point.y + font.baselineDistance
g.font = font.tagFont
g.color = font.foregroundColor1
g.drawString(firstChar, x, y)
if (secondChar.isNotEmpty()) {
g.color = font.foregroundColor2
g.drawString(secondChar, x + font.tagCharWidth, y)
}
}
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
val boundaries = VISIBLE_ON_SCREEN
if (hasSpaceRight || offsetL !in 1 until editor.document.textLength || isLineEnding(editor.immutableText[offsetL - 1])) {
val rectR = createRightAlignedTagRect(editor, cache, font)
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects) }
}
val rectL = createLeftAlignedTagRect(editor, cache, font)
if (occupied.none(rectL::intersects))
return rectL.takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
val rectR = createRightAlignedTagRect(editor, cache, font)
if (occupied.none(rectR::intersects))
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
return null
}
private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetR)
val char = if (offsetR >= editor.document.textLength)
' ' // Use the width of a space on the last line.
else editor.immutableText[offsetR].let {
// Use the width of a space on empty lines.
if (isLineEnding(it)) ' ' else it
}
val shift = font.editorFontMetrics.charWidth(char) + (font.tagCharWidth * shiftR)
return Rectangle(pos.x + shift, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
}
private fun createLeftAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetL)
val shift = -(font.tagCharWidth * length)
return Rectangle(pos.x + shift - 4, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
val pos = cache.offsetToXY(editor, offset)
val rect = Rectangle(pos.x, pos.y, font.tagCharWidth * length, font.lineHeight)
return rect.takeIf { occupied.none(it::intersects) }
}
}

View File

@@ -1,58 +1,37 @@
package org.acejump.view
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.codeInsight.hint.*
import com.intellij.codeInsight.hint.HintManagerImpl.HIDE_BY_ESCAPE
import com.intellij.codeInsight.hint.HintManagerImpl.HIDE_BY_TEXT_CHANGE
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.markup.*
import com.intellij.openapi.editor.markup.HighlighterTargetArea.EXACT_RANGE
import com.intellij.ui.*
import com.intellij.ui.util.preferredHeight
import com.intellij.util.DocumentUtil
import com.intellij.util.ui.*
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.ui.ColorUtil
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.*
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig
import org.acejump.input.JumpMode
import org.acejump.immutableText
import org.acejump.search.SearchQuery
import java.awt.*
import javax.swing.*
import kotlin.math.max
import java.awt.Color
import java.awt.Graphics
/**
* Renders highlights for search occurrences.
*/
internal class TextHighlighter {
private companion object { private const val LAYER = HighlighterLayer.LAST + 1 }
private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>()
private var previousHint: LightweightHint? = null
/**
* Label for the search notification.
* Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change.
*/
private class NotificationLabel(text: String?): JLabel(text) {
init {
background = HintUtil.getInformationColor()
foreground = JBColor.foreground()
this.isOpaque = true
}
}
/**
* Removes all current highlights and re-creates them from scratch.
* Must be called whenever any of the method parameters change.
*/
fun render(results: Map<Editor, IntList>, query: SearchQuery, jumpMode: JumpMode) {
val renderer = when {
query is SearchQuery.RegularExpression -> RegexRenderer
jumpMode === JumpMode.TARGET -> SearchedWordWithOutlineRenderer
fun renderOccurrences(results: Map<Editor, IntList>, query: SearchQuery) {
render(results, when (query) {
is SearchQuery.RegularExpression -> RegexRenderer
else -> SearchedWordRenderer
}, query::getHighlightLength)
}
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
for ((editor, offsets) in results) {
val highlights = previousHighlights[editor]
@@ -63,158 +42,81 @@ internal class TextHighlighter {
val modifications = (highlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000
DocumentUtil.executeInBulk(document, enableBulkEditing) {
try {
if (enableBulkEditing) {
document.isInBulkUpdate = true
}
highlights?.forEach(markup::removeHighlighter)
previousHighlights[editor] = Array(offsets.size) { index ->
val start = offsets.getInt(index)
val end = start + query.getHighlightLength(chars, start)
val end = start + getHighlightLength(chars, start)
markup.addRangeHighlighter(start, end, LAYER, null, EXACT_RANGE)
.apply { customRenderer = renderer }
markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
customRenderer = renderer
}
}
} finally {
if (enableBulkEditing) {
document.isInBulkUpdate = false
}
}
}
if (AceConfig.showSearchNotification)
showSearchNotification(results, query, jumpMode)
for (editor in previousHighlights.keys.toList()) {
if (!results.containsKey(editor))
previousHighlights.remove(editor)
?.forEach(editor.markupModel::removeHighlighter)
if (!results.containsKey(editor)) {
previousHighlights.remove(editor)?.forEach(editor.markupModel::removeHighlighter)
}
}
/**
* Show a notification with the current search text.
*/
private fun showSearchNotification(results: Map<Editor, IntList>,
query: SearchQuery, jumpMode: JumpMode) {
// clear previous hint
previousHint?.hide()
// add notification hint to first editor
val editor = results.keys.first()
val component: JComponent = editor.component
val label1 = NotificationLabel(" $jumpMode Mode:")
.apply { font = UIUtil.getLabelFont().deriveFont(Font.BOLD) }
val queryText = " " +
(if (query is SearchQuery.RegularExpression) query.toRegex().toString()
else query.rawText[0] + query.rawText.drop(1).lowercase()) + " "
val label2 = NotificationLabel(queryText)
val label3 = NotificationLabel(
"Found ${results.values.flatMap { it.asIterable() }.size}" +
" results in ${results.keys.size}" +
" editor" + if (1 != results.keys.size) "s" else ". "
)
val panel = JPanel(BorderLayout()).apply {
add(label1, BorderLayout.WEST)
add(label2, BorderLayout.CENTER)
add(label3, BorderLayout.EAST)
border = BorderFactory.createLineBorder(
if (jumpMode == JumpMode.DISABLED) JBColor.BLACK else jumpMode.caretColor
)
preferredHeight = label1.preferredSize.height + 10
}
val hint = LightweightHint(panel)
val x = SwingUtilities.convertPoint(component, 0, 0, component).x
val y: Int = -hint.component.preferredSize.height
val p = SwingUtilities.convertPoint(
component, x, y,
component.rootPane.layeredPane
)
HintManagerImpl.getInstanceImpl().showEditorHint(
hint,
editor,
p,
HIDE_BY_ESCAPE or HIDE_BY_TEXT_CHANGE,
0,
false,
HintHint(editor, p).setAwtTooltip(false)
)
previousHint = hint
}
fun reset() {
previousHighlights.forEach { (editor, highlighters) ->
highlighters.forEach(editor.markupModel::removeHighlighter)
}
previousHighlights.keys.forEach { it.markupModel.removeAllHighlighters() }
previousHighlights.clear()
previousHint?.hide()
}
/**
* Renders a filled highlight in the background of a searched text occurrence.
*/
private object SearchedWordRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) =
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset)
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = AceConfig.textHighlightColor
g.fillRect(start.x, start.y, end.x - start.x, editor.lineHeight)
g.color = AceConfig.tagBackgroundColor
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
}
}
/**
* Renders a filled highlight in the background of a searched
* text occurrence, as well as an outline indicating the range
* of characters that will be selected by [JumpMode.TARGET].
*/
private object SearchedWordWithOutlineRenderer: CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
SearchedWordRenderer.paint(editor, highlighter, g)
val chars = editor.immutableText
val startOffset = highlighter.startOffset
if (chars.getOrNull(startOffset)?.isWordPart == true)
drawOutline(g, editor, chars.wordStart(startOffset), chars.wordEnd(startOffset) + 1)
}
private fun drawOutline(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = AceConfig.targetModeColor
g.drawRect(max(0, start.x - JBUI.scale(1)), start.y,
end.x - start.x + JBUI.scale(2), editor.lineHeight)
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.searchHighlightColor)
}
}
/**
* Renders a filled highlight in the background of the first highlighted
* position. Used for regex search queries.
* Renders a filled highlight in the background of the first highlighted position. Used for regex search queries.
*/
private object RegexRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) =
drawSingle(g, editor, highlighter.startOffset)
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawSingle(g, editor, highlighter.startOffset, AceConfig.searchHighlightColor)
}
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int) {
private companion object {
private const val LAYER = HighlighterLayer.LAST + 1
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int, color: Color) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = ColorUtil.withAlpha(AceConfig.searchHighlightColor, 0.2)
g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1)
g.color = color
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int, color: Color) {
val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset)
val char = editor.immutableText.getOrNull(offset)
?.takeUnless { it == '\n' || it == '\t' } ?: ' '
val char = editor.immutableText.getOrNull(offset)?.takeUnless { it == '\n' || it == '\t' } ?: ' '
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = AceConfig.textHighlightColor
g.fillRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
g.color = ColorUtil.withAlpha(AceConfig.searchHighlightColor, 0.2)
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.color = color
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
}
}

View File

@@ -1,6 +1,6 @@
<idea-plugin url="https://github.com/acejump/AceJump">
<idea-plugin>
<name>AceJump</name>
<id>AceJump</id>
<id>AceJump-chylex</id>
<description><![CDATA[
AceJump allows you to quickly navigate the caret to any position visible in the editor.
@@ -9,6 +9,7 @@
</description>
<depends>com.intellij.modules.platform</depends>
<depends>IdeaVIM</depends>
<category>Navigation</category>
<vendor url="https://github.com/acejump/AceJump">AceJump</vendor>
@@ -20,98 +21,31 @@
id="preferences.AceConfigurable" dynamic="true"/>
<editorActionHandler action="EditorEscape" order="first"
implementationClass="org.acejump.action.AceEditorAction$Reset"
id="AceHandlerEscape"/>
implementationClass="org.acejump.action.AceEditorAction$Reset"/>
<editorActionHandler action="EditorBackSpace" order="first"
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"
id="AceHandlerBackSpace"/>
<editorActionHandler action="EditorStartNewLine" order="first"
implementationClass="org.acejump.action.AceEditorAction$SelectBackward"
id="AceHandlerStartNewLine"/>
<editorActionHandler action="EditorEnter" order="first"
implementationClass="org.acejump.action.AceEditorAction$SelectForward"
id="AceHandlerEnter"/>
<editorActionHandler action="EditorTab" order="first"
implementationClass="org.acejump.action.AceEditorAction$ScrollToNextScreenful"
id="AceHandlerTab"/>
<editorActionHandler action="EditorUnindentSelection" order="first"
implementationClass="org.acejump.action.AceEditorAction$ScrollToPreviousScreenful"
id="AceHandlerUnindentSelection"/>
<editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"
id="AceHandlerUp"/>
<editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"
id="AceHandlerLeft"/>
<editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"
id="AceHandlerLineStart"/>
<editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"
id="AceHandlerRight"/>
<editorActionHandler action="EditorLineEnd" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"
id="AceHandlerLineEnd"/>
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
<editorActionHandler action="EditorEnter" order="first, before terminalEnter"
implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/>
</extensions>
<actions>
<action id="AceAction"
class="org.acejump.action.AceAction$ActivateOrCycleMode"
text="Activate / Cycle AceJump Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action>
<action id="AceReverseAction"
class="org.acejump.action.AceAction$ActivateOrReverseCycleMode"
text="Activate / Reverse Cycle AceJump Mode"/>
<action id="AceForwardAction"
class="org.acejump.action.AceAction$ToggleForwardJumpMode"
text="Start AceJump in Jump After Caret Mode"/>
<action id="AceBackwardAction"
class="org.acejump.action.AceAction$ToggleBackwardJumpMode"
text="Start AceJump in Jump Before Caret Mode"/>
<action id="AceWordStartAction"
class="org.acejump.action.AceAction$ToggleJumpMode"
text="Start AceJump in Jump Mode"/>
<action id="AceWordEndAction"
class="org.acejump.action.AceAction$ToggleJumpEndMode"
text="Start AceJump in Jump End Mode"/>
<action id="AceTargetAction"
class="org.acejump.action.AceAction$ToggleTargetMode"
text="Start AceJump in Target Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl alt SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl alt SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt SEMICOLON"/>
</action>
<action id="AceDeclarationAction"
class="org.acejump.action.AceAction$ToggleDeclarationMode"
text="Start AceJump in Declaration Mode"/>
<action id="AceLineAction"
class="org.acejump.action.AceAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
</action>
<action id="AceLineStartsAction"
class="org.acejump.action.AceAction$StartAllLineStartsMode"
text="Start AceJump in All Line Starts Mode"/>
<action id="AceLineEndsAction"
class="org.acejump.action.AceAction$StartAllLineEndsMode"
text="Start AceJump in All Line Ends Mode"/>
<action id="AceLineIndentsAction"
class="org.acejump.action.AceAction$StartAllLineIndentsMode"
text="Start AceJump in All Line Indents Mode"/>
<action id="AceWordAction"
class="org.acejump.action.AceAction$StartAllWordsMode"
text="Start AceJump in All Words Mode"/>
<action id="AceWordForwardAction"
class="org.acejump.action.AceAction$StartAllWordsForwardMode"
text="Start AceJump in All Words After Caret Mode"/>
<action id="AceWordBackwardsAction"
class="org.acejump.action.AceAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/>
<action id="AceVimAction_JumpAllEditors" class="org.acejump.action.AceVimAction$JumpAllEditors" text="AceJump Vim - Jump All Editors" />
<action id="AceVimAction_JumpAllEditors_GoToDeclaration" class="org.acejump.action.AceVimAction$JumpAllEditorsGoToDeclaration" text="AceJump Vim - Jump All Editors - Go To Declaration" />
<action id="AceVimAction_JumpForward" class="org.acejump.action.AceVimAction$JumpForward" text="AceJump Vim - Jump Forward" />
<action id="AceVimAction_JumpBackward" class="org.acejump.action.AceVimAction$JumpBackward" text="AceJump Vim - Jump Backward" />
<action id="AceVimAction_JumpTillForward" class="org.acejump.action.AceVimAction$JumpTillForward" text="AceJump Vim - Jump Till Forward" />
<action id="AceVimAction_JumpTillBackward" class="org.acejump.action.AceVimAction$JumpTillBackward" text="AceJump Vim - Jump Till Backward" />
<action id="AceVimAction_JumpOnLineForward" class="org.acejump.action.AceVimAction$JumpOnLineForward" text="AceJump Vim - Jump On Line Forward" />
<action id="AceVimAction_JumpOnLineBackward" class="org.acejump.action.AceVimAction$JumpOnLineBackward" text="AceJump Vim - Jump On Line Backward" />
<action id="AceVimAction_JumpLineIndentsForward" class="org.acejump.action.AceVimAction$JumpLineIndentsForward" text="AceJump Vim - Jump Line Indents Forward" />
<action id="AceVimAction_JumpLineIndentsBackward" class="org.acejump.action.AceVimAction$JumpLineIndentsBackward" text="AceJump Vim - Jump Line Indents Backward" />
<action id="AceVimAction_JumpLWordForward" class="org.acejump.action.AceVimAction$JumpLWordForward" text="AceJump Vim - Jump LWord Forward" />
<action id="AceVimAction_JumpUWordForward" class="org.acejump.action.AceVimAction$JumpUWordForward" text="AceJump Vim - Jump UWord Forward" />
<action id="AceVimAction_JumpLWordBackward" class="org.acejump.action.AceVimAction$JumpLWordBackward" text="AceJump Vim - Jump LWord Backward" />
<action id="AceVimAction_JumpUWordBackward" class="org.acejump.action.AceVimAction$JumpUWordBackward" text="AceJump Vim - Jump UWord Backward" />
<action id="AceVimAction_JumpLWordEndForward" class="org.acejump.action.AceVimAction$JumpLWordEndForward" text="AceJump Vim - Jump LWord End Forward" />
<action id="AceVimAction_JumpUWordEndForward" class="org.acejump.action.AceVimAction$JumpUWordEndForward" text="AceJump Vim - Jump UWord End Forward" />
<action id="AceVimAction_JumpLWordEndBackward" class="org.acejump.action.AceVimAction$JumpLWordEndBackward" text="AceJump Vim - Jump LWord End Backward" />
<action id="AceVimAction_JumpUWordEndBackward" class="org.acejump.action.AceVimAction$JumpUWordEndBackward" text="AceJump Vim - Jump UWord End Backward" />
</actions>
</idea-plugin>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 40 41" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 40 41" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-635)">
<g id="pluginIcon" transform="matrix(0.154082,0,0,0.154082,-39.7746,553.645)">
<rect x="258.139" y="529.264" width="259.602" height="259.602" style="fill:none;"/>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -1,165 +0,0 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
import com.intellij.openapi.editor.actions.EnterAction
import org.acejump.action.AceAction
import org.acejump.config.AceConfig
import org.acejump.test.util.BaseTest
/**
* Functional test cases and end-to-end performance tests.
*
* TODO: Add more structure to test cases, use test resources to define files.
*/
class AceTest : BaseTest() {
fun `test that scanner finds all occurrences of single character`() =
assertEquals("test test test".search("t"), setOf(0, 3, 5, 8, 10, 13))
fun `test empty results for an absent query`() =
assertEmpty("test test test".search("best"))
fun `test sticky results on a query with extra characters`() =
assertEquals("test test test".search("testz"), setOf(0, 5, 10))
fun `test a query inside text with some variations`() =
assertEquals("abcd dabc cdab".search("cd"), setOf(2, 10))
fun `test a query containing a space character`() =
assertEquals("abcd dabc cdab".search("cd "), setOf(2))
fun `test a query containing a { character`() =
assertEquals("abcd{dabc cdab".search("cd{"), setOf(2))
fun `test that jumping to first occurrence succeeds`() {
"<caret>testing 1234".search("1")
takeAction(ACTION_EDITOR_ENTER)
myFixture.checkResult("testing <caret>1234")
}
fun `test that jumping to second occurrence succeeds`() {
"<caret>testing 1234".search("ti")
takeAction(ACTION_EDITOR_ENTER)
myFixture.checkResult("tes<caret>ting 1234")
}
fun `test that jumping to previous occurrence succeeds`() {
"te<caret>sting 1234".search("t")
takeAction(ACTION_EDITOR_START_NEW_LINE)
myFixture.checkResult("<caret>testing 1234")
}
fun `test tag selection`() {
"<caret>testing 1234".search("g")
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("testin<caret>g 1234")
}
fun `test shift selection`() {
"<caret>testing 1234".search("4")
typeAndWaitForResults(session.tags[0].key.uppercase())
myFixture.checkResult("<selection>testing 123<caret></selection>4")
}
fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two")
takeAction(AceAction.StartAllWordsBackwardsMode())
assertEquals(2, session.tags.size)
}
fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four")
takeAction(AceAction.StartAllWordsForwardMode())
assertEquals(4, session.tags.size)
}
fun `test word mode`() {
makeEditor("test word action")
takeAction(AceAction.StartAllWordsMode())
assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key)
myFixture.checkResult("test <caret>word action")
}
fun `test target mode`() {
"<caret>test target action".search("target")
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("test <selection>target<caret></selection> action")
}
fun `test cache invalidation`() {
"first line".search("first")
typeAndWaitForResults(session.tags[0].key)
repeat(3) { takeAction(EnterAction()) }
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults("first")
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("\n\n\n<selection>first<caret></selection> line")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines")
takeAction(AceAction.StartAllLineMarksMode())
assertEquals(9, session.tags.size)
}
fun `test chinese selection`() {
AceConfig.settings.mapToASCII = true
"test 拼音 selection".search("py")
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("test <selection>拼音<caret></selection> selection")
}
fun `test japanese selection`() {
AceConfig.settings.mapToASCII = true
"あみだにょらい".search("am")
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("<selection>あみだにょらい<caret></selection>")
}
// https://github.com/acejump/AceJump/issues/355
fun `ignore test a word that is difficult to tag`() {
makeEditor("aaCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
takeAction(AceAction.ActivateOrCycleMode())
typeAndWaitForResults("c")
assertEquals(2, session.tags.size)
}
}

View File

@@ -1,188 +0,0 @@
import com.intellij.mock.MockVirtualFile
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.util.ui.UIUtil
import it.unimi.dsi.fastutil.ints.IntArrayList
import junit.framework.TestCase
import org.acejump.action.AceAction
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
import org.acejump.input.JumpMode
import org.acejump.search.Pattern.ALL_WORDS
import org.acejump.session.AceJumpListener
import org.acejump.session.SessionManager
import org.acejump.test.util.BaseTest
/**
* Test [org.acejump.ExternalUsage] endpoints.
*/
class ExternalUsageTest: BaseTest() {
fun `test externally tagged results and listener notification`() {
makeEditor("test externally tagged results")
SessionManager.start(myFixture.editor).markResults(sortedSetOf(4, 10, 15))
TestCase.assertEquals(3, session.tags.size)
var shouldBeTrueAfterFinished = false
session.addAceJumpListener(object: AceJumpListener {
override fun finished(mark: String?, query: String?) {
shouldBeTrueAfterFinished = true
}
})
typeAndWaitForResults(session.tags[0].key)
TestCase.assertTrue(shouldBeTrueAfterFinished)
}
fun `test externally tagged results with multiple editors`() {
val fileA = MockVirtualFile("a.txt", "first file")
val fileB = MockVirtualFile("b.txt", "second file with more markers")
manager?.openFile(fileA, true)
manager?.openFile(fileB, false)
val mainEditor = (manager?.selectedEditor as TextEditor).editor
val editorA = (manager?.getEditors(fileA)?.single() as TextEditor).editor
val editorB = (manager?.getEditors(fileB)?.single() as TextEditor).editor
val session = SessionManager.start(mainEditor, listOf(editorA, editorB))
session.markResults(mapOf(
editorA to IntArrayList(intArrayOf(0, 6)),
editorB to IntArrayList(intArrayOf(0, 7, 22))
))
TestCase.assertEquals(5, session.tags.size)
TestCase.assertEquals(2, session.tags.count { it.value.editor === editorA })
TestCase.assertEquals(3, session.tags.count { it.value.editor === editorB })
TestCase.assertEquals(listOf(0, 6), session.tags
.filter { it.value.editor === editorA }
.map { it.value.offset }
.sorted())
TestCase.assertEquals(listOf(0, 7, 22), session.tags
.filter { it.value.editor === editorB }
.map { it.value.offset }
.sorted())
}
fun `test external pattern usage`() {
makeEditor("test external pattern usage")
SessionManager.start(myFixture.editor)
.startRegexSearch(ALL_WORDS, WHOLE_FILE)
TestCase.assertEquals(4, session.tags.size)
}
fun `test external regex usage`() {
makeEditor("test external regex usage")
SessionManager.start(myFixture.editor)
.startRegexSearch("[aeiou]+", WHOLE_FILE)
TestCase.assertEquals(9, session.tags.size)
}
fun `test external jump with bounds`() {
makeEditor("test word and word usage")
SessionManager.start(myFixture.editor)
.toggleJumpMode(JumpMode.JUMP, object : Boundaries {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 14..18
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in 14..18
}
})
typeAndWaitForResults("word")
TestCase.assertEquals(1, session.tags.size)
TestCase.assertEquals(14, session.tags.single().value.offset)
}
fun `test listener query and mark`() {
"<caret>testing 1234".search("g")
var detectedMark: String? = null
var detectedQuery: String? = null
session.addAceJumpListener(object: AceJumpListener {
override fun finished(mark: String?, query: String?) {
detectedMark = mark
detectedQuery = query
}
})
val mark = session.tags[0].key
typeAndWaitForResults(mark)
TestCase.assertEquals(mark, detectedMark)
TestCase.assertEquals("g", detectedQuery)
}
fun `test listener after escape`() {
"<caret>testing 1234".search("g")
var detectedMark: String? = null
var detectedQuery: String? = null
session.addAceJumpListener(object: AceJumpListener {
override fun finished(mark: String?, query: String?) {
detectedMark = mark
detectedQuery = query
}
})
myFixture.performEditorAction("EditorEscape")
UIUtil.dispatchAllInvocationEvents()
TestCase.assertEquals(null, detectedMark)
TestCase.assertEquals(null, detectedQuery)
}
fun `test listener for word motion`() {
makeEditor("test word action")
takeAction(AceAction.StartAllWordsMode())
var detectedMark: String? = null
var detectedQuery: String? = null
session.addAceJumpListener(object: AceJumpListener {
override fun finished(mark: String?, query: String?) {
detectedMark = mark
detectedQuery = query
}
})
val mark = session.tags[1].key
typeAndWaitForResults(mark)
TestCase.assertEquals(mark, detectedMark)
TestCase.assertEquals("", detectedQuery)
}
fun `test do not remove other highlights when the session ends`() {
makeEditor("test do not remove other highlights when the session ends")
val markupModel = myFixture.editor.markupModel
val layer = HighlighterLayer.SELECTION - 1
val existedHighlighter = markupModel.addRangeHighlighter(0, 1, layer, null, HighlighterTargetArea.EXACT_RANGE)
takeAction(AceAction.StartAllWordsMode())
val mark = session.tags[0].key
typeAndWaitForResults(mark)
TestCase.assertEquals("last session should be disposed", null, SessionManager[myFixture.editor])
TestCase.assertTrue("existed highlighter should not be removed", existedHighlighter.isValid)
existedHighlighter.dispose()
}
}

View File

@@ -1,12 +1,12 @@
import org.acejump.action.AceAction
import org.acejump.action.AceVimAction
import org.acejump.test.util.BaseTest
import org.junit.Ignore
import java.io.File
import kotlin.random.Random
import kotlin.system.measureTimeMillis
@Ignore
class LatencyTest : BaseTest() {
private fun `test tag latency`(editorText: String) {
val chars = editorText.toCharArray().distinct().filter { !it.isWhitespace() }
val avg = averageTimeWithWarmup(warmupRuns = 10, timedRuns = 10) {
@@ -14,7 +14,7 @@ class LatencyTest: BaseTest() {
for (query in chars) {
makeEditor(editorText)
myFixture.testAction(AceAction.ActivateOrCycleMode())
myFixture.testAction(AceVimAction.JumpAllEditors())
time += measureTimeMillis { typeAndWaitForResults("$query") }
// TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" }
resetEditor()
@@ -37,6 +37,8 @@ class LatencyTest: BaseTest() {
)
fun `test lorem ipsum latency`() = `test tag latency`(
File(javaClass.classLoader.getResource("lipsum.txt")!!.file).readText()
File(
javaClass.classLoader.getResource("lipsum.txt")!!.file
).readText()
)
}

View File

@@ -2,24 +2,33 @@ package org.acejump.test.util
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.psi.PsiFile
import com.intellij.testFramework.FileEditorManagerTestCase
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.UIUtil
import org.acejump.action.AceAction
import org.acejump.action.AceVimAction
import org.acejump.session.SessionManager
abstract class BaseTest: FileEditorManagerTestCase() {
abstract class BaseTest : BasePlatformTestCase() {
companion object {
inline fun averageTimeWithWarmup(warmupRuns: Int, timedRuns: Int, action: () -> Long): Long {
repeat(warmupRuns) { action() }
repeat(warmupRuns) {
action()
}
var time = 0L
repeat(timedRuns) { time += action() }
repeat(timedRuns) {
time += action()
}
return time / timedRuns
}
}
protected val session get() = SessionManager[myFixture.editor]!!
protected val session
get() = SessionManager[myFixture.editor]!!
override fun tearDown() {
resetEditor()
@@ -29,16 +38,16 @@ abstract class BaseTest: FileEditorManagerTestCase() {
fun takeAction(action: String) = myFixture.performEditorAction(action)
fun takeAction(action: AnAction) = myFixture.testAction(action)
fun makeEditor(contents: String): PsiFile =
myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
fun makeEditor(contents: String): PsiFile {
val file = myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
(myFixture.editor as EditorImpl).scrollPane.viewport.setSize(1000, 100)
return file
}
fun resetEditor() {
myFixture.editor?.let {
takeAction(IdeActions.ACTION_EDITOR_ESCAPE)
UIUtil.dispatchAllInvocationEvents()
assertEmpty(it.markupModel.allHighlighters)
}
manager?.closeAllFiles()
assertEmpty(myFixture.editor.markupModel.allHighlighters)
}
fun typeAndWaitForResults(string: String) {
@@ -46,11 +55,13 @@ abstract class BaseTest: FileEditorManagerTestCase() {
UIUtil.dispatchAllInvocationEvents()
}
fun String.executeQuery(query: String) = myFixture.run {
private fun String.executeQuery(query: String) {
myFixture.run {
makeEditor(this@executeQuery)
testAction(AceAction.ActivateOrCycleMode())
testAction(AceVimAction.JumpAllEditors())
typeAndWaitForResults(query)
}
}
fun String.search(query: String): Set<Int> {
this@search.executeQuery(query)
@@ -58,8 +69,10 @@ abstract class BaseTest: FileEditorManagerTestCase() {
return myFixture.editor.markupModel.allHighlighters.map { it.startOffset }.toSet()
}
private fun String.assertCorrectNumberOfTags(query: String) =
private fun String.assertCorrectNumberOfTags(query: String) {
assertEquals(split(query.fold("") { prefix, char ->
if ((prefix + char) in this) prefix + char else return
}).size - 1, myFixture.editor.markupModel.allHighlighters.size)
}
}