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

19 Commits

Author SHA1 Message Date
23e177410b [WIP] Add progressive selection mode 2021-02-06 16:04:02 +01:00
47a84da904 [WIP] Pressing Enter before typing query starts jump mode for character at caret 2021-02-06 10:59:57 +01:00
2f6a5f2a23 [WIP] Interactive modes 2021-02-06 08:49:15 +01:00
054f604f96 Add tag shadow & highlight outline, increase padding 2021-02-06 07:38:27 +01:00
97a5de919f Change default color scheme 2021-02-06 07:38:27 +01:00
9e73deef4f Swap editor shortcuts for searching line starts and indents 2021-02-06 07:20:37 +01:00
26945c6e20 Make Enter immediately tag search occurrences 2021-01-18 20:26:48 +01:00
1f8d6b8d6f Add horizontal padding to tags and tweak highlighting 2021-01-18 20:26:48 +01:00
36c5fdcb45 Remove option for rounded tag corners 2021-01-17 13:07:31 +01:00
4cd91a20e9 Remove tag visiting functionality 2021-01-17 13:07:31 +01:00
983720dbb8 Remove whole file search 2021-01-17 13:07:30 +01:00
91e285a3ff Add option to set minimum typed characters for tagging to simplify tags 2021-01-17 12:49:35 +01:00
18bb6ddb9b Fix exception when regex occurrence highlight goes beyond the end of file 2021-01-16 15:03:25 +01:00
6f950ecf95 Fix occasional conflicts between tags and search query when assigning vacant results 2021-01-16 10:49:09 +01:00
d7789be9a3 Prevent editing document while AceJump is active 2020-12-13 08:04:15 +01:00
ed0fc19cfb Enforce LF line endings & fix build.gradle 2020-12-13 07:13:11 +01:00
ad8c84be27 Make jump mode cycling wrap around & add shortcut to cycle modes in reverse 2020-12-13 06:00:41 +01:00
f44003b47a Add all regex search patterns to keymap 2020-12-13 06:00:36 +01:00
feaffb3640 Major AceJump refactoring!
See https://github.com/acejump/AceJump/issues/348 for information on what's changed and what more needs to be done.
2020-12-06 06:46:10 +01:00
45 changed files with 752 additions and 975 deletions

View File

@@ -1,17 +1,8 @@
# Changelog # Changelog
### 3.7 ### 3.6.4
- Improvements to tag latency - Improvements to tag latency. Thanks to @chylex for [the PR](https://github.com/acejump/AceJump/pull/339)!
- 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 ### 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. * [John Lindquist](https://github.com/johnlindquist) for creating AceJump and supporting it for many years.
* [Breandan Considine](https://github.com/breandan) for maintaining the project and adding some new features. * [Breandan Considine](https://github.com/breandan) for maintaining the project and adding some new features.
* [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292). * [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292).
* [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex). * [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pull/339).
* [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency. * [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency.
* [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years. * [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years.
* [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design. * [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design.

View File

@@ -1,15 +1,29 @@
import org.jetbrains.changelog.closure
import org.jetbrains.intellij.tasks.PatchPluginXmlTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
idea apply true idea apply true
kotlin("jvm") version "1.5.0" kotlin("jvm") version "1.3.72"
id("org.jetbrains.intellij") version "0.7.2" id("org.jetbrains.intellij") version "0.6.5"
id("org.jetbrains.changelog") version "0.6.2"
} }
tasks { tasks {
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() 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 { dependencies {
@@ -22,11 +36,11 @@ repositories {
} }
intellij { intellij {
version = "2021.1" version = "2020.2"
pluginName = "AceJump" pluginName = "AceJump"
updateSinceUntilBuild = false updateSinceUntilBuild = false
setPlugins("java", "IdeaVIM:0.66") setPlugins("java")
} }
group = "org.acejump" group = "org.acejump"
version = "chylex-8" version = "4.0"

Binary file not shown.

View File

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

4
gradlew vendored
View File

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

22
gradlew.bat vendored
View File

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

View File

@@ -2,7 +2,6 @@ package org.acejump
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actions.EditorActionUtil import com.intellij.openapi.editor.actions.EditorActionUtil
import it.unimi.dsi.fastutil.ints.IntArrayList
annotation class ExternalUsage annotation class ExternalUsage
@@ -38,7 +37,7 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
* Determines which characters form a "word" for the purposes of functions below. * Determines which characters form a "word" for the purposes of functions below.
*/ */
val Char.isWordPart val Char.isWordPart
get() = this in 'a'..'z' || this.isJavaIdentifierPart() get() = this.isJavaIdentifierPart()
/** /**
* Finds index of the first character in a word. * Finds index of the first character in a word.
@@ -58,9 +57,8 @@ inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Ch
*/ */
inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int { inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos var end = pos
val limit = length - 1
while (end < limit && isPartOfWord(this[end + 1])) { while (end < length - 1 && isPartOfWord(this[end + 1])) {
++end ++end
} }
@@ -98,25 +96,14 @@ inline fun CharSequence.humpEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char
*/ */
inline fun CharSequence.wordEndPlus(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int { inline fun CharSequence.wordEndPlus(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = this.wordEnd(pos, isPartOfWord) var end = this.wordEnd(pos, isPartOfWord)
val limit = length - 1
while (end < limit && !isPartOfWord(this[end + 1])) { while (end < length - 1 && !isPartOfWord(this[end + 1])) {
++end ++end
} }
if (end < limit && isPartOfWord(this[end + 1])) { if (end < length - 1 && isPartOfWord(this[end + 1])) {
++end ++end
} }
return end return end
} }
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
val clone = HashMap<Editor, IntArrayList>(size)
for ((editor, offsets) in this) {
clone[editor] = offsets.clone()
}
return clone
}

View File

@@ -4,6 +4,7 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.Pattern import org.acejump.search.Pattern
import org.acejump.session.Session import org.acejump.session.Session
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
@@ -11,7 +12,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]. * Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
*/ */
abstract class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() { sealed class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext) return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
} }
@@ -44,14 +45,14 @@ abstract class AceEditorAction(private val originalHandler: EditorActionHandler)
} }
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS) override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS) override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS) override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
} }

View File

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

View File

