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

30 Commits

Author SHA1 Message Date
8062cf5cab Update dependency versions 2023-01-09 07:19:07 +01:00
9151ee376c Set plugin version to chylex-11 2022-07-06 15:46:45 +02:00
19ce1c69fd Improve tag order for non-QWERTY layouts 2022-07-06 15:46:35 +02:00
f2a053505c Remove no longer necessary actions 2022-07-06 15:46:35 +02:00
647cfb14f1 Remove unused code 2022-07-06 15:46:35 +02:00
c31ba60909 Implement a customized Vim easymotion plugin 2022-07-06 15:46:35 +02:00
9c60a8a4ba Update build.gradle IDEA & plugin versions 2022-06-18 20:59:52 +02:00
1e8b7d7963 Remove Kotlin stdlib from distribution 2022-06-18 20:54:58 +02:00
9157ce19b0 Remove all special modes introduced in the rework 2021-11-14 14:35:54 +01:00
9a435feccc Fix broken special actions in Rider 2021-09-29 08:56:15 +02:00
c1feb891e4 Update Gradle wrapper to 7.1 2021-09-29 08:25:09 +02:00
0b6ba62cda Simplify existing modes and add new vim shortcuts 2021-05-15 20:58:58 +02:00
dfd5b122a0 Optimizations 2021-05-15 20:30:18 +02:00
f2400d134d Update build.gradle 2021-05-15 19:46:51 +02:00
98997b4d86 [WIP] Fix tests 2021-05-08 03:54:27 +02:00
1e172e9c49 [WIP] Implement jumping across splitters 2021-05-08 02:39:27 +02:00
01b37c878e [WIP] Add more vim-friendly jump actions (declaration, usages) 2021-04-11 08:34:52 +02:00
1630c706a9 [WIP] Fix easymotion-like actions in visual mode 2021-04-08 13:57:11 +02:00
041a130c5f [WIP] Add a few easymotion-like actions 2021-04-07 03:05:11 +02:00
7561bfde36 [WIP] Make quick jump the default mode and rename non-quick jump to advanced mode 2021-04-07 00:16:50 +02:00
724e469f21 [WIP] Remove 'From Caret' mode 2021-04-06 02:09:29 +02:00
bce9a5f636 [WIP] Add quick jump mode 2021-03-29 18:43:00 +02:00
bfe0aa536e [WIP] Change plugin version 2021-03-29 18:42:40 +02:00
2ca21b8423 [WIP] Work around weird highlight offset bug 2021-03-29 18:42:00 +02:00
430bcf6883 [WIP] Remove word start jump to simplify 2021-03-29 18:41:59 +02:00
092650af81 [WIP] Pressing Enter before typing query starts jump mode for character at caret 2021-03-29 18:41:57 +02:00
2009123114 [WIP] Interactive modes 2021-03-29 18:41:54 +02:00
de19c0e0b8 Make Enter immediately tag search occurrences 2021-03-29 18:39:15 +02:00
5021a07fb9 Remove tag visiting functionality 2021-03-29 18:38:32 +02:00
1180547b06 Remove whole file search 2021-03-29 18:36:53 +02:00
51 changed files with 1327 additions and 1145 deletions

1
.gitignore vendored
View File

@@ -15,7 +15,6 @@ build/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar !gradle-wrapper.jar
gradle.properties
# Mac OS # Mac OS
.DS_Store .DS_Store

View File

@@ -1,66 +1,26 @@
import org.jetbrains.changelog.closure
import org.jetbrains.intellij.tasks.PatchPluginXmlTask
import org.jetbrains.intellij.tasks.PublishTask
import org.jetbrains.intellij.tasks.RunIdeTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
idea apply true idea
kotlin("jvm") version "1.3.72" kotlin("jvm") version "1.5.10"
id("org.jetbrains.intellij") version "0.7.2" id("org.jetbrains.intellij") version "1.11.0"
id("org.jetbrains.changelog") version "1.1.2"
id("com.github.ben-manes.versions") version "0.38.0"
}
tasks {
withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
named<Zip>("buildPlugin") {
dependsOn("test")
archiveFileName.set("AceJump.zip")
}
withType<RunIdeTask> {
dependsOn("test")
findProperty("luginDev")?.let { args = listOf(projectDir.absolutePath) }
}
withType<PublishTask> {
val intellijPublishToken: String? by project
token(intellijPublishToken)
}
withType<PatchPluginXmlTask> {
sinceBuild("201.6668.0")
changeNotes({ changelog.getLatest().toHTML() })
}
}
changelog {
path = "${project.projectDir}/CHANGES.md"
header = closure { "${project.version}" }
}
dependencies {
// gradle-intellij-plugin doesn't attach sources properly for Kotlin :(
compileOnly(kotlin("stdlib-jdk8"))
// https://github.com/promeG/TinyPinyin
implementation("com.github.promeg:tinypinyin:2.0.3")
} }
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
} }
intellij { intellij {
version = "2020.2" version.set("2022.3.1")
pluginName = "AceJump" updateSinceUntilBuild.set(false)
updateSinceUntilBuild = false pluginsRepositories.custom("https://intellij.chylex.com")
setPlugins("java") plugins.add("IdeaVIM:chylex-15")
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
kotlinOptions.freeCompilerArgs = listOf("-Xjvm-default=all")
} }
group = "org.acejump" group = "org.acejump"
version = "3.7" version = "chylex-11"

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
kotlin.stdlib.default.dependency=false

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-6.8-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -72,7 +72,7 @@ case "`uname`" in
Darwin* ) Darwin* )
darwin=true darwin=true
;; ;;
MINGW* ) MSYS* | MINGW* )
msys=true msys=true
;; ;;
NONSTOP* ) NONSTOP* )

View File

@@ -1,6 +1,11 @@
package org.acejump package org.acejump
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.Project
import com.intellij.util.IncorrectOperationException
import it.unimi.dsi.fastutil.ints.IntArrayList
annotation class ExternalUsage annotation class ExternalUsage
@@ -10,6 +15,21 @@ annotation class ExternalUsage
val Editor.immutableText val Editor.immutableText
get() = this.document.immutableCharSequence get() = this.document.immutableCharSequence
/**
* Returns all open editors in the project.
*/
val Project.openEditors: List<Editor>
get() {
return try {
FileEditorManagerEx.getInstanceEx(this)
.splitters
.selectedEditors
.mapNotNull { (it as? TextEditor)?.editor }
} catch (e: IncorrectOperationException) {
emptyList()
}
}
/** /**
* Returns true if [this] contains [otherText] at the specified offset. * Returns true if [this] contains [otherText] at the specified offset.
*/ */
@@ -36,7 +56,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.isJavaIdentifierPart() get() = this in 'a'..'z' || this.isJavaIdentifierPart()
/** /**
* Finds index of the first character in a word. * Finds index of the first character in a word.
@@ -56,8 +76,9 @@ 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 < length - 1 && isPartOfWord(this[end + 1])) { while (end < limit && isPartOfWord(this[end + 1])) {
++end ++end
} }
@@ -69,14 +90,25 @@ inline fun CharSequence.wordEnd(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 < length - 1 && !isPartOfWord(this[end + 1])) { while (end < limit && !isPartOfWord(this[end + 1])) {
++end ++end
} }
if (end < length - 1 && isPartOfWord(this[end + 1])) { if (end < limit && 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

@@ -1,71 +0,0 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.project.DumbAwareAction
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.input.JumpMode
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated actions that create or update an AceJump [Session].
*/
sealed class AceAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
invoke(SessionManager.start(e.getData(EDITOR) ?: return))
}
abstract operator fun invoke(session: Session)
/**
* Generic action type that toggles a specific [JumpMode].
*/
abstract class BaseToggleJumpModeAction(private val mode: JumpMode) : AceAction() {
final override fun invoke(session: Session) = session.toggleJumpMode(mode)
}
/**
* Generic action type that starts a regex search.
*/
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries) : AceAction() {
override fun invoke(session: Session) = session.startRegexSearch(pattern, boundaries)
}
/**
* Initiates an AceJump session in the first [JumpMode], or cycles to the next [JumpMode] as defined in configuration.
*/
object ActivateOrCycleMode : AceAction() {
override fun invoke(session: Session) = session.cycleNextJumpMode()
}
/**
* Initiates an AceJump session in the last [JumpMode], or cycles to the previous [JumpMode] as defined in configuration.
*/
object ActivateOrReverseCycleMode : AceAction() {
override fun invoke(session: Session) = session.cyclePreviousJumpMode()
}
// @formatter:off
object ToggleJumpMode : BaseToggleJumpModeAction(JumpMode.JUMP)
object ToggleJumpEndMode : BaseToggleJumpModeAction(JumpMode.JUMP_END)
object ToggleTargetMode : BaseToggleJumpModeAction(JumpMode.TARGET)
object ToggleDeclarationMode : BaseToggleJumpModeAction(JumpMode.DEFINE)
object StartAllWordsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.WHOLE_FILE)
object StartAllWordsBackwardsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.BEFORE_CARET)
object StartAllWordsForwardMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.AFTER_CARET)
object StartAllLineStartsMode : BaseRegexSearchAction(Pattern.LINE_STARTS, StandardBoundaries.WHOLE_FILE)
object StartAllLineEndsMode : BaseRegexSearchAction(Pattern.LINE_ENDS, StandardBoundaries.WHOLE_FILE)
object StartAllLineIndentsMode : BaseRegexSearchAction(Pattern.LINE_INDENTS, StandardBoundaries.WHOLE_FILE)
object StartAllLineMarksMode : BaseRegexSearchAction(Pattern.LINE_ALL_MARKS, StandardBoundaries.WHOLE_FILE)
// @formatter:on
}

View File

@@ -4,15 +4,13 @@ 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.session.Session import org.acejump.session.Session
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
/** /**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session]. * Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
*/ */
sealed class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() { abstract 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)
} }
@@ -40,23 +38,7 @@ sealed class AceEditorAction(private val originalHandler: EditorActionHandler) :
override fun run(session: Session) = session.restart() override fun run(session: Session) = session.restart()
} }
class SelectBackward(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitPreviousTag() override fun run(session: Session) = session.tagImmediately()
}
class SelectForward(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitNextTag()
}
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.WHOLE_FILE)
}
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.WHOLE_FILE)
}
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.WHOLE_FILE)
} }
} }

View File

