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

164 Commits

Author SHA1 Message Date
04c1519261 Optimize screen visibility checks during search
When 'Search whole file' is disabled, search results are tested for visibility on the screen. There is already an optimization that only checks results on visible lines, but the number of offset-to-XY conversions still scales linearly with the number of results, which can be very large in files with long lines.

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

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

The worst case for this optimization is when every line has exactly one search result; before, this would lead to one offset-to-XY conversion per line, whereas now it leads to two XY-to-offset conversions per line. However, the maximum number of conversions is twice the number of visible lines, which will generally be very small.
2024-09-05 01:48:30 +02:00
breandan
efc81b8fde moved to gradle/libs.versions.toml 2024-08-02 15:47:33 -04:00
breandan
974c5cf258 Merge remote-tracking branch 'origin/master' 2024-08-02 15:11:58 -04:00
breandan
ae458094cf update to intellij gradle plugin v2 2024-08-02 15:11:49 -04:00
breandan
db62460d83 Merge pull request #460 from skyrylyuk/AceJump-139
#139 Improve test coverage - Add plugin for calculation Code Coverage
2024-08-02 15:07:08 -04:00
breandan
09888c2c34 Merge pull request #462 from KaelWD/patch-1
Add ENGRAM key layout
2024-08-02 14:55:30 -04:00
Kael
1d1f469663 Add ENGRAM key layout 2024-07-08 18:41:53 +10:00
Serhiy Kyrylyuk
21df680792 #139 Improve test coverage - Add plugin for calculation Code Coverage 2024-06-27 12:39:20 +03:00
breandan
1af49c2e91 Merge pull request #458 from Zilainfo/add-canary-key-layout
Add CANARY keyboard layout
2024-05-30 16:06:27 -04:00
kstas
ec7a769ae7 Add CANARY keyboard layout 2024-05-29 20:47:38 +03:00
breandan
d6d162e4cc Merge remote-tracking branch 'origin/master' 2024-04-29 10:10:20 -04:00
breandan
9d478c5b6e benchmark test latency 2024-04-29 10:10:12 -04:00
breandan
b66868664e remove obsolete warning 2024-04-23 21:02:02 -04:00
breandan
c68487c989 build against platform 2024.1, #457 2024-04-06 16:43:35 -04:00
breandan
ca2b0a8dbe fix language 2024-02-27 10:21:51 -05:00
breandan
76bc72335b update versions 2024-02-26 23:11:57 -05:00
breandan
fe4e180aeb add Jinho's project 2024-02-26 19:56:10 -05:00
breandan
932c933969 update versions and change notes, release 3.8.18 2024-01-01 17:46:38 -05:00
breandan
16f205b11b Merge pull request #455 from chylex/pr-folds
Do not assign tags inside folded regions
2024-01-01 17:29:17 -05:00
d0ead7d87a Do not assign tags inside folded regions
Closes #453
2024-01-01 11:39:35 +01:00
breandan
c2e672f869 add tmux-copycat 2023-12-19 18:25:04 -05:00
breandan
1256ffc237 update comparison chart 2023-12-19 18:20:00 -05:00
breandan
29a71bdddc fixes #404 2023-12-19 17:22:16 -05:00
breandan
9d2e7f7569 adjust hint styling and fix #394 2023-12-19 07:57:25 -05:00
breandan
751e6d890b fixes #452 2023-12-12 08:41:59 -05:00
breandan
283c1a5644 update change notes and release 3.8.17 2023-11-04 02:30:56 -04:00
breandan
73a37df7e2 add helpers for read and write actions 2023-10-28 18:10:14 -04:00
9921409aff Add buttons to reset colors to default values in Settings
Closes #411
2023-10-28 13:58:06 +02:00
breandan
3039b5fbb3 Merge pull request #450 from chylex/pr/kotlin-stdlib
Do not bundle kotlin stdlib
2023-10-28 00:21:29 -04:00
cb3d4559f9 Do not bundle kotlin stdlib
Fixes #449
2023-10-27 20:42:45 +02:00
breandan
d905909e31 attempt to fix #446 2023-09-16 16:34:46 -04:00
breandan
64c6956b88 add tmux plugins 2023-08-28 21:12:52 -04:00
breandan
b1a044dbf8 update change notes 2023-08-28 17:27:29 -04:00
breandan
e46cbe1d06 Merge remote-tracking branch 'origin/master' 2023-08-28 17:09:18 -04:00
breandan
f64e25a0a9 fixes #442 2023-08-28 17:09:12 -04:00
breandan
eba043dfb2 further instructions for locating AceJump.xml 2023-08-25 20:54:44 -04:00
breandan
a746b0a516 add note about settings deserialization 2023-08-25 09:33:49 -04:00
breandan
209b6c97b7 update versions 2023-08-08 01:19:41 -04:00
breandan
3c89cd07b9 try @YannCebron's solution to #415, pt. II 2023-07-26 22:08:35 -10:00
breandan
2ffcc2c57a try @YannCebron's solution to #415 2023-07-26 21:49:42 -10:00
breandan
56b1dfa9a9 quiet plugin verifier error 2023-06-18 14:22:04 -04:00
breandan
48cadfef4c update versions, acknowledge @wuruofan 2023-06-18 14:11:42 -04:00
breandan
6d47331c71 Merge pull request #440 from wuruofan/master
fix(compatibility): avoid 'java.lang.NoSuchFieldError: Companion' error on lower version IDEA ide
2023-06-18 13:45:42 -04:00
wuruofan
3339f6bdb8 fix(compatibility): avoid 'java.lang.NoSuchFieldError: Companion' error on lower version IDEA ide such as PhpStorm, DevEco Studio etc, which related to FileEditorManagerEx.getInstanceEx() companion
Related issues: [#434](https://github.com/acejump/AceJump/issues/434), [#438](https://github.com/acejump/AceJump/issues/438), [#435](https://github.com/acejump/AceJump/issues/435), etc.
2023-06-12 16:34:18 +08:00
breandan
e7e85a6944 update to gradle 8.2-rc-2 and new property assignment syntax 2023-06-11 21:42:16 -04:00
breandan
1839efb59f update version 2023-05-28 19:20:17 -04:00
breandan
da913f61b6 Merge remote-tracking branch 'origin/master' 2023-05-13 15:04:28 -04:00
breandan
7dd12a0457 fixes #431 2023-05-13 15:04:06 -04:00
breandan
dae7bd8fa4 fixes #431 2023-05-13 14:59:39 -04:00
breandan
7d39ea2fef fixes #429 2023-04-28 16:55:42 -04:00
breandan
64ad5039f2 update versions 2023-03-28 23:58:45 -04:00
breandan
fb3bfa068e try increasing heap size to appease teamcity? 2023-03-01 14:19:57 -05:00
breandan
c52f046a6f switch to K2 compiler 2023-02-14 00:21:43 -05:00
breandan
ba739501e8 use BGT and migrate awt.Color to JBColor 2023-02-13 18:45:18 -05:00
breandan
e939479368 fixes #426 2023-02-13 16:36:21 -05:00
breandan
1c604ce03c update versions 2023-02-09 16:10:41 -05:00
breandan
099c852e32 Merge remote-tracking branch 'origin/master' 2022-12-29 16:38:27 +11:00
breandan
6642eff1e0 update versions 2022-12-29 16:38:21 +11:00
breandan
73ba06b296 remove duplicate 2022-12-25 17:18:14 +13:00
breandan
9507496092 add Tridactyl 2022-12-25 17:17:30 +13:00
breandan
99e0a50884 fixes #417 2022-12-19 21:30:27 +13:00
breandan
c070b211e3 update to latest IDE version 2022-11-30 19:43:24 -05:00
breandan
6a2b5dd422 update build script 2022-11-27 14:05:06 -05:00
breandan
b7024842ba add leap and lightspeed 2022-10-13 22:49:12 -04:00
breandan
4831f38b34 update versions and remove unnecessary build instructions 2022-08-22 11:22:17 -04:00
breandan
07014455e6 Merge pull request #410 from AlexPl292/master
Add ids to editor action handlers
2022-08-22 10:59:32 -04:00
Alex Plate
ab6c6a702b Add ids to editor action handlers 2022-08-17 08:20:04 +03:00
breandan
8b9e8bb89b Merge pull request #409 from colossatr0n/patch-1
Update README.md to specify normal mode mapping for vim-sneak like behavior.
2022-07-24 15:35:48 -04:00
breandan
6c6544cf47 update versions 2022-07-15 23:47:12 -04:00
colossatr0n
7fe9d13f48 Update README.md to specify normal mode mapping for vim-sneak like behavior.
Also update ideavim action syntax to the latest syntax.
2022-07-12 14:23:17 -06:00
breandan
f1f9ec5974 fixes #408 2022-06-28 17:53:44 -07:00
breandan
5271d97c00 new kotlin version 2022-06-09 00:55:13 -07:00
breandan
872a99c84c update versions 2022-05-30 07:25:01 -07:00
breandan
f8b2db5090 Merge pull request #407 from huoguangjin/master
Remove only the highlighters added by AceJump when the jump session ends
2022-05-16 19:53:40 -04:00
breandan
5f0965c921 update versions 2022-05-16 19:52:12 -04:00
guangjinhuo
5ee0245f0b Add a test case to ensure that other highlighters are not removed #406 2022-04-28 17:30:37 +08:00
guangjinhuo
176d450855 Remove only the highlighters added by AceJump when the jump session ends #406 2022-04-23 15:20:23 +08:00
breandan
1341c6aff4 update version 2022-04-12 12:05:48 -04:00
breandan
0b0622dcdf update anyascii and remove jitpack, fixes anyascii/anyascii#6 2022-04-07 04:00:14 -04:00
breandan
929b08e9d0 update versions 2022-04-05 01:47:41 -04:00
breandan
54ea6ac9c1 Merge pull request #401 from colossatr0n/vim-sneak-bounded-toggle-actions
Add bounded toggle mode classes to start jump mode before the caret or after the caret.
2022-03-27 12:18:56 -04:00
colossatr0n
e477a487a2 Add bounded toggle mode classes to start jump mode before the caret or after the caret. 2022-03-27 10:05:42 -06:00
breandan
f56dc3d457 fixes #399 2022-03-26 14:39:21 -04:00
breandan
0befd7895e update versions 2022-03-26 14:34:43 -04:00
breandan
df94f4ce2a update buildscript as per https://youtrack.jetbrains.com/issue/IDEA-278926#focus=Comments-27-5881199.0-0 2022-03-26 14:26:17 -04:00
breandan
978edff59e update versions and change notes, acknowledge @delphinaubin 2022-03-14 12:42:52 -04:00
breandan
f9938590fb Merge pull request #398 from delphinaubin/add-azerty-key-layout
Add AZERTY keyboard layout (french keyboard)
2022-03-14 12:36:06 -04:00
Delphin Aubin
c534192902 Add AZERTY keyboard layout (french keyboard) 2022-02-25 17:01:04 +01:00
breandan
6deac53165 Merge remote-tracking branch 'origin/master' 2022-02-16 04:13:14 -05:00
breandan
2c4d4431bc update versions 2022-02-16 04:13:07 -05:00
breandan
2185da7cb4 Merge pull request #396 from DavidLGoldberg/update-similar-projects-jumpy 2022-02-04 21:27:37 -05:00
David L. Goldberg
c2a71a2c43 📝 Update README.md with Jumpy's status
- Jumpy1 is stable and has always been maintained as needed.  Atom's dev has slowed down. Will be fixed as needed.
- Adding Jumpy2.
2022-02-04 19:48:36 -05:00
breandan
d29dd3fa34 fixes #392 2021-12-19 12:49:04 -05:00
breandan
fdbcea0b4f update IDE version compatibility, fixes #389 2021-12-12 12:12:35 -05:00
breandan
c14ae702ba update change notes 2021-12-11 12:30:55 -05:00
breandan
8de4495885 update versions and remove deprecated 2021-12-11 12:06:29 -05:00
breandan
dd75e1ddea update unmaintained and remove canary builds 2021-11-30 00:25:37 -05:00
breandan
02f9ecec5a update language and reorder 2021-11-30 00:19:23 -05:00
f17f4ef8c4 Fix not highlighting last line
Closes #360
2021-11-21 20:55:19 +01:00
breandan
c9034bced6 update changelog 2021-11-21 02:39:25 -05:00
35003b0bab Improve tag order for non-QWERTY layouts 2021-11-21 05:29:43 +01:00
breandan
dc137e4d23 add hop 2021-11-20 15:54:52 -05:00
breandan
6858b745e6 fixes #356 2021-11-14 21:22:06 -05:00
breandan
1505e98d66 update changelog, migrate deprecated and fix compiler error 2021-11-14 17:01:11 -05:00
breandan
2b7015474e fix markdown 2021-11-14 16:32:44 -05:00
breandan
8d62b0d130 Merge pull request #384 from chylex/dpi
Workaround tag misalignment on high DPI screens
2021-11-14 16:25:55 -05:00
828940a53a Update attribution in README 2021-11-14 22:24:03 +01:00
41071dbaf9 Workaround tag misalignment on high DPI screens
Fixes #362.
2021-11-14 21:07:26 +01:00
breandan
cb10ff7789 reference IdeActions constants instead of hardcoded strings #379 2021-10-01 01:03:17 -04:00
breandan
71f8d7150d update versions 2021-09-12 22:35:15 -04:00
breandan
e619f5feb0 update versions 2021-08-26 00:46:21 -04:00
breandan
9dd85c1815 fixes #379 2021-07-09 00:48:53 -07:00
breandan
194dc3a14e update API usage example 2021-07-04 13:08:13 -04:00
breandan.considine
f1488f7fdf update versions 2021-06-24 11:54:15 -04:00
breandan
952a9af9ad run plugin verifier 2021-05-27 13:01:13 -04:00
breandan
86a7ad751f update versions 2021-05-27 12:49:48 -04:00
breandan
650a00f491 remove closed ticket from wishlist 2021-05-17 21:10:10 -04:00
breandan
9573315eff update change notes 2021-05-17 20:50:20 -04:00
breandan
eef4c2e08f display regex for regex queries 2021-05-17 20:45:18 -04:00
breandan
d1277dcb8a Merge pull request #377 from SaiKai/fix-line-mode
prevent setting INHERITED_COLOR_MARKER as border color
2021-05-17 20:27:18 -04:00
Jonas Gutenschwager
7ef147c941 prevent setting INHERITED_COLOR_MARKER as border color 2021-05-17 23:10:45 +02:00
breandan
4504da02ba clean up TextHighlighter and add release notes 2021-05-16 20:44:46 -04:00
breandan
7cce1b7034 Merge pull request #375 from SaiKai/display-search-text
#227 Add a notification hint with the current search text
2021-05-16 20:18:01 -04:00
breandan
16fd472132 Merge pull request #374 from SaiKai/fix-focus-editor
ensure editor focus also when using select function
2021-05-16 20:15:57 -04:00
Jonas Gutenschwager
3c231327d0 add a notification hint with the current search text 2021-05-17 01:09:22 +02:00
Jonas Gutenschwager
9284514ce3 ensure editor focus also when using select function 2021-05-16 22:07:17 +02:00
breandan
767a72a97c update contribution wishlist 2021-05-13 22:49:22 -04:00
breandan
d1ad283c8c bump versions 2021-05-13 22:43:45 -04:00
breandan
a169e3c751 fixes #373 2021-05-13 21:43:15 -04:00
breandan
0ee9f3e1cb support unicode characters, fixes #368 2021-05-10 21:45:41 -04:00
breandan
a05e92f3cb Merge pull request #371 from chylex/jump-across-splitters
Implement jumping across splitters
2021-05-10 20:59:05 -04:00
17a762e4d8 Update version to 3.8.0 2021-05-10 16:20:34 +02:00
aaa3f5d922 Add unit test for markResults with multiple editors 2021-05-09 08:44:53 +02:00
d407fd5333 Fix tests 2021-05-08 02:54:04 +02:00
12ae15c0f7 Implement jumping across splitters 2021-05-08 02:25:14 +02:00
breandan
5fd99c7f9b Merge remote-tracking branch 'origin/master' 2021-05-05 22:10:09 -04:00
breandan
d0cd15ef2c update versions 2021-05-05 22:10:03 -04:00
breandan
e6184ca6d3 add Japanese support to wishlist 2021-04-25 11:00:10 -04:00
breandan
020b6c1c7c update version to 3.7.1 2021-04-24 14:21:53 -04:00
breandan
9839a43da4 update wishlist 2021-04-17 17:09:53 -04:00
breandan
a7ebbcadb4 fixes #363 2021-04-17 16:13:15 -04:00
breandan
4622fbb20b pass back necessary info instead of reconstructing
Merge branch 'listener_update' of https://github.com/AlexPl292/AceJump into AlexPl292-listener_update

# Conflicts:
#	src/test/kotlin/ExternalUsageTest.kt
2021-04-17 15:44:08 -04:00
breandan
b3df72ef81 Merge remote-tracking branch 'origin/master' 2021-04-14 21:50:00 -04:00
breandan
00ed423faa update to RC 2021-04-14 21:49:54 -04:00
breandan
615ac0bc7a remove item from wishlist 2021-04-13 10:55:18 -04:00
breandan
2c0a236ed8 update gradle 2021-04-11 00:21:26 -04:00
breandan
d6553cd358 Merge remote-tracking branch 'origin/master' 2021-04-10 17:51:13 -04:00
breandan
5bb9a98723 update to 2021.1 2021-04-10 17:51:02 -04:00
breandan
85163d4fb9 Merge pull request #358 from AlexPl292/jump_boundaries
Allow defining jump mode with boundaries
2021-04-09 22:59:10 -04:00
breandan
02fee84923 Merge pull request #359 from AlexPl292/kotlin_objects
Do not use kotlin objects for actions
2021-04-09 22:57:45 -04:00
Alex Plate
89c3f88fbf Do not use kotlin objects for actions 2021-04-09 10:19:34 +03:00
Alex Plate
8fe050dc15 Allow defining jump mode with boundaries 2021-04-09 10:12:41 +03:00
breandan
61f0e3cf77 add test case for #355 2021-04-04 16:53:10 -04:00
breandan
27b897848d note that tab scrolling is unavailable in 3.7, cf. #356 2021-04-04 14:13:05 -04:00
breandan
e17abbdabb fix changenotes 2021-04-03 23:59:06 -04:00
breandan
5da53c603e update changes and readme acknowledging #353, #348 🎉 thanks @chylex! 2021-04-03 21:08:09 -04:00
breandan
59e0082236 document ExternalUsage endpoints in README.md #354 2021-04-03 20:37:22 -04:00
breandan
d2b1fb96dd move ExternalUsage tests to new class and add test cases for pattern and regex #354 2021-04-03 20:19:04 -04:00
breandan
d5dfa95896 test whether listeners are notified post-disposal #354 2021-04-03 18:37:34 -04:00
breandan
a109ff7fce notify listeners of Session disposal, cf. #354 2021-04-03 18:20:07 -04:00
breandan
b33ae482b0 reimplements #307, cf. #354 2021-04-03 17:56:54 -04:00
breandan
c6b70109da more reformatting 2021-04-03 16:13:17 -04:00
breandan
beb39c95c8 refactor, add EditorCache and reimplement Pinyin support 2021-04-03 11:43:33 -04:00
breandan
f675efbc12 Merge branch 'chylex-refactor-pr' 2021-04-03 09:01:17 -04:00
48 changed files with 2500 additions and 1380 deletions

3
.github/FUNDING.yml vendored
View File

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

View File

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

114
README.md
View File

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

View File

@@ -1,66 +1,100 @@
import org.jetbrains.changelog.closure
import org.jetbrains.intellij.tasks.PatchPluginXmlTask
import org.jetbrains.intellij.tasks.PublishTask
import org.jetbrains.intellij.tasks.RunIdeTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.changelog.Changelog.OutputType.HTML
import org.jetbrains.changelog.date
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
plugins {
idea apply true
kotlin("jvm") version "1.3.72"
id("org.jetbrains.intellij") version "0.7.2"
id("org.jetbrains.changelog") version "1.1.2"
id("com.github.ben-manes.versions") version "0.38.0"
idea
alias(libs.plugins.kotlin) // Kotlin support
alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin
alias(libs.plugins.changelog) // Gradle Changelog Plugin
alias(libs.plugins.kover) // Gradle Kover Plugin
id("com.github.ben-manes.versions") version "0.51.0"
}
tasks {
withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
named<Zip>("buildPlugin") {
dependsOn("test")
archiveFileName.set("AceJump.zip")
archiveFileName = "AceJump.zip"
}
withType<RunIdeTask> {
dependsOn("test")
runIde {
findProperty("luginDev")?.let { args = listOf(projectDir.absolutePath) }
}
withType<PublishTask> {
publishPlugin {
val intellijPublishToken: String? by project
token(intellijPublishToken)
token = intellijPublishToken
}
withType<PatchPluginXmlTask> {
sinceBuild("201.6668.0")
changeNotes({ changelog.getLatest().toHTML() })
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 {
version = acejumpVersion
path = "${project.projectDir}/CHANGES.md"
header = closure { "${project.version}" }
}
dependencies {
// gradle-intellij-plugin doesn't attach sources properly for Kotlin :(
compileOnly(kotlin("stdlib-jdk8"))
// https://github.com/promeG/TinyPinyin
implementation("com.github.promeg:tinypinyin:2.0.3")
header = provider { "[${project.version}] - ${date()}" }
itemPrefix = "-"
unreleasedTerm = "Unreleased"
}
repositories {
mavenCentral()
jcenter()
intellijPlatform.defaultRepositories()
// intellijPlatform.localPlatformArtifacts()
}
intellij {
version = "2020.2"
pluginName = "AceJump"
updateSinceUntilBuild = false
setPlugins("java")
dependencies {
// https://github.com/anyascii/anyascii
implementation("com.anyascii:anyascii:0.3.2")
intellijPlatform{
testImplementation(libs.junit)
bundledPlugins("com.intellij.java")
create("IC", "2024.1.4")
pluginVerifier()
instrumentationTools()
testFramework(TestFrameworkType.Platform)
}
}
intellijPlatform {
pluginConfiguration {
version = acejumpVersion
name = "AceJump"
}
pluginVerification.ides.recommended()
}
group = "org.acejump"
version = "3.7"
version = acejumpVersion

4
gradle.properties Normal file
View File

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

18
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,18 @@
[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" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,24 +1,57 @@
package org.acejump
import com.intellij.openapi.editor.Editor
import com.anyascii.AnyAscii
import com.intellij.diff.util.DiffUtil.getLineCount
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.*
import com.intellij.openapi.util.Computable
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.config.AceConfig
import java.awt.Point
import kotlin.math.*
/**
* This annotation is a marker which means that the annotated function is
* used in external plugins.
*/
@Retention(AnnotationRetention.SOURCE)
annotation class ExternalUsage
/**
* Returns an immutable version of the currently edited document.
*/
val Editor.immutableText
get() = this.document.immutableCharSequence
val Editor.immutableText get() = EditorsCache.getText(this)
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.
*/
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean): Boolean {
return this.regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
}
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean) =
regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
/**
* Calculates the length of a common prefix in [this] starting at index [selfOffset], and [otherText] starting at index 0.
* Calculates the length of a common prefix in [this] starting
* at index [selfOffset], and [otherText] starting at index 0.
*/
fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): Int {
var i = 0
@@ -41,12 +74,12 @@ val Char.isWordPart
/**
* Finds index of the first character in a word.
*/
inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
inline fun CharSequence.wordStart(
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var start = pos
while (start > 0 && isPartOfWord(this[start - 1])) {
--start
}
while (start > 0 && isPartOfWord(this[start - 1])) --start
return start
}
@@ -54,29 +87,175 @@ inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Ch
/**
* Finds index of the last character in a word.
*/
inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
inline fun CharSequence.wordEnd(
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var end = pos
while (end < length - 1 && isPartOfWord(this[end + 1])) {
++end
}
while (end < length - 1 && isPartOfWord(this[end + 1])) ++end
return end
}
/**
* Finds index of the first word character following a sequence of non-word characters following the end of a word.
* 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 {
inline fun CharSequence.wordEndPlus(
pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart
): Int {
var end = this.wordEnd(pos, isPartOfWord)
while (end < length - 1 && !isPartOfWord(this[end + 1])) {
++end
}
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
}
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,25 +1,61 @@
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
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() {
sealed class AceAction: DumbAwareAction() {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
action.presentation.isEnabled =
(action.getData(EDITOR) ?: (action.getData(LAST_ACTIVE_FILE_EDITOR) as? TextEditor)?.editor) != null
}
final override fun actionPerformed(e: AnActionEvent) {
invoke(SessionManager.start(e.getData(EDITOR) ?: return))
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)
@@ -27,45 +63,58 @@ sealed class AceAction : DumbAwareAction() {
/**
* Generic action type that toggles a specific [JumpMode].
*/
abstract class BaseToggleJumpModeAction(private val mode: JumpMode) : AceAction() {
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() {
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.
*/
object ActivateOrCycleMode : AceAction() {
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.
*/
object ActivateOrReverseCycleMode : AceAction() {
class ActivateOrReverseCycleMode: AceAction() {
override fun invoke(session: Session) = session.cyclePreviousJumpMode()
}
// @formatter:off
object ToggleJumpMode : BaseToggleJumpModeAction(JumpMode.JUMP)
object ToggleJumpEndMode : BaseToggleJumpModeAction(JumpMode.JUMP_END)
object ToggleTargetMode : BaseToggleJumpModeAction(JumpMode.TARGET)
object ToggleDeclarationMode : BaseToggleJumpModeAction(JumpMode.DEFINE)
// Unbounded Toggle Modes
class ToggleJumpMode : BaseToggleJumpModeAction(JUMP)
class ToggleJumpEndMode : BaseToggleJumpModeAction(JUMP_END)
class ToggleTargetMode : BaseToggleJumpModeAction(TARGET)
class ToggleDeclarationMode : BaseToggleJumpModeAction(DECLARATION)
object StartAllWordsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.WHOLE_FILE)
object StartAllWordsBackwardsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.BEFORE_CARET)
object StartAllWordsForwardMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.AFTER_CARET)
object StartAllLineStartsMode : BaseRegexSearchAction(Pattern.LINE_STARTS, StandardBoundaries.WHOLE_FILE)
object StartAllLineEndsMode : BaseRegexSearchAction(Pattern.LINE_ENDS, StandardBoundaries.WHOLE_FILE)
object StartAllLineIndentsMode : BaseRegexSearchAction(Pattern.LINE_INDENTS, StandardBoundaries.WHOLE_FILE)
object StartAllLineMarksMode : BaseRegexSearchAction(Pattern.LINE_ALL_MARKS, StandardBoundaries.WHOLE_FILE)
// 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,59 +4,63 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.Pattern
import org.acejump.boundaries.StandardBoundaries.*
import org.acejump.search.Pattern.*
import org.acejump.session.Session
import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
*/
sealed class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
}
sealed class AceEditorAction(private val originalHandler: EditorActionHandler): EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean =
SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val session = SessionManager[editor]
if (session != null) {
run(session)
}
else if (originalHandler.isEnabled(editor, caret, dataContext)) {
if (session != null) run(session)
else if (originalHandler.isEnabled(editor, caret, dataContext))
originalHandler.execute(editor, caret, dataContext)
}
}
protected abstract fun run(session: Session)
// Actions
class Reset(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
class Reset(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
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()
}
class SelectBackward(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
class SelectBackward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitPreviousTag()
}
class SelectForward(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
class SelectForward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitNextTag()
}
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.WHOLE_FILE)
class ScrollToNextScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) { session.scrollToNextScreenful() }
}
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.WHOLE_FILE)
class ScrollToPreviousScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
override fun run(session: Session) { session.scrollToPreviousScreenful() }
}
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.WHOLE_FILE)
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

