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

39 Commits

Author SHA1 Message Date
9a435feccc Fix broken special actions in Rider 2021-09-29 08:56:15 +02:00
c1feb891e4 Update Gradle wrapper to 7.1 2021-09-29 08:25:09 +02:00
0b6ba62cda Simplify existing modes and add new vim shortcuts 2021-05-15 20:58:58 +02:00
dfd5b122a0 Optimizations 2021-05-15 20:30:18 +02:00
f2400d134d Update build.gradle 2021-05-15 19:46:51 +02:00
98997b4d86 [WIP] Fix tests 2021-05-08 03:54:27 +02:00
1e172e9c49 [WIP] Implement jumping across splitters 2021-05-08 02:39:27 +02:00
01b37c878e [WIP] Add more vim-friendly jump actions (declaration, usages) 2021-04-11 08:34:52 +02:00
1630c706a9 [WIP] Fix easymotion-like actions in visual mode 2021-04-08 13:57:11 +02:00
041a130c5f [WIP] Add a few easymotion-like actions 2021-04-07 03:05:11 +02:00
7561bfde36 [WIP] Make quick jump the default mode and rename non-quick jump to advanced mode 2021-04-07 00:16:50 +02:00
724e469f21 [WIP] Remove 'From Caret' mode 2021-04-06 02:09:29 +02:00
bce9a5f636 [WIP] Add quick jump mode 2021-03-29 18:43:00 +02:00
bfe0aa536e [WIP] Change plugin version 2021-03-29 18:42:40 +02:00
2ca21b8423 [WIP] Work around weird highlight offset bug 2021-03-29 18:42:00 +02:00
430bcf6883 [WIP] Remove word start jump to simplify 2021-03-29 18:41:59 +02:00
092650af81 [WIP] Pressing Enter before typing query starts jump mode for character at caret 2021-03-29 18:41:57 +02:00
2009123114 [WIP] Interactive modes 2021-03-29 18:41:54 +02:00
de19c0e0b8 Make Enter immediately tag search occurrences 2021-03-29 18:39:15 +02:00
5021a07fb9 Remove tag visiting functionality 2021-03-29 18:38:32 +02:00
1180547b06 Remove whole file search 2021-03-29 18:36:53 +02:00
d2ae335de1 Redesign tags and highlighting (padding, tag shadow, highlight outline) 2021-03-29 02:11:07 +02:00
324296b55a Remove option for rounded tag corners 2021-03-29 02:11:07 +02:00
a4ebeffe05 Change default color scheme 2021-03-29 02:11:07 +02:00
7716242e55 Swap editor shortcuts for searching line starts and indents 2021-03-29 02:11:07 +02:00
eb70ef5097 Add option to set minimum typed characters for tagging to simplify tags 2021-03-29 02:11:06 +02:00
4d5a0b3e6a Fix occasional conflicts between tags and search query when assigning vacant results 2021-03-29 02:11:06 +02:00
eb1bbb2e03 Prevent editing document while AceJump is active 2021-03-29 02:11:06 +02:00
74a65a6510 Make jump mode cycling wrap around & add shortcut to cycle modes in reverse 2021-03-29 02:11:06 +02:00
2c08494a71 Add all regex search patterns to keymap 2021-03-29 02:11:06 +02:00
b61abee04d Major AceJump refactoring!
See https://github.com/acejump/AceJump/issues/348 for information on what's changed and what more needs to be done.
2021-03-29 02:11:06 +02:00
breandan
89af38422a remove unnecessary options 2021-03-04 23:01:51 -05:00
breandan
80f25c39b2 update versions 2021-03-04 22:54:20 -05:00
breandan
8e09ab83d7 update to kotlin 1.5 and fix method reference 2021-02-07 00:44:47 -05:00
breandan
a9df9b7970 bump gradlew 2021-01-08 15:59:08 -05:00
breandan
b1d69bf251 update change notes and prepare next release -- thanks @chylex
I think this deserves a +.1 version release!
2020-11-29 06:16:45 -05:00
breandan
e37e1d92b3 Merge pull request #347 from chylex/jump-end-mode
Add 'Jump to End' mode
2020-11-29 06:07:21 -05:00
breandan
c5008ab26e bump gradlew version 2020-11-27 01:03:15 -05:00
ee1ce3c37e Add 'Jump to End' mode 2020-11-24 14:04:03 +01:00
45 changed files with 1247 additions and 795 deletions

View File

@@ -1,8 +1,17 @@
# Changelog
### 3.6.4
### 3.7
- Improvements to tag latency. Thanks to @chylex for [the PR](https://github.com/acejump/AceJump/pull/339)!
- Improvements to tag latency
- Redesign settings panel
- Add missing configuration for definition mode color
- Adds option to switch between straight and rounded tag corners
- Adds option to only consider visible area
- Add customizable jump mode cycling
- Jump-to-End mode jumps to the end of a word
- Fixes toggle keys not resetting mode when pressed twice
- Increase limit for what is considered a large file
- Thanks to @chylex for [all the PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex)!
### 3.6.3

View File

@@ -171,7 +171,7 @@ 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.
* [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292).
* [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pull/339).
* [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex).
* [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency.
* [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years.
* [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design.

View File

@@ -1,29 +1,15 @@
import org.jetbrains.changelog.closure
import org.jetbrains.intellij.tasks.PatchPluginXmlTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
idea apply true
kotlin("jvm") version "1.3.72"
id("org.jetbrains.intellij") version "0.6.5"
id("org.jetbrains.changelog") version "0.6.2"
kotlin("jvm") version "1.5.0"
id("org.jetbrains.intellij") version "0.7.2"
}
tasks {
withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
kotlinOptions.freeCompilerArgs += "-progressive"
}
withType<PatchPluginXmlTask> {
sinceBuild("201.6668.0")
changeNotes({ changelog.getLatest().toHTML() })
}
}
changelog {
path = "${project.projectDir}/CHANGES.md"
header = closure { "${project.version}" }
}
dependencies {
@@ -36,11 +22,11 @@ repositories {
}
intellij {
version = "2020.2"
version = "2021.1"
pluginName = "AceJump"
updateSinceUntilBuild = false
setPlugins("java")
setPlugins("java", "IdeaVIM:0.66")
}
group = "org.acejump"
version = "4.0"
version = "chylex-8"

Binary file not shown.

View File

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

4
gradlew vendored
View File

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

22
gradlew.bat vendored
View File

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

View File

@@ -2,6 +2,7 @@ package org.acejump
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actions.EditorActionUtil
import it.unimi.dsi.fastutil.ints.IntArrayList
annotation class ExternalUsage
@@ -37,7 +38,7 @@ 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.
@@ -57,8 +58,9 @@ inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Ch
*/
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])) {
while (end < limit && isPartOfWord(this[end + 1])) {
++end
}
@@ -96,14 +98,25 @@ inline fun CharSequence.humpEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char
*/
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])) {
while (end < limit && !isPartOfWord(this[end + 1])) {
++end
}
if (end < length - 1 && isPartOfWord(this[end + 1])) {
if (end < limit && isPartOfWord(this[end + 1])) {
++end
}
return end
}
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
val clone = HashMap<Editor, IntArrayList>(size)
for ((editor, offsets) in this) {
clone[editor] = offsets.clone()
}
return clone
}

View File

@@ -4,7 +4,6 @@ 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
@@ -12,7 +11,7 @@ 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() {
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)
}
@@ -40,15 +39,19 @@ sealed class AceEditorAction(private val originalHandler: EditorActionHandler) :
override fun run(session: Session) = session.restart()
}
class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.tagImmediately()
}
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.VISIBLE_ON_SCREEN)
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS)
}
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.VISIBLE_ON_SCREEN)
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS)
}
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.VISIBLE_ON_SCREEN)
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS)
}
}

View File