@@ -0,0 +1,52 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.project.DumbAwareAction
import org.acejump.boundaries.Boundaries
import org.acejump.openEditors
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated actions that create or update an AceJump [Session].
*/
abstract class AceKeyboardAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(EDITOR) ?: return
val project = e.project
if (project != null) {
val jumpEditors = project.openEditors
.sortedBy { if (it === editor) 0 else 1 }
.ifEmpty { listOf(editor) }
invoke(SessionManager.start(editor, jumpEditors))
}
else {
invoke(SessionManager.start(editor))
}
}
abstract operator fun invoke(session: Session)
/**
* Generic action type that starts a regex search.
*/
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries) : AceKeyboardAction() {
override fun invoke(session: Session) {
session.defaultBoundary = boundaries
session.startRegexSearch(pattern)
}
}
/**
* Starts or ends an AceJump session in quick jump mode.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.startJumpMode()
}
}

View File

@@ -0,0 +1,180 @@
package org.acejump.action
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.search.SearchProcessor
import org.acejump.wordStart
/**
* Base class for actions available after typing a tag.
*/
sealed class AceTagAction {
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean)
abstract class BaseJumpAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
recordCaretPosition(editor)
if (isFinal) {
ensureEditorFocused(editor)
}
moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset))
if (shiftMode) {
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
}
abstract fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int
}
abstract class BaseWordAction : BaseJumpAction() {
final override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
val matchingChars = countMatchingCharacters(editor, searchProcessor, offset)
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && editor.immutableText.let { it[targetOffset - 1].isWordPart && it[targetOffset].isWordPart }
return getCaretOffset(editor, offset, targetOffset, isInsideWord)
}
abstract fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int
}
private companion object {
fun countMatchingCharacters(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return editor.immutableText.countMatchingCharacters(offset, searchProcessor.query.rawText)
}
fun recordCaretPosition(editor: Editor) = with(editor) {
project?.let { addCurrentPositionToHistory(it, document) }
}
fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
selectionModel.removeSelection(true)
caretModel.removeSecondaryCarets()
caretModel.moveToOffset(offset)
}
fun performAction(actionName: String) {
val actionManager = ActionManager.getInstance()
val action = actionManager.getAction(actionName)
if (action != null) {
actionManager.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
}
fun ensureEditorFocused(editor: Editor) {
val project = editor.project ?: return
val fem = FileEditorManagerEx.getInstanceEx(project)
val window = fem.windows.firstOrNull { (it.selectedEditor?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
if (window != null && window !== fem.currentWindow) {
fem.currentWindow = window
}
}
private fun addCurrentPositionToHistory(project: Project, document: Document) {
CommandProcessor.getInstance().executeCommand(project, {
with(IdeDocumentHistory.getInstance(project)) {
setCurrentCommandHasMoves()
includeCurrentCommandAsNavigation()
includeCurrentPlaceAsChangePlace()
}
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document), UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document)
}
}
// Actions
/**
* On default action, places the caret at the first character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpToSearchStart : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset
}
}
/**
* On default action, places the caret at the start of a word. Word detection uses [Character.isJavaIdentifierPart] to count some special
* characters, such as underscores, as part of a word. If there is no word at the last character of the search query, then the caret is
* placed at the first character of the search query.
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordStart : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordStart(queryEndOffset)
else
queryStartOffset
}
}
/**
* On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
* On shift action, performs the Go To Type Declaration action, available via `Navigate | Type Declaration`.
* Always places the caret at the start of the word.
*/
object GoToDeclaration : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_GOTO_TYPE_DECLARATION else IdeActions.ACTION_GOTO_DECLARATION) }
}
}
/**
* On default action, performs the Show Usages action, available via the context menu.
* On shift action, performs the Find Usages action, available via the context menu.
* Always places the caret at the start of the word.
*/
object ShowUsages : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_FIND_USAGES else ShowUsagesAction.ID) }
}
}
/**
* Performs the Show Context Actions action, available via the context menu or Alt+Enter.
* Always places the caret at the start of the word.
*/
object ShowIntentions : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS) }
}
}
/**
* On default action, performs the Refactor This action, available via the main menu.
* On shift action, performs the Rename... refactoring, available via the main menu.
* Always places the caret at the start of the word.
*/
object Refactor : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_RENAME else "Refactorings.QuickListPopupAction") }
}
}
}

View File

@@ -0,0 +1,66 @@
package org.acejump.action
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.modes.ActionMode
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 abstract fun start(session: Session)
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

@@ -1,108 +0,0 @@
package org.acejump.action
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import org.acejump.*
import org.acejump.input.JumpMode
import org.acejump.input.JumpMode.*
import org.acejump.search.SearchProcessor
/**
* Performs [JumpMode] navigation and actions.
*/
internal class TagJumper(private val editor: Editor, private val mode: JumpMode, private val searchProcessor: SearchProcessor?) {
/**
* Moves caret to a specific offset in the editor according to the positioning and selection rules of the current [JumpMode].
*/
fun visit(offset: Int) {
if (mode === JUMP_END || mode === TARGET) {
val chars = editor.immutableText
val matchingChars = searchProcessor?.let { chars.countMatchingCharacters(offset, it.query.rawText) } ?: 0
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && chars[targetOffset - 1].isWordPart && chars[targetOffset].isWordPart
val finalTargetOffset = if (isInsideWord) chars.wordEnd(targetOffset) + 1 else targetOffset
if (mode === JUMP_END) {
moveCaretTo(editor, finalTargetOffset)
}
else if (mode === TARGET) {
if (isInsideWord) {
selectRange(editor, chars.wordStart(targetOffset), finalTargetOffset)
}
else {
selectRange(editor, offset, finalTargetOffset)
}
}
}
else {
moveCaretTo(editor, offset)
}
}
/**
* Updates caret and selection by [visit]ing a specific offset in the editor, and applying session-finalizing [JumpMode] actions such as
* using the Go To Declaration action, or selecting text between caret and target offset/word if Shift was held during the jump.
*/
fun jump(offset: Int, shiftMode: Boolean) {
val oldOffset = editor.caretModel.offset
visit(offset)
if (mode === DEFINE) {
performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
return
}
if (shiftMode) {
val newOffset = editor.caretModel.offset
if (mode === TARGET) {
selectRange(editor, oldOffset, when {
newOffset < oldOffset -> editor.selectionModel.selectionStart
else -> editor.selectionModel.selectionEnd
})
}
else {
selectRange(editor, oldOffset, newOffset)
}
}
}
private companion object {
private fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
project?.let { addCurrentPositionToHistory(it, document) }
selectionModel.removeSelection(true)
caretModel.moveToOffset(offset)
}
private fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int) = with(editor) {
selectionModel.removeSelection(true)
selectionModel.setSelection(fromOffset, toOffset)
caretModel.moveToOffset(toOffset)
}
private fun addCurrentPositionToHistory(project: Project, document: Document) {
CommandProcessor.getInstance().executeCommand(project, {
with(IdeDocumentHistory.getInstance(project)) {
setCurrentCommandHasMoves()
includeCurrentCommandAsNavigation()
includeCurrentPlaceAsChangePlace()
}
}, "AceJumpHistoryAppender", DocCommandGroupId.noneGroupId(document), UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, document)
}
private fun performAction(action: AnAction) {
ActionManager.getInstance().tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
}
}

View File

@@ -1,73 +0,0 @@
package org.acejump.action
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.SelectionModel
import org.acejump.search.SearchProcessor
import kotlin.math.abs
/**
* Enables navigation between currently active tags.
*/
internal class TagVisitor(private val editor: Editor, private val searchProcessor: SearchProcessor, private val tagJumper: TagJumper) {
/**
* Places caret at the closest tag following the caret position, according to the rules of the current jump mode (see [TagJumper.visit]).
* If the caret is at or past the last tag, it moves to the first tag instead.
* If there is only one tag, it immediately performs the jump action as described in [TagJumper.jump].
*/
fun visitNext(): Boolean {
return visit(SelectionModel::getSelectionEnd) { if (it < 0) -it - 1 else it + 1 }
}
/**
* Places caret at the closest tag preceding the caret position, according to the rules of the current jump mode (see [TagJumper.visit]).
* If the caret is at or before the first tag, it moves to the last tag instead.
* If there is only one tag, it immediately performs the jump action as described in [TagJumper.jump].
*/
fun visitPrevious(): Boolean {
return visit(SelectionModel::getSelectionStart) { if (it < 0) -it - 2 else it - 1 }
}
/**
* Scrolls to the closest result to the caret.
*/
fun scrollToClosest() {
val caret = editor.caretModel.offset
val results = searchProcessor.results.takeUnless { it.isEmpty } ?: return
val index = results.binarySearch(caret).let { if (it < 0) -it - 1 else it }
val targetOffset = listOfNotNull(
results.getOrNull(index - 1),
results.getOrNull(index)
).minBy {
abs(it - caret)
}
if (targetOffset != null) {
editor.scrollingModel.scrollTo(editor.offsetToLogicalPosition(targetOffset), ScrollType.RELATIVE)
}
}
private inline fun visit(caretPosition: SelectionModel.() -> Int, indexModifier: (Int) -> Int): Boolean {
val results = searchProcessor.results.takeUnless { it.isEmpty } ?: return false
val nextIndex = indexModifier(results.binarySearch(caretPosition(editor.selectionModel)))
val targetOffset = results.getInt(when {
nextIndex < 0 -> results.lastIndex
nextIndex > results.lastIndex -> 0
else -> nextIndex
})
val onlyResult = results.size == 1
if (onlyResult) {
tagJumper.jump(targetOffset, shiftMode = false)
}
else {
tagJumper.visit(targetOffset)
}
editor.scrollingModel.scrollToCaret(ScrollType.RELATIVE)
return onlyResult
}
}

View File

@@ -3,16 +3,6 @@ package org.acejump.boundaries
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
enum class StandardBoundaries : Boundaries { enum class StandardBoundaries : Boundaries {
WHOLE_FILE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0 until editor.document.textLength
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in (0 until editor.document.textLength)
}
},
VISIBLE_ON_SCREEN { VISIBLE_ON_SCREEN {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange { override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val (topLeft, bottomRight) = cache.visibleArea(editor) val (topLeft, bottomRight) = cache.visibleArea(editor)
@@ -45,21 +35,34 @@ 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..(editor.caretModel.offset) return 0 until 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 until editor.document.textLength return (editor.caretModel.offset + 1) 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
}
},
CARET_LINE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val document = editor.document
val offset = editor.caretModel.offset
val line = document.getLineNumber(offset)
return document.getLineStartOffset(line)..document.getLineEndOffset(line)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in getOffsetRange(editor, cache)
} }
} }
} }

View File