@@ -1,30 +1,43 @@
package org.acejump.action
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction
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.*
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import org.acejump.input.JumpMode
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 editor: Editor, private val mode: JumpMode, private val searchProcessor: SearchProcessor?) {
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(offset: Int) {
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
@@ -34,17 +47,14 @@ internal class TagJumper(private val editor: Editor, private val mode: JumpMode,
if (mode === JUMP_END) {
moveCaretTo(editor, finalTargetOffset)
}
else if (mode === TARGET) {
} else if (mode === TARGET) {
if (isInsideWord) {
selectRange(editor, chars.wordStart(targetOffset), finalTargetOffset)
}
else {
} else {
selectRange(editor, offset, finalTargetOffset)
}
}
}
else {
} else {
moveCaretTo(editor, offset)
}
}
@@ -53,26 +63,28 @@ internal class TagJumper(private val editor: Editor, private val mode: JumpMode,
* 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(offset: Int, shiftMode: Boolean) {
fun jump(tag: Tag, shiftMode: Boolean, isCrossEditor: Boolean) {
val editor = tag.editor
val oldOffset = editor.caretModel.offset
visit(offset)
visit(tag)
if (mode === DEFINE) {
performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
if (mode === DECLARATION) {
performAction(ActionManager.getInstance().getAction(if (shiftMode) ACTION_GOTO_TYPE_DECLARATION else ACTION_GOTO_DECLARATION))
return
}
if (shiftMode) {
if (shiftMode && !isCrossEditor) {
val newOffset = editor.caretModel.offset
if (mode === TARGET) {
selectRange(editor, oldOffset, when {
selectRange(
editor, oldOffset, when {
newOffset < oldOffset -> editor.selectionModel.selectionStart
else -> editor.selectionModel.selectionEnd
})
}
else {
)
} else {
selectRange(editor, oldOffset, newOffset)
}
}
@@ -80,29 +92,43 @@ internal class TagJumper(private val editor: Editor, private val mode: JumpMode,
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, {
CommandProcessor.getInstance().executeCommand(
project, {
with(IdeDocumentHistory.getInstance(project)) {
setCurrentCommandHasMoves()
includeCurrentCommandAsNavigation()
includeCurrentPlaceAsChangePlace()
}
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document), UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document)
}, "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)
}
private fun performAction(action: AnAction) = ActionManager.getInstance()
.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
}

View File

@@ -0,0 +1,64 @@
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,9 +1,9 @@
package org.acejump.action
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.SelectionModel
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
/**
@@ -11,63 +11,59 @@ import kotlin.math.abs
*/
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]).
* 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 {
return visit(SelectionModel::getSelectionEnd) { if (it < 0) -it - 1 else it + 1 }
}
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]).
* 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 {
return visit(SelectionModel::getSelectionStart) { if (it < 0) -it - 2 else it - 1 }
}
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.takeUnless { it.isEmpty } ?: return
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)
).minBy {
abs(it - caret)
}
).minByOrNull { abs(it - caret) }
if (targetOffset != null) {
editor.scrollingModel.scrollTo(editor.offsetToLogicalPosition(targetOffset), ScrollType.RELATIVE)
}
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.takeUnless { it.isEmpty } ?: return false
val results = searchProcessor.results[editor].takeUnless { it.isNullOrEmpty() } ?: return false
val nextIndex = indexModifier(results.binarySearch(caretPosition(editor.selectionModel)))
val targetOffset = results.getInt(when {
val targetOffset = results.getInt(
when {
nextIndex < 0 -> results.lastIndex
nextIndex > results.lastIndex -> 0
else -> nextIndex
})
}
)
val onlyResult = results.size == 1
if (onlyResult) {
tagJumper.jump(targetOffset, shiftMode = false)
}
else {
tagJumper.visit(targetOffset)
}
if (onlyResult) tagJumper.jump(Tag(editor, targetOffset), shiftMode = false, isCrossEditor = false)
else tagJumper.visit(Tag(editor, targetOffset))
editor.scrollingModel.scrollToCaret(ScrollType.RELATIVE)
editor.scrollingModel.scrollToCaret(RELATIVE)
return onlyResult
}
}