@@ -2,7 +2,10 @@ package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.util.IncorrectOperationException
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.*
import org.acejump.search.Pattern
@@ -12,13 +15,30 @@ import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated actions that create or update an AceJump [Session].
*/
sealed class AceKeyboardAction : DumbAwareAction() {
abstract class AceKeyboardAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
invoke(SessionManager.start(e.getData(EDITOR) ?: return))
val editor = e.getData(EDITOR) ?: return
val project = e.project
if (project != null) {
try {
val openEditors = FileEditorManagerEx.getInstanceEx(project)
.splitters
.selectedEditors
.mapNotNull { (it as? TextEditor)?.editor }
.sortedBy { if (it === editor) 0 else 1 }
invoke(SessionManager.start(editor, openEditors))
} catch (e: IncorrectOperationException) {
invoke(SessionManager.start(editor))
}
}
else {
invoke(SessionManager.start(editor))
}
}
abstract operator fun invoke(session: Session)
@@ -27,14 +47,24 @@ sealed class AceKeyboardAction : DumbAwareAction() {
* Generic action type that starts a regex search.
*/
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries) : AceKeyboardAction() {
override fun invoke(session: Session) = session.startRegexSearch(pattern, boundaries)
override fun invoke(session: Session) {
session.defaultBoundary = boundaries
session.startRegexSearch(pattern)
}
}
/**
* Starts or ends an AceJump session.
* Starts or ends an AceJump session in quick jump mode.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.cycleMode()
override fun invoke(session: Session) = session.startJumpMode()
}
/**
* Starts or cycles main AceJump modes.
*/
object ActivateAceJumpSpecial : AceKeyboardAction() {
override fun invoke(session: Session) = session.startOrCycleSpecialModes()
}
// @formatter:off

View File

@@ -1,16 +1,9 @@
package org.acejump.action
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction
import com.intellij.find.actions.FindUsagesAction
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.ide.actions.CopyAction
import com.intellij.ide.actions.CutAction
import com.intellij.ide.actions.PasteAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
@@ -19,11 +12,15 @@ import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.editor.actions.EditorActionUtil
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 com.intellij.openapi.util.TextRange
import com.intellij.refactoring.actions.RefactoringQuickListPopupAction
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.codeStyle.CodeStyleManager
import org.acejump.*
import org.acejump.search.SearchProcessor
import kotlin.math.max
@@ -32,14 +29,19 @@ import kotlin.math.max
* Base class for actions available after typing a tag.
*/
sealed class AceTagAction {
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
abstract 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) {
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) {
@@ -50,8 +52,20 @@ sealed class AceTagAction {
abstract fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int
}
abstract class BaseWordAction : BaseJumpAction() {
final override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
val matchingChars = countMatchingCharacters(editor, searchProcessor, offset)
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && editor.immutableText.let { it[targetOffset - 1].isWordPart && it[targetOffset].isWordPart }
return getCaretOffset(editor, offset, targetOffset, isInsideWord)
}
abstract fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int
}
abstract class BaseSelectAction : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
if (shiftMode) {
val caretModel = editor.caretModel
val oldCarets = caretModel.caretsAndSelections
@@ -77,26 +91,50 @@ sealed class AceTagAction {
protected abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int)
}
abstract class BaseWordAction : BaseJumpAction() {
final override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
val matchingChars = countMatchingCharacters(editor, searchProcessor, offset)
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && editor.immutableText.let { it[targetOffset - 1].isWordPart && it[targetOffset].isWordPart }
return getCaretOffset(editor, offset, targetOffset, isInsideWord)
}
abstract fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int
}
abstract class BaseCaretRestoringAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
abstract class BasePerCaretWriteAction(private val selector: AceTagAction) : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
doInvoke(editor, searchProcessor, offset, shiftMode)
selector(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
val range = editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) }
editor.caretModel.caretsAndSelections = oldCarets
invoke(editor, range, shiftMode)
}
protected abstract fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
protected abstract operator fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean)
protected fun insertAtCarets(editor: Editor, text: String) {
val document = editor.document
editor.caretModel.runForEachCaret {
if (it.hasSelection()) {
document.replaceString(it.selectionStart, it.selectionEnd, text)
fixIndents(editor, it.selectionStart, it.selectionEnd)
}
else {
document.insertString(it.offset, text)
fixIndents(editor, it.offset, it.offset + text.length)
}
}
}
private fun fixIndents(editor: Editor, startOffset: Int, endOffset: Int) {
val project = editor.project ?: return
val document = editor.document
val documentManager = PsiDocumentManager.getInstance(project)
documentManager.commitAllDocuments()
val file = documentManager.getPsiFile(document) ?: return
val text = document.charsSequence
if (startOffset > 0 && endOffset > startOffset + 1 && text[endOffset - 1] == '\n' && text[startOffset - 1] == '\n') {
CodeStyleManager.getInstance(project).adjustLineIndent(file, TextRange(startOffset, endOffset - 1))
}
else {
CodeStyleManager.getInstance(project).adjustLineIndent(file, TextRange(startOffset, endOffset))
}
}
}
private companion object {
@@ -120,8 +158,22 @@ sealed class AceTagAction {
caretModel.moveToOffset(cursorOffset)
}
fun performAction(action: AnAction) {
ActionManager.getInstance().tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
fun performAction(actionName: String) {
val actionManager = ActionManager.getInstance()
val action = actionManager.getAction(actionName)
if (action != null) {
actionManager.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
}
fun ensureEditorFocused(editor: Editor) {
val project = editor.project ?: return
val fem = FileEditorManagerEx.getInstanceEx(project)
val window = fem.windows.firstOrNull { (it.selectedEditor?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
if (window != null && window !== fem.currentWindow) {
fem.currentWindow = window
}
}
private fun addCurrentPositionToHistory(project: Project, document: Document) {
@@ -174,7 +226,7 @@ sealed class AceTagAction {
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordStartTag : BaseWordAction() {
object JumpToWordStart : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordStart(queryEndOffset)
@@ -190,7 +242,7 @@ sealed class AceTagAction {
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordEndTag : BaseWordAction() {
object JumpToWordEnd : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordEnd(queryEndOffset) + 1
@@ -199,6 +251,22 @@ sealed class AceTagAction {
}
}
/**
* On default action, places the caret at the end of the line.
* On shift action, adds the new caret to existing carets.
*/
object JumpToLineEnd : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
val document = editor.document
val line = document.getLineNumber(queryEndOffset)
return document.getLineEndOffset(line)
}
}
/**
* On default action, selects all characters covered by the search query.
* On shift action, adds the new selection to existing selections.
*/
object SelectQuery : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
recordCaretPosition(editor)
@@ -213,7 +281,7 @@ sealed class AceTagAction {
/**
* On default action, places the caret at the end of a word, and also selects the entire word. Word detection uses
* [Character.isJavaIdentifierPart] to count some special characters, such as underscores, as part of a word. If there is no word at the
* first character of the search query, then the caret is placed after the last character of the search query, and all text between the
* last character of the search query, then the caret is placed after the last character of the search query, and all text between the
* start and end of the search query is selected.
*
* On shift action, adds the new selection to existing selections.
@@ -226,17 +294,23 @@ sealed class AceTagAction {
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = JumpToWordStartTag.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val endOffset = JumpToWordEndTag.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val startOffset = JumpToWordStart.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val endOffset = JumpToWordEnd.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false)
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
}
}
}
/**
* On default action, places the caret at the end of a camel hump inside a word, and also selects the hump. If there is no word at the
* last character of the search query, then the search query is selected. See [SelectWord] and [SelectQuery] for details.
*
* On shift action, adds the new selection to existing selections.
*/
object SelectHump : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
@@ -251,37 +325,55 @@ sealed class AceTagAction {
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false)
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
}
}
}
/**
* On default action, selects the line at the tag, excluding the indent.
* On shift action, adds the new selection to existing selections.
*/
object SelectLine : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
editor.selectionModel.selectLineAtCaret()
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
val document = editor.document
val line = editor.caretModel.logicalPosition.line
val lineStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(document, line)
val lineEnd = document.getLineEndOffset(line)
selectRange(editor, lineStart, lineEnd)
}
}
/**
* On default action, places the caret at the last character of the search query, and then performs Extend Selection a set amount of
* times.
*
* On shift action, adds the new selection to existing selections.
*/
class SelectExtended(private val extendCount: Int) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
repeat(extendCount) {
performAction(action)
performAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
}
}
}
/**
* On default action, selects the range between the caret and a position decided by the provided [BaseJumpAction].
* On shift action, adds the new selection to existing selections.
*/
class SelectToCaret(private val jumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val caretModel = editor.caretModel
val oldOffset = caretModel.offset
val oldSelection = editor.selectionModel.takeIf { it.hasSelection(false) }?.let { it.selectionStart..it.selectionEnd }
jumper(editor, searchProcessor, offset, shiftMode = false)
jumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
val newOffset = caretModel.offset
@@ -294,87 +386,51 @@ sealed class AceTagAction {
}
}
/**
* On default action, selects the range between [firstOffset] and a position decided by the provided [BaseJumpAction].
* On shift action, adds the new selection to existing selections.
*/
class SelectBetweenPoints(private val firstOffset: Int, private val secondOffsetJumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false)
secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
selectRange(editor, firstOffset, editor.caretModel.offset)
}
}
class Cut(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
performAction(CutAction())
}
}
class Copy(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
performAction(CopyAction())
}
}
class Paste(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
performAction(PasteAction())
}
}
class Delete(private val selector: AceTagAction) : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
selector(editor, searchProcessor, offset, shiftMode = false)
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Delete").run<Throwable> {
editor.selectionModel.let { editor.document.deleteString(it.selectionStart, it.selectionEnd) }
}
}
}
class CloneToCaret(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val document = editor.document
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
val text = document.getText(editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) })
editor.caretModel.caretsAndSelections = oldCarets
/**
* Selects text based on the provided [selector] action and clones it at every existing caret, selecting the cloned text. If a caret
* has a selection, the selected text will be replaced.
*/
class CloneToCaret(selector: AceTagAction) : BasePerCaretWriteAction(selector) {
override fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean) {
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Clone").run<Throwable> {
insertAtCarets(editor, editor.document.getText(range))
}
}
}
/**
* Selects text based on the provided [selector] action and clones it to every existing caret, selecting the cloned text and deleting
* the original. If a caret has a selection, the selected text will be replaced.
*/
open class MoveToCaret(selector: AceTagAction) : BasePerCaretWriteAction(selector) {
override fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean) {
val difference = if (shiftMode) editor.caretModel.caretsAndSelections.sumBy {
val start = it.selectionStart?.let(editor::logicalPositionToOffset)
val end = it.selectionEnd?.let(editor::logicalPositionToOffset)
if (start == null || end == null || end > range.endOffset) 0 else range.length - (end - start)
} else 0
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Move").run<Throwable> {
val document = editor.document
val text = document.getText(range)
document.deleteString(range.startOffset, range.endOffset)
insertAtCarets(editor, text)
}
}
companion object {
fun insertAtCarets(editor: Editor, text: String) {
val document = editor.document
editor.caretModel.runForEachCaret {
if (it.hasSelection()) {
document.replaceString(it.selectionStart, it.selectionEnd, text)
}
else {
document.insertString(it.offset, text)
}
}
}
}
}
class MoveToCaret(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val document = editor.document
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
val start = editor.selectionModel.selectionStart
val end = editor.selectionModel.selectionEnd
val text = document.getText(TextRange(start, end))
editor.caretModel.caretsAndSelections = oldCarets
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Move").run<Throwable> {
document.deleteString(start, end)
CloneToCaret.insertAtCarets(editor, text)
if (shiftMode) {
editor.selectionModel.removeSelection(true)
editor.caretModel.moveToOffset(range.startOffset + difference)
}
}
}
@@ -382,41 +438,47 @@ sealed class AceTagAction {
/**
* On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
* On shift action, performs the Go To Type Declaration action, available via `Navigate | Type Declaration`.
* Always places the caret at the end of the search query.
* Always places the caret at the start of the word.
*/
object GoToDeclaration : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
object GoToDeclaration : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_GOTO_TYPE_DECLARATION else IdeActions.ACTION_GOTO_DECLARATION) }
}
}
/**
* On default action, performs the Show Usages action, available via the context menu.
* On shift action, performs the Find Usages action, available via the context menu.
* Always places the caret at the end of the search query.
* Always places the caret at the start of the word.
*/
object ShowUsages : BaseCaretRestoringAction() {
override fun doInvoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) FindUsagesAction() else ShowUsagesAction())
object ShowUsages : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_FIND_USAGES else ShowUsagesAction.ID) }
}
}
/**
* Performs the Show Context Actions action, available via the context menu or Alt+Enter.
* Always places the caret at the start of the word.
*/
object ShowIntentions : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStartTag(editor, searchProcessor, offset, shiftMode = false)
performAction(ShowIntentionActionsAction())
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS) }
}
}
/**
* On default action, performs the Refactor This action, available via the main menu.
* On shift action, performs the Rename... refactoring, available via the main menu.
* Always places the caret at the start of the word.
*/
object Refactor : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStartTag(editor, searchProcessor, offset, shiftMode = false)
performAction(RefactoringQuickListPopupAction())
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_RENAME else "Refactorings.QuickListPopupAction") }
}
}
}