@@ -19,16 +19,12 @@ class AceConfig : PersistentStateComponent<AceSettings> {
get() = ServiceManager.getService(AceConfig::class.java).aceSettings get() = ServiceManager.getService(AceConfig::class.java).aceSettings
val layout get() = settings.layout val layout get() = settings.layout
val cycleModes get() = settings.let { arrayOf(it.cycleMode1, it.cycleMode2, it.cycleMode3, it.cycleMode4) }
val minQueryLength get() = settings.minQueryLength val minQueryLength get() = settings.minQueryLength
val jumpModeColor get() = settings.jumpModeColor val jumpModeColor get() = settings.jumpModeColor
val jumpEndModeColor get() = settings.jumpEndModeColor
val targetModeColor get() = settings.targetModeColor
val definitionModeColor get() = settings.definitionModeColor
val textHighlightColor get() = settings.textHighlightColor val textHighlightColor get() = settings.textHighlightColor
val tagForegroundColor get() = settings.tagForegroundColor val tagForegroundColor get() = settings.tagForegroundColor
val tagBackgroundColor get() = settings.tagBackgroundColor val tagBackgroundColor get() = settings.tagBackgroundColor
val searchWholeFile get() = settings.searchWholeFile val acceptedTagColor get() = settings.acceptedTagColor
} }
override fun getState(): AceSettings { override fun getState(): AceSettings {

View File

@@ -14,36 +14,22 @@ class AceConfigurable : Configurable {
override fun isModified() = override fun isModified() =
panel.allowedChars != settings.allowedChars || panel.allowedChars != settings.allowedChars ||
panel.keyboardLayout != settings.layout || panel.keyboardLayout != settings.layout ||
panel.cycleMode1 != settings.cycleMode1 ||
panel.cycleMode2 != settings.cycleMode2 ||
panel.cycleMode3 != settings.cycleMode3 ||
panel.cycleMode4 != settings.cycleMode4 ||
panel.minQueryLengthInt != settings.minQueryLength || panel.minQueryLengthInt != settings.minQueryLength ||
panel.jumpModeColor != settings.jumpModeColor || panel.jumpModeColor != settings.jumpModeColor ||
panel.jumpEndModeColor != settings.jumpEndModeColor ||
panel.targetModeColor != settings.targetModeColor ||
panel.definitionModeColor != settings.definitionModeColor ||
panel.textHighlightColor != settings.textHighlightColor || panel.textHighlightColor != settings.textHighlightColor ||
panel.tagForegroundColor != settings.tagForegroundColor || panel.tagForegroundColor != settings.tagForegroundColor ||
panel.tagBackgroundColor != settings.tagBackgroundColor || panel.tagBackgroundColor != settings.tagBackgroundColor ||
panel.searchWholeFile != settings.searchWholeFile panel.acceptedTagColor != settings.acceptedTagColor
override fun apply() { override fun apply() {
settings.allowedChars = panel.allowedChars settings.allowedChars = panel.allowedChars
settings.layout = panel.keyboardLayout settings.layout = panel.keyboardLayout
settings.cycleMode1 = panel.cycleMode1
settings.cycleMode2 = panel.cycleMode2
settings.cycleMode3 = panel.cycleMode3
settings.cycleMode4 = panel.cycleMode4
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.jumpEndModeColor?.let { settings.jumpEndModeColor = it }
panel.targetModeColor?.let { settings.targetModeColor = it }
panel.definitionModeColor?.let { settings.definitionModeColor = 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 }
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it } panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it }
settings.searchWholeFile = panel.searchWholeFile panel.acceptedTagColor?.let { settings.acceptedTagColor = it }
KeyLayoutCache.reset(settings) KeyLayoutCache.reset(settings)
} }

View File

@@ -1,7 +1,6 @@
package org.acejump.config package org.acejump.config
import com.intellij.util.xmlb.annotations.OptionTag import com.intellij.util.xmlb.annotations.OptionTag
import org.acejump.input.JumpMode
import org.acejump.input.KeyLayout import org.acejump.input.KeyLayout
import org.acejump.input.KeyLayout.QWERTY import org.acejump.input.KeyLayout.QWERTY
import java.awt.Color import java.awt.Color
@@ -9,24 +8,11 @@ import java.awt.Color
data class AceSettings( data class AceSettings(
var layout: KeyLayout = QWERTY, var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars, var allowedChars: String = layout.allChars,
var cycleMode1: JumpMode = JumpMode.JUMP,
var cycleMode2: JumpMode = JumpMode.DEFINE,
var cycleMode3: JumpMode = JumpMode.TARGET,
var cycleMode4: JumpMode = JumpMode.JUMP_END,
var minQueryLength: Int = 1, var minQueryLength: Int = 1,
@OptionTag("jumpModeRGB", converter = ColorConverter::class) @OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF), var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("jumpEndModeRGB", converter = ColorConverter::class)
var jumpEndModeColor: Color = Color(0x33E78A),
@OptionTag("targetModeRGB", converter = ColorConverter::class)
var targetModeColor: Color = Color(0xFFB700),
@OptionTag("definitionModeRGB", converter = ColorConverter::class)
var definitionModeColor: Color = Color(0x6FC5FF),
@OptionTag("textHighlightRGB", converter = ColorConverter::class) @OptionTag("textHighlightRGB", converter = ColorConverter::class)
var textHighlightColor: Color = Color(0x394B58), var textHighlightColor: Color = Color(0x394B58),
@@ -36,5 +22,6 @@ data class AceSettings(
@OptionTag("tagBackgroundRGB", converter = ColorConverter::class) @OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
var tagBackgroundColor: Color = Color(0x008299), var tagBackgroundColor: Color = Color(0x008299),
var searchWholeFile: Boolean = true @OptionTag("acceptedTagRGB", converter = ColorConverter::class)
var acceptedTagColor: Color = Color(0x394B58)
) )

View File

@@ -2,14 +2,12 @@ package org.acejump.config
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.ColorPanel import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.Cell import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.GrowPolicy.MEDIUM_TEXT import com.intellij.ui.layout.GrowPolicy.MEDIUM_TEXT
import com.intellij.ui.layout.GrowPolicy.SHORT_TEXT import com.intellij.ui.layout.GrowPolicy.SHORT_TEXT
import com.intellij.ui.layout.panel import com.intellij.ui.layout.panel
import org.acejump.input.JumpMode
import org.acejump.input.KeyLayout import org.acejump.input.KeyLayout
import java.awt.Color import java.awt.Color
import java.awt.Font import java.awt.Font
@@ -27,28 +25,17 @@ internal class AceSettingsPanel {
private val tagCharsField = JBTextField() private val tagCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>() private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false } private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val cycleModeCombo1 = ComboBox<JumpMode>()
private val cycleModeCombo2 = ComboBox<JumpMode>()
private val cycleModeCombo3 = ComboBox<JumpMode>()
private val cycleModeCombo4 = ComboBox<JumpMode>()
private val minQueryLengthField = JBTextField() private val minQueryLengthField = JBTextField()
private val jumpModeColorWheel = ColorPanel() private val jumpModeColorWheel = ColorPanel()
private val jumpEndModeColorWheel = ColorPanel()
private val targetModeColorWheel = ColorPanel()
private val definitionModeColorWheel = ColorPanel()
private val textHighlightColorWheel = ColorPanel() private val textHighlightColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel() private val tagForegroundColorWheel = ColorPanel()
private val tagBackgroundColorWheel = ColorPanel() private val tagBackgroundColorWheel = ColorPanel()
private val searchWholeFileCheckBox = JBCheckBox() private val acceptedTagColorWheel = ColorPanel()
init { init {
tagCharsField.apply { font = Font("monospaced", font.style, font.size) } tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) } keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") } keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") }
cycleModeCombo1.setupEnumItems { cycleMode1 = it }
cycleModeCombo2.setupEnumItems { cycleMode2 = it }
cycleModeCombo3.setupEnumItems { cycleMode3 = it }
cycleModeCombo4.setupEnumItems { cycleMode4 = it }
} }
internal val rootPanel: JPanel = panel { internal val rootPanel: JPanel = panel {
@@ -61,30 +48,16 @@ internal class AceSettingsPanel {
row("Keyboard design:") { short(keyboardLayoutArea) } row("Keyboard design:") { short(keyboardLayoutArea) }
} }
titledRow("Modes") { titledRow("Behavior") {
row("Cycle order:") { row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
cell(isVerticalFlow = false, isFullWidth = false) {
cycleModeCombo1()
cycleModeCombo2()
cycleModeCombo3()
cycleModeCombo4()
}
}
} }
titledRow("Colors") { titledRow("Colors") {
row("Jump mode caret background:") { short(jumpModeColorWheel) } row("Caret background:") { short(jumpModeColorWheel) }
row("Jump to End mode caret background:") { short(jumpEndModeColorWheel) }
row("Target mode caret background:") { short(targetModeColorWheel) }
row("Definition mode caret background:") { short(definitionModeColorWheel) }
row("Searched text background:") { short(textHighlightColorWheel) } row("Searched text background:") { short(textHighlightColorWheel) }
row("Tag foreground:") { short(tagForegroundColorWheel) } row("Tag foreground:") { short(tagForegroundColorWheel) }
row("Tag background:") { short(tagBackgroundColorWheel) } row("Tag background:") { short(tagBackgroundColorWheel) }
} row("Accepted tag position background:") { short(acceptedTagColorWheel) }
titledRow("Behavior") {
row { short(searchWholeFileCheckBox.apply { text = "Search whole file" }) }
row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
} }
} }
@@ -92,19 +65,12 @@ internal class AceSettingsPanel {
internal var allowedChars by tagCharsField internal var allowedChars by tagCharsField
internal var keyboardLayout by keyboardLayoutCombo internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea internal var keyChars by keyboardLayoutArea
internal var cycleMode1 by cycleModeCombo1
internal var cycleMode2 by cycleModeCombo2
internal var cycleMode3 by cycleModeCombo3
internal var cycleMode4 by cycleModeCombo4
internal var minQueryLength by minQueryLengthField internal var minQueryLength by minQueryLengthField
internal var jumpModeColor by jumpModeColorWheel internal var jumpModeColor by jumpModeColorWheel
internal var jumpEndModeColor by jumpEndModeColorWheel
internal var targetModeColor by targetModeColorWheel
internal var definitionModeColor by definitionModeColorWheel
internal var textHighlightColor by textHighlightColorWheel internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel internal var tagBackgroundColor by tagBackgroundColorWheel
internal var searchWholeFile by searchWholeFileCheckBox internal var acceptedTagColor by acceptedTagColorWheel
internal var minQueryLengthInt internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10) get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
@@ -113,19 +79,12 @@ internal class AceSettingsPanel {
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars allowedChars = settings.allowedChars
keyboardLayout = settings.layout keyboardLayout = settings.layout
cycleMode1 = settings.cycleMode1
cycleMode2 = settings.cycleMode2
cycleMode3 = settings.cycleMode3
cycleMode4 = settings.cycleMode4
minQueryLength = settings.minQueryLength.toString() minQueryLength = settings.minQueryLength.toString()
jumpModeColor = settings.jumpModeColor jumpModeColor = settings.jumpModeColor
jumpEndModeColor = settings.jumpEndModeColor
targetModeColor = settings.targetModeColor
definitionModeColor = settings.definitionModeColor
textHighlightColor = settings.textHighlightColor textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor tagForegroundColor = settings.tagForegroundColor
tagBackgroundColor = settings.tagBackgroundColor tagBackgroundColor = settings.tagBackgroundColor
searchWholeFile = settings.searchWholeFile acceptedTagColor = settings.acceptedTagColor
} }
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575 // Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575

View File

