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

30 Commits

Author SHA1 Message Date
49dd8ddaf6 [WIP] Simplify tags by only tagging if the search query has at 2 or more alnum characters (or other symbols) 2021-01-16 01:19:05 +01:00
752e661bb7 [WIP] Make most actions always restore carets & tweak intention/refactor actions 2021-01-16 00:24:16 +01:00
a4eff1f5a6 [WIP] Add 'Clone/Move to Caret' actions and quick selection modes to 'Between Points' mode 2021-01-16 00:18:23 +01:00
0e4caafbba [WIP] Implement shift mode for 'From Caret' and 'Between Points' modes 2021-01-09 18:08:36 +01:00
bf2cb0b0a1 [WIP] Fix selection overlaps in shift mode (again) 2021-01-09 18:08:29 +01:00
af10e87811 [WIP] Add 'Between Points' cycle mode 2021-01-09 17:01:52 +01:00
8859498066 [WIP] Move 'Select to Caret' modes into an old style cycle-able AceJump mode 2021-01-09 17:01:51 +01:00
76b751274b [WIP] Remove tag visiting functionality 2021-01-09 14:38:51 +01:00
197ae268dc [WIP] Remove whole file search 2021-01-09 09:52:41 +01:00
ffbb3747b4 [WIP] Remove currently broken multi-caret mode 2021-01-04 22:56:13 +01:00
30a9ebb471 [WIP] Fix issue with caret/selection append modes 2021-01-04 22:46:28 +01:00
31cfbcc940 [WIP] Replace 'Caret to...' sub-menu with Shift modifier for selection-based modes 2021-01-04 22:24:38 +01:00
8891fd562a Fix occasional conflicts between tags and search query when assigning vacant results 2020-12-23 03:28:30 +01:00
d46cc80ccf [WIP] Make Shift mode add caret/selection to existing ones 2020-12-23 02:57:13 +01:00
3507aebed0 [WIP] Refactor new modes 2020-12-23 02:18:10 +01:00
2ef2d32f15 [WIP] Fix selection mode tooltip 2020-12-22 04:26:20 +01:00
5400910c7d [WIP] Add caret selection variants to all selection-based modes 2020-12-22 04:21:07 +01:00
f08286cd8d [WIP] Add cut/copy/paste modes 2020-12-22 04:19:22 +01:00
1ccab7cd32 [WIP] Add copy/paste modes 2020-12-21 20:14:31 +01:00
53e882e752 Remove unnecessary method 2020-12-21 19:41:47 +01:00
658c2062a7 [WIP] Add interactive modes for selection and deletion 2020-12-21 06:04:01 +01:00
402517c412 Add 'Refactor' action 2020-12-18 13:59:00 +01:00
5e1a6f47a1 Make selection jumps expand current selection 2020-12-14 13:32:46 +01:00
42232fa0f2 Add multi-caret session mode 2020-12-14 12:57:51 +01:00
56010a7128 Replace 'jump modes' with various actions activated by letters after typing a tag 2020-12-14 12:32:10 +01:00
d7789be9a3 Prevent editing document while AceJump is active 2020-12-13 08:04:15 +01:00
ed0fc19cfb Enforce LF line endings & fix build.gradle 2020-12-13 07:13:11 +01:00
ad8c84be27 Make jump mode cycling wrap around & add shortcut to cycle modes in reverse 2020-12-13 06:00:41 +01:00
f44003b47a Add all regex search patterns to keymap 2020-12-13 06:00:36 +01:00
feaffb3640 Major AceJump refactoring!
See https://github.com/acejump/AceJump/issues/348 for information on what's changed and what more needs to be done.
2020-12-06 06:46:10 +01:00
59 changed files with 2126 additions and 2913 deletions

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

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

View File

@@ -1,336 +1,224 @@
# Changelog # Changelog
## Unreleased ### 3.6.4
## 3.8.19 - Improvements to tag latency. Thanks to @chylex for [the PR](https://github.com/acejump/AceJump/pull/339)!
- Enable support for 2024.1, fixing ([#457](https://github.com/acejump/AceJump/issues/457)) ### 3.6.3
## 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
- Adds option to switch between straight and rounded tag corners
- Adds option to only consider visible area
- Add customizable jump mode cycling
- 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)!
## 3.6.3
- Vote for your favorite <a href="https://twitter.com/breandan/status/1274169810411274241">AceJump logo</a>! - Vote for your favorite <a href="https://twitter.com/breandan/status/1274169810411274241">AceJump logo</a>!
- Fixes potential bug. - Fixes potential bug.
- Increases test coverage. - 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. - 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 [#325](https://github.com/acejump/AceJump/issues/325).
- Fixes Pinyin support. - Fixes Pinyin support.
## 3.6.0 ### 3.6.0
- Adds support for Chinese [#314](https://github.com/acejump/AceJump/issues/314). - 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). - 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). - 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). - 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 - Tagging improvements
- Support for external plugin integration - Support for external plugin integration
- Fixes [#304](https://github.com/acejump/AceJump/issues/304), [#255](https://github.com/acejump/AceJump/issues/255) - 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. - <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 - 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. - 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) - 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>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) - <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) - 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. - 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. - 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. - Various improvements to settings page, including a keyboard layout selector.
- Shorter tags on average, AceJump tries to use a single-character tag more often. - 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. - 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. - 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). - 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. - **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). - 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. - 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. - 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). - **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). - 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). - 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. - 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. - 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. - 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). - Fix for [#129](https://github.com/acejump/AceJump/issues/129).
## 3.3.5 ### 3.3.5
- Minor bugfix release. Improve handling of window resizing. - Minor bugfix release. Improve handling of window resizing.
## 3.3.4 ### 3.3.4
- Add a settings page. (Settings > Tools > AceJump) - Add a settings page. (Settings > Tools > AceJump)
## 3.3.3 ### 3.3.3
- Improve latency and fix a bug in line selection mode. - 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. - 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. - 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! - 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. - Minor fixes and stability improvements.
## 3.2.6 ### 3.2.6
- Fixes an error affecting older versions of the IntelliJ Platform. - 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. - 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. - 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. - 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. - Adds scrolling support and fixes some line spacing issues.
## 3.2.1 ### 3.2.1
- AceJump now synchronizes font style changes in real-time. - AceJump now synchronizes font style changes in real-time.
## 3.2.0 ### 3.2.0
- Support Back/Forward navigation in the IntelliJ Platform. - 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. - 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 - 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+; - Allow users to enter target mode directly by pressing Ctrl+Alt+;
## 3.1.4 ### 3.1.4
- Fixes the "Assertion Failed" exception popup - Fixes the "Assertion Failed" exception popup
## 3.1.3 ### 3.1.3
- Fixes an error affecting some users during startup. - Fixes an error affecting some users during startup.
## 3.1.2 ### 3.1.2
- Fixes an Android Studio regression. - Fixes an Android Studio regression.
## 3.1.1 ### 3.1.1
- Hotfix for broken target mode. - Hotfix for broken target mode.
## 3.1.0 ### 3.1.0
- Removes the search box, lots of small usability improvements. - Removes the search box, lots of small usability improvements.
## 3.0.7 ### 3.0.7
- No longer tags "folded" regions and minor alignment adjustments. - 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. - 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. - Hotfix for target mode.
## 3.0.4 ### 3.0.4
- Adds *Line Mode* - press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>;</kbd> to activate. - 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. - Updates to tag placement and performance improvements.
## 3.0.2 ### 3.0.2
- Fixes target mode and default shortcut activation for Mac users. - 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. - 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: - Major rewrite of AceJump. Introducing:
* Realtime search: Just type the word where you want to jump and AceJump will do the rest. * Realtime search: Just type the word where you want to jump and AceJump will do the rest.
@@ -338,82 +226,82 @@
* Keyboard-aware tagging: Tries to minimize finger travel distance on QWERTY keyboards. * Keyboard-aware tagging: Tries to minimize finger travel distance on QWERTY keyboards.
* Colorful highlighting: AceJump will now highlight the editor text, as you type. * 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. - 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) - 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. - One hundred percent all natural Kotlin.
## 2.0.10 ### 2.0.10
- Support 2016.2, remove upper version limit, update internal Kotlin version - 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) - 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) - 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. - 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) - 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) - 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) - 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 - More work on Ubuntu focus bug
## 2.0.2 ### 2.0.2
- Fixed bug when there's only 1 search result - Fixed bug when there's only 1 search result
## 2.0.1 ### 2.0.1
- Fixing Ubuntu focus bug - Fixing Ubuntu focus bug
## 2.0.0 ### 2.0.0
- Major release: Added "target mode", many speed increases, multi-char search implemented - 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 - 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) - 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 - 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) - 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) - 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 - Cleaned up code base for release