View File

@@ -0,0 +1,148 @@
package org.acejump.action
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.modes.ActionMode
import org.acejump.modes.VimJumpMode
import org.acejump.search.Pattern
import org.acejump.session.Session
sealed class AceVimAction : AceKeyboardAction() {
protected abstract val boundary: Boundaries
final override fun invoke(session: Session) {
session.defaultBoundary = boundary
start(session)
}
protected open fun start(session: Session) {
session.startJumpMode(::VimJumpMode)
}
class JumpToChar : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
}
class JumpToCharAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
}
class JumpToCharBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
}
class LWordsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD)
}
}
class UWordsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD)
}
}
class LWordsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD)
}
}
class UWordsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD)
}
}
class LWordEndsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD_END)
}
}
class UWordEndsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD_END)
}
}
class LWordEndsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD_END)
}
}
class UWordEndsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD_END)
}
}
class GoToDeclaration : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.GoToDeclaration, shiftMode = false) }
}
}
class GoToTypeDeclaration : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.GoToDeclaration, shiftMode = true) }
}
}
class ShowIntentions : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.ShowIntentions, shiftMode = false) }
}
}
class ShowUsages : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.ShowUsages, shiftMode = false) }
}
}
class FindUsages : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.ShowUsages, shiftMode = true) }
}
}
class Refactor : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.Refactor, shiftMode = false) }
}
}
class Rename : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.Refactor, shiftMode = true) }
}
}
}

View File

@@ -35,21 +35,21 @@ enum class StandardBoundaries : Boundaries {
BEFORE_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0..(editor.caretModel.offset)
return 0 until editor.caretModel.offset
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset <= editor.caretModel.offset
return offset < editor.caretModel.offset
}
},
AFTER_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return editor.caretModel.offset until editor.document.textLength
return (editor.caretModel.offset + 1) until editor.document.textLength
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset >= editor.caretModel.offset
return offset > editor.caretModel.offset
}
}
}

View File