@@ -1,68 +0,0 @@
package org.acejump.input
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import org.acejump.config.AceConfig
import java.awt.Color
/**
* Describes modes that determine the behavior of a "jump" to a tag. Most modes have two variations:
* - **Default jump** happens when jumping without holding the Shift key
* - **Shift jump** happens when jumping while holding the Shift key
*/
enum class JumpMode {
/**
* Default value at the start of a session. If the session does not get assigned a proper [JumpMode] by the time the user requests a jump,
* the results of the jump are undefined.
*/
DISABLED,
/**
* On default jump, places the caret at the first character of the search query.
* On shift jump, does the above but also selects all text between the original and new caret positions.
*/
JUMP,
/**
* On default jump, places the caret at the end of a word. Word detection uses [Character.isJavaIdentifierPart] to count some special
* characters, such as underscores, as part of a word. If there is no word at the first character of the search query, then the caret is
* placed after the last character of the search query.
*
* On shift jump, does the above but also selects all text between the original and new caret positions.
*/
JUMP_END,
/**
* On default jump, places the caret at the end of a word, and also selects the entire word. Word detection uses
* [Character.isJavaIdentifierPart] to count some special characters, such as underscores, as part of a word. If there is no word at the
* first character of the search query, then the caret is placed after the last character of the search query, and all text between the
* start and end of the search query is selected.
*
* On shift jump, does the above but also selects all text between the original caret position and the new selection, merging the
* selections into one.
*/
TARGET,
/**
* On default jump, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
* On shift jump, performs the Go To Type Declaration action, available via `Navigate | Type Declaration`.
* Always places the caret at the first character of the search query.
*/
DEFINE;
val caretColor: Color
get() = when (this) {
JUMP -> AceConfig.jumpModeColor
JUMP_END -> AceConfig.jumpEndModeColor
DEFINE -> AceConfig.definitionModeColor
TARGET -> AceConfig.targetModeColor
DISABLED -> AbstractColorsScheme.INHERITED_COLOR_MARKER
}
override fun toString() = when (this) {
DISABLED -> "(Skip)"
JUMP -> "Jump"
JUMP_END -> "Jump to End"
TARGET -> "Target"
DEFINE -> "Definition"
}
}

View File

@@ -1,53 +0,0 @@
package org.acejump.input
import org.acejump.config.AceConfig
/**
* Remembers the current [JumpMode] for a session. Allows cycling [JumpMode]s according to the order defined in configuration, or toggling
* one specific [JumpMode] on or off.
*/
internal class JumpModeTracker {
private var currentMode = JumpMode.DISABLED
private var currentIndex = 0
/**
* Switches to the next/previous [JumpMode] defined in configuration, skipping any [JumpMode]s that are not assigned. If at least two
* [JumpMode]s are assigned in the cycle order, then cycling will wrap around. If only one [JumpMode] is assigned, then cycling will
* toggle that one mode.
*/
fun cycle(forward: Boolean): JumpMode {
val cycleModes = AceConfig.cycleModes
val direction = if (forward) 1 else -1
val start = if (currentIndex == 0 && !forward) 0 else currentIndex - 1
for (offset in 1 until cycleModes.size) {
val index = (start + cycleModes.size + (offset * direction)) % cycleModes.size
if (cycleModes[index] != JumpMode.DISABLED) {
currentMode = cycleModes[index]
currentIndex = index + 1
return currentMode
}
}
currentMode = JumpMode.DISABLED
currentIndex = 0
return currentMode
}
/**
* Switches to the specified [JumpMode]. If the current mode already equals the specified one, it resets to [JumpMode.DISABLED].
*/
fun toggle(newMode: JumpMode): JumpMode {
if (currentMode == newMode) {
currentMode = JumpMode.DISABLED
currentIndex = 0
}
else {
currentMode = newMode
currentIndex = AceConfig.cycleModes.indexOfFirst { it == newMode } + 1
}
return currentMode
}
}

View File

@@ -1,5 +1,10 @@
package org.acejump.input package org.acejump.input
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import java.awt.geom.Point2D
import kotlin.math.floor
/** /**
* Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how * Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how
* ergonomically difficult they are to press. * ergonomically difficult they are to press.
@@ -18,7 +23,38 @@ enum class KeyLayout(internal val rows: Array<String>, priority: String) {
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("") internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap() internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
private val keyDistances: Map<Char, Object2IntMap<Char>> by lazy {
val keyDistanceMap = mutableMapOf<Char, Object2IntMap<Char>>()
val keyLocations = mutableMapOf<Char, Point2D>()
for ((rowIndex, rowChars) in rows.withIndex()) {
val keyY = rowIndex * 1.2F // Slightly increase cost of traveling between rows.
for ((columnIndex, char) in rowChars.withIndex()) {
val keyX = columnIndex + (0.25F * rowIndex) // Assume a 1/4-key uniform stagger.
keyLocations[char] = Point2D.Float(keyX, keyY)
}
}
for (fromChar in allChars) {
val distances = Object2IntOpenHashMap<Char>()
val fromLocation = keyLocations.getValue(fromChar)
for (toChar in allChars) {
distances[toChar] = floor(2F * fromLocation.distanceSq(keyLocations.getValue(toChar))).toInt()
}
keyDistanceMap[fromChar] = distances
}
keyDistanceMap
}
internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int? { internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int? {
return { allPriorities[tagToChar(it)] } return { allPriorities[tagToChar(it)] }
} }
internal fun distanceBetweenKeys(char1: Char, char2: Char): Int {
return keyDistances.getValue(char1).getValue(char2)
}
} }

View File

@@ -7,48 +7,6 @@ import org.acejump.config.AceSettings
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ). * with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/ */
internal object KeyLayoutCache { internal object KeyLayoutCache {
/**
* Stores keys ordered by proximity to other keys for the QWERTY layout.
* TODO: Support more layouts, perhaps generate automatically.
*/
private val qwertyCharacterDistances = mapOf(
'j' to "jikmnhuolbgypvftcdrxsezawq8796054321",
'f' to "ftgvcdryhbxseujnzawqikmolp5463728190",
'k' to "kolmjipnhubgyvftcdrxsezawq9807654321",
'd' to "drfcxsetgvzawyhbqujnikmolp4352617890",
'l' to "lkopmjinhubgyvftcdrxsezawq0987654321",
's' to "sedxzawrfcqtgvyhbujnikmolp3241567890",
'a' to "aqwszedxrfctgvyhbujnikmolp1234567890",
'h' to "hujnbgyikmvftolcdrpxsezawq6758493021",
'g' to "gyhbvftujncdrikmxseolzawpq5647382910",
'y' to "yuhgtijnbvfrokmcdeplxswzaq6758493021",
't' to "tygfruhbvcdeijnxswokmzaqpl5647382910",
'u' to "uijhyokmnbgtplvfrcdexswzaq7869504321",
'r' to "rtfdeygvcxswuhbzaqijnokmpl4536271890",
'n' to "nbhjmvgyuiklocftpxdrzseawq7685940321",
'v' to "vcfgbxdrtyhnzseujmawikqolp5463728190",
'm' to "mnjkbhuilvgyopcftxdrzseawq8970654321",
'c' to "cxdfvzsertgbawyhnqujmikolp4352617890",
'b' to "bvghncftyujmxdrikzseolawqp6574839201",
'i' to "iokjuplmnhybgtvfrcdexswzaq8970654321",
'e' to "erdswtfcxzaqygvuhbijnokmpl3425167890",
'x' to "xzsdcawerfvqtgbyhnujmikolp3241567890",
'z' to "zasxqwedcrfvtgbyhnujmikolp1234567890",
'o' to "oplkimjunhybgtvfrcdexswzaq9087654321",
'w' to "wesaqrdxztfcygvuhbijnokmpl2314567890",
'p' to "plokimjunhybgtvfrcdexswzaq0987654321",
'q' to "qwaeszrdxtfcygvuhbijnokmpl1234567890",
'1' to "1234567890qawzsexdrcftvgybhunjimkolp",
'2' to "2134567890qwasezxdrcftvgybhunjimkolp",
'3' to "3241567890weqasdrzxcftvgybhunjimkolp",
'4' to "4352617890erwsdftqazxcvgybhunjimkolp",
'5' to "5463728190rtedfgywsxcvbhuqaznjimkolp",
'6' to "6574839201tyrfghuedcvbnjiwsxmkoqazlp",
'7' to "7685940321yutghjirfvbnmkoedclpwsxqaz",
'8' to "8796054321uiyhjkotgbnmlprfvedcwsxqaz",
'9' to "9807654321ioujklpyhnmtgbrfvedcwsxqaz",
'0' to "0987654321opiklujmyhntgbrfvedcwsxqaz").mapValues { (_, v) -> v.mapIndexed { index, char -> char to index }.toMap() }
/** /**
* Sorts tags according to current keyboard layout settings, and some predefined rules that force tags with digits, and tags with two * Sorts tags according to current keyboard layout settings, and some predefined rules that force tags with digits, and tags with two
* keys far apart, to be sorted after other (easier to type) tags. * keys far apart, to be sorted after other (easier to type) tags.
@@ -77,7 +35,7 @@ internal object KeyLayoutCache {
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
tagOrder = compareBy( tagOrder = compareBy(
{ it[0].isDigit() || it[1].isDigit() }, { it[0].isDigit() || it[1].isDigit() },
{ qwertyCharacterDistances.getValue(it[0]).getValue(it[1]) }, { settings.layout.distanceBetweenKeys(it[0], it[1]) },
settings.layout.priority { it[0] } settings.layout.priority { it[0] }
) )

View File

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

View File

@@ -0,0 +1,25 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
open class JumpMode : SessionMode {
override val caretColor
get() = AceConfig.jumpModeColor
protected var wasUpperCase = false
private set
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
wasUpperCase = charTyped.isUpperCase()
return state.type(charTyped)
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
return true
}
}

View File

@@ -0,0 +1,13 @@
package org.acejump.modes
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
import java.awt.Color
interface SessionMode {
val caretColor: Color
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult
fun accept(state: SessionState, acceptedTag: Tag): Boolean
}

View File

@@ -4,6 +4,9 @@ enum class Pattern(val regex: String) {
LINE_STARTS("^.|^\\n"), LINE_STARTS("^.|^\\n"),
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), 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

@@ -10,27 +10,24 @@ import org.acejump.matchesAt
/** /**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character. * Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character.
*/ */
internal class SearchProcessor private constructor(private val editor: Editor, query: SearchQuery, boundaries: Boundaries) { class SearchProcessor private constructor(query: SearchQuery, results: MutableMap<Editor, IntArrayList>) {
companion object { companion object {
fun fromChar(editor: Editor, char: Char, boundaries: Boundaries): SearchProcessor { fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.Literal(char.toString()), boundaries) return SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
} }
fun fromRegex(editor: Editor, pattern: String, boundaries: Boundaries): SearchProcessor { fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.RegularExpression(pattern), boundaries) return SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
} }
} }
var query = query private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
private set
var results = IntArrayList(0)
private set
init {
val regex = query.toRegex() val regex = query.toRegex()
if (regex != null) { if (regex != null) {
for (editor in editors) {
val offsets = IntArrayList()
val offsetRange = boundaries.getOffsetRange(editor) val offsetRange = boundaries.getOffsetRange(editor)
var result = regex.find(editor.immutableText, offsetRange.first) var result = regex.find(editor.immutableText, offsetRange.first)
@@ -42,13 +39,22 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
break break
} }
else if (boundaries.isOffsetInside(editor, index)) { else if (boundaries.isOffsetInside(editor, index)) {
results.add(index) offsets.add(index)
} }
result = result.next() result = result.next()
} }
results[editor] = offsets
} }
} }
}
internal var query = query
private set
internal var results = results
private set
/** /**
* Appends a character to the search query and removes all search results that no longer match the query. If the last typed character * Appends a character to the search query and removes all search results that no longer match the query. If the last typed character
@@ -57,13 +63,12 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
*/ */
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 && results.none { chars.matchesAt(it, newQuery, ignoreCase = true) }) { if (newQuery.length > 1 && !canMatchTag && !isContinuation(newQuery)) {
return false return false
} }
@@ -76,7 +81,10 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
query = SearchQuery.Literal(char.toString()) query = SearchQuery.Literal(char.toString())
tagger.unmark() tagger.unmark()
val iter = results.iterator() for ((editor, offsets) in results) {
val chars = editor.immutableText
val iter = offsets.iterator()
while (iter.hasNext()) { while (iter.hasNext()) {
val movedOffset = iter.nextInt() + newQuery.length - 1 val movedOffset = iter.nextInt() + newQuery.length - 1
@@ -88,6 +96,7 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
} }
} }
} }
}
else { else {
removeObsoleteResults(newQuery, tagger) removeObsoleteResults(newQuery, tagger)
query = SearchQuery.Literal(newQuery) query = SearchQuery.Literal(newQuery)
@@ -96,6 +105,20 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
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.
*/ */
@@ -103,21 +126,24 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
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()
for ((editor, offsets) in results.entries.toList()) {
val chars = editor.immutableText val chars = editor.immutableText
val remaining = IntArrayList() val remaining = IntArrayList()
val iter = results.iterator() val iter = offsets.iterator()
while (iter.hasNext()) { while (iter.hasNext()) {
val offset = iter.nextInt() val offset = iter.nextInt()
val endOffset = offset + lastCharOffset val endOffset = offset + lastCharOffset
val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase) val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase)
if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, offset)) { if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
remaining.add(offset) remaining.add(offset)
} }
} }
results = remaining results[editor] = remaining
}
} }
} }