@@ -1,7 +1,12 @@
package org.acejump.action 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.find.actions.ShowUsagesAction
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.CommandProcessor
@@ -13,14 +18,14 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.editor.actions.EditorActionUtil 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.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand import com.intellij.openapi.ui.playback.commands.ActionCommand
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.refactoring.actions.RefactoringQuickListPopupAction
import com.intellij.refactoring.actions.RenameElementAction
import org.acejump.* import org.acejump.*
import org.acejump.search.SearchProcessor import org.acejump.search.SearchProcessor
import kotlin.math.max import kotlin.math.max
@@ -29,19 +34,14 @@ import kotlin.math.max
* Base class for actions available after typing a tag. * Base class for actions available after typing a tag.
*/ */
sealed class AceTagAction { sealed class AceTagAction {
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
abstract class BaseJumpAction : AceTagAction() { abstract class BaseJumpAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val caretModel = editor.caretModel val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList() val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
recordCaretPosition(editor) recordCaretPosition(editor)
if (isFinal) {
ensureEditorFocused(editor)
}
moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset)) moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset))
if (shiftMode) { if (shiftMode) {
@@ -65,7 +65,7 @@ sealed class AceTagAction {
} }
abstract class BaseSelectAction : AceTagAction() { abstract class BaseSelectAction : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
if (shiftMode) { if (shiftMode) {
val caretModel = editor.caretModel val caretModel = editor.caretModel
val oldCarets = caretModel.caretsAndSelections val oldCarets = caretModel.caretsAndSelections
@@ -92,9 +92,9 @@ sealed class AceTagAction {
} }
abstract class BasePerCaretWriteAction(private val selector: AceTagAction) : AceTagAction() { abstract class BasePerCaretWriteAction(private val selector: AceTagAction) : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal) selector(editor, searchProcessor, offset, shiftMode = false)
val range = editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) } val range = editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) }
editor.caretModel.caretsAndSelections = oldCarets editor.caretModel.caretsAndSelections = oldCarets
@@ -158,22 +158,8 @@ sealed class AceTagAction {
caretModel.moveToOffset(cursorOffset) caretModel.moveToOffset(cursorOffset)
} }
fun performAction(actionName: String) { fun performAction(action: AnAction) {
val actionManager = ActionManager.getInstance() ActionManager.getInstance().tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
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) { private fun addCurrentPositionToHistory(project: Project, document: Document) {
@@ -300,7 +286,7 @@ sealed class AceTagAction {
selectRange(editor, startOffset, endOffset) selectRange(editor, startOffset, endOffset)
} }
else { else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true) SelectQuery(editor, searchProcessor, offset, shiftMode = false)
} }
} }
} }
@@ -325,18 +311,48 @@ sealed class AceTagAction {
selectRange(editor, startOffset, endOffset) selectRange(editor, startOffset, endOffset)
} }
else { else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true) SelectQuery(editor, searchProcessor, offset, shiftMode = false)
} }
} }
} }
/**
* On default action, selects a word according to [SelectWord], and then extends the selection in either or both directions based
* on the characters around the word. TODO
*
* On shift action, adds the new selection to existing selections.
*/
object SelectAroundWord : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
SelectWord(editor, searchProcessor, offset, shiftMode = false)
val text = editor.immutableText
var selectionStart = editor.selectionModel.selectionStart
var selectionEnd = editor.selectionModel.selectionEnd
val indentStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(editor.document, editor.caretModel.logicalPosition.line)
while (selectionStart > 0 && selectionStart > indentStart && text[selectionStart - 1].let { it == ' ' || it == ',' }) {
--selectionStart
}
while (selectionEnd < text.length && text[selectionEnd].let { it == ' ' || it == ',' }) {
++selectionEnd
}
if (selectionStart > 0 && text[selectionStart - 1] == '!') {
--selectionStart
}
selectRange(editor, selectionStart, selectionEnd)
}
}
/** /**
* On default action, selects the line at the tag, excluding the indent. * On default action, selects the line at the tag, excluding the indent.
* On shift action, adds the new selection to existing selections. * On shift action, adds the new selection to existing selections.
*/ */
object SelectLine : BaseSelectAction() { object SelectLine : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true) JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
val document = editor.document val document = editor.document
val line = editor.caretModel.logicalPosition.line val line = editor.caretModel.logicalPosition.line
@@ -355,10 +371,12 @@ sealed class AceTagAction {
*/ */
class SelectExtended(private val extendCount: Int) : BaseSelectAction() { class SelectExtended(private val extendCount: Int) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true) JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
repeat(extendCount) { repeat(extendCount) {
performAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET) performAction(action)
} }
} }
} }
@@ -373,7 +391,7 @@ sealed class AceTagAction {
val oldOffset = caretModel.offset val oldOffset = caretModel.offset
val oldSelection = editor.selectionModel.takeIf { it.hasSelection(false) }?.let { it.selectionStart..it.selectionEnd } val oldSelection = editor.selectionModel.takeIf { it.hasSelection(false) }?.let { it.selectionStart..it.selectionEnd }
jumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true) jumper(editor, searchProcessor, offset, shiftMode = false)
val newOffset = caretModel.offset val newOffset = caretModel.offset
@@ -392,11 +410,30 @@ sealed class AceTagAction {
*/ */
class SelectBetweenPoints(private val firstOffset: Int, private val secondOffsetJumper: BaseJumpAction) : BaseSelectAction() { class SelectBetweenPoints(private val firstOffset: Int, private val secondOffsetJumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true) secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false)
selectRange(editor, firstOffset, editor.caretModel.offset) selectRange(editor, firstOffset, editor.caretModel.offset)
} }
} }
/**
* On default action, selects text based on the provided [selector] action, and deletes it without moving the existing carets.
* On shift action, moves caret to the position where deletion occurred.
*/
class Delete(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
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) }
}
if (!shiftMode) {
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 * 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. * has a selection, the selected text will be replaced.
@@ -441,9 +478,9 @@ sealed class AceTagAction {
* Always places the caret at the start of the word. * Always places the caret at the start of the word.
*/ */
object GoToDeclaration : AceTagAction() { object GoToDeclaration : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal) JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_GOTO_TYPE_DECLARATION else IdeActions.ACTION_GOTO_DECLARATION) } performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
} }
} }
@@ -453,9 +490,9 @@ sealed class AceTagAction {
* Always places the caret at the start of the word. * Always places the caret at the start of the word.
*/ */
object ShowUsages : AceTagAction() { object ShowUsages : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal) JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_FIND_USAGES else ShowUsagesAction.ID) } performAction(if (shiftMode) FindUsagesAction() else ShowUsagesAction())
} }
} }
@@ -464,9 +501,9 @@ sealed class AceTagAction {
* Always places the caret at the start of the word. * Always places the caret at the start of the word.
*/ */
object ShowIntentions : AceTagAction() { object ShowIntentions : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal) JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
ApplicationManager.getApplication().invokeLater { performAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS) } performAction(ShowIntentionActionsAction())
} }
} }
@@ -476,9 +513,14 @@ sealed class AceTagAction {
* Always places the caret at the start of the word. * Always places the caret at the start of the word.
*/ */
object Refactor : AceTagAction() { object Refactor : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal) JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_RENAME else "Refactorings.QuickListPopupAction") } if (shiftMode) {
ApplicationManager.getApplication().invokeLater { performAction(RenameElementAction()) }
}
else {
performAction(RefactoringQuickListPopupAction())
}
} }
} }
} }