@@ -19,14 +19,14 @@ class AceConfig : PersistentStateComponent<AceSettings> {
get() = ServiceManager.getService(AceConfig::class.java).aceSettings
val layout get() = settings.layout
val minQueryLength get() = settings.minQueryLength
val jumpModeColor get() = settings.jumpModeColor
val fromCaretModeColor get() = settings.fromCaretModeColor
val advancedModeColor get() = settings.advancedModeColor
val betweenPointsModeColor get() = settings.betweenPointsModeColor
val textHighlightColor get() = settings.textHighlightColor
val tagForegroundColor get() = settings.tagForegroundColor
val tagBackgroundColor get() = settings.tagBackgroundColor
val acceptedTagColor get() = settings.acceptedTagColor
val roundedTagCorners get() = settings.roundedTagCorners
}
override fun getState(): AceSettings {

View File

@@ -14,26 +14,26 @@ class AceConfigurable : Configurable {
override fun isModified() =
panel.allowedChars != settings.allowedChars ||
panel.keyboardLayout != settings.layout ||
panel.minQueryLengthInt != settings.minQueryLength ||
panel.jumpModeColor != settings.jumpModeColor ||
panel.fromCaretModeColor != settings.fromCaretModeColor ||
panel.advancedModeColor != settings.advancedModeColor ||
panel.betweenPointsModeColor != settings.betweenPointsModeColor ||
panel.textHighlightColor != settings.textHighlightColor ||
panel.tagForegroundColor != settings.tagForegroundColor ||
panel.tagBackgroundColor != settings.tagBackgroundColor ||
panel.acceptedTagColor != settings.acceptedTagColor ||
panel.roundedTagCorners != settings.roundedTagCorners
panel.acceptedTagColor != settings.acceptedTagColor
override fun apply() {
settings.allowedChars = panel.allowedChars
settings.layout = panel.keyboardLayout
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.fromCaretModeColor?.let { settings.fromCaretModeColor = it }
panel.advancedModeColor?.let { settings.advancedModeColor = it }
panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = 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 }
settings.roundedTagCorners = panel.roundedTagCorners
KeyLayoutCache.reset(settings)
}

View File

@@ -8,27 +8,26 @@ import java.awt.Color
data class AceSettings(
var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars,
var minQueryLength: Int = 1,
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color.BLUE,
var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("fromCaretModeRGB", converter = ColorConverter::class)
var fromCaretModeColor: Color = Color.ORANGE,
@OptionTag("advancedModeRGB", converter = ColorConverter::class)
var advancedModeColor: Color = Color(0xFFB700),
@OptionTag("betweenPointsModeRGB", converter = ColorConverter::class)
var betweenPointsModeColor: Color = Color.YELLOW,
var betweenPointsModeColor: Color = Color(0x6FC5FF),
@OptionTag("textHighlightRGB", converter = ColorConverter::class)
var textHighlightColor: Color = Color.GREEN,
var textHighlightColor: Color = Color(0x394B58),
@OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var tagForegroundColor: Color = Color.BLACK,
var tagForegroundColor: Color = Color(0xFFFFFF),
@OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
var tagBackgroundColor: Color = Color.YELLOW,
var tagBackgroundColor: Color = Color(0x008299),
@OptionTag("acceptedTagRGB", converter = ColorConverter::class)
var acceptedTagColor: Color = Color.CYAN,
var roundedTagCorners: Boolean = true
var acceptedTagColor: Color = Color(0x394B58)
)

View File

@@ -2,7 +2,6 @@ package org.acejump.config
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.Cell
@@ -26,14 +25,14 @@ internal class AceSettingsPanel {
private val tagCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val minQueryLengthField = JBTextField()
private val jumpModeColorWheel = ColorPanel()
private val fromCaretModeColorWheel = ColorPanel()
private val advancedModeColorWheel = ColorPanel()
private val betweenPointsModeColorWheel = ColorPanel()
private val textHighlightColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel()
private val tagBackgroundColorWheel = ColorPanel()
private val acceptedTagColorWheel = ColorPanel()
private val roundedTagCornersCheckBox = JBCheckBox()
init {
tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
@@ -51,45 +50,49 @@ internal class AceSettingsPanel {
row("Keyboard design:") { short(keyboardLayoutArea) }
}
titledRow("Behavior") {
row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
}
titledRow("Colors") {
row("Jump mode caret background:") { short(jumpModeColorWheel) }
row("From Caret mode caret background:") { short(fromCaretModeColorWheel) }
row("Advanced mode caret background:") { short(advancedModeColorWheel) }
row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) }
row("Searched text background:") { short(textHighlightColorWheel) }
row("Tag foreground:") { short(tagForegroundColorWheel) }
row("Tag background:") { short(tagBackgroundColorWheel) }
row("Accepted tag position background:") { short(acceptedTagColorWheel) }
}
titledRow("Appearance") {
row { short(roundedTagCornersCheckBox.apply { text = "Rounded tag corners" }) }
}
}
// Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342
internal var allowedChars by tagCharsField
internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea
internal var minQueryLength by minQueryLengthField
internal var jumpModeColor by jumpModeColorWheel
internal var fromCaretModeColor by fromCaretModeColorWheel
internal var advancedModeColor by advancedModeColorWheel
internal var betweenPointsModeColor by betweenPointsModeColorWheel
internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel
internal var acceptedTagColor by acceptedTagColorWheel
internal var roundedTagCorners by roundedTagCornersCheckBox
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) { minQueryLength = value.toString() }
fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars
keyboardLayout = settings.layout
minQueryLength = settings.minQueryLength.toString()
jumpModeColor = settings.jumpModeColor
fromCaretModeColor = settings.fromCaretModeColor
advancedModeColor = settings.advancedModeColor
betweenPointsModeColor = settings.betweenPointsModeColor
textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor
tagBackgroundColor = settings.tagBackgroundColor
acceptedTagColor = settings.acceptedTagColor
roundedTagCorners = settings.roundedTagCorners
}
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575

View File

@@ -0,0 +1,12 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.search.Tag
import org.acejump.session.SessionState
class ActionMode(private val action: AceTagAction, private val shiftMode: Boolean) : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
state.act(action, acceptedTag, shiftMode, isFinal = true)
return true
}
}

View File

@@ -0,0 +1,73 @@
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
class AdvancedMode : SessionMode {
companion object {
private val JUMP_HINT = arrayOf(
"<f>[J]</f>ump / <f>[L]</f> past Query",
"<f>[E]</f> Word End / <f>[M]</f> Line End"
)
val JUMP_ALT_HINT = JUMP_HINT.map { it.replace("<f>[J]</f>ump ", "<f>[J]</f> at Tag ") }.toTypedArray()
val JUMP_ACTION_MAP = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'L' to AceTagAction.JumpPastSearchEnd,
'E' to AceTagAction.JumpToWordEnd,
'M' to AceTagAction.JumpToLineEnd
)
val SELECT_HINT = arrayOf(
"Select <f>[W]</f>ord / <f>[H]</f>ump",
"Select <f>[Q]</f>uery / <f>[N]</f> Line / <f>[1-9]</f> Expansion"
)
val SELECT_ACTION_MAP = mapOf(
'W' to AceTagAction.SelectWord,
'H' to AceTagAction.SelectHump,
'Q' to AceTagAction.SelectQuery,
'N' to AceTagAction.SelectLine,
*('1'..'9').mapIndexed { index, char -> char to AceTagAction.SelectExtended(index + 1) }.toTypedArray()
)
private val ALL_HINTS = arrayOf(
*JUMP_HINT,
*SELECT_HINT,
)
private val ALL_ACTION_MAP = mapOf(
*JUMP_ACTION_MAP.map { it.key to it.value }.toTypedArray(),
*SELECT_ACTION_MAP.map { it.key to it.value }.toTypedArray(),
)
}
override val caretColor
get() = AceConfig.advancedModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
if (acceptedTag == null) {
return state.type(charTyped)
}
val action = ALL_ACTION_MAP[charTyped.toUpperCase()]
if (action != null) {
state.act(action, acceptedTag, shiftMode = charTyped.isUpperCase(), isFinal = true)
return TypeResult.EndSession
}
return TypeResult.Nothing
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
return false
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return ALL_HINTS.takeIf { acceptedTag != null }
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
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
fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>?
}

View File

@@ -0,0 +1,17 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.search.Tag
import org.acejump.session.SessionState
class VimJumpMode : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
val action = if (acceptedTag.editor.selectionModel.hasSelection())
AceTagAction.SelectToCaret(AceTagAction.JumpToSearchStart)
else
AceTagAction.JumpToSearchStart
state.act(action, acceptedTag, wasUpperCase, isFinal = true)
return true
}
}

View File

@@ -5,5 +5,9 @@ enum class Pattern(val regex: String) {
LINE_ENDS("\\n|\\Z"),
LINE_INDENTS("[^\\s].*|^\\n"),
LINE_ALL_MARKS(LINE_ENDS.regex + "|" + LINE_STARTS.regex + "|" + LINE_INDENTS.regex),
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]");
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
VIM_LWORD("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
VIM_UWORD("(?<=\\s|\\A)[^\\s]"),
VIM_LWORD_END("[a-zA-Z0-9_](?=[^a-zA-Z0-9_]|\\Z)"),
VIM_UWORD_END("[^\\s](?=\\s|\\Z)")
}

View File