View File

@@ -25,7 +25,7 @@ internal sealed class SearchQuery {
* Each occurrence must either match the entire query, or match the query up to a point so that the rest of the query matches the * Each occurrence must either match the entire query, or match the query up to a point so that the rest of the query matches the
* beginning of a tag at the location of the occurrence. * beginning of a tag at the location of the occurrence.
*/ */
class Literal(override var rawText: String) : SearchQuery() { class Literal(override val rawText: String) : SearchQuery() {
init { init {
require(rawText.isNotEmpty()) require(rawText.isNotEmpty())
} }
@@ -48,7 +48,7 @@ internal sealed class SearchQuery {
/** /**
* Searches for all matches of a regular expression. * Searches for all matches of a regular expression.
*/ */
class RegularExpression(private var pattern: String) : SearchQuery() { class RegularExpression(private val pattern: String) : SearchQuery() {
override val rawText = "" override val rawText = ""
override fun getHighlightLength(text: CharSequence, offset: Int): Int { override fun getHighlightLength(text: CharSequence, offset: Int): Int {

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

@@ -0,0 +1,13 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
data class Tag(val editor: Editor, val offset: Int) {
override fun equals(other: Any?): Boolean {
return other is Tag && other.offset == offset && other.editor === editor
}
override fun hashCode(): Int {
return (offset * 31) + editor.hashCode()
}
}

View File

@@ -1,5 +1,6 @@
package org.acejump.search 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
@@ -11,39 +12,41 @@ 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.Tag import org.acejump.view.TagMarker
import java.util.AbstractMap.SimpleImmutableEntry import java.util.AbstractMap.SimpleImmutableEntry
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.math.min
/** /**
* Assigns tags to search occurrences, updates them when the search query changes, and requests a jump if the search query matches a tag. * Assigns tags to search occurrences, updates them when the search query changes, and requests a jump if the search query matches a tag.
* The ordering of [editors] may be used to prioritize tagging editors earlier in the list in case of conflicts.
*/ */
internal class Tagger(private val editor: Editor) { class Tagger(private val editors: List<Editor>) {
private var tagMap = HashBiMap.create<String, Int>() private var tagMap = HashBiMap.create<String, Tag>()
val hasTags val hasTags
get() = tagMap.isNotEmpty() get() = tagMap.isNotEmpty()
@ExternalUsage @ExternalUsage
val tags internal val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value } get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value.offset }
/** /**
* Removes all markers, allowing them to be regenerated from scratch. * Removes all markers, allowing them to be regenerated from scratch.
*/ */
fun unmark() { internal fun unmark() {
tagMap = HashBiMap.create() tagMap = HashBiMap.create()
} }
/** /**
* Assigns tags to as many results as possible, keeping previously assigned tags. Returns a [TaggingResult.Jump] if the current search * Assigns tags to as many results as possible, keeping previously assigned tags. Returns a [TaggingResult.Accept] if the current search
* query matches any existing tag and we should jump to it and end the session, or [TaggingResult.Mark] to continue the session with * query matches any existing tag and we should jump to it and end the session, or [TaggingResult.Mark] to continue the session with
* updated tag markers. * updated tag markers.
* *
* Note that the [results] collection will be mutated. * Note that the [results] collection will be mutated.
*/ */
fun markOrJump(query: SearchQuery, results: IntList): TaggingResult { internal fun update(query: SearchQuery, results: Map<Editor, 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()
@@ -52,12 +55,14 @@ internal class Tagger(private val editor: Editor) {
if (!isRegex) { if (!isRegex) {
for (entry in tagMap.entries) { for (entry in tagMap.entries) {
if (entry solves queryText) { if (entry solves queryText) {
return TaggingResult.Jump(entry.value) return TaggingResult.Accept(entry.value)
} }
} }
if (queryText.length == 1) { if (queryText.length == 1) {
removeResultsWithOverlappingTags(results) for ((editor, offsets) in results) {
removeResultsWithOverlappingTags(editor, offsets)
}
} }
} }
@@ -65,16 +70,22 @@ internal class Tagger(private val editor: Editor) {
tagMap = assignTagsAndMerge(results, availableTags, query, queryText) tagMap = assignTagsAndMerge(results, availableTags, query, queryText)
} }
return TaggingResult.Mark(createTagMarkers(results, query.rawText.ifEmpty { null })) val resultTags = results.flatMap { (editor, offsets) -> offsets.map { Tag(editor, it) } }
return TaggingResult.Mark(createTagMarkers(resultTags, query.rawText.ifEmpty { null }))
} }
/** /**
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags. * Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
*/ */
private fun assignTagsAndMerge(results: IntList, availableTags: List<String>, query: SearchQuery, queryText: String): HashBiMap<String, Int> { private fun assignTagsAndMerge(
val cache = EditorOffsetCache.new() results: Map<Editor, IntList>, availableTags: List<String>, query: SearchQuery, queryText: String,
): HashBiMap<String, Tag> {
val caches = results.keys.associateWith { EditorOffsetCache.new() }
results.sort { a, b -> for ((editor, offsets) in results) {
val cache = caches.getValue(editor)
offsets.sort { a, b ->
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache) val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache) val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
@@ -84,52 +95,61 @@ internal class Tagger(private val editor: Editor) {
else -> 0 else -> 0
} }
} }
}
val allAssignedTags = mutableMapOf<String, Int>() val allAssignedTags = mutableMapOf<String, Tag>()
val oldCompatibleTags = tagMap.filter { isTagCompatibleWithQuery(it.key, it.value, queryText) || it.value in results } val oldCompatibleTags = tagMap.filter { (mark, tag) ->
val vacantResults: IntList isTagCompatibleWithQuery(mark, tag, queryText) || results[tag.editor]?.contains(tag.offset) == true
}
val vacantResults: Map<Editor, IntList>
if (oldCompatibleTags.isEmpty()) { if (oldCompatibleTags.isEmpty()) {
vacantResults = results vacantResults = results
} }
else { else {
vacantResults = IntArrayList() val vacant = mutableMapOf<Editor, IntList>()
for ((editor, offsets) in results) {
val list = IntArrayList()
val iter = offsets.iterator()
val iter = results.iterator()
while (iter.hasNext()) { while (iter.hasNext()) {
val offset = iter.nextInt() val tag = Tag(editor, iter.nextInt())
if (tag !in oldCompatibleTags.values) {
list.add(tag.offset)
}
}
if (offset !in oldCompatibleTags.values) { vacant[editor] = list
vacantResults.add(offset)
}
} }
vacantResults = vacant
} }
allAssignedTags.putAll(oldCompatibleTags) allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editor, query, vacantResults, results, availableTags, cache)) allAssignedTags.putAll(Solver.solve(editors, query, vacantResults, results, availableTags, caches))
val assignedMarkers = allAssignedTags.keys.groupBy { it[0] }
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) -> return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
// Avoid matching query - will trigger a jump. if (canShortenTag(tag, assignedMarkers, queryText))
// 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, Int>.solves(query: String): Boolean { private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean {
return query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query) return query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
} }
private fun isTagCompatibleWithQuery(tag: String, offset: Int, query: String): Boolean { private fun isTagCompatibleWithQuery(marker: String, tag: Tag, query: String): Boolean {
return editor.immutableText.matchesAt(offset, getPlaintextPortion(query, tag), ignoreCase = true) return tag.editor.immutableText.matchesAt(tag.offset, getPlaintextPortion(query, marker), ignoreCase = true)
} }
fun isQueryCompatibleWithTagAt(query: String, offset: Int): Boolean { fun isQueryCompatibleWithTagAt(query: String, tag: Tag): Boolean {
return tagMap.inverse()[offset].let { it != null && isTagCompatibleWithQuery(it, offset, query) } return tagMap.inverse()[tag].let { it != null && isTagCompatibleWithQuery(it, tag, query) }
} }
fun canQueryMatchAnyTag(query: String): Boolean { fun canQueryMatchAnyTag(query: String): Boolean {
@@ -139,8 +159,8 @@ internal class Tagger(private val editor: Editor) {
} }
} }
private fun removeResultsWithOverlappingTags(results: IntList) { private fun removeResultsWithOverlappingTags(editor: Editor, offsets: IntList) {
val iter = results.iterator() val iter = offsets.iterator()
val chars = editor.immutableText val chars = editor.immutableText
while (iter.hasNext()) { while (iter.hasNext()) {
@@ -150,9 +170,18 @@ internal class Tagger(private val editor: Editor) {
} }
} }
private fun createTagMarkers(results: IntList, literalQueryText: String?): List<Tag> { private fun createTagMarkers(tags: Collection<Tag>, literalQueryText: String?): MutableMap<Editor, Collection<TagMarker>> {
val tagMapInv = tagMap.inverse() val tagMapInv = tagMap.inverse()
return results.mapNotNull { index -> tagMapInv[index]?.let { tag -> Tag.create(editor, tag, index, literalQueryText) } } val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tags.size, 50))
for (tag in tags) {
val mark = tagMapInv[tag] ?: continue
val editor = tag.editor
val marker = TagMarker.create(editor, mark, tag.offset, literalQueryText)
markers.put(editor, marker)
}
return markers.asMap()
} }
private companion object { private companion object {
@@ -173,26 +202,28 @@ internal class Tagger(private val editor: 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, tag: String) = when { private fun getPlaintextPortion(query: String, marker: String) = when {
query.endsWith(tag, true) -> query.dropLast(tag.length) query.endsWith(marker, true) -> query.dropLast(marker.length)
query.endsWith(tag.first(), true) -> query.dropLast(1) query.endsWith(marker.first(), true) -> query.dropLast(1)
else -> query else -> query
} }
private fun getTagPortion(query: String, tag: String) = when { private fun getTagPortion(query: String, marker: String) = when {
query.endsWith(tag, true) -> query.takeLast(tag.length) query.endsWith(marker, true) -> query.takeLast(marker.length)
query.endsWith(tag.first(), true) -> query.takeLast(1) query.endsWith(marker.first(), true) -> query.takeLast(1)
else -> "" else -> ""
} }
private fun canShortenTag(tag: String, tagMap: Map<String, Int>): Boolean { private fun canShortenTag(marker: String, markers: Map<Char, List<String>>, queryText: String): Boolean {
for (other in tagMap.keys) { // Avoid matching query - will trigger a jump.
if (tag != other && tag[0] == other[0]) { // TODO: lift this constraint.
val queryEndsWith = queryText.endsWith(marker[0]) || queryText.endsWith(marker)
if (queryEndsWith) {
return false return false
} }
}
return true val startingWithSameLetter = markers[marker[0]]
return startingWithSameLetter == null || startingWithSameLetter.singleOrNull() == marker
} }
} }
} }