152
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://plugins.jetbrains.com/plugin/7086"> <img src="logo.png" alt="AceJumpLogo"></a></p>
<p align="center"> <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://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="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> <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) ![](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 ## Tips
- Press <kbd>Tab</kbd> when searching to jump to the next group of matches in the editor. - Press <kbd>Tab</kbd> when searching to jump to the next group of matches in the editor.
@@ -31,10 +33,6 @@ Press the AceJump shortcut, followed by <kbd>→</kbd> to target the last, <kbd>
- If you make a mistake searching, just press <kbd>Backspace</kbd> to restart from scratch. - If you make a mistake searching, just press <kbd>Backspace</kbd> to restart from scratch.
- If no matches can be found on-screen, AceJump will scroll to the next match it can find. - 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. - Pressing <kbd>Enter</kbd> or <kbd>Shift</kbd>+<kbd>Enter</kbd> during a search will cycle through tagged results on screen.
@@ -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"**. 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 ## 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: [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 ' echo -e '
" Press `f` to activate AceJump " Press `f` to activate AceJump
map f <Action>(AceAction) map f :action AceAction<CR>
" Press `F` to activate Target Mode " Press `F` to activate Target Mode
map F <Action>(AceTargetAction) map F :action AceTargetAction<CR>
" Press `g` to activate Line Mode " Press `g` to activate Line Mode
map g <Action>(AceLineAction) map g :action AceLineAction<CR>
' >> ~/.ideavimrc ' >> ~/.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>. 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) ![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).* *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 ## Contributing
AceJump is supported by community members like you. Contributions are highly welcome! 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) * [Improve test coverage](https://github.com/acejump/AceJump/issues/139)
* [Add action to repeat last search](https://github.com/acejump/AceJump/issues/316) * [Add option to place the caret after the search text](https://github.com/acejump/AceJump/issues/225)
* [Add configurable RegEx modes](https://github.com/acejump/AceJump/issues/215) * [Support user-configurable keyboard layouts](https://github.com/acejump/AceJump/issues/172)
* [Add font family and size options](https://github.com/acejump/AceJump/issues/192) * [Speed up tagging on large files](https://github.com/acejump/AceJump/issues/217)
* [Tag placement and visibility improvements](https://github.com/acejump/AceJump/issues/323)
* [Animated documentation](https://github.com/acejump/AceJump/issues/145) * [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) * [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]`. To start [IntelliJ IDEA CE](https://github.com/JetBrains/intellij-community) with AceJump installed, run `./gradlew runIde -PluginDev [-x test]`.
@@ -173,52 +128,41 @@ 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>). * **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. * **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. * **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: The following plugins have a similar UI for navigating text and web browsing:
| Source Code | Download | Application | Actively Maintained | Language | | 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/) | | 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/) | | [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) | :heavy_check_mark: | [Java](https://www.java.com) |
| [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) | | [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) | :x: | [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 | :x: | [Kotlin](http://kotlinlang.org/) | | [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) | | [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) | | [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/) | | [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/) | | [Sublime EasyMotion](https://github.com/tednaleid/sublime-EasyMotion) | [](https://packagecontrol.io/packages/EasyMotion) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.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/) | | [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/) |
| [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) | | [Jumpy](https://github.com/DavidLGoldberg/jumpy) | [](https://atom.io/packages/jumpy) | [Atom](https://atom.io/) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.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) | | [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/) |
| [Sublime EasyMotion](https://github.com/tednaleid/sublime-EasyMotion) | [](https://packagecontrol.io/packages/EasyMotion) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.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/) |
| [AceJump](https://github.com/ice9js/ace-jump-sublime) | [](https://packagecontrol.io/packages/AceJump) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.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/) |
| [Jumpy](https://github.com/DavidLGoldberg/jumpy) | [](https://atom.io/packages/jumpy) | [Atom](https://atom.io/) | :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/) | :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/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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [Vimperator](https://github.com/vimperator/vimperator-labs/) | [](https://github.com/vimperator/vimperator-labs/releases) | [Firefox](https://www.mozilla.org/firefox) | :x: | [JavaScript](https://www.javascript.com/) |
| [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) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) | | [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/) |
| [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/) |
| [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/) |
| [Vimperator](https://github.com/vimperator/vimperator-labs/) | [](https://github.com/vimperator/vimperator-labs/releases) | [Firefox](https://www.mozilla.org/firefox) | :x: | [JavaScript](https://www.javascript.com/) |
| [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/) |
## Acknowledgements ## 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. * [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. * [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). * [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/pull/339).
* [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency. * [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. * [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. * [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,46 @@
import org.jetbrains.changelog.Changelog.OutputType.HTML import org.jetbrains.changelog.closure
import org.jetbrains.changelog.date import org.jetbrains.intellij.tasks.PatchPluginXmlTask
import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
idea idea apply true
alias(libs.plugins.kotlin) // Kotlin support kotlin("jvm") version "1.3.72"
alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin id("org.jetbrains.intellij") version "0.6.5"
alias(libs.plugins.changelog) // Gradle Changelog Plugin id("org.jetbrains.changelog") version "0.6.2"
alias(libs.plugins.kover) // Gradle Kover Plugin
id("com.github.ben-manes.versions") version "0.51.0"
} }
tasks { tasks {
named<Zip>("buildPlugin") { withType<KotlinCompile> {
dependsOn("test") kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
archiveFileName = "AceJump.zip" kotlinOptions.freeCompilerArgs += "-progressive"
} }
runIde { withType<PatchPluginXmlTask> {
findProperty("luginDev")?.let { args = listOf(projectDir.absolutePath) } sinceBuild("201.6668.0")
} changeNotes({ changelog.getLatest().toHTML() })
publishPlugin {
val intellijPublishToken: String? by project
token = intellijPublishToken
}
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")
})
)
} }
} }
kotlin {
jvmToolchain(17)
sourceSets.all {
languageSettings.apply {
languageVersion = "2.0"
}
}
}
val acejumpVersion = "3.8.19"
changelog { changelog {
version = acejumpVersion
path = "${project.projectDir}/CHANGES.md" path = "${project.projectDir}/CHANGES.md"
header = provider { "[${project.version}] - ${date()}" } header = closure { "${project.version}" }
itemPrefix = "-" }
unreleasedTerm = "Unreleased"
dependencies {
compileOnly(kotlin("stdlib-jdk8"))
} }
repositories { repositories {
mavenCentral() mavenCentral()
intellijPlatform.defaultRepositories() jcenter()
// intellijPlatform.localPlatformArtifacts()
} }
dependencies { intellij {
// https://github.com/anyascii/anyascii version = "2020.2"
implementation("com.anyascii:anyascii:0.3.2") pluginName = "AceJump"
intellijPlatform{ updateSinceUntilBuild = false
testImplementation(libs.junit) setPlugins("java")
bundledPlugins("com.intellij.java")
create("IC", "2024.1.4")
pluginVerifier()
instrumentationTools()
testFramework(TestFrameworkType.Platform)
}
}
intellijPlatform {
pluginConfiguration {
version = acejumpVersion
name = "AceJump"
}
pluginVerification.ides.recommended()
} }
group = "org.acejump" group = "org.acejump"
version = acejumpVersion version = "4.0"

View File

@@ -1,4 +0,0 @@
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,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -82,7 +82,6 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -130,7 +129,6 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath

22
gradlew.bat vendored
View File

@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if "%ERRORLEVEL%" == "0" goto init
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto init
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,14 +64,28 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@@ -1,67 +1,35 @@
package org.acejump package org.acejump
import com.anyascii.AnyAscii import com.intellij.openapi.editor.Editor
import com.intellij.diff.util.DiffUtil.getLineCount import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.*
import com.intellij.openapi.util.Computable
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 annotation class ExternalUsage
/** /**
* Returns an immutable version of the currently edited document. * 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()
}
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. * Returns true if [this] contains [otherText] at the specified offset.
*/ */
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean) = fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean): Boolean {
regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase) return this.regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
}
/** /**
* Calculates the length of a common prefix in [this] starting * Calculates the length of a common prefix in [this] starting at index [selfOffset], and [otherText] starting at index 0.
* at index [selfOffset], and [otherText] starting at index 0.
*/ */
fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): Int { fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): Int {
var i = 0 var i = 0
var o = selfOffset + i var o = selfOffset + i
while (i < otherText.length && o < this.length && otherText[i].equals(this[o], ignoreCase = true)) { while (i < otherText.length && o < this.length && otherText[i].equals(this[o], ignoreCase = true)) {
i++ i++
o++ o++
} }
return i return i
} }
@@ -74,188 +42,68 @@ val Char.isWordPart
/** /**
* Finds index of the first character in a word. * Finds index of the first character in a word.
*/ */
inline fun CharSequence.wordStart( inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var start = pos var start = pos
while (start > 0 && isPartOfWord(this[start - 1])) --start while (start > 0 && isPartOfWord(this[start - 1])) {
--start
}
return start return start
} }
/** /**
* Finds index of the last character in a word. * Finds index of the last character in a word.
*/ */
inline fun CharSequence.wordEnd( inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var end = pos var end = pos
while (end < length - 1 && isPartOfWord(this[end + 1])) ++end while (end < length - 1 && isPartOfWord(this[end + 1])) {
++end
}
return end return end
} }
/** /**
* Finds index of the first word character following a sequence of non-word * Finds index of the previous "camelHumps" hump in a word.
* characters following the end of a word.
*/ */
inline fun CharSequence.wordEndPlus( inline fun CharSequence.humpStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart var start = pos
): Int {
while (start > 0 && isPartOfWord(this[start - 1]) && !EditorActionUtil.isHumpBound(this, start, true)) {
--start
}
return start
}
/**
* Finds index of the next "camelHumps" hump in a word.
*/
inline fun CharSequence.humpEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos
while (end < length - 1 && isPartOfWord(this[end + 1]) && !EditorActionUtil.isHumpBound(this, end + 1, false)) {
++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) var end = this.wordEnd(pos, isPartOfWord)
while (end < length - 1 && !isPartOfWord(this[end + 1])) ++end while (end < length - 1 && !isPartOfWord(this[end + 1])) {
++end
if (end < length - 1 && isPartOfWord(this[end + 1])) ++end }
if (end < length - 1 && isPartOfWord(this[end + 1])) {
++end
}
return end return end
} }
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
val clone = HashMap<Editor, IntArrayList>(size)
for ((editor, offsets) in this) {
clone[editor] = offsets.clone()
}
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,63 +4,51 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries.* import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.Pattern.* import org.acejump.search.Pattern
import org.acejump.session.Session import org.acejump.session.Session
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
/** /**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session]. * 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() { sealed class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean = final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext) return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
}
final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val session = SessionManager[editor] val session = SessionManager[editor]
if (session != null) run(session) if (session != null) {
else if (originalHandler.isEnabled(editor, caret, dataContext)) run(session)
}
else if (originalHandler.isEnabled(editor, caret, dataContext)) {
originalHandler.execute(editor, caret, dataContext) originalHandler.execute(editor, caret, dataContext)
}
} }
protected abstract fun run(session: Session) protected abstract fun run(session: Session)
// Actions // Actions
class Reset(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) { class Reset(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.end() override fun run(session: Session) = session.end()
} }
class ClearSearch(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) { class ClearSearch(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.restart() override fun run(session: Session) = session.restart()
} }
class SelectBackward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) { class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitPreviousTag() override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
class SelectForward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) { class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitNextTag() override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
class ScrollToNextScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) { class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) { session.scrollToNextScreenful() } override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.VISIBLE_ON_SCREEN)
}
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)
} }
} }

View File

@@ -0,0 +1,51 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.project.DumbAwareAction
import org.acejump.boundaries.Boundaries
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 actions that create or update an AceJump [Session].
*/
sealed class AceKeyboardAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
invoke(SessionManager.start(e.getData(EDITOR) ?: return))
}
abstract operator fun invoke(session: Session)
/**
* Generic action type that starts a regex search.
*/
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries) : AceKeyboardAction() {
override fun invoke(session: Session) = session.startRegexSearch(pattern, boundaries)
}
/**
* Starts or ends an AceJump session.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.cycleMode()
}
// @formatter:off
object StartAllWordsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, VISIBLE_ON_SCREEN)
object StartAllWordsBackwardsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
object StartAllWordsForwardMode : BaseRegexSearchAction(Pattern.ALL_WORDS, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
object StartAllLineStartsMode : BaseRegexSearchAction(Pattern.LINE_STARTS, VISIBLE_ON_SCREEN)
object StartAllLineEndsMode : BaseRegexSearchAction(Pattern.LINE_ENDS, VISIBLE_ON_SCREEN)
object StartAllLineIndentsMode : BaseRegexSearchAction(Pattern.LINE_INDENTS, VISIBLE_ON_SCREEN)
object StartAllLineMarksMode : BaseRegexSearchAction(Pattern.LINE_ALL_MARKS, VISIBLE_ON_SCREEN)
// @formatter:on
}

View File

@@ -0,0 +1,422 @@
package org.acejump.action
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction
import com.intellij.find.actions.FindUsagesAction
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.ide.actions.CopyAction
import com.intellij.ide.actions.CutAction
import com.intellij.ide.actions.PasteAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import com.intellij.openapi.util.TextRange
import com.intellij.refactoring.actions.RefactoringQuickListPopupAction
import org.acejump.*
import org.acejump.search.SearchProcessor
import kotlin.math.max
/**
* Base class for actions available after typing a tag.
*/
sealed class AceTagAction {
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
abstract class BaseJumpAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
recordCaretPosition(editor)
moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset))
if (shiftMode) {
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
}
abstract fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int
}
abstract class BaseSelectAction : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
if (shiftMode) {
val caretModel = editor.caretModel
val oldCarets = caretModel.caretsAndSelections
val oldOffsetPosition = caretModel.logicalPosition
invoke(editor, searchProcessor, offset)
if (caretModel.caretsAndSelections.any { isSelectionOverlapping(oldOffsetPosition, it) }) {
oldCarets.removeAll { isSelectionOverlapping(oldOffsetPosition, it) }
}
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
else {
invoke(editor, searchProcessor, offset)
}
}
private fun isSelectionOverlapping(offset: LogicalPosition, oldCaret: CaretState): Boolean {
return oldCaret.caretPosition == offset || oldCaret.selectionStart == offset || oldCaret.selectionEnd == offset
}
protected abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int)
}
abstract class BaseWordAction : BaseJumpAction() {
final override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
val matchingChars = countMatchingCharacters(editor, searchProcessor, offset)
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && editor.immutableText.let { it[targetOffset - 1].isWordPart && it[targetOffset].isWordPart }
return getCaretOffset(editor, offset, targetOffset, isInsideWord)
}
abstract fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int
}
abstract class BaseCaretRestoringAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
doInvoke(editor, searchProcessor, offset, shiftMode)
editor.caretModel.caretsAndSelections = oldCarets
}
protected abstract fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
}
private companion object {
fun countMatchingCharacters(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return editor.immutableText.countMatchingCharacters(offset, searchProcessor.query.rawText)
}
fun recordCaretPosition(editor: Editor) = with(editor) {
project?.let { addCurrentPositionToHistory(it, document) }
}
fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
selectionModel.removeSelection(true)
caretModel.removeSecondaryCarets()
caretModel.moveToOffset(offset)
}
fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int, cursorOffset: Int = toOffset) = with(editor) {
selectionModel.removeSelection(true)
selectionModel.setSelection(fromOffset, toOffset)
caretModel.moveToOffset(cursorOffset)
}
fun performAction(action: AnAction) {
ActionManager.getInstance().tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
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() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset
}
}
/**
* On default action, places the caret at the last character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpToSearchEnd : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset + max(0, countMatchingCharacters(editor, searchProcessor, offset) - 1)
}
}
/**
* On default action, places the caret just past the last character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpPastSearchEnd : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset + countMatchingCharacters(editor, searchProcessor, offset)
}
}
/**
* On default action, places the caret at the start 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 last character of the search query, then the caret is
* placed at the first character of the search query.
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordStartTag : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordStart(queryEndOffset)
else
queryStartOffset
}
}
/**
* On default action, 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 last character of the search query, then the caret is
* placed after the last character of the search query.
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordEndTag : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordEnd(queryEndOffset) + 1
else
queryEndOffset
}
}
object SelectQuery : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
recordCaretPosition(editor)
val startOffset = JumpToSearchStart.getCaretOffset(editor, searchProcessor, offset)
val endOffset = JumpPastSearchEnd.getCaretOffset(editor, searchProcessor, offset)
selectRange(editor, startOffset, endOffset)
}
}
/**
* On default action, 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 action, adds the new selection to existing selections.
*/
object SelectWord : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = JumpToWordStartTag.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val endOffset = JumpToWordEndTag.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false)
}
}
}
object SelectHump : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = chars.humpStart(queryEndOffset)
val endOffset = chars.humpEnd(queryEndOffset) + 1
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false)
}
}
}
object SelectLine : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
editor.selectionModel.selectLineAtCaret()
}
}
class SelectExtended(private val extendCount: Int) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
repeat(extendCount) {
performAction(action)
}
}
}
class SelectToCaret(private val jumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val caretModel = editor.caretModel
val oldOffset = caretModel.offset
val oldSelection = editor.selectionModel.takeIf { it.hasSelection(false) }?.let { it.selectionStart..it.selectionEnd }
jumper(editor, searchProcessor, offset, shiftMode = false)
val newOffset = caretModel.offset
if (oldSelection == null) {
selectRange(editor, oldOffset, newOffset)
}
else {
selectRange(editor, minOf(oldOffset, newOffset, oldSelection.first), maxOf(oldOffset, newOffset, oldSelection.last), newOffset)
}
}
}
class SelectBetweenPoints(private val firstOffset: Int, private val secondOffsetJumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false)
selectRange(editor, firstOffset, editor.caretModel.offset)
}
}
class Cut(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
performAction(CutAction())
}
}
class Copy(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
performAction(CopyAction())
}
}
class Paste(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
performAction(PasteAction())
}
}
class Delete(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Delete").run<Throwable> {
editor.selectionModel.let { editor.document.deleteString(it.selectionStart, it.selectionEnd) }
}
}
}
class CloneToCaret(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val document = editor.document
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
val text = document.getText(editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) })
editor.caretModel.caretsAndSelections = oldCarets
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Clone").run<Throwable> {
insertAtCarets(editor, text)
}
}
companion object {
fun insertAtCarets(editor: Editor, text: String) {
val document = editor.document
editor.caretModel.runForEachCaret {
if (it.hasSelection()) {
document.replaceString(it.selectionStart, it.selectionEnd, text)
}
else {
document.insertString(it.offset, text)
}
}
}
}
}
class MoveToCaret(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val document = editor.document
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
val start = editor.selectionModel.selectionStart
val end = editor.selectionModel.selectionEnd
val text = document.getText(TextRange(start, end))
editor.caretModel.caretsAndSelections = oldCarets
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Move").run<Throwable> {
document.deleteString(start, end)
CloneToCaret.insertAtCarets(editor, text)
}
}
}
/**
* 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 end of the search query.
*/
object GoToDeclaration : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
}
}
/**
* On default action, performs the Show Usages action, available via the context menu.
* On shift action, performs the Find Usages action, available via the context menu.
* Always places the caret at the end of the search query.
*/
object ShowUsages : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) FindUsagesAction() else ShowUsagesAction())
}
}
/**
* Performs the Show Context Actions action, available via the context menu or Alt+Enter.
*/
object ShowIntentions : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStartTag(editor, searchProcessor, offset, shiftMode = false)
performAction(ShowIntentionActionsAction())
}
}
object Refactor : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStartTag(editor, searchProcessor, offset, shiftMode = false)
performAction(RefactoringQuickListPopupAction())
}
}
}

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

View File

@@ -3,104 +3,89 @@ package org.acejump.boundaries
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.acejump.read
import java.awt.Point import java.awt.Point
/** /**
* Optionally caches slow operations of (1) retrieving the currently * Optionally caches slow operations of (1) retrieving the currently visible editor area, and (2) converting between editor offsets and
* visible editor area, and (2) converting between editor offsets and
* pixel coordinates. * pixel coordinates.
* *
* To avoid unnecessary overhead, there is no automatic detection of when * To avoid unnecessary overhead, there is no automatic detection of when the editor, its contents, or its visible area has changed, so the
* the editor, its contents, or its visible area has changed, so the cache * cache must only be used for a single rendered frame of a single [Editor].
* must only be used for a single rendered frame of a single [Editor].
*/ */
sealed class EditorOffsetCache { sealed class EditorOffsetCache {
/** /**
* Returns the top left and bottom right points of the visible area rectangle. * Returns the top left and bottom right points of the visible area rectangle.
*/ */
abstract fun visibleArea(editor: Editor): Pair<Point, Point> abstract fun visibleArea(editor: Editor): Pair<Point, Point>
/**
* Returns whether the offset is in the visible area rectangle.
*/
abstract fun isVisible(editor: Editor, offset: Int): Boolean
/** /**
* Returns the editor offset at the provided pixel coordinate. * Returns the editor offset at the provided pixel coordinate.
*/ */
abstract fun xyToOffset(editor: Editor, pos: Point): Int abstract fun xyToOffset(editor: Editor, pos: Point): Int
/** /**
* Returns the top left pixel coordinate of the character at the provided editor offset. * Returns the top left pixel coordinate of the character at the provided editor offset.
*/ */
abstract fun offsetToXY(editor: Editor, offset: Int): Point abstract fun offsetToXY(editor: Editor, offset: Int): Point
companion object { fun new(): EditorOffsetCache = Cache() } companion object {
fun new(): EditorOffsetCache {
private class Cache: EditorOffsetCache() { return Cache()
private var visibleArea: Pair<Point, Point>? = null
private val lineToVisibleOffsetRange = Int2ObjectOpenHashMap<IntRange>()
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 isVisible(editor: Editor, offset: Int): Boolean {
val visualLine = editor.offsetToVisualLine(offset, false)
var visibleRange = lineToVisibleOffsetRange.get(visualLine)
if (visibleRange == null) {
val (topLeft, bottomRight) = visibleArea(editor)
val lineY = editor.visualLineToY(visualLine)
val firstVisibleOffset = xyToOffset(editor, Point(topLeft.x, lineY))
val lastVisibleOffset = xyToOffset(editor, Point(bottomRight.x, lineY))
visibleRange = firstVisibleOffset..lastVisibleOffset
lineToVisibleOffsetRange.put(visualLine, visibleRange)
}
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 offsetToXY(editor: Editor, offset: Int) =
offsetToPoint.get(offset) ?: Uncached.offsetToXY(editor, offset)
.also { offsetToPoint.put(offset, it) }
} }
object Uncached: EditorOffsetCache() { private class Cache : EditorOffsetCache() {
override fun visibleArea(editor: Editor): Pair<Point, Point> = private var visibleArea: Pair<Point, Point>? = null
editor.scrollingModel.visibleArea.let { visibleRect -> private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
Pair( private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
visibleRect.location, visibleRect.location.apply {
translate(visibleRect.width, visibleRect.height) override fun visibleArea(editor: Editor): Pair<Point, Point> {
} return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
) }
}
override fun xyToOffset(editor: Editor, pos: Point): Int {
override fun isVisible(editor: Editor, offset: Int): Boolean { val offset = pointToOffset.getInt(pos)
val (topLeft, bottomRight) = visibleArea(editor)
val pos = offsetToXY(editor, offset) if (offset != -1) {
val x = pos.x return offset
val y = pos.y }
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y return Uncached.xyToOffset(editor, pos).also {
@Suppress("ReplacePutWithAssignment")
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 {
@Suppress("ReplacePutWithAssignment")
offsetToPoint.put(offset, it)
}
}
}
object Uncached : EditorOffsetCache() {
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) }
)
}
override fun xyToOffset(editor: Editor, pos: Point): Int {
return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
}
override fun offsetToXY(editor: Editor, offset: Int): Point {
return editor.offsetToXY(offset, true, false)
} }
override fun xyToOffset(editor: Editor, pos: Point): Int =
read { editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos)) }
override fun offsetToXY(editor: Editor, offset: Int): Point =
editor.offsetToXY(offset, true, false)
} }
} }