View File

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

View File

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

@@ -4,13 +4,11 @@ import com.intellij.openapi.editor.Editor
enum class StandardBoundaries : Boundaries {
WHOLE_FILE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0 until editor.document.textLength
}
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
0..editor.document.textLength
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in (0 until editor.document.textLength)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache) =
offset in (0..editor.document.textLength)
},
VISIBLE_ON_SCREEN {
@@ -23,43 +21,23 @@ enum class StandardBoundaries : Boundaries {
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
// 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
return cache.isVisible(editor, offset)
}
},
BEFORE_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0..(editor.caretModel.offset)
}
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
0..(editor.caretModel.offset)
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset <= editor.caretModel.offset
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
offset <= editor.caretModel.offset
},
AFTER_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return editor.caretModel.offset until editor.document.textLength
}
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
editor.caretModel.offset until editor.document.textLength
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset >= editor.caretModel.offset
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
offset >= editor.caretModel.offset
}
}

View File

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

View File

@@ -4,7 +4,7 @@ import com.intellij.openapi.options.Configurable
import org.acejump.config.AceConfig.Companion.settings
import org.acejump.input.KeyLayoutCache
class AceConfigurable : Configurable {
class AceConfigurable: Configurable {
private val panel by lazy(::AceSettingsPanel)
override fun getDisplayName() = "AceJump"
@@ -19,14 +19,16 @@ class AceConfigurable : Configurable {
panel.cycleMode3 != settings.cycleMode3 ||
panel.cycleMode4 != settings.cycleMode4 ||
panel.minQueryLengthInt != settings.minQueryLength ||
panel.jumpModeColor != settings.jumpModeColor ||
panel.jumpEndModeColor != settings.jumpEndModeColor ||
panel.targetModeColor != settings.targetModeColor ||
panel.definitionModeColor != settings.definitionModeColor ||
panel.textHighlightColor != settings.textHighlightColor ||
panel.tagForegroundColor != settings.tagForegroundColor ||
panel.tagBackgroundColor != settings.tagBackgroundColor ||
panel.searchWholeFile != settings.searchWholeFile
panel.jumpModeColor?.rgb != settings.jumpModeColor ||
panel.jumpEndModeColor?.rgb != settings.jumpEndModeColor ||
panel.targetModeColor?.rgb != settings.targetModeColor ||
panel.definitionModeColor?.rgb != settings.definitionModeColor ||
panel.textHighlightColor?.rgb != settings.textHighlightColor ||
panel.tagForegroundColor?.rgb != settings.tagForegroundColor ||
panel.tagBackgroundColor?.rgb != settings.tagBackgroundColor ||
panel.searchWholeFile != settings.searchWholeFile ||
panel.mapToASCII != settings.mapToASCII ||
panel.showSearchNotification != settings.showSearchNotification
override fun apply() {
settings.allowedChars = panel.allowedChars
@@ -36,14 +38,16 @@ class AceConfigurable : Configurable {
settings.cycleMode3 = panel.cycleMode3
settings.cycleMode4 = panel.cycleMode4
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.jumpEndModeColor?.let { settings.jumpEndModeColor = it }
panel.targetModeColor?.let { settings.targetModeColor = it }
panel.definitionModeColor?.let { settings.definitionModeColor = it }
panel.textHighlightColor?.let { settings.textHighlightColor = it }
panel.tagForegroundColor?.let { settings.tagForegroundColor = it }
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it }
panel.jumpModeColor?.let { settings.jumpModeColor = it.rgb }
panel.jumpEndModeColor?.let { settings.jumpEndModeColor = it.rgb }
panel.targetModeColor?.let { settings.targetModeColor = it.rgb }
panel.definitionModeColor?.let { settings.definitionModeColor = it.rgb }
panel.textHighlightColor?.let { settings.textHighlightColor = it.rgb }
panel.tagForegroundColor?.let { settings.tagForegroundColor = it.rgb }
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it.rgb }
settings.searchWholeFile = panel.searchWholeFile
settings.mapToASCII = panel.mapToASCII
settings.showSearchNotification = panel.showSearchNotification
KeyLayoutCache.reset(settings)
}

View File

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

View File

@@ -1,21 +1,12 @@
package org.acejump.config
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBCheckBox
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 com.intellij.ui.components.*
import com.intellij.ui.dsl.builder.*
import org.acejump.input.JumpMode
import org.acejump.input.KeyLayout
import java.awt.Color
import java.awt.Font
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JPanel
import java.awt.*
import javax.swing.*
import javax.swing.text.JTextComponent
import kotlin.reflect.KProperty
@@ -24,6 +15,8 @@ import kotlin.reflect.KProperty
*/
@Suppress("UsePropertyAccessSyntax")
internal class AceSettingsPanel {
private val defaults = AceSettings()
private val tagCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
@@ -32,14 +25,16 @@ internal class AceSettingsPanel {
private val cycleModeCombo3 = ComboBox<JumpMode>()
private val cycleModeCombo4 = ComboBox<JumpMode>()
private val minQueryLengthField = JBTextField()
private val jumpModeColorWheel = ColorPanel()
private val jumpEndModeColorWheel = ColorPanel()
private val targetModeColorWheel = ColorPanel()
private val definitionModeColorWheel = ColorPanel()
private val textHighlightColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel()
private val tagBackgroundColorWheel = ColorPanel()
private val jumpModeColorWheel = ResettableColorPicker(defaults.getJumpModeJBC())
private val jumpEndModeColorWheel = ResettableColorPicker(defaults.getJumpEndModeJBC())
private val targetModeColorWheel = ResettableColorPicker(defaults.getTargetModeJBC())
private val definitionModeColorWheel = ResettableColorPicker(defaults.getDefinitionModeJBC())
private val textHighlightColorWheel = ResettableColorPicker(defaults.getTextHighlightJBC())
private val tagForegroundColorWheel = ResettableColorPicker(defaults.getTagForegroundJBC())
private val tagBackgroundColorWheel = ResettableColorPicker(defaults.getTagBackgroundJBC())
private val searchWholeFileCheckBox = JBCheckBox()
private val mapToASCIICheckBox = JBCheckBox()
private val showSearchNotificationCheckBox = JBCheckBox()
init {
tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
@@ -52,39 +47,40 @@ internal class AceSettingsPanel {
}
internal val rootPanel: JPanel = panel {
fun Cell.short(component: JComponent) = component(growPolicy = SHORT_TEXT)
fun Cell.medium(component: JComponent) = component(growPolicy = MEDIUM_TEXT)
titledRow("Characters and Layout") {
row("Allowed characters in tags:") { medium(tagCharsField) }
row("Keyboard layout:") { short(keyboardLayoutCombo) }
row("Keyboard design:") { short(keyboardLayoutArea) }
group("Characters and Layout") {
row("Allowed characters in tags:") { cell(tagCharsField).columns(COLUMNS_LARGE) }
row("Keyboard layout:") { cell(keyboardLayoutCombo).columns(COLUMNS_SHORT) }
row("Keyboard design:") { cell(keyboardLayoutArea).columns(COLUMNS_SHORT) }
}
titledRow("Modes") {
group("Modes") {
row("Cycle order:") {
cell(isVerticalFlow = false, isFullWidth = false) {
cycleModeCombo1()
cycleModeCombo2()
cycleModeCombo3()
cycleModeCombo4()
}
cell(cycleModeCombo1).columns(10)
cell(cycleModeCombo2).columns(10)
cell(cycleModeCombo3).columns(10)
cell(cycleModeCombo4).columns(10)
}
}
titledRow("Colors") {
row("Jump mode caret background:") { short(jumpModeColorWheel) }
row("Jump to End mode caret background:") { short(jumpEndModeColorWheel) }
row("Target mode caret background:") { short(targetModeColorWheel) }
row("Definition mode caret background:") { short(definitionModeColorWheel) }
row("Searched text background:") { short(textHighlightColorWheel) }
row("Tag foreground:") { short(tagForegroundColorWheel) }
row("Tag background:") { short(tagBackgroundColorWheel) }
group("Colors") {
row("Jump mode caret background:") { cell(jumpModeColorWheel) }
row("Jump to End mode caret background:") { cell(jumpEndModeColorWheel) }
row("Target mode caret background:") { cell(targetModeColorWheel) }
row("Definition mode caret background:") { cell(definitionModeColorWheel) }
row("Searched text background:") { cell(textHighlightColorWheel) }
row("Tag foreground:") { cell(tagForegroundColorWheel) }
row("Tag background:") { cell(tagBackgroundColorWheel) }
}
titledRow("Behavior") {
row { short(searchWholeFileCheckBox.apply { text = "Search whole file" }) }
row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
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" }) }
}
}
@@ -105,10 +101,14 @@ internal class AceSettingsPanel {
internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel
internal var searchWholeFile by searchWholeFileCheckBox
internal var mapToASCII by mapToASCIICheckBox
internal var showSearchNotification by showSearchNotificationCheckBox
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) { minQueryLength = value.toString() }
set(value) {
minQueryLength = value.toString()
}
fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars
@@ -118,31 +118,33 @@ internal class AceSettingsPanel {
cycleMode3 = settings.cycleMode3
cycleMode4 = settings.cycleMode4
minQueryLength = settings.minQueryLength.toString()
jumpModeColor = settings.jumpModeColor
jumpEndModeColor = settings.jumpEndModeColor
targetModeColor = settings.targetModeColor
definitionModeColor = settings.definitionModeColor
textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor
tagBackgroundColor = settings.tagBackgroundColor
jumpModeColor = settings.getJumpModeJBC()
jumpEndModeColor = settings.getJumpEndModeJBC()
targetModeColor = settings.getTargetModeJBC()
definitionModeColor = settings.getDefinitionModeJBC()
textHighlightColor = settings.getTextHighlightJBC()
tagForegroundColor = settings.getTagForegroundJBC()
tagBackgroundColor = settings.getTagBackgroundJBC()
searchWholeFile = settings.searchWholeFile
mapToASCII = settings.mapToASCII
showSearchNotification = settings.showSearchNotification
}
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.toLowerCase()
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.lowercase()
private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s)
private operator fun ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor
private operator fun ColorPanel.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c)
private operator fun ResettableColorPicker.getValue(a: AceSettingsPanel, p: KProperty<*>) = getSelectedColor()
private operator fun ResettableColorPicker.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c)
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
private operator fun JCheckBox.setValue(a: AceSettingsPanel, p: KProperty<*>, selected: Boolean) = setSelected(selected)
private operator fun <T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private inline operator fun <reified T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)
private inline fun <reified T : Enum<T>> ComboBox<T>.setupEnumItems(crossinline onChanged: (T) -> Unit) {
private inline fun <reified T: Enum<T>> ComboBox<T>.setupEnumItems(crossinline onChanged: (T) -> Unit) {
T::class.java.enumConstants.forEach(this::addItem)
addActionListener { onChanged(selectedItem as T) }
}

View File

@@ -1,14 +0,0 @@
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

@@ -0,0 +1,47 @@
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,10 +6,11 @@ import com.intellij.openapi.editor.actionSystem.TypedAction
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
/**
* If at least one session exists, this listener redirects all characters, typed in [Editor]s with attached sessions, to the appropriate
* sessions' own handlers.
* If at least one session exists, this listener redirects all characters
* typed in [Editor]s with attached sessions to the appropriate sessions'
* own handlers.
*/
internal object EditorKeyListener : TypedActionHandler {
internal object EditorKeyListener: TypedActionHandler {
private val action = TypedAction.getInstance()
private val attached = mutableMapOf<Editor, TypedActionHandler>()
private var originalHandler: TypedActionHandler? = null

View File

@@ -5,57 +5,68 @@ 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:
* 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.
* 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.
* 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 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.
* 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 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 original caret position and the new selection, merging the
* selections into one.
* 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`.
* 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.
*/
DEFINE;
DECLARATION;
val caretColor: Color
get() = when (this) {
val caretColor: Color get() = when (this) {
DISABLED -> AbstractColorsScheme.INHERITED_COLOR_MARKER
JUMP -> AceConfig.jumpModeColor
JUMP_END -> AceConfig.jumpEndModeColor
DEFINE -> AceConfig.definitionModeColor
TARGET -> AceConfig.targetModeColor
DISABLED -> AbstractColorsScheme.INHERITED_COLOR_MARKER
DECLARATION -> AceConfig.definitionModeColor
}
override fun toString() = when (this) {
@@ -63,6 +74,6 @@ enum class JumpMode {
JUMP -> "Jump"
JUMP_END -> "Jump to End"
TARGET -> "Target"
DEFINE -> "Definition"
DECLARATION -> "Definition"
}
}

View File

@@ -3,16 +3,19 @@ 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.
* 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
* 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 {
@@ -36,14 +39,14 @@ internal class JumpModeTracker {
}
/**
* Switches to the specified [JumpMode]. If the current mode already equals the specified one, it resets to [JumpMode.DISABLED].
* 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 {
} else {
currentMode = newMode
currentIndex = AceConfig.cycleModes.indexOfFirst { it == newMode } + 1
}

View File

@@ -1,8 +1,14 @@
package org.acejump.input
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import java.awt.geom.Point2D
import kotlin.math.floor
/**
* Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how
* ergonomically difficult they are to press.
* Defines common keyboard layouts. Each layout has a key priority order,
* based on each key's distance from the home row and how ergonomically
* difficult they are to press.
*/
@Suppress("unused")
enum class KeyLayout(internal val rows: Array<String>, priority: String) {
@@ -13,12 +19,46 @@ enum class KeyLayout(internal val rows: Array<String>, priority: String) {
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
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 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? {
return { allPriorities[tagToChar(it)] }
}
internal fun distanceBetweenKeys(char1: Char, char2: Char): Int {
return keyDistances.getValue(char1).getValue(char2)
}
}

View File

@@ -7,48 +7,6 @@ import org.acejump.config.AceSettings
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/
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
* keys far apart, to be sorted after other (easier to type) tags.
@@ -65,11 +23,8 @@ internal object KeyLayoutCache {
/**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/
fun ensureInitialized(settings: AceSettings) {
if (!::tagOrder.isInitialized) {
reset(settings)
}
}
fun ensureInitialized(settings: AceSettings) =
if (!::tagOrder.isInitialized) reset(settings) else Unit
/**
* Re-initializes cached data according to updated settings.
@@ -77,7 +32,7 @@ internal object KeyLayoutCache {
fun reset(settings: AceSettings) {
tagOrder = compareBy(
{ it[0].isDigit() || it[1].isDigit() },
{ qwertyCharacterDistances.getValue(it[0]).getValue(it[1]) },
{ settings.layout.distanceBetweenKeys(it[0], it[1]) },
settings.layout.priority { it[0] }
)
@@ -88,6 +43,8 @@ internal object KeyLayoutCache {
.joinToString("")
.ifEmpty(settings.layout::allChars)
allPossibleTags = allPossibleChars.flatMap { a -> allPossibleChars.map { b -> "$a$b".intern() } }.sortedWith(tagOrder)
allPossibleTags = allPossibleChars.flatMap { a ->
allPossibleChars.map { b -> "$a$b".intern() }
}.sortedWith(tagOrder)
}
}

View File

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

View File

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

View File

@@ -19,27 +19,27 @@ internal sealed class SearchQuery {
abstract fun toRegex(): Regex?
/**
* Searches for all occurrences of a literal text query. If the first character of the query is lowercase, then the entire query will be
* Searches for all occurrences of a literal text query. If the first
* character of the query is lowercase, then the entire query will be
* case-insensitive.
*
* Each occurrence must either match the entire query, or match the query up to a point so that the rest of the query matches the
* beginning of a tag at the location of the occurrence.
* Each occurrence must either match the entire query, or match the query
* up to a point so that the rest of the query matches the beginning of
* a tag at the location of the occurrence.
*/
class Literal(override var rawText: String) : SearchQuery() {
class Literal(override var rawText: String): SearchQuery() {
init {
require(rawText.isNotEmpty())
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return text.countMatchingCharacters(offset, rawText)
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int =
text.countMatchingCharacters(offset, rawText)
override fun toRegex(): Regex {
val options = mutableSetOf(RegexOption.MULTILINE)
if (rawText.first().isLowerCase()) {
if (rawText.first().isLowerCase())
options.add(RegexOption.IGNORE_CASE)
}
return Regex(Regex.escape(rawText), options)
}
@@ -48,15 +48,12 @@ internal sealed class SearchQuery {
/**
* Searches for all matches of a regular expression.
*/
class RegularExpression(private var pattern: String) : SearchQuery() {
class RegularExpression(private var pattern: String): SearchQuery() {
override val rawText = ""
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return 1
}
override fun getHighlightLength(text: CharSequence, offset: Int) = 0
override fun toRegex(): Regex {
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
}
override fun toRegex(): Regex =
Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,13 +4,17 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.openapi.editor.colors.EditorColors
import org.acejump.ExternalUsage
import com.intellij.openapi.editor.colors.EditorColors.CARET_COLOR
import com.intellij.util.containers.ContainerUtil
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.*
import org.acejump.action.TagScroller
import org.acejump.action.TagJumper
import org.acejump.action.TagVisitor
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
import org.acejump.config.AceConfig
import org.acejump.input.EditorKeyListener
import org.acejump.input.JumpMode
@@ -22,17 +26,23 @@ import org.acejump.search.Tagger
import org.acejump.search.TaggingResult
import org.acejump.view.TagCanvas
import org.acejump.view.TextHighlighter
import java.util.*
/**
* Manages an AceJump session for a single [Editor].
* Manages an AceJump session for one or more [Editor]s.
*/
class Session(private val editor: Editor) {
class Session(private val mainEditor: Editor, private val jumpEditors: List<Editor>) {
private val listeners: MutableList<AceJumpListener> =
ContainerUtil.createLockFreeCopyOnWriteList()
private var boundaries: Boundaries = defaultBoundaries
private companion object {
private val defaultBoundaries
get() = if (AceConfig.searchWholeFile) StandardBoundaries.WHOLE_FILE else StandardBoundaries.VISIBLE_ON_SCREEN
get() = if (AceConfig.searchWholeFile) WHOLE_FILE else VISIBLE_ON_SCREEN
}
private val originalSettings = EditorSettings.setup(editor)
private val originalSettings = EditorSettings.setup(mainEditor)
private val jumpModeTracker = JumpModeTracker()
private var jumpMode = JumpMode.DISABLED
@@ -41,25 +51,27 @@ class Session(private val editor: Editor) {
if (value === JumpMode.DISABLED) {
end()
}
else {
} else {
searchProcessor?.let { textHighlighter.render(it.results, it.query, jumpMode) }
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, value.caretColor)
editor.contentComponent.repaint()
mainEditor.colorsScheme.setColor(CARET_COLOR, value.caretColor)
mainEditor.contentComponent.repaint()
}
}
private var searchProcessor: SearchProcessor? = null
private var tagger = Tagger(editor)
private var tagger = Tagger(jumpEditors)
private val tagJumper
get() = TagJumper(editor, jumpMode, searchProcessor)
get() = TagJumper(jumpMode, searchProcessor)
private val tagVisitor
get() = searchProcessor?.let { TagVisitor(editor, it, tagJumper) }
get() = searchProcessor?.let { TagVisitor(mainEditor, it, tagJumper) }
private val textHighlighter = TextHighlighter(editor)
private val tagCanvas = TagCanvas(editor)
private val tagScroller
get() = searchProcessor?.let { TagScroller(mainEditor, it) }
private val textHighlighter = TextHighlighter()
private val tagCanvases = jumpEditors.associateWith(::TagCanvas)
@ExternalUsage
val tags
@@ -68,76 +80,127 @@ class Session(private val editor: Editor) {
init {
KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(editor, object : TypedActionHandler {
EditorKeyListener.attach(mainEditor, object: TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
var processor = searchProcessor
val hadTags = tagger.hasTags
if (processor == null) {
processor = SearchProcessor.fromChar(editor, charTyped, defaultBoundaries).also { searchProcessor = it }
}
else if (!processor.type(charTyped, tagger)) {
processor = SearchProcessor.fromChar(
jumpEditors, charTyped, boundaries
).also { searchProcessor = it }
} else if (!processor.type(charTyped, tagger)) {
return
}
updateSearch(processor, markImmediately = hadTags, shiftMode = charTyped.isUpperCase())
updateSearch(
processor, markImmediately = hadTags,
shiftMode = charTyped.isUpperCase()
)
}
})
}
/**
* Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag.
* Updates text highlights and tag markers according to the current
* search state. Dispatches jumps if the search query matches a tag.
* If all tags are outside view, scrolls to the closest one.
*/
private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean, shiftMode: Boolean = false) {
private fun updateSearch(
processor: SearchProcessor,
markImmediately: Boolean,
shiftMode: Boolean = false
) {
val query = processor.query
val results = processor.results
textHighlighter.render(results, query, jumpMode)
if (!markImmediately && query.rawText.let { it.length < AceConfig.minQueryLength && it.all(Char::isLetterOrDigit) }) {
if (!markImmediately &&
query.rawText.let {
it.length < AceConfig.minQueryLength &&
it.all(Char::isLetterOrDigit)
}
) {
return
}
when (val result = tagger.markOrJump(query, results.clone())) {
is TaggingResult.Jump -> {
tagJumper.jump(result.offset, shiftMode)
tagCanvas.removeMarkers()
end()
tagJumper.jump(result.tag, shiftMode, isCrossEditor = mainEditor !== result.tag.editor)
tagCanvases.values.forEach(TagCanvas::removeMarkers)
end(result)
}
is TaggingResult.Mark -> {
val tags = result.tags
tagCanvas.setMarkers(tags)
val markers = result.markers
for ((editor, canvas) in tagCanvases) {
canvas.setMarkers(markers[editor].orEmpty())
}
if (jumpEditors.all { editor ->
val cache = EditorOffsetCache.new()
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
if (tags.none { boundaries.isOffsetInside(editor, it.offsetL, cache) || boundaries.isOffsetInside(editor, it.offsetR, cache) }) {
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()
}
}
}
}
/**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/
fun startRegexSearch(pattern: String, boundaries: Boundaries) {
tagger = Tagger(editor)
tagCanvas.setMarkers(emptyList())
@ExternalUsage
fun markResults(resultsToMark: SortedSet<Int>) {
val jumpEditor = jumpEditors.singleOrNull() ?: return
markResults(mapOf(jumpEditor to resultsToMark))
}
@ExternalUsage
fun markResults(resultsToMark: Map<Editor, Collection<Int>>) {
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(jumpEditors, "", defaultBoundaries)
.apply {
results.clear()
for ((editor, offsets) in resultsToMark) {
if (editor in jumpEditors) {
results[editor] = IntArrayList(offsets)
}
}
}
val processor = SearchProcessor.fromRegex(editor, pattern, boundaries.intersection(defaultBoundaries)).also { searchProcessor = it }
updateSearch(processor, markImmediately = true)
}
/**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
* Starts a regular expression search. If a search was already active,
* it will be reset alongside its tags and highlights.
*/
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) {
startRegexSearch(pattern.regex, boundaries)
@ExternalUsage
fun startRegexSearch(pattern: String, boundaries: Boundaries) {
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(
jumpEditors, pattern,
boundaries.intersection(defaultBoundaries)
).also { searchProcessor = it }
updateSearch(processor, markImmediately = true)
}
/**
* Starts a regular expression search. If a search was already active,
* it will be reset alongside its tags and highlights.
*/
@ExternalUsage
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) =
startRegexSearch(pattern.regex, boundaries)
/**
* See [JumpModeTracker.cycle].
*/
@@ -159,54 +222,85 @@ class Session(private val editor: Editor) {
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()
}
}
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()
}
}
fun visitNextTag() =
if (tagVisitor?.visitNext() == true) end() else Unit
/**
* See [TagVisitor.visitPrevious]. If there are no tags, nothing happens.
*/
fun scrollToNextScreenful() = tagScroller?.scroll(true)
/**
* See [TagVisitor.visitNext]. If there are no tags, nothing happens.
*/
fun scrollToPreviousScreenful() = tagScroller?.scroll(false)
/**
* Ends this session.
*/
fun end() {
SessionManager.end(editor)
}
fun end(taggingResult: TaggingResult? = null) =
SessionManager.end(mainEditor, taggingResult)
/**
* Clears any currently active search, tags, and highlights. Does not reset [JumpMode].
* Clears any currently active search, tags, and highlights.
* Does not reset [JumpMode].
*/
fun restart() {
tagger = Tagger(editor)
tagger = Tagger(jumpEditors)
searchProcessor = null
tagCanvas.removeMarkers()
tagCanvases.values.forEach(TagCanvas::removeMarkers)
textHighlighter.reset()
}
/**
* Should only be used from [SessionManager] to dispose a successfully ended session.
* Should only be used from [SessionManager] to dispose a
* successfully ended session.
*/
internal fun dispose() {
tagger = Tagger(editor)
EditorKeyListener.detach(editor)
tagCanvas.unbind()
internal fun dispose(taggingResult: TaggingResult?) {
tagger = Tagger(jumpEditors)
EditorKeyListener.detach(mainEditor)
tagCanvases.values.forEach(TagCanvas::unbind)
textHighlighter.reset()
EditorsCache.invalidate()
if (!editor.isDisposed) {
originalSettings.restore(editor)
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, JumpMode.DISABLED.caretColor)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
val jumpResult = taggingResult as? TaggingResult.Jump
val mark = jumpResult?.mark
val query = jumpResult?.query
listeners.forEach { it.finished(mark, query) }
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,41 +1,53 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.ExternalUsage
import org.acejump.search.TaggingResult
/**
* Manages active [Session]s in [Editor]s. There may only be one [Session] per [Editor], but multiple [Session]s across multiple [Editor]s
* may be active at once.
* Manages active [Session]s in [Editor]s. There may only be
* one [Session] per [Editor], but multiple [Session]s across
* multiple [Editor]s may be active at once.
*
* It is possible for an [Editor] to be disposed with an active [Session]. In such case, the reference to both will remain until a new
* [Session] starts, at which point the [SessionManager.cleanup] method will purge disposed [Editor]s.
* It is possible for an [Editor] to be disposed with an active
* [Session]. In such case, the reference to both will remain
* until a new [Session] starts, at which point the
* [SessionManager.cleanup] method will purge disposed [Editor]s.
*/
@ExternalUsage
object SessionManager {
private val sessions = HashMap<Editor, Session>(4)
/**
* Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
* Starts a new [Session], or returns an existing [Session]
* if the specified [Editor] already has one.
*/
fun start(editor: Editor): Session {
return sessions.getOrPut(editor) { cleanup(); Session(editor) }
fun start(editor: Editor): Session = start(editor, listOf(editor))
/**
* Starts a new multi-editor [Session], or returns an existing [Session] if the specified main [Editor] already has one.
* The [mainEditor] is used for typing the search query and tag.
* The [jumpEditors] are all editors that will be searched and tagged. The list is ordered so that editors earlier in the list will be
* prioritized for tagging in case of conflicts.
*/
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
}
/**
* Returns the active [Session] in the specified [Editor], or null if the [Editor] has no active session.
* Returns the active [Session] in the specified [Editor],
* or null if the [Editor] has no active session.
*/
operator fun get(editor: Editor): Session? {
return sessions[editor]
}
operator fun get(editor: Editor): Session? = sessions[editor]
/**
* Ends the active [Session] in the specified [Editor], or does nothing if the [Editor] has no active session.
* Ends the active [Session] in the specified [Editor],
* or does nothing if the [Editor] has no active session.
*/
fun end(editor: Editor) {
sessions.remove(editor)?.dispose()
}
fun end(editor: Editor, taggingResult: TaggingResult?) =
sessions.remove(editor)?.dispose(taggingResult) ?: Unit
private fun cleanup() {
for (disposedEditor in sessions.keys.filter { it.isDisposed }) {
sessions.remove(disposedEditor)?.dispose()
}
}
private fun cleanup() = sessions.keys.filter { it.isDisposed }
.forEach { disposedEditor -> sessions.remove(disposedEditor)?.dispose(null) }
}

View File

@@ -5,7 +5,8 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.read
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Rectangle
@@ -16,17 +17,16 @@ import javax.swing.SwingUtilities
/**
* Holds all active tag markers and renders them on top of the editor.
*/
internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener {
private var markers: List<Tag>? = null
internal class TagCanvas(private val editor: Editor): JComponent(), CaretListener {
private var markers: Collection<TagMarker>? = null
init {
val contentComponent = editor.contentComponent
contentComponent.add(this)
setBounds(0, 0, contentComponent.width, contentComponent.height)
if (ApplicationInfo.getInstance().build.components.first() < 173) {
SwingUtilities.convertPoint(this, location, editor.component.rootPane).let { setLocation(-it.x, -it.y) }
}
if (ApplicationInfo.getInstance().build.components.first() < 173)
SwingUtilities.convertPoint(this, location, editor.component.rootPane)
.let { setLocation(-it.x, -it.y) }
editor.caretModel.addCaretListener(this)
}
@@ -38,44 +38,45 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
}
/**
* Ensures that all tags and the outline around the selected tag are repainted. It should not be necessary to repaint the entire tag
* Ensures that all tags and the outline around the selected tag are
* repainted. It should not be necessary to repaint the entire tag
* canvas, but the cost of repainting visible tags is negligible.
*/
override fun caretPositionChanged(event: CaretEvent) {
repaint()
}
override fun caretPositionChanged(event: CaretEvent) = repaint()
fun setMarkers(markers: List<Tag>) {
fun setMarkers(markers: Collection<TagMarker>) {
this.markers = markers
repaint()
}
fun removeMarkers() {
this.markers = emptyList()
markers = emptyList()
}
override fun paint(g: Graphics) {
if (!markers.isNullOrEmpty()) {
super.paint(g)
}
}
override fun paint(g: Graphics) =
read { if (!markers.isNullOrEmpty()) super.paint(g) else Unit }
override fun paintChildren(g: Graphics) {
super.paintChildren(g)
val markers = markers ?: return
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)
// 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 font = TagFont(editor)
// TODO instead of immediately painting, we could calculate the layout of everything first, and then remove tags that interfere with
val cache = EditorOffsetCache.new()
val viewRange = VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val foldingModel = editor.foldingModel
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
@@ -83,9 +84,8 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
caretMarker?.paint(g, editor, cache, font, occupied)
for (marker in markers) {
if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
if (marker.isOffsetInRange(viewRange) && !foldingModel.isOffsetCollapsed(marker.offsetL) && marker !== caretMarker)
marker.paint(g, editor, cache, font, occupied)
}
}
}
}

View File

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

View File

@@ -2,9 +2,10 @@ package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil
import com.intellij.ui.JreHiDpiUtil
import com.intellij.ui.scale.JBUIScale
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.config.AceConfig
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
@@ -17,7 +18,7 @@ import kotlin.math.max
/**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/
internal class Tag(
class TagMarker(
private val tag: String,
val offsetL: Int,
val offsetR: Int,
@@ -33,17 +34,17 @@ internal class Tag(
* 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.
*/
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): Tag {
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker {
val chars = editor.immutableText
val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
val displayedTag = if (literalQueryText != null && literalQueryText.last().equals(tag.first(), ignoreCase = true))
tag.drop(1).toUpperCase()
tag.drop(1).uppercase()
else
tag.toUpperCase()
tag.uppercase()
return Tag(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
}
/**
@@ -51,7 +52,16 @@ internal class Tag(
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
// Workaround for misalignment on high DPI screens.
if (JreHiDpiUtil.isJreHiDPI(g)) {
g.translate(0.0, -0.5)
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
g.translate(0.0, 0.5)
}
else {
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
}
}
/**
@@ -71,15 +81,17 @@ internal class Tag(
g.color = AceConfig.tagForegroundColor
g.drawString(text, x, y)
}
private fun isLineEnding(char: Char): Boolean {
return char == '\n' || char == '\r'
}
}
/**
* Returns true if the left-aligned offset is in the range. Use to cull tags outside visible range.
* Only the left offset is checked, because if the tag was right-aligned on the last index of the range, it would not be visible anyway.
*/
fun isOffsetInRange(range: IntRange): Boolean {
return offsetL in range
}
fun isOffsetInRange(range: IntRange): Boolean = offsetL in range
/**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
@@ -96,29 +108,35 @@ internal class Tag(
}
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
val boundaries = VISIBLE_ON_SCREEN
if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) {
if (hasSpaceRight || offsetL !in 1 until editor.document.textLength || isLineEnding(editor.immutableText[offsetL - 1])) {
val rectR = createRightAlignedTagRect(editor, cache, font)
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects) }
}
val rectL = createLeftAlignedTagRect(editor, cache, font)
if (occupied.none(rectL::intersects)) {
if (occupied.none(rectL::intersects))
return rectL.takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
}
val rectR = createRightAlignedTagRect(editor, cache, font)
if (occupied.none(rectR::intersects)) {
if (occupied.none(rectR::intersects))
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
}
return null
}
private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetR)
val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR)
val char = if (offsetR >= editor.document.textLength)
' ' // Use the width of a space on the last line.
else editor.immutableText[offsetR].let {
// Use the width of a space on empty lines.
if (isLineEnding(it)) ' ' else it
}
val shift = font.editorFontMetrics.charWidth(char) + (font.tagCharWidth * shiftR)
return Rectangle(pos.x + shift, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
}

View File

@@ -1,40 +1,51 @@
package org.acejump.view
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.codeInsight.hint.*
import com.intellij.codeInsight.hint.HintManagerImpl.HIDE_BY_ESCAPE
import com.intellij.codeInsight.hint.HintManagerImpl.HIDE_BY_TEXT_CHANGE
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.util.ui.JBUI
import com.intellij.openapi.editor.markup.*
import com.intellij.openapi.editor.markup.HighlighterTargetArea.EXACT_RANGE
import com.intellij.ui.*
import com.intellij.ui.util.preferredHeight
import com.intellij.util.DocumentUtil
import com.intellij.util.ui.*
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.*
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.JumpMode
import org.acejump.isWordPart
import org.acejump.search.SearchQuery
import org.acejump.wordEnd
import org.acejump.wordStart
import java.awt.Graphics
import java.awt.*
import javax.swing.*
import kotlin.math.max
/**
* Renders highlights for search occurrences.
*/
internal class TextHighlighter(private val editor: Editor) {
private companion object {
private const val LAYER = HighlighterLayer.LAST + 1
}
private var previousHighlights: Array<RangeHighlighter>? = null
internal class TextHighlighter {
private companion object { private const val LAYER = HighlighterLayer.LAST + 1 }
private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>()
private var previousHint: LightweightHint? = null
/**
* Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change.
* Label for the search notification.
*/
fun render(offsets: IntList, query: SearchQuery, jumpMode: JumpMode) {
val markup = editor.markupModel
val chars = editor.immutableText
private class NotificationLabel(text: String?): JLabel(text) {
init {
background = HintUtil.getInformationColor()
foreground = JBColor.foreground()
this.isOpaque = true
}
}
/**
* Removes all current highlights and re-creates them from scratch.
* Must be called whenever any of the method parameters change.
*/
fun render(results: Map<Editor, IntList>, query: SearchQuery, jumpMode: JumpMode) {
val renderer = when {
query is SearchQuery.RegularExpression -> RegexRenderer
@@ -42,51 +53,117 @@ internal class TextHighlighter(private val editor: Editor) {
else -> SearchedWordRenderer
}
val modifications = (previousHighlights?.size ?: 0) + offsets.size
for ((editor, offsets) in results) {
val highlights = previousHighlights[editor]
val markup = editor.markupModel
val document = editor.document
val chars = editor.immutableText
val modifications = (highlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000
val document = editor.document
try {
if (enableBulkEditing) {
document.isInBulkUpdate = true
}
previousHighlights?.forEach(markup::removeHighlighter)
previousHighlights = Array(offsets.size) { index ->
DocumentUtil.executeInBulk(document, enableBulkEditing) {
highlights?.forEach(markup::removeHighlighter)
previousHighlights[editor] = Array(offsets.size) { index ->
val start = offsets.getInt(index)
val end = start + query.getHighlightLength(chars, start)
markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
customRenderer = renderer
}
}
} finally {
if (enableBulkEditing) {
document.isInBulkUpdate = false
markup.addRangeHighlighter(start, end, LAYER, null, EXACT_RANGE)
.apply { customRenderer = renderer }
}
}
}
if (AceConfig.showSearchNotification)
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() {
editor.markupModel.removeAllHighlighters()
previousHighlights = null
previousHighlights.forEach { (editor, highlighters) ->
highlighters.forEach(editor.markupModel::removeHighlighter)
}
previousHighlights.clear()
previousHint?.hide()
}
/**
* Renders a filled highlight in the background of a searched text occurrence.
*/
private object SearchedWordRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
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 + 1, end.x - start.x, editor.lineHeight - 1)
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)
@@ -94,46 +171,48 @@ internal class TextHighlighter(private val editor: Editor) {
}
/**
* 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].
* 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 {
private object SearchedWordWithOutlineRenderer: CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
SearchedWordRenderer.paint(editor, highlighter, g)
val chars = editor.immutableText
val startOffset = highlighter.startOffset
if (chars.getOrNull(startOffset)?.isWordPart == true) {
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)
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 position. Used for regex search queries.
* Renders a filled highlight in the background of the first highlighted
* position. Used for regex search queries.
*/
private object RegexRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
private object RegexRenderer: CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) =
drawSingle(g, editor, highlighter.startOffset)
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int) {
val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset)
val char = editor.immutableText.getOrNull(offset)?.takeUnless { it == '\n' || it == '\t' } ?: ' '
val char = editor.immutableText.getOrNull(offset)
?.takeUnless { it == '\n' || it == '\t' } ?: ' '
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = AceConfig.textHighlightColor
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.fillRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
g.color = AceConfig.tagBackgroundColor
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)

View File

@@ -20,23 +20,39 @@
id="preferences.AceConfigurable" dynamic="true"/>
<editorActionHandler action="EditorEscape" order="first"
implementationClass="org.acejump.action.AceEditorAction$Reset"/>
implementationClass="org.acejump.action.AceEditorAction$Reset"
id="AceHandlerEscape"/>
<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"/>
implementationClass="org.acejump.action.AceEditorAction$SelectBackward"
id="AceHandlerStartNewLine"/>
<editorActionHandler action="EditorEnter" order="first"
implementationClass="org.acejump.action.AceEditorAction$SelectForward"/>
implementationClass="org.acejump.action.AceEditorAction$SelectForward"
id="AceHandlerEnter"/>
<editorActionHandler action="EditorTab" order="first"
implementationClass="org.acejump.action.AceEditorAction$ScrollToNextScreenful"
id="AceHandlerTab"/>
<editorActionHandler action="EditorUnindentSelection" order="first"
implementationClass="org.acejump.action.AceEditorAction$ScrollToPreviousScreenful"
id="AceHandlerUnindentSelection"/>
<editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"
id="AceHandlerUp"/>
<editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"
id="AceHandlerLeft"/>
<editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"
id="AceHandlerLineStart"/>
<editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"
id="AceHandlerRight"/>
<editorActionHandler action="EditorLineEnd" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"
id="AceHandlerLineEnd"/>
</extensions>
<actions>
@@ -50,6 +66,12 @@
<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"/>

View File

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

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -1,6 +1,8 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
import com.intellij.openapi.editor.actions.EnterAction
import org.acejump.action.AceAction
import org.acejump.config.AceConfig
import org.acejump.test.util.BaseTest
/**
@@ -63,7 +65,7 @@ class AceTest : BaseTest() {
fun `test shift selection`() {
"<caret>testing 1234".search("4")
typeAndWaitForResults(session.tags[0].key.toUpperCase())
typeAndWaitForResults(session.tags[0].key.uppercase())
myFixture.checkResult("<selection>testing 123<caret></selection>4")
}
@@ -71,7 +73,7 @@ class AceTest : BaseTest() {
fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two")
takeAction(AceAction.StartAllWordsBackwardsMode)
takeAction(AceAction.StartAllWordsBackwardsMode())
assertEquals(2, session.tags.size)
}
@@ -79,7 +81,7 @@ class AceTest : BaseTest() {
fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four")
takeAction(AceAction.StartAllWordsForwardMode)
takeAction(AceAction.StartAllWordsForwardMode())
assertEquals(4, session.tags.size)
}
@@ -87,7 +89,7 @@ class AceTest : BaseTest() {
fun `test word mode`() {
makeEditor("test word action")
takeAction(AceAction.StartAllWordsMode)
takeAction(AceAction.StartAllWordsMode())
assertEquals(3, session.tags.size)
@@ -99,17 +101,65 @@ class AceTest : BaseTest() {
fun `test target mode`() {
"<caret>test target action".search("target")
takeAction(AceAction.ToggleTargetMode)
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("test <selection>target<caret></selection> action")
}
fun `test cache invalidation`() {
"first line".search("first")
typeAndWaitForResults(session.tags[0].key)
repeat(3) { takeAction(EnterAction()) }
takeAction(AceAction.ToggleTargetMode())
typeAndWaitForResults("first")
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("\n\n\n<selection>first<caret></selection> line")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines\n")
makeEditor(" test\n three\n lines")
takeAction(AceAction.StartAllLineMarksMode)
takeAction(AceAction.StartAllLineMarksMode())
assertEquals(8, session.tags.size) // last empty line does not count
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

@@ -0,0 +1,188 @@
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

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

View File

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