View File

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

View File

@@ -26,6 +26,22 @@ internal data class EditorSettings(private val isBlockCursor: Boolean, private v
} }
} }
fun startEditing(editor: Editor) {
editor.document.setReadOnly(isReadOnly)
}
fun stopEditing(editor: Editor) {
editor.document.setReadOnly(true)
}
fun onTagAccepted(editor: Editor) = editor.let {
it.settings.isBlockCursor = isBlockCursor
}
fun onTagUnaccepted(editor: Editor) = editor.let {
it.settings.isBlockCursor = true
}
fun restore(editor: Editor) { fun restore(editor: Editor) {
val settings = editor.settings val settings = editor.settings
val document = editor.document val document = editor.document

View File

@@ -1,179 +1,160 @@
package org.acejump.session package org.acejump.session
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.actionSystem.TypedActionHandler import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import org.acejump.ExternalUsage import org.acejump.ExternalUsage
import org.acejump.action.TagJumper
import org.acejump.action.TagVisitor
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries import org.acejump.boundaries.StandardBoundaries
import org.acejump.clone
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.input.EditorKeyListener import org.acejump.input.EditorKeyListener
import org.acejump.input.JumpMode
import org.acejump.input.JumpModeTracker
import org.acejump.input.KeyLayoutCache import org.acejump.input.KeyLayoutCache
import org.acejump.search.Pattern import org.acejump.modes.JumpMode
import org.acejump.search.SearchProcessor import org.acejump.modes.SessionMode
import org.acejump.search.Tagger import org.acejump.search.*
import org.acejump.search.TaggingResult
import org.acejump.view.TagCanvas import org.acejump.view.TagCanvas
import org.acejump.view.TextHighlighter 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 editor: Editor) { class Session(private val mainEditor: Editor, private val jumpEditors: List<Editor>) {
private companion object { private val editorSettings = EditorSettings.setup(mainEditor)
private val defaultBoundaries private lateinit var mode: SessionMode
get() = if (AceConfig.searchWholeFile) StandardBoundaries.WHOLE_FILE else StandardBoundaries.VISIBLE_ON_SCREEN
}
private val originalSettings = EditorSettings.setup(editor) private var state: SessionStateImpl? = null
private var tagger = Tagger(jumpEditors)
private val jumpModeTracker = JumpModeTracker() private var acceptedTag: Tag? = null
private var jumpMode = JumpMode.DISABLED
set(value) { set(value) {
field = value field = value
if (value === JumpMode.DISABLED) { if (value != null) {
end() tagCanvases.values.forEach(TagCanvas::removeMarkers)
} editorSettings.onTagAccepted(mainEditor)
else {
searchProcessor?.let { textHighlighter.render(it.results, it.query, jumpMode) }
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, value.caretColor)
editor.contentComponent.repaint()
} }
} }
private var searchProcessor: SearchProcessor? = null private val textHighlighter = TextHighlighter()
private var tagger = Tagger(editor) private val tagCanvases = jumpEditors.associateWith(::TagCanvas)
private val tagJumper
get() = TagJumper(editor, jumpMode, searchProcessor)
private val tagVisitor
get() = searchProcessor?.let { TagVisitor(editor, it, tagJumper) }
private val textHighlighter = TextHighlighter(editor)
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(editor, object : TypedActionHandler { EditorKeyListener.attach(mainEditor, object : TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) { override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
var processor = searchProcessor val state = state ?: return
val hadTags = tagger.hasTags val hadTags = tagger.hasTags
if (processor == null) { editorSettings.startEditing(editor)
processor = SearchProcessor.fromChar(editor, charTyped, defaultBoundaries).also { searchProcessor = it } val result = mode.type(state, charTyped, acceptedTag)
} editorSettings.stopEditing(editor)
else if (!processor.type(charTyped, tagger)) {
return when (result) {
TypeResult.Nothing -> return;
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
is TypeResult.ChangeMode -> setMode(result.mode)
TypeResult.RestartSearch -> restart().also {
this@Session.state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
} }
updateSearch(processor, markImmediately = hadTags, shiftMode = charTyped.isUpperCase()) TypeResult.EndSession -> end()
}
} }
}) })
} }
/** /**
* Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag. * Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag.
* If all tags are outside view, scrolls to the closest one.
*/ */
private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean, shiftMode: Boolean = false) { private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean) {
val query = processor.query val query = processor.query
val results = processor.results val results = processor.results
textHighlighter.render(results, query, jumpMode)
if (!markImmediately && query.rawText.let { it.length < AceConfig.minQueryLength && it.all(Char::isLetterOrDigit) }) { if (!markImmediately && query.rawText.let { it.length < AceConfig.minQueryLength && it.all(Char::isLetterOrDigit) }) {
textHighlighter.renderOccurrences(results, query)
return return
} }
when (val result = tagger.markOrJump(query, results.clone())) { when (val result = tagger.update(query, results.clone())) {
is TaggingResult.Jump -> { is TaggingResult.Accept -> {
tagJumper.jump(result.offset, shiftMode) acceptedTag = result.tag
tagCanvas.removeMarkers() textHighlighter.renderFinal(result.tag, processor.query)
if (state?.let { mode.accept(it, result.tag) } == true) {
end() end()
return
}
} }
is TaggingResult.Mark -> { is TaggingResult.Mark -> {
val tags = result.tags for ((editor, canvas) in tagCanvases) {
tagCanvas.setMarkers(tags) canvas.setMarkers(result.markers[editor].orEmpty())
}
val cache = EditorOffsetCache.new() textHighlighter.renderOccurrences(results, query)
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN }
}
}
if (tags.none { boundaries.isOffsetInside(editor, it.offsetL, cache) || boundaries.isOffsetInside(editor, it.offsetR, cache) }) { private fun setMode(mode: SessionMode) {
tagVisitor?.scrollToClosest() this.mode = mode
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
} }
fun startJumpMode() {
startJumpMode(::JumpMode)
} }
fun startJumpMode(mode: () -> JumpMode) {
if (this::mode.isInitialized && mode is JumpMode) {
end()
return
} }
if (this::mode.isInitialized) {
restart()
}
setMode(mode())
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
} }
/** /**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights. * Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/ */
fun startRegexSearch(pattern: String, boundaries: Boundaries) { fun startRegexSearch(pattern: Pattern) {
tagger = Tagger(editor) if (!this::mode.isInitialized) {
tagCanvas.setMarkers(emptyList()) setMode(JumpMode())
}
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary)
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary, processor)
val processor = SearchProcessor.fromRegex(editor, pattern, boundaries.intersection(defaultBoundaries)).also { searchProcessor = it }
updateSearch(processor, markImmediately = true) updateSearch(processor, markImmediately = true)
} }
/** fun tagImmediately() {
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights. val state = state ?: return
*/ val processor = state.currentProcessor
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) {
startRegexSearch(pattern.regex, boundaries)
}
/** if (processor != null) {
* See [JumpModeTracker.cycle]. updateSearch(processor, markImmediately = true)
*/
fun cycleNextJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = true)
}
/**
* See [JumpModeTracker.cycle].
*/
fun cyclePreviousJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = false)
}
/**
* See [JumpModeTracker.toggle]
*/
fun toggleJumpMode(newMode: JumpMode) {
jumpMode = jumpModeTracker.toggle(newMode)
}
/**
* See [TagVisitor.visitPrevious]. If there are no tags, nothing happens.
*/
fun visitPreviousTag() {
if (tagVisitor?.visitPrevious() == true) {
end()
}
}
/**
* See [TagVisitor.visitNext]. If there are no tags, nothing happens.
*/
fun visitNextTag() {
if (tagVisitor?.visitNext() == true) {
end()
} }
} }
@@ -181,32 +162,41 @@ class Session(private val editor: Editor) {
* Ends this session. * Ends this session.
*/ */
fun end() { fun end() {
SessionManager.end(editor) SessionManager.end(mainEditor)
} }
/** /**
* Clears any currently active search, tags, and highlights. Does not reset [JumpMode]. * Clears any currently active search, tags, and highlights.
*/ */
fun restart() { fun restart() {
tagger = Tagger(editor) state = null
searchProcessor = null tagger = Tagger(jumpEditors)
tagCanvas.removeMarkers() acceptedTag = null
tagCanvases.values.forEach(TagCanvas::removeMarkers)
textHighlighter.reset() textHighlighter.reset()
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.onTagUnaccepted(mainEditor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
jumpEditors.forEach { it.contentComponent.repaint() }
} }
/** /**
* Should only be used from [SessionManager] to dispose a successfully ended session. * Should only be used from [SessionManager] to dispose a successfully ended session.
*/ */
internal fun dispose() { internal fun dispose() {
tagger = Tagger(editor) tagger = Tagger(jumpEditors)
EditorKeyListener.detach(editor) tagCanvases.values.forEach(TagCanvas::unbind)
tagCanvas.unbind()
textHighlighter.reset() textHighlighter.reset()
EditorKeyListener.detach(mainEditor)
if (!editor.isDisposed) { if (!mainEditor.isDisposed) {
originalSettings.restore(editor) HintManagerImpl.getInstanceImpl().hideAllHints()
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, JumpMode.DISABLED.caretColor) editorSettings.restore(mainEditor)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
val focusedEditor = acceptedTag?.editor ?: mainEditor
focusedEditor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
} }
} }
} }

View File

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

View File

@@ -0,0 +1,9 @@
package org.acejump.session
import org.acejump.action.AceTagAction
import org.acejump.search.Tag
interface SessionState {
fun type(char: Char): TypeResult
fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean)
}