View File

@@ -3,14 +3,6 @@ package org.acejump.boundaries
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
enum class StandardBoundaries : Boundaries { 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 { VISIBLE_ON_SCREEN {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange { override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val (topLeft, bottomRight) = cache.visibleArea(editor) val (topLeft, bottomRight) = cache.visibleArea(editor)
@@ -21,23 +13,43 @@ enum class StandardBoundaries : Boundaries {
} }
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean { override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return cache.isVisible(editor, offset)
// If we are not using a cache, calling getOffsetRange will cause additional 1-2 pixel coordinate -> offset lookups, which is a lot
// more expensive than one lookup compared against the visible area.
// However, if we are using a cache, it's likely that the topmost and bottommost positions are already cached whereas the provided
// offset isn't, so we save a lookup for every offset outside the range.
if (cache !== EditorOffsetCache.Uncached && offset !in getOffsetRange(editor, cache)) {
return false
}
val (topLeft, bottomRight) = cache.visibleArea(editor)
val pos = cache.offsetToXY(editor, offset)
val x = pos.x
val y = pos.y
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
} }
}, },
BEFORE_CARET { BEFORE_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) = override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
0..(editor.caretModel.offset) return 0..(editor.caretModel.offset)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean = override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
offset <= editor.caretModel.offset return offset <= editor.caretModel.offset
}
}, },
AFTER_CARET { AFTER_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) = override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
editor.caretModel.offset until editor.document.textLength return editor.caretModel.offset until editor.document.textLength
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean = override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
offset >= editor.caretModel.offset return offset >= editor.caretModel.offset
}
} }
} }

View File

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

View File

@@ -4,52 +4,38 @@ import com.intellij.openapi.options.Configurable
import org.acejump.config.AceConfig.Companion.settings import org.acejump.config.AceConfig.Companion.settings
import org.acejump.input.KeyLayoutCache import org.acejump.input.KeyLayoutCache
class AceConfigurable: Configurable { class AceConfigurable : Configurable {
private val panel by lazy(::AceSettingsPanel) private val panel by lazy(::AceSettingsPanel)
override fun getDisplayName() = "AceJump" override fun getDisplayName() = "AceJump"
override fun createComponent() = panel.rootPanel override fun createComponent() = panel.rootPanel
override fun isModified() = override fun isModified() =
panel.allowedChars != settings.allowedChars || panel.allowedChars != settings.allowedChars ||
panel.keyboardLayout != settings.layout || panel.keyboardLayout != settings.layout ||
panel.cycleMode1 != settings.cycleMode1 || panel.jumpModeColor != settings.jumpModeColor ||
panel.cycleMode2 != settings.cycleMode2 || panel.fromCaretModeColor != settings.fromCaretModeColor ||
panel.cycleMode3 != settings.cycleMode3 || panel.betweenPointsModeColor != settings.betweenPointsModeColor ||
panel.cycleMode4 != settings.cycleMode4 || panel.textHighlightColor != settings.textHighlightColor ||
panel.minQueryLengthInt != settings.minQueryLength || panel.tagForegroundColor != settings.tagForegroundColor ||
panel.jumpModeColor?.rgb != settings.jumpModeColor || panel.tagBackgroundColor != settings.tagBackgroundColor ||
panel.jumpEndModeColor?.rgb != settings.jumpEndModeColor || panel.acceptedTagColor != settings.acceptedTagColor ||
panel.targetModeColor?.rgb != settings.targetModeColor || panel.roundedTagCorners != settings.roundedTagCorners
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
override fun apply() { override fun apply() {
settings.allowedChars = panel.allowedChars settings.allowedChars = panel.allowedChars
settings.layout = panel.keyboardLayout settings.layout = panel.keyboardLayout
settings.cycleMode1 = panel.cycleMode1 panel.jumpModeColor?.let { settings.jumpModeColor = it }
settings.cycleMode2 = panel.cycleMode2 panel.fromCaretModeColor?.let { settings.fromCaretModeColor = it }
settings.cycleMode3 = panel.cycleMode3 panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = it }
settings.cycleMode4 = panel.cycleMode4 panel.textHighlightColor?.let { settings.textHighlightColor = it }
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength panel.tagForegroundColor?.let { settings.tagForegroundColor = it }
panel.jumpModeColor?.let { settings.jumpModeColor = it.rgb } panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it }
panel.jumpEndModeColor?.let { settings.jumpEndModeColor = it.rgb } panel.acceptedTagColor?.let { settings.acceptedTagColor = it }
panel.targetModeColor?.let { settings.targetModeColor = it.rgb } settings.roundedTagCorners = panel.roundedTagCorners
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
KeyLayoutCache.reset(settings) KeyLayoutCache.reset(settings)
} }
override fun reset() = panel.reset(settings) override fun reset() = panel.reset(settings)
} }

View File

@@ -1,43 +1,34 @@
package org.acejump.config package org.acejump.config
import com.intellij.ui.JBColor import com.intellij.util.xmlb.annotations.OptionTag
import org.acejump.input.* import org.acejump.input.KeyLayout
import org.acejump.input.KeyLayout.QWERTY import org.acejump.input.KeyLayout.QWERTY
import java.awt.Color
data class AceSettings( data class AceSettings(
var layout: KeyLayout = QWERTY, var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars, var allowedChars: String = layout.allChars,
var cycleMode1: JumpMode = JumpMode.JUMP,
var cycleMode2: JumpMode = JumpMode.DECLARATION, @OptionTag("jumpModeRGB", converter = ColorConverter::class)
var cycleMode3: JumpMode = JumpMode.TARGET, var jumpModeColor: Color = Color.BLUE,
var cycleMode4: JumpMode = JumpMode.JUMP_END,
var minQueryLength: Int = 1, @OptionTag("fromCaretModeRGB", converter = ColorConverter::class)
var fromCaretModeColor: Color = Color.ORANGE,
var jumpModeColor: Int = 0xFFFFFF,
@OptionTag("betweenPointsModeRGB", converter = ColorConverter::class)
var jumpEndModeColor: Int = 0x33E78A, var betweenPointsModeColor: Color = Color.YELLOW,
var targetModeColor: Int = 0xFFB700, @OptionTag("textHighlightRGB", converter = ColorConverter::class)
var textHighlightColor: Color = Color.GREEN,
var definitionModeColor: Int = 0x6FC5FF,
@OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var textHighlightColor: Int = 0x394B58, var tagForegroundColor: Color = Color.BLACK,
var tagForegroundColor: Int = 0xFFFFFF, @OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
var tagBackgroundColor: Color = Color.YELLOW,
var tagBackgroundColor: Int = 0x008299,
@OptionTag("acceptedTagRGB", converter = ColorConverter::class)
var searchWholeFile: Boolean = true, var acceptedTagColor: Color = Color.CYAN,
var mapToASCII: Boolean = false, var roundedTagCorners: Boolean = true
)
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)
}

View File

@@ -1,12 +1,20 @@
package org.acejump.config package org.acejump.config
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.* import com.intellij.ui.ColorPanel
import com.intellij.ui.dsl.builder.* import com.intellij.ui.components.JBCheckBox
import org.acejump.input.JumpMode import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.GrowPolicy.MEDIUM_TEXT
import com.intellij.ui.layout.GrowPolicy.SHORT_TEXT
import com.intellij.ui.layout.panel
import org.acejump.input.KeyLayout import org.acejump.input.KeyLayout
import java.awt.* import java.awt.Color
import javax.swing.* import java.awt.Font
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.text.JTextComponent import javax.swing.text.JTextComponent
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@@ -15,136 +23,90 @@ import kotlin.reflect.KProperty
*/ */
@Suppress("UsePropertyAccessSyntax") @Suppress("UsePropertyAccessSyntax")
internal class AceSettingsPanel { internal class AceSettingsPanel {
private val defaults = AceSettings()
private val tagCharsField = JBTextField() private val tagCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>() private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false } private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val cycleModeCombo1 = ComboBox<JumpMode>() private val jumpModeColorWheel = ColorPanel()
private val cycleModeCombo2 = ComboBox<JumpMode>() private val fromCaretModeColorWheel = ColorPanel()
private val cycleModeCombo3 = ComboBox<JumpMode>() private val betweenPointsModeColorWheel = ColorPanel()
private val cycleModeCombo4 = ComboBox<JumpMode>() private val textHighlightColorWheel = ColorPanel()
private val minQueryLengthField = JBTextField() private val tagForegroundColorWheel = ColorPanel()
private val jumpModeColorWheel = ResettableColorPicker(defaults.getJumpModeJBC()) private val tagBackgroundColorWheel = ColorPanel()
private val jumpEndModeColorWheel = ResettableColorPicker(defaults.getJumpEndModeJBC()) private val acceptedTagColorWheel = ColorPanel()
private val targetModeColorWheel = ResettableColorPicker(defaults.getTargetModeJBC()) private val roundedTagCornersCheckBox = JBCheckBox()
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()
init { init {
tagCharsField.apply { font = Font("monospaced", font.style, font.size) } tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) } keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") } 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 { internal val rootPanel: JPanel = panel {
group("Characters and Layout") { fun Cell.short(component: JComponent) = component(growPolicy = SHORT_TEXT)
row("Allowed characters in tags:") { cell(tagCharsField).columns(COLUMNS_LARGE) } fun Cell.medium(component: JComponent) = component(growPolicy = MEDIUM_TEXT)
row("Keyboard layout:") { cell(keyboardLayoutCombo).columns(COLUMNS_SHORT) }
row("Keyboard design:") { cell(keyboardLayoutArea).columns(COLUMNS_SHORT) } titledRow("Characters and Layout") {
row("Allowed characters in tags:") { medium(tagCharsField) }
row("Keyboard layout:") { short(keyboardLayoutCombo) }
row("Keyboard design:") { short(keyboardLayoutArea) }
} }
group("Modes") { titledRow("Colors") {
row("Cycle order:") { row("Jump mode caret background:") { short(jumpModeColorWheel) }
cell(cycleModeCombo1).columns(10) row("From Caret mode caret background:") { short(fromCaretModeColorWheel) }
cell(cycleModeCombo2).columns(10) row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) }
cell(cycleModeCombo3).columns(10) row("Searched text background:") { short(textHighlightColorWheel) }
cell(cycleModeCombo4).columns(10) row("Tag foreground:") { short(tagForegroundColorWheel) }
} row("Tag background:") { short(tagBackgroundColorWheel) }
row("Accepted tag position background:") { short(acceptedTagColorWheel) }
} }
group("Colors") { titledRow("Appearance") {
row("Jump mode caret background:") { cell(jumpModeColorWheel) } row { short(roundedTagCornersCheckBox.apply { text = "Rounded tag corners" }) }
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) }
}
group("Behavior") {
row { cell(searchWholeFileCheckBox.apply { text = "Search whole file" }) }
row("Minimum typed characters (1-10):") { cell(minQueryLengthField) }
}
group("Language Settings") {
row { cell(mapToASCIICheckBox.apply { text = "Map unicode to ASCII" }) }
}
group("Visual") {
row { cell(showSearchNotificationCheckBox.apply { text = "Show hint with search text" }) }
} }
} }
// Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342 // Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342
internal var allowedChars by tagCharsField internal var allowedChars by tagCharsField
internal var keyboardLayout by keyboardLayoutCombo internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea 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 jumpModeColor by jumpModeColorWheel internal var jumpModeColor by jumpModeColorWheel
internal var jumpEndModeColor by jumpEndModeColorWheel internal var fromCaretModeColor by fromCaretModeColorWheel
internal var targetModeColor by targetModeColorWheel internal var betweenPointsModeColor by betweenPointsModeColorWheel
internal var definitionModeColor by definitionModeColorWheel
internal var textHighlightColor by textHighlightColorWheel internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel internal var tagBackgroundColor by tagBackgroundColorWheel
internal var searchWholeFile by searchWholeFileCheckBox internal var acceptedTagColor by acceptedTagColorWheel
internal var mapToASCII by mapToASCIICheckBox internal var roundedTagCorners by roundedTagCornersCheckBox
internal var showSearchNotification by showSearchNotificationCheckBox
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) {
minQueryLength = value.toString()
}
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars allowedChars = settings.allowedChars
keyboardLayout = settings.layout keyboardLayout = settings.layout
cycleMode1 = settings.cycleMode1 jumpModeColor = settings.jumpModeColor
cycleMode2 = settings.cycleMode2 fromCaretModeColor = settings.fromCaretModeColor
cycleMode3 = settings.cycleMode3 betweenPointsModeColor = settings.betweenPointsModeColor
cycleMode4 = settings.cycleMode4 textHighlightColor = settings.textHighlightColor
minQueryLength = settings.minQueryLength.toString() tagForegroundColor = settings.tagForegroundColor
jumpModeColor = settings.getJumpModeJBC() tagBackgroundColor = settings.tagBackgroundColor
jumpEndModeColor = settings.getJumpEndModeJBC() acceptedTagColor = settings.acceptedTagColor
targetModeColor = settings.getTargetModeJBC() roundedTagCorners = settings.roundedTagCorners
definitionModeColor = settings.getDefinitionModeJBC()
textHighlightColor = settings.getTextHighlightJBC()
tagForegroundColor = settings.getTagForegroundJBC()
tagBackgroundColor = settings.getTagBackgroundJBC()
searchWholeFile = settings.searchWholeFile
mapToASCII = settings.mapToASCII
showSearchNotification = settings.showSearchNotification
} }
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575 // Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.lowercase() private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.toLowerCase()
private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s) 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 ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor
private operator fun ResettableColorPicker.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c) 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.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
private operator fun JCheckBox.setValue(a: AceSettingsPanel, p: KProperty<*>, selected: Boolean) = setSelected(selected) 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 <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 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) { private inline fun <reified T : Enum<T>> ComboBox<T>.setupEnumItems(crossinline onChanged: (T) -> Unit) {
T::class.java.enumConstants.forEach(this::addItem) T::class.java.enumConstants.forEach(this::addItem)
addActionListener { onChanged(selectedItem as T) } addActionListener { onChanged(selectedItem as T) }
} }

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,31 +6,30 @@ import com.intellij.openapi.editor.actionSystem.TypedAction
import com.intellij.openapi.editor.actionSystem.TypedActionHandler import com.intellij.openapi.editor.actionSystem.TypedActionHandler
/** /**
* If at least one session exists, this listener redirects all characters * If at least one session exists, this listener redirects all characters, typed in [Editor]s with attached sessions, to the appropriate
* typed in [Editor]s with attached sessions to the appropriate sessions' * sessions' own handlers.
* own handlers.
*/ */
internal object EditorKeyListener: TypedActionHandler { internal object EditorKeyListener : TypedActionHandler {
private val action = TypedAction.getInstance() private val action = TypedAction.getInstance()
private val attached = mutableMapOf<Editor, TypedActionHandler>() private val attached = mutableMapOf<Editor, TypedActionHandler>()
private var originalHandler: TypedActionHandler? = null private var originalHandler: TypedActionHandler? = null
override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) { override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) {
(attached[editor] ?: originalHandler ?: return).execute(editor, charTyped, dataContext) (attached[editor] ?: originalHandler ?: return).execute(editor, charTyped, dataContext)
} }
fun attach(editor: Editor, callback: TypedActionHandler) { fun attach(editor: Editor, callback: TypedActionHandler) {
if (attached.isEmpty()) { if (attached.isEmpty()) {
originalHandler = action.rawHandler originalHandler = action.rawHandler
action.setupRawHandler(this) action.setupRawHandler(this)
} }
attached[editor] = callback attached[editor] = callback
} }
fun detach(editor: Editor) { fun detach(editor: Editor) {
attached.remove(editor) attached.remove(editor)
if (attached.isEmpty()) { if (attached.isEmpty()) {
originalHandler?.let(action::setupRawHandler) originalHandler?.let(action::setupRawHandler)
originalHandler = null 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,14 +1,8 @@
package org.acejump.input 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, * Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how
* based on each key's distance from the home row and how ergonomically * ergonomically difficult they are to press.
* difficult they are to press.
*/ */
@Suppress("unused") @Suppress("unused")
enum class KeyLayout(internal val rows: Array<String>, priority: String) { enum class KeyLayout(internal val rows: Array<String>, priority: String) {
@@ -19,46 +13,12 @@ enum class KeyLayout(internal val rows: Array<String>, priority: String) {
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"), QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"), QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"), QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"),
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210"), 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");
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("") internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap() internal 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>()
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)
}
}
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? { internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int? {
return { allPriorities[tagToChar(it)] } return { allPriorities[tagToChar(it)] }
} }
internal fun distanceBetweenKeys(char1: Char, char2: Char): Int {
return keyDistances.getValue(char1).getValue(char2)
}
} }

