mirror of
https://github.com/chylex/IntelliJ-AceJump.git
synced 2025-09-15 22:32:11 +02:00
Compare commits
35 Commits
master
...
100001ffca
Author | SHA1 | Date | |
---|---|---|---|
100001ffca
|
|||
184896a6cb
|
|||
8563400946
|
|||
a07c61a384
|
|||
e072003c5c
|
|||
8062cf5cab
|
|||
9151ee376c
|
|||
19ce1c69fd
|
|||
f2a053505c
|
|||
647cfb14f1
|
|||
c31ba60909
|
|||
9c60a8a4ba
|
|||
1e8b7d7963
|
|||
9157ce19b0
|
|||
9a435feccc
|
|||
c1feb891e4
|
|||
0b6ba62cda
|
|||
dfd5b122a0
|
|||
f2400d134d
|
|||
98997b4d86
|
|||
1e172e9c49
|
|||
01b37c878e | |||
1630c706a9 | |||
041a130c5f | |||
7561bfde36 | |||
724e469f21 | |||
bce9a5f636 | |||
bfe0aa536e | |||
2ca21b8423 | |||
430bcf6883 | |||
092650af81 | |||
2009123114 | |||
de19c0e0b8 | |||
5021a07fb9 | |||
1180547b06 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: breandan
|
24
.gitignore
vendored
24
.gitignore
vendored
@@ -1,21 +1,5 @@
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
## Directory-based project format:
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
## Plugin-specific files:
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
### Gradle template
|
||||
.gradle
|
||||
build/
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
gradle.properties
|
||||
|
||||
# Mac OS
|
||||
.DS_Store
|
||||
/.gradle/
|
||||
/build/
|
||||
|
237
CHANGES.md
237
CHANGES.md
@@ -1,94 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
### 3.7
|
||||
|
||||
- Add buttons to reset colors to default values in Settings, [#411](https://github.com/acejump/AceJump/issues/411)
|
||||
|
||||
## 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
|
||||
@@ -98,225 +11,223 @@
|
||||
- Jump-to-End mode jumps to the end of a word
|
||||
- Fixes toggle keys not resetting mode when pressed twice
|
||||
- Increase limit for what is considered a large file
|
||||
- Major refactoring, [#350](https://github.com/acejump/AceJump/pull/353)
|
||||
- [Many bug fixes](https://github.com/acejump/AceJump/issues/348#issuecomment-739454920): [#338](https://github.com/acejump/AceJump/issues/338), [#336](https://github.com/acejump/AceJump/issues/336), [#329](https://github.com/acejump/AceJump/issues/329), [#327](https://github.com/acejump/AceJump/issues/327), [#310](https://github.com/acejump/AceJump/issues/310), [#233](https://github.com/acejump/AceJump/issues/233), [#228](https://github.com/acejump/AceJump/issues/228), [#187](https://github.com/acejump/AceJump/issues/187), [#147](https://github.com/acejump/AceJump/issues/147), [#132](https://github.com/acejump/AceJump/issues/132), [#71](https://github.com/acejump/AceJump/issues/71)
|
||||
- Huge thanks to [@chylex](https://github.com/chylex) for [all the PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex)!
|
||||
- Thanks to @chylex for [all the PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex)!
|
||||
|
||||
## 3.6.3
|
||||
### 3.6.3
|
||||
|
||||
- Vote for your favorite <a href="https://twitter.com/breandan/status/1274169810411274241">AceJump logo</a>!
|
||||
- Fixes potential bug.
|
||||
- Increases test coverage.
|
||||
|
||||
## 3.6.2
|
||||
### 3.6.2
|
||||
|
||||
- Fixes [#226](https://github.com/acejump/AceJump/issues/226). Thanks [@AlexPl292](https://github.com/AlexPl292)!
|
||||
- Fixes [#226](https://github.com/acejump/AceJump/issues/226). Thanks @AlexPl292!
|
||||
- Update Pinyin engine.
|
||||
|
||||
## 3.6.1
|
||||
### 3.6.1
|
||||
|
||||
- Fixes [#324](https://github.com/acejump/AceJump/issues/324). Thanks [@AlexPl292](https://github.com/AlexPl292)!
|
||||
- Fixes [#324](https://github.com/acejump/AceJump/issues/324). Thanks @AlexPl292!
|
||||
- Fixes [#325](https://github.com/acejump/AceJump/issues/325).
|
||||
- Fixes Pinyin support.
|
||||
|
||||
## 3.6.0
|
||||
### 3.6.0
|
||||
|
||||
- Adds support for Chinese [#314](https://github.com/acejump/AceJump/issues/314).
|
||||
- Fixes constantly loading settings page [#303](https://github.com/acejump/AceJump/issues/303).
|
||||
- Honor camel humps [#315](https://github.com/acejump/AceJump/issues/315). Thanks to [@clojj](https://github.com/clojj).
|
||||
- Honor camel humps [#315](https://github.com/acejump/AceJump/issues/315). Thanks to @clojj.
|
||||
- Support dynamic application reloading [#322](https://github.com/acejump/AceJump/issues/322).
|
||||
|
||||
## 3.5.9
|
||||
### 3.5.9
|
||||
|
||||
- Fix a build configuration error affecting plugins which depend on AceJump. Fixes [#305](https://github.com/acejump/AceJump/issues/305).
|
||||
|
||||
## 3.5.8
|
||||
### 3.5.8
|
||||
|
||||
- Tagging improvements
|
||||
- Support for external plugin integration
|
||||
- Fixes [#304](https://github.com/acejump/AceJump/issues/304), [#255](https://github.com/acejump/AceJump/issues/255)
|
||||
|
||||
## 3.5.7
|
||||
### 3.5.7
|
||||
|
||||
- <kbd>Tab</kbd>/<kbd>Enter</kbd> will now scroll horizontally if results are not visible.
|
||||
- Fixes [#294](https://github.com/acejump/AceJump/issues/294) "Access is allowed from event dispatch thread only" error
|
||||
|
||||
## 3.5.6
|
||||
### 3.5.6
|
||||
|
||||
- Key prioritization for most common keyboard layouts and fixes for a number of minor issues.
|
||||
- Fixes: Index OOB [#242](https://github.com/acejump/AceJump/issues/242), Missing editor [#249](https://github.com/acejump/AceJump/issues/249), [#275](https://github.com/acejump/AceJump/issues/275), Forgotten block caret [#278](https://github.com/acejump/AceJump/issues/278), QWERTZ layout [#273](https://github.com/acejump/AceJump/issues/273)
|
||||
|
||||
## 3.5.5
|
||||
### 3.5.5
|
||||
|
||||
- <kbd>Enter</kbd> will now escape exit from AceJump when there is a single visible tag. [#274](https://github.com/acejump/AceJump/issues/274)
|
||||
- <kbd>Shift</kbd>+<kbd>Tab</kbd> to scroll to previous occurrences now works properly. [#179](https://github.com/acejump/AceJump/issues/179)
|
||||
- Fixes an error with sticky block caret mode. [#269](https://github.com/acejump/AceJump/issues/269)
|
||||
|
||||
## 3.5.4
|
||||
### 3.5.4
|
||||
|
||||
- Introduces cyclical selection: press Enter or Shift + Enter to cycle through tags on the screen. Press Escape to return to the editor.
|
||||
|
||||
## 3.5.3
|
||||
### 3.5.3
|
||||
|
||||
- Fixes for two regressions affecting caret color and shift-selection.
|
||||
|
||||
## 3.5.2
|
||||
### 3.5.2
|
||||
|
||||
- Various improvements to settings page, including a keyboard layout selector.
|
||||
- Shorter tags on average, AceJump tries to use a single-character tag more often.
|
||||
- Tag characters are now prioritized by user-defined order from the settings page.
|
||||
- Fixes an issue when running the plugin on platform version 2018.3 and above.
|
||||
|
||||
## 3.5.1
|
||||
### 3.5.1
|
||||
|
||||
- Now supports searching for CaPiTaLiZeD letters (typing capital letters in the query will force a case-sensitive search).
|
||||
- **Declaration Mode**: Press the AceJump shortcut a second time to activate Declaration Mode, which will jump to the declaration of a variable in the editor.
|
||||
- Keep hitting the AceJump shortcut to cycle between modes (default, declaration, target, disabled).
|
||||
- Bug fix: AceJump settings should now properly persist after restarting the IDE.
|
||||
|
||||
## 3.5.0
|
||||
### 3.5.0
|
||||
|
||||
- Adds two new features. "**Word-Mode**" and quick tag selection.
|
||||
- **Word Mode** removes search and addresses latency issues raised in [#161](https://github.com/acejump/AceJump/issues/161). To learn more about **Word Mode**, see the [readme](https://github.com/johnlindquist/AceJump#tips).
|
||||
- Pressing <kbd>Enter</kbd> during a search will jump to the next visible match (or closest match, if next is not visible), as per [#133](https://github.com/acejump/AceJump/issues/133).
|
||||
|
||||
## 3.4.3
|
||||
### 3.4.3
|
||||
|
||||
- Stability improvements and tagging optimizations. Fixes [#206](https://github.com/acejump/AceJump/issues/206), [#202](https://github.com/acejump/AceJump/issues/202).
|
||||
|
||||
## 3.4.2
|
||||
### 3.4.2
|
||||
|
||||
- Fixes [a regression](https://github.com/johnlindquist/AceJump/issues/197) affecting older platform versions.
|
||||
|
||||
## 3.4.1
|
||||
### 3.4.1
|
||||
|
||||
- Fixes a regression affecting tag alignment when line spacing is greater than 1.0. Minor speed improvements.
|
||||
|
||||
## 3.4.0
|
||||
### 3.4.0
|
||||
|
||||
- Restores original scroll position if tab search cancelled. Minor improvements to latency and tag painting.
|
||||
|
||||
## 3.3.6
|
||||
### 3.3.6
|
||||
|
||||
- Fix for [#129](https://github.com/acejump/AceJump/issues/129).
|
||||
|
||||
## 3.3.5
|
||||
### 3.3.5
|
||||
|
||||
- Minor bugfix release. Improve handling of window resizing.
|
||||
|
||||
## 3.3.4
|
||||
### 3.3.4
|
||||
|
||||
- Add a settings page. (Settings > Tools > AceJump)
|
||||
|
||||
## 3.3.3
|
||||
### 3.3.3
|
||||
|
||||
- Improve latency and fix a bug in line selection mode.
|
||||
|
||||
## 3.3.2
|
||||
### 3.3.2
|
||||
|
||||
- AceJump now persists target mode state when scrolling or tabbing.
|
||||
|
||||
## 3.3.1
|
||||
### 3.3.1
|
||||
|
||||
- Fixes a minor regression where tags are not displaying correctly.
|
||||
|
||||
## 3.3.0
|
||||
### 3.3.0
|
||||
|
||||
- AceJump now searches the entire document. Press TAB to get the next set of results!
|
||||
|
||||
## 3.2.7
|
||||
### 3.2.7
|
||||
|
||||
- Minor fixes and stability improvements.
|
||||
|
||||
## 3.2.6
|
||||
### 3.2.6
|
||||
|
||||
- Fixes an error affecting older versions of the IntelliJ Platform.
|
||||
|
||||
## 3.2.5
|
||||
### 3.2.5
|
||||
|
||||
- AceJump 3 now supports older IntelliJ Platform and Kotlin versions.
|
||||
|
||||
## 3.2.4
|
||||
### 3.2.4
|
||||
|
||||
- Tagging improvements (tags now shorter on average) and visual updates.
|
||||
|
||||
## 3.2.3
|
||||
### 3.2.3
|
||||
|
||||
- Fixes a critical issue affecting users with multiple editor windows open.
|
||||
|
||||
## 3.2.2
|
||||
### 3.2.2
|
||||
|
||||
- Adds scrolling support and fixes some line spacing issues.
|
||||
|
||||
## 3.2.1
|
||||
### 3.2.1
|
||||
|
||||
- AceJump now synchronizes font style changes in real-time.
|
||||
|
||||
## 3.2.0
|
||||
### 3.2.0
|
||||
|
||||
- Support Back/Forward navigation in the IntelliJ Platform.
|
||||
|
||||
## 3.1.8
|
||||
### 3.1.8
|
||||
|
||||
- Fixes some errors that occur when the user closes an editor prematurely.
|
||||
|
||||
## 3.1.6
|
||||
### 3.1.6
|
||||
|
||||
- Fixes a rare tag collision scenario and UninitializedPropertyAccess exception
|
||||
|
||||
## 3.1.5
|
||||
### 3.1.5
|
||||
|
||||
- Allow users to enter target mode directly by pressing Ctrl+Alt+;
|
||||
|
||||
## 3.1.4
|
||||
### 3.1.4
|
||||
|
||||
- Fixes the "Assertion Failed" exception popup
|
||||
|
||||
## 3.1.3
|
||||
### 3.1.3
|
||||
|
||||
- Fixes an error affecting some users during startup.
|
||||
|
||||
## 3.1.2
|
||||
### 3.1.2
|
||||
|
||||
- Fixes an Android Studio regression.
|
||||
|
||||
## 3.1.1
|
||||
### 3.1.1
|
||||
|
||||
- Hotfix for broken target mode.
|
||||
|
||||
## 3.1.0
|
||||
### 3.1.0
|
||||
|
||||
- Removes the search box, lots of small usability improvements.
|
||||
|
||||
## 3.0.7
|
||||
### 3.0.7
|
||||
|
||||
- No longer tags "folded" regions and minor alignment adjustments.
|
||||
|
||||
## 3.0.6
|
||||
### 3.0.6
|
||||
|
||||
- Fixes alignment issues, removes top and bottom alignments until there is a better way to visually differentiate adjacent tags.
|
||||
|
||||
## 3.0.5
|
||||
### 3.0.5
|
||||
|
||||
- Hotfix for target mode.
|
||||
|
||||
## 3.0.4
|
||||
### 3.0.4
|
||||
|
||||
- Adds *Line Mode* - press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>;</kbd> to activate.
|
||||
|
||||
## 3.0.3
|
||||
### 3.0.3
|
||||
|
||||
- Updates to tag placement and performance improvements.
|
||||
|
||||
## 3.0.2
|
||||
### 3.0.2
|
||||
|
||||
- Fixes target mode and default shortcut activation for Mac users.
|
||||
|
||||
## 3.0.1
|
||||
### 3.0.1
|
||||
|
||||
- Fixes target-mode issues affecting users with non-default shortcuts and adds support for Home/End.
|
||||
|
||||
## 3.0.0
|
||||
### 3.0.0
|
||||
|
||||
- Major rewrite of AceJump. Introducing:
|
||||
* Realtime search: Just type the word where you want to jump and AceJump will do the rest.
|
||||
@@ -324,82 +235,82 @@
|
||||
* Keyboard-aware tagging: Tries to minimize finger travel distance on QWERTY keyboards.
|
||||
* Colorful highlighting: AceJump will now highlight the editor text, as you type.
|
||||
|
||||
## 2.0.13
|
||||
### 2.0.13
|
||||
|
||||
- Fix a regression affecting *Target Mode* and line-based navigation.
|
||||
|
||||
## 2.0.12
|
||||
### 2.0.12
|
||||
|
||||
- Fix ClassCastException when input letter not present: [#73](https://github.com/acejump/AceJump/issues/73)
|
||||
|
||||
## 2.0.11
|
||||
### 2.0.11
|
||||
|
||||
- One hundred percent all natural Kotlin.
|
||||
|
||||
## 2.0.10
|
||||
### 2.0.10
|
||||
|
||||
- Support 2016.2, remove upper version limit, update internal Kotlin version
|
||||
|
||||
## 2.0.9
|
||||
### 2.0.9
|
||||
|
||||
- Compile on Java 7 to address: [#61](https://github.com/acejump/AceJump/issues/61)
|
||||
|
||||
## 2.0.8
|
||||
### 2.0.8
|
||||
|
||||
- Compile on Java 6 to address: [#59](https://github.com/acejump/AceJump/issues/59)
|
||||
|
||||
## 2.0.7
|
||||
### 2.0.7
|
||||
|
||||
- Language update for Kotlin 1.0 release.
|
||||
|
||||
## 2.0.6
|
||||
### 2.0.6
|
||||
|
||||
- Fixing "lost focus" bugs mentioned here: [#41](https://github.com/acejump/AceJump/issues/41)
|
||||
|
||||
## 2.0.5
|
||||
### 2.0.5
|
||||
|
||||
- Fixing "backspace" bugs mentioned here: [#20](https://github.com/acejump/AceJump/issues/20)
|
||||
|
||||
## 2.0.4
|
||||
### 2.0.4
|
||||
|
||||
- Fixing "code folding" bugs mentioned here: [#24](https://github.com/acejump/AceJump/issues/24)
|
||||
|
||||
## 2.0.3
|
||||
### 2.0.3
|
||||
|
||||
- More work on Ubuntu focus bug
|
||||
|
||||
## 2.0.2
|
||||
### 2.0.2
|
||||
|
||||
- Fixed bug when there's only 1 search result
|
||||
|
||||
## 2.0.1
|
||||
### 2.0.1
|
||||
|
||||
- Fixing Ubuntu focus bug
|
||||
|
||||
## 2.0.0
|
||||
### 2.0.0
|
||||
|
||||
- Major release: Added "target mode", many speed increases, multi-char search implemented
|
||||
|
||||
## 1.1.0
|
||||
### 1.1.0
|
||||
|
||||
- Switching to Kotlin for the code base
|
||||
|
||||
## 1.0.4
|
||||
### 1.0.4
|
||||
|
||||
- Fixing [#9](https://github.com/acejump/AceJump/issues/9) and [#6](https://github.com/acejump/AceJump/issues/6)
|
||||
|
||||
## 1.0.3
|
||||
### 1.0.3
|
||||
|
||||
- Fixed minor visual lag when removing the "jumpers" from the editor
|
||||
|
||||
## 1.0.2
|
||||
### 1.0.2
|
||||
|
||||
- Cleaning up minor bugs (npe when editor not in focus, not removing layers)
|
||||
|
||||
## 1.0.1
|
||||
### 1.0.1
|
||||
|
||||
- Adding a new jump: "Enter" will take you to the first non-whitespace char in a new line (compare to "Home" which takes you to a new line)
|
||||
|
||||
## 1.0.0
|
||||
### 1.0.0
|
||||
|
||||
- Cleaned up code base for release
|
||||
|
103
README.md
103
README.md
@@ -1,20 +1,13 @@
|
||||
<p align="center"><a href="https://plugins.jetbrains.com/plugin/7086"> <img src="logo.png" alt="AceJumpLogo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub" title="JetBrains on GitHub"><img src="https://jb.gg/badges/team.svg"></a>
|
||||
<a href="https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub" title="JetBrains on GitHub"><img src="http://jb.gg/badges/team.svg"></a>
|
||||
<a href="https://teamcity.jetbrains.com/viewType.html?buildTypeId=acejump_buildplugin&guest=1" title="Build Plugin"><img src="https://teamcity.jetbrains.com/app/rest/builds/buildType:acejump_buildplugin/statusIcon.svg"></a>
|
||||
<a href="https://plugins.jetbrains.com/plugin/7086-acejump" title="Jetbrains Plugin"><img src="https://img.shields.io/jetbrains/plugin/v/7086-acejump.svg"></a>
|
||||
<a href="LICENSE" title="License"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://twitter.com/search?q=AceJump&f=live" title="Twitter"><img src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"></a>
|
||||
</p>
|
||||
|
||||
> **Note**: There is currently an outstanding issue with [settings deserialization](https://github.com/acejump/AceJump/issues/445). If the AceJump settings were changed in the past, they can become corrupted and may need to be manually deleted. The location varies depending on the operating system and platform version used. For IntelliJ IDEA, the plugin settings file is located in either `options` or `plugins` in the following directories:
|
||||
> * Mac: `~/Library/Application Support/JetBrains/IntelliJIdea<VERSION>`
|
||||
> * Windows: `%APPDATA%\JetBrains\IntelliJIdea<VERSION>`
|
||||
> * Linux: `~/.local/share/JetBrains/IntelliJIdea<VERSION>`
|
||||
>
|
||||
> If not found, you can locate this file via the command, `find . | grep AceJump.xml` under the `JetBrains` directory. We apologize for any inconvenience this may have caused.
|
||||
|
||||
[AceJump](https://plugins.jetbrains.com/plugin/7086) is a plugin for the [IntelliJ Platform](https://github.com/JetBrains/intellij-community/) that lets you jump to any symbol in the editor with just a few keystrokes. Press the keyboard shortcut for `AceAction` (<kbd>Ctrl</kbd>+<kbd>;</kbd> by default) to activate AceJump. Type any string in the editor, followed by one of the illustrated tags, to jump its position:
|
||||
|
||||

|
||||
@@ -61,6 +54,8 @@ 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:
|
||||
@@ -69,25 +64,15 @@ AceJump can be [installed directly from the IDE](https://www.jetbrains.com/help/
|
||||
echo -e '
|
||||
|
||||
" Press `f` to activate AceJump
|
||||
map f <Action>(AceAction)
|
||||
map f :action AceAction<CR>
|
||||
" Press `F` to activate Target Mode
|
||||
map F <Action>(AceTargetAction)
|
||||
map F :action AceTargetAction<CR>
|
||||
" Press `g` to activate Line Mode
|
||||
map g <Action>(AceLineAction)
|
||||
map g :action AceLineAction<CR>
|
||||
|
||||
' >> ~/.ideavimrc
|
||||
```
|
||||
|
||||
To customize AceJump's behavior further with additional actions, see the `<action>` tags in [plugin.xml](src/main/resources/META-INF/plugin.xml). The following example shows how to activate AceJump before or after the caret.
|
||||
|
||||
```
|
||||
" Press `S` in normal mode to activate AceJump mode before the caret
|
||||
nmap S <Action>(AceBackwardAction)
|
||||
|
||||
" Press `s` in normal mode to activate AceJump mode after the caret
|
||||
nmap s <Action>(AceForwardAction)
|
||||
```
|
||||
|
||||
To change the default keyboard shortcuts, open **File \| Settings \| Keymap \| 🔍 "AceJump" \| AceJump \|** <kbd>Enter⏎</kbd>.
|
||||
|
||||

|
||||
@@ -106,56 +91,21 @@ The build artifact will be placed in `build/distributions/`.
|
||||
|
||||
*Miscellaneous: AceJump is built using [Gradle](https://gradle.com/) with the [Gradle Kotlin DSL](https://docs.gradle.org/5.1/userguide/kotlin_dsl.html) and the [gradle-intellij-plugin](https://github.com/JetBrains/gradle-intellij-plugin).*
|
||||
|
||||
## Extending
|
||||
|
||||
AceJump can be used by other [IntelliJ Platform](https://plugins.jetbrains.com/docs/intellij/welcome.html) plugins. To do so, add the following snippet to your `build.gradle.kts` file:
|
||||
|
||||
```kotlin
|
||||
intellij {
|
||||
plugins.set("AceJump:<LATEST_VERSION>")
|
||||
}
|
||||
```
|
||||
|
||||
Callers who pass an instance of [`Editor`](https://github.com/JetBrains/intellij-community/blob/master/platform/editor-ui-api/src/com/intellij/openapi/editor/Editor.java) into `SessionManager.start(editor)` will receive a [`Session`](src/main/kotlin/org/acejump/session/Session.kt) instance in return. Sessions are disposed after use.
|
||||
|
||||
To use AceJump externally, please see the following example:
|
||||
|
||||
```kotlin
|
||||
import org.acejump.session.SessionManager
|
||||
import org.acejump.session.AceJumpListener
|
||||
import org.acejump.boundaries.StandardBoundaries.*
|
||||
import org.acejump.search.Pattern.*
|
||||
|
||||
val aceJumpSession = SessionManager.start(editorInstance)
|
||||
|
||||
aceJumpSession.addAceJumpListener(object: AceJumpListener {
|
||||
override fun finished() {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
|
||||
// Sessions provide these endpoints for external consumers:
|
||||
|
||||
/*1.*/ aceJumpSession.markResults(sortedSetOf(/*...*/)) // Pass a set of offsets
|
||||
/*2.*/ aceJumpSession.startRegexSearch("[aeiou]+", WHOLE_FILE) // Search for regex
|
||||
/*3.*/ aceJumpSession.startRegexSearch(ALL_WORDS, VISIBLE_ON_SCREEN) // Search for Pattern
|
||||
```
|
||||
|
||||
Custom boundaries for search (i.e. current line before caret etc.) can also be defined using the [Boundaries](src/main/kotlin/org/acejump/boundaries/Boundaries.kt) interface.
|
||||
|
||||
## Contributing
|
||||
|
||||
AceJump is supported by community members like you. Contributions are highly welcome!
|
||||
|
||||
If you would like to [contribute](https://github.com/acejump/AceJump/pulls?q=is%3Apr), here are a few of the ways you can help improve AceJump:
|
||||
If you would like to [contribute](https://github.com/acejump/AceJump/pulls), here are a few of the ways you can help improve AceJump:
|
||||
|
||||
* [Improve test coverage](https://github.com/acejump/AceJump/issues/139)
|
||||
* [Add action to repeat last search](https://github.com/acejump/AceJump/issues/316)
|
||||
* [Add configurable RegEx modes](https://github.com/acejump/AceJump/issues/215)
|
||||
* [Add font family and size options](https://github.com/acejump/AceJump/issues/192)
|
||||
* [Tag placement and visibility improvements](https://github.com/acejump/AceJump/issues/323)
|
||||
* [Add option to place the caret after the search text](https://github.com/acejump/AceJump/issues/225)
|
||||
* [Support user-configurable keyboard layouts](https://github.com/acejump/AceJump/issues/172)
|
||||
* [Speed up tagging on large files](https://github.com/acejump/AceJump/issues/217)
|
||||
* [Animated documentation](https://github.com/acejump/AceJump/issues/145)
|
||||
* [Display current search text](https://github.com/acejump/AceJump/issues/227)
|
||||
* [Support for full screen tagging](https://github.com/acejump/AceJump/issues/144)
|
||||
* [Fold text between matches](https://github.com/acejump/AceJump/issues/255)
|
||||
* [Multi-platform support](https://github.com/acejump/AceJump/issues/229)
|
||||
|
||||
To start [IntelliJ IDEA CE](https://github.com/JetBrains/intellij-community) with AceJump installed, run `./gradlew runIde -PluginDev [-x test]`.
|
||||
|
||||
@@ -178,7 +128,7 @@ AceJump is inspired by prior work, but adds several improvements, including:
|
||||
* **Line Mode**: Jump to the first, last, or first non-whitespace character of any line on-screen (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>;</kbd>).
|
||||
* **Word Mode**: Jump to the first character of any visible word on-screen in two keystrokes or less.
|
||||
* **Declaration Mode**: Jump to the declaration of a token (if it is available) rather than the token itself.
|
||||
* **Unicode support**: Unicode search and selection, e.g. to search for "拼音", activate AceJump and type: <kbd>p</kbd><kbd>y</kbd>
|
||||
* **Pinyin support**: Pinyin search and selection, e.g. to search for "拼音", activate AceJump and type: <kbd>p</kbd><kbd>y</kbd>
|
||||
|
||||
The following plugins have a similar UI for navigating text and web browsing:
|
||||
|
||||
@@ -186,37 +136,30 @@ The following plugins have a similar UI for navigating text and web browsing:
|
||||
|:----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|:-------------------:|:------------------------------------------------------------------------:|
|
||||
| AceJump | [⬇](https://plugins.jetbrains.com/plugin/7086-acejump) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
|
||||
| [IdeaVim-EasyMotion](https://github.com/AlexPl292/IdeaVim-EasyMotion) | [⬇](https://github.com/AlexPl292/IdeaVim-EasyMotion) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
|
||||
| [KJump](https://github.com/a690700752/KJump) | [⬇](https://plugins.jetbrains.com/plugin/10149-kjump) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
|
||||
| [AceJump-Lite](https://github.com/EeeMt/AceJump-Lite) | [⬇](https://plugins.jetbrains.com/plugin/9803-acejump-lite) | [IntelliJ Platform](https://jetbrains.com) | :x: | [Java](https://www.java.com) |
|
||||
| [emacsIDEAs](https://github.com/whunmr/emacsIDEAs) | [⬇](https://plugins.jetbrains.com/plugin/7163-emacsideas) | [IntelliJ Platform](https://jetbrains.com) | :x: | [Java](https://www.java.com) |
|
||||
| [AceJump-Lite](https://github.com/EeeMt/AceJump-Lite) | [⬇](https://plugins.jetbrains.com/plugin/9803-acejump-lite) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Java](https://www.java.com) |
|
||||
| [KJump](https://github.com/a690700752/KJump) | [⬇](https://plugins.jetbrains.com/plugin/10149-kjump) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Java](https://www.java.com) |
|
||||
| [emacsIDEAs](https://github.com/whunmr/emacsIDEAs) | [⬇](https://plugins.jetbrains.com/plugin/7163-emacsideas) | [IntelliJ Platform](https://jetbrains.com) | :heavy_check_mark: | [Java](https://www.java.com) |
|
||||
| [TraceJump](https://github.com/acejump/tracejump) | [⬇](https://github.com/acejump/tracejump) | Desktop | :heavy_check_mark: | [Kotlin](http://kotlinlang.org/) |
|
||||
| [ace-jump-mode](https://github.com/winterTTr/ace-jump-mode) | [⬇](https://melpa.org/#/ace-jump-mode) | [emacs](https://www.gnu.org/software/emacs/) | :x: | [Emacs Lisp](https://www.gnu.org/software/emacs/manual/eintr.html) |
|
||||
| [avy](https://github.com/abo-abo/avy) | [⬇](https://melpa.org/#/avy) | [emacs](https://www.gnu.org/software/emacs/) | :heavy_check_mark: | [Emacs Lisp](https://www.gnu.org/software/emacs/manual/eintr.html) |
|
||||
| [EasyMotion](https://github.com/easymotion/vim-easymotion) | [⬇](https://vimawesome.com/plugin/easymotion) | [Vim](http://www.vim.org/) | :x: | [Vimscript](http://learnvimscriptthehardway.stevelosh.com/) |
|
||||
| [Hop](https://github.com/phaazon/hop.nvim) | [⬇](https://github.com/phaazon/hop.nvim#installation) | [NeoVim](https://neovim.io/) | :heavy_check_mark: | [Lua](https://www.lua.org/) |
|
||||
| [leap.nvim](https://github.com/ggandor/leap.nvim) | [⬇](https://github.com/ggandor/leap.nvim#installation) | [NeoVim](https://neovim.io/) | :heavy_check_mark: | [Fennel](https://fennel-lang.org) |
|
||||
| [lightspeed.nvim](https://github.com/ggandor/lightspeed.nvim) | [⬇](https://github.com/ggandor/lightspeed.nvim#installation) | [NeoVim](https://neovim.io/) | :x: | [Fennel](https://fennel-lang.org) |
|
||||
| [Sublime EasyMotion](https://github.com/tednaleid/sublime-EasyMotion) | [⬇](https://packagecontrol.io/packages/EasyMotion) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.org/) |
|
||||
| [AceJump](https://github.com/ice9js/ace-jump-sublime) | [⬇](https://packagecontrol.io/packages/AceJump) | [Sublime](https://www.sublimetext.com/) | :x: | [Python](https://www.python.org/) |
|
||||
| [Jumpy](https://github.com/DavidLGoldberg/jumpy) | [⬇](https://atom.io/packages/jumpy) | [Atom](https://atom.io/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [Jumpy2](https://github.com/DavidLGoldberg/jumpy2) | [⬇](https://marketplace.visualstudio.com/items?itemName=DavidLGoldberg.jumpy2) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [AceJump](https://github.com/ice9js/ace-jump-sublime) | [⬇](https://packagecontrol.io/packages/AceJump) | [Sublime](https://www.sublimetext.com/) | :heavy_check_mark: | [Python](https://www.python.org/) |
|
||||
| [Jumpy](https://github.com/DavidLGoldberg/jumpy) | [⬇](https://atom.io/packages/jumpy) | [Atom](https://atom.io/) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.org/) |
|
||||
| [Find-Jump](https://github.com/msafi/xvsc/tree/master/findJump) | [⬇](https://marketplace.visualstudio.com/items?itemName=mksafi.find-jump) | [Visual Studio Code](https://code.visualstudio.com/) | :x: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [MetaGo](https://github.com/metaseed/metaGo) | [⬇](https://marketplace.visualstudio.com/items?itemName=metaseed.metago) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [VSCodeVim](https://github.com/VSCodeVim/Vim) | [⬇](https://marketplace.visualstudio.com/items?itemName=vscodevim.vim) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [CodeAceJumper](https://github.com/lucax88x/CodeAceJumper) | [⬇](https://marketplace.visualstudio.com/items?itemName=lucax88x.codeacejumper) | [Visual Studio Code](https://code.visualstudio.com/) | :x: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [CodeAceJumper](https://github.com/lucax88x/CodeAceJumper) | [⬇](https://marketplace.visualstudio.com/items?itemName=lucax88x.codeacejumper) | [Visual Studio Code](https://code.visualstudio.com/) | :heavy_check_mark: | [TypeScript](https://www.typescriptlang.org/) |
|
||||
| [AceJump](https://github.com/jsturtevant/ace-jump) | [⬇](https://marketplace.visualstudio.com/items?itemName=jsturtevant.AceJump) | [Visual Studio](https://www.visualstudio.com/) | :x: | [C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/) |
|
||||
| [EasyMotion](https://github.com/jaredpar/EasyMotion) | [⬇](https://marketplace.visualstudio.com/items?itemName=JaredParMSFT.EasyMotion) | [Visual Studio](https://www.visualstudio.com/) | :x: | [C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/) |
|
||||
| [tmux-fingers](https://github.com/Morantron/tmux-fingers) | [⬇](https://github.com/Morantron/tmux-fingers#using-tmux-plugin-manager) | [tmux](https://github.com/tmux/tmux) | :heavy_check_mark: | [Bash](https://www.gnu.org/software/bash/) |
|
||||
| [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) |
|
||||
| [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/) |
|
||||
| [cVim](https://github.com/1995eaton/chromium-vim) | [⬇](https://chrome.google.com/webstore/detail/cvim/ihlenndgcmojhcghmfjfneahoeklbjjh) | [Chrome](https://www.google.com/chrome) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
|
||||
| [SurfingKeys](https://github.com/brookhong/Surfingkeys) | [⬇](https://chrome.google.com/webstore/detail/surfingkeys/gfbliohnnapiefjpjlpjnehglfpaknnc) | [Chrome](https://www.google.com/chrome) / [Firefox](https://www.mozilla.org/firefox) | :heavy_check_mark: | [JavaScript](https://www.javascript.com/) |
|
||||
| [Vimium](https://github.com/philc/vimium) | [⬇](https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb) | [Chrome](https://www.google.com/chrome) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.org/) |
|
||||
| [Vrome](https://github.com/jinzhu/vrome) | [⬇](https://chrome.google.com/webstore/detail/vrome/godjoomfiimiddapohpmfklhgmbfffjj) | [Chrome](https://www.google.com/chrome) | :x: | [CoffeeScript](http://coffeescript.org/) |
|
||||
| [ViChrome](https://github.com/k2nr/ViChrome) | [⬇](https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi) | [Chrome](https://www.google.com/chrome) | :x: | [CoffeeScript](http://coffeescript.org/) |
|
||||
| [VimFx](https://github.com/akhodakivskiy/VimFx) | [⬇](https://github.com/akhodakivskiy/VimFx/releases) | [Firefox](https://www.mozilla.org/firefox) | :heavy_check_mark: | [CoffeeScript](http://coffeescript.org/) |
|
||||
| [Vimperator](https://github.com/vimperator/vimperator-labs/) | [⬇](https://github.com/vimperator/vimperator-labs/releases) | [Firefox](https://www.mozilla.org/firefox) | :x: | [JavaScript](https://www.javascript.com/) |
|
||||
| [Pentadactyl](https://github.com/5digits/dactyl) | [⬇](http://bug.5digits.org/pentadactyl/#sect-download) | [Firefox](https://www.mozilla.org/firefox) | :x: | [JavaScript](https://www.typescriptlang.org/) |
|
||||
| [Pentadactyl](https://github.com/5digits/dactyl) | [⬇](http://bug.5digits.org/pentadactyl/#sect-download) | [Firefox](https://www.mozilla.org/firefox) | :x: | [JavaScript](https://www.javascript.com/) |
|
||||
| [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/) |
|
||||
@@ -227,8 +170,8 @@ The following individuals have significantly improved AceJump through their cont
|
||||
|
||||
* [John Lindquist](https://github.com/johnlindquist) for creating AceJump and supporting it for many years.
|
||||
* [Breandan Considine](https://github.com/breandan) for maintaining the project and adding some new features.
|
||||
* [chylex](https://github.com/chylex) for numerous [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex), [bug fixes](https://github.com/acejump/AceJump/issues/348#issuecomment-739454920) and [refactoring](https://github.com/acejump/AceJump/pull/353).
|
||||
* [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292).
|
||||
* [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex).
|
||||
* [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency.
|
||||
* [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years.
|
||||
* [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design.
|
||||
|
@@ -1,85 +1,43 @@
|
||||
import org.jetbrains.changelog.Changelog.OutputType.HTML
|
||||
import org.jetbrains.changelog.date
|
||||
@file:Suppress("ConvertLambdaToReference")
|
||||
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
idea
|
||||
kotlin("jvm") version "1.8.20" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library
|
||||
kotlin("jvm") version "1.8.0"
|
||||
id("org.jetbrains.intellij") version "1.15.0"
|
||||
id("org.jetbrains.changelog") version "2.2.0"
|
||||
id("com.github.ben-manes.versions") version "0.48.0"
|
||||
}
|
||||
|
||||
tasks {
|
||||
named<Zip>("buildPlugin") {
|
||||
dependsOn("test")
|
||||
archiveFileName = "AceJump.zip"
|
||||
}
|
||||
|
||||
runIde {
|
||||
dependsOn("test")
|
||||
findProperty("luginDev")?.let { args = listOf(projectDir.absolutePath) }
|
||||
}
|
||||
|
||||
publishPlugin {
|
||||
val intellijPublishToken: String? by project
|
||||
token = intellijPublishToken
|
||||
}
|
||||
|
||||
patchPluginXml {
|
||||
sinceBuild = "223.7571.182"
|
||||
changeNotes = provider {
|
||||
changelog.renderItem(changelog.getAll().values.take(2).last(), HTML)
|
||||
}
|
||||
}
|
||||
|
||||
runPluginVerifier {
|
||||
ideVersions = listOf("2023.2")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
sourceSets.all {
|
||||
languageSettings.apply {
|
||||
languageVersion = "2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val acejumpVersion = "3.8.16"
|
||||
|
||||
changelog {
|
||||
version = acejumpVersion
|
||||
path = "${project.projectDir}/CHANGES.md"
|
||||
header = provider { "[${project.version}] - ${date()}" }
|
||||
itemPrefix = "-"
|
||||
unreleasedTerm = "Unreleased"
|
||||
}
|
||||
group = "org.acejump"
|
||||
version = "chylex-13"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://github.com/anyascii/anyascii
|
||||
implementation("com.anyascii:anyascii:0.3.2")
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
intellij {
|
||||
version = "2023.2.1"
|
||||
pluginName = "AceJump"
|
||||
updateSinceUntilBuild = false
|
||||
plugins = listOf("java")
|
||||
version.set("2023.2")
|
||||
updateSinceUntilBuild.set(false)
|
||||
plugins.add("IdeaVIM:chylex-16")
|
||||
|
||||
pluginsRepositories {
|
||||
custom("https://intellij.chylex.com")
|
||||
}
|
||||
}
|
||||
|
||||
group = "org.acejump"
|
||||
version = acejumpVersion
|
||||
tasks.patchPluginXml {
|
||||
sinceBuild.set("231")
|
||||
}
|
||||
|
||||
tasks.buildSearchableOptions {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs = listOf(
|
||||
"-Xjvm-default=all"
|
||||
)
|
||||
}
|
||||
|
@@ -1,4 +1 @@
|
||||
kotlin.stdlib.default.dependency=false
|
||||
kotlin.incremental.useClasspathSnapshot=false
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
2
gradlew
vendored
2
gradlew
vendored
@@ -72,7 +72,7 @@ case "`uname`" in
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
MSYS* | MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
|
@@ -1,56 +1,44 @@
|
||||
package org.acejump
|
||||
|
||||
import com.anyascii.AnyAscii
|
||||
import com.intellij.diff.util.DiffUtil.getLineCount
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.*
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.util.IncorrectOperationException
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import org.acejump.config.AceConfig
|
||||
import java.awt.Point
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* This annotation is a marker which means that the annotated function is
|
||||
* used in external plugins.
|
||||
*/
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class ExternalUsage
|
||||
|
||||
/**
|
||||
* Returns an immutable version of the currently edited document.
|
||||
*/
|
||||
val Editor.immutableText get() = EditorsCache.getText(this)
|
||||
val Editor.immutableText
|
||||
get() = this.document.immutableCharSequence
|
||||
|
||||
object EditorsCache {
|
||||
private var stale = true
|
||||
fun invalidate() {
|
||||
stale = true
|
||||
editorTexts.clear()
|
||||
/**
|
||||
* Returns all open editors in the project.
|
||||
*/
|
||||
val Project.openEditors: List<Editor>
|
||||
get() {
|
||||
return try {
|
||||
FileEditorManagerEx.getInstanceEx(this)
|
||||
.splitters
|
||||
.getSelectedEditors()
|
||||
.mapNotNull { (it as? TextEditor)?.editor }
|
||||
} catch (e: IncorrectOperationException) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
private val editorTexts = mutableMapOf<Editor, CharSequence>()
|
||||
|
||||
fun getText(editor: Editor) =
|
||||
if (stale || editor !in editorTexts)
|
||||
editor.document.immutableCharSequence
|
||||
.let { if (AceConfig.mapToASCII) it.mapToASCII() else it }
|
||||
.also { editorTexts[editor] = it; stale = false }
|
||||
else editorTexts[editor]!!
|
||||
}
|
||||
|
||||
fun CharSequence.mapToASCII() =
|
||||
map { AnyAscii.transliterate("$it").first() }.joinToString("")
|
||||
|
||||
/**
|
||||
* Returns true if [this] contains [otherText] at the specified offset.
|
||||
*/
|
||||
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean) =
|
||||
regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
|
||||
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean): Boolean {
|
||||
return this.regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the length of a common prefix in [this] starting
|
||||
* at index [selfOffset], and [otherText] starting at index 0.
|
||||
* Calculates the length of a common prefix in [this] starting at index [selfOffset], and [otherText] starting at index 0.
|
||||
*/
|
||||
fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): Int {
|
||||
var i = 0
|
||||
@@ -68,17 +56,17 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
|
||||
* Determines which characters form a "word" for the purposes of functions below.
|
||||
*/
|
||||
val Char.isWordPart
|
||||
get() = this.isJavaIdentifierPart()
|
||||
get() = this in 'a'..'z' || this.isJavaIdentifierPart()
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -86,28 +74,31 @@ inline fun CharSequence.wordStart(
|
||||
/**
|
||||
* 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
|
||||
val limit = length - 1
|
||||
|
||||
while (end < length - 1 && isPartOfWord(this[end + 1])) ++end
|
||||
while (end < limit && 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)
|
||||
val limit = length - 1
|
||||
|
||||
while (end < length - 1 && !isPartOfWord(this[end + 1])) ++end
|
||||
while (end < limit && !isPartOfWord(this[end + 1])) {
|
||||
++end
|
||||
}
|
||||
|
||||
if (end < length - 1 && isPartOfWord(this[end + 1])) ++end
|
||||
if (end < limit && isPartOfWord(this[end + 1])) {
|
||||
++end
|
||||
}
|
||||
|
||||
return end
|
||||
}
|
||||
@@ -121,134 +112,3 @@ fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
fun Editor.offsetCenter(first: Int, second: Int): LogicalPosition {
|
||||
val firstIndexLine = offsetToLogicalPosition(first).line
|
||||
val lastIndexLine = offsetToLogicalPosition(second).line
|
||||
val center = (firstIndexLine + lastIndexLine) / 2
|
||||
return offsetToLogicalPosition(getLineStartOffset(center))
|
||||
}
|
||||
|
||||
// Borrowed from Editor.calculateVisibleRange() but only available after 232.6095.10
|
||||
fun Editor.getView(): IntRange {
|
||||
ApplicationManager.getApplication().assertIsDispatchThread()
|
||||
val rect = scrollingModel.visibleArea
|
||||
val startPosition = xyToLogicalPosition(Point(rect.x, rect.y))
|
||||
val visibleStart = logicalPositionToOffset(startPosition)
|
||||
val endPosition = xyToLogicalPosition(Point(rect.x + rect.width, rect.y + rect.height))
|
||||
val visibleEnd = logicalPositionToOffset(LogicalPosition(endPosition.line + 1, 0))
|
||||
return visibleStart..visibleEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the start of the requested line.
|
||||
*
|
||||
* @param line The logical line to get the start offset for.
|
||||
*
|
||||
* @return 0 if line is < 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 < 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))
|
||||
|
||||
|
@@ -1,120 +0,0 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
|
||||
import com.intellij.openapi.actionSystem.PlatformDataKeys.LAST_ACTIVE_FILE_EDITOR
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.util.IncorrectOperationException
|
||||
import org.acejump.boundaries.Boundaries
|
||||
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
|
||||
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
|
||||
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
|
||||
import org.acejump.input.JumpMode
|
||||
import org.acejump.input.JumpMode.DECLARATION
|
||||
import org.acejump.input.JumpMode.JUMP
|
||||
import org.acejump.input.JumpMode.JUMP_END
|
||||
import org.acejump.input.JumpMode.TARGET
|
||||
import org.acejump.search.Pattern
|
||||
import org.acejump.search.Pattern.ALL_WORDS
|
||||
import org.acejump.search.Pattern.LINE_ALL_MARKS
|
||||
import org.acejump.search.Pattern.LINE_ENDS
|
||||
import org.acejump.search.Pattern.LINE_INDENTS
|
||||
import org.acejump.search.Pattern.LINE_STARTS
|
||||
import org.acejump.session.Session
|
||||
import org.acejump.session.SessionManager
|
||||
|
||||
/**
|
||||
* Base class for keyboard-activated actions that create or update an AceJump [Session].
|
||||
*/
|
||||
sealed class AceAction: DumbAwareAction() {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
final override fun update(action: AnActionEvent) {
|
||||
action.presentation.isEnabled =
|
||||
(action.getData(EDITOR) ?: (action.getData(LAST_ACTIVE_FILE_EDITOR) as? TextEditor)?.editor) != null
|
||||
}
|
||||
|
||||
final override fun actionPerformed(e: AnActionEvent) {
|
||||
val editor = e.getData(EDITOR) ?: (e.getData(LAST_ACTIVE_FILE_EDITOR) as? TextEditor)?.editor ?: return
|
||||
val project = e.project
|
||||
|
||||
if (project != null) {
|
||||
try {
|
||||
val fem = FileEditorManager.getInstance(project) as FileEditorManagerEx
|
||||
val openEditors = fem.splitters.getSelectedEditors()
|
||||
.mapNotNull { (it as? TextEditor)?.editor }
|
||||
.sortedBy { if (it === editor) 0 else 1 }
|
||||
invoke(SessionManager.start(editor, openEditors))
|
||||
} catch (e: IncorrectOperationException) {
|
||||
invoke(SessionManager.start(editor))
|
||||
}
|
||||
} else {
|
||||
invoke(SessionManager.start(editor))
|
||||
}
|
||||
}
|
||||
|
||||
abstract operator fun invoke(session: Session)
|
||||
|
||||
/**
|
||||
* Generic action type that toggles a specific [JumpMode].
|
||||
*/
|
||||
abstract class BaseToggleJumpModeAction(private val mode: JumpMode): AceAction() {
|
||||
final override fun invoke(session: Session) = session.toggleJumpMode(mode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic action type that toggles a specific [JumpMode] with [Boundaries].
|
||||
*/
|
||||
abstract class BaseToggleBoundedJumpModeAction(private val mode: JumpMode, private val boundaries: Boundaries): AceAction() {
|
||||
final override fun invoke(session: Session) = session.toggleJumpMode(mode, boundaries)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic action type that starts a regex search.
|
||||
*/
|
||||
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries): AceAction() {
|
||||
override fun invoke(session: Session) = session.startRegexSearch(pattern, boundaries)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates an AceJump session in the first [JumpMode], or cycles to the next [JumpMode] as defined in configuration.
|
||||
*/
|
||||
class ActivateOrCycleMode: AceAction() {
|
||||
override fun invoke(session: Session) = session.cycleNextJumpMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates an AceJump session in the last [JumpMode], or cycles to the previous [JumpMode] as defined in configuration.
|
||||
*/
|
||||
class ActivateOrReverseCycleMode: AceAction() {
|
||||
override fun invoke(session: Session) = session.cyclePreviousJumpMode()
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
// Unbounded Toggle Modes
|
||||
class ToggleJumpMode : BaseToggleJumpModeAction(JUMP)
|
||||
class ToggleJumpEndMode : BaseToggleJumpModeAction(JUMP_END)
|
||||
class ToggleTargetMode : BaseToggleJumpModeAction(TARGET)
|
||||
class ToggleDeclarationMode : BaseToggleJumpModeAction(DECLARATION)
|
||||
|
||||
// Bounded Toggle Modes
|
||||
class ToggleBackwardJumpMode : BaseToggleBoundedJumpModeAction(JUMP, BEFORE_CARET)
|
||||
class ToggleForwardJumpMode : BaseToggleBoundedJumpModeAction(JUMP, AFTER_CARET)
|
||||
|
||||
// Regex Modes
|
||||
class StartAllWordsMode : BaseRegexSearchAction(ALL_WORDS, WHOLE_FILE)
|
||||
class StartAllWordsBackwardsMode : BaseRegexSearchAction(ALL_WORDS, BEFORE_CARET)
|
||||
class StartAllWordsForwardMode : BaseRegexSearchAction(ALL_WORDS, AFTER_CARET)
|
||||
class StartAllLineStartsMode : BaseRegexSearchAction(LINE_STARTS, WHOLE_FILE)
|
||||
class StartAllLineEndsMode : BaseRegexSearchAction(LINE_ENDS, WHOLE_FILE)
|
||||
class StartAllLineIndentsMode : BaseRegexSearchAction(LINE_INDENTS, WHOLE_FILE)
|
||||
class StartAllLineMarksMode : BaseRegexSearchAction(LINE_ALL_MARKS, WHOLE_FILE)
|
||||
|
||||
// @formatter:on
|
||||
}
|
@@ -4,25 +4,27 @@ import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.editor.Caret
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
||||
import org.acejump.boundaries.StandardBoundaries.*
|
||||
import org.acejump.search.Pattern.*
|
||||
import org.acejump.session.Session
|
||||
import org.acejump.session.SessionManager
|
||||
|
||||
/**
|
||||
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
|
||||
*/
|
||||
sealed class AceEditorAction(private val originalHandler: EditorActionHandler): EditorActionHandler() {
|
||||
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean =
|
||||
SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
|
||||
abstract class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
|
||||
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
val session = SessionManager[editor]
|
||||
|
||||
if (session != null) run(session)
|
||||
else if (originalHandler.isEnabled(editor, caret, dataContext))
|
||||
if (session != null) {
|
||||
run(session)
|
||||
}
|
||||
else if (originalHandler.isEnabled(editor, caret, dataContext)) {
|
||||
originalHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun run(session: Session)
|
||||
|
||||
@@ -36,31 +38,7 @@ sealed class AceEditorAction(private val originalHandler: EditorActionHandler):
|
||||
override fun run(session: Session) = session.restart()
|
||||
}
|
||||
|
||||
class SelectBackward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) = session.visitPreviousTag()
|
||||
}
|
||||
|
||||
class SelectForward(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) = session.visitNextTag()
|
||||
}
|
||||
|
||||
class ScrollToNextScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) { session.scrollToNextScreenful() }
|
||||
}
|
||||
|
||||
class ScrollToPreviousScreenful(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) { session.scrollToPreviousScreenful() }
|
||||
}
|
||||
|
||||
class SearchLineStarts(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) = session.startRegexSearch(LINE_STARTS, WHOLE_FILE)
|
||||
}
|
||||
|
||||
class SearchLineEnds(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) = session.startRegexSearch(LINE_ENDS, WHOLE_FILE)
|
||||
}
|
||||
|
||||
class SearchLineIndents(originalHandler: EditorActionHandler): AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) = session.startRegexSearch(LINE_INDENTS, WHOLE_FILE)
|
||||
class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
|
||||
override fun run(session: Session) = session.tagImmediately()
|
||||
}
|
||||
}
|
||||
|
84
src/main/kotlin/org/acejump/action/AceTagAction.kt
Normal file
84
src/main/kotlin/org/acejump/action/AceTagAction.kt
Normal file
@@ -0,0 +1,84 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.command.UndoConfirmationPolicy
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
|
||||
import com.intellij.openapi.project.Project
|
||||
import org.acejump.search.SearchProcessor
|
||||
|
||||
/**
|
||||
* Base class for actions available after typing a tag.
|
||||
*/
|
||||
sealed class AceTagAction {
|
||||
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean)
|
||||
|
||||
abstract class BaseJumpAction : AceTagAction() {
|
||||
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
|
||||
val caretModel = editor.caretModel
|
||||
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
|
||||
|
||||
recordCaretPosition(editor)
|
||||
|
||||
if (isFinal) {
|
||||
ensureEditorFocused(editor)
|
||||
}
|
||||
|
||||
moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset))
|
||||
|
||||
if (shiftMode) {
|
||||
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun recordCaretPosition(editor: Editor) = with(editor) {
|
||||
project?.let { addCurrentPositionToHistory(it, document) }
|
||||
}
|
||||
|
||||
fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
|
||||
selectionModel.removeSelection(true)
|
||||
caretModel.removeSecondaryCarets()
|
||||
caretModel.moveToOffset(offset)
|
||||
}
|
||||
|
||||
fun ensureEditorFocused(editor: Editor) {
|
||||
val project = editor.project ?: return
|
||||
val fem = FileEditorManagerEx.getInstanceEx(project)
|
||||
|
||||
val window = fem.windows.firstOrNull { (it.selectedComposite?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
|
||||
if (window != null && window !== fem.currentWindow) {
|
||||
fem.currentWindow = window
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCurrentPositionToHistory(project: Project, document: Document) {
|
||||
CommandProcessor.getInstance().executeCommand(project, {
|
||||
with(IdeDocumentHistory.getInstance(project)) {
|
||||
setCurrentCommandHasMoves()
|
||||
includeCurrentCommandAsNavigation()
|
||||
includeCurrentPlaceAsChangePlace()
|
||||
}
|
||||
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document), UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document)
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/**
|
||||
* On default action, places the caret at the first character of the search query.
|
||||
* On shift action, adds the new caret to existing carets.
|
||||
*/
|
||||
object JumpToSearchStart : BaseJumpAction() {
|
||||
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
|
||||
return offset
|
||||
}
|
||||
}
|
||||
}
|
162
src/main/kotlin/org/acejump/action/AceVimAction.kt
Normal file
162
src/main/kotlin/org/acejump/action/AceVimAction.kt
Normal file
@@ -0,0 +1,162 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.action.change.change.ChangeVisualAction
|
||||
import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction
|
||||
import com.maddyhome.idea.vim.action.copy.YankVisualAction
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.command.MappingMode.OP_PENDING
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.visual.vimSetSelection
|
||||
import com.maddyhome.idea.vim.helper.inVisualMode
|
||||
import com.maddyhome.idea.vim.helper.vimSelectionStart
|
||||
import com.maddyhome.idea.vim.helper.vimStateMachine
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||
import org.acejump.boundaries.StandardBoundaries.*
|
||||
import org.acejump.modes.JumpMode
|
||||
import org.acejump.search.Pattern
|
||||
import org.acejump.search.Tag
|
||||
import org.acejump.session.SessionManager
|
||||
import org.acejump.session.SessionState
|
||||
|
||||
sealed class AceVimAction : DumbAwareAction() {
|
||||
protected abstract val mode: AceVimMode
|
||||
|
||||
final override fun actionPerformed(e: AnActionEvent) {
|
||||
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
|
||||
val context = e.dataContext.vim
|
||||
|
||||
val caret = editor.caretModel.currentCaret
|
||||
val initialOffset = caret.offset
|
||||
val selectionStart = if (editor.inVisualMode) caret.vimSelectionStart else null
|
||||
|
||||
val session = SessionManager.start(editor, mode.getJumpEditors(editor))
|
||||
|
||||
session.defaultBoundary = mode.boundaries
|
||||
session.startJumpMode {
|
||||
object : JumpMode() {
|
||||
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
|
||||
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
|
||||
|
||||
if (selectionStart != null) {
|
||||
caret.vim.vimSetSelection(selectionStart, caret.offset, moveCaretToSelectionEnd = true)
|
||||
}
|
||||
else {
|
||||
val vim = editor.vim
|
||||
val commandState = vim.vimStateMachine
|
||||
if (commandState.isOperatorPending) {
|
||||
val key = commandState.commandBuilder.keys.singleOrNull()?.keyChar
|
||||
|
||||
commandState.reset()
|
||||
KeyHandler.getInstance().fullReset(vim)
|
||||
|
||||
AceVimUtil.enterVisualMode(vim, SelectionType.CHARACTER_WISE)
|
||||
caret.vim.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true)
|
||||
|
||||
val action = when (key) {
|
||||
'd' -> DeleteVisualAction()
|
||||
'c' -> ChangeVisualAction()
|
||||
'y' -> YankVisualAction()
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
WriteAction.run<Nothing> {
|
||||
commandState.commandBuilder.pushCommandPart(action)
|
||||
|
||||
val cmd = commandState.commandBuilder.buildCommand()
|
||||
val operatorArguments = OperatorArguments(commandState.mappingState.mappingMode == OP_PENDING, cmd.rawCount, commandState.mode)
|
||||
|
||||
commandState.executingCommand = cmd
|
||||
injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments)
|
||||
// TODO does not update status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mode.finishSession(editor, session)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mode.setupSession(editor, session)
|
||||
}
|
||||
|
||||
class JumpAllEditors : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpAllEditors
|
||||
}
|
||||
|
||||
class JumpForward : AceVimAction() {
|
||||
override val mode = AceVimMode.Jump(AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.Jump(BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpTillForward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpTillForward(AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpTillBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpTillBackward(BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpOnLineForward : AceVimAction() {
|
||||
override val mode = AceVimMode.Jump(AFTER_CARET.intersection(CARET_LINE))
|
||||
}
|
||||
|
||||
class JumpOnLineBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.Jump(BEFORE_CARET.intersection(CARET_LINE))
|
||||
}
|
||||
|
||||
class JumpLineIndentsForward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpLineIndentsBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpLWordForward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpUWordForward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpLWordBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpUWordBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpLWordEndForward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpUWordEndForward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpLWordEndBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
|
||||
class JumpUWordEndBackward : AceVimAction() {
|
||||
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
|
||||
}
|
||||
}
|
64
src/main/kotlin/org/acejump/action/AceVimMode.kt
Normal file
64
src/main/kotlin/org/acejump/action/AceVimMode.kt
Normal file
@@ -0,0 +1,64 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import org.acejump.boundaries.Boundaries
|
||||
import org.acejump.boundaries.StandardBoundaries
|
||||
import org.acejump.openEditors
|
||||
import org.acejump.search.Pattern
|
||||
import org.acejump.session.Session
|
||||
|
||||
sealed class AceVimMode {
|
||||
abstract val boundaries: Boundaries
|
||||
|
||||
open fun getJumpEditors(mainEditor: Editor): List<Editor> {
|
||||
return listOf(mainEditor)
|
||||
}
|
||||
|
||||
open fun setupSession(editor: Editor, session: Session) {}
|
||||
open fun finishSession(editor: Editor, session: Session) {}
|
||||
|
||||
class Jump(override val boundaries: Boundaries) : AceVimMode()
|
||||
|
||||
object JumpAllEditors : AceVimMode() {
|
||||
override val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
override fun getJumpEditors(mainEditor: Editor): List<Editor> {
|
||||
val project = mainEditor.project ?: return super.getJumpEditors(mainEditor)
|
||||
|
||||
return project.openEditors
|
||||
.sortedBy { if (it === mainEditor) 0 else 1 }
|
||||
.ifEmpty { listOf(mainEditor) }
|
||||
}
|
||||
}
|
||||
|
||||
class JumpTillForward(override val boundaries: Boundaries) : AceVimMode() {
|
||||
override fun finishSession(editor: Editor, session: Session) {
|
||||
val document = editor.document
|
||||
|
||||
for (caret in editor.caretModel.allCarets) {
|
||||
val offset = caret.offset
|
||||
if (offset > document.getLineStartOffset(document.getLineNumber(offset))) {
|
||||
caret.moveToOffset(offset - 1, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JumpTillBackward(override val boundaries: Boundaries) : AceVimMode() {
|
||||
override fun finishSession(editor: Editor, session: Session) {
|
||||
val document = editor.document
|
||||
|
||||
for (caret in editor.caretModel.allCarets) {
|
||||
val offset = caret.offset
|
||||
if (offset < document.getLineEndOffset(document.getLineNumber(offset))) {
|
||||
caret.moveToOffset(offset + 1, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JumpToPattern(private val pattern: Pattern, override val boundaries: Boundaries) : AceVimMode() {
|
||||
override fun setupSession(editor: Editor, session: Session) {
|
||||
session.startRegexSearch(pattern)
|
||||
}
|
||||
}
|
||||
}
|
13
src/main/kotlin/org/acejump/action/AceVimUtil.java
Normal file
13
src/main/kotlin/org/acejump/action/AceVimUtil.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.acejump.action;
|
||||
|
||||
import com.maddyhome.idea.vim.VimPlugin;
|
||||
import com.maddyhome.idea.vim.api.VimEditor;
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType;
|
||||
|
||||
final class AceVimUtil {
|
||||
private AceVimUtil() {}
|
||||
|
||||
public static void enterVisualMode(final VimEditor vim, final SelectionType mode) {
|
||||
VimPlugin.getVisualMotion().enterVisualMode(vim, mode);
|
||||
}
|
||||
}
|
@@ -1,134 +0,0 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.IdeActions.ACTION_GOTO_DECLARATION
|
||||
import com.intellij.openapi.actionSystem.IdeActions.ACTION_GOTO_TYPE_DECLARATION
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.command.UndoConfirmationPolicy
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.playback.commands.ActionCommand
|
||||
import org.acejump.countMatchingCharacters
|
||||
import org.acejump.immutableText
|
||||
import org.acejump.input.JumpMode
|
||||
import org.acejump.input.JumpMode.DECLARATION
|
||||
import org.acejump.input.JumpMode.JUMP_END
|
||||
import org.acejump.input.JumpMode.TARGET
|
||||
import org.acejump.isWordPart
|
||||
import org.acejump.search.SearchProcessor
|
||||
import org.acejump.search.Tag
|
||||
import org.acejump.wordEnd
|
||||
import org.acejump.wordStart
|
||||
|
||||
/**
|
||||
* Performs [JumpMode] navigation and actions.
|
||||
*/
|
||||
internal class TagJumper(private val mode: JumpMode, private val searchProcessor: SearchProcessor?) {
|
||||
/**
|
||||
* Moves caret to a specific offset in the editor according to the positioning and selection rules of the current [JumpMode].
|
||||
*/
|
||||
fun visit(tag: Tag) {
|
||||
val editor = tag.editor
|
||||
val offset = tag.offset
|
||||
|
||||
if (mode === JUMP_END || mode === TARGET) {
|
||||
val chars = editor.immutableText
|
||||
val matchingChars = searchProcessor?.let { chars.countMatchingCharacters(offset, it.query.rawText) } ?: 0
|
||||
val targetOffset = offset + matchingChars
|
||||
val isInsideWord = matchingChars > 0 && chars[targetOffset - 1].isWordPart && chars[targetOffset].isWordPart
|
||||
val finalTargetOffset = if (isInsideWord) chars.wordEnd(targetOffset) + 1 else targetOffset
|
||||
|
||||
if (mode === JUMP_END) {
|
||||
moveCaretTo(editor, finalTargetOffset)
|
||||
} else if (mode === TARGET) {
|
||||
if (isInsideWord) {
|
||||
selectRange(editor, chars.wordStart(targetOffset), finalTargetOffset)
|
||||
} else {
|
||||
selectRange(editor, offset, finalTargetOffset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
moveCaretTo(editor, offset)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates caret and selection by [visit]ing a specific offset in the editor, and applying session-finalizing [JumpMode] actions such as
|
||||
* using the Go To Declaration action, or selecting text between caret and target offset/word if Shift was held during the jump.
|
||||
*/
|
||||
fun jump(tag: Tag, shiftMode: Boolean, isCrossEditor: Boolean) {
|
||||
val editor = tag.editor
|
||||
val oldOffset = editor.caretModel.offset
|
||||
|
||||
visit(tag)
|
||||
|
||||
if (mode === DECLARATION) {
|
||||
performAction(ActionManager.getInstance().getAction(if (shiftMode) ACTION_GOTO_TYPE_DECLARATION else ACTION_GOTO_DECLARATION))
|
||||
return
|
||||
}
|
||||
|
||||
if (shiftMode && !isCrossEditor) {
|
||||
val newOffset = editor.caretModel.offset
|
||||
|
||||
if (mode === TARGET) {
|
||||
selectRange(
|
||||
editor, oldOffset, when {
|
||||
newOffset < oldOffset -> editor.selectionModel.selectionStart
|
||||
else -> editor.selectionModel.selectionEnd
|
||||
}
|
||||
)
|
||||
} else {
|
||||
selectRange(editor, oldOffset, newOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
|
||||
ensureEditorFocused(this)
|
||||
project?.let { addCurrentPositionToHistory(it, document) }
|
||||
selectionModel.removeSelection(true)
|
||||
caretModel.moveToOffset(offset)
|
||||
}
|
||||
|
||||
private fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int) = with(editor) {
|
||||
ensureEditorFocused(this)
|
||||
selectionModel.removeSelection(true)
|
||||
selectionModel.setSelection(fromOffset, toOffset)
|
||||
caretModel.moveToOffset(toOffset)
|
||||
}
|
||||
|
||||
private fun ensureEditorFocused(editor: Editor) {
|
||||
val project = editor.project ?: return
|
||||
val fem = FileEditorManager.getInstance(project) as FileEditorManagerEx
|
||||
|
||||
val window = fem.windows.firstOrNull { (it.getSelectedComposite(false)?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
|
||||
if (window != null && window !== fem.currentWindow) {
|
||||
fem.currentWindow = window
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCurrentPositionToHistory(project: Project, document: Document) {
|
||||
CommandProcessor.getInstance().executeCommand(
|
||||
project, {
|
||||
with(IdeDocumentHistory.getInstance(project)) {
|
||||
setCurrentCommandHasMoves()
|
||||
includeCurrentCommandAsNavigation()
|
||||
includeCurrentPlaceAsChangePlace()
|
||||
}
|
||||
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document),
|
||||
UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document
|
||||
)
|
||||
}
|
||||
|
||||
private fun performAction(action: AnAction) = ActionManager.getInstance()
|
||||
.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
|
||||
}
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.editor.*
|
||||
import org.acejump.*
|
||||
import org.acejump.search.SearchProcessor
|
||||
|
||||
internal class TagScroller(private val editor: Editor, private val searchProcessor: SearchProcessor) {
|
||||
fun scroll(
|
||||
forward: Boolean = true,
|
||||
position: LogicalPosition? = if (forward) findNextPosition() else findPreviousPosition()
|
||||
) = if (position != null) true.also { scrollTo(position) } else false
|
||||
|
||||
private fun scrollTo(position: LogicalPosition) = editor.run {
|
||||
scrollingModel.disableAnimation()
|
||||
scrollingModel.scrollTo(position, ScrollType.CENTER)
|
||||
|
||||
val firstInView = textMatches.first { it in editor.getView() }
|
||||
val horizontalOffset = offsetToLogicalPosition(firstInView).column
|
||||
if (horizontalOffset > scrollingModel.visibleArea.width)
|
||||
scrollingModel.scrollHorizontally(horizontalOffset)
|
||||
}
|
||||
|
||||
val textMatches by lazy { searchProcessor.results[editor]!! }
|
||||
|
||||
private fun findPreviousPosition(): LogicalPosition? {
|
||||
val prevIndex = textMatches.toList().dropLastWhile { it > editor.getView().first }
|
||||
.lastOrNull() ?: textMatches.lastOrNull() ?: return null
|
||||
|
||||
val prevLineNum = editor.offsetToLogicalPosition(prevIndex).line
|
||||
|
||||
// Try to capture as many previous results as will fit in a screenful
|
||||
fun maximizeCoverageOfPreviousOccurrence(): LogicalPosition {
|
||||
val minVisibleLine = prevLineNum - editor.getScreenHeight()
|
||||
val firstVisibleIndex = editor.getLineStartOffset(minVisibleLine)
|
||||
val firstIndex = textMatches.dropWhile { it < firstVisibleIndex }.first()
|
||||
return editor.offsetCenter(firstIndex, prevIndex)
|
||||
}
|
||||
|
||||
return maximizeCoverageOfPreviousOccurrence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center of the next set of results that will fit in the editor.
|
||||
* [textMatches] must be sorted prior to using Scroller. If [textMatches] have
|
||||
* not previously been sorted, the result of calling this method is undefined.
|
||||
*/
|
||||
|
||||
private fun findNextPosition(): LogicalPosition? {
|
||||
val nextIndex = textMatches.dropWhile { it <= editor.getView().last }
|
||||
.firstOrNull() ?: textMatches.firstOrNull() ?: return null
|
||||
|
||||
val nextLineNum = editor.offsetToLogicalPosition(nextIndex).line
|
||||
|
||||
// Try to capture as many subsequent results as will fit in a screenful
|
||||
fun maximizeCoverageOfNextOccurrence(): LogicalPosition {
|
||||
val maxVisibleLine = nextLineNum + editor.getScreenHeight()
|
||||
val lastVisibleIndex = editor.getLineEndOffset(maxVisibleLine, true)
|
||||
val lastIndex = textMatches.toList().dropLastWhile { it > lastVisibleIndex }.last()
|
||||
return editor.offsetCenter(nextIndex, lastIndex)
|
||||
}
|
||||
|
||||
return maximizeCoverageOfNextOccurrence()
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
package org.acejump.action
|
||||
|
||||
import com.intellij.openapi.editor.*
|
||||
import com.intellij.openapi.editor.ScrollType.*
|
||||
import org.acejump.search.SearchProcessor
|
||||
import org.acejump.search.Tag
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Enables navigation between currently active tags.
|
||||
*/
|
||||
internal class TagVisitor(private val editor: Editor, private val searchProcessor: SearchProcessor, private val tagJumper: TagJumper) {
|
||||
/**
|
||||
* Places caret at the closest tag following the caret position, according
|
||||
* to the rules of the current jump mode (see [TagJumper.visit]).
|
||||
* If the caret is at or past the last tag, it moves to the first tag instead.
|
||||
* If there is only one tag, it immediately performs the jump action as described in [TagJumper.jump].
|
||||
*/
|
||||
fun visitNext(): Boolean =
|
||||
visit(SelectionModel::getSelectionEnd) { if (it < 0) -it - 1 else it + 1 }
|
||||
|
||||
/**
|
||||
* Places caret at the closest tag preceding the caret position, according to
|
||||
* the rules of the current jump mode (see [TagJumper.visit]).
|
||||
* If the caret is at or before the first tag, it moves to the last tag instead.
|
||||
* If there is only one tag, it immediately performs the jump action as described in [TagJumper.jump].
|
||||
*/
|
||||
fun visitPrevious(): Boolean =
|
||||
visit(SelectionModel::getSelectionStart) { if (it < 0) -it - 2 else it - 1 }
|
||||
|
||||
/**
|
||||
* Scrolls to the closest result to the caret.
|
||||
*/
|
||||
fun scrollToClosest() {
|
||||
val caret = editor.caretModel.offset
|
||||
val results = searchProcessor.results[editor].takeUnless { it.isNullOrEmpty() } ?: return
|
||||
val index = results.binarySearch(caret).let { if (it < 0) -it - 1 else it }
|
||||
|
||||
val targetOffset = listOfNotNull(
|
||||
results.getOrNull(index - 1),
|
||||
results.getOrNull(index)
|
||||
).minByOrNull { abs(it - caret) }
|
||||
|
||||
if (targetOffset != null)
|
||||
editor.scrollingModel.scrollTo(editor.offsetToLogicalPosition(targetOffset), RELATIVE)
|
||||
}
|
||||
|
||||
private inline fun visit(caretPosition: SelectionModel.() -> Int, indexModifier: (Int) -> Int): Boolean {
|
||||
val results = searchProcessor.results[editor].takeUnless { it.isNullOrEmpty() } ?: return false
|
||||
val nextIndex = indexModifier(results.binarySearch(caretPosition(editor.selectionModel)))
|
||||
|
||||
val targetOffset = results.getInt(
|
||||
when {
|
||||
nextIndex < 0 -> results.lastIndex
|
||||
nextIndex > results.lastIndex -> 0
|
||||
else -> nextIndex
|
||||
}
|
||||
)
|
||||
|
||||
val onlyResult = results.size == 1
|
||||
|
||||
if (onlyResult) tagJumper.jump(Tag(editor, targetOffset), shiftMode = false, isCrossEditor = false)
|
||||
else tagJumper.visit(Tag(editor, targetOffset))
|
||||
|
||||
editor.scrollingModel.scrollToCaret(RELATIVE)
|
||||
return onlyResult
|
||||
}
|
||||
|
||||
}
|
@@ -5,40 +5,40 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Defines a (possibly) disjoint set of editor offsets that partitions
|
||||
* the whole editor into two groups - offsets inside the range, and
|
||||
* Defines a (possibly) disjoint set of editor offsets that partitions the whole editor into two groups - offsets inside the range, and
|
||||
* offsets outside the range.
|
||||
*/
|
||||
interface Boundaries {
|
||||
/**
|
||||
* Returns a range of editor offsets, starting at the first offset in the
|
||||
* boundary, and ending at the last offset in the boundary. May include
|
||||
* offsets outside the boundary, for ex. when the boundary is rectangular
|
||||
* and the file has long lines which are only partially visible.
|
||||
* Returns a range of editor offsets, starting at the first offset in the boundary, and ending at the last offset in the boundary.
|
||||
* May include offsets outside the boundary, for ex. when the boundary is rectangular and the file has long lines which are only
|
||||
* partially visible.
|
||||
*/
|
||||
fun getOffsetRange(editor: Editor, cache: EditorOffsetCache = EditorOffsetCache.Uncached): IntRange =
|
||||
StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
|
||||
fun getOffsetRange(editor: Editor, cache: EditorOffsetCache = EditorOffsetCache.Uncached): IntRange
|
||||
|
||||
/**
|
||||
* Returns whether the editor offset is included within the boundary.
|
||||
*/
|
||||
fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache = EditorOffsetCache.Uncached): Boolean =
|
||||
StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, offset, cache)
|
||||
fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache = EditorOffsetCache.Uncached): Boolean
|
||||
|
||||
/**
|
||||
* Creates a boundary so that an offset/range is within the boundary
|
||||
* iff it is within both original boundaries.
|
||||
* Creates a boundary so that an offset/range is within the boundary iff it is within both original boundaries.
|
||||
*/
|
||||
fun intersection(other: Boundaries): Boundaries =
|
||||
if (this === other) this
|
||||
else object: Boundaries {
|
||||
fun intersection(other: Boundaries): Boundaries {
|
||||
if (this === other) {
|
||||
return this
|
||||
}
|
||||
|
||||
return object : Boundaries {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
|
||||
val b1 = this@Boundaries.getOffsetRange(editor, cache)
|
||||
val b2 = other.getOffsetRange(editor, cache)
|
||||
return max(b1.first, b2.first)..min(b1.last, b2.last)
|
||||
}
|
||||
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
|
||||
this@Boundaries.isOffsetInside(editor, offset, cache) && other.isOffsetInside(editor, offset, cache)
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
||||
return this@Boundaries.isOffsetInside(editor, offset, cache) && other.isOffsetInside(editor, offset, cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,13 +6,11 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
|
||||
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 {
|
||||
/**
|
||||
@@ -30,42 +28,64 @@ sealed class EditorOffsetCache {
|
||||
*/
|
||||
abstract fun offsetToXY(editor: Editor, offset: Int): Point
|
||||
|
||||
companion object { fun new(): EditorOffsetCache = Cache() }
|
||||
companion object {
|
||||
fun new(): EditorOffsetCache {
|
||||
return Cache()
|
||||
}
|
||||
}
|
||||
|
||||
private class Cache : EditorOffsetCache() {
|
||||
private var visibleArea: Pair<Point, Point>? = null
|
||||
private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
|
||||
private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
|
||||
|
||||
override fun visibleArea(editor: Editor): Pair<Point, Point> =
|
||||
visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
|
||||
|
||||
override fun 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 visibleArea(editor: Editor): Pair<Point, Point> {
|
||||
return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
|
||||
}
|
||||
|
||||
override fun offsetToXY(editor: Editor, offset: Int) =
|
||||
offsetToPoint.get(offset) ?: Uncached.offsetToXY(editor, offset)
|
||||
.also { offsetToPoint.put(offset, it) }
|
||||
override fun xyToOffset(editor: Editor, pos: Point): Int {
|
||||
val offset = pointToOffset.getInt(pos)
|
||||
|
||||
if (offset != -1) {
|
||||
return offset
|
||||
}
|
||||
|
||||
return Uncached.xyToOffset(editor, pos).also {
|
||||
@Suppress("ReplacePutWithAssignment")
|
||||
pointToOffset.put(pos, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun offsetToXY(editor: Editor, offset: Int): Point {
|
||||
val pos = offsetToPoint.get(offset)
|
||||
|
||||
if (pos != null) {
|
||||
return pos
|
||||
}
|
||||
|
||||
return Uncached.offsetToXY(editor, offset).also {
|
||||
@Suppress("ReplacePutWithAssignment")
|
||||
offsetToPoint.put(offset, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Uncached : EditorOffsetCache() {
|
||||
override fun visibleArea(editor: Editor): Pair<Point, Point> =
|
||||
editor.scrollingModel.visibleArea.let { visibleRect ->
|
||||
Pair(
|
||||
visibleRect.location, visibleRect.location.apply {
|
||||
translate(visibleRect.width, visibleRect.height)
|
||||
}
|
||||
override fun visibleArea(editor: Editor): Pair<Point, Point> {
|
||||
val visibleRect = editor.scrollingModel.visibleArea
|
||||
|
||||
return Pair(
|
||||
visibleRect.location,
|
||||
visibleRect.location.apply { translate(visibleRect.width, visibleRect.height) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun xyToOffset(editor: Editor, pos: Point): Int =
|
||||
editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
|
||||
override fun xyToOffset(editor: Editor, pos: Point): Int {
|
||||
return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
|
||||
}
|
||||
|
||||
override fun offsetToXY(editor: Editor, offset: Int): Point =
|
||||
editor.offsetToXY(offset, true, false)
|
||||
override fun offsetToXY(editor: Editor, offset: Int): Point {
|
||||
return editor.offsetToXY(offset, true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,14 +3,6 @@ package org.acejump.boundaries
|
||||
import com.intellij.openapi.editor.Editor
|
||||
|
||||
enum class StandardBoundaries : Boundaries {
|
||||
WHOLE_FILE {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
|
||||
0..editor.document.textLength
|
||||
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache) =
|
||||
offset in (0..editor.document.textLength)
|
||||
},
|
||||
|
||||
VISIBLE_ON_SCREEN {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
|
||||
val (topLeft, bottomRight) = cache.visibleArea(editor)
|
||||
@@ -21,15 +13,16 @@ 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
|
||||
|
||||
// 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.
|
||||
// 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
|
||||
if (cache !== EditorOffsetCache.Uncached && offset !in getOffsetRange(editor, cache)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val (topLeft, bottomRight) = cache.visibleArea(editor)
|
||||
val pos = cache.offsetToXY(editor, offset)
|
||||
@@ -41,18 +34,35 @@ enum class StandardBoundaries : Boundaries {
|
||||
},
|
||||
|
||||
BEFORE_CARET {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
|
||||
0..(editor.caretModel.offset)
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
|
||||
return 0 until editor.caretModel.offset
|
||||
}
|
||||
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
|
||||
offset <= editor.caretModel.offset
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
||||
return offset < editor.caretModel.offset
|
||||
}
|
||||
},
|
||||
|
||||
AFTER_CARET {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache) =
|
||||
editor.caretModel.offset until editor.document.textLength
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
|
||||
return (editor.caretModel.offset + 1) until editor.document.textLength
|
||||
}
|
||||
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean =
|
||||
offset >= editor.caretModel.offset
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
||||
return offset > editor.caretModel.offset
|
||||
}
|
||||
},
|
||||
|
||||
CARET_LINE {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
|
||||
val document = editor.document
|
||||
val offset = editor.caretModel.offset
|
||||
val line = document.getLineNumber(offset)
|
||||
return document.getLineStartOffset(line)..document.getLineEndOffset(line)
|
||||
}
|
||||
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
||||
return offset in getOffsetRange(editor, cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,14 +1,13 @@
|
||||
package org.acejump.config
|
||||
|
||||
import com.intellij.openapi.components.PersistentStateComponent
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.components.Storage
|
||||
import com.intellij.util.application
|
||||
import org.acejump.input.KeyLayoutCache
|
||||
|
||||
/**
|
||||
* Ensures consistency between [AceSettings] and [AceSettingsPanel].
|
||||
* Persists the state of the AceJump IDE settings across IDE restarts.
|
||||
* Ensures consistiency between [AceSettings] and [AceSettingsPanel]. Persists the state of the AceJump IDE settings across IDE restarts.
|
||||
* [https://www.jetbrains.org/intellij/sdk/docs/basics/persisting_state_of_components.html]
|
||||
*/
|
||||
@State(name = "AceConfig", storages = [(Storage("\$APP_CONFIG\$/AceJump.xml"))])
|
||||
@@ -16,26 +15,21 @@ class AceConfig: PersistentStateComponent<AceSettings> {
|
||||
private var aceSettings = AceSettings()
|
||||
|
||||
companion object {
|
||||
val settings get() = application.getService(AceConfig::class.java).aceSettings
|
||||
val settings
|
||||
get() = ServiceManager.getService(AceConfig::class.java).aceSettings
|
||||
|
||||
// @formatter:off
|
||||
val layout get() = settings.layout
|
||||
val cycleModes get() = settings.let { arrayOf(it.cycleMode1, it.cycleMode2, it.cycleMode3, it.cycleMode4) }
|
||||
val minQueryLength get() = settings.minQueryLength
|
||||
val jumpModeColor get() = settings.getJumpModeJBC()
|
||||
val jumpEndModeColor get() = settings.getJumpEndModeJBC()
|
||||
val targetModeColor get() = settings.getTargetModeJBC()
|
||||
val definitionModeColor get() = settings.getDefinitionModeJBC()
|
||||
val textHighlightColor get() = settings.getTextHighlightJBC()
|
||||
val tagForegroundColor get() = settings.getTagForegroundJBC()
|
||||
val tagBackgroundColor get() = settings.getTagBackgroundJBC()
|
||||
val searchWholeFile get() = settings.searchWholeFile
|
||||
val mapToASCII get() = settings.mapToASCII
|
||||
val showSearchNotification get() = settings.showSearchNotification
|
||||
// @formatter:on
|
||||
val jumpModeColor get() = settings.jumpModeColor
|
||||
val textHighlightColor get() = settings.textHighlightColor
|
||||
val tagForegroundColor get() = settings.tagForegroundColor
|
||||
val tagBackgroundColor get() = settings.tagBackgroundColor
|
||||
val acceptedTagColor get() = settings.acceptedTagColor
|
||||
}
|
||||
|
||||
override fun getState() = aceSettings
|
||||
override fun getState(): AceSettings {
|
||||
return aceSettings
|
||||
}
|
||||
|
||||
override fun loadState(state: AceSettings) {
|
||||
aceSettings = state
|
||||
|
@@ -14,40 +14,22 @@ class AceConfigurable: Configurable {
|
||||
override fun isModified() =
|
||||
panel.allowedChars != settings.allowedChars ||
|
||||
panel.keyboardLayout != settings.layout ||
|
||||
panel.cycleMode1 != settings.cycleMode1 ||
|
||||
panel.cycleMode2 != settings.cycleMode2 ||
|
||||
panel.cycleMode3 != settings.cycleMode3 ||
|
||||
panel.cycleMode4 != settings.cycleMode4 ||
|
||||
panel.minQueryLengthInt != settings.minQueryLength ||
|
||||
panel.jumpModeColor?.rgb != settings.jumpModeColor ||
|
||||
panel.jumpEndModeColor?.rgb != settings.jumpEndModeColor ||
|
||||
panel.targetModeColor?.rgb != settings.targetModeColor ||
|
||||
panel.definitionModeColor?.rgb != settings.definitionModeColor ||
|
||||
panel.textHighlightColor?.rgb != settings.textHighlightColor ||
|
||||
panel.tagForegroundColor?.rgb != settings.tagForegroundColor ||
|
||||
panel.tagBackgroundColor?.rgb != settings.tagBackgroundColor ||
|
||||
panel.searchWholeFile != settings.searchWholeFile ||
|
||||
panel.mapToASCII != settings.mapToASCII ||
|
||||
panel.showSearchNotification != settings.showSearchNotification
|
||||
panel.jumpModeColor != settings.jumpModeColor ||
|
||||
panel.textHighlightColor != settings.textHighlightColor ||
|
||||
panel.tagForegroundColor != settings.tagForegroundColor ||
|
||||
panel.tagBackgroundColor != settings.tagBackgroundColor ||
|
||||
panel.acceptedTagColor != settings.acceptedTagColor
|
||||
|
||||
override fun apply() {
|
||||
settings.allowedChars = panel.allowedChars
|
||||
settings.layout = panel.keyboardLayout
|
||||
settings.cycleMode1 = panel.cycleMode1
|
||||
settings.cycleMode2 = panel.cycleMode2
|
||||
settings.cycleMode3 = panel.cycleMode3
|
||||
settings.cycleMode4 = panel.cycleMode4
|
||||
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
|
||||
panel.jumpModeColor?.let { settings.jumpModeColor = it.rgb }
|
||||
panel.jumpEndModeColor?.let { settings.jumpEndModeColor = it.rgb }
|
||||
panel.targetModeColor?.let { settings.targetModeColor = it.rgb }
|
||||
panel.definitionModeColor?.let { settings.definitionModeColor = it.rgb }
|
||||
panel.textHighlightColor?.let { settings.textHighlightColor = it.rgb }
|
||||
panel.tagForegroundColor?.let { settings.tagForegroundColor = it.rgb }
|
||||
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it.rgb }
|
||||
settings.searchWholeFile = panel.searchWholeFile
|
||||
settings.mapToASCII = panel.mapToASCII
|
||||
settings.showSearchNotification = panel.showSearchNotification
|
||||
panel.jumpModeColor?.let { settings.jumpModeColor = it }
|
||||
panel.textHighlightColor?.let { settings.textHighlightColor = it }
|
||||
panel.tagForegroundColor?.let { settings.tagForegroundColor = it }
|
||||
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it }
|
||||
panel.acceptedTagColor?.let { settings.acceptedTagColor = it }
|
||||
KeyLayoutCache.reset(settings)
|
||||
}
|
||||
|
||||
|
@@ -1,43 +1,27 @@
|
||||
package org.acejump.config
|
||||
|
||||
import com.intellij.ui.JBColor
|
||||
import org.acejump.input.*
|
||||
import com.intellij.util.xmlb.annotations.OptionTag
|
||||
import org.acejump.input.KeyLayout
|
||||
import org.acejump.input.KeyLayout.QWERTY
|
||||
import java.awt.Color
|
||||
|
||||
data class AceSettings(
|
||||
var layout: KeyLayout = QWERTY,
|
||||
var allowedChars: String = layout.allChars,
|
||||
var cycleMode1: JumpMode = JumpMode.JUMP,
|
||||
var cycleMode2: JumpMode = JumpMode.DECLARATION,
|
||||
var cycleMode3: JumpMode = JumpMode.TARGET,
|
||||
var cycleMode4: JumpMode = JumpMode.JUMP_END,
|
||||
var minQueryLength: Int = 1,
|
||||
|
||||
var jumpModeColor: Int = 0xFFFFFF,
|
||||
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
|
||||
var jumpModeColor: Color = Color(0xFFFFFF),
|
||||
|
||||
var jumpEndModeColor: Int = 0x33E78A,
|
||||
@OptionTag("textHighlightRGB", converter = ColorConverter::class)
|
||||
var textHighlightColor: Color = Color(0x394B58),
|
||||
|
||||
var targetModeColor: Int = 0xFFB700,
|
||||
@OptionTag("tagForegroundRGB", converter = ColorConverter::class)
|
||||
var tagForegroundColor: Color = Color(0xFFFFFF),
|
||||
|
||||
var definitionModeColor: Int = 0x6FC5FF,
|
||||
@OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
|
||||
var tagBackgroundColor: Color = Color(0x008299),
|
||||
|
||||
var textHighlightColor: Int = 0x394B58,
|
||||
|
||||
var tagForegroundColor: Int = 0xFFFFFF,
|
||||
|
||||
var tagBackgroundColor: Int = 0x008299,
|
||||
|
||||
var searchWholeFile: Boolean = true,
|
||||
|
||||
var mapToASCII: Boolean = false,
|
||||
|
||||
var showSearchNotification: Boolean = false
|
||||
) {
|
||||
fun getJumpModeJBC() = JBColor.namedColor("jumpModeRGB", jumpModeColor)
|
||||
fun getJumpEndModeJBC() = JBColor.namedColor("jumpEndModeRGB", jumpEndModeColor)
|
||||
fun getTargetModeJBC() = JBColor.namedColor("targetModeRGB", targetModeColor)
|
||||
fun getDefinitionModeJBC() = JBColor.namedColor("definitionModeRGB", definitionModeColor)
|
||||
fun getTextHighlightJBC() = JBColor.namedColor("textHighlightRGB", textHighlightColor)
|
||||
fun getTagForegroundJBC() = JBColor.namedColor("tagForegroundRGB", tagForegroundColor)
|
||||
fun getTagBackgroundJBC() = JBColor.namedColor("tagBackgroundRGB", tagBackgroundColor)
|
||||
}
|
||||
@OptionTag("acceptedTagRGB", converter = ColorConverter::class)
|
||||
var acceptedTagColor: Color = Color(0x394B58)
|
||||
)
|
||||
|
@@ -1,12 +1,19 @@
|
||||
package org.acejump.config
|
||||
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.ui.components.*
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import org.acejump.input.JumpMode
|
||||
import com.intellij.ui.ColorPanel
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.ui.layout.Cell
|
||||
import com.intellij.ui.layout.GrowPolicy.MEDIUM_TEXT
|
||||
import com.intellij.ui.layout.GrowPolicy.SHORT_TEXT
|
||||
import com.intellij.ui.layout.panel
|
||||
import org.acejump.input.KeyLayout
|
||||
import java.awt.*
|
||||
import javax.swing.*
|
||||
import java.awt.Color
|
||||
import java.awt.Font
|
||||
import javax.swing.JCheckBox
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.text.JTextComponent
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -15,72 +22,42 @@ 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 }
|
||||
private val cycleModeCombo1 = ComboBox<JumpMode>()
|
||||
private val cycleModeCombo2 = ComboBox<JumpMode>()
|
||||
private val cycleModeCombo3 = ComboBox<JumpMode>()
|
||||
private val cycleModeCombo4 = ComboBox<JumpMode>()
|
||||
private val minQueryLengthField = JBTextField()
|
||||
private val jumpModeColorWheel = ResettableColorPicker(defaults.getJumpModeJBC())
|
||||
private val jumpEndModeColorWheel = ResettableColorPicker(defaults.getJumpEndModeJBC())
|
||||
private val targetModeColorWheel = ResettableColorPicker(defaults.getTargetModeJBC())
|
||||
private val definitionModeColorWheel = ResettableColorPicker(defaults.getDefinitionModeJBC())
|
||||
private val textHighlightColorWheel = ResettableColorPicker(defaults.getTextHighlightJBC())
|
||||
private val tagForegroundColorWheel = ResettableColorPicker(defaults.getTagForegroundJBC())
|
||||
private val tagBackgroundColorWheel = ResettableColorPicker(defaults.getTagBackgroundJBC())
|
||||
private val searchWholeFileCheckBox = JBCheckBox()
|
||||
private val mapToASCIICheckBox = JBCheckBox()
|
||||
private val showSearchNotificationCheckBox = JBCheckBox()
|
||||
private val jumpModeColorWheel = ColorPanel()
|
||||
private val textHighlightColorWheel = ColorPanel()
|
||||
private val tagForegroundColorWheel = ColorPanel()
|
||||
private val tagBackgroundColorWheel = ColorPanel()
|
||||
private val acceptedTagColorWheel = ColorPanel()
|
||||
|
||||
init {
|
||||
tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
|
||||
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
|
||||
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") }
|
||||
cycleModeCombo1.setupEnumItems { cycleMode1 = it }
|
||||
cycleModeCombo2.setupEnumItems { cycleMode2 = it }
|
||||
cycleModeCombo3.setupEnumItems { cycleMode3 = it }
|
||||
cycleModeCombo4.setupEnumItems { cycleMode4 = it }
|
||||
}
|
||||
|
||||
internal val rootPanel: JPanel = panel {
|
||||
group("Characters and Layout") {
|
||||
row("Allowed characters in tags:") { cell(tagCharsField).columns(COLUMNS_LARGE) }
|
||||
row("Keyboard layout:") { cell(keyboardLayoutCombo).columns(COLUMNS_SHORT) }
|
||||
row("Keyboard design:") { cell(keyboardLayoutArea).columns(COLUMNS_SHORT) }
|
||||
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("Modes") {
|
||||
row("Cycle order:") {
|
||||
cell(cycleModeCombo1).columns(10)
|
||||
cell(cycleModeCombo2).columns(10)
|
||||
cell(cycleModeCombo3).columns(10)
|
||||
cell(cycleModeCombo4).columns(10)
|
||||
}
|
||||
titledRow("Behavior") {
|
||||
row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
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" }) }
|
||||
titledRow("Colors") {
|
||||
row("Caret background:") { short(jumpModeColorWheel) }
|
||||
row("Searched text background:") { short(textHighlightColorWheel) }
|
||||
row("Tag foreground:") { short(tagForegroundColorWheel) }
|
||||
row("Tag background:") { short(tagBackgroundColorWheel) }
|
||||
row("Accepted tag position background:") { short(acceptedTagColorWheel) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,60 +65,40 @@ internal class AceSettingsPanel {
|
||||
internal var allowedChars by tagCharsField
|
||||
internal var keyboardLayout by keyboardLayoutCombo
|
||||
internal var keyChars by keyboardLayoutArea
|
||||
internal var cycleMode1 by cycleModeCombo1
|
||||
internal var cycleMode2 by cycleModeCombo2
|
||||
internal var cycleMode3 by cycleModeCombo3
|
||||
internal var cycleMode4 by cycleModeCombo4
|
||||
internal var minQueryLength by minQueryLengthField
|
||||
internal var jumpModeColor by jumpModeColorWheel
|
||||
internal var jumpEndModeColor by jumpEndModeColorWheel
|
||||
internal var targetModeColor by targetModeColorWheel
|
||||
internal var definitionModeColor by definitionModeColorWheel
|
||||
internal var textHighlightColor by textHighlightColorWheel
|
||||
internal var tagForegroundColor by tagForegroundColorWheel
|
||||
internal var tagBackgroundColor by tagBackgroundColorWheel
|
||||
internal var searchWholeFile by searchWholeFileCheckBox
|
||||
internal var mapToASCII by mapToASCIICheckBox
|
||||
internal var showSearchNotification by showSearchNotificationCheckBox
|
||||
internal var acceptedTagColor by acceptedTagColorWheel
|
||||
|
||||
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
|
||||
keyboardLayout = settings.layout
|
||||
cycleMode1 = settings.cycleMode1
|
||||
cycleMode2 = settings.cycleMode2
|
||||
cycleMode3 = settings.cycleMode3
|
||||
cycleMode4 = settings.cycleMode4
|
||||
minQueryLength = settings.minQueryLength.toString()
|
||||
jumpModeColor = settings.getJumpModeJBC()
|
||||
jumpEndModeColor = settings.getJumpEndModeJBC()
|
||||
targetModeColor = settings.getTargetModeJBC()
|
||||
definitionModeColor = settings.getDefinitionModeJBC()
|
||||
textHighlightColor = settings.getTextHighlightJBC()
|
||||
tagForegroundColor = settings.getTagForegroundJBC()
|
||||
tagBackgroundColor = settings.getTagBackgroundJBC()
|
||||
searchWholeFile = settings.searchWholeFile
|
||||
mapToASCII = settings.mapToASCII
|
||||
showSearchNotification = settings.showSearchNotification
|
||||
jumpModeColor = settings.jumpModeColor
|
||||
textHighlightColor = settings.textHighlightColor
|
||||
tagForegroundColor = settings.tagForegroundColor
|
||||
tagBackgroundColor = settings.tagBackgroundColor
|
||||
acceptedTagColor = settings.acceptedTagColor
|
||||
}
|
||||
|
||||
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
|
||||
|
||||
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.lowercase()
|
||||
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.toLowerCase()
|
||||
private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s)
|
||||
|
||||
private operator fun ResettableColorPicker.getValue(a: AceSettingsPanel, p: KProperty<*>) = getSelectedColor()
|
||||
private operator fun ResettableColorPicker.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c)
|
||||
private operator fun ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor
|
||||
private operator fun ColorPanel.setValue(a: AceSettingsPanel, p: KProperty<*>, c: Color?) = setSelectedColor(c)
|
||||
|
||||
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
|
||||
private operator fun JCheckBox.setValue(a: AceSettingsPanel, p: KProperty<*>, selected: Boolean) = setSelected(selected)
|
||||
|
||||
private inline operator fun <reified T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
|
||||
private operator fun <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) {
|
||||
|
14
src/main/kotlin/org/acejump/config/ColorConverter.kt
Normal file
14
src/main/kotlin/org/acejump/config/ColorConverter.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.acejump.config
|
||||
|
||||
import com.intellij.util.xmlb.Converter
|
||||
import java.awt.Color
|
||||
|
||||
internal class ColorConverter : Converter<Color>() {
|
||||
override fun toString(value: Color): String {
|
||||
return value.rgb.toString()
|
||||
}
|
||||
|
||||
override fun fromString(value: String): Color? {
|
||||
return value.toIntOrNull()?.let(::Color)
|
||||
}
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
package org.acejump.config
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.actionSystem.impl.ActionButton
|
||||
import com.intellij.ui.ColorPanel
|
||||
import com.intellij.ui.JBColor
|
||||
import java.awt.*
|
||||
import javax.swing.*
|
||||
|
||||
internal class ResettableColorPicker(private val defaultColor: JBColor) : JPanel(FlowLayout()) {
|
||||
private val resetAction = object : AnAction({ "Reset to Default" }, AllIcons.General.Reset) {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread {
|
||||
return ActionUpdateThread.EDT
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabled = colorPanel.selectedColor != defaultColor
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
setSelectedColor(defaultColor)
|
||||
}
|
||||
}
|
||||
|
||||
private val colorPanel = ColorPanel()
|
||||
private val resetButton = ActionButton(resetAction, null, ActionPlaces.UNKNOWN, ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE)
|
||||
|
||||
init {
|
||||
add(colorPanel)
|
||||
add(resetButton)
|
||||
setSelectedColor(defaultColor)
|
||||
|
||||
colorPanel.addActionListener {
|
||||
resetButton.update()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedColor(): Color? {
|
||||
return colorPanel.selectedColor
|
||||
}
|
||||
|
||||
fun setSelectedColor(color: Color?) {
|
||||
colorPanel.selectedColor = color
|
||||
resetButton.update()
|
||||
}
|
||||
}
|
@@ -6,9 +6,8 @@ import com.intellij.openapi.editor.actionSystem.TypedAction
|
||||
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
|
||||
|
||||
/**
|
||||
* If at least one session exists, this listener redirects all characters
|
||||
* typed in [Editor]s with attached sessions to the appropriate sessions'
|
||||
* own handlers.
|
||||
* If at least one session exists, this listener redirects all characters, typed in [Editor]s with attached sessions, to the appropriate
|
||||
* sessions' own handlers.
|
||||
*/
|
||||
internal object EditorKeyListener : TypedActionHandler {
|
||||
private val action = TypedAction.getInstance()
|
||||
|
@@ -1,79 +0,0 @@
|
||||
package org.acejump.input
|
||||
|
||||
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
|
||||
import org.acejump.config.AceConfig
|
||||
import java.awt.Color
|
||||
|
||||
/**
|
||||
* Describes modes that determine the behavior of a "jump" to a tag.
|
||||
* Most modes have two variations:
|
||||
* - **Default jump** happens when jumping without holding the Shift key
|
||||
* - **Shift jump** happens when jumping while holding the Shift key
|
||||
*/
|
||||
enum class JumpMode {
|
||||
/**
|
||||
* Default value at the start of a session. If the session does not get
|
||||
* assigned a proper [JumpMode] by the time the user requests a jump, the
|
||||
* results of the jump are undefined.
|
||||
*/
|
||||
DISABLED,
|
||||
|
||||
/**
|
||||
* On default jump, places the caret at the first character of the search
|
||||
* query. On shift jump, does the above but also selects all text between
|
||||
* the original and new caret positions.
|
||||
*/
|
||||
JUMP,
|
||||
|
||||
/**
|
||||
* On default jump, places the caret at the end of a word. Word detection
|
||||
* uses [Character.isJavaIdentifierPart] to count some special characters,
|
||||
* such as underscores, as part of a word. If there is no word at the first
|
||||
* character of the search query, then the caret is placed after the last
|
||||
* character of the search query.
|
||||
*
|
||||
* On shift jump, does the above but also selects all text between
|
||||
* the original and new caret positions.
|
||||
*/
|
||||
JUMP_END,
|
||||
|
||||
/**
|
||||
* On default jump, places the caret at the end of a word, and also selects
|
||||
* the entire word. Word detection uses [Character.isJavaIdentifierPart] to
|
||||
* count some special characters, such as underscores, as part of a word.
|
||||
* If there is no word at the first character of the search query, then the
|
||||
* caret is placed after the last character of the search query, and all
|
||||
* text between the start and end of the search query is selected.
|
||||
*
|
||||
* On shift jump, does the above but also selects all text between the
|
||||
* start of the original caret position and the end of the new selection.
|
||||
*/
|
||||
TARGET,
|
||||
|
||||
/**
|
||||
* On default jump, performs the Go To Declaration action, available
|
||||
* via `Navigate | Declaration or Usages`.
|
||||
*
|
||||
* On shift jump, performs the Go To Type Declaration action, available
|
||||
* via `Navigate | Type Declaration`.
|
||||
*
|
||||
* Always places the caret at the first character of the search query.
|
||||
*/
|
||||
DECLARATION;
|
||||
|
||||
val caretColor: Color get() = when (this) {
|
||||
DISABLED -> AbstractColorsScheme.INHERITED_COLOR_MARKER
|
||||
JUMP -> AceConfig.jumpModeColor
|
||||
JUMP_END -> AceConfig.jumpEndModeColor
|
||||
TARGET -> AceConfig.targetModeColor
|
||||
DECLARATION -> AceConfig.definitionModeColor
|
||||
}
|
||||
|
||||
override fun toString() = when (this) {
|
||||
DISABLED -> "(Skip)"
|
||||
JUMP -> "Jump"
|
||||
JUMP_END -> "Jump to End"
|
||||
TARGET -> "Target"
|
||||
DECLARATION -> "Definition"
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package org.acejump.input
|
||||
|
||||
import org.acejump.config.AceConfig
|
||||
|
||||
/**
|
||||
* Remembers the current [JumpMode] for a session. Allows cycling
|
||||
* [JumpMode]s according to the order defined in configuration, or
|
||||
* toggling one specific [JumpMode] on or off.
|
||||
*/
|
||||
internal class JumpModeTracker {
|
||||
private var currentMode = JumpMode.DISABLED
|
||||
private var currentIndex = 0
|
||||
|
||||
/**
|
||||
* Switches to the next/previous [JumpMode] defined in configuration,
|
||||
* skipping any [JumpMode]s that are not assigned. If at least two
|
||||
* [JumpMode]s are assigned in the cycle order, then cycling will
|
||||
* wrap around. If only one [JumpMode] is assigned, then cycling will
|
||||
* toggle that one mode.
|
||||
*/
|
||||
fun cycle(forward: Boolean): JumpMode {
|
||||
val cycleModes = AceConfig.cycleModes
|
||||
val direction = if (forward) 1 else -1
|
||||
val start = if (currentIndex == 0 && !forward) 0 else currentIndex - 1
|
||||
|
||||
for (offset in 1 until cycleModes.size) {
|
||||
val index = (start + cycleModes.size + (offset * direction)) % cycleModes.size
|
||||
|
||||
if (cycleModes[index] != JumpMode.DISABLED) {
|
||||
currentMode = cycleModes[index]
|
||||
currentIndex = index + 1
|
||||
return currentMode
|
||||
}
|
||||
}
|
||||
|
||||
currentMode = JumpMode.DISABLED
|
||||
currentIndex = 0
|
||||
return currentMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the specified [JumpMode]. If the current mode already
|
||||
* equals the specified one, it resets to [JumpMode.DISABLED].
|
||||
*/
|
||||
fun toggle(newMode: JumpMode): JumpMode {
|
||||
if (currentMode == newMode) {
|
||||
currentMode = JumpMode.DISABLED
|
||||
currentIndex = 0
|
||||
} else {
|
||||
currentMode = newMode
|
||||
currentIndex = AceConfig.cycleModes.indexOfFirst { it == newMode } + 1
|
||||
}
|
||||
|
||||
return currentMode
|
||||
}
|
||||
}
|
@@ -6,9 +6,8 @@ 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) {
|
||||
@@ -19,8 +18,7 @@ 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"),
|
||||
AZERTY(arrayOf("1234567890", "azertyuiop", "qsdfghjklm", "wxcvbn"), priority = "fjghdkslqvncmbxwrutyeizoap5849673210");
|
||||
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210");
|
||||
|
||||
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
|
||||
internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
|
||||
|
@@ -23,8 +23,11 @@ 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) else Unit
|
||||
fun ensureInitialized(settings: AceSettings) {
|
||||
if (!::tagOrder.isInitialized) {
|
||||
reset(settings)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-initializes cached data according to updated settings.
|
||||
@@ -43,8 +46,6 @@ 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)
|
||||
}
|
||||
}
|
||||
|
25
src/main/kotlin/org/acejump/modes/JumpMode.kt
Normal file
25
src/main/kotlin/org/acejump/modes/JumpMode.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package org.acejump.modes
|
||||
|
||||
import org.acejump.action.AceTagAction
|
||||
import org.acejump.config.AceConfig
|
||||
import org.acejump.search.Tag
|
||||
import org.acejump.session.SessionState
|
||||
import org.acejump.session.TypeResult
|
||||
|
||||
open class JumpMode : SessionMode {
|
||||
override val caretColor
|
||||
get() = AceConfig.jumpModeColor
|
||||
|
||||
protected var wasUpperCase = false
|
||||
private set
|
||||
|
||||
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
|
||||
wasUpperCase = charTyped.isUpperCase()
|
||||
return state.type(charTyped)
|
||||
}
|
||||
|
||||
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
|
||||
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
|
||||
return true
|
||||
}
|
||||
}
|
13
src/main/kotlin/org/acejump/modes/SessionMode.kt
Normal file
13
src/main/kotlin/org/acejump/modes/SessionMode.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.acejump.modes
|
||||
|
||||
import org.acejump.search.Tag
|
||||
import org.acejump.session.SessionState
|
||||
import org.acejump.session.TypeResult
|
||||
import java.awt.Color
|
||||
|
||||
interface SessionMode {
|
||||
val caretColor: Color
|
||||
|
||||
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult
|
||||
fun accept(state: SessionState, acceptedTag: Tag): Boolean
|
||||
}
|
@@ -1,9 +1,12 @@
|
||||
package org.acejump.search
|
||||
|
||||
enum class Pattern(val regex: String) {
|
||||
LINE_STARTS("^.|^\\n|(?<!.)\\Z"),
|
||||
LINE_STARTS("^.|^\\n"),
|
||||
LINE_ENDS("\\n|\\Z"),
|
||||
LINE_INDENTS("[^\\s].*|^\\n|(?<!.)\\Z"),
|
||||
LINE_ALL_MARKS(listOf(LINE_ENDS, LINE_STARTS, LINE_INDENTS).flatMap { it.regex.split("|") }.distinct().joinToString("|")),
|
||||
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]");
|
||||
LINE_INDENTS("[^\\s].*|^\\n"),
|
||||
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
|
||||
VIM_LWORD("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
|
||||
VIM_UWORD("(?<=\\s|\\A)[^\\s]"),
|
||||
VIM_LWORD_END("[a-zA-Z0-9_](?=[^a-zA-Z0-9_]|\\Z)"),
|
||||
VIM_UWORD_END("[^\\s](?=\\s|\\Z)")
|
||||
}
|
||||
|
@@ -8,23 +8,20 @@ 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 editors: List<Editor>,
|
||||
query: SearchQuery,
|
||||
results: MutableMap<Editor, IntArrayList>
|
||||
) {
|
||||
class SearchProcessor private constructor(query: SearchQuery, results: MutableMap<Editor, IntArrayList>) {
|
||||
companion object {
|
||||
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries) =
|
||||
SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
|
||||
|
||||
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries) =
|
||||
SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
|
||||
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries): SearchProcessor {
|
||||
return SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
|
||||
}
|
||||
|
||||
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) {
|
||||
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries): SearchProcessor {
|
||||
return SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
|
||||
val regex = query.toRegex()
|
||||
|
||||
if (regex != null) {
|
||||
@@ -35,9 +32,7 @@ internal class SearchProcessor private constructor(
|
||||
var result = regex.find(editor.immutableText, offsetRange.first)
|
||||
|
||||
while (result != null) {
|
||||
// For some reason regex matches can be out of bounds, but
|
||||
// boundary check prevents an exception.
|
||||
val index = result.range.first
|
||||
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception.
|
||||
val highlightEnd = index + query.getHighlightLength("", index)
|
||||
|
||||
if (highlightEnd > offsetRange.last) {
|
||||
@@ -55,38 +50,32 @@ internal class SearchProcessor private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
var query: SearchQuery = query
|
||||
internal var query = query
|
||||
private set
|
||||
|
||||
var results: MutableMap<Editor, IntArrayList> = results
|
||||
internal var results = 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 is invalid 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 does not
|
||||
* make sense because it would remove every result, the change is reverted and this function returns false.
|
||||
*/
|
||||
fun type(char: Char, tagger: Tagger): Boolean {
|
||||
val newQuery = query.rawText + char
|
||||
val canMatchTag = tagger.canQueryMatchAnyVisibleTag(newQuery)
|
||||
val canMatchTag = tagger.canQueryMatchAnyTag(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 && !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())
|
||||
@@ -107,7 +96,8 @@ internal class SearchProcessor private constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
removeObsoleteResults(newQuery, tagger)
|
||||
query = SearchQuery.Literal(newQuery)
|
||||
}
|
||||
@@ -130,8 +120,7 @@ internal class SearchProcessor private constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* After updating the query, removes all results that no longer match
|
||||
* the search query.
|
||||
* 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
|
||||
@@ -140,17 +129,16 @@ internal class SearchProcessor private constructor(
|
||||
|
||||
for ((editor, offsets) in results.entries.toList()) {
|
||||
val chars = editor.immutableText
|
||||
|
||||
val remaining = IntArrayList()
|
||||
val iter = offsets.iterator()
|
||||
|
||||
while (iter.hasNext()) {
|
||||
val offset = iter.nextInt()
|
||||
val endOffset = offset + lastCharOffset
|
||||
val lastTypedCharMatches = endOffset < chars.length &&
|
||||
chars[endOffset].equals(lastChar, ignoreCase)
|
||||
val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase)
|
||||
|
||||
if (lastTypedCharMatches ||
|
||||
tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
|
||||
if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
|
||||
remaining.add(offset)
|
||||
}
|
||||
}
|
||||
|
@@ -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 val rawText: String) : SearchQuery() {
|
||||
init {
|
||||
require(rawText.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun getHighlightLength(text: CharSequence, offset: Int): Int =
|
||||
text.countMatchingCharacters(offset, rawText)
|
||||
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
|
||||
return 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,12 +48,15 @@ internal sealed class SearchQuery {
|
||||
/**
|
||||
* Searches for all matches of a regular expression.
|
||||
*/
|
||||
class RegularExpression(private var pattern: String): SearchQuery() {
|
||||
class RegularExpression(private val pattern: String) : SearchQuery() {
|
||||
override val rawText = ""
|
||||
|
||||
override fun getHighlightLength(text: CharSequence, offset: Int) = 0
|
||||
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun toRegex(): Regex =
|
||||
Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
|
||||
override fun toRegex(): Regex {
|
||||
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,13 +4,13 @@ import com.intellij.openapi.editor.Editor
|
||||
import it.unimi.dsi.fastutil.ints.IntList
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
|
||||
import org.acejump.boundaries.EditorOffsetCache
|
||||
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
import org.acejump.boundaries.StandardBoundaries
|
||||
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 java.util.IdentityHashMap
|
||||
import kotlin.math.max
|
||||
|
||||
/*
|
||||
@@ -54,7 +54,7 @@ internal class Solver private constructor(
|
||||
private val editorPriority: List<Editor>,
|
||||
private val queryLength: Int,
|
||||
private val newResults: Map<Editor, IntList>,
|
||||
private val allResults: Map<Editor, IntList>
|
||||
private val allResults: Map<Editor, IntList>,
|
||||
) {
|
||||
companion object {
|
||||
fun solve(
|
||||
@@ -63,34 +63,37 @@ internal class Solver private constructor(
|
||||
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)
|
||||
caches: Map<Editor, EditorOffsetCache>,
|
||||
): Map<String, Tag> {
|
||||
return 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 {
|
||||
private var allWordFragments = HashSet<String>(allResults.values.sumBy(IntList::size)).apply {
|
||||
for ((editor, offsets) in allResults) {
|
||||
val chars = editor.immutableText
|
||||
val iter = offsets.iterator()
|
||||
while (iter.hasNext()) forEachWordFragment(editor, iter.nextInt()) { add(it) }
|
||||
while (iter.hasNext()) {
|
||||
forEachWordFragment(chars, iter.nextInt(), this::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun map(availableTags: List<String>, caches: Map<Editor, EditorOffsetCache>): Map<String, Tag> {
|
||||
private fun generateEligibleSites(availableTags: List<String>): Map<String, MutableList<Tag>> {
|
||||
val eligibleSitesByTag = HashMap<String, MutableList<Tag>>(100)
|
||||
val tagsByFirstLetter = availableTags.groupBy { it[0] }
|
||||
|
||||
for ((editor, offsets) in newResults) {
|
||||
val chars = editor.immutableText
|
||||
val iter = offsets.iterator()
|
||||
while (iter.hasNext()) {
|
||||
val site = iter.nextInt()
|
||||
|
||||
for ((firstLetter, tags) in tagsByFirstLetter.entries) {
|
||||
if (canTagBeginWithChar(editor, site, firstLetter)) {
|
||||
if (canTagBeginWithChar(chars, site, firstLetter)) {
|
||||
for (tag in tags) {
|
||||
eligibleSitesByTag.getOrPut(tag, ::mutableListOf).add(Tag(editor, site))
|
||||
}
|
||||
@@ -99,11 +102,32 @@ internal class Solver private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val matchingSites = HashMap<MutableList<Tag>, MutableList<Tag>>()
|
||||
// Keys are guaranteed to be from a single collection.
|
||||
val matchingSitesAsArrays = IdentityHashMap<String, MutableList<Tag>>()
|
||||
return eligibleSitesByTag
|
||||
}
|
||||
|
||||
private fun generateMatchingSites(
|
||||
eligibleSites: Map<String, MutableList<Tag>>,
|
||||
caches: Map<Editor, EditorOffsetCache>,
|
||||
): Map<String, Iterator<Tag>> {
|
||||
val matchingSites = HashMap<MutableList<Tag>, Iterator<Tag>>()
|
||||
val matchingSitesSorted = IdentityHashMap<String, Iterator<Tag>>() // Keys are guaranteed to be from a single collection.
|
||||
|
||||
val siteOrder = siteOrder(caches)
|
||||
|
||||
for ((mark, tags) in eligibleSites.entries) {
|
||||
matchingSitesSorted[mark] = matchingSites.getOrPut(tags) {
|
||||
@Suppress("ConvertLambdaToReference")
|
||||
tags.toMutableList().apply { sortWith(siteOrder) }.iterator()
|
||||
}
|
||||
}
|
||||
|
||||
return matchingSitesSorted
|
||||
}
|
||||
|
||||
fun map(availableTags: List<String>, caches: Map<Editor, EditorOffsetCache>): Map<String, Tag> {
|
||||
val eligibleSitesByTag = generateEligibleSites(availableTags)
|
||||
val matchingSitesSorted = generateMatchingSites(eligibleSitesByTag, caches)
|
||||
|
||||
val tagOrder = KeyLayoutCache.tagOrder
|
||||
.thenComparingInt { eligibleSitesByTag.getValue(it).size }
|
||||
.thenBy(AceConfig.layout.priority(String::last))
|
||||
@@ -112,21 +136,15 @@ internal class Solver private constructor(
|
||||
sortWith(tagOrder)
|
||||
}
|
||||
|
||||
for ((mark, tags) in eligibleSitesByTag.entries) {
|
||||
matchingSitesAsArrays[mark] = matchingSites.getOrPut(tags) {
|
||||
tags.toMutableList().apply { sortWith(siteOrder) }
|
||||
}
|
||||
}
|
||||
|
||||
var totalAssigned = 0
|
||||
val totalResults = newResults.values.sumOf(IntList::size)
|
||||
val totalResults = newResults.values.sumBy(IntList::size)
|
||||
|
||||
for (tag in sortedTags) {
|
||||
if (totalAssigned == totalResults) {
|
||||
break
|
||||
}
|
||||
|
||||
if (tryToAssignTag(tag, matchingSitesAsArrays.getValue(tag))) {
|
||||
if (tryToAssignTag(tag, matchingSitesSorted.getValue(tag))) {
|
||||
totalAssigned++
|
||||
}
|
||||
}
|
||||
@@ -134,15 +152,24 @@ internal class Solver private constructor(
|
||||
return newTags
|
||||
}
|
||||
|
||||
private fun tryToAssignTag(mark: String, tags: List<Tag>): Boolean {
|
||||
if (newTags.containsKey(mark)) return false
|
||||
private fun tryToAssignTag(mark: String, tags: Iterator<Tag>): Boolean {
|
||||
if (newTags.containsKey(mark)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val tag = tags.firstOrNull { it.offset !in newTagIndices.getValue(it.editor) } ?: return false
|
||||
@Suppress("ReplacePutWithAssignment")
|
||||
newTags.put(mark, tag)
|
||||
newTagIndices.getValue(tag.editor).add(tag.offset)
|
||||
while (tags.hasNext()) {
|
||||
val tag = tags.next()
|
||||
val assigned = newTagIndices.getValue(tag.editor)
|
||||
|
||||
if (tag.offset !in assigned) {
|
||||
newTags[mark] = tag
|
||||
assigned.add(tag.offset)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun siteOrder(caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b ->
|
||||
val aEditor = a.editor
|
||||
@@ -155,8 +182,8 @@ internal class Solver private constructor(
|
||||
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
|
||||
}
|
||||
|
||||
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, caches.getValue(aEditor))
|
||||
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, caches.getValue(bEditor))
|
||||
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, caches.getValue(aEditor))
|
||||
val bIsVisible = StandardBoundaries.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
|
||||
@@ -176,25 +203,28 @@ internal class Solver private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun canTagBeginWithChar(editor: Editor, site: Int, char: Char): Boolean {
|
||||
if (char.toString() in allWordFragments) return false
|
||||
private fun canTagBeginWithChar(chars: CharSequence, site: Int, char: Char): Boolean {
|
||||
if (char.toString() in allWordFragments) {
|
||||
return false
|
||||
}
|
||||
|
||||
forEachWordFragment(chars, site) {
|
||||
if (it + char in allWordFragments) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
forEachWordFragment(editor, site) { if (it + char in allWordFragments) return false }
|
||||
return true
|
||||
}
|
||||
|
||||
private inline fun forEachWordFragment(editor: Editor, site: Int, callback: (String) -> Unit) {
|
||||
val chars = editor.immutableText
|
||||
private inline fun forEachWordFragment(chars: CharSequence, site: Int, callback: (String) -> Unit) {
|
||||
val left = max(0, site + queryLength - 1)
|
||||
val right = chars.wordEndPlus(site)
|
||||
if (right >= chars.length) {
|
||||
return
|
||||
}
|
||||
|
||||
val builder = StringBuilder(1 + right - left)
|
||||
|
||||
for (i in left..right) {
|
||||
builder.append(chars[i].lowercase())
|
||||
builder.append(chars[i].toLowerCase())
|
||||
callback(builder.toString())
|
||||
}
|
||||
}
|
||||
|
@@ -2,4 +2,12 @@ package org.acejump.search
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
|
||||
data class Tag(val editor: Editor, val offset: Int)
|
||||
data class Tag(val editor: Editor, val offset: Int) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Tag && other.offset == offset && other.editor === editor
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return (offset * 31) + editor.hashCode()
|
||||
}
|
||||
}
|
||||
|
@@ -5,10 +5,13 @@ import com.google.common.collect.HashBiMap
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import it.unimi.dsi.fastutil.ints.IntList
|
||||
import org.acejump.*
|
||||
import org.acejump.ExternalUsage
|
||||
import org.acejump.boundaries.EditorOffsetCache
|
||||
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
import org.acejump.boundaries.StandardBoundaries
|
||||
import org.acejump.immutableText
|
||||
import org.acejump.input.KeyLayoutCache.allPossibleTags
|
||||
import org.acejump.isWordPart
|
||||
import org.acejump.matchesAt
|
||||
import org.acejump.view.TagMarker
|
||||
import java.util.AbstractMap.SimpleImmutableEntry
|
||||
import kotlin.collections.component1
|
||||
@@ -19,40 +22,40 @@ 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 editors: List<Editor>) {
|
||||
class Tagger(private val editors: List<Editor>) {
|
||||
private var tagMap = HashBiMap.create<String, Tag>()
|
||||
|
||||
val hasTags
|
||||
get() = tagMap.isNotEmpty()
|
||||
|
||||
@ExternalUsage
|
||||
val tags
|
||||
internal val tags
|
||||
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value.offset }
|
||||
|
||||
/**
|
||||
* Removes all markers, allowing them to be regenerated from scratch.
|
||||
*/
|
||||
fun unmark() {
|
||||
internal fun unmark() {
|
||||
tagMap = HashBiMap.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.Accept] if the current search
|
||||
* query matches any existing tag and we should jump to it and end the session, or [TaggingResult.Mark] to continue the session with
|
||||
* updated tag markers.
|
||||
*
|
||||
* Note that the [results] collection will be mutated.
|
||||
*/
|
||||
fun markOrJump(query: SearchQuery, results: Map<Editor, IntList>): TaggingResult {
|
||||
internal fun update(query: SearchQuery, results: Map<Editor, IntList>): TaggingResult {
|
||||
val isRegex = query is SearchQuery.RegularExpression
|
||||
val queryText = if (isRegex) " ${query.rawText}" else query.rawText[0] + query.rawText.drop(1).lowercase()
|
||||
val queryText = if (isRegex) " ${query.rawText}" else query.rawText[0] + query.rawText.drop(1).toLowerCase()
|
||||
|
||||
val availableTags = allPossibleTags.filter { !queryText.endsWith(it[0]) && it !in tagMap }
|
||||
|
||||
if (!isRegex) {
|
||||
for (entry in tagMap.entries) {
|
||||
if (entry solves queryText) {
|
||||
return TaggingResult.Jump(query = queryText.substringBefore(entry.key), mark = entry.key, tag = entry.value)
|
||||
return TaggingResult.Accept(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,22 +66,19 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRegex || tagMap.isEmpty())
|
||||
if (!isRegex || tagMap.isEmpty()) {
|
||||
tagMap = assignTagsAndMerge(results, availableTags, query, queryText)
|
||||
}
|
||||
|
||||
val resultTags = results.flatMap { (editor, offsets) -> offsets.map { Tag(editor, it) } }
|
||||
return TaggingResult.Mark(createTagMarkers(resultTags, query.rawText.ifEmpty { null }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns as many unassigned tags as possible, and merges them with
|
||||
* the existing compatible tags.
|
||||
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
|
||||
*/
|
||||
private fun assignTagsAndMerge(
|
||||
results: Map<Editor, IntList>,
|
||||
availableTags: List<String>,
|
||||
query: SearchQuery,
|
||||
queryText: String
|
||||
results: Map<Editor, IntList>, availableTags: List<String>, query: SearchQuery, queryText: String,
|
||||
): HashBiMap<String, Tag> {
|
||||
val caches = results.keys.associateWith { EditorOffsetCache.new() }
|
||||
|
||||
@@ -86,13 +86,14 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
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)
|
||||
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
|
||||
val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
|
||||
|
||||
when {
|
||||
aIsVisible && !bIsVisible -> -1
|
||||
bIsVisible && !aIsVisible -> 1
|
||||
else -> 0}
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +103,11 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
}
|
||||
|
||||
val vacantResults: Map<Editor, IntList>
|
||||
|
||||
if (oldCompatibleTags.isEmpty()) {
|
||||
vacantResults = results
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
val vacant = mutableMapOf<Editor, IntList>()
|
||||
|
||||
for ((editor, offsets) in results) {
|
||||
@@ -113,7 +116,6 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
|
||||
while (iter.hasNext()) {
|
||||
val tag = Tag(editor, iter.nextInt())
|
||||
|
||||
if (tag !in oldCompatibleTags.values) {
|
||||
list.add(tag.offset)
|
||||
}
|
||||
@@ -128,34 +130,33 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
allAssignedTags.putAll(oldCompatibleTags)
|
||||
allAssignedTags.putAll(Solver.solve(editors, query, vacantResults, results, availableTags, caches))
|
||||
|
||||
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
|
||||
// Avoid matching query - will trigger a jump.
|
||||
// TODO: lift this constraint.
|
||||
val queryEndsWith = queryText.endsWith(tag[0]) || queryText.endsWith(tag)
|
||||
val assignedMarkers = allAssignedTags.keys.groupBy { it[0] }
|
||||
|
||||
if (!queryEndsWith && canShortenTag(tag, allAssignedTags.keys))
|
||||
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
|
||||
if (canShortenTag(tag, assignedMarkers, queryText))
|
||||
tag[0].toString()
|
||||
else
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean =
|
||||
query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
|
||||
private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean {
|
||||
return 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)
|
||||
private fun isTagCompatibleWithQuery(marker: String, tag: Tag, query: String): Boolean {
|
||||
return 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 isQueryCompatibleWithTagAt(query: String, tag: Tag): Boolean {
|
||||
return 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.offset in tag.editor.getView()
|
||||
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(editor: Editor, offsets: IntList) {
|
||||
@@ -197,9 +198,9 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
else -> false
|
||||
}
|
||||
|
||||
private infix fun Char.isUnlike(other: Char) =
|
||||
this.isWordPart xor other.isWordPart ||
|
||||
this.isWhitespace() xor other.isWhitespace()
|
||||
private infix fun Char.isUnlike(other: Char): Boolean {
|
||||
return this.isWordPart xor other.isWordPart || this.isWhitespace() xor other.isWhitespace()
|
||||
}
|
||||
|
||||
private fun getPlaintextPortion(query: String, marker: String) = when {
|
||||
query.endsWith(marker, true) -> query.dropLast(marker.length)
|
||||
@@ -213,12 +214,16 @@ internal class Tagger(private val editors: List<Editor>) {
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private fun canShortenTag(marker: String, markers: Collection<String>): Boolean {
|
||||
for (other in markers)
|
||||
if (marker != other && marker[0] == other[0])
|
||||
private fun canShortenTag(marker: String, markers: Map<Char, List<String>>, queryText: String): Boolean {
|
||||
// Avoid matching query - will trigger a jump.
|
||||
// TODO: lift this constraint.
|
||||
val queryEndsWith = queryText.endsWith(marker[0]) || queryText.endsWith(marker)
|
||||
if (queryEndsWith) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
val startingWithSameLetter = markers[marker[0]]
|
||||
return startingWithSameLetter == null || startingWithSameLetter.singleOrNull() == marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package org.acejump.search
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import org.acejump.view.TagMarker
|
||||
|
||||
sealed class TaggingResult {
|
||||
class Jump(val query: String, val mark: String, val tag: Tag): TaggingResult()
|
||||
internal sealed class TaggingResult {
|
||||
class Accept(val tag: Tag) : TaggingResult()
|
||||
class Mark(val markers: MutableMap<Editor, Collection<TagMarker>>) : TaggingResult()
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
package org.acejump.session
|
||||
|
||||
interface AceJumpListener {
|
||||
fun finished(mark: String?, query: String?)
|
||||
}
|
@@ -3,14 +3,10 @@ package org.acejump.session
|
||||
import com.intellij.openapi.editor.Editor
|
||||
|
||||
/**
|
||||
* Holds [Editor] caret settings. The settings are saved the
|
||||
* moment a [Session] starts, modified to indicate AceJump
|
||||
* states, and restored once the [Session] ends.
|
||||
* Holds [Editor] caret settings. The settings are saved the moment a [Session] starts, modified to indicate AceJump states, and restored
|
||||
* once the [Session] ends.
|
||||
*/
|
||||
internal data class EditorSettings(
|
||||
private val isBlockCursor: Boolean,
|
||||
private val isBlinkCaret: Boolean,
|
||||
) {
|
||||
internal data class EditorSettings(private val isBlockCursor: Boolean, private val isBlinkCaret: Boolean, private val isReadOnly: Boolean) {
|
||||
companion object {
|
||||
fun setup(editor: Editor): EditorSettings {
|
||||
val settings = editor.settings
|
||||
@@ -19,20 +15,39 @@ internal data class EditorSettings(
|
||||
val original = EditorSettings(
|
||||
isBlockCursor = settings.isBlockCursor,
|
||||
isBlinkCaret = settings.isBlinkCaret,
|
||||
isReadOnly = !document.isWritable
|
||||
)
|
||||
|
||||
settings.isBlockCursor = true
|
||||
settings.isBlinkCaret = false
|
||||
document.setReadOnly(true)
|
||||
|
||||
return original
|
||||
}
|
||||
}
|
||||
|
||||
fun startEditing(editor: Editor) {
|
||||
editor.document.setReadOnly(isReadOnly)
|
||||
}
|
||||
|
||||
fun stopEditing(editor: Editor) {
|
||||
editor.document.setReadOnly(true)
|
||||
}
|
||||
|
||||
fun onTagAccepted(editor: Editor) = editor.let {
|
||||
it.settings.isBlockCursor = isBlockCursor
|
||||
}
|
||||
|
||||
fun onTagUnaccepted(editor: Editor) = editor.let {
|
||||
it.settings.isBlockCursor = true
|
||||
}
|
||||
|
||||
fun restore(editor: Editor) {
|
||||
val settings = editor.settings
|
||||
val document = editor.document
|
||||
|
||||
settings.isBlockCursor = isBlockCursor
|
||||
settings.isBlinkCaret = isBlinkCaret
|
||||
document.setReadOnly(isReadOnly)
|
||||
}
|
||||
}
|
||||
|
@@ -1,75 +1,45 @@
|
||||
package org.acejump.session
|
||||
|
||||
import com.intellij.codeInsight.hint.HintManagerImpl
|
||||
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.CARET_COLOR
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import org.acejump.*
|
||||
import org.acejump.action.TagScroller
|
||||
import org.acejump.action.TagJumper
|
||||
import org.acejump.action.TagVisitor
|
||||
import com.intellij.openapi.editor.colors.EditorColors
|
||||
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
|
||||
import org.acejump.ExternalUsage
|
||||
import org.acejump.boundaries.Boundaries
|
||||
import org.acejump.boundaries.EditorOffsetCache
|
||||
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
|
||||
import org.acejump.boundaries.StandardBoundaries
|
||||
import org.acejump.clone
|
||||
import org.acejump.config.AceConfig
|
||||
import org.acejump.input.EditorKeyListener
|
||||
import org.acejump.input.JumpMode
|
||||
import org.acejump.input.JumpModeTracker
|
||||
import org.acejump.input.KeyLayoutCache
|
||||
import org.acejump.search.Pattern
|
||||
import org.acejump.search.SearchProcessor
|
||||
import org.acejump.search.Tagger
|
||||
import org.acejump.search.TaggingResult
|
||||
import org.acejump.modes.JumpMode
|
||||
import org.acejump.modes.SessionMode
|
||||
import org.acejump.search.*
|
||||
import org.acejump.view.TagCanvas
|
||||
import org.acejump.view.TextHighlighter
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Manages an AceJump session for one or more [Editor]s.
|
||||
* Manages an AceJump session for a single [Editor].
|
||||
*/
|
||||
class Session(private val mainEditor: Editor, private val jumpEditors: List<Editor>) {
|
||||
private val listeners: MutableList<AceJumpListener> =
|
||||
ContainerUtil.createLockFreeCopyOnWriteList()
|
||||
private val editorSettings = EditorSettings.setup(mainEditor)
|
||||
private lateinit var mode: SessionMode
|
||||
|
||||
private var boundaries: Boundaries = defaultBoundaries
|
||||
private var state: SessionStateImpl? = null
|
||||
private var tagger = Tagger(jumpEditors)
|
||||
|
||||
private companion object {
|
||||
private val defaultBoundaries
|
||||
get() = if (AceConfig.searchWholeFile) WHOLE_FILE else VISIBLE_ON_SCREEN
|
||||
}
|
||||
|
||||
private val originalSettings = EditorSettings.setup(mainEditor)
|
||||
|
||||
private val jumpModeTracker = JumpModeTracker()
|
||||
private var jumpMode = JumpMode.DISABLED
|
||||
private var acceptedTag: Tag? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
if (value === JumpMode.DISABLED) {
|
||||
end()
|
||||
} else {
|
||||
searchProcessor?.let { textHighlighter.render(it.results, it.query, jumpMode) }
|
||||
mainEditor.colorsScheme.setColor(CARET_COLOR, value.caretColor)
|
||||
mainEditor.contentComponent.repaint()
|
||||
if (value != null) {
|
||||
tagCanvases.values.forEach(TagCanvas::removeMarkers)
|
||||
editorSettings.onTagAccepted(mainEditor)
|
||||
}
|
||||
}
|
||||
|
||||
private var searchProcessor: SearchProcessor? = null
|
||||
private var tagger = Tagger(jumpEditors)
|
||||
|
||||
private val tagJumper
|
||||
get() = TagJumper(jumpMode, searchProcessor)
|
||||
|
||||
private val tagVisitor
|
||||
get() = searchProcessor?.let { TagVisitor(mainEditor, it, tagJumper) }
|
||||
|
||||
private val tagScroller
|
||||
get() = searchProcessor?.let { TagScroller(mainEditor, it) }
|
||||
|
||||
private val textHighlighter = TextHighlighter()
|
||||
private val tagCanvases = jumpEditors.associateWith(::TagCanvas)
|
||||
|
||||
@@ -77,230 +47,156 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
|
||||
val tags
|
||||
get() = tagger.tags
|
||||
|
||||
var defaultBoundary: Boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
|
||||
init {
|
||||
KeyLayoutCache.ensureInitialized(AceConfig.settings)
|
||||
|
||||
EditorKeyListener.attach(mainEditor, object : TypedActionHandler {
|
||||
override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
|
||||
var processor = searchProcessor
|
||||
val state = state ?: return
|
||||
val hadTags = tagger.hasTags
|
||||
|
||||
if (processor == null) {
|
||||
processor = SearchProcessor.fromChar(
|
||||
jumpEditors, charTyped, boundaries
|
||||
).also { searchProcessor = it }
|
||||
} else if (!processor.type(charTyped, tagger)) {
|
||||
return
|
||||
editorSettings.startEditing(editor)
|
||||
val result = mode.type(state, charTyped, acceptedTag)
|
||||
editorSettings.stopEditing(editor)
|
||||
|
||||
when (result) {
|
||||
TypeResult.Nothing -> return;
|
||||
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
|
||||
is TypeResult.ChangeMode -> setMode(result.mode)
|
||||
|
||||
TypeResult.RestartSearch -> restart().also {
|
||||
this@Session.state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
|
||||
}
|
||||
|
||||
updateSearch(
|
||||
processor, markImmediately = hadTags,
|
||||
shiftMode = charTyped.isUpperCase()
|
||||
)
|
||||
TypeResult.EndSession -> end()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag.
|
||||
*/
|
||||
private fun updateSearch(
|
||||
processor: SearchProcessor,
|
||||
markImmediately: Boolean,
|
||||
shiftMode: Boolean = false
|
||||
) {
|
||||
private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean) {
|
||||
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) }) {
|
||||
textHighlighter.renderOccurrences(results, query)
|
||||
return
|
||||
}
|
||||
|
||||
when (val result = tagger.markOrJump(query, results.clone())) {
|
||||
is TaggingResult.Jump -> {
|
||||
tagJumper.jump(result.tag, shiftMode, isCrossEditor = mainEditor !== result.tag.editor)
|
||||
tagCanvases.values.forEach(TagCanvas::removeMarkers)
|
||||
end(result)
|
||||
when (val result = tagger.update(query, results.clone())) {
|
||||
is TaggingResult.Accept -> {
|
||||
acceptedTag = result.tag
|
||||
textHighlighter.renderFinal(result.tag, processor.query)
|
||||
|
||||
if (state?.let { mode.accept(it, result.tag) } == true) {
|
||||
end()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
is TaggingResult.Mark -> {
|
||||
val markers = result.markers
|
||||
|
||||
for ((editor, canvas) in tagCanvases) {
|
||||
canvas.setMarkers(markers[editor].orEmpty())
|
||||
canvas.setMarkers(result.markers[editor].orEmpty())
|
||||
}
|
||||
|
||||
if (jumpEditors.all { editor ->
|
||||
val cache = EditorOffsetCache.new()
|
||||
markers[editor].let { it == null || it.none { marker ->
|
||||
VISIBLE_ON_SCREEN.isOffsetInside(editor, marker.offsetL, cache) ||
|
||||
VISIBLE_ON_SCREEN.isOffsetInside(editor, marker.offsetR, cache) } }
|
||||
}) {
|
||||
tagVisitor?.scrollToClosest()
|
||||
}
|
||||
textHighlighter.renderOccurrences(results, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExternalUsage
|
||||
fun markResults(resultsToMark: SortedSet<Int>) {
|
||||
val jumpEditor = jumpEditors.singleOrNull() ?: return
|
||||
markResults(mapOf(jumpEditor to resultsToMark))
|
||||
private fun setMode(mode: SessionMode) {
|
||||
this.mode = mode
|
||||
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
|
||||
}
|
||||
|
||||
fun startJumpMode() {
|
||||
startJumpMode(::JumpMode)
|
||||
}
|
||||
|
||||
fun startJumpMode(mode: () -> JumpMode) {
|
||||
if (this::mode.isInitialized && mode is JumpMode) {
|
||||
end()
|
||||
return
|
||||
}
|
||||
|
||||
if (this::mode.isInitialized) {
|
||||
restart()
|
||||
}
|
||||
|
||||
setMode(mode())
|
||||
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
|
||||
*/
|
||||
fun startRegexSearch(pattern: Pattern) {
|
||||
if (!this::mode.isInitialized) {
|
||||
setMode(JumpMode())
|
||||
}
|
||||
|
||||
@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(jumpEditors, pattern.regex, defaultBoundary)
|
||||
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary, processor)
|
||||
|
||||
updateSearch(processor, markImmediately = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a regular expression search. If a search was already active,
|
||||
* it will be reset alongside its tags and highlights.
|
||||
*/
|
||||
fun tagImmediately() {
|
||||
val state = state ?: return
|
||||
val processor = state.currentProcessor
|
||||
|
||||
@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 }
|
||||
if (processor != null) {
|
||||
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].
|
||||
*/
|
||||
fun cycleNextJumpMode() {
|
||||
jumpMode = jumpModeTracker.cycle(forward = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [JumpModeTracker.cycle].
|
||||
*/
|
||||
fun cyclePreviousJumpMode() {
|
||||
jumpMode = jumpModeTracker.cycle(forward = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [JumpModeTracker.toggle]
|
||||
*/
|
||||
fun toggleJumpMode(newMode: JumpMode) {
|
||||
jumpMode = jumpModeTracker.toggle(newMode)
|
||||
}
|
||||
|
||||
@ExternalUsage
|
||||
fun toggleJumpMode(newMode: JumpMode, boundaries: Boundaries) {
|
||||
this.boundaries = this.boundaries.intersection(boundaries)
|
||||
jumpMode = jumpModeTracker.toggle(newMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [TagVisitor.visitPrevious]. If there are no tags, nothing happens.
|
||||
*/
|
||||
fun visitPreviousTag() =
|
||||
if (tagVisitor?.visitPrevious() == true) end() else Unit
|
||||
|
||||
/**
|
||||
* See [TagVisitor.visitNext]. If there are no tags, nothing happens.
|
||||
*/
|
||||
fun visitNextTag() =
|
||||
if (tagVisitor?.visitNext() == true) end() else Unit
|
||||
|
||||
/**
|
||||
* See [TagVisitor.visitPrevious]. If there are no tags, nothing happens.
|
||||
*/
|
||||
fun scrollToNextScreenful() = tagScroller?.scroll(true)
|
||||
|
||||
/**
|
||||
* See [TagVisitor.visitNext]. If there are no tags, nothing happens.
|
||||
*/
|
||||
fun scrollToPreviousScreenful() = tagScroller?.scroll(false)
|
||||
|
||||
/**
|
||||
* Ends this session.
|
||||
*/
|
||||
fun end(taggingResult: TaggingResult? = null) =
|
||||
SessionManager.end(mainEditor, taggingResult)
|
||||
fun end() {
|
||||
SessionManager.end(mainEditor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any currently active search, tags, and highlights.
|
||||
* Does not reset [JumpMode].
|
||||
*/
|
||||
fun restart() {
|
||||
state = null
|
||||
tagger = Tagger(jumpEditors)
|
||||
searchProcessor = null
|
||||
acceptedTag = null
|
||||
tagCanvases.values.forEach(TagCanvas::removeMarkers)
|
||||
textHighlighter.reset()
|
||||
|
||||
HintManagerImpl.getInstanceImpl().hideAllHints()
|
||||
editorSettings.onTagUnaccepted(mainEditor)
|
||||
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
|
||||
jumpEditors.forEach { it.contentComponent.repaint() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be used from [SessionManager] to dispose a
|
||||
* successfully ended session.
|
||||
* Should only be used from [SessionManager] to dispose a successfully ended session.
|
||||
*/
|
||||
internal fun dispose(taggingResult: TaggingResult?) {
|
||||
internal fun dispose() {
|
||||
tagger = Tagger(jumpEditors)
|
||||
EditorKeyListener.detach(mainEditor)
|
||||
tagCanvases.values.forEach(TagCanvas::unbind)
|
||||
textHighlighter.reset()
|
||||
EditorsCache.invalidate()
|
||||
|
||||
val jumpResult = taggingResult as? TaggingResult.Jump
|
||||
val mark = jumpResult?.mark
|
||||
val query = jumpResult?.query
|
||||
listeners.forEach { it.finished(mark, query) }
|
||||
EditorKeyListener.detach(mainEditor)
|
||||
|
||||
if (!mainEditor.isDisposed) {
|
||||
originalSettings.restore(mainEditor)
|
||||
mainEditor.colorsScheme.setColor(CARET_COLOR, JumpMode.DISABLED.caretColor)
|
||||
}
|
||||
HintManagerImpl.getInstanceImpl().hideAllHints()
|
||||
editorSettings.restore(mainEditor)
|
||||
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
|
||||
|
||||
val focusedEditor = jumpResult?.tag?.editor ?: mainEditor
|
||||
if (!focusedEditor.isDisposed) {
|
||||
val focusedEditor = acceptedTag?.editor ?: mainEditor
|
||||
focusedEditor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
|
||||
}
|
||||
}
|
||||
|
||||
@ExternalUsage
|
||||
fun addAceJumpListener(listener: AceJumpListener) {
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
@ExternalUsage
|
||||
fun removeAceJumpListener(listener: AceJumpListener) {
|
||||
listeners -= listener
|
||||
}
|
||||
}
|
||||
|
@@ -1,53 +1,50 @@
|
||||
package org.acejump.session
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import org.acejump.ExternalUsage
|
||||
import org.acejump.search.TaggingResult
|
||||
|
||||
/**
|
||||
* Manages active [Session]s in [Editor]s. There may only be
|
||||
* one [Session] per [Editor], but multiple [Session]s across
|
||||
* multiple [Editor]s may be active at once.
|
||||
* Manages active [Session]s in [Editor]s. There may only be one [Session] per [Editor], but multiple [Session]s across multiple [Editor]s
|
||||
* may be active at once.
|
||||
*
|
||||
* It is possible for an [Editor] to be disposed with an active
|
||||
* [Session]. In such case, the reference to both will remain
|
||||
* until a new [Session] starts, at which point the
|
||||
* [SessionManager.cleanup] method will purge disposed [Editor]s.
|
||||
* It is possible for an [Editor] to be disposed with an active [Session]. In such case, the reference to both will remain until a new
|
||||
* [Session] starts, at which point the [SessionManager.cleanup] method will purge disposed [Editor]s.
|
||||
*/
|
||||
|
||||
@ExternalUsage
|
||||
object SessionManager {
|
||||
private val sessions = HashMap<Editor, Session>(4)
|
||||
|
||||
/**
|
||||
* Starts a new [Session], or returns an existing [Session]
|
||||
* if the specified [Editor] already has one.
|
||||
* Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
|
||||
*/
|
||||
fun start(editor: Editor): Session = start(editor, listOf(editor))
|
||||
fun start(editor: Editor): Session {
|
||||
return start(editor, listOf(editor))
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new multi-editor [Session], or returns an existing [Session] if the specified main [Editor] already has one.
|
||||
* The [mainEditor] is used for typing the search query and tag.
|
||||
* The [jumpEditors] are all editors that will be searched and tagged. The list is ordered so that editors earlier in the list will be
|
||||
* prioritized for tagging in case of conflicts.
|
||||
* The [jumpEditors] are all editors that will be searched and tagged.
|
||||
*/
|
||||
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
|
||||
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active [Session] in the specified [Editor],
|
||||
* or null if the [Editor] has no active session.
|
||||
* Returns the active [Session] in the specified [Editor], or null if the [Editor] has no active session.
|
||||
*/
|
||||
operator fun get(editor: Editor): Session? = sessions[editor]
|
||||
operator fun get(editor: Editor): Session? {
|
||||
return sessions[editor]
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the active [Session] in the specified [Editor],
|
||||
* or does nothing if the [Editor] has no active session.
|
||||
* Ends the active [Session] in the specified [Editor], or does nothing if the [Editor] has no active session.
|
||||
*/
|
||||
fun end(editor: Editor, taggingResult: TaggingResult?) =
|
||||
sessions.remove(editor)?.dispose(taggingResult) ?: Unit
|
||||
|
||||
private fun cleanup() = sessions.keys.filter { it.isDisposed }
|
||||
.forEach { disposedEditor -> sessions.remove(disposedEditor)?.dispose(null) }
|
||||
fun end(editor: Editor) {
|
||||
sessions.remove(editor)?.dispose()
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
for (disposedEditor in sessions.keys.filter { it.isDisposed }) {
|
||||
sessions.remove(disposedEditor)?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
src/main/kotlin/org/acejump/session/SessionState.kt
Normal file
9
src/main/kotlin/org/acejump/session/SessionState.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package org.acejump.session
|
||||
|
||||
import org.acejump.action.AceTagAction
|
||||
import org.acejump.search.Tag
|
||||
|
||||
interface SessionState {
|
||||
fun type(char: Char): TypeResult
|
||||
fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean)
|
||||
}
|
36
src/main/kotlin/org/acejump/session/SessionStateImpl.kt
Normal file
36
src/main/kotlin/org/acejump/session/SessionStateImpl.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package org.acejump.session
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import org.acejump.action.AceTagAction
|
||||
import org.acejump.boundaries.Boundaries
|
||||
import org.acejump.search.SearchProcessor
|
||||
import org.acejump.search.Tag
|
||||
import org.acejump.search.Tagger
|
||||
|
||||
internal class SessionStateImpl(
|
||||
private val jumpEditors: List<Editor>,
|
||||
private val tagger: Tagger,
|
||||
private val defaultBoundary: Boundaries,
|
||||
processor: SearchProcessor? = null
|
||||
) : SessionState {
|
||||
internal var currentProcessor: SearchProcessor? = processor
|
||||
|
||||
override fun type(char: Char): TypeResult {
|
||||
val processor = currentProcessor
|
||||
|
||||
if (processor == null) {
|
||||
val newProcessor = SearchProcessor.fromChar(jumpEditors, char, defaultBoundary)
|
||||
return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it })
|
||||
}
|
||||
|
||||
if (processor.type(char, tagger)) {
|
||||
return TypeResult.UpdateResults(processor)
|
||||
}
|
||||
|
||||
return TypeResult.Nothing
|
||||
}
|
||||
|
||||
override fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean) {
|
||||
currentProcessor?.let { action(tag.editor, it, tag.offset, shiftMode, isFinal) }
|
||||
}
|
||||
}
|
12
src/main/kotlin/org/acejump/session/TypeResult.kt
Normal file
12
src/main/kotlin/org/acejump/session/TypeResult.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package org.acejump.session
|
||||
|
||||
import org.acejump.modes.SessionMode
|
||||
import org.acejump.search.SearchProcessor
|
||||
|
||||
sealed class TypeResult {
|
||||
object Nothing : TypeResult()
|
||||
class UpdateResults(val processor: SearchProcessor) : TypeResult()
|
||||
class ChangeMode(val mode: SessionMode) : TypeResult()
|
||||
object RestartSearch : TypeResult()
|
||||
object EndSession : TypeResult()
|
||||
}
|
@@ -5,7 +5,7 @@ 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.VISIBLE_ON_SCREEN
|
||||
import org.acejump.boundaries.StandardBoundaries
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Rectangle
|
||||
@@ -18,14 +18,15 @@ import javax.swing.SwingUtilities
|
||||
*/
|
||||
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)
|
||||
}
|
||||
@@ -37,11 +38,12 @@ internal class TagCanvas(private val editor: Editor): JComponent(), CaretListene
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: Collection<TagMarker>) {
|
||||
this.markers = markers
|
||||
@@ -49,11 +51,14 @@ internal class TagCanvas(private val editor: Editor): JComponent(), CaretListene
|
||||
}
|
||||
|
||||
fun removeMarkers() {
|
||||
markers = emptyList()
|
||||
this.markers = emptyList()
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) =
|
||||
if (!markers.isNullOrEmpty()) super.paint(g) else Unit
|
||||
override fun paint(g: Graphics) {
|
||||
if (!markers.isNullOrEmpty()) {
|
||||
super.paint(g)
|
||||
}
|
||||
}
|
||||
|
||||
override fun paintChildren(g: Graphics) {
|
||||
super.paintChildren(g)
|
||||
@@ -65,24 +70,23 @@ internal class TagCanvas(private val editor: Editor): JComponent(), CaretListene
|
||||
val font = TagFont(editor)
|
||||
|
||||
val cache = EditorOffsetCache.new()
|
||||
val viewRange = VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
|
||||
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
|
||||
val occupied = mutableListOf<Rectangle>()
|
||||
|
||||
// If there is a tag at the caret location, prioritize its rendering over
|
||||
// all other tags. This is helpful for seeing which tag is currently
|
||||
// selected while navigating highly clustered tags, although it does end
|
||||
// up rearranging nearby tags which can be confusing.
|
||||
// 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
|
||||
// TODO instead of immediately painting, we could calculate the layout of everything first, and then remove tags that interfere with
|
||||
// the caret tag to avoid changing the alignment of the caret tag
|
||||
|
||||
val caretOffset = editor.caretModel.offset
|
||||
val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset }
|
||||
caretMarker?.paint(g, editor, cache, font, occupied)
|
||||
|
||||
for (marker in markers)
|
||||
if (marker.isOffsetInRange(viewRange) && marker !== caretMarker)
|
||||
for (marker in markers) {
|
||||
if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
|
||||
marker.paint(g, editor, cache, font, occupied)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,24 @@
|
||||
package org.acejump.view
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.colors.EditorFontType.BOLD
|
||||
import com.intellij.openapi.editor.colors.EditorFontType.PLAIN
|
||||
import com.intellij.openapi.editor.colors.EditorFontType
|
||||
import com.intellij.ui.ColorUtil
|
||||
import org.acejump.config.AceConfig
|
||||
import java.awt.Font
|
||||
import java.awt.FontMetrics
|
||||
|
||||
/**
|
||||
* Stores font metrics for aligning and rendering [TagMarker]s.
|
||||
*/
|
||||
class TagFont(editor: Editor) {
|
||||
val tagFont: Font = editor.colorsScheme.getFont(BOLD)
|
||||
internal class TagFont(editor: Editor) {
|
||||
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.BOLD)
|
||||
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W')
|
||||
|
||||
val editorFontMetrics: FontMetrics =
|
||||
editor.component.getFontMetrics(editor.colorsScheme.getFont(PLAIN))
|
||||
val foregroundColor = AceConfig.tagForegroundColor
|
||||
var backgroundColor = AceConfig.tagBackgroundColor
|
||||
val isForegroundDark = ColorUtil.isDark(foregroundColor)
|
||||
|
||||
val editorFontMetrics: FontMetrics = editor.component.getFontMetrics(editor.colorsScheme.getFont(EditorFontType.PLAIN))
|
||||
val lineHeight = editor.lineHeight
|
||||
val baselineDistance = editor.ascent
|
||||
}
|
||||
|
@@ -1,12 +1,10 @@
|
||||
package org.acejump.view
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.ui.ColorUtil
|
||||
import com.intellij.ui.JreHiDpiUtil
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.ui.scale.JBUIScale
|
||||
import org.acejump.boundaries.EditorOffsetCache
|
||||
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
import org.acejump.config.AceConfig
|
||||
import org.acejump.boundaries.StandardBoundaries
|
||||
import org.acejump.countMatchingCharacters
|
||||
import org.acejump.immutableText
|
||||
import java.awt.Color
|
||||
@@ -18,7 +16,7 @@ import kotlin.math.max
|
||||
/**
|
||||
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
|
||||
*/
|
||||
class TagMarker(
|
||||
internal class TagMarker(
|
||||
private val tag: String,
|
||||
val offsetL: Int,
|
||||
val offsetR: Int,
|
||||
@@ -30,6 +28,13 @@ class TagMarker(
|
||||
companion object {
|
||||
private const val ARC = 1
|
||||
|
||||
/**
|
||||
* TODO This might be due to DPI settings.
|
||||
*/
|
||||
private val HIGHLIGHT_OFFSET = if (SystemInfo.isMac) -0.5 else 0.0
|
||||
|
||||
private val SHADOW_COLOR = Color(0F, 0F, 0F, 0.35F)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -40,9 +45,9 @@ class TagMarker(
|
||||
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
|
||||
|
||||
val displayedTag = if (literalQueryText != null && literalQueryText.last().equals(tag.first(), ignoreCase = true))
|
||||
tag.drop(1).uppercase()
|
||||
tag.drop(1).toUpperCase()
|
||||
else
|
||||
tag.uppercase()
|
||||
tag.toUpperCase()
|
||||
|
||||
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
|
||||
}
|
||||
@@ -52,16 +57,9 @@ class TagMarker(
|
||||
*/
|
||||
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.translate(0.0, HIGHLIGHT_OFFSET)
|
||||
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)
|
||||
}
|
||||
g.translate(0.0, -HIGHLIGHT_OFFSET)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,25 +71,23 @@ class TagMarker(
|
||||
|
||||
g.font = font.tagFont
|
||||
|
||||
if (!ColorUtil.isDark(AceConfig.tagForegroundColor)) {
|
||||
g.color = Color(0F, 0F, 0F, 0.35F)
|
||||
if (!font.isForegroundDark) {
|
||||
g.color = SHADOW_COLOR
|
||||
g.drawString(text, x + 1, y + 1)
|
||||
}
|
||||
|
||||
g.color = AceConfig.tagForegroundColor
|
||||
g.color = font.foregroundColor
|
||||
g.drawString(text, x, y)
|
||||
}
|
||||
|
||||
private fun isLineEnding(char: Char): Boolean {
|
||||
return char == '\n' || char == '\r'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the left-aligned offset is in the range. Use to cull tags outside visible range.
|
||||
* Only the left offset is checked, because if the tag was right-aligned on the last index of the range, it would not be visible anyway.
|
||||
*/
|
||||
fun isOffsetInRange(range: IntRange): Boolean = offsetL in range
|
||||
fun isOffsetInRange(range: IntRange): Boolean {
|
||||
return offsetL in range
|
||||
}
|
||||
|
||||
/**
|
||||
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
|
||||
@@ -100,7 +96,7 @@ class TagMarker(
|
||||
fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? {
|
||||
val rect = alignTag(editor, cache, font, occupied) ?: return null
|
||||
|
||||
drawHighlight(g, rect, AceConfig.tagBackgroundColor)
|
||||
drawHighlight(g, rect, font.backgroundColor)
|
||||
drawForeground(g, font, rect.location, tag)
|
||||
|
||||
occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) })
|
||||
@@ -108,35 +104,29 @@ class TagMarker(
|
||||
}
|
||||
|
||||
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
|
||||
val boundaries = VISIBLE_ON_SCREEN
|
||||
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
|
||||
|
||||
if (hasSpaceRight || offsetL !in 1 until editor.document.textLength || isLineEnding(editor.immutableText[offsetL - 1])) {
|
||||
if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) {
|
||||
val rectR = createRightAlignedTagRect(editor, cache, font)
|
||||
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 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)
|
||||
val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR)
|
||||
return Rectangle(pos.x + shift, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
|
||||
}
|
||||
|
||||
|
@@ -1,57 +1,45 @@
|
||||
package org.acejump.view
|
||||
|
||||
import com.intellij.codeInsight.CodeInsightBundle
|
||||
import com.intellij.codeInsight.hint.*
|
||||
import com.intellij.codeInsight.hint.HintManagerImpl.HIDE_BY_ESCAPE
|
||||
import com.intellij.codeInsight.hint.HintManagerImpl.HIDE_BY_TEXT_CHANGE
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.colors.EditorFontType
|
||||
import com.intellij.openapi.editor.markup.*
|
||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea.EXACT_RANGE
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.util.DocumentUtil
|
||||
import com.intellij.util.ui.*
|
||||
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
|
||||
import com.intellij.openapi.editor.markup.HighlighterLayer
|
||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea
|
||||
import com.intellij.openapi.editor.markup.RangeHighlighter
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import it.unimi.dsi.fastutil.ints.IntList
|
||||
import org.acejump.*
|
||||
import org.acejump.boundaries.EditorOffsetCache
|
||||
import org.acejump.config.AceConfig
|
||||
import org.acejump.input.JumpMode
|
||||
import org.acejump.immutableText
|
||||
import org.acejump.search.SearchQuery
|
||||
import java.awt.*
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
import org.acejump.search.Tag
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics
|
||||
|
||||
/**
|
||||
* Renders highlights for search occurrences.
|
||||
*/
|
||||
internal class TextHighlighter {
|
||||
private companion object { private const val LAYER = HighlighterLayer.LAST + 1 }
|
||||
private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>()
|
||||
private var previousHint: LightweightHint? = null
|
||||
|
||||
/**
|
||||
* Label for the search notification.
|
||||
* Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change.
|
||||
*/
|
||||
private class NotificationLabel(text: String?): JLabel(text) {
|
||||
init {
|
||||
background = HintUtil.getInformationColor()
|
||||
foreground = JBColor.foreground()
|
||||
this.isOpaque = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all current highlights and re-creates them from scratch.
|
||||
* Must be called whenever any of the method parameters change.
|
||||
*/
|
||||
fun render(results: Map<Editor, IntList>, query: SearchQuery, jumpMode: JumpMode) {
|
||||
|
||||
val renderer = when {
|
||||
query is SearchQuery.RegularExpression -> RegexRenderer
|
||||
jumpMode === JumpMode.TARGET -> SearchedWordWithOutlineRenderer
|
||||
fun renderOccurrences(results: Map<Editor, IntList>, query: SearchQuery) {
|
||||
render(results, when (query) {
|
||||
is SearchQuery.RegularExpression -> RegexRenderer
|
||||
else -> SearchedWordRenderer
|
||||
}, query::getHighlightLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
|
||||
*/
|
||||
fun renderFinal(tag: Tag, query: SearchQuery) {
|
||||
render(mutableMapOf(tag.editor to IntArrayList(intArrayOf(tag.offset))), AcceptedTagRenderer, query::getHighlightLength)
|
||||
}
|
||||
|
||||
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
|
||||
for ((editor, offsets) in results) {
|
||||
val highlights = previousHighlights[editor]
|
||||
|
||||
@@ -62,161 +50,88 @@ internal class TextHighlighter {
|
||||
val modifications = (highlights?.size ?: 0) + offsets.size
|
||||
val enableBulkEditing = modifications > 1000
|
||||
|
||||
DocumentUtil.executeInBulk(document, enableBulkEditing) {
|
||||
try {
|
||||
if (enableBulkEditing) {
|
||||
document.isInBulkUpdate = true
|
||||
}
|
||||
|
||||
highlights?.forEach(markup::removeHighlighter)
|
||||
previousHighlights[editor] = Array(offsets.size) { index ->
|
||||
val start = offsets.getInt(index)
|
||||
val end = start + query.getHighlightLength(chars, start)
|
||||
val end = start + getHighlightLength(chars, start)
|
||||
|
||||
markup.addRangeHighlighter(start, end, LAYER, null, EXACT_RANGE)
|
||||
.apply { customRenderer = renderer }
|
||||
markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
|
||||
customRenderer = renderer
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (enableBulkEditing) {
|
||||
document.isInBulkUpdate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AceConfig.showSearchNotification)
|
||||
showSearchNotification(results, query, jumpMode)
|
||||
|
||||
for (editor in previousHighlights.keys.toList()) {
|
||||
if (!results.containsKey(editor))
|
||||
previousHighlights.remove(editor)
|
||||
?.forEach(editor.markupModel::removeHighlighter)
|
||||
if (!results.containsKey(editor)) {
|
||||
previousHighlights.remove(editor)?.forEach(editor.markupModel::removeHighlighter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification with the current search text.
|
||||
*/
|
||||
private fun showSearchNotification(results: Map<Editor, IntList>,
|
||||
query: SearchQuery, jumpMode: JumpMode) {
|
||||
// clear previous hint
|
||||
previousHint?.hide()
|
||||
|
||||
// add notification hint to first editor
|
||||
val editor = results.keys.first()
|
||||
val component: JComponent = editor.component
|
||||
|
||||
val label1 = NotificationLabel(
|
||||
" " +
|
||||
CodeInsightBundle.message("incremental.search.tooltip.prefix")
|
||||
).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
|
||||
)
|
||||
|
||||
preferredSize = Dimension(
|
||||
editor.contentComponent.width +
|
||||
label1.preferredSize.width, preferredSize.height
|
||||
)
|
||||
}
|
||||
|
||||
val hint = LightweightHint(panel)
|
||||
|
||||
val x = SwingUtilities.convertPoint(component, 0, 0, component).x
|
||||
val y: Int = -hint.component.preferredSize.height
|
||||
val p = SwingUtilities.convertPoint(
|
||||
component, x, y,
|
||||
component.rootPane.layeredPane
|
||||
)
|
||||
|
||||
HintManagerImpl.getInstanceImpl().showEditorHint(
|
||||
hint,
|
||||
editor,
|
||||
p,
|
||||
HIDE_BY_ESCAPE or HIDE_BY_TEXT_CHANGE,
|
||||
0,
|
||||
false,
|
||||
HintHint(editor, p).setAwtTooltip(false)
|
||||
)
|
||||
previousHint = hint
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
previousHighlights.forEach { (editor, highlighters) ->
|
||||
highlighters.forEach(editor.markupModel::removeHighlighter)
|
||||
}
|
||||
previousHighlights.keys.forEach { it.markupModel.removeAllHighlighters() }
|
||||
previousHighlights.clear()
|
||||
previousHint?.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a filled highlight in the background of a searched text occurrence.
|
||||
*/
|
||||
private object SearchedWordRenderer : CustomHighlighterRenderer {
|
||||
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) =
|
||||
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset)
|
||||
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
|
||||
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.textHighlightColor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int) {
|
||||
/**
|
||||
* Renders a filled highlight in the background of the first highlighted position. Used for regex search queries.
|
||||
*/
|
||||
private object RegexRenderer : CustomHighlighterRenderer {
|
||||
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
|
||||
drawSingle(g, editor, highlighter.startOffset, AceConfig.textHighlightColor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a filled highlight in the background of the accepted tag position and search query.
|
||||
*/
|
||||
private object AcceptedTagRenderer : CustomHighlighterRenderer {
|
||||
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
|
||||
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.acceptedTagColor)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val LAYER = HighlighterLayer.LAST + 1
|
||||
|
||||
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int, color: Color) {
|
||||
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
|
||||
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
|
||||
|
||||
g.color = AceConfig.textHighlightColor
|
||||
g.fillRect(start.x, start.y, end.x - start.x, editor.lineHeight)
|
||||
g.color = color
|
||||
g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1)
|
||||
|
||||
g.color = AceConfig.tagBackgroundColor
|
||||
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a filled highlight in the background of a searched
|
||||
* text occurrence, as well as an outline indicating the range
|
||||
* of characters that will be selected by [JumpMode.TARGET].
|
||||
*/
|
||||
private object SearchedWordWithOutlineRenderer: CustomHighlighterRenderer {
|
||||
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
|
||||
SearchedWordRenderer.paint(editor, highlighter, g)
|
||||
|
||||
val chars = editor.immutableText
|
||||
val startOffset = highlighter.startOffset
|
||||
|
||||
if (chars.getOrNull(startOffset)?.isWordPart == true)
|
||||
drawOutline(g, editor, chars.wordStart(startOffset), chars.wordEnd(startOffset) + 1)
|
||||
}
|
||||
|
||||
private fun drawOutline(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int) {
|
||||
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
|
||||
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
|
||||
|
||||
g.color = AceConfig.targetModeColor
|
||||
g.drawRect(max(0, start.x - JBUI.scale(1)), start.y,
|
||||
end.x - start.x + JBUI.scale(2), editor.lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a filled highlight in the background of the first highlighted
|
||||
* position. Used for regex search queries.
|
||||
*/
|
||||
private object RegexRenderer: CustomHighlighterRenderer {
|
||||
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) =
|
||||
drawSingle(g, editor, highlighter.startOffset)
|
||||
|
||||
private fun drawSingle(g: Graphics, editor: Editor, offset: Int) {
|
||||
private fun drawSingle(g: Graphics, editor: Editor, offset: Int, color: Color) {
|
||||
val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset)
|
||||
val char = editor.immutableText.getOrNull(offset)
|
||||
?.takeUnless { it == '\n' || it == '\t' } ?: ' '
|
||||
val char = editor.immutableText.getOrNull(offset)?.takeUnless { it == '\n' || it == '\t' } ?: ' '
|
||||
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
|
||||
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
|
||||
|
||||
g.color = AceConfig.textHighlightColor
|
||||
g.fillRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
|
||||
g.color = color
|
||||
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
|
||||
|
||||
g.color = AceConfig.tagBackgroundColor
|
||||
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<idea-plugin url="https://github.com/acejump/AceJump">
|
||||
<idea-plugin>
|
||||
<name>AceJump</name>
|
||||
<id>AceJump</id>
|
||||
<id>AceJump-chylex</id>
|
||||
|
||||
<description><![CDATA[
|
||||
AceJump allows you to quickly navigate the caret to any position visible in the editor.
|
||||
@@ -9,6 +9,7 @@
|
||||
</description>
|
||||
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
<depends>IdeaVIM</depends>
|
||||
|
||||
<category>Navigation</category>
|
||||
<vendor url="https://github.com/acejump/AceJump">AceJump</vendor>
|
||||
@@ -20,98 +21,30 @@
|
||||
id="preferences.AceConfigurable" dynamic="true"/>
|
||||
|
||||
<editorActionHandler action="EditorEscape" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$Reset"
|
||||
id="AceHandlerEscape"/>
|
||||
implementationClass="org.acejump.action.AceEditorAction$Reset"/>
|
||||
<editorActionHandler action="EditorBackSpace" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"
|
||||
id="AceHandlerBackSpace"/>
|
||||
<editorActionHandler action="EditorStartNewLine" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SelectBackward"
|
||||
id="AceHandlerStartNewLine"/>
|
||||
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
|
||||
<editorActionHandler action="EditorEnter" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SelectForward"
|
||||
id="AceHandlerEnter"/>
|
||||
<editorActionHandler action="EditorTab" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$ScrollToNextScreenful"
|
||||
id="AceHandlerTab"/>
|
||||
<editorActionHandler action="EditorUnindentSelection" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$ScrollToPreviousScreenful"
|
||||
id="AceHandlerUnindentSelection"/>
|
||||
<editorActionHandler action="EditorUp" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"
|
||||
id="AceHandlerUp"/>
|
||||
<editorActionHandler action="EditorLeft" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"
|
||||
id="AceHandlerLeft"/>
|
||||
<editorActionHandler action="EditorLineStart" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"
|
||||
id="AceHandlerLineStart"/>
|
||||
<editorActionHandler action="EditorRight" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"
|
||||
id="AceHandlerRight"/>
|
||||
<editorActionHandler action="EditorLineEnd" order="first"
|
||||
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"
|
||||
id="AceHandlerLineEnd"/>
|
||||
|
||||
implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/>
|
||||
</extensions>
|
||||
|
||||
<actions>
|
||||
<action id="AceAction"
|
||||
class="org.acejump.action.AceAction$ActivateOrCycleMode"
|
||||
text="Activate / Cycle AceJump Mode">
|
||||
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
|
||||
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
|
||||
</action>
|
||||
<action id="AceReverseAction"
|
||||
class="org.acejump.action.AceAction$ActivateOrReverseCycleMode"
|
||||
text="Activate / Reverse Cycle AceJump Mode"/>
|
||||
<action id="AceForwardAction"
|
||||
class="org.acejump.action.AceAction$ToggleForwardJumpMode"
|
||||
text="Start AceJump in Jump After Caret Mode"/>
|
||||
<action id="AceBackwardAction"
|
||||
class="org.acejump.action.AceAction$ToggleBackwardJumpMode"
|
||||
text="Start AceJump in Jump Before Caret Mode"/>
|
||||
<action id="AceWordStartAction"
|
||||
class="org.acejump.action.AceAction$ToggleJumpMode"
|
||||
text="Start AceJump in Jump Mode"/>
|
||||
<action id="AceWordEndAction"
|
||||
class="org.acejump.action.AceAction$ToggleJumpEndMode"
|
||||
text="Start AceJump in Jump End Mode"/>
|
||||
<action id="AceTargetAction"
|
||||
class="org.acejump.action.AceAction$ToggleTargetMode"
|
||||
text="Start AceJump in Target Mode">
|
||||
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl alt SEMICOLON"/>
|
||||
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl alt SEMICOLON"/>
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt SEMICOLON"/>
|
||||
</action>
|
||||
<action id="AceDeclarationAction"
|
||||
class="org.acejump.action.AceAction$ToggleDeclarationMode"
|
||||
text="Start AceJump in Declaration Mode"/>
|
||||
<action id="AceLineAction"
|
||||
class="org.acejump.action.AceAction$StartAllLineMarksMode"
|
||||
text="Start AceJump in All Line Marks Mode">
|
||||
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl shift SEMICOLON"/>
|
||||
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl shift SEMICOLON"/>
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
|
||||
</action>
|
||||
<action id="AceLineStartsAction"
|
||||
class="org.acejump.action.AceAction$StartAllLineStartsMode"
|
||||
text="Start AceJump in All Line Starts Mode"/>
|
||||
<action id="AceLineEndsAction"
|
||||
class="org.acejump.action.AceAction$StartAllLineEndsMode"
|
||||
text="Start AceJump in All Line Ends Mode"/>
|
||||
<action id="AceLineIndentsAction"
|
||||
class="org.acejump.action.AceAction$StartAllLineIndentsMode"
|
||||
text="Start AceJump in All Line Indents Mode"/>
|
||||
<action id="AceWordAction"
|
||||
class="org.acejump.action.AceAction$StartAllWordsMode"
|
||||
text="Start AceJump in All Words Mode"/>
|
||||
<action id="AceWordForwardAction"
|
||||
class="org.acejump.action.AceAction$StartAllWordsForwardMode"
|
||||
text="Start AceJump in All Words After Caret Mode"/>
|
||||
<action id="AceWordBackwardsAction"
|
||||
class="org.acejump.action.AceAction$StartAllWordsBackwardsMode"
|
||||
text="Start AceJump in All Words Before Caret Mode"/>
|
||||
<action id="AceVimAction_JumpAllEditors" class="org.acejump.action.AceVimAction$JumpAllEditors" text="AceJump Vim - Jump All Editors" />
|
||||
<action id="AceVimAction_JumpForward" class="org.acejump.action.AceVimAction$JumpForward" text="AceJump Vim - Jump Forward" />
|
||||
<action id="AceVimAction_JumpBackward" class="org.acejump.action.AceVimAction$JumpBackward" text="AceJump Vim - Jump Backward" />
|
||||
<action id="AceVimAction_JumpTillForward" class="org.acejump.action.AceVimAction$JumpTillForward" text="AceJump Vim - Jump Till Forward" />
|
||||
<action id="AceVimAction_JumpTillBackward" class="org.acejump.action.AceVimAction$JumpTillBackward" text="AceJump Vim - Jump Till Backward" />
|
||||
<action id="AceVimAction_JumpOnLineForward" class="org.acejump.action.AceVimAction$JumpOnLineForward" text="AceJump Vim - Jump On Line Forward" />
|
||||
<action id="AceVimAction_JumpOnLineBackward" class="org.acejump.action.AceVimAction$JumpOnLineBackward" text="AceJump Vim - Jump On Line Backward" />
|
||||
<action id="AceVimAction_JumpLineIndentsForward" class="org.acejump.action.AceVimAction$JumpLineIndentsForward" text="AceJump Vim - Jump Line Indents Forward" />
|
||||
<action id="AceVimAction_JumpLineIndentsBackward" class="org.acejump.action.AceVimAction$JumpLineIndentsBackward" text="AceJump Vim - Jump Line Indents Backward" />
|
||||
<action id="AceVimAction_JumpLWordForward" class="org.acejump.action.AceVimAction$JumpLWordForward" text="AceJump Vim - Jump LWord Forward" />
|
||||
<action id="AceVimAction_JumpUWordForward" class="org.acejump.action.AceVimAction$JumpUWordForward" text="AceJump Vim - Jump UWord Forward" />
|
||||
<action id="AceVimAction_JumpLWordBackward" class="org.acejump.action.AceVimAction$JumpLWordBackward" text="AceJump Vim - Jump LWord Backward" />
|
||||
<action id="AceVimAction_JumpUWordBackward" class="org.acejump.action.AceVimAction$JumpUWordBackward" text="AceJump Vim - Jump UWord Backward" />
|
||||
<action id="AceVimAction_JumpLWordEndForward" class="org.acejump.action.AceVimAction$JumpLWordEndForward" text="AceJump Vim - Jump LWord End Forward" />
|
||||
<action id="AceVimAction_JumpUWordEndForward" class="org.acejump.action.AceVimAction$JumpUWordEndForward" text="AceJump Vim - Jump UWord End Forward" />
|
||||
<action id="AceVimAction_JumpLWordEndBackward" class="org.acejump.action.AceVimAction$JumpLWordEndBackward" text="AceJump Vim - Jump LWord End Backward" />
|
||||
<action id="AceVimAction_JumpUWordEndBackward" class="org.acejump.action.AceVimAction$JumpUWordEndBackward" text="AceJump Vim - Jump UWord End Backward" />
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 40 41" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 40 41" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,0,-635)">
|
||||
<g id="pluginIcon" transform="matrix(0.154082,0,0,0.154082,-39.7746,553.645)">
|
||||
<rect x="258.139" y="529.264" width="259.602" height="259.602" style="fill:none;"/>
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -1,8 +1,4 @@
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -30,30 +26,6 @@ class AceTest : BaseTest() {
|
||||
fun `test a query containing a { character`() =
|
||||
assertEquals("abcd{dabc cdab".search("cd{"), setOf(2))
|
||||
|
||||
fun `test that jumping to first occurrence succeeds`() {
|
||||
"<caret>testing 1234".search("1")
|
||||
|
||||
takeAction(ACTION_EDITOR_ENTER)
|
||||
|
||||
myFixture.checkResult("testing <caret>1234")
|
||||
}
|
||||
|
||||
fun `test that jumping to second occurrence succeeds`() {
|
||||
"<caret>testing 1234".search("ti")
|
||||
|
||||
takeAction(ACTION_EDITOR_ENTER)
|
||||
|
||||
myFixture.checkResult("tes<caret>ting 1234")
|
||||
}
|
||||
|
||||
fun `test that jumping to previous occurrence succeeds`() {
|
||||
"te<caret>sting 1234".search("t")
|
||||
|
||||
takeAction(ACTION_EDITOR_START_NEW_LINE)
|
||||
|
||||
myFixture.checkResult("<caret>testing 1234")
|
||||
}
|
||||
|
||||
fun `test tag selection`() {
|
||||
"<caret>testing 1234".search("g")
|
||||
|
||||
@@ -61,105 +33,4 @@ class AceTest : BaseTest() {
|
||||
|
||||
myFixture.checkResult("testin<caret>g 1234")
|
||||
}
|
||||
|
||||
fun `test shift selection`() {
|
||||
"<caret>testing 1234".search("4")
|
||||
|
||||
typeAndWaitForResults(session.tags[0].key.uppercase())
|
||||
|
||||
myFixture.checkResult("<selection>testing 123<caret></selection>4")
|
||||
}
|
||||
|
||||
fun `test words before caret action`() {
|
||||
makeEditor("test words <caret> before caret is two")
|
||||
|
||||
takeAction(AceAction.StartAllWordsBackwardsMode())
|
||||
|
||||
assertEquals(2, session.tags.size)
|
||||
}
|
||||
|
||||
fun `test words after caret action`() {
|
||||
makeEditor("test words <caret> after caret is four")
|
||||
|
||||
takeAction(AceAction.StartAllWordsForwardMode())
|
||||
|
||||
assertEquals(4, session.tags.size)
|
||||
}
|
||||
|
||||
fun `test word mode`() {
|
||||
makeEditor("test word action")
|
||||
|
||||
takeAction(AceAction.StartAllWordsMode())
|
||||
|
||||
assertEquals(3, session.tags.size)
|
||||
|
||||
typeAndWaitForResults(session.tags[1].key)
|
||||
|
||||
myFixture.checkResult("test <caret>word action")
|
||||
}
|
||||
|
||||
fun `test target mode`() {
|
||||
"<caret>test target action".search("target")
|
||||
|
||||
takeAction(AceAction.ToggleTargetMode())
|
||||
typeAndWaitForResults(session.tags[0].key)
|
||||
|
||||
myFixture.checkResult("test <selection>target<caret></selection> action")
|
||||
}
|
||||
|
||||
fun `test cache invalidation`() {
|
||||
"first line".search("first")
|
||||
typeAndWaitForResults(session.tags[0].key)
|
||||
|
||||
repeat(3) { takeAction(EnterAction()) }
|
||||
|
||||
takeAction(AceAction.ToggleTargetMode())
|
||||
typeAndWaitForResults("first")
|
||||
typeAndWaitForResults(session.tags[0].key)
|
||||
|
||||
myFixture.checkResult("\n\n\n<selection>first<caret></selection> line")
|
||||
}
|
||||
|
||||
fun `test line mode`() {
|
||||
makeEditor(" test\n three\n lines")
|
||||
|
||||
takeAction(AceAction.StartAllLineMarksMode())
|
||||
|
||||
assertEquals(9, session.tags.size)
|
||||
}
|
||||
|
||||
fun `test chinese selection`() {
|
||||
AceConfig.settings.mapToASCII = true
|
||||
|
||||
"test 拼音 selection".search("py")
|
||||
|
||||
takeAction(AceAction.ToggleTargetMode())
|
||||
|
||||
typeAndWaitForResults(session.tags[0].key)
|
||||
|
||||
myFixture.checkResult("test <selection>拼音<caret></selection> selection")
|
||||
}
|
||||
|
||||
fun `test japanese selection`() {
|
||||
AceConfig.settings.mapToASCII = true
|
||||
|
||||
"あみだにょらい".search("am")
|
||||
|
||||
takeAction(AceAction.ToggleTargetMode())
|
||||
|
||||
typeAndWaitForResults(session.tags[0].key)
|
||||
|
||||
myFixture.checkResult("<selection>あみだにょらい<caret></selection>")
|
||||
}
|
||||
|
||||
// https://github.com/acejump/AceJump/issues/355
|
||||
fun `ignore test a word that is difficult to tag`() {
|
||||
makeEditor("aaCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||||
|
||||
takeAction(AceAction.ActivateOrCycleMode())
|
||||
|
||||
typeAndWaitForResults("c")
|
||||
|
||||
assertEquals(2, session.tags.size)
|
||||
}
|
||||
}
|
||||
|
@@ -1,188 +0,0 @@
|
||||
import com.intellij.mock.MockVirtualFile
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.markup.HighlighterLayer
|
||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import junit.framework.TestCase
|
||||
import org.acejump.action.AceAction
|
||||
import org.acejump.boundaries.Boundaries
|
||||
import org.acejump.boundaries.EditorOffsetCache
|
||||
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
|
||||
import org.acejump.input.JumpMode
|
||||
import org.acejump.search.Pattern.ALL_WORDS
|
||||
import org.acejump.session.AceJumpListener
|
||||
import org.acejump.session.SessionManager
|
||||
import org.acejump.test.util.BaseTest
|
||||
|
||||
/**
|
||||
* Test [org.acejump.ExternalUsage] endpoints.
|
||||
*/
|
||||
|
||||
class ExternalUsageTest: BaseTest() {
|
||||
fun `test externally tagged results and listener notification`() {
|
||||
makeEditor("test externally tagged results")
|
||||
|
||||
SessionManager.start(myFixture.editor).markResults(sortedSetOf(4, 10, 15))
|
||||
|
||||
TestCase.assertEquals(3, session.tags.size)
|
||||
|
||||
var shouldBeTrueAfterFinished = false
|
||||
session.addAceJumpListener(object: AceJumpListener {
|
||||
override fun finished(mark: String?, query: String?) {
|
||||
shouldBeTrueAfterFinished = true
|
||||
}
|
||||
})
|
||||
|
||||
typeAndWaitForResults(session.tags[0].key)
|
||||
|
||||
TestCase.assertTrue(shouldBeTrueAfterFinished)
|
||||
}
|
||||
|
||||
fun `test externally tagged results with multiple editors`() {
|
||||
val fileA = MockVirtualFile("a.txt", "first file")
|
||||
val fileB = MockVirtualFile("b.txt", "second file with more markers")
|
||||
manager?.openFile(fileA, true)
|
||||
manager?.openFile(fileB, false)
|
||||
|
||||
val mainEditor = (manager?.selectedEditor as TextEditor).editor
|
||||
val editorA = (manager?.getEditors(fileA)?.single() as TextEditor).editor
|
||||
val editorB = (manager?.getEditors(fileB)?.single() as TextEditor).editor
|
||||
|
||||
val session = SessionManager.start(mainEditor, listOf(editorA, editorB))
|
||||
|
||||
session.markResults(mapOf(
|
||||
editorA to IntArrayList(intArrayOf(0, 6)),
|
||||
editorB to IntArrayList(intArrayOf(0, 7, 22))
|
||||
))
|
||||
|
||||
TestCase.assertEquals(5, session.tags.size)
|
||||
TestCase.assertEquals(2, session.tags.count { it.value.editor === editorA })
|
||||
TestCase.assertEquals(3, session.tags.count { it.value.editor === editorB })
|
||||
|
||||
TestCase.assertEquals(listOf(0, 6), session.tags
|
||||
.filter { it.value.editor === editorA }
|
||||
.map { it.value.offset }
|
||||
.sorted())
|
||||
|
||||
TestCase.assertEquals(listOf(0, 7, 22), session.tags
|
||||
.filter { it.value.editor === editorB }
|
||||
.map { it.value.offset }
|
||||
.sorted())
|
||||
}
|
||||
|
||||
fun `test external pattern usage`() {
|
||||
makeEditor("test external pattern usage")
|
||||
|
||||
SessionManager.start(myFixture.editor)
|
||||
.startRegexSearch(ALL_WORDS, WHOLE_FILE)
|
||||
|
||||
TestCase.assertEquals(4, session.tags.size)
|
||||
}
|
||||
|
||||
fun `test external regex usage`() {
|
||||
makeEditor("test external regex usage")
|
||||
|
||||
SessionManager.start(myFixture.editor)
|
||||
.startRegexSearch("[aeiou]+", WHOLE_FILE)
|
||||
|
||||
TestCase.assertEquals(9, session.tags.size)
|
||||
}
|
||||
|
||||
fun `test external jump with bounds`() {
|
||||
makeEditor("test word and word usage")
|
||||
|
||||
SessionManager.start(myFixture.editor)
|
||||
.toggleJumpMode(JumpMode.JUMP, object : Boundaries {
|
||||
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
|
||||
return 14..18
|
||||
}
|
||||
|
||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
||||
return offset in 14..18
|
||||
}
|
||||
})
|
||||
|
||||
typeAndWaitForResults("word")
|
||||
|
||||
TestCase.assertEquals(1, session.tags.size)
|
||||
TestCase.assertEquals(14, session.tags.single().value.offset)
|
||||
}
|
||||
|
||||
fun `test listener query and mark`() {
|
||||
"<caret>testing 1234".search("g")
|
||||
|
||||
var detectedMark: String? = null
|
||||
var detectedQuery: String? = null
|
||||
session.addAceJumpListener(object: AceJumpListener {
|
||||
override fun finished(mark: String?, query: String?) {
|
||||
detectedMark = mark
|
||||
detectedQuery = query
|
||||
}
|
||||
})
|
||||
|
||||
val mark = session.tags[0].key
|
||||
typeAndWaitForResults(mark)
|
||||
|
||||
TestCase.assertEquals(mark, detectedMark)
|
||||
TestCase.assertEquals("g", detectedQuery)
|
||||
}
|
||||
|
||||
fun `test listener after escape`() {
|
||||
"<caret>testing 1234".search("g")
|
||||
|
||||
var detectedMark: String? = null
|
||||
var detectedQuery: String? = null
|
||||
session.addAceJumpListener(object: AceJumpListener {
|
||||
override fun finished(mark: String?, query: String?) {
|
||||
detectedMark = mark
|
||||
detectedQuery = query
|
||||
}
|
||||
})
|
||||
|
||||
myFixture.performEditorAction("EditorEscape")
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
TestCase.assertEquals(null, detectedMark)
|
||||
TestCase.assertEquals(null, detectedQuery)
|
||||
}
|
||||
|
||||
fun `test listener for word motion`() {
|
||||
makeEditor("test word action")
|
||||
|
||||
takeAction(AceAction.StartAllWordsMode())
|
||||
|
||||
var detectedMark: String? = null
|
||||
var detectedQuery: String? = null
|
||||
session.addAceJumpListener(object: AceJumpListener {
|
||||
override fun finished(mark: String?, query: String?) {
|
||||
detectedMark = mark
|
||||
detectedQuery = query
|
||||
}
|
||||
})
|
||||
|
||||
val mark = session.tags[1].key
|
||||
typeAndWaitForResults(mark)
|
||||
|
||||
TestCase.assertEquals(mark, detectedMark)
|
||||
TestCase.assertEquals("", detectedQuery)
|
||||
}
|
||||
|
||||
fun `test do not remove other highlights when the session ends`() {
|
||||
makeEditor("test do not remove other highlights when the session ends")
|
||||
|
||||
val markupModel = myFixture.editor.markupModel
|
||||
val layer = HighlighterLayer.SELECTION - 1
|
||||
val existedHighlighter = markupModel.addRangeHighlighter(0, 1, layer, null, HighlighterTargetArea.EXACT_RANGE)
|
||||
|
||||
takeAction(AceAction.StartAllWordsMode())
|
||||
val mark = session.tags[0].key
|
||||
typeAndWaitForResults(mark)
|
||||
|
||||
TestCase.assertEquals("last session should be disposed", null, SessionManager[myFixture.editor])
|
||||
TestCase.assertTrue("existed highlighter should not be removed", existedHighlighter.isValid)
|
||||
|
||||
existedHighlighter.dispose()
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
import org.acejump.action.AceAction
|
||||
|
||||
import org.acejump.action.AceVimAction
|
||||
import org.acejump.test.util.BaseTest
|
||||
import org.junit.Ignore
|
||||
import java.io.File
|
||||
import kotlin.random.Random
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Ignore
|
||||
class LatencyTest : BaseTest() {
|
||||
|
||||
private fun `test tag latency`(editorText: String) {
|
||||
val chars = editorText.toCharArray().distinct().filter { !it.isWhitespace() }
|
||||
val avg = averageTimeWithWarmup(warmupRuns = 10, timedRuns = 10) {
|
||||
@@ -14,7 +14,7 @@ class LatencyTest: BaseTest() {
|
||||
|
||||
for (query in chars) {
|
||||
makeEditor(editorText)
|
||||
myFixture.testAction(AceAction.ActivateOrCycleMode())
|
||||
myFixture.testAction(AceVimAction.JumpAllEditors())
|
||||
time += measureTimeMillis { typeAndWaitForResults("$query") }
|
||||
// TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" }
|
||||
resetEditor()
|
||||
@@ -37,6 +37,8 @@ class LatencyTest: BaseTest() {
|
||||
)
|
||||
|
||||
fun `test lorem ipsum latency`() = `test tag latency`(
|
||||
File(javaClass.classLoader.getResource("lipsum.txt")!!.file).readText()
|
||||
File(
|
||||
javaClass.classLoader.getResource("lipsum.txt")!!.file
|
||||
).readText()
|
||||
)
|
||||
}
|
||||
|
@@ -2,24 +2,33 @@ package org.acejump.test.util
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.editor.impl.EditorImpl
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.testFramework.FileEditorManagerTestCase
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.acejump.action.AceAction
|
||||
import org.acejump.action.AceVimAction
|
||||
import org.acejump.session.SessionManager
|
||||
|
||||
abstract class BaseTest: FileEditorManagerTestCase() {
|
||||
abstract class BaseTest : BasePlatformTestCase() {
|
||||
companion object {
|
||||
inline fun averageTimeWithWarmup(warmupRuns: Int, timedRuns: Int, action: () -> Long): Long {
|
||||
repeat(warmupRuns) { action() }
|
||||
repeat(warmupRuns) {
|
||||
action()
|
||||
}
|
||||
|
||||
var time = 0L
|
||||
repeat(timedRuns) { time += action() }
|
||||
|
||||
repeat(timedRuns) {
|
||||
time += action()
|
||||
}
|
||||
|
||||
return time / timedRuns
|
||||
}
|
||||
}
|
||||
|
||||
protected val session get() = SessionManager[myFixture.editor]!!
|
||||
protected val session
|
||||
get() = SessionManager[myFixture.editor]!!
|
||||
|
||||
override fun tearDown() {
|
||||
resetEditor()
|
||||
@@ -29,16 +38,16 @@ abstract class BaseTest: FileEditorManagerTestCase() {
|
||||
fun takeAction(action: String) = myFixture.performEditorAction(action)
|
||||
fun takeAction(action: AnAction) = myFixture.testAction(action)
|
||||
|
||||
fun makeEditor(contents: String): PsiFile =
|
||||
myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
|
||||
fun makeEditor(contents: String): PsiFile {
|
||||
val file = myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
|
||||
(myFixture.editor as EditorImpl).scrollPane.viewport.setSize(1000, 100)
|
||||
return file
|
||||
}
|
||||
|
||||
fun resetEditor() {
|
||||
myFixture.editor?.let {
|
||||
takeAction(IdeActions.ACTION_EDITOR_ESCAPE)
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
assertEmpty(it.markupModel.allHighlighters)
|
||||
}
|
||||
manager?.closeAllFiles()
|
||||
assertEmpty(myFixture.editor.markupModel.allHighlighters)
|
||||
}
|
||||
|
||||
fun typeAndWaitForResults(string: String) {
|
||||
@@ -46,11 +55,13 @@ abstract class BaseTest: FileEditorManagerTestCase() {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
}
|
||||
|
||||
fun String.executeQuery(query: String) = myFixture.run {
|
||||
private fun String.executeQuery(query: String) {
|
||||
myFixture.run {
|
||||
makeEditor(this@executeQuery)
|
||||
testAction(AceAction.ActivateOrCycleMode())
|
||||
testAction(AceVimAction.JumpAllEditors())
|
||||
typeAndWaitForResults(query)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.search(query: String): Set<Int> {
|
||||
this@search.executeQuery(query)
|
||||
@@ -58,8 +69,10 @@ abstract class BaseTest: FileEditorManagerTestCase() {
|
||||
return myFixture.editor.markupModel.allHighlighters.map { it.startOffset }.toSet()
|
||||
}
|
||||
|
||||
private fun String.assertCorrectNumberOfTags(query: String) =
|
||||
private fun String.assertCorrectNumberOfTags(query: String) {
|
||||
assertEquals(split(query.fold("") { prefix, char ->
|
||||
if ((prefix + char) in this) prefix + char else return
|
||||
}).size - 1, myFixture.editor.markupModel.allHighlighters.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user