View File

@@ -1,148 +0,0 @@
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 { BEFORE_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange { override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0 until editor.caretModel.offset return 0..(editor.caretModel.offset)
} }
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean { override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset < editor.caretModel.offset return offset <= editor.caretModel.offset
} }
}, },
AFTER_CARET { AFTER_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange { override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return (editor.caretModel.offset + 1) until editor.document.textLength return editor.caretModel.offset until editor.document.textLength
} }
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean { override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset > editor.caretModel.offset return offset >= editor.caretModel.offset
} }
} }
} }

View File

@@ -21,7 +21,7 @@ class AceConfig : PersistentStateComponent<AceSettings> {
val layout get() = settings.layout val layout get() = settings.layout
val minQueryLength get() = settings.minQueryLength val minQueryLength get() = settings.minQueryLength
val jumpModeColor get() = settings.jumpModeColor val jumpModeColor get() = settings.jumpModeColor
val advancedModeColor get() = settings.advancedModeColor val fromCaretModeColor get() = settings.fromCaretModeColor
val betweenPointsModeColor get() = settings.betweenPointsModeColor val betweenPointsModeColor get() = settings.betweenPointsModeColor
val textHighlightColor get() = settings.textHighlightColor val textHighlightColor get() = settings.textHighlightColor
val tagForegroundColor get() = settings.tagForegroundColor val tagForegroundColor get() = settings.tagForegroundColor

View File

@@ -16,7 +16,7 @@ class AceConfigurable : Configurable {
panel.keyboardLayout != settings.layout || panel.keyboardLayout != settings.layout ||
panel.minQueryLengthInt != settings.minQueryLength || panel.minQueryLengthInt != settings.minQueryLength ||
panel.jumpModeColor != settings.jumpModeColor || panel.jumpModeColor != settings.jumpModeColor ||
panel.advancedModeColor != settings.advancedModeColor || panel.fromCaretModeColor != settings.fromCaretModeColor ||
panel.betweenPointsModeColor != settings.betweenPointsModeColor || panel.betweenPointsModeColor != settings.betweenPointsModeColor ||
panel.textHighlightColor != settings.textHighlightColor || panel.textHighlightColor != settings.textHighlightColor ||
panel.tagForegroundColor != settings.tagForegroundColor || panel.tagForegroundColor != settings.tagForegroundColor ||
@@ -28,7 +28,7 @@ class AceConfigurable : Configurable {
settings.layout = panel.keyboardLayout settings.layout = panel.keyboardLayout
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
panel.jumpModeColor?.let { settings.jumpModeColor = it } panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.advancedModeColor?.let { settings.advancedModeColor = it } panel.fromCaretModeColor?.let { settings.fromCaretModeColor = it }
panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = it } panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = it }
panel.textHighlightColor?.let { settings.textHighlightColor = it } panel.textHighlightColor?.let { settings.textHighlightColor = it }
panel.tagForegroundColor?.let { settings.tagForegroundColor = it } panel.tagForegroundColor?.let { settings.tagForegroundColor = it }

View File

@@ -13,8 +13,8 @@ data class AceSettings(
@OptionTag("jumpModeRGB", converter = ColorConverter::class) @OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF), var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("advancedModeRGB", converter = ColorConverter::class) @OptionTag("fromCaretModeRGB", converter = ColorConverter::class)
var advancedModeColor: Color = Color(0xFFB700), var fromCaretModeColor: Color = Color(0xFFB700),
@OptionTag("betweenPointsModeRGB", converter = ColorConverter::class) @OptionTag("betweenPointsModeRGB", converter = ColorConverter::class)
var betweenPointsModeColor: Color = Color(0x6FC5FF), var betweenPointsModeColor: Color = Color(0x6FC5FF),

View File

@@ -27,7 +27,7 @@ internal class AceSettingsPanel {
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false } private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val minQueryLengthField = JBTextField() private val minQueryLengthField = JBTextField()
private val jumpModeColorWheel = ColorPanel() private val jumpModeColorWheel = ColorPanel()
private val advancedModeColorWheel = ColorPanel() private val fromCaretModeColorWheel = ColorPanel()
private val betweenPointsModeColorWheel = ColorPanel() private val betweenPointsModeColorWheel = ColorPanel()
private val textHighlightColorWheel = ColorPanel() private val textHighlightColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel() private val tagForegroundColorWheel = ColorPanel()
@@ -56,7 +56,7 @@ internal class AceSettingsPanel {
titledRow("Colors") { titledRow("Colors") {
row("Jump mode caret background:") { short(jumpModeColorWheel) } row("Jump mode caret background:") { short(jumpModeColorWheel) }
row("Advanced mode caret background:") { short(advancedModeColorWheel) } row("From Caret mode caret background:") { short(fromCaretModeColorWheel) }
row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) } row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) }
row("Searched text background:") { short(textHighlightColorWheel) } row("Searched text background:") { short(textHighlightColorWheel) }
row("Tag foreground:") { short(tagForegroundColorWheel) } row("Tag foreground:") { short(tagForegroundColorWheel) }
@@ -71,7 +71,7 @@ internal class AceSettingsPanel {
internal var keyChars by keyboardLayoutArea internal var keyChars by keyboardLayoutArea
internal var minQueryLength by minQueryLengthField internal var minQueryLength by minQueryLengthField
internal var jumpModeColor by jumpModeColorWheel internal var jumpModeColor by jumpModeColorWheel
internal var advancedModeColor by advancedModeColorWheel internal var fromCaretModeColor by fromCaretModeColorWheel
internal var betweenPointsModeColor by betweenPointsModeColorWheel internal var betweenPointsModeColor by betweenPointsModeColorWheel
internal var textHighlightColor by textHighlightColorWheel internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel internal var tagForegroundColor by tagForegroundColorWheel
@@ -87,7 +87,7 @@ internal class AceSettingsPanel {
keyboardLayout = settings.layout keyboardLayout = settings.layout
minQueryLength = settings.minQueryLength.toString() minQueryLength = settings.minQueryLength.toString()
jumpModeColor = settings.jumpModeColor jumpModeColor = settings.jumpModeColor
advancedModeColor = settings.advancedModeColor fromCaretModeColor = settings.fromCaretModeColor
betweenPointsModeColor = settings.betweenPointsModeColor betweenPointsModeColor = settings.betweenPointsModeColor
textHighlightColor = settings.textHighlightColor textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor tagForegroundColor = settings.tagForegroundColor

View File

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

@@ -1,73 +0,0 @@
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,7 +3,6 @@ package org.acejump.modes
import com.intellij.openapi.editor.CaretState import com.intellij.openapi.editor.CaretState
import org.acejump.action.AceTagAction import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState import org.acejump.session.SessionState
import org.acejump.session.TypeResult import org.acejump.session.TypeResult
@@ -14,11 +13,17 @@ class BetweenPointsMode : SessionMode {
) )
private val ACTION_MODE_HINT = arrayOf( private val ACTION_MODE_HINT = arrayOf(
"<f>[S]</f>elect... / <f>[F]</f>rom Caret...",
"<f>[D]</f>elete...",
"<f>[C]</f>lone to Caret...", "<f>[C]</f>lone to Caret...",
"<f>[M]</f>ove to Caret..." "<f>[M]</f>ove to Caret..."
) )
private const val ACTION_MODE_FROM_CARET = 'F'
private val ACTION_MODE_MAP = mapOf( private val ACTION_MODE_MAP = mapOf(
'S' to ({ action: AceTagAction.BaseSelectAction -> action }),
'D' to (AceTagAction::Delete),
'C' to (AceTagAction::CloneToCaret), 'C' to (AceTagAction::CloneToCaret),
'M' to (AceTagAction::MoveToCaret) 'M' to (AceTagAction::MoveToCaret)
) )
@@ -31,9 +36,13 @@ class BetweenPointsMode : SessionMode {
private var originalCarets: List<CaretState>? = null private var originalCarets: List<CaretState>? = null
private var firstOffset: Int? = null private var firstOffset: Int? = null
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult { override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
val actionMode = actionMode val actionMode = actionMode
if (actionMode == null) { if (actionMode == null) {
if (charTyped.equals(ACTION_MODE_FROM_CARET, ignoreCase = true)) {
return TypeResult.ChangeMode(SelectFromCaretMode())
}
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()] this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing return TypeResult.Nothing
} }
@@ -43,50 +52,40 @@ class BetweenPointsMode : SessionMode {
} }
if (firstOffset == null) { if (firstOffset == null) {
val selectAction = AdvancedMode.SELECT_ACTION_MAP[charTyped.toUpperCase()] val selectAction = JumpMode.SELECT_ACTION_MAP[charTyped.toUpperCase()]
if (selectAction != null) { if (selectAction != null) {
state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase(), isFinal = true) state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession return TypeResult.EndSession
} }
} }
val jumpAction = AdvancedMode.JUMP_ACTION_MAP[charTyped.toUpperCase()] val jumpAction = JumpMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) { if (jumpAction == null) {
return TypeResult.Nothing return TypeResult.Nothing
} }
val firstOffset = firstOffset val firstOffset = firstOffset
if (firstOffset == null) { if (firstOffset == null) {
val caretModel = acceptedTag.editor.caretModel val caretModel = state.editor.caretModel
this.originalCarets = caretModel.caretsAndSelections this.originalCarets = caretModel.caretsAndSelections
state.act(jumpAction, acceptedTag, shiftMode = false, isFinal = false) state.act(jumpAction, acceptedTag, shiftMode = false)
this.firstOffset = caretModel.offset this.firstOffset = caretModel.offset
return TypeResult.RestartSearch return TypeResult.RestartSearch
} }
originalCarets?.let { acceptedTag.editor.caretModel.caretsAndSelections = it } originalCarets?.let { state.editor.caretModel.caretsAndSelections = it }
state.act(actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)), acceptedTag, shiftMode = charTyped.isUpperCase())
state.act(
actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)),
acceptedTag,
shiftMode = charTyped.isUpperCase(),
isFinal = true
)
return TypeResult.EndSession return TypeResult.EndSession
} }
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
return false
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? { override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return when { return when {
actionMode == null -> ACTION_MODE_HINT actionMode == null -> ACTION_MODE_HINT
acceptedTag == null -> TYPE_TAG_HINT.takeUnless { hasQuery } acceptedTag == null -> TYPE_TAG_HINT.takeUnless { hasQuery }
firstOffset == null -> AdvancedMode.JUMP_ALT_HINT + AdvancedMode.SELECT_HINT firstOffset == null -> JumpMode.JUMP_ALT_HINT + JumpMode.SELECT_HINT
else -> AdvancedMode.JUMP_ALT_HINT else -> JumpMode.JUMP_ALT_HINT
} }
} }
} }