@@ -3,6 +3,7 @@ package org.acejump.search
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries
import org.acejump.clone
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.matchesAt
@@ -10,43 +11,52 @@ import org.acejump.matchesAt
/**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character.
*/
class SearchProcessor private constructor(private val editor: Editor, query: SearchQuery) {
class SearchProcessor private constructor(
private val editors: List<Editor>, query: SearchQuery, results: MutableMap<Editor, IntArrayList>
) {
companion object {
fun fromChar(editor: Editor, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.Literal(char.toString()), boundaries)
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
}
fun fromRegex(editor: Editor, pattern: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.RegularExpression(pattern), boundaries)
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
}
}
private constructor(editor: Editor, query: SearchQuery, boundaries: Boundaries) : this(editor, query) {
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) {
val regex = query.toRegex()
if (regex != null) {
for (editor in editors) {
val offsets = IntArrayList()
val offsetRange = boundaries.getOffsetRange(editor)
var result = regex.find(editor.immutableText, offsetRange.first)
while (result != null) {
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception.
val highlightEnd = index + query.getHighlightLength("", index)
if (boundaries.isOffsetInside(editor, index)) {
results.add(index)
}
else if (index > offsetRange.last) {
if (highlightEnd > offsetRange.last) {
break
}
else if (boundaries.isOffsetInside(editor, index)) {
offsets.add(index)
}
result = result.next()
}
results[editor] = offsets
}
}
}
internal var query = query
private set
internal var results = IntArrayList(0)
internal var results = results
private set
/**
@@ -56,13 +66,12 @@ class SearchProcessor private constructor(private val editor: Editor, query: Sea
*/
fun type(char: Char, tagger: Tagger): Boolean {
val newQuery = query.rawText + char
val chars = editor.immutableText
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 (newQuery.length > 1 && !canMatchTag && results.none { chars.matchesAt(it, newQuery, ignoreCase = true) }) {
if (newQuery.length > 1 && !canMatchTag && !isContinuation(newQuery)) {
return false
}
@@ -75,7 +84,10 @@ class SearchProcessor private constructor(private val editor: Editor, query: Sea
query = SearchQuery.Literal(char.toString())
tagger.unmark()
val iter = results.iterator()
for ((editor, offsets) in results) {
val chars = editor.immutableText
val iter = offsets.iterator()
while (iter.hasNext()) {
val movedOffset = iter.nextInt() + newQuery.length - 1
@@ -87,6 +99,7 @@ class SearchProcessor private constructor(private val editor: Editor, query: Sea
}
}
}
}
else {
removeObsoleteResults(newQuery, tagger)
query = SearchQuery.Literal(newQuery)
@@ -95,6 +108,20 @@ class SearchProcessor private constructor(private val editor: Editor, query: Sea
return true
}
/**
* Returns true if the new query is a continuation of any remaining search query.
*/
private fun isContinuation(newQuery: String): Boolean {
for ((editor, offsets) in results) {
val chars = editor.immutableText
if (offsets.any { chars.matchesAt(it, newQuery, ignoreCase = true) }) {
return true
}
}
return false
}
/**
* After updating the query, removes all results that no longer match the search query.
*/
@@ -102,25 +129,28 @@ class SearchProcessor private constructor(private val editor: Editor, query: Sea
val lastCharOffset = newQuery.lastIndex
val lastChar = newQuery[lastCharOffset]
val ignoreCase = newQuery[0].isLowerCase()
for ((editor, offsets) in results.entries.toList()) {
val chars = editor.immutableText
val remaining = IntArrayList()
val iter = results.iterator()
val iter = offsets.iterator()
while (iter.hasNext()) {
val offset = iter.nextInt()
val endOffset = offset + lastCharOffset
val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase)
if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, offset)) {
if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
remaining.add(offset)
}
}
results = remaining
results[editor] = remaining
}
}
fun clone(): SearchProcessor {
return SearchProcessor(editor, query).also { it.results.addAll(results) }
return SearchProcessor(editors, query, results.clone())
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
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()
}
}

View File