View File

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

View File

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

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: List<Tag>? = null private var markers: Collection<TagMarker>? = 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: List<Tag>) { fun setMarkers(markers: Collection<TagMarker>) {
this.markers = markers this.markers = markers
repaint() repaint()
} }
@@ -64,14 +64,15 @@ 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,16 +2,22 @@ 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 [Tag]s. * Stores font metrics for aligning and rendering [TagMarker]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

@@ -1,11 +1,10 @@
package org.acejump.view package org.acejump.view
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil import com.intellij.openapi.util.SystemInfo
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
@@ -17,7 +16,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 Tag( internal class TagMarker(
private val tag: String, private val tag: String,
val offsetL: Int, val offsetL: Int,
val offsetR: Int, val offsetR: Int,
@@ -29,11 +28,18 @@ internal class Tag(
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?): Tag { fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker {
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()
@@ -43,7 +49,7 @@ internal class Tag(
else else
tag.toUpperCase() tag.toUpperCase()
return Tag(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight) return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
} }
/** /**
@@ -51,7 +57,9 @@ internal class Tag(
*/ */
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)
} }
/** /**
@@ -63,12 +71,12 @@ internal class Tag(
g.font = font.tagFont g.font = font.tagFont
if (!ColorUtil.isDark(AceConfig.tagForegroundColor)) { if (!font.isForegroundDark) {
g.color = Color(0F, 0F, 0F, 0.35F) g.color = SHADOW_COLOR
g.drawString(text, x + 1, y + 1) g.drawString(text, x + 1, y + 1)
} }
g.color = AceConfig.tagForegroundColor g.color = font.foregroundColor
g.drawString(text, x, y) g.drawString(text, x, y)
} }
} }
@@ -88,7 +96,7 @@ internal class Tag(
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, AceConfig.tagBackgroundColor) drawHighlight(g, rect, font.backgroundColor)
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

@@ -6,56 +6,59 @@ import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.util.ui.JBUI import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.boundaries.EditorOffsetCache 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.input.JumpMode
import org.acejump.isWordPart
import org.acejump.search.SearchQuery import org.acejump.search.SearchQuery
import org.acejump.wordEnd import org.acejump.search.Tag
import org.acejump.wordStart import java.awt.Color
import java.awt.Graphics import java.awt.Graphics
import kotlin.math.max
/** /**
* Renders highlights for search occurrences. * Renders highlights for search occurrences.
*/ */
internal class TextHighlighter(private val editor: Editor) { internal class TextHighlighter {
private companion object { private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>()
private const val LAYER = HighlighterLayer.LAST + 1
}
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 render(offsets: IntList, query: SearchQuery, jumpMode: JumpMode) { fun renderOccurrences(results: Map<Editor, IntList>, query: SearchQuery) {
val markup = editor.markupModel render(results, when (query) {
val chars = editor.immutableText is SearchQuery.RegularExpression -> RegexRenderer
val renderer = when {
query is SearchQuery.RegularExpression -> RegexRenderer
jumpMode === JumpMode.TARGET -> SearchedWordWithOutlineRenderer
else -> SearchedWordRenderer else -> SearchedWordRenderer
}, query::getHighlightLength)
} }
val modifications = (previousHighlights?.size ?: 0) + offsets.size /**
val enableBulkEditing = modifications > 1000 * Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
*/
fun renderFinal(tag: Tag, query: SearchQuery) {
render(mutableMapOf(tag.editor to IntArrayList(intArrayOf(tag.offset))), AcceptedTagRenderer, query::getHighlightLength)
}
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
for ((editor, offsets) in results) {
val highlights = previousHighlights[editor]
val markup = editor.markupModel
val document = editor.document val document = editor.document
val chars = editor.immutableText
val modifications = (highlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000
try { try {
if (enableBulkEditing) { if (enableBulkEditing) {
document.isInBulkUpdate = true document.isInBulkUpdate = true
} }
previousHighlights?.forEach(markup::removeHighlighter) highlights?.forEach(markup::removeHighlighter)
previousHighlights = Array(offsets.size) { index -> previousHighlights[editor] = Array(offsets.size) { index ->
val start = offsets.getInt(index) val start = offsets.getInt(index)
val end = start + query.getHighlightLength(chars, start) val end = start + getHighlightLength(chars, start)
markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply { markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
customRenderer = renderer customRenderer = renderer
@@ -68,9 +71,16 @@ internal class TextHighlighter(private val editor: Editor) {
} }
} }
for (editor in previousHighlights.keys.toList()) {
if (!results.containsKey(editor)) {
previousHighlights.remove(editor)?.forEach(editor.markupModel::removeHighlighter)
}
}
}
fun reset() { fun reset() {
editor.markupModel.removeAllHighlighters() previousHighlights.keys.forEach { it.markupModel.removeAllHighlighters() }
previousHighlights = null previousHighlights.clear()
} }
/** /**
@@ -78,43 +88,7 @@ internal class TextHighlighter(private val editor: Editor) {
*/ */
private object SearchedWordRenderer : CustomHighlighterRenderer { private object SearchedWordRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) { override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset) drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.textHighlightColor)
}
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = AceConfig.textHighlightColor
g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
}
}
/**
* Renders a filled highlight in the background of a searched text occurrence, as well as an outline indicating the range of characters
* that will be selected by [JumpMode.TARGET].
*/
private object SearchedWordWithOutlineRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
SearchedWordRenderer.paint(editor, highlighter, g)
val chars = editor.immutableText
val startOffset = highlighter.startOffset
if (chars.getOrNull(startOffset)?.isWordPart == true) {
drawOutline(g, editor, chars.wordStart(startOffset), chars.wordEnd(startOffset) + 1)
}
}
private fun drawOutline(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = AceConfig.targetModeColor
g.drawRect(max(0, start.x - JBUI.scale(1)), start.y, end.x - start.x + JBUI.scale(2), editor.lineHeight)
} }
} }
@@ -123,16 +97,40 @@ internal class TextHighlighter(private val editor: Editor) {
*/ */
private object RegexRenderer : CustomHighlighterRenderer { private object RegexRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) { override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawSingle(g, editor, highlighter.startOffset) drawSingle(g, editor, highlighter.startOffset, AceConfig.textHighlightColor)
}
} }
private fun drawSingle(g: Graphics, editor: Editor, offset: Int) { /**
* Renders a filled highlight in the background of the accepted tag position and search query.
*/
private object AcceptedTagRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.acceptedTagColor)
}
}
private companion object {
private const val LAYER = HighlighterLayer.LAST + 1
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int, color: Color) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = color
g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
}
private fun drawSingle(g: Graphics, editor: Editor, offset: Int, color: Color) {
val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset) val pos = EditorOffsetCache.Uncached.offsetToXY(editor, offset)
val char = editor.immutableText.getOrNull(offset)?.takeUnless { it == '\n' || it == '\t' } ?: ' ' val char = editor.immutableText.getOrNull(offset)?.takeUnless { it == '\n' || it == '\t' } ?: ' '
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char) val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = AceConfig.textHighlightColor g.color = color
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1) g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor g.color = AceConfig.tagBackgroundColor

View File

@@ -0,0 +1,59 @@
package org.acejump.vim
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.extension.VimExtension
import com.maddyhome.idea.vim.extension.VimExtensionFacade
import com.maddyhome.idea.vim.key.MappingOwner
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.Pattern
class AceVimExtension : VimExtension {
private companion object {
const val NAME = "AceJump"
const val PLUG_PREFIX = "<Plug>(acejump)"
private val OWNER = MappingOwner.Plugin.get(NAME)
private fun register(keys: String, mode: AceVimMode) {
val keysPrefixed = injector.parser.parseKeys("${PLUG_PREFIX}$keys")
val keysCommand = injector.parser.parseKeys(command(keys))
VimExtensionFacade.putExtensionHandlerMapping(MappingMode.NVO, keysCommand, OWNER, AceVimHandler(mode), false)
VimExtensionFacade.putKeyMapping(MappingMode.NVO, keysPrefixed, OWNER, keysCommand, true)
}
private fun command(name: String): String {
return "<Plug>(acejump-$name)"
}
}
override fun getName(): String {
return NAME
}
override fun init() {
register("<Space>", AceVimMode.JumpAllEditors)
register("s", AceVimMode.JumpAllEditors)
register("f", AceVimMode.Jump(StandardBoundaries.AFTER_CARET))
register("F", AceVimMode.Jump(StandardBoundaries.BEFORE_CARET))
register("t", AceVimMode.JumpTillForward(StandardBoundaries.AFTER_CARET))
register("T", AceVimMode.JumpTillBackward(StandardBoundaries.BEFORE_CARET))
register("w", AceVimMode.JumpToPattern(Pattern.VIM_LWORD, StandardBoundaries.AFTER_CARET))
register("W", AceVimMode.JumpToPattern(Pattern.VIM_UWORD, StandardBoundaries.AFTER_CARET))
register("b", AceVimMode.JumpToPattern(Pattern.VIM_LWORD, StandardBoundaries.BEFORE_CARET))
register("B", AceVimMode.JumpToPattern(Pattern.VIM_UWORD, StandardBoundaries.BEFORE_CARET))
register("e", AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, StandardBoundaries.AFTER_CARET))
register("E", AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, StandardBoundaries.AFTER_CARET))
register("ge", AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, StandardBoundaries.BEFORE_CARET))
register("gE", AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, StandardBoundaries.BEFORE_CARET))
register("j", AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, StandardBoundaries.AFTER_CARET))
register("k", AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, StandardBoundaries.BEFORE_CARET))
register("l", AceVimMode.Jump(StandardBoundaries.AFTER_CARET.intersection(StandardBoundaries.CARET_LINE)))
register("h", AceVimMode.Jump(StandardBoundaries.BEFORE_CARET.intersection(StandardBoundaries.CARET_LINE)))
}
}

View File

@@ -0,0 +1,92 @@
package org.acejump.vim
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.WriteAction
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.action.change.change.ChangeVisualAction
import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction
import com.maddyhome.idea.vim.action.copy.YankVisualAction
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.command.VimStateMachine
import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.group.visual.vimSetSelection
import com.maddyhome.idea.vim.helper.inVisualMode
import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.ij
import org.acejump.action.AceTagAction
import org.acejump.modes.JumpMode
import org.acejump.search.Tag
import org.acejump.session.SessionManager
import org.acejump.session.SessionState
class AceVimHandler(private val mode: AceVimMode) : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext) {
val ij = editor.ij
val caret = ij.caretModel.currentCaret
val initialOffset = caret.offset
val selectionStart = if (ij.inVisualMode) caret.vimSelectionStart else null
val session = SessionManager.start(ij, mode.getJumpEditors(ij))
session.defaultBoundary = mode.boundaries
session.startJumpMode {
object : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
if (selectionStart != null) {
caret.vimSetSelection(selectionStart, caret.offset, moveCaretToSelectionEnd = true)
}
else {
val commandState = editor.vimStateMachine
if (commandState.isOperatorPending) {
val key = commandState.commandBuilder.keys.singleOrNull()?.keyChar
commandState.reset()
KeyHandler.getInstance().fullReset(editor)
VimPlugin.getVisualMotion().enterVisualMode(editor, VimStateMachine.SubMode.VISUAL_CHARACTER)
caret.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true)
val action = when (key) {
'd' -> DeleteVisualAction()
'c' -> ChangeVisualAction()
'y' -> YankVisualAction()
else -> null
}
if (action != null) {
ApplicationManager.getApplication().invokeLater {
WriteAction.run<Nothing> {
commandState.commandBuilder.pushCommandPart(action)
val cmd = commandState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(
commandState.mappingState.mappingMode == MappingMode.OP_PENDING,
cmd.rawCount, commandState.mode, commandState.subMode
)
commandState.setExecutingCommand(cmd)
injector.actionExecutor.executeVimAction(editor, action, context, operatorArguments)
// TODO does not update status
}
}
}
}
}
mode.finishSession(ij, session)
return true
}
}
}
mode.setupSession(ij, session)
}
}