View File

@@ -2,28 +2,82 @@ package org.acejump.modes
import org.acejump.action.AceTagAction import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState import org.acejump.session.SessionState
import org.acejump.session.TypeResult import org.acejump.session.TypeResult
open class JumpMode : SessionMode { class JumpMode : SessionMode {
companion object {
private val JUMP_HINT = arrayOf(
"<f>[J]</f>ump / <f>[L]</f> past Query / <f>[M]</f> Line End",
"Word <f>[S]</f>tart / <f>[E]</f>nd"
)
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,
'M' to AceTagAction.JumpToLineEnd,
'S' to AceTagAction.JumpToWordStart,
'E' to AceTagAction.JumpToWordEnd
)
val SELECT_HINT = arrayOf(
"Select <f>[W]</f>ord / <f>[H]</f>ump / <f>[A]</f>round",
"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,
'A' to AceTagAction.SelectAroundWord,
'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,
"Select <f>[P]</f>rogressively...",
"<f>[D]</f>eclaration / <f>[U]</f>sages",
"<f>[I]</f>ntentions / <f>[R]</f>efactor"
)
private const val ACTION_SELECT_PROGRESSIVELY = 'P'
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(),
'D' to AceTagAction.GoToDeclaration,
'U' to AceTagAction.ShowUsages,
'I' to AceTagAction.ShowIntentions,
'R' to AceTagAction.Refactor
)
}
override val caretColor override val caretColor
get() = AceConfig.jumpModeColor get() = AceConfig.jumpModeColor
protected var wasUpperCase = false override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
private set if (acceptedTag == null) {
return state.type(charTyped)
}
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult { val action = ALL_ACTION_MAP[charTyped.toUpperCase()]
wasUpperCase = charTyped.isUpperCase() if (action != null) {
return state.type(charTyped) state.act(action, acceptedTag, charTyped.isUpperCase())
} return TypeResult.EndSession
}
else if (charTyped.equals(ACTION_SELECT_PROGRESSIVELY, ignoreCase = true)) {
state.act(AceTagAction.SelectQuery, acceptedTag, charTyped.isUpperCase())
return TypeResult.ChangeMode(ProgressiveSelectionMode())
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean { return TypeResult.Nothing
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
return true
} }
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? { override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return null return ALL_HINTS.takeIf { acceptedTag != null }
} }
} }