View File

@@ -7,6 +7,48 @@ import org.acejump.config.AceSettings
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ). * with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/ */
internal object KeyLayoutCache { internal object KeyLayoutCache {
/**
* Stores keys ordered by proximity to other keys for the QWERTY layout.
* TODO: Support more layouts, perhaps generate automatically.
*/
private val qwertyCharacterDistances = mapOf(
'j' to "jikmnhuolbgypvftcdrxsezawq8796054321",
'f' to "ftgvcdryhbxseujnzawqikmolp5463728190",
'k' to "kolmjipnhubgyvftcdrxsezawq9807654321",
'd' to "drfcxsetgvzawyhbqujnikmolp4352617890",
'l' to "lkopmjinhubgyvftcdrxsezawq0987654321",
's' to "sedxzawrfcqtgvyhbujnikmolp3241567890",
'a' to "aqwszedxrfctgvyhbujnikmolp1234567890",
'h' to "hujnbgyikmvftolcdrpxsezawq6758493021",
'g' to "gyhbvftujncdrikmxseolzawpq5647382910",
'y' to "yuhgtijnbvfrokmcdeplxswzaq6758493021",
't' to "tygfruhbvcdeijnxswokmzaqpl5647382910",
'u' to "uijhyokmnbgtplvfrcdexswzaq7869504321",
'r' to "rtfdeygvcxswuhbzaqijnokmpl4536271890",
'n' to "nbhjmvgyuiklocftpxdrzseawq7685940321",
'v' to "vcfgbxdrtyhnzseujmawikqolp5463728190",
'm' to "mnjkbhuilvgyopcftxdrzseawq8970654321",
'c' to "cxdfvzsertgbawyhnqujmikolp4352617890",
'b' to "bvghncftyujmxdrikzseolawqp6574839201",
'i' to "iokjuplmnhybgtvfrcdexswzaq8970654321",
'e' to "erdswtfcxzaqygvuhbijnokmpl3425167890",
'x' to "xzsdcawerfvqtgbyhnujmikolp3241567890",
'z' to "zasxqwedcrfvtgbyhnujmikolp1234567890",
'o' to "oplkimjunhybgtvfrcdexswzaq9087654321",
'w' to "wesaqrdxztfcygvuhbijnokmpl2314567890",
'p' to "plokimjunhybgtvfrcdexswzaq0987654321",
'q' to "qwaeszrdxtfcygvuhbijnokmpl1234567890",
'1' to "1234567890qawzsexdrcftvgybhunjimkolp",
'2' to "2134567890qwasezxdrcftvgybhunjimkolp",
'3' to "3241567890weqasdrzxcftvgybhunjimkolp",
'4' to "4352617890erwsdftqazxcvgybhunjimkolp",
'5' to "5463728190rtedfgywsxcvbhuqaznjimkolp",
'6' to "6574839201tyrfghuedcvbnjiwsxmkoqazlp",
'7' to "7685940321yutghjirfvbnmkoedclpwsxqaz",
'8' to "8796054321uiyhjkotgbnmlprfvedcwsxqaz",
'9' to "9807654321ioujklpyhnmtgbrfvedcwsxqaz",
'0' to "0987654321opiklujmyhntgbrfvedcwsxqaz").mapValues { (_, v) -> v.mapIndexed { index, char -> char to index }.toMap() }
/** /**
* Sorts tags according to current keyboard layout settings, and some predefined rules that force tags with digits, and tags with two * 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. * keys far apart, to be sorted after other (easier to type) tags.
@@ -23,16 +65,19 @@ internal object KeyLayoutCache {
/** /**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing. * Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/ */
fun ensureInitialized(settings: AceSettings) = fun ensureInitialized(settings: AceSettings) {
if (!::tagOrder.isInitialized) reset(settings) else Unit if (!::tagOrder.isInitialized) {
reset(settings)
}
}
/** /**
* Re-initializes cached data according to updated settings. * Re-initializes cached data according to updated settings.
*/ */
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
tagOrder = compareBy( tagOrder = compareBy(
{ it[0].isDigit() || it[1].isDigit() }, { it[0].isDigit() || it[1].isDigit() },
{ settings.layout.distanceBetweenKeys(it[0], it[1]) }, { qwertyCharacterDistances.getValue(it[0]).getValue(it[1]) },
settings.layout.priority { it[0] } settings.layout.priority { it[0] }
) )
@@ -43,8 +88,6 @@ internal object KeyLayoutCache {
.joinToString("") .joinToString("")
.ifEmpty(settings.layout::allChars) .ifEmpty(settings.layout::allChars)
allPossibleTags = allPossibleChars.flatMap { a -> allPossibleTags = allPossibleChars.flatMap { a -> allPossibleChars.map { b -> "$a$b".intern() } }.sortedWith(tagOrder)
allPossibleChars.map { b -> "$a$b".intern() }
}.sortedWith(tagOrder)
} }
} }

View File

@@ -0,0 +1,109 @@
package org.acejump.modes
import com.intellij.openapi.editor.CaretState
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionMode
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class BetweenPointsMode : SessionMode {
private companion object {
private val HINT_TYPE_TAG = arrayOf(
"<b>Type to Search...</b>"
)
private val HINT_ACTION_MODE = arrayOf(
"<h>Between Points Mode</h>",
"<f>[S]</f>elect... / <f>[D]</f>elete...",
"<f>[C]</f>lone to Caret...",
"<f>[M]</f>ove to Caret..."
)
private val HINT_JUMP_MODE = arrayOf(
"<f>[J]</f> at Tag / <f>[L]</f> past Query",
"Word <f>[S]</f>tart / Word <f>[E]</f>nd"
)
private val HINT_JUMP_OR_SELECT_MODE = HINT_JUMP_MODE + arrayOf(
"Select <f>[W]</f>ord / <f>[H]</f>ump / <f>[Q]</f>uery / <f>[1-9]</f> Expansion"
)
private val ACTION_MODE_MAP = mapOf(
'S' to ({ action: AceTagAction.BaseSelectAction -> action }),
'D' to (AceTagAction::Delete),
'C' to (AceTagAction::CloneToCaret),
'M' to (AceTagAction::MoveToCaret)
)
private val JUMP_MODE_MAP = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'L' to AceTagAction.JumpPastSearchEnd,
'S' to AceTagAction.JumpToWordStartTag,
'E' to AceTagAction.JumpToWordEndTag
)
private val SELECTION_MODE_MAP = mapOf(
'W' to AceTagAction.SelectWord,
'H' to AceTagAction.SelectHump,
'Q' to AceTagAction.SelectQuery,
*('1'..'9').mapIndexed { index, char -> char to AceTagAction.SelectExtended(index + 1) }.toTypedArray()
)
}
override val caretColor
get() = AceConfig.betweenPointsModeColor
private var actionMode: ((AceTagAction.BaseSelectAction) -> AceTagAction)? = null
private var originalCarets: List<CaretState>? = null
private var firstOffset: Int? = null
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
val actionMode = actionMode
if (actionMode == null) {
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing
}
if (acceptedTag == null) {
return state.type(charTyped)
}
if (firstOffset == null) {
val selectAction = SELECTION_MODE_MAP[charTyped.toUpperCase()]
if (selectAction != null) {
state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
}
val jumpAction = JUMP_MODE_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
val firstOffset = firstOffset
if (firstOffset == null) {
val caretModel = state.editor.caretModel
this.originalCarets = caretModel.caretsAndSelections
state.act(jumpAction, acceptedTag, shiftMode = false)
this.firstOffset = caretModel.offset
return TypeResult.RestartSearch
}
originalCarets?.let { state.editor.caretModel.caretsAndSelections = it }
state.act(actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
override fun getHint(acceptedTag: Int?): Array<String> {
return when {
actionMode == null -> HINT_ACTION_MODE
acceptedTag == null -> HINT_TYPE_TAG
firstOffset == null -> HINT_JUMP_OR_SELECT_MODE
else -> HINT_JUMP_MODE
}
}
}

View File

@@ -0,0 +1,74 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionMode
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class FromCaretMode : SessionMode {
private companion object {
private val HINT_TYPE_TAG = arrayOf(
"<b>Type to Search...</b>"
)
private val HINT_ACTION_MODE = arrayOf(
"<h>From Caret Mode</h>",
"<f>[S]</f>elect... / <f>[D]</f>elete...",
"<f>[X]</f> Cut... / <f>[C]</f>opy... / <f>[P]</f>aste..."
)
private val HINT_JUMP_MODE = arrayOf(
"<f>[J]</f> at Tag / <f>[L]</f> past Query",
"Word <f>[S]</f>tart / Word <f>[E]</f>nd"
)
private val ACTION_MODE_MAP = mapOf(
'S' to ({ action: AceTagAction.SelectToCaret -> action }),
'D' to (AceTagAction::Delete),
'X' to (AceTagAction::Cut),
'C' to (AceTagAction::Copy),
'P' to (AceTagAction::Paste)
)
private val JUMP_MODE_MAP = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'L' to AceTagAction.JumpPastSearchEnd,
'S' to AceTagAction.JumpToWordStartTag,
'E' to AceTagAction.JumpToWordEndTag
)
}
override val caretColor
get() = AceConfig.fromCaretModeColor
private var actionMode: ((AceTagAction.SelectToCaret) -> AceTagAction)? = null
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
val actionMode = actionMode
if (actionMode == null) {
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing
}
if (acceptedTag == null) {
return state.type(charTyped)
}
val jumpAction = JUMP_MODE_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
state.act(actionMode(AceTagAction.SelectToCaret(jumpAction)), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
override fun getHint(acceptedTag: Int?): Array<String>? {
return when {
actionMode == null -> HINT_ACTION_MODE
acceptedTag == null -> HINT_TYPE_TAG
else -> HINT_JUMP_MODE
}
}
}

View File

@@ -0,0 +1,56 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionMode
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class JumpMode : SessionMode {
private companion object {
private val HINT_ACTIONS = arrayOf(
"<f>[J]</f>ump to Tag / <f>[L]</f> past Query",
"Word <f>[S]</f>tart / <f>[E]</f>nd",
"Select <f>[W]</f>ord / <f>[H]</f>ump / <f>[Q]</f>uery / <f>[1-9]</f> Expansion",
"<f>[D]</f>eclaration / <f>[U]</f>sages",
"<f>[I]</f>ntentions / <f>[R]</f>efactor"
)
private val ACTION_MAP = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'L' to AceTagAction.JumpPastSearchEnd,
'S' to AceTagAction.JumpToWordStartTag,
'E' to AceTagAction.JumpToWordEndTag,
'W' to AceTagAction.SelectWord,
'H' to AceTagAction.SelectHump,
'Q' to AceTagAction.SelectQuery,
'D' to AceTagAction.GoToDeclaration,
'U' to AceTagAction.ShowUsages,
'I' to AceTagAction.ShowIntentions,
'R' to AceTagAction.Refactor,
*('1'..'9').mapIndexed { index, char -> char to AceTagAction.SelectExtended(index + 1) }.toTypedArray()
)
}
override val caretColor
get() = AceConfig.jumpModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
if (acceptedTag == null) {
return state.type(charTyped)
}
val action = ACTION_MAP[charTyped.toUpperCase()]
if (action != null) {
state.act(action, acceptedTag, charTyped.isUpperCase())
return TypeResult.EndSession
}
return TypeResult.Nothing
}
override fun getHint(acceptedTag: Int?): Array<String>? {
return HINT_ACTIONS.takeIf { acceptedTag != null }
}
}

View File

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

View File

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

View File