@@ -1,5 +1,6 @@
package org.acejump.search
import com.google.common.collect.ArrayListMultimap
import com.google.common.collect.HashBiMap
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
@@ -11,23 +12,25 @@ import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache.allPossibleTags
import org.acejump.isWordPart
import org.acejump.matchesAt
import org.acejump.view.Tag
import org.acejump.view.TagMarker
import java.util.AbstractMap.SimpleImmutableEntry
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.min
/**
* Assigns tags to search occurrences, updates them when the search query changes, and requests a jump if the search query matches a tag.
* The ordering of [editors] may be used to prioritize tagging editors earlier in the list in case of conflicts.
*/
class Tagger(private val editor: Editor) {
private var tagMap = HashBiMap.create<String, Int>()
class Tagger(private val editors: List<Editor>) {
private var tagMap = HashBiMap.create<String, Tag>()
val hasTags
get() = tagMap.isNotEmpty()
@ExternalUsage
internal val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value }
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value.offset }
/**
* Removes all markers, allowing them to be regenerated from scratch.
@@ -43,7 +46,7 @@ class Tagger(private val editor: Editor) {
*
* Note that the [results] collection will be mutated.
*/
internal fun update(query: SearchQuery, results: 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).toLowerCase()
@@ -57,7 +60,9 @@ class Tagger(private val editor: Editor) {
}
if (queryText.length == 1) {
removeResultsWithOverlappingTags(results)
for ((editor, offsets) in results) {
removeResultsWithOverlappingTags(editor, offsets)
}
}
}
@@ -65,20 +70,26 @@ class Tagger(private val editor: Editor) {
tagMap = assignTagsAndMerge(results, availableTags, query, queryText)
}
return TaggingResult.Mark(createTagMarkers(results, query.rawText.ifEmpty { null }))
val resultTags = results.flatMap { (editor, offsets) -> offsets.map { Tag(editor, it) } }
return TaggingResult.Mark(createTagMarkers(resultTags, query.rawText.ifEmpty { null }))
}
fun clone(): Tagger {
return Tagger(editor).also { it.tagMap.putAll(tagMap) }
return Tagger(editors).also { it.tagMap.putAll(tagMap) }
}
/**
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
*/
private fun assignTagsAndMerge(results: IntList, availableTags: List<String>, query: SearchQuery, queryText: String): HashBiMap<String, Int> {
val cache = EditorOffsetCache.new()
private fun assignTagsAndMerge(
results: Map<Editor, IntList>, availableTags: List<String>, query: SearchQuery, queryText: String,
): HashBiMap<String, Tag> {
val caches = results.keys.associateWith { EditorOffsetCache.new() }
results.sort { a, b ->
for ((editor, offsets) in results) {
val cache = caches.getValue(editor)
offsets.sort { a, b ->
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
@@ -88,52 +99,61 @@ class Tagger(private val editor: Editor) {
else -> 0
}
}
}
val allAssignedTags = mutableMapOf<String, Int>()
val oldCompatibleTags = tagMap.filter { isTagCompatibleWithQuery(it.key, it.value, queryText) || it.value in results }
val vacantResults: IntList
val allAssignedTags = mutableMapOf<String, Tag>()
val oldCompatibleTags = tagMap.filter { (mark, tag) ->
isTagCompatibleWithQuery(mark, tag, queryText) || results[tag.editor]?.contains(tag.offset) == true
}
val vacantResults: Map<Editor, IntList>
if (oldCompatibleTags.isEmpty()) {
vacantResults = results
}
else {
vacantResults = IntArrayList()
val vacant = mutableMapOf<Editor, IntList>()
for ((editor, offsets) in results) {
val list = IntArrayList()
val iter = offsets.iterator()
val iter = results.iterator()
while (iter.hasNext()) {
val offset = iter.nextInt()
val tag = Tag(editor, iter.nextInt())
if (tag !in oldCompatibleTags.values) {
list.add(tag.offset)
}
}
if (offset !in oldCompatibleTags.values) {
vacantResults.add(offset)
}
vacant[editor] = list
}
vacantResults = vacant
}
allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editor, query, vacantResults, results, availableTags, cache))
allAssignedTags.putAll(Solver.solve(editors, query, vacantResults, results, availableTags, caches))
val assignedMarkers = allAssignedTags.keys.groupBy { it[0] }
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
// Avoid matching query - will trigger a jump.
// TODO: lift this constraint.
val queryEndsWith = queryText.endsWith(tag[0]) || queryText.endsWith(tag)
if (!queryEndsWith && canShortenTag(tag, allAssignedTags))
if (canShortenTag(tag, assignedMarkers, queryText))
tag[0].toString()
else
tag
}
}
private infix fun Map.Entry<String, Int>.solves(query: String): Boolean {
private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean {
return query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
}
private fun isTagCompatibleWithQuery(tag: String, offset: Int, query: String): Boolean {
return editor.immutableText.matchesAt(offset, getPlaintextPortion(query, tag), 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, offset: Int): Boolean {
return tagMap.inverse()[offset].let { it != null && isTagCompatibleWithQuery(it, offset, query) }
fun isQueryCompatibleWithTagAt(query: String, tag: Tag): Boolean {
return tagMap.inverse()[tag].let { it != null && isTagCompatibleWithQuery(it, tag, query) }
}
fun canQueryMatchAnyTag(query: String): Boolean {
@@ -143,8 +163,8 @@ class Tagger(private val editor: Editor) {
}
}
private fun removeResultsWithOverlappingTags(results: IntList) {
val iter = results.iterator()
private fun removeResultsWithOverlappingTags(editor: Editor, offsets: IntList) {
val iter = offsets.iterator()
val chars = editor.immutableText
while (iter.hasNext()) {
@@ -154,9 +174,18 @@ class Tagger(private val editor: Editor) {
}
}
private fun createTagMarkers(results: IntList, literalQueryText: String?): List<Tag> {
private fun createTagMarkers(tags: Collection<Tag>, literalQueryText: String?): MutableMap<Editor, Collection<TagMarker>> {
val tagMapInv = tagMap.inverse()
return results.mapNotNull { index -> tagMapInv[index]?.let { tag -> Tag.create(editor, tag, index, literalQueryText) } }
val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tags.size, 50))
for (tag in tags) {
val mark = tagMapInv[tag] ?: continue
val editor = tag.editor
val marker = TagMarker.create(editor, mark, tag.offset, literalQueryText)
markers.put(editor, marker)
}
return markers.asMap()
}
private companion object {
@@ -177,26 +206,28 @@ class Tagger(private val editor: Editor) {
return this.isWordPart xor other.isWordPart || this.isWhitespace() xor other.isWhitespace()
}
private fun getPlaintextPortion(query: String, tag: String) = when {
query.endsWith(tag, true) -> query.dropLast(tag.length)
query.endsWith(tag.first(), true) -> query.dropLast(1)
private fun getPlaintextPortion(query: String, marker: String) = when {
query.endsWith(marker, true) -> query.dropLast(marker.length)
query.endsWith(marker.first(), true) -> query.dropLast(1)
else -> query
}
private fun getTagPortion(query: String, tag: String) = when {
query.endsWith(tag, true) -> query.takeLast(tag.length)
query.endsWith(tag.first(), true) -> query.takeLast(1)
private fun getTagPortion(query: String, marker: String) = when {
query.endsWith(marker, true) -> query.takeLast(marker.length)
query.endsWith(marker.first(), true) -> query.takeLast(1)
else -> ""
}
private fun canShortenTag(tag: String, tagMap: Map<String, Int>): Boolean {
for (other in tagMap.keys) {
if (tag != other && tag[0] == other[0]) {
private fun canShortenTag(marker: String, markers: 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
}
}
}

View File

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

View File

@@ -12,12 +12,16 @@ import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import com.intellij.ui.LightweightHint
import org.acejump.ExternalUsage
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.clone
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.EditorKeyListener
import org.acejump.input.KeyLayoutCache
import org.acejump.modes.AdvancedMode
import org.acejump.modes.BetweenPointsMode
import org.acejump.modes.FromCaretMode
import org.acejump.modes.JumpMode
import org.acejump.modes.SessionMode
import org.acejump.search.*
import org.acejump.view.TagCanvas
import org.acejump.view.TextHighlighter
@@ -25,34 +29,36 @@ import org.acejump.view.TextHighlighter
/**
* Manages an AceJump session for a single [Editor].
*/
class Session(private val editor: Editor) {
private val editorSettings = EditorSettings.setup(editor)
class Session(private val mainEditor: Editor, private val jumpEditors: List<Editor>) {
private val editorSettings = EditorSettings.setup(mainEditor)
private lateinit var mode: SessionMode
private var state: SessionState? = null
private var tagger = Tagger(editor)
private var state: SessionStateImpl? = null
private var tagger = Tagger(jumpEditors)
private var acceptedTag: Int? = null
private var acceptedTag: Tag? = null
set(value) {
field = value
if (value != null) {
tagCanvas.removeMarkers()
editorSettings.onTagAccepted(editor)
tagCanvases.values.forEach(TagCanvas::removeMarkers)
editorSettings.onTagAccepted(mainEditor)
}
}
private val textHighlighter = TextHighlighter(editor)
private val tagCanvas = TagCanvas(editor)
private val textHighlighter = TextHighlighter()
private val tagCanvases = jumpEditors.associateWith(::TagCanvas)
@ExternalUsage
val tags
get() = tagger.tags
var defaultBoundary: Boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
init {
KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(editor, object : TypedActionHandler {
EditorKeyListener.attach(mainEditor, object : TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
val state = state ?: return
val hadTags = tagger.hasTags
@@ -63,9 +69,14 @@ class Session(private val editor: Editor) {
when (result) {
TypeResult.Nothing -> updateHint()
TypeResult.RestartSearch -> restart().also { this@Session.state = SessionState(editor, tagger); updateHint() }
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
is TypeResult.ChangeMode -> setMode(result.mode)
TypeResult.RestartSearch -> restart().also {
this@Session.state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
updateHint()
}
TypeResult.EndSession -> end()
}
}
@@ -74,27 +85,32 @@ class Session(private val editor: Editor) {
/**
* Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag.
* If all tags are outside view, scrolls to the closest one.
*/
private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean) {
val query = processor.query
val results = processor.results
if (query is SearchQuery.Literal && !markImmediately && query.rawText.let { it.length < 2 && 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.update(query, results.clone())) {
is TaggingResult.Accept -> {
val offset = result.offset
acceptedTag = offset
textHighlighter.renderFinal(offset, processor.query)
acceptedTag = result.tag
textHighlighter.renderFinal(result.tag, processor.query)
if (state?.let { mode.accept(it, result.tag) } == true) {
end()
return
}
}
is TaggingResult.Mark -> {
val tags = result.tags
tagCanvas.setMarkers(tags, isRegex = query is SearchQuery.RegularExpression)
for ((editor, canvas) in tagCanvases) {
canvas.setMarkers(result.markers[editor].orEmpty())
}
textHighlighter.renderOccurrences(results, query)
}
}
@@ -104,73 +120,108 @@ class Session(private val editor: Editor) {
private fun setMode(mode: SessionMode) {
this.mode = mode
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
updateHint()
}
private fun updateHint() {
val hintArray = mode.getHint(acceptedTag) ?: return
val acceptedTag = acceptedTag
val editor = mainEditor
val offset = when {
acceptedTag == null -> null
acceptedTag.editor === editor -> acceptedTag.offset
else -> mainEditor.caretModel.offset
}
val hintArray = mode.getHint(offset, state?.currentProcessor.let { it != null && it.query.rawText.isNotEmpty() }) ?: return
val hintText = hintArray
.joinToString("\n")
.replace("<f>", "<span style=\"font-family:'${editor.colorsScheme.editorFontName}';font-weight:bold\">")
.replace("</f>", "</span>")
.replace("<h>", "<b><u>")
.replace("</h>", "</u></b>")
val hint = LightweightHint(HintUtil.createInformationLabel(hintText))
val pos = acceptedTag?.let(editor::offsetToLogicalPosition) ?: editor.caretModel.logicalPosition
val pos = offset?.let(editor::offsetToLogicalPosition) ?: editor.caretModel.logicalPosition
val point = HintManagerImpl.getHintPosition(hint, editor, pos, HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, point, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, true, info)
}
fun cycleMode() {
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)
}
fun startOrCycleSpecialModes() {
if (!this::mode.isInitialized) {
setMode(JumpMode())
state = SessionState(editor, tagger)
setMode(AdvancedMode())
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
return
}
restart()
setMode(when (mode) {
is JumpMode -> FromCaretMode()
is FromCaretMode -> BetweenPointsMode()
else -> JumpMode()
is AdvancedMode -> BetweenPointsMode()
else -> AdvancedMode()
})
state = SessionState(editor, tagger)
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: String, boundaries: Boundaries) {
if (this::mode.isInitialized) {
end()
return
fun startRegexSearch(pattern: Pattern) {
if (!this::mode.isInitialized) {
setMode(JumpMode())
}
setMode(JumpMode())
tagger = Tagger(editor)
tagCanvas.setMarkers(emptyList(), isRegex = true)
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary).also {
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary, it)
}
val processor = SearchProcessor.fromRegex(editor, pattern, boundaries).also { state = SessionState(editor, tagger, it) }
updateSearch(processor, markImmediately = true)
}
/**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) {
startRegexSearch(pattern.regex, boundaries)
fun tagImmediately() {
val state = state ?: return
val processor = state.currentProcessor
if (processor != null) {
updateSearch(processor, markImmediately = true)
}
else if (mode is AdvancedMode) {
val offset = mainEditor.caretModel.offset
val result = mainEditor.immutableText.getOrNull(offset)?.let(state::type)
if (result is TypeResult.UpdateResults) {
val tag = Tag(mainEditor, offset).also { acceptedTag = it }
textHighlighter.renderFinal(tag, result.processor.query)
updateHint()
}
}
}
/**
* Ends this session.
*/
fun end() {
SessionManager.end(editor)
SessionManager.end(mainEditor)
}
/**
@@ -178,31 +229,33 @@ class Session(private val editor: Editor) {
*/
fun restart() {
state = null
tagger = Tagger(editor)
tagger = Tagger(jumpEditors)
acceptedTag = null
tagCanvas.removeMarkers()
tagCanvases.values.forEach(TagCanvas::removeMarkers)
textHighlighter.reset()
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.onTagUnaccepted(editor)
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
editor.contentComponent.repaint()
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.
*/
internal fun dispose() {
tagger = Tagger(editor)
tagCanvas.unbind()
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach(TagCanvas::unbind)
textHighlighter.reset()
EditorKeyListener.detach(editor)
EditorKeyListener.detach(mainEditor)
if (!editor.isDisposed) {
if (!mainEditor.isDisposed) {
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.restore(editor)
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
editorSettings.restore(mainEditor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
val focusedEditor = acceptedTag?.editor ?: mainEditor
focusedEditor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
}
}

View File

@@ -16,7 +16,16 @@ object SessionManager {
* Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
*/
fun start(editor: Editor): Session {
return sessions.getOrPut(editor) { cleanup(); Session(editor) }
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.
*/
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
}
/**

View File

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

View File

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

View 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) }
}
}

View File

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

View File

@@ -4,10 +4,8 @@ import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.ui.ColorUtil
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Rectangle
@@ -19,8 +17,7 @@ import javax.swing.SwingUtilities
* Holds all active tag markers and renders them on top of the editor.
*/
internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener {
private var markers: List<Tag>? = null
private var isRegex = false
private var markers: Collection<TagMarker>? = null
init {
val contentComponent = editor.contentComponent
@@ -48,9 +45,8 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
repaint()
}
fun setMarkers(markers: List<Tag>, isRegex: Boolean) {
fun setMarkers(markers: Collection<TagMarker>) {
this.markers = markers
this.isRegex = isRegex
repaint()
}
@@ -68,14 +64,15 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
super.paintChildren(g)
val markers = markers ?: return
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val font = TagFont(editor)
val cache = EditorOffsetCache.new()
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val occupied = mutableListOf<Rectangle>()
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
// If there is a tag at the caret location, prioritize its rendering over all other tags. This is helpful for seeing which tag is
// currently selected while navigating highly clustered tags, although it does end up rearranging nearby tags which can be confusing.
@@ -84,18 +81,12 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
val caretOffset = editor.caretModel.offset
val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset }
val caretRect = caretMarker?.paint(g, editor, cache, font, occupied, isRegex)
caretMarker?.paint(g, editor, cache, font, occupied)
for (marker in markers) {
if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
marker.paint(g, editor, cache, font, occupied, isRegex)
marker.paint(g, editor, cache, font, occupied)
}
}
if (caretRect != null) {
g.color = ColorUtil.brighter(AceConfig.tagBackgroundColor, 10)
// Only adding 1 to width because it seems the right side of the tag highlight is slightly off.
g.drawRoundRect(caretRect.x - 1, caretRect.y, caretRect.width + 1, caretRect.height, font.tagCornerArc, font.tagCornerArc)
}
}
}

View File

@@ -1,18 +1,22 @@
package org.acejump.view;
package org.acejump.view
import com.intellij.openapi.editor.Editor
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 [Tag]s.
* Stores font metrics for aligning and rendering [TagMarker]s.
*/
internal class TagFont(editor: Editor) {
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.BOLD)
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('w')
val tagCornerArc = if (AceConfig.roundedTagCorners) editor.colorsScheme.editorFontSize - 3 else 1
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W')
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

View File

@@ -1,11 +1,10 @@
package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil
import com.intellij.openapi.util.SystemInfo
import com.intellij.ui.scale.JBUIScale
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import java.awt.Color
@@ -17,7 +16,7 @@ import kotlin.math.max
/**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/
internal class Tag(
internal class TagMarker(
private val tag: String,
val offsetL: Int,
val offsetR: Int,
@@ -27,11 +26,20 @@ internal class Tag(
private val length = tag.length
companion object {
private const val ARC = 1
/**
* TODO This might be due to DPI settings.
*/
private val HIGHLIGHT_OFFSET = if (SystemInfo.isMac) -0.5 else 0.0
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.
*/
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): Tag {
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker {
val chars = editor.immutableText
val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
@@ -41,24 +49,35 @@ internal class Tag(
else
tag.toUpperCase()
return Tag(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color, arc: Int) {
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.fillRoundRect(rect.x, rect.y + 1, rect.width, rect.height - 1, arc, arc)
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
g.translate(0.0, -HIGHLIGHT_OFFSET)
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) {
val x = point.x + 2
val y = point.y + font.baselineDistance
g.font = font.tagFont
g.color = AceConfig.tagForegroundColor
g.drawString(text, point.x, point.y + font.baselineDistance)
if (!font.isForegroundDark) {
g.color = SHADOW_COLOR
g.drawString(text, x + 1, y + 1)
}
g.color = font.foregroundColor
g.drawString(text, x, y)
}
}
@@ -70,56 +89,36 @@ internal class Tag(
return offsetL in range
}
/**
* Determines on which side of the target character the tag is positioned.
*/
enum class TagAlignment {
LEFT,
RIGHT
}
/**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
* Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap.
*/
fun paint(
g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>, isRegex: Boolean
): Rectangle? {
val (rect, alignment) = alignTag(editor, cache, font, occupied) ?: return null
fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? {
val rect = alignTag(editor, cache, font, occupied) ?: return null
val highlightColor = when {
alignment != TagAlignment.RIGHT || hasSpaceRight || isRegex -> AceConfig.tagBackgroundColor
else -> ColorUtil.darker(AceConfig.tagBackgroundColor, 3)
}
drawHighlight(g, rect, highlightColor, font.tagCornerArc)
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) })
return rect
}
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Pair<Rectangle, TagAlignment>? {
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) {
val rectR = createRightAlignedTagRect(editor, cache, font)
return (rectR to TagAlignment.RIGHT).takeIf {
boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects)
}
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects) }
}
val rectL = createLeftAlignedTagRect(editor, cache, font)
if (occupied.none(rectL::intersects)) {
return (rectL to TagAlignment.LEFT).takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
return rectL.takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
}
val rectR = createRightAlignedTagRect(editor, cache, font)
if (occupied.none(rectR::intersects)) {
return (rectR to TagAlignment.RIGHT).takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
}
return null
@@ -128,12 +127,12 @@ internal class Tag(
private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetR)
val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR)
return Rectangle(pos.x + shift, pos.y, font.tagCharWidth * length, font.lineHeight)
return Rectangle(pos.x + shift, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
}
private fun createLeftAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetL)
val shift = -(font.tagCharWidth * length)
return Rectangle(pos.x + shift, pos.y, font.tagCharWidth * length, font.lineHeight)
return Rectangle(pos.x + shift - 4, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
}
}

View File

@@ -12,20 +12,21 @@ import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.search.SearchQuery
import org.acejump.search.Tag
import java.awt.Color
import java.awt.Graphics
/**
* Renders highlights for search occurrences.
*/
internal class TextHighlighter(private val editor: Editor) {
private var previousHighlights: Array<RangeHighlighter>? = null
internal class TextHighlighter {
private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>()
/**
* Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change.
*/
fun renderOccurrences(offsets: IntList, query: SearchQuery) {
render(offsets, when (query) {
fun renderOccurrences(results: Map<Editor, IntList>, query: SearchQuery) {
render(results, when (query) {
is SearchQuery.RegularExpression -> RegexRenderer
else -> SearchedWordRenderer
}, query::getHighlightLength)
@@ -34,28 +35,28 @@ internal class TextHighlighter(private val editor: Editor) {
/**
* Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
*/
fun renderFinal(offset: Int, query: SearchQuery) {
render(IntArrayList(intArrayOf(offset)), AcceptedTagRenderer, query::getHighlightLength)
fun renderFinal(tag: Tag, query: SearchQuery) {
render(mutableMapOf(tag.editor to IntArrayList(intArrayOf(tag.offset))), AcceptedTagRenderer, query::getHighlightLength)
}
private inline fun render(offsets: IntList, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
for ((editor, offsets) in results) {
val highlights = previousHighlights[editor]
val markup = editor.markupModel
val document = editor.document
val chars = editor.immutableText
ARC = TagFont(editor).tagCornerArc
val modifications = (previousHighlights?.size ?: 0) + offsets.size
val modifications = (highlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000
val document = editor.document
try {
if (enableBulkEditing) {
document.isInBulkUpdate = true
}
previousHighlights?.forEach(markup::removeHighlighter)
previousHighlights = Array(offsets.size) { index ->
highlights?.forEach(markup::removeHighlighter)
previousHighlights[editor] = Array(offsets.size) { index ->
val start = offsets.getInt(index)
val end = start + getHighlightLength(chars, start)
@@ -70,9 +71,16 @@ internal class TextHighlighter(private val editor: Editor) {
}
}
for (editor in previousHighlights.keys.toList()) {
if (!results.containsKey(editor)) {
previousHighlights.remove(editor)?.forEach(editor.markupModel::removeHighlighter)
}
}
}
fun reset() {
editor.markupModel.removeAllHighlighters()
previousHighlights = null
previousHighlights.keys.forEach { it.markupModel.removeAllHighlighters() }
previousHighlights.clear()
}
/**
@@ -104,14 +112,16 @@ internal class TextHighlighter(private val editor: Editor) {
private companion object {
private const val LAYER = HighlighterLayer.LAST + 1
private var ARC = 0
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int, color: Color) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = color
g.fillRoundRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1, ARC, ARC)
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)
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int, color: Color) {
@@ -121,7 +131,10 @@ internal class TextHighlighter(private val editor: Editor) {
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = color
g.fillRoundRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1, ARC, ARC)
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
}
}
}

View File

@@ -1,6 +1,6 @@
<idea-plugin url="https://github.com/acejump/AceJump">
<idea-plugin>
<name>AceJump</name>
<id>AceJump</id>
<id>AceJump-chylex</id>
<description><![CDATA[
AceJump allows you to quickly navigate the caret to any position visible in the editor.
@@ -23,12 +23,14 @@
implementationClass="org.acejump.action.AceEditorAction$Reset"/>
<editorActionHandler action="EditorBackSpace" order="first"
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
<editorActionHandler action="EditorEnter" order="first"
implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/>
<editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
<editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
<editorActionHandler action="EditorLineEnd" order="first"
@@ -38,11 +40,18 @@
<actions>
<action id="AceAction"
class="org.acejump.action.AceKeyboardAction$ActivateAceJump"
text="Activate AceJump Mode">
text="Activate AceJump">
<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="AceSpecialAction"
class="org.acejump.action.AceKeyboardAction$ActivateAceJumpSpecial"
text="Activate / Cycle AceJump Special Modes">
<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="AceLineAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode">
@@ -68,5 +77,59 @@
<action id="AceWordBackwardsAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/>
<action id="AceVimAction_JumpToChar"
class="org.acejump.action.AceVimAction$JumpToChar"
text="AceJump Vim - JumpToChar"/>
<action id="AceVimAction_JumpToCharBeforeCaret"
class="org.acejump.action.AceVimAction$JumpToCharBeforeCaret"
text="AceJump Vim - JumpToCharBeforeCaret"/>
<action id="AceVimAction_JumpToCharAfterCaret"
class="org.acejump.action.AceVimAction$JumpToCharAfterCaret"
text="AceJump Vim - JumpToCharAfterCaret"/>
<action id="AceVimAction_LWordsAfterCaret"
class="org.acejump.action.AceVimAction$LWordsAfterCaret"
text="AceJump Vim - LWordsAfterCaret"/>
<action id="AceVimAction_UWordsAfterCaret"
class="org.acejump.action.AceVimAction$UWordsAfterCaret"
text="AceJump Vim - UWordsAfterCaret"/>
<action id="AceVimAction_LWordsBeforeCaret"
class="org.acejump.action.AceVimAction$LWordsBeforeCaret"
text="AceJump Vim - LWordsBeforeCaret"/>
<action id="AceVimAction_UWordsBeforeCaret"
class="org.acejump.action.AceVimAction$UWordsBeforeCaret"
text="AceJump Vim - UWordsBeforeCaret"/>
<action id="AceVimAction_LWordEndsAfterCaret"
class="org.acejump.action.AceVimAction$LWordEndsAfterCaret"
text="AceJump Vim - LWordEndsAfterCaret"/>
<action id="AceVimAction_UWordEndsAfterCaret"
class="org.acejump.action.AceVimAction$UWordEndsAfterCaret"
text="AceJump Vim - UWordEndsAfterCaret"/>
<action id="AceVimAction_LWordEndsBeforeCaret"
class="org.acejump.action.AceVimAction$LWordEndsBeforeCaret"
text="AceJump Vim - LWordEndsBeforeCaret"/>
<action id="AceVimAction_UWordEndsBeforeCaret"
class="org.acejump.action.AceVimAction$UWordEndsBeforeCaret"
text="AceJump Vim - UWordEndsBeforeCaret"/>
<action id="AceVimAction_GoToDeclaration"
class="org.acejump.action.AceVimAction$GoToDeclaration"
text="AceJump Vim - GoToDeclaration"/>
<action id="AceVimAction_GoToTypeDeclaration"
class="org.acejump.action.AceVimAction$GoToTypeDeclaration"
text="AceJump Vim - GoToTypeDeclaration"/>
<action id="AceVimAction_ShowIntentions"
class="org.acejump.action.AceVimAction$ShowIntentions"
text="AceJump Vim - ShowIntentions"/>
<action id="AceVimAction_ShowUsages"
class="org.acejump.action.AceVimAction$ShowUsages"
text="AceJump Vim - ShowUsages"/>
<action id="AceVimAction_FindUsages"
class="org.acejump.action.AceVimAction$FindUsages"
text="AceJump Vim - FindUsages"/>
<action id="AceVimAction_Refactor"
class="org.acejump.action.AceVimAction$Refactor"
text="AceJump Vim - Refactor"/>
<action id="AceVimAction_Rename"
class="org.acejump.action.AceVimAction$Rename"
text="AceJump Vim - Rename"/>
</actions>
</idea-plugin>

View File

@@ -1,5 +1,4 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
import org.acejump.action.AceKeyboardAction
import org.acejump.test.util.BaseTest
@@ -28,48 +27,14 @@ 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")
typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("j")
myFixture.checkResult("testin<caret>g 1234")
}
fun `test shift selection`() {
"<caret>testing 1234".search("4")
typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("J")
myFixture.checkResult("<selection>testing 123<caret></selection>4")
}
fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two")
@@ -94,20 +59,10 @@ class AceTest : BaseTest() {
assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key)
typeAndWaitForResults("j")
myFixture.checkResult("test <caret>word action")
}
fun `test target mode`() {
"<caret>test target action".search("target")
typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("s")
myFixture.checkResult("test <selection>target<caret></selection> action")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines\n")

View File

@@ -15,7 +15,7 @@ class LatencyTest : BaseTest() {
for (query in chars) {
makeEditor(editorText)
myFixture.testAction(AceKeyboardAction.ActivateAceJump)
myFixture.testAction(AceKeyboardAction.ActivateAceJumpSpecial)
time += measureTimeMillis { typeAndWaitForResults("$query") }
// TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" }
resetEditor()

View File

@@ -2,6 +2,7 @@ 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.fixtures.BasePlatformTestCase
@@ -38,7 +39,9 @@ abstract class BaseTest : BasePlatformTestCase() {
fun takeAction(action: AnAction) = myFixture.testAction(action)
fun makeEditor(contents: String): PsiFile {
return myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
val file = myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
(myFixture.editor as EditorImpl).scrollPane.viewport.setSize(1000, 100)
return file
}
fun resetEditor() {
@@ -52,7 +55,7 @@ abstract class BaseTest : BasePlatformTestCase() {
UIUtil.dispatchAllInvocationEvents()
}
fun String.executeQuery(query: String) {
private fun String.executeQuery(query: String) {
myFixture.run {
makeEditor(this@executeQuery)
testAction(AceKeyboardAction.ActivateAceJump)