View File

@@ -0,0 +1,142 @@
package org.acejump.modes
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class ProgressiveSelectionMode : SessionMode {
private companion object {
private val EXPANSION_HINT = arrayOf(
"<f>[W]</f>ord / <f>[C]</f>har / <f>[L]</f>ine / <f>[S]</f>pace"
)
private val EXPANSION_MODES = mapOf(
'W' to SelectionMode.Word,
'C' to SelectionMode.Char,
'L' to SelectionMode.Line,
'S' to SelectionMode.Space
)
}
override val caretColor
get() = AceConfig.jumpModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
val editor = state.editor
val mode = EXPANSION_MODES[charTyped.toUpperCase()]
if (mode != null) {
val hintOffset = if (charTyped.isUpperCase()) {
editor.caretModel.runForEachCaret { mode.extendLeft(editor, it) }
editor.caretModel.allCarets.first().selectionStart
}
else {
editor.caretModel.runForEachCaret { mode.extendRight(editor, it); it.moveToOffset(it.selectionEnd) }
editor.caretModel.allCarets.last().selectionEnd
}
editor.scrollingModel.scrollTo(editor.offsetToLogicalPosition(hintOffset), ScrollType.RELATIVE)
return TypeResult.MoveHint(hintOffset)
}
return TypeResult.Nothing
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String> {
return EXPANSION_HINT
}
private sealed class SelectionMode {
abstract fun extendLeft(editor: Editor, caret: Caret)
abstract fun extendRight(editor: Editor, caret: Caret)
object Word : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
val text = editor.immutableText
val wordPart = when {
caret.selectionStart == 0 -> caret.selectionStart
text[caret.selectionStart - 1].isWordPart -> caret.selectionStart - 1
else -> (caret.selectionStart - 1 downTo 0).find { text[it].isWordPart } ?: return
}
caret.setSelection(caret.selectionEnd, AceTagAction.JumpToWordStart.getCaretOffset(editor, wordPart, wordPart, isInsideWord = true))
}
override fun extendRight(editor: Editor, caret: Caret) {
val text = editor.immutableText
val wordPart = when {
text[caret.selectionEnd].isWordPart -> caret.selectionEnd
else -> (caret.selectionEnd until text.length).find { text[it].isWordPart } ?: return
}
caret.setSelection(caret.selectionStart, AceTagAction.JumpToWordEnd.getCaretOffset(editor, wordPart, wordPart, isInsideWord = true))
}
}
object Char : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
caret.setSelection((caret.selectionStart - 1).coerceAtLeast(0), caret.selectionEnd)
}
override fun extendRight(editor: Editor, caret: Caret) {
caret.setSelection(caret.selectionStart, (caret.selectionEnd + 1).coerceAtMost(editor.immutableText.length))
}
}
object Line : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
val document = editor.document
val line = document.getLineNumber(caret.selectionStart)
val lineOffset = document.getLineStartOffset(line)
if (caret.selectionStart > lineOffset) {
caret.setSelection(lineOffset, caret.selectionEnd)
}
else if (line - 1 >= 0) {
caret.setSelection(document.getLineStartOffset(line - 1), caret.selectionEnd)
}
}
override fun extendRight(editor: Editor, caret: Caret) {
val document = editor.document
val line = document.getLineNumber(caret.selectionEnd)
val lineOffset = document.getLineEndOffset(line)
if (caret.selectionEnd < lineOffset) {
caret.setSelection(caret.selectionStart, lineOffset)
}
else if (line + 1 < document.lineCount) {
caret.setSelection(caret.selectionStart, document.getLineEndOffset(line + 1))
}
}
}
object Space : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
var offset = caret.selectionStart
while (offset > 0 && editor.immutableText[offset - 1].isWhitespace()) {
--offset
}
caret.setSelection(offset, caret.selectionEnd)
}
override fun extendRight(editor: Editor, caret: Caret) {
var offset = caret.selectionEnd
while (offset < editor.immutableText.length && editor.immutableText[offset].isWhitespace()) {
++offset
}
caret.setSelection(caret.selectionStart, offset)
}
}
}
}

View File

@@ -0,0 +1,29 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class SelectFromCaretMode : SessionMode {
override val caretColor
get() = AceConfig.fromCaretModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
if (acceptedTag == null) {
return state.type(charTyped)
}
val jumpAction = JumpMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
state.act(AceTagAction.SelectToCaret(jumpAction), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return JumpMode.JUMP_ALT_HINT.takeIf { acceptedTag != null }
}
}

View File

@@ -1,6 +1,5 @@
package org.acejump.modes package org.acejump.modes
import org.acejump.search.Tag
import org.acejump.session.SessionState import org.acejump.session.SessionState
import org.acejump.session.TypeResult import org.acejump.session.TypeResult
import java.awt.Color import java.awt.Color
@@ -8,7 +7,6 @@ import java.awt.Color
interface SessionMode { interface SessionMode {
val caretColor: Color val caretColor: Color
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult
fun accept(state: SessionState, acceptedTag: Tag): Boolean
fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>?
} }

View File