@@ -7,53 +7,56 @@ import org.acejump.countMatchingCharacters
*/ */
internal sealed class SearchQuery { internal sealed class SearchQuery {
abstract val rawText: String abstract val rawText: String
/** /**
* Returns how many characters the search occurrence highlight should cover. * Returns how many characters the search occurrence highlight should cover.
*/ */
abstract fun getHighlightLength(text: CharSequence, offset: Int): Int abstract fun getHighlightLength(text: CharSequence, offset: Int): Int
/** /**
* Converts the query into a regular expression to find the initial matches. * Converts the query into a regular expression to find the initial matches.
*/ */
abstract fun toRegex(): Regex? abstract fun toRegex(): Regex?
/** /**
* Searches for all occurrences of a literal text query. If the first * Searches for all occurrences of a literal text query. If the first character of the query is lowercase, then the entire query will be
* character of the query is lowercase, then the entire query will be
* case-insensitive. * case-insensitive.
* *
* Each occurrence must either match the entire query, or match the query * 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
* up to a point so that the rest of the query matches the beginning of * beginning of a tag at the location of the occurrence.
* a tag at the location of the occurrence.
*/ */
class Literal(override var rawText: String): SearchQuery() { class Literal(override val rawText: String) : SearchQuery() {
init { init {
require(rawText.isNotEmpty()) require(rawText.isNotEmpty())
} }
override fun getHighlightLength(text: CharSequence, offset: Int): Int = override fun getHighlightLength(text: CharSequence, offset: Int): Int {
text.countMatchingCharacters(offset, rawText) return text.countMatchingCharacters(offset, rawText)
}
override fun toRegex(): Regex { override fun toRegex(): Regex {
val options = mutableSetOf(RegexOption.MULTILINE) val options = mutableSetOf(RegexOption.MULTILINE)
if (rawText.first().isLowerCase()) if (rawText.first().isLowerCase()) {
options.add(RegexOption.IGNORE_CASE) options.add(RegexOption.IGNORE_CASE)
}
return Regex(Regex.escape(rawText), options) return Regex(Regex.escape(rawText), options)
} }
} }
/** /**
* Searches for all matches of a regular expression. * 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 val rawText = ""
override fun getHighlightLength(text: CharSequence, offset: Int) = 0 override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return 1
override fun toRegex(): Regex = }
Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
override fun toRegex(): Regex {
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
}
} }
} }

View File

@@ -1,16 +1,18 @@
package org.acejump.search package org.acejump.search
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.*
import it.unimi.dsi.fastutil.ints.IntOpenHashSet import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.immutableText import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache import org.acejump.input.KeyLayoutCache
import org.acejump.isWordPart import org.acejump.isWordPart
import org.acejump.wordEndPlus import org.acejump.wordEndPlus
import java.util.IdentityHashMap import java.util.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import kotlin.math.max import kotlin.math.max
/* /*
@@ -51,154 +53,140 @@ import kotlin.math.max
*/ */
internal class Solver private constructor( internal class Solver private constructor(
private val editorPriority: List<Editor>, private val editor: Editor,
private val queryLength: Int, private val queryLength: Int,
private val newResults: Map<Editor, IntList>, private val newResults: IntList,
private val allResults: Map<Editor, IntList> private val allResults: IntList
) { ) {
companion object { companion object {
fun solve( fun solve(
editorPriority: List<Editor>, editor: Editor, query: SearchQuery, newResults: IntList, allResults: IntList, tags: List<String>, cache: EditorOffsetCache
query: SearchQuery, ): Map<String, Int> {
newResults: Map<Editor, IntList>, return Solver(editor, max(1, query.rawText.length), newResults, allResults).map(tags, cache)
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) private var newTags = Object2IntOpenHashMap<String>(KeyLayoutCache.allPossibleTags.size)
private val newTagIndices = IntOpenHashSet()
private var allWordFragments = HashSet<String>(allResults.size).apply {
val iter = allResults.iterator()
while (iter.hasNext()) {
forEachWordFragment(iter.nextInt()) { add(it) }
}
}
fun map(availableTags: List<String>, cache: EditorOffsetCache): Map<String, Int> {
val eligibleSitesByTag = HashMap<String, IntList>(100)
val tagsByFirstLetter = availableTags.groupBy { it[0] } val tagsByFirstLetter = availableTags.groupBy { it[0] }
for ((editor, offsets) in newResults) { val iter = newResults.iterator()
val iter = offsets.iterator() while (iter.hasNext()) {
while (iter.hasNext()) { val site = iter.nextInt()
val site = iter.nextInt()
for ((firstLetter, tags) in tagsByFirstLetter.entries) {
if (editor.foldingModel.isOffsetCollapsed(site)) { if (canTagBeginWithChar(site, firstLetter)) {
continue for (tag in tags) {
} eligibleSitesByTag.getOrPut(tag) { IntArrayList(10) }.add(site)
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>>() val matchingSites = HashMap<IntList, IntArray>()
// Keys are guaranteed to be from a single collection. val matchingSitesAsArrays = IdentityHashMap<String, IntArray>() // Keys are guaranteed to be from a single collection.
val matchingSitesAsArrays = IdentityHashMap<String, MutableList<Tag>>()
val siteOrder = siteOrder(caches) val siteOrder = siteOrder(cache)
val tagOrder = KeyLayoutCache.tagOrder val tagOrder = KeyLayoutCache.tagOrder
.thenComparingInt { eligibleSitesByTag.getValue(it).size } .thenComparingInt { eligibleSitesByTag.getValue(it).size }
.thenBy(AceConfig.layout.priority(String::last)) .thenBy(AceConfig.layout.priority(String::last))
val sortedTags = eligibleSitesByTag.keys.toMutableList().apply { val sortedTags = eligibleSitesByTag.keys.toMutableList().apply {
sortWith(tagOrder) sortWith(tagOrder)
} }
for ((mark, tags) in eligibleSitesByTag.entries) { for ((key, value) in eligibleSitesByTag.entries) {
matchingSitesAsArrays[mark] = matchingSites.getOrPut(tags) { matchingSitesAsArrays[key] = matchingSites.getOrPut(value) {
tags.toMutableList().apply { sortWith(siteOrder) } value.toIntArray().apply { IntArrays.mergeSort(this, siteOrder) }
} }
} }
var totalAssigned = 0 var totalAssigned = 0
val totalResults = newResults.values.sumOf(IntList::size)
for (tag in sortedTags) { for (tag in sortedTags) {
if (totalAssigned == totalResults) { if (totalAssigned == newResults.size) {
break break
} }
if (tryToAssignTag(tag, matchingSitesAsArrays.getValue(tag))) { if (tryToAssignTag(tag, matchingSitesAsArrays.getValue(tag))) {
totalAssigned++ totalAssigned++
} }
} }
return newTags return newTags
} }
private fun tryToAssignTag(mark: String, tags: List<Tag>): Boolean { private fun tryToAssignTag(tag: String, sites: IntArray): Boolean {
if (newTags.containsKey(mark)) return false if (newTags.containsKey(tag)) {
return false
val tag = tags.firstOrNull { it.offset !in newTagIndices.getValue(it.editor) } ?: return false }
val index = sites.firstOrNull { it !in newTagIndices } ?: return false
@Suppress("ReplacePutWithAssignment") @Suppress("ReplacePutWithAssignment")
newTags.put(mark, tag) newTags.put(tag, index)
newTagIndices.getValue(tag.editor).add(tag.offset) newTagIndices.add(index)
return true return true
} }
private fun siteOrder(caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b ->
val aEditor = a.editor
val bEditor = b.editor
if (aEditor !== bEditor) { private fun siteOrder(cache: EditorOffsetCache) = IntComparator { a, b ->
val aEditorIndex = editorPriority.indexOf(aEditor) val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bEditorIndex = editorPriority.indexOf(bEditor) val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
// 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) { if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first. // Sites in immediate view should come first.
return@Comparator if (aIsVisible) -1 else 1 return@IntComparator if (aIsVisible) -1 else 1
} }
val aIsNotWordStart = aEditor.immutableText[max(0, a.offset - 1)].isWordPart val chars = editor.immutableText
val bIsNotWordStart = bEditor.immutableText[max(0, b.offset - 1)].isWordPart val aIsNotWordStart = chars[max(0, a - 1)].isWordPart
val bIsNotWordStart = chars[max(0, b - 1)].isWordPart
if (aIsNotWordStart != bIsNotWordStart) { if (aIsNotWordStart != bIsNotWordStart) {
// Ensure that the first letter of a word is prioritized for tagging. // Ensure that the first letter of a word is prioritized for tagging.
return@Comparator if (bIsNotWordStart) -1 else 1 return@IntComparator if (bIsNotWordStart) -1 else 1
} }
when { when {
a.offset < b.offset -> -1 a < b -> -1
a.offset > b.offset -> 1 a > b -> 1
else -> 0 else -> 0
} }
} }
private fun canTagBeginWithChar(editor: Editor, site: Int, char: Char): Boolean { private fun canTagBeginWithChar(site: Int, char: Char): Boolean {
if (char.toString() in allWordFragments) return false if (char.toString() in allWordFragments) {
return false
forEachWordFragment(editor, site) { if (it + char in allWordFragments) return false } }
forEachWordFragment(site) {
if (it + char in allWordFragments) {
return false
}
}
return true return true
} }
private inline fun forEachWordFragment(editor: Editor, site: Int, callback: (String) -> Unit) { private inline fun forEachWordFragment(site: Int, callback: (String) -> Unit) {
val chars = editor.immutableText val chars = editor.immutableText
val left = max(0, site + queryLength - 1) val left = max(0, site + queryLength - 1)
val right = chars.wordEndPlus(site) val right = chars.wordEndPlus(site)
if (right >= chars.length) {
return
}
val builder = StringBuilder(1 + right - left) val builder = StringBuilder(1 + right - left)
for (i in left..right) { for (i in left..right) {
builder.append(chars[i].lowercase()) builder.append(chars[i].toLowerCase())
callback(builder.toString()) callback(builder.toString())
} }
} }

View File

@@ -1,8 +0,0 @@
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)
}

View File

@@ -1,165 +1,150 @@
package org.acejump.search package org.acejump.search
import com.google.common.collect.ArrayListMultimap
import com.google.common.collect.HashBiMap import com.google.common.collect.HashBiMap
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.* import org.acejump.ExternalUsage
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN import org.acejump.boundaries.StandardBoundaries
import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache.allPossibleTags import org.acejump.input.KeyLayoutCache.allPossibleTags
import org.acejump.view.TagMarker import org.acejump.isWordPart
import org.acejump.matchesAt
import org.acejump.view.Tag
import java.util.AbstractMap.SimpleImmutableEntry import java.util.AbstractMap.SimpleImmutableEntry
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 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, updates them when the search query changes, and requests a jump if the search query matches a tag.
* 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>) { class Tagger(private val editor: Editor) {
private var tagMap = HashBiMap.create<String, Tag>() private var tagMap = HashBiMap.create<String, Int>()
val hasTags val hasTags
get() = tagMap.isNotEmpty() get() = tagMap.isNotEmpty()
@ExternalUsage
val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value.offset }
@ExternalUsage
internal val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value }
/** /**
* Removes all markers, allowing them to be regenerated from scratch. * Removes all markers, allowing them to be regenerated from scratch.
*/ */
fun unmark() { internal fun unmark() {
tagMap = HashBiMap.create() tagMap = HashBiMap.create()
} }
/** /**
* Assigns tags to as many results as possible, keeping previously assigned * Assigns tags to as many results as possible, keeping previously assigned tags. Returns a [TaggingResult.Accept] if the current search
* tags. Returns a [TaggingResult.Jump] if the current search query matches * query matches any existing tag and we should jump to it and end the session, or [TaggingResult.Mark] to continue the session with
* any existing tag and we should jump to it and end the session, or * updated tag markers.
* [TaggingResult.Mark] to continue the session with updated tag markers.
* *
* Note that the [results] collection will be mutated. * Note that the [results] collection will be mutated.
*/ */
fun markOrJump(query: SearchQuery, results: Map<Editor, IntList>): TaggingResult { internal fun update(query: SearchQuery, results: IntList): TaggingResult {
val isRegex = query is SearchQuery.RegularExpression val isRegex = query is SearchQuery.RegularExpression
val queryText = if (isRegex) " ${query.rawText}" else query.rawText[0] + query.rawText.drop(1).lowercase() val queryText = if (isRegex) " ${query.rawText}" else query.rawText[0] + query.rawText.drop(1).toLowerCase()
val availableTags = allPossibleTags.filter { !queryText.endsWith(it[0]) && it !in tagMap } val availableTags = allPossibleTags.filter { !queryText.endsWith(it[0]) && it !in tagMap }
if (!isRegex) { if (!isRegex) {
for (entry in tagMap.entries) { for (entry in tagMap.entries) {
if (entry solves queryText) { if (entry solves queryText) {
return TaggingResult.Jump(query = queryText.substringBefore(entry.key), mark = entry.key, tag = entry.value) return TaggingResult.Accept(entry.value)
} }
} }
if (queryText.length == 1) { if (queryText.length == 1) {
for ((editor, offsets) in results) { removeResultsWithOverlappingTags(results)
removeResultsWithOverlappingTags(editor, offsets)
}
} }
} }
if (!isRegex || tagMap.isEmpty()) if (!isRegex || tagMap.isEmpty()) {
tagMap = assignTagsAndMerge(results, availableTags, query, queryText) 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 })) return TaggingResult.Mark(createTagMarkers(results, query.rawText.ifEmpty { null }))
} }
fun clone(): Tagger {
return Tagger(editor).also { it.tagMap.putAll(tagMap) }
}
/** /**
* Assigns as many unassigned tags as possible, and merges them with * Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
* the existing compatible tags.
*/ */
private fun assignTagsAndMerge( private fun assignTagsAndMerge(results: IntList, availableTags: List<String>, query: SearchQuery, queryText: String): HashBiMap<String, Int> {
results: Map<Editor, IntList>, val cache = EditorOffsetCache.new()
availableTags: List<String>,
query: SearchQuery, results.sort { a, b ->
queryText: String val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
): HashBiMap<String, Tag> { val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
val caches = results.keys.associateWith { EditorOffsetCache.new() }
for ((editor, offsets) in results) {
val cache = caches.getValue(editor)
offsets.sort { a, b ->
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
when { when {
aIsVisible && !bIsVisible -> -1 aIsVisible && !bIsVisible -> -1
bIsVisible && !aIsVisible -> 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> val allAssignedTags = mutableMapOf<String, Int>()
val oldCompatibleTags = tagMap.filter { isTagCompatibleWithQuery(it.key, it.value, queryText) || it.value in results }
val vacantResults: IntList
if (oldCompatibleTags.isEmpty()) { if (oldCompatibleTags.isEmpty()) {
vacantResults = results 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
} }
else {
vacantResults = IntArrayList()
val iter = results.iterator()
while (iter.hasNext()) {
val offset = iter.nextInt()
if (offset !in oldCompatibleTags.values) {
vacantResults.add(offset)
}
}
}
allAssignedTags.putAll(oldCompatibleTags) allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editors, query, vacantResults, results, availableTags, caches)) allAssignedTags.putAll(Solver.solve(editor, query, vacantResults, results, availableTags, cache))
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) -> return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
// Avoid matching query - will trigger a jump. // Avoid matching query - will trigger a jump.
// TODO: lift this constraint. // TODO: lift this constraint.
val queryEndsWith = queryText.endsWith(tag[0]) || queryText.endsWith(tag) val queryEndsWith = queryText.endsWith(tag[0]) || queryText.endsWith(tag)
if (!queryEndsWith && canShortenTag(tag, allAssignedTags.keys)) if (!queryEndsWith && canShortenTag(tag, allAssignedTags))
tag[0].toString() tag[0].toString()
else else
tag tag
} }
} }
private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean = private infix fun Map.Entry<String, Int>.solves(query: String): Boolean {
query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query) return query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
}
private fun isTagCompatibleWithQuery(marker: String, tag: Tag, query: String): Boolean = private fun isTagCompatibleWithQuery(tag: String, offset: Int, query: String): Boolean {
tag.editor.immutableText.matchesAt(tag.offset, getPlaintextPortion(query, marker), ignoreCase = true) return editor.immutableText.matchesAt(offset, getPlaintextPortion(query, tag), ignoreCase = true)
}
fun isQueryCompatibleWithTagAt(query: String, tag: Tag): Boolean = fun isQueryCompatibleWithTagAt(query: String, offset: Int): Boolean {
tagMap.inverse()[tag].let { it != null && isTagCompatibleWithQuery(it, tag, query) } return tagMap.inverse()[offset].let { it != null && isTagCompatibleWithQuery(it, offset, query) }
}
fun canQueryMatchAnyVisibleTag(query: String): Boolean = fun canQueryMatchAnyTag(query: String): Boolean {
tagMap.any { (label, tag) -> return tagMap.any { (tag, offset) ->
val tagPortion = getTagPortion(query, label) val tagPortion = getTagPortion(query, tag)
tagPortion.isNotEmpty() tagPortion.isNotEmpty() && tag.startsWith(tagPortion, ignoreCase = true) && isTagCompatibleWithQuery(tag, offset, query)
&& label.startsWith(tagPortion, ignoreCase = true)
&& isTagCompatibleWithQuery(label, tag, query)
&& tag.isVisible()
} }
}
private fun removeResultsWithOverlappingTags(editor: Editor, offsets: IntList) {
val iter = offsets.iterator() private fun removeResultsWithOverlappingTags(results: IntList) {
val iter = results.iterator()
val chars = editor.immutableText val chars = editor.immutableText
while (iter.hasNext()) { while (iter.hasNext()) {
@@ -169,55 +154,48 @@ internal class Tagger(private val editors: List<Editor>) {
} }
} }
private fun createTagMarkers(tags: Collection<Tag>, literalQueryText: String?): MutableMap<Editor, Collection<TagMarker>> { private fun createTagMarkers(results: IntList, literalQueryText: String?): List<Tag> {
val tagMapInv = tagMap.inverse() val tagMapInv = tagMap.inverse()
val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tags.size, 50)) return results.mapNotNull { index -> tagMapInv[index]?.let { tag -> Tag.create(editor, tag, index, literalQueryText) } }
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)
}
return markers.asMap()
} }
private companion object { private companion object {
private fun CharSequence.canTagWithoutOverlap(loc: Int) = when { private fun CharSequence.canTagWithoutOverlap(loc: Int) = when {
loc - 1 < 0 -> true loc - 1 < 0 -> true
loc + 1 >= length -> true loc + 1 >= length -> true
this[loc] isUnlike this[loc - 1] -> true this[loc] isUnlike this[loc - 1] -> 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] != this[loc + 1] -> true this[loc] != this[loc + 1] -> true
this[loc + 1] == '\r' || this[loc + 1] == '\n' -> true this[loc + 1] == '\r' || this[loc + 1] == '\n' -> true
this[loc - 1] == this[loc] && this[loc] == this[loc + 1] -> false this[loc - 1] == this[loc] && this[loc] == this[loc + 1] -> false
this[loc + 1].isWhitespace() && this[(loc + 2).coerceAtMost(length - 1)].isWhitespace() -> true this[loc + 1].isWhitespace() && this[(loc + 2).coerceAtMost(length - 1)].isWhitespace() -> true
else -> false else -> false
} }
private infix fun Char.isUnlike(other: Char) = private infix fun Char.isUnlike(other: Char): Boolean {
this.isWordPart xor other.isWordPart || return this.isWordPart xor other.isWordPart || this.isWhitespace() xor other.isWhitespace()
this.isWhitespace() xor other.isWhitespace()
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 { private fun getPlaintextPortion(query: String, tag: String) = when {
query.endsWith(marker, true) -> query.takeLast(marker.length) query.endsWith(tag, true) -> query.dropLast(tag.length)
query.endsWith(marker.first(), true) -> query.takeLast(1) query.endsWith(tag.first(), true) -> query.dropLast(1)
else -> "" else -> query
} }
private fun canShortenTag(marker: String, markers: Collection<String>): Boolean { private fun getTagPortion(query: String, tag: String) = when {
for (other in markers) query.endsWith(tag, true) -> query.takeLast(tag.length)
if (marker != other && marker[0] == other[0]) query.endsWith(tag.first(), true) -> query.takeLast(1)
else -> ""
}
private fun canShortenTag(tag: String, tagMap: Map<String, Int>): Boolean {
for (other in tagMap.keys) {
if (tag != other && tag[0] == other[0]) {
return false return false
}
}
return true return true
} }
} }

View File

@@ -1,9 +1,8 @@
package org.acejump.search package org.acejump.search
import com.intellij.openapi.editor.Editor import org.acejump.view.Tag
import org.acejump.view.TagMarker
sealed class TaggingResult { internal sealed class TaggingResult {
class Jump(val query: String, val mark: String, val tag: Tag): TaggingResult() class Accept(val offset: Int) : TaggingResult()
class Mark(val markers: MutableMap<Editor, Collection<TagMarker>>): TaggingResult() class Mark(val tags: List<Tag>) : TaggingResult()
} }

View File

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

View File

@@ -3,36 +3,51 @@ package org.acejump.session
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
/** /**
* Holds [Editor] caret settings. The settings are saved the * Holds [Editor] caret settings. The settings are saved the moment a [Session] starts, modified to indicate AceJump states, and restored
* moment a [Session] starts, modified to indicate AceJump * once the [Session] ends.
* states, and restored once the [Session] ends.
*/ */
internal data class EditorSettings( internal data class EditorSettings(private val isBlockCursor: Boolean, private val isBlinkCaret: Boolean, private val isReadOnly: Boolean) {
private val isBlockCursor: Boolean,
private val isBlinkCaret: Boolean,
) {
companion object { companion object {
fun setup(editor: Editor): EditorSettings { fun setup(editor: Editor): EditorSettings {
val settings = editor.settings val settings = editor.settings
val document = editor.document val document = editor.document
val original = EditorSettings( val original = EditorSettings(
isBlockCursor = settings.isBlockCursor, isBlockCursor = settings.isBlockCursor,
isBlinkCaret = settings.isBlinkCaret, isBlinkCaret = settings.isBlinkCaret,
isReadOnly = !document.isWritable
) )
settings.isBlockCursor = true settings.isBlockCursor = true
settings.isBlinkCaret = false settings.isBlinkCaret = false
document.setReadOnly(true)
return original 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) { fun restore(editor: Editor) {
val settings = editor.settings val settings = editor.settings
val document = editor.document val document = editor.document
settings.isBlockCursor = isBlockCursor settings.isBlockCursor = isBlockCursor
settings.isBlinkCaret = isBlinkCaret settings.isBlinkCaret = isBlinkCaret
document.setReadOnly(isReadOnly)
} }
} }

View File

@@ -1,306 +1,208 @@
package org.acejump.session package org.acejump.session
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.codeInsight.hint.HintUtil
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.actionSystem.TypedActionHandler import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.openapi.editor.colors.EditorColors.CARET_COLOR import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.util.containers.ContainerUtil import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import it.unimi.dsi.fastutil.ints.IntArrayList import com.intellij.ui.LightweightHint
import org.acejump.* import org.acejump.ExternalUsage
import org.acejump.action.TagScroller
import org.acejump.action.TagJumper
import org.acejump.action.TagVisitor
import org.acejump.boundaries.Boundaries 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.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.input.EditorKeyListener import org.acejump.input.EditorKeyListener
import org.acejump.input.JumpMode
import org.acejump.input.JumpModeTracker
import org.acejump.input.KeyLayoutCache import org.acejump.input.KeyLayoutCache
import org.acejump.search.Pattern import org.acejump.modes.BetweenPointsMode
import org.acejump.search.SearchProcessor import org.acejump.modes.FromCaretMode
import org.acejump.search.Tagger import org.acejump.modes.JumpMode
import org.acejump.search.TaggingResult import org.acejump.search.*
import org.acejump.view.TagCanvas import org.acejump.view.TagCanvas
import org.acejump.view.TextHighlighter 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>) { class Session(private val editor: Editor) {
private val listeners: MutableList<AceJumpListener> = private val editorSettings = EditorSettings.setup(editor)
ContainerUtil.createLockFreeCopyOnWriteList() private lateinit var mode: SessionMode
private var boundaries: Boundaries = defaultBoundaries private var state: SessionState? = null
private var tagger = Tagger(editor)
private companion object {
private val defaultBoundaries private var acceptedTag: Int? = null
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
set(value) { set(value) {
field = value field = value
if (value === JumpMode.DISABLED) { if (value != null) {
end() tagCanvas.removeMarkers()
} else { editorSettings.onTagAccepted(editor)
searchProcessor?.let { textHighlighter.render(it.results, it.query, jumpMode) }
mainEditor.colorsScheme.setColor(CARET_COLOR, value.caretColor)
mainEditor.contentComponent.repaint()
} }
} }
private var searchProcessor: SearchProcessor? = null private val textHighlighter = TextHighlighter(editor)
private var tagger = Tagger(jumpEditors) private val tagCanvas = TagCanvas(editor)
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 @ExternalUsage
val tags val tags
get() = tagger.tags get() = tagger.tags
init { init {
KeyLayoutCache.ensureInitialized(AceConfig.settings) KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(mainEditor, object: TypedActionHandler { EditorKeyListener.attach(editor, object : TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) { override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
var processor = searchProcessor val state = state ?: return
val hadTags = tagger.hasTags val hadTags = tagger.hasTags
if (processor == null) { editorSettings.startEditing(editor)
processor = SearchProcessor.fromChar( val result = mode.type(state, charTyped, acceptedTag)
jumpEditors, charTyped, boundaries editorSettings.stopEditing(editor)
).also { searchProcessor = it }
} else if (!processor.type(charTyped, tagger)) { when (result) {
return TypeResult.Nothing -> updateHint()
TypeResult.RestartSearch -> restart().also { this@Session.state = SessionState(editor, tagger); updateHint() }
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
is TypeResult.ChangeMode -> setMode(result.mode)
TypeResult.EndSession -> end()
} }
updateSearch(
processor, markImmediately = hadTags,
shiftMode = charTyped.isUpperCase()
)
} }
}) })
} }
/** /**
* Updates text highlights and tag markers according to the current * Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag.
* search state. Dispatches jumps if the search query matches a tag.
* If all tags are outside view, scrolls to the closest one. * If all tags are outside view, scrolls to the closest one.
*/ */
private fun updateSearch( private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean) {
processor: SearchProcessor,
markImmediately: Boolean,
shiftMode: Boolean = false
) {
val query = processor.query val query = processor.query
val results = processor.results val results = processor.results
textHighlighter.render(results, query, jumpMode) if (query is SearchQuery.Literal && !markImmediately && query.rawText.let { it.length < 2 && it.all(Char::isLetterOrDigit) }) {
textHighlighter.renderOccurrences(results, query)
if (!markImmediately &&
query.rawText.let {
it.length < AceConfig.minQueryLength &&
it.all(Char::isLetterOrDigit)
}
) {
return return
} }
when (val result = tagger.markOrJump(query, results.clone())) { when (val result = tagger.update(query, results.clone())) {
is TaggingResult.Jump -> { is TaggingResult.Accept -> {
tagJumper.jump(result.tag, shiftMode, isCrossEditor = mainEditor !== result.tag.editor) val offset = result.offset
tagCanvases.values.forEach(TagCanvas::removeMarkers) acceptedTag = offset
end(result) textHighlighter.renderFinal(offset, processor.query)
} }
is TaggingResult.Mark -> { is TaggingResult.Mark -> {
val markers = result.markers val tags = result.tags
tagCanvas.setMarkers(tags, isRegex = query is SearchQuery.RegularExpression)
for ((editor, canvas) in tagCanvases) { textHighlighter.renderOccurrences(results, query)
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()
}
} }
} }
}
updateHint()
@ExternalUsage
fun markResults(resultsToMark: SortedSet<Int>) {
val jumpEditor = jumpEditors.singleOrNull() ?: return
markResults(mapOf(jumpEditor to resultsToMark))
} }
@ExternalUsage private fun setMode(mode: SessionMode) {
fun markResults(resultsToMark: Map<Editor, Collection<Int>>) { this.mode = mode
tagger = Tagger(jumpEditors) editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
tagCanvases.values.forEach { it.setMarkers(emptyList()) } updateHint()
val processor = SearchProcessor.fromRegex(jumpEditors, "", defaultBoundaries)
.apply {
results.clear()
for ((editor, offsets) in resultsToMark) {
if (editor in jumpEditors) {
results[editor] = IntArrayList(offsets)
}
}
}
updateSearch(processor, markImmediately = true)
} }
private fun updateHint() {
val hintArray = mode.getHint(acceptedTag) ?: return
val hintText = hintArray
.joinToString("\n")
.replace("<f>", "<span style=\"font-family:'${editor.colorsScheme.editorFontName}';font-weight:bold\">")
.replace("</f>", "</span>")
.replace("<h>", "<b><u>")
.replace("</h>", "</u></b>")
val hint = LightweightHint(HintUtil.createInformationLabel(hintText))
val pos = acceptedTag?.let(editor::offsetToLogicalPosition) ?: editor.caretModel.logicalPosition
val point = HintManagerImpl.getHintPosition(hint, editor, pos, HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, point, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, true, info)
}
fun cycleMode() {
if (!this::mode.isInitialized) {
setMode(JumpMode())
state = SessionState(editor, tagger)
return
}
restart()
setMode(when (mode) {
is JumpMode -> FromCaretMode()
is FromCaretMode -> BetweenPointsMode()
else -> JumpMode()
})
state = SessionState(editor, tagger)
}
/** /**
* Starts a regular expression search. If a search was already active, * Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
* it will be reset alongside its tags and highlights.
*/ */
@ExternalUsage
fun startRegexSearch(pattern: String, boundaries: Boundaries) { fun startRegexSearch(pattern: String, boundaries: Boundaries) {
tagger = Tagger(jumpEditors) if (this::mode.isInitialized) {
tagCanvases.values.forEach { it.setMarkers(emptyList()) } end()
return
val processor = SearchProcessor.fromRegex( }
jumpEditors, pattern,
boundaries.intersection(defaultBoundaries) setMode(JumpMode())
).also { searchProcessor = it } tagger = Tagger(editor)
tagCanvas.setMarkers(emptyList(), isRegex = true)
val processor = SearchProcessor.fromRegex(editor, pattern, boundaries).also { state = SessionState(editor, tagger, it) }
updateSearch(processor, markImmediately = true) updateSearch(processor, markImmediately = true)
} }
/** /**
* Starts a regular expression search. If a search was already active, * Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
* it will be reset alongside its tags and highlights.
*/ */
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) {
@ExternalUsage
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) =
startRegexSearch(pattern.regex, boundaries) startRegexSearch(pattern.regex, boundaries)
/**
* See [JumpModeTracker.cycle].
*/
fun cycleNextJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = true)
} }
/**
* See [JumpModeTracker.cycle].
*/
fun cyclePreviousJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = false)
}
/**
* See [JumpModeTracker.toggle]
*/
fun toggleJumpMode(newMode: JumpMode) {
jumpMode = jumpModeTracker.toggle(newMode)
}
@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. * Ends this session.
*/ */
fun end(taggingResult: TaggingResult? = null) = fun end() {
SessionManager.end(mainEditor, taggingResult) SessionManager.end(editor)
}
/** /**
* Clears any currently active search, tags, and highlights. * Clears any currently active search, tags, and highlights.
* Does not reset [JumpMode].
*/ */
fun restart() { fun restart() {
tagger = Tagger(jumpEditors) state = null
searchProcessor = null tagger = Tagger(editor)
tagCanvases.values.forEach(TagCanvas::removeMarkers) acceptedTag = null
tagCanvas.removeMarkers()
textHighlighter.reset() textHighlighter.reset()
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.onTagUnaccepted(editor)
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
editor.contentComponent.repaint()
} }
/** /**
* Should only be used from [SessionManager] to dispose a * Should only be used from [SessionManager] to dispose a successfully ended session.
* successfully ended session.
*/ */
internal fun dispose(taggingResult: TaggingResult?) { internal fun dispose() {
tagger = Tagger(jumpEditors) tagger = Tagger(editor)
EditorKeyListener.detach(mainEditor) tagCanvas.unbind()
tagCanvases.values.forEach(TagCanvas::unbind)
textHighlighter.reset() textHighlighter.reset()
EditorsCache.invalidate() EditorKeyListener.detach(editor)
val jumpResult = taggingResult as? TaggingResult.Jump if (!editor.isDisposed) {
val mark = jumpResult?.mark HintManagerImpl.getInstanceImpl().hideAllHints()
val query = jumpResult?.query editorSettings.restore(editor)
listeners.forEach { it.finished(mark, query) } editor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
if (!mainEditor.isDisposed) {
originalSettings.restore(mainEditor)
mainEditor.colorsScheme.setColor(CARET_COLOR, JumpMode.DISABLED.caretColor)
} }
val focusedEditor = jumpResult?.tag?.editor ?: mainEditor
if (!focusedEditor.isDisposed) {
focusedEditor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
}
@ExternalUsage
fun addAceJumpListener(listener: AceJumpListener) {
listeners += listener
}
@ExternalUsage
fun removeAceJumpListener(listener: AceJumpListener) {
listeners -= listener
} }
} }