View File

@@ -0,0 +1,64 @@
package org.acejump.vim
import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.openEditors
import org.acejump.search.Pattern
import org.acejump.session.Session
sealed class AceVimMode {
open val boundaries: Boundaries
get() = StandardBoundaries.VISIBLE_ON_SCREEN
open fun getJumpEditors(mainEditor: Editor): List<Editor> {
return listOf(mainEditor)
}
open fun setupSession(editor: Editor, session: Session) {}
open fun finishSession(editor: Editor, session: Session) {}
class Jump(override val boundaries: Boundaries) : AceVimMode()
object JumpAllEditors : AceVimMode() {
override fun getJumpEditors(mainEditor: Editor): List<Editor> {
val project = mainEditor.project ?: return super.getJumpEditors(mainEditor)
return project.openEditors
.sortedBy { if (it === mainEditor) 0 else 1 }
.ifEmpty { listOf(mainEditor) }
}
}
class JumpTillForward(override val boundaries: Boundaries) : AceVimMode() {
override fun finishSession(editor: Editor, session: Session) {
val document = editor.document
for (caret in editor.caretModel.allCarets) {
val offset = caret.offset
if (offset > document.getLineStartOffset(document.getLineNumber(offset))) {
caret.moveToOffset(offset - 1, false)
}
}
}
}
class JumpTillBackward(override val boundaries: Boundaries) : AceVimMode() {
override fun finishSession(editor: Editor, session: Session) {
val document = editor.document
for (caret in editor.caretModel.allCarets) {
val offset = caret.offset
if (offset < document.getLineEndOffset(document.getLineNumber(offset))) {
caret.moveToOffset(offset + 1, false)
}
}
}
}
class JumpToPattern(private val pattern: Pattern, override val boundaries: Boundaries) : AceVimMode() {
override fun setupSession(editor: Editor, session: Session) {
session.startRegexSearch(pattern)
}
}
}

View File

@@ -0,0 +1,5 @@
<idea-plugin>
<extensions defaultExtensionNs="IdeaVIM">
<vimExtension implementation="org.acejump.vim.AceVimExtension" name="acejump" />
</extensions>
</idea-plugin>

View File

@@ -1,6 +1,6 @@
<idea-plugin url="https://github.com/acejump/AceJump"> <idea-plugin>
<name>AceJump</name> <name>AceJump</name>
<id>AceJump</id> <id>AceJump-chylex</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.
@@ -13,6 +13,8 @@
<category>Navigation</category> <category>Navigation</category>
<vendor url="https://github.com/acejump/AceJump">AceJump</vendor> <vendor url="https://github.com/acejump/AceJump">AceJump</vendor>
<depends optional="true" config-file="acejump-ideavim.xml">IdeaVIM</depends>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="org.acejump.config.AceConfig"/> <applicationService serviceImplementation="org.acejump.config.AceConfig"/>
<applicationConfigurable groupId="tools" displayName="AceJump" <applicationConfigurable groupId="tools" displayName="AceJump"
@@ -23,73 +25,38 @@
implementationClass="org.acejump.action.AceEditorAction$Reset"/> implementationClass="org.acejump.action.AceEditorAction$Reset"/>
<editorActionHandler action="EditorBackSpace" order="first" <editorActionHandler action="EditorBackSpace" order="first"
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/> implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
<editorActionHandler action="EditorStartNewLine" order="first"
implementationClass="org.acejump.action.AceEditorAction$SelectBackward"/>
<editorActionHandler action="EditorEnter" order="first" <editorActionHandler action="EditorEnter" order="first"
implementationClass="org.acejump.action.AceEditorAction$SelectForward"/> implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/>
<editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
<editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
<editorActionHandler action="EditorLineEnd" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
</extensions> </extensions>
<actions> <actions>
<action id="AceAction" <action id="AceAction"
class="org.acejump.action.AceAction$ActivateOrCycleMode" class="org.acejump.action.AceKeyboardAction$ActivateAceJump"
text="Activate / Cycle AceJump Mode"> text="Activate AceJump">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action> </action>
<action id="AceReverseAction" <action id="AceVimAction_GoToDeclaration"
class="org.acejump.action.AceAction$ActivateOrReverseCycleMode" class="org.acejump.action.AceVimAction$GoToDeclaration"
text="Activate / Reverse Cycle AceJump Mode"/> text="AceJump Vim - GoToDeclaration"/>
<action id="AceWordStartAction" <action id="AceVimAction_GoToTypeDeclaration"
class="org.acejump.action.AceAction$ToggleJumpMode" class="org.acejump.action.AceVimAction$GoToTypeDeclaration"
text="Start AceJump in Jump Mode"/> text="AceJump Vim - GoToTypeDeclaration"/>
<action id="AceWordEndAction" <action id="AceVimAction_ShowIntentions"
class="org.acejump.action.AceAction$ToggleJumpEndMode" class="org.acejump.action.AceVimAction$ShowIntentions"
text="Start AceJump in Jump End Mode"/> text="AceJump Vim - ShowIntentions"/>
<action id="AceTargetAction" <action id="AceVimAction_ShowUsages"
class="org.acejump.action.AceAction$ToggleTargetMode" class="org.acejump.action.AceVimAction$ShowUsages"
text="Start AceJump in Target Mode"> text="AceJump Vim - ShowUsages"/>
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl alt SEMICOLON"/> <action id="AceVimAction_FindUsages"
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl alt SEMICOLON"/> class="org.acejump.action.AceVimAction$FindUsages"
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt SEMICOLON"/> text="AceJump Vim - FindUsages"/>
</action> <action id="AceVimAction_Refactor"
<action id="AceDeclarationAction" class="org.acejump.action.AceVimAction$Refactor"
class="org.acejump.action.AceAction$ToggleDeclarationMode" text="AceJump Vim - Refactor"/>
text="Start AceJump in Declaration Mode"/> <action id="AceVimAction_Rename"
<action id="AceLineAction" class="org.acejump.action.AceVimAction$Rename"
class="org.acejump.action.AceAction$StartAllLineMarksMode" text="AceJump Vim - Rename"/>
text="Start AceJump in All Line Marks Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
</action>
<action id="AceLineStartsAction"
class="org.acejump.action.AceAction$StartAllLineStartsMode"
text="Start AceJump in All Line Starts Mode"/>
<action id="AceLineEndsAction"
class="org.acejump.action.AceAction$StartAllLineEndsMode"
text="Start AceJump in All Line Ends Mode"/>
<action id="AceLineIndentsAction"
class="org.acejump.action.AceAction$StartAllLineIndentsMode"
text="Start AceJump in All Line Indents Mode"/>
<action id="AceWordAction"
class="org.acejump.action.AceAction$StartAllWordsMode"
text="Start AceJump in All Words Mode"/>
<action id="AceWordForwardAction"
class="org.acejump.action.AceAction$StartAllWordsForwardMode"
text="Start AceJump in All Words After Caret Mode"/>
<action id="AceWordBackwardsAction"
class="org.acejump.action.AceAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/>
</actions> </actions>
</idea-plugin> </idea-plugin>

View File

@@ -1,6 +1,4 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
import org.acejump.action.AceAction
import org.acejump.test.util.BaseTest import org.acejump.test.util.BaseTest
/** /**
@@ -28,30 +26,6 @@ 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")
@@ -59,57 +33,4 @@ class AceTest : BaseTest() {
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.toUpperCase())
myFixture.checkResult("<selection>testing 123<caret></selection>4")
}
fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two")
takeAction(AceAction.StartAllWordsBackwardsMode)
assertEquals(2, session.tags.size)
}
fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four")
takeAction(AceAction.StartAllWordsForwardMode)
assertEquals(4, session.tags.size)
}
fun `test word mode`() {
makeEditor("test word action")
takeAction(AceAction.StartAllWordsMode)
assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key)
myFixture.checkResult("test <caret>word action")
}
fun `test target mode`() {
"<caret>test target action".search("target")
takeAction(AceAction.ToggleTargetMode)
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("test <selection>target<caret></selection> action")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines\n")
takeAction(AceAction.StartAllLineMarksMode)
assertEquals(8, session.tags.size) // last empty line does not count
}
} }

View File

@@ -1,4 +1,4 @@
import org.acejump.action.AceAction import org.acejump.action.AceKeyboardAction
import org.acejump.test.util.BaseTest import org.acejump.test.util.BaseTest
import org.junit.Ignore import org.junit.Ignore
import java.io.File import java.io.File
@@ -15,7 +15,7 @@ class LatencyTest : BaseTest() {
for (query in chars) { for (query in chars) {
makeEditor(editorText) makeEditor(editorText)
myFixture.testAction(AceAction.ActivateOrCycleMode) myFixture.testAction(AceKeyboardAction.ActivateAceJump)
time += measureTimeMillis { typeAndWaitForResults("$query") } time += measureTimeMillis { typeAndWaitForResults("$query") }
// TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" } // TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" }
resetEditor() resetEditor()

View File

@@ -2,11 +2,12 @@ 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
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import org.acejump.action.AceAction import org.acejump.action.AceKeyboardAction
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
abstract class BaseTest : BasePlatformTestCase() { abstract class BaseTest : BasePlatformTestCase() {
@@ -38,7 +39,9 @@ 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 {
return myFixture.configureByText(PlainTextFileType.INSTANCE, contents) val file = myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
(myFixture.editor as EditorImpl).scrollPane.viewport.setSize(1000, 100)
return file
} }
fun resetEditor() { fun resetEditor() {
@@ -52,10 +55,10 @@ abstract class BaseTest : BasePlatformTestCase() {
UIUtil.dispatchAllInvocationEvents() UIUtil.dispatchAllInvocationEvents()
} }
fun String.executeQuery(query: String) { private fun String.executeQuery(query: String) {
myFixture.run { myFixture.run {
makeEditor(this@executeQuery) makeEditor(this@executeQuery)
testAction(AceAction.ActivateOrCycleMode) testAction(AceKeyboardAction.ActivateAceJump)
typeAndWaitForResults(query) typeAndWaitForResults(query)
} }
} }