@@ -1,17 +0,0 @@
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,9 +5,5 @@ enum class Pattern(val regex: String) {
LINE_ENDS("\\n|\\Z"), LINE_ENDS("\\n|\\Z"),
LINE_INDENTS("[^\\s].*|^\\n"), LINE_INDENTS("[^\\s].*|^\\n"),
LINE_ALL_MARKS(LINE_ENDS.regex + "|" + LINE_STARTS.regex + "|" + LINE_INDENTS.regex), 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,7 +3,6 @@ package org.acejump.search
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.Boundaries
import org.acejump.clone
import org.acejump.immutableText import org.acejump.immutableText
import org.acejump.isWordPart import org.acejump.isWordPart
import org.acejump.matchesAt import org.acejump.matchesAt
@@ -11,44 +10,36 @@ import org.acejump.matchesAt
/** /**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character. * Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character.
*/ */
class SearchProcessor private constructor( class SearchProcessor private constructor(private val editor: Editor, query: SearchQuery) {
private val editors: List<Editor>, query: SearchQuery, results: MutableMap<Editor, IntArrayList>
) {
companion object { companion object {
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries): SearchProcessor { fun fromChar(editor: Editor, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries) return SearchProcessor(editor, SearchQuery.Literal(char.toString()), boundaries)
} }
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries): SearchProcessor { fun fromRegex(editor: Editor, pattern: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries) return SearchProcessor(editor, SearchQuery.RegularExpression(pattern), boundaries)
} }
} }
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) { private constructor(editor: Editor, query: SearchQuery, boundaries: Boundaries) : this(editor, query) {
val regex = query.toRegex() val regex = query.toRegex()
if (regex != null) { if (regex != null) {
for (editor in editors) { val offsetRange = boundaries.getOffsetRange(editor)
val offsets = IntArrayList() var result = regex.find(editor.immutableText, offsetRange.first)
val offsetRange = boundaries.getOffsetRange(editor) while (result != null) {
var result = regex.find(editor.immutableText, offsetRange.first) val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception.
val highlightEnd = index + query.getHighlightLength("", index)
while (result != null) { if (highlightEnd > offsetRange.last) {
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception. break
val highlightEnd = index + query.getHighlightLength("", index) }
else if (boundaries.isOffsetInside(editor, index)) {
if (highlightEnd > offsetRange.last) { results.add(index)
break
}
else if (boundaries.isOffsetInside(editor, index)) {
offsets.add(index)
}
result = result.next()
} }
results[editor] = offsets result = result.next()
} }
} }
} }
@@ -56,7 +47,7 @@ class SearchProcessor private constructor(
internal var query = query internal var query = query
private set private set
internal var results = results internal var results = IntArrayList(0)
private set private set
/** /**
@@ -66,12 +57,13 @@ class SearchProcessor private constructor(
*/ */
fun type(char: Char, tagger: Tagger): Boolean { fun type(char: Char, tagger: Tagger): Boolean {
val newQuery = query.rawText + char val newQuery = query.rawText + char
val chars = editor.immutableText
val canMatchTag = tagger.canQueryMatchAnyTag(newQuery) val canMatchTag = tagger.canQueryMatchAnyTag(newQuery)
// If the typed character is not compatible with any existing tag or as a continuation of any previous occurrence, reject the query // 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. // change and return false to indicate that nothing else should happen.
if (newQuery.length > 1 && !canMatchTag && !isContinuation(newQuery)) { if (newQuery.length > 1 && !canMatchTag && results.none { chars.matchesAt(it, newQuery, ignoreCase = true) }) {
return false return false
} }
@@ -84,19 +76,15 @@ class SearchProcessor private constructor(
query = SearchQuery.Literal(char.toString()) query = SearchQuery.Literal(char.toString())
tagger.unmark() tagger.unmark()
for ((editor, offsets) in results) { val iter = results.iterator()
val chars = editor.immutableText while (iter.hasNext()) {
val iter = offsets.iterator() val movedOffset = iter.nextInt() + newQuery.length - 1
while (iter.hasNext()) { if (movedOffset < chars.length && chars[movedOffset].equals(char, ignoreCase = true)) {
val movedOffset = iter.nextInt() + newQuery.length - 1 iter.set(movedOffset)
}
if (movedOffset < chars.length && chars[movedOffset].equals(char, ignoreCase = true)) { else {
iter.set(movedOffset) iter.remove()
}
else {
iter.remove()
}
} }
} }
} }
@@ -108,20 +96,6 @@ class SearchProcessor private constructor(
return true 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. * After updating the query, removes all results that no longer match the search query.
*/ */
@@ -129,28 +103,25 @@ class SearchProcessor private constructor(
val lastCharOffset = newQuery.lastIndex val lastCharOffset = newQuery.lastIndex
val lastChar = newQuery[lastCharOffset] val lastChar = newQuery[lastCharOffset]
val ignoreCase = newQuery[0].isLowerCase() val ignoreCase = newQuery[0].isLowerCase()
val chars = editor.immutableText
for ((editor, offsets) in results.entries.toList()) { val remaining = IntArrayList()
val chars = editor.immutableText val iter = results.iterator()
val remaining = IntArrayList() while (iter.hasNext()) {
val iter = offsets.iterator() val offset = iter.nextInt()
val endOffset = offset + lastCharOffset
val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase)
while (iter.hasNext()) { if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, offset)) {
val offset = iter.nextInt() remaining.add(offset)
val endOffset = offset + lastCharOffset
val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase)
if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
remaining.add(offset)
}
} }
results[editor] = remaining
} }
results = remaining
} }
fun clone(): SearchProcessor { fun clone(): SearchProcessor {
return SearchProcessor(editors, query, results.clone()) return SearchProcessor(editor, query).also { it.results.addAll(results) }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,16 +16,7 @@ object SessionManager {
* Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one. * Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
*/ */
fun start(editor: Editor): Session { fun start(editor: Editor): Session {
return start(editor, listOf(editor)) return sessions.getOrPut(editor) { cleanup(); Session(editor) }
}
/**
* Starts a new multi-editor [Session], or returns an existing [Session] if the specified main [Editor] already has one.
* The [mainEditor] is used for typing the search query and tag.
* The [jumpEditors] are all editors that will be searched and tagged.
*/
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
} }
/** /**

View File

@@ -1,9 +1,10 @@
package org.acejump.session package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction import org.acejump.action.AceTagAction
import org.acejump.search.Tag
interface SessionState { interface SessionState {
val editor: Editor
fun type(char: Char): TypeResult fun type(char: Char): TypeResult
fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean) fun act(action: AceTagAction, offset: Int, shiftMode: Boolean)
} }

View File

@@ -2,24 +2,18 @@ package org.acejump.session
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction import org.acejump.action.AceTagAction
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.SearchProcessor import org.acejump.search.SearchProcessor
import org.acejump.search.Tag
import org.acejump.search.Tagger import org.acejump.search.Tagger
internal class SessionStateImpl( internal class SessionStateImpl(override val editor: Editor, private val tagger: Tagger, processor: SearchProcessor? = null) : SessionState {
private val jumpEditors: List<Editor>,
private val tagger: Tagger,
private val defaultBoundary: Boundaries,
processor: SearchProcessor? = null
) : SessionState {
internal var currentProcessor: SearchProcessor? = processor internal var currentProcessor: SearchProcessor? = processor
override fun type(char: Char): TypeResult { override fun type(char: Char): TypeResult {
val processor = currentProcessor val processor = currentProcessor
if (processor == null) { if (processor == null) {
val newProcessor = SearchProcessor.fromChar(jumpEditors, char, defaultBoundary) val newProcessor = SearchProcessor.fromChar(editor, char, StandardBoundaries.VISIBLE_ON_SCREEN)
return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it }) return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it })
} }
@@ -30,7 +24,7 @@ internal class SessionStateImpl(
return TypeResult.Nothing return TypeResult.Nothing
} }
override fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean) { override fun act(action: AceTagAction, offset: Int, shiftMode: Boolean) {
currentProcessor?.let { action(tag.editor, it, tag.offset, shiftMode, isFinal) } currentProcessor?.let { action(editor, it, offset, shiftMode) }
} }
} }

View File

@@ -5,8 +5,9 @@ import org.acejump.search.SearchProcessor
sealed class TypeResult { sealed class TypeResult {
object Nothing : TypeResult() object Nothing : TypeResult()
class UpdateResults(val processor: SearchProcessor) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
object RestartSearch : TypeResult() object RestartSearch : TypeResult()
class UpdateResults(val processor: SearchProcessor) : TypeResult()
class MoveHint(val offset: Int) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
object EndSession : TypeResult() object EndSession : TypeResult()
} }

View File

@@ -1,10 +1,11 @@
package org.acejump.view package org.acejump.view
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.SystemInfo import com.intellij.ui.ColorUtil
import com.intellij.ui.scale.JBUIScale import com.intellij.ui.scale.JBUIScale
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.countMatchingCharacters import org.acejump.countMatchingCharacters
import org.acejump.immutableText import org.acejump.immutableText
import java.awt.Color import java.awt.Color
@@ -16,7 +17,7 @@ import kotlin.math.max
/** /**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor. * Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/ */
internal class TagMarker( internal class Tag(
private val tag: String, private val tag: String,
val offsetL: Int, val offsetL: Int,
val offsetR: Int, val offsetR: Int,
@@ -28,18 +29,11 @@ internal class TagMarker(
companion object { companion object {
private const val ARC = 1 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 * Creates a new tag, precomputing some information about the nearby characters to reduce rendering overhead. If the last typed
* character ([literalQueryText]) matches the first [tag] character, only the second [tag] character is displayed. * character ([literalQueryText]) matches the first [tag] character, only the second [tag] character is displayed.
*/ */
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker { fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): Tag {
val chars = editor.immutableText val chars = editor.immutableText
val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0 val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace() val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
@@ -49,7 +43,7 @@ internal class TagMarker(
else else
tag.toUpperCase() tag.toUpperCase()
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight) return Tag(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
} }
/** /**
@@ -57,9 +51,7 @@ internal class TagMarker(
*/ */
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) { private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color g.color = color
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC) g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
g.translate(0.0, -HIGHLIGHT_OFFSET)
} }
/** /**
@@ -71,12 +63,12 @@ internal class TagMarker(
g.font = font.tagFont g.font = font.tagFont
if (!font.isForegroundDark) { if (!ColorUtil.isDark(AceConfig.tagForegroundColor)) {
g.color = SHADOW_COLOR g.color = Color(0F, 0F, 0F, 0.35F)
g.drawString(text, x + 1, y + 1) g.drawString(text, x + 1, y + 1)
} }
g.color = font.foregroundColor g.color = AceConfig.tagForegroundColor
g.drawString(text, x, y) g.drawString(text, x, y)
} }
} }
@@ -96,7 +88,7 @@ internal class TagMarker(
fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? { fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? {
val rect = alignTag(editor, cache, font, occupied) ?: return null val rect = alignTag(editor, cache, font, occupied) ?: return null
drawHighlight(g, rect, font.backgroundColor) drawHighlight(g, rect, AceConfig.tagBackgroundColor)
drawForeground(g, font, rect.location, tag) drawForeground(g, font, rect.location, tag)
occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) }) occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) })

View File

@@ -17,7 +17,7 @@ import javax.swing.SwingUtilities
* Holds all active tag markers and renders them on top of the editor. * Holds all active tag markers and renders them on top of the editor.
*/ */
internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener { internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener {
private var markers: Collection<TagMarker>? = null private var markers: List<Tag>? = null
init { init {
val contentComponent = editor.contentComponent val contentComponent = editor.contentComponent
@@ -45,7 +45,7 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
repaint() repaint()
} }
fun setMarkers(markers: Collection<TagMarker>) { fun setMarkers(markers: List<Tag>) {
this.markers = markers this.markers = markers
repaint() repaint()
} }
@@ -64,15 +64,14 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
super.paintChildren(g) super.paintChildren(g)
val markers = markers ?: return val markers = markers ?: return
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val font = TagFont(editor) val font = TagFont(editor)
val cache = EditorOffsetCache.new() val cache = EditorOffsetCache.new()
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache) val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val occupied = mutableListOf<Rectangle>() 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 // 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. // currently selected while navigating highly clustered tags, although it does end up rearranging nearby tags which can be confusing.

View File

@@ -2,22 +2,16 @@ package org.acejump.view
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.ui.ColorUtil
import org.acejump.config.AceConfig
import java.awt.Font import java.awt.Font
import java.awt.FontMetrics import java.awt.FontMetrics
/** /**
* Stores font metrics for aligning and rendering [TagMarker]s. * Stores font metrics for aligning and rendering [Tag]s.
*/ */
internal class TagFont(editor: Editor) { internal class TagFont(editor: Editor) {
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.BOLD) val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.BOLD)
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W') val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W')
val foregroundColor = AceConfig.tagForegroundColor
var backgroundColor = AceConfig.tagBackgroundColor
val isForegroundDark = ColorUtil.isDark(foregroundColor)
val editorFontMetrics: FontMetrics = editor.component.getFontMetrics(editor.colorsScheme.getFont(EditorFontType.PLAIN)) val editorFontMetrics: FontMetrics = editor.component.getFontMetrics(editor.colorsScheme.getFont(EditorFontType.PLAIN))
val lineHeight = editor.lineHeight val lineHeight = editor.lineHeight
val baselineDistance = editor.ascent val baselineDistance = editor.ascent

View File

@@ -12,21 +12,20 @@ import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.immutableText import org.acejump.immutableText
import org.acejump.search.SearchQuery import org.acejump.search.SearchQuery
import org.acejump.search.Tag
import java.awt.Color import java.awt.Color
import java.awt.Graphics import java.awt.Graphics
/** /**
* Renders highlights for search occurrences. * Renders highlights for search occurrences.
*/ */
internal class TextHighlighter { internal class TextHighlighter(private val editor: Editor) {
private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>() private var previousHighlights: Array<RangeHighlighter>? = null
/** /**
* Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change. * Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change.
*/ */
fun renderOccurrences(results: Map<Editor, IntList>, query: SearchQuery) { fun renderOccurrences(offsets: IntList, query: SearchQuery) {
render(results, when (query) { render(offsets, when (query) {
is SearchQuery.RegularExpression -> RegexRenderer is SearchQuery.RegularExpression -> RegexRenderer
else -> SearchedWordRenderer else -> SearchedWordRenderer
}, query::getHighlightLength) }, query::getHighlightLength)
@@ -35,52 +34,43 @@ internal class TextHighlighter {
/** /**
* Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color. * Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
*/ */
fun renderFinal(tag: Tag, query: SearchQuery) { fun renderFinal(offset: Int, query: SearchQuery) {
render(mutableMapOf(tag.editor to IntArrayList(intArrayOf(tag.offset))), AcceptedTagRenderer, query::getHighlightLength) render(IntArrayList(intArrayOf(offset)), AcceptedTagRenderer, query::getHighlightLength)
} }
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) { private inline fun render(offsets: IntList, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
for ((editor, offsets) in results) { val markup = editor.markupModel
val highlights = previousHighlights[editor] val chars = editor.immutableText
val markup = editor.markupModel val modifications = (previousHighlights?.size ?: 0) + offsets.size
val document = editor.document val enableBulkEditing = modifications > 1000
val chars = editor.immutableText
val modifications = (highlights?.size ?: 0) + offsets.size val document = editor.document
val enableBulkEditing = modifications > 1000
try { try {
if (enableBulkEditing) { if (enableBulkEditing) {
document.isInBulkUpdate = true document.isInBulkUpdate = true
} }
highlights?.forEach(markup::removeHighlighter) previousHighlights?.forEach(markup::removeHighlighter)
previousHighlights[editor] = Array(offsets.size) { index -> previousHighlights = Array(offsets.size) { index ->
val start = offsets.getInt(index) val start = offsets.getInt(index)
val end = start + getHighlightLength(chars, start) val end = start + getHighlightLength(chars, start)
markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply { markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
customRenderer = renderer customRenderer = renderer
}
}
} finally {
if (enableBulkEditing) {
document.isInBulkUpdate = false
} }
} }
} } finally {
if (enableBulkEditing) {
for (editor in previousHighlights.keys.toList()) { document.isInBulkUpdate = false
if (!results.containsKey(editor)) {
previousHighlights.remove(editor)?.forEach(editor.markupModel::removeHighlighter)
} }
} }
} }
fun reset() { fun reset() {
previousHighlights.keys.forEach { it.markupModel.removeAllHighlighters() } editor.markupModel.removeAllHighlighters()
previousHighlights.clear() previousHighlights = null
} }
/** /**

View File

@@ -1,6 +1,6 @@
<idea-plugin> <idea-plugin url="https://github.com/acejump/AceJump">
<name>AceJump</name> <name>AceJump</name>
<id>AceJump-chylex</id> <id>AceJump</id>
<description><![CDATA[ <description><![CDATA[
AceJump allows you to quickly navigate the caret to any position visible in the editor. AceJump allows you to quickly navigate the caret to any position visible in the editor.
@@ -40,18 +40,11 @@
<actions> <actions>
<action id="AceAction" <action id="AceAction"
class="org.acejump.action.AceKeyboardAction$ActivateAceJump" class="org.acejump.action.AceKeyboardAction$ActivateAceJump"
text="Activate AceJump"> text="Activate AceJump Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action> </action>
<action id="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" <action id="AceLineAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode" class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode"> text="Start AceJump in All Line Marks Mode">
@@ -77,59 +70,5 @@
<action id="AceWordBackwardsAction" <action id="AceWordBackwardsAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/> text="Start AceJump in All Words Before Caret Mode"/>
<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> </actions>
</idea-plugin> </idea-plugin>

View File

@@ -1,4 +1,5 @@
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.action.AceKeyboardAction
import org.acejump.test.util.BaseTest import org.acejump.test.util.BaseTest
@@ -27,14 +28,48 @@ class AceTest : BaseTest() {
fun `test a query containing a { character`() = fun `test a query containing a { character`() =
assertEquals("abcd{dabc cdab".search("cd{"), setOf(2)) 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`() { fun `test tag selection`() {
"<caret>testing 1234".search("g") "<caret>testing 1234".search("g")
typeAndWaitForResults(session.tags[0].key) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("j")
myFixture.checkResult("testin<caret>g 1234") myFixture.checkResult("testin<caret>g 1234")
} }
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`() { fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two") makeEditor("test words <caret> before caret is two")
@@ -59,10 +94,20 @@ class AceTest : BaseTest() {
assertEquals(3, session.tags.size) assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key) typeAndWaitForResults(session.tags[1].key)
typeAndWaitForResults("j")
myFixture.checkResult("test <caret>word action") myFixture.checkResult("test <caret>word action")
} }
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`() { fun `test line mode`() {
makeEditor(" test\n three\n lines\n") makeEditor(" test\n three\n lines\n")

View File

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

View File

@@ -2,7 +2,6 @@ package org.acejump.test.util
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.fixtures.BasePlatformTestCase
@@ -39,9 +38,7 @@ abstract class BaseTest : BasePlatformTestCase() {
fun takeAction(action: AnAction) = myFixture.testAction(action) fun takeAction(action: AnAction) = myFixture.testAction(action)
fun makeEditor(contents: String): PsiFile { fun makeEditor(contents: String): PsiFile {
val file = myFixture.configureByText(PlainTextFileType.INSTANCE, contents) return myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
(myFixture.editor as EditorImpl).scrollPane.viewport.setSize(1000, 100)
return file
} }
fun resetEditor() { fun resetEditor() {
@@ -55,7 +52,7 @@ abstract class BaseTest : BasePlatformTestCase() {
UIUtil.dispatchAllInvocationEvents() UIUtil.dispatchAllInvocationEvents()
} }
private fun String.executeQuery(query: String) { fun String.executeQuery(query: String) {
myFixture.run { myFixture.run {
makeEditor(this@executeQuery) makeEditor(this@executeQuery)
testAction(AceKeyboardAction.ActivateAceJump) testAction(AceKeyboardAction.ActivateAceJump)