View File

@@ -1,53 +1,41 @@
package org.acejump.session package org.acejump.session
import com.intellij.openapi.editor.Editor 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 * Manages active [Session]s in [Editor]s. There may only be one [Session] per [Editor], but multiple [Session]s across multiple [Editor]s
* one [Session] per [Editor], but multiple [Session]s across * may be active at once.
* multiple [Editor]s may be active at once.
* *
* It is possible for an [Editor] to be disposed with an active * 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]. In such case, the reference to both will remain * [Session] starts, at which point the [SessionManager.cleanup] method will purge disposed [Editor]s.
* until a new [Session] starts, at which point the
* [SessionManager.cleanup] method will purge disposed [Editor]s.
*/ */
@ExternalUsage
object SessionManager { object SessionManager {
private val sessions = HashMap<Editor, Session>(4) private val sessions = HashMap<Editor, Session>(4)
/** /**
* Starts a new [Session], or returns an existing [Session] * Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
* if the specified [Editor] already has one.
*/ */
fun start(editor: Editor): Session = start(editor, listOf(editor)) fun start(editor: Editor): Session {
return sessions.getOrPut(editor) { cleanup(); Session(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.
*/
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
} }
/** /**
* Returns the active [Session] in the specified [Editor], * Returns the active [Session] in the specified [Editor], or null if the [Editor] has no active session.
* 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], * Ends the active [Session] in the specified [Editor], or does nothing if the [Editor] has no active session.
* or does nothing if the [Editor] has no active session.
*/ */
fun end(editor: Editor, taggingResult: TaggingResult?) = fun end(editor: Editor) {
sessions.remove(editor)?.dispose(taggingResult) ?: Unit sessions.remove(editor)?.dispose()
}
private fun cleanup() = sessions.keys.filter { it.isDisposed }
.forEach { disposedEditor -> sessions.remove(disposedEditor)?.dispose(null) } private fun cleanup() {
for (disposedEditor in sessions.keys.filter { it.isDisposed }) {
sessions.remove(disposedEditor)?.dispose()
}
}
} }

View File

@@ -0,0 +1,10 @@
package org.acejump.session
import java.awt.Color
interface SessionMode {
val caretColor: Color
fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult
fun getHint(acceptedTag: Int?): Array<String>?
}

View File

@@ -0,0 +1,30 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
class SessionState(val editor: Editor, private val tagger: Tagger, processor: SearchProcessor? = null) {
private var currentProcessor: SearchProcessor? = processor
fun type(char: Char): TypeResult {
val processor = currentProcessor
if (processor == null) {
val newProcessor = SearchProcessor.fromChar(editor, char, StandardBoundaries.VISIBLE_ON_SCREEN)
return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it })
}
if (processor.type(char, tagger)) {
return TypeResult.UpdateResults(processor)
}
return TypeResult.Nothing
}
fun act(action: AceTagAction, offset: Int, shiftMode: Boolean) {
currentProcessor?.let { action(editor, it, offset, shiftMode) }
}
}

View File

@@ -0,0 +1,11 @@
package org.acejump.session
import org.acejump.search.SearchProcessor
sealed class TypeResult {
object Nothing : TypeResult()
object RestartSearch : TypeResult()
class UpdateResults(val processor: SearchProcessor) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
object EndSession : TypeResult()
}

View File

@@ -2,10 +2,9 @@ package org.acejump.view
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil import com.intellij.ui.ColorUtil
import com.intellij.ui.JreHiDpiUtil
import com.intellij.ui.scale.JBUIScale import com.intellij.ui.scale.JBUIScale
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.countMatchingCharacters import org.acejump.countMatchingCharacters
import org.acejump.immutableText import org.acejump.immutableText
@@ -18,7 +17,7 @@ import kotlin.math.max
/** /**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor. * Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/ */
class TagMarker( internal class Tag(
private val tag: String, private val tag: String,
val offsetL: Int, val offsetL: Int,
val offsetR: Int, val offsetR: Int,
@@ -26,123 +25,115 @@ class TagMarker(
private val hasSpaceRight: Boolean private val hasSpaceRight: Boolean
) { ) {
private val length = tag.length private val length = tag.length
companion object { companion object {
private const val ARC = 1
/** /**
* Creates a new tag, precomputing some information about the nearby characters to reduce rendering overhead. If the last typed * 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 ([literalQueryText]) matches the first [tag] character, only the second [tag] character is displayed.
*/ */
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker { fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): Tag {
val chars = editor.immutableText val chars = editor.immutableText
val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0 val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace() val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
val displayedTag = if (literalQueryText != null && literalQueryText.last().equals(tag.first(), ignoreCase = true)) val displayedTag = if (literalQueryText != null && literalQueryText.last().equals(tag.first(), ignoreCase = true))
tag.drop(1).uppercase() tag.drop(1).toUpperCase()
else else
tag.uppercase() tag.toUpperCase()
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight) return Tag(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
} }
/** /**
* Renders the tag background. * Renders the tag background.
*/ */
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) { private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color, arc: Int) {
g.color = color g.color = color
g.fillRoundRect(rect.x, rect.y + 1, rect.width, rect.height - 1, arc, arc)
// 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)
}
} }
/** /**
* Renders the tag text. * Renders the tag text.
*/ */
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) { 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 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.color = AceConfig.tagForegroundColor
g.drawString(text, x, y) g.drawString(text, point.x, point.y + font.baselineDistance)
}
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. * 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. * 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 fun isOffsetInRange(range: IntRange): Boolean {
return offsetL in range
}
/**
* Determines on which side of the target character the tag is positioned.
*/
enum class TagAlignment {
LEFT,
RIGHT
}
/** /**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags. * 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. * Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap.
*/ */
fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? { fun paint(
val rect = alignTag(editor, cache, font, occupied) ?: return null g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>, isRegex: Boolean
): Rectangle? {
drawHighlight(g, rect, AceConfig.tagBackgroundColor) val (rect, alignment) = alignTag(editor, cache, font, occupied) ?: return null
val highlightColor = when {
alignment != TagAlignment.RIGHT || hasSpaceRight || isRegex -> AceConfig.tagBackgroundColor
else -> ColorUtil.darker(AceConfig.tagBackgroundColor, 3)
}
drawHighlight(g, rect, highlightColor, font.tagCornerArc)
drawForeground(g, font, rect.location, tag) drawForeground(g, font, rect.location, tag)
occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) }) occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) })
return rect return rect
} }
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? { private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Pair<Rectangle, TagAlignment>? {
val boundaries = VISIBLE_ON_SCREEN val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
if (hasSpaceRight || offsetL !in 1 until editor.document.textLength || isLineEnding(editor.immutableText[offsetL - 1])) { if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) {
val rectR = createRightAlignedTagRect(editor, cache, font) val rectR = createRightAlignedTagRect(editor, cache, font)
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects) }
return (rectR to TagAlignment.RIGHT).takeIf {
boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects)
}
} }
val rectL = createLeftAlignedTagRect(editor, cache, font) val rectL = createLeftAlignedTagRect(editor, cache, font)
if (occupied.none(rectL::intersects))
return rectL.takeIf { boundaries.isOffsetInside(editor, offsetL, cache) } if (occupied.none(rectL::intersects)) {
return (rectL to TagAlignment.LEFT).takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
}
val rectR = createRightAlignedTagRect(editor, cache, font) val rectR = createRightAlignedTagRect(editor, cache, font)
if (occupied.none(rectR::intersects))
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) } if (occupied.none(rectR::intersects)) {
return (rectR to TagAlignment.RIGHT).takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
}
return null return null
} }
private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle { private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetR) val pos = cache.offsetToXY(editor, offsetR)
val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR)
val char = if (offsetR >= editor.document.textLength) return Rectangle(pos.x + shift, pos.y, font.tagCharWidth * length, font.lineHeight)
' ' // 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 { private fun createLeftAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetL) val pos = cache.offsetToXY(editor, offsetL)
val shift = -(font.tagCharWidth * length) val shift = -(font.tagCharWidth * length)
return Rectangle(pos.x + shift - 4, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight) return Rectangle(pos.x + shift, pos.y, font.tagCharWidth * length, font.lineHeight)
} }
} }

View File

@@ -4,9 +4,10 @@ import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.event.CaretListener
import com.intellij.ui.ColorUtil
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN import org.acejump.boundaries.StandardBoundaries
import org.acejump.read import org.acejump.config.AceConfig
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.Rectangle import java.awt.Rectangle
@@ -17,75 +18,84 @@ import javax.swing.SwingUtilities
/** /**
* Holds all active tag markers and renders them on top of the editor. * Holds all active tag markers and renders them on top of the editor.
*/ */
internal class TagCanvas(private val editor: Editor): JComponent(), CaretListener { internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener {
private var markers: Collection<TagMarker>? = null private var markers: List<Tag>? = null
private var isRegex = false
init { init {
val contentComponent = editor.contentComponent val contentComponent = editor.contentComponent
contentComponent.add(this) contentComponent.add(this)
setBounds(0, 0, contentComponent.width, contentComponent.height) setBounds(0, 0, contentComponent.width, contentComponent.height)
if (ApplicationInfo.getInstance().build.components.first() < 173) if (ApplicationInfo.getInstance().build.components.first() < 173) {
SwingUtilities.convertPoint(this, location, editor.component.rootPane) SwingUtilities.convertPoint(this, location, editor.component.rootPane).let { setLocation(-it.x, -it.y) }
.let { setLocation(-it.x, -it.y) } }
editor.caretModel.addCaretListener(this) editor.caretModel.addCaretListener(this)
} }
fun unbind() { fun unbind() {
markers = null markers = null
editor.contentComponent.remove(this) editor.contentComponent.remove(this)
editor.caretModel.removeCaretListener(this) editor.caretModel.removeCaretListener(this)
} }
/** /**
* Ensures that all tags and the outline around the selected tag are * Ensures that all tags and the outline around the selected tag are repainted. It should not be necessary to repaint the entire tag
* repainted. It should not be necessary to repaint the entire tag
* canvas, but the cost of repainting visible tags is negligible. * canvas, but the cost of repainting visible tags is negligible.
*/ */
override fun caretPositionChanged(event: CaretEvent) = repaint() override fun caretPositionChanged(event: CaretEvent) {
repaint()
fun setMarkers(markers: Collection<TagMarker>) { }
fun setMarkers(markers: List<Tag>, isRegex: Boolean) {
this.markers = markers this.markers = markers
this.isRegex = isRegex
repaint() repaint()
} }
fun removeMarkers() { fun removeMarkers() {
markers = emptyList() this.markers = emptyList()
} }
override fun paint(g: Graphics) = override fun paint(g: Graphics) {
read { if (!markers.isNullOrEmpty()) super.paint(g) else Unit } if (!markers.isNullOrEmpty()) {
super.paint(g)
}
}
override fun paintChildren(g: Graphics) { override fun paintChildren(g: Graphics) {
super.paintChildren(g) super.paintChildren(g)
val markers = markers ?: return val markers = markers ?: return
val font = TagFont(editor)
val cache = EditorOffsetCache.new()
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val occupied = mutableListOf<Rectangle>()
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) (g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val font = TagFont(editor) // 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.
val cache = EditorOffsetCache.new()
val viewRange = VISIBLE_ON_SCREEN.getOffsetRange(editor, cache) // TODO instead of immediately painting, we could calculate the layout of everything first, and then remove tags that interfere with
val foldingModel = editor.foldingModel // the caret tag to avoid changing the alignment of the caret tag
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 caretOffset = editor.caretModel.offset
val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset } val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset }
caretMarker?.paint(g, editor, cache, font, occupied) val caretRect = caretMarker?.paint(g, editor, cache, font, occupied, isRegex)
for (marker in markers) { for (marker in markers) {
if (marker.isOffsetInRange(viewRange) && !foldingModel.isOffsetCollapsed(marker.offsetL) && marker !== caretMarker) if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
marker.paint(g, editor, cache, font, occupied) marker.paint(g, editor, cache, font, occupied, isRegex)
}
}
if (caretRect != null) {
g.color = ColorUtil.brighter(AceConfig.tagBackgroundColor, 10)
// Only adding 1 to width because it seems the right side of the tag highlight is slightly off.
g.drawRoundRect(caretRect.x - 1, caretRect.y, caretRect.width + 1, caretRect.height, font.tagCornerArc, font.tagCornerArc)
} }
} }
} }

View File

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

View File

@@ -1,221 +1,127 @@
package org.acejump.view 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.Editor
import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.markup.* import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterTargetArea.EXACT_RANGE import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.ui.* import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.ui.util.preferredHeight import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.util.DocumentUtil import it.unimi.dsi.fastutil.ints.IntArrayList
import com.intellij.util.ui.*
import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.*
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.input.JumpMode import org.acejump.immutableText
import org.acejump.search.SearchQuery import org.acejump.search.SearchQuery
import java.awt.* import java.awt.Color
import javax.swing.* import java.awt.Graphics
import kotlin.math.max
/** /**
* Renders highlights for search occurrences. * Renders highlights for search occurrences.
*/ */
internal class TextHighlighter { internal class TextHighlighter(private val editor: Editor) {
private companion object { private const val LAYER = HighlighterLayer.LAST + 1 } private var previousHighlights: Array<RangeHighlighter>? = null
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) { fun renderOccurrences(offsets: IntList, query: SearchQuery) {
init { render(offsets, when (query) {
background = HintUtil.getInformationColor() is SearchQuery.RegularExpression -> RegexRenderer
foreground = JBColor.foreground() else -> SearchedWordRenderer
this.isOpaque = true }, query::getHighlightLength)
}
} }
/** /**
* Removes all current highlights and re-creates them from scratch. * Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
* Must be called whenever any of the method parameters change.
*/ */
fun render(results: Map<Editor, IntList>, query: SearchQuery, jumpMode: JumpMode) { fun renderFinal(offset: Int, query: SearchQuery) {
render(IntArrayList(intArrayOf(offset)), AcceptedTagRenderer, query::getHighlightLength)
val renderer = when { }
query is SearchQuery.RegularExpression -> RegexRenderer
jumpMode === JumpMode.TARGET -> SearchedWordWithOutlineRenderer private inline fun render(offsets: IntList, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
else -> SearchedWordRenderer val markup = editor.markupModel
} val chars = editor.immutableText
for ((editor, offsets) in results) { ARC = TagFont(editor).tagCornerArc
val highlights = previousHighlights[editor]
val modifications = (previousHighlights?.size ?: 0) + offsets.size
val markup = editor.markupModel val enableBulkEditing = modifications > 1000
val document = editor.document
val chars = editor.immutableText val document = editor.document
val modifications = (highlights?.size ?: 0) + offsets.size try {
val enableBulkEditing = modifications > 1000 if (enableBulkEditing) {
document.isInBulkUpdate = true
DocumentUtil.executeInBulk(document, enableBulkEditing) { }
highlights?.forEach(markup::removeHighlighter)
previousHighlights[editor] = Array(offsets.size) { index -> previousHighlights?.forEach(markup::removeHighlighter)
val start = offsets.getInt(index) previousHighlights = Array(offsets.size) { index ->
val end = start + query.getHighlightLength(chars, start) val start = offsets.getInt(index)
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) {
if (AceConfig.showSearchNotification) document.isInBulkUpdate = false
showSearchNotification(results, query, jumpMode) }
for (editor in previousHighlights.keys.toList()) {
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() { fun reset() {
previousHighlights.forEach { (editor, highlighters) -> editor.markupModel.removeAllHighlighters()
highlighters.forEach(editor.markupModel::removeHighlighter) previousHighlights = null
}
previousHighlights.clear()
previousHint?.hide()
} }
/** /**
* Renders a filled highlight in the background of a searched text occurrence. * Renders a filled highlight in the background of a searched text occurrence.
*/ */
private object SearchedWordRenderer: CustomHighlighterRenderer { 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) { override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
SearchedWordRenderer.paint(editor, highlighter, g) drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.textHighlightColor)
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)
} }
} }
/** /**
* Renders a filled highlight in the background of the first highlighted * Renders a filled highlight in the background of the first highlighted position. Used for regex search queries.
* position. Used for regex search queries.
*/ */
private object RegexRenderer: CustomHighlighterRenderer { private object RegexRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) = override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawSingle(g, editor, highlighter.startOffset) drawSingle(g, editor, highlighter.startOffset, AceConfig.textHighlightColor)
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int) { }
/**
* Renders a filled highlight in the background of the accepted tag position and search query.
*/
private object AcceptedTagRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.acceptedTagColor)
}
}
private companion object {
private const val LAYER = HighlighterLayer.LAST + 1
private var ARC = 0
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 = color
g.fillRoundRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1, ARC, ARC)
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int, color: Color) {
val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset) val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset)
val char = editor.immutableText.getOrNull(offset) val char = editor.immutableText.getOrNull(offset)?.takeUnless { it == '\n' || it == '\t' } ?: ' '
?.takeUnless { it == '\n' || it == '\t' } ?: ' '
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char) val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = AceConfig.textHighlightColor g.color = color
g.fillRect(pos.x, pos.y, lastCharWidth, editor.lineHeight) g.fillRoundRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1, ARC, ARC)
g.color = AceConfig.tagBackgroundColor
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
} }
} }
} }

View File

@@ -20,98 +20,53 @@
id="preferences.AceConfigurable" dynamic="true"/> id="preferences.AceConfigurable" dynamic="true"/>
<editorActionHandler action="EditorEscape" order="first" <editorActionHandler action="EditorEscape" order="first"
implementationClass="org.acejump.action.AceEditorAction$Reset" implementationClass="org.acejump.action.AceEditorAction$Reset"/>
id="AceHandlerEscape"/>
<editorActionHandler action="EditorBackSpace" order="first" <editorActionHandler action="EditorBackSpace" order="first"
implementationClass="org.acejump.action.AceEditorAction$ClearSearch" 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" <editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts" implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
id="AceHandlerUp"/>
<editorActionHandler action="EditorLeft" order="first" <editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents" implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
id="AceHandlerLeft"/>
<editorActionHandler action="EditorLineStart" order="first" <editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents" implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
id="AceHandlerLineStart"/>
<editorActionHandler action="EditorRight" order="first" <editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds" implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
id="AceHandlerRight"/>
<editorActionHandler action="EditorLineEnd" order="first" <editorActionHandler action="EditorLineEnd" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds" implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
id="AceHandlerLineEnd"/>
</extensions> </extensions>
<actions> <actions>
<action id="AceAction" <action id="AceAction"
class="org.acejump.action.AceAction$ActivateOrCycleMode" class="org.acejump.action.AceKeyboardAction$ActivateAceJump"
text="Activate / Cycle AceJump Mode"> text="Activate AceJump Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/> <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="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action> </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" <action id="AceLineAction"
class="org.acejump.action.AceAction$StartAllLineMarksMode" class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode"> 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" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" 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"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
</action> </action>
<action id="AceLineStartsAction" <action id="AceLineStartsAction"
class="org.acejump.action.AceAction$StartAllLineStartsMode" class="org.acejump.action.AceKeyboardAction$StartAllLineStartsMode"
text="Start AceJump in All Line Starts Mode"/> text="Start AceJump in All Line Starts Mode"/>
<action id="AceLineEndsAction" <action id="AceLineEndsAction"
class="org.acejump.action.AceAction$StartAllLineEndsMode" class="org.acejump.action.AceKeyboardAction$StartAllLineEndsMode"
text="Start AceJump in All Line Ends Mode"/> text="Start AceJump in All Line Ends Mode"/>
<action id="AceLineIndentsAction" <action id="AceLineIndentsAction"
class="org.acejump.action.AceAction$StartAllLineIndentsMode" class="org.acejump.action.AceKeyboardAction$StartAllLineIndentsMode"
text="Start AceJump in All Line Indents Mode"/> text="Start AceJump in All Line Indents Mode"/>
<action id="AceWordAction" <action id="AceWordAction"
class="org.acejump.action.AceAction$StartAllWordsMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsMode"
text="Start AceJump in All Words Mode"/> text="Start AceJump in All Words Mode"/>
<action id="AceWordForwardAction" <action id="AceWordForwardAction"
class="org.acejump.action.AceAction$StartAllWordsForwardMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsForwardMode"
text="Start AceJump in All Words After Caret Mode"/> text="Start AceJump in All Words After Caret Mode"/>
<action id="AceWordBackwardsAction" <action id="AceWordBackwardsAction"
class="org.acejump.action.AceAction$StartAllWordsBackwardsMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/> text="Start AceJump in All Words Before Caret Mode"/>
</actions> </actions>
</idea-plugin> </idea-plugin>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?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"> <!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 transform="matrix(1,0,0,1,0,-635)">
<g id="pluginIcon" transform="matrix(0.154082,0,0,0.154082,-39.7746,553.645)"> <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;"/> <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,8 +1,6 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
import com.intellij.openapi.editor.actions.EnterAction import org.acejump.action.AceKeyboardAction
import org.acejump.action.AceAction
import org.acejump.config.AceConfig
import org.acejump.test.util.BaseTest import org.acejump.test.util.BaseTest
/** /**
@@ -58,6 +56,7 @@ class AceTest : BaseTest() {
"<caret>testing 1234".search("g") "<caret>testing 1234".search("g")
typeAndWaitForResults(session.tags[0].key) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("j")
myFixture.checkResult("testin<caret>g 1234") myFixture.checkResult("testin<caret>g 1234")
} }
@@ -65,7 +64,8 @@ class AceTest : BaseTest() {
fun `test shift selection`() { fun `test shift selection`() {
"<caret>testing 1234".search("4") "<caret>testing 1234".search("4")
typeAndWaitForResults(session.tags[0].key.uppercase()) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("J")
myFixture.checkResult("<selection>testing 123<caret></selection>4") myFixture.checkResult("<selection>testing 123<caret></selection>4")
} }
@@ -73,7 +73,7 @@ class AceTest : BaseTest() {
fun `test words before caret action`() { fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two") makeEditor("test words <caret> before caret is two")
takeAction(AceAction.StartAllWordsBackwardsMode()) takeAction(AceKeyboardAction.StartAllWordsBackwardsMode)
assertEquals(2, session.tags.size) assertEquals(2, session.tags.size)
} }
@@ -81,7 +81,7 @@ class AceTest : BaseTest() {
fun `test words after caret action`() { fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four") makeEditor("test words <caret> after caret is four")
takeAction(AceAction.StartAllWordsForwardMode()) takeAction(AceKeyboardAction.StartAllWordsForwardMode)
assertEquals(4, session.tags.size) assertEquals(4, session.tags.size)
} }
@@ -89,11 +89,12 @@ class AceTest : BaseTest() {
fun `test word mode`() { fun `test word mode`() {
makeEditor("test word action") makeEditor("test word action")
takeAction(AceAction.StartAllWordsMode()) takeAction(AceKeyboardAction.StartAllWordsMode)
assertEquals(3, session.tags.size) assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key) typeAndWaitForResults(session.tags[1].key)
typeAndWaitForResults("j")
myFixture.checkResult("test <caret>word action") myFixture.checkResult("test <caret>word action")
} }
@@ -101,65 +102,17 @@ class AceTest : BaseTest() {
fun `test target mode`() { fun `test target mode`() {
"<caret>test target action".search("target") "<caret>test target action".search("target")
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults(session.tags[0].key) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("s")
myFixture.checkResult("test <selection>target<caret></selection> action") 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`() { fun `test line mode`() {
makeEditor(" test\n three\n lines") makeEditor(" test\n three\n lines\n")
takeAction(AceAction.StartAllLineMarksMode()) takeAction(AceKeyboardAction.StartAllLineMarksMode)
assertEquals(9, session.tags.size) 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,4 +1,4 @@
import org.acejump.action.AceAction import org.acejump.action.AceKeyboardAction
import org.acejump.test.util.BaseTest import org.acejump.test.util.BaseTest
import org.junit.Ignore import org.junit.Ignore
import java.io.File import java.io.File
@@ -6,26 +6,27 @@ import kotlin.random.Random
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@Ignore @Ignore
class LatencyTest: BaseTest() { class LatencyTest : BaseTest() {
private fun `test tag latency`(editorText: String) { private fun `test tag latency`(editorText: String) {
val chars = editorText.toCharArray().distinct().filter { !it.isWhitespace() } val chars = editorText.toCharArray().distinct().filter { !it.isWhitespace() }
val avg = averageTimeWithWarmup(warmupRuns = 10, timedRuns = 10) { val avg = averageTimeWithWarmup(warmupRuns = 10, timedRuns = 10) {
var time = 0L var time = 0L
for (query in chars) { for (query in chars) {
makeEditor(editorText) makeEditor(editorText)
myFixture.testAction(AceAction.ActivateOrCycleMode()) myFixture.testAction(AceKeyboardAction.ActivateAceJump)
time += measureTimeMillis { typeAndWaitForResults("$query") } time += measureTimeMillis { typeAndWaitForResults("$query") }
// TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" } // TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" }
resetEditor() resetEditor()
} }
time time
} }
println("Average time to tag results: ${String.format("%.1f", avg.toDouble() / chars.size)} ms") println("Average time to tag results: ${String.format("%.1f", avg.toDouble() / chars.size)} ms")
} }
fun `test random text latency`() = `test tag latency`( fun `test random text latency`() = `test tag latency`(
generateSequence { generateSequence {
generateSequence { generateSequence {
@@ -35,8 +36,10 @@ class LatencyTest: BaseTest() {
}.take(20).joinToString(" ") }.take(20).joinToString(" ")
}.take(100).joinToString("\n") }.take(100).joinToString("\n")
) )
fun `test lorem ipsum latency`() = `test tag latency`( 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

@@ -4,62 +4,72 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.testFramework.FileEditorManagerTestCase import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import org.acejump.action.AceAction import org.acejump.action.AceKeyboardAction
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
abstract class BaseTest: FileEditorManagerTestCase() { abstract class BaseTest : BasePlatformTestCase() {
companion object { companion object {
inline fun averageTimeWithWarmup(warmupRuns: Int, timedRuns: Int, action: () -> Long): Long { inline fun averageTimeWithWarmup(warmupRuns: Int, timedRuns: Int, action: () -> Long): Long {
repeat(warmupRuns) { action() } repeat(warmupRuns) {
action()
}
var time = 0L var time = 0L
repeat(timedRuns) { time += action() }
repeat(timedRuns) {
time += action()
}
return time / timedRuns return time / timedRuns
} }
} }
protected val session get() = SessionManager[myFixture.editor]!! protected val session
get() = SessionManager[myFixture.editor]!!
override fun tearDown() { override fun tearDown() {
resetEditor() resetEditor()
super.tearDown() super.tearDown()
} }
fun takeAction(action: String) = myFixture.performEditorAction(action) fun takeAction(action: String) = myFixture.performEditorAction(action)
fun takeAction(action: AnAction) = myFixture.testAction(action) fun takeAction(action: AnAction) = myFixture.testAction(action)
fun makeEditor(contents: String): PsiFile = fun makeEditor(contents: String): PsiFile {
myFixture.configureByText(PlainTextFileType.INSTANCE, contents) return myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
fun resetEditor() {
myFixture.editor?.let {
takeAction(IdeActions.ACTION_EDITOR_ESCAPE)
UIUtil.dispatchAllInvocationEvents()
assertEmpty(it.markupModel.allHighlighters)
}
manager?.closeAllFiles()
} }
fun resetEditor() {
takeAction(IdeActions.ACTION_EDITOR_ESCAPE)
UIUtil.dispatchAllInvocationEvents()
assertEmpty(myFixture.editor.markupModel.allHighlighters)
}
fun typeAndWaitForResults(string: String) { fun typeAndWaitForResults(string: String) {
myFixture.type(string) myFixture.type(string)
UIUtil.dispatchAllInvocationEvents() UIUtil.dispatchAllInvocationEvents()
} }
fun String.executeQuery(query: String) = myFixture.run { fun String.executeQuery(query: String) {
makeEditor(this@executeQuery) myFixture.run {
testAction(AceAction.ActivateOrCycleMode()) makeEditor(this@executeQuery)
typeAndWaitForResults(query) testAction(AceKeyboardAction.ActivateAceJump)
typeAndWaitForResults(query)
}
} }
fun String.search(query: String): Set<Int> { fun String.search(query: String): Set<Int> {
this@search.executeQuery(query) this@search.executeQuery(query)
this@search.replace(Regex("<[^>]*>"), "").assertCorrectNumberOfTags(query) this@search.replace(Regex("<[^>]*>"), "").assertCorrectNumberOfTags(query)
return myFixture.editor.markupModel.allHighlighters.map { it.startOffset }.toSet() 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 -> assertEquals(split(query.fold("") { prefix, char ->
if ((prefix + char) in this) prefix + char else return if ((prefix + char) in this) prefix + char else return
}).size - 1, myFixture.editor.markupModel.allHighlighters.size) }).size - 1, myFixture.editor.markupModel.allHighlighters.size)
}
} }