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

9 Commits

32 changed files with 324 additions and 927 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,32 +1,26 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
idea apply true
kotlin("jvm") version "1.5.0"
id("org.jetbrains.intellij") version "0.7.2"
}
tasks {
withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
compileOnly(kotlin("stdlib-jdk8"))
idea
kotlin("jvm") version "1.5.10"
id("org.jetbrains.intellij") version "1.11.0"
}
repositories {
mavenCentral()
jcenter()
}
intellij {
version = "2021.1"
pluginName = "AceJump"
updateSinceUntilBuild = false
setPlugins("java", "IdeaVIM:0.66")
version.set("2022.3.1")
updateSinceUntilBuild.set(false)
pluginsRepositories.custom("https://intellij.chylex.com")
plugins.add("IdeaVIM:chylex-15")
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
kotlinOptions.freeCompilerArgs = listOf("-Xjvm-default=all")
}
group = "org.acejump"
version = "chylex-8"
version = "chylex-11"

1
gradle.properties Normal file
View File

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

View File

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

View File

@@ -1,7 +1,10 @@
package org.acejump
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actions.EditorActionUtil
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
@@ -12,6 +15,21 @@ annotation class ExternalUsage
val Editor.immutableText
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.
*/
@@ -67,32 +85,6 @@ inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char
return end
}
/**
* Finds index of the previous "camelHumps" hump in a word.
*/
inline fun CharSequence.humpStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var start = pos
while (start > 0 && isPartOfWord(this[start - 1]) && !EditorActionUtil.isHumpBound(this, start, true)) {
--start
}
return start
}
/**
* Finds index of the next "camelHumps" hump in a word.
*/
inline fun CharSequence.humpEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos
while (end < length - 1 && isPartOfWord(this[end + 1]) && !EditorActionUtil.isHumpBound(this, end + 1, false)) {
++end
}
return end
}
/**
* Finds index of the first word character following a sequence of non-word characters following the end of a word.
*/

View File

@@ -4,7 +4,6 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
@@ -42,16 +41,4 @@ abstract class AceEditorAction(private val originalHandler: EditorActionHandler)
class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.tagImmediately()
}
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS)
}
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS)
}
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS)
}
}

View File

@@ -2,12 +2,9 @@ package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.util.IncorrectOperationException
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.*
import org.acejump.openEditors
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
@@ -23,18 +20,11 @@ abstract class AceKeyboardAction : DumbAwareAction() {
final override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(EDITOR) ?: return
val project = e.project
if (project != null) {
try {
val openEditors = FileEditorManagerEx.getInstanceEx(project)
.splitters
.selectedEditors
.mapNotNull { (it as? TextEditor)?.editor }
val jumpEditors = project.openEditors
.sortedBy { if (it === editor) 0 else 1 }
invoke(SessionManager.start(editor, openEditors))
} catch (e: IncorrectOperationException) {
invoke(SessionManager.start(editor))
}
.ifEmpty { listOf(editor) }
invoke(SessionManager.start(editor, jumpEditors))
}
else {
invoke(SessionManager.start(editor))
@@ -59,23 +49,4 @@ abstract class AceKeyboardAction : DumbAwareAction() {
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.startJumpMode()
}
/**
* Starts or cycles main AceJump modes.
*/
object ActivateAceJumpSpecial : AceKeyboardAction() {
override fun invoke(session: Session) = session.startOrCycleSpecialModes()
}
// @formatter:off
object StartAllWordsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, VISIBLE_ON_SCREEN)
object StartAllWordsBackwardsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
object StartAllWordsForwardMode : BaseRegexSearchAction(Pattern.ALL_WORDS, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
object StartAllLineStartsMode : BaseRegexSearchAction(Pattern.LINE_STARTS, VISIBLE_ON_SCREEN)
object StartAllLineEndsMode : BaseRegexSearchAction(Pattern.LINE_ENDS, VISIBLE_ON_SCREEN)
object StartAllLineIndentsMode : BaseRegexSearchAction(Pattern.LINE_INDENTS, VISIBLE_ON_SCREEN)
object StartAllLineMarksMode : BaseRegexSearchAction(Pattern.LINE_ALL_MARKS, VISIBLE_ON_SCREEN)
// @formatter:on
}

View File

@@ -6,24 +6,19 @@ import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.codeStyle.CodeStyleManager
import org.acejump.*
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.search.SearchProcessor
import kotlin.math.max
import org.acejump.wordStart
/**
* Base class for actions available after typing a tag.
@@ -64,79 +59,6 @@ sealed class AceTagAction {
abstract fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int
}
abstract class BaseSelectAction : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
if (shiftMode) {
val caretModel = editor.caretModel
val oldCarets = caretModel.caretsAndSelections
val oldOffsetPosition = caretModel.logicalPosition
invoke(editor, searchProcessor, offset)
if (caretModel.caretsAndSelections.any { isSelectionOverlapping(oldOffsetPosition, it) }) {
oldCarets.removeAll { isSelectionOverlapping(oldOffsetPosition, it) }
}
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
else {
invoke(editor, searchProcessor, offset)
}
}
private fun isSelectionOverlapping(offset: LogicalPosition, oldCaret: CaretState): Boolean {
return oldCaret.caretPosition == offset || oldCaret.selectionStart == offset || oldCaret.selectionEnd == offset
}
protected abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int)
}
abstract class BasePerCaretWriteAction(private val selector: AceTagAction) : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
val range = editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) }
editor.caretModel.caretsAndSelections = oldCarets
invoke(editor, range, shiftMode)
}
protected abstract operator fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean)
protected fun insertAtCarets(editor: Editor, text: String) {
val document = editor.document
editor.caretModel.runForEachCaret {
if (it.hasSelection()) {
document.replaceString(it.selectionStart, it.selectionEnd, text)
fixIndents(editor, it.selectionStart, it.selectionEnd)
}
else {
document.insertString(it.offset, text)
fixIndents(editor, it.offset, it.offset + text.length)
}
}
}
private fun fixIndents(editor: Editor, startOffset: Int, endOffset: Int) {
val project = editor.project ?: return
val document = editor.document
val documentManager = PsiDocumentManager.getInstance(project)
documentManager.commitAllDocuments()
val file = documentManager.getPsiFile(document) ?: return
val text = document.charsSequence
if (startOffset > 0 && endOffset > startOffset + 1 && text[endOffset - 1] == '\n' && text[startOffset - 1] == '\n') {
CodeStyleManager.getInstance(project).adjustLineIndent(file, TextRange(startOffset, endOffset - 1))
}
else {
CodeStyleManager.getInstance(project).adjustLineIndent(file, TextRange(startOffset, endOffset))
}
}
}
private companion object {
fun countMatchingCharacters(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return editor.immutableText.countMatchingCharacters(offset, searchProcessor.query.rawText)
@@ -152,12 +74,6 @@ sealed class AceTagAction {
caretModel.moveToOffset(offset)
}
fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int, cursorOffset: Int = toOffset) = with(editor) {
selectionModel.removeSelection(true)
selectionModel.setSelection(fromOffset, toOffset)
caretModel.moveToOffset(cursorOffset)
}
fun performAction(actionName: String) {
val actionManager = ActionManager.getInstance()
val action = actionManager.getAction(actionName)
@@ -199,26 +115,6 @@ sealed class AceTagAction {
}
}
/**
* On default action, places the caret at the last character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpToSearchEnd : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset + max(0, countMatchingCharacters(editor, searchProcessor, offset) - 1)
}
}
/**
* On default action, places the caret just past the last character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpPastSearchEnd : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset + countMatchingCharacters(editor, searchProcessor, 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
@@ -235,206 +131,6 @@ sealed class AceTagAction {
}
}
/**
* On default action, 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 last character of the search query, then the caret is
* placed after the last character of the search query.
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordEnd : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordEnd(queryEndOffset) + 1
else
queryEndOffset
}
}
/**
* On default action, places the caret at the end of the line.
* On shift action, adds the new caret to existing carets.
*/
object JumpToLineEnd : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
val document = editor.document
val line = document.getLineNumber(queryEndOffset)
return document.getLineEndOffset(line)
}
}
/**
* On default action, selects all characters covered by the search query.
* On shift action, adds the new selection to existing selections.
*/
object SelectQuery : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
recordCaretPosition(editor)
val startOffset = JumpToSearchStart.getCaretOffset(editor, searchProcessor, offset)
val endOffset = JumpPastSearchEnd.getCaretOffset(editor, searchProcessor, offset)
selectRange(editor, startOffset, endOffset)
}
}
/**
* On default action, places the caret at the end of a word, and also selects the entire word. Word detection uses
* [Character.isJavaIdentifierPart] to count some special characters, such as underscores, as part of a word. If there is no word at the
* last character of the search query, then the caret is placed after the last character of the search query, and all text between the
* start and end of the search query is selected.
*
* On shift action, adds the new selection to existing selections.
*/
object SelectWord : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = JumpToWordStart.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val endOffset = JumpToWordEnd.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
}
}
}
/**
* On default action, places the caret at the end of a camel hump inside a word, and also selects the hump. If there is no word at the
* last character of the search query, then the search query is selected. See [SelectWord] and [SelectQuery] for details.
*
* On shift action, adds the new selection to existing selections.
*/
object SelectHump : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = chars.humpStart(queryEndOffset)
val endOffset = chars.humpEnd(queryEndOffset) + 1
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
}
}
}
/**
* On default action, selects the line at the tag, excluding the indent.
* On shift action, adds the new selection to existing selections.
*/
object SelectLine : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
val document = editor.document
val line = editor.caretModel.logicalPosition.line
val lineStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(document, line)
val lineEnd = document.getLineEndOffset(line)
selectRange(editor, lineStart, lineEnd)
}
}
/**
* On default action, places the caret at the last character of the search query, and then performs Extend Selection a set amount of
* times.
*
* On shift action, adds the new selection to existing selections.
*/
class SelectExtended(private val extendCount: Int) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
repeat(extendCount) {
performAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
}
}
}
/**
* On default action, selects the range between the caret and a position decided by the provided [BaseJumpAction].
* On shift action, adds the new selection to existing selections.
*/
class SelectToCaret(private val jumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val caretModel = editor.caretModel
val oldOffset = caretModel.offset
val oldSelection = editor.selectionModel.takeIf { it.hasSelection(false) }?.let { it.selectionStart..it.selectionEnd }
jumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
val newOffset = caretModel.offset
if (oldSelection == null) {
selectRange(editor, oldOffset, newOffset)
}
else {
selectRange(editor, minOf(oldOffset, newOffset, oldSelection.first), maxOf(oldOffset, newOffset, oldSelection.last), newOffset)
}
}
}
/**
* On default action, selects the range between [firstOffset] and a position decided by the provided [BaseJumpAction].
* On shift action, adds the new selection to existing selections.
*/
class SelectBetweenPoints(private val firstOffset: Int, private val secondOffsetJumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
selectRange(editor, firstOffset, editor.caretModel.offset)
}
}
/**
* Selects text based on the provided [selector] action and clones it at every existing caret, selecting the cloned text. If a caret
* has a selection, the selected text will be replaced.
*/
class CloneToCaret(selector: AceTagAction) : BasePerCaretWriteAction(selector) {
override fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean) {
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Clone").run<Throwable> {
insertAtCarets(editor, editor.document.getText(range))
}
}
}
/**
* Selects text based on the provided [selector] action and clones it to every existing caret, selecting the cloned text and deleting
* the original. If a caret has a selection, the selected text will be replaced.
*/
open class MoveToCaret(selector: AceTagAction) : BasePerCaretWriteAction(selector) {
override fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean) {
val difference = if (shiftMode) editor.caretModel.caretsAndSelections.sumBy {
val start = it.selectionStart?.let(editor::logicalPositionToOffset)
val end = it.selectionEnd?.let(editor::logicalPositionToOffset)
if (start == null || end == null || end > range.endOffset) 0 else range.length - (end - start)
} else 0
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Move").run<Throwable> {
val document = editor.document
val text = document.getText(range)
document.deleteString(range.startOffset, range.endOffset)
insertAtCarets(editor, text)
}
if (shiftMode) {
editor.selectionModel.removeSelection(true)
editor.caretModel.moveToOffset(range.startOffset + difference)
}
}
}
/**
* 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`.

View File

@@ -1,12 +1,8 @@
package org.acejump.action
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.modes.ActionMode
import org.acejump.modes.VimJumpMode
import org.acejump.search.Pattern
import org.acejump.session.Session
sealed class AceVimAction : AceKeyboardAction() {
@@ -17,85 +13,7 @@ sealed class AceVimAction : AceKeyboardAction() {
start(session)
}
protected open fun start(session: Session) {
session.startJumpMode(::VimJumpMode)
}
class JumpToChar : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
}
class JumpToCharAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
}
class JumpToCharBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
}
class LWordsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD)
}
}
class UWordsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD)
}
}
class LWordsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD)
}
}
class UWordsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD)
}
}
class LWordEndsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD_END)
}
}
class UWordEndsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD_END)
}
}
class LWordEndsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD_END)
}
}
class UWordEndsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD_END)
}
}
protected abstract fun start(session: Session)
class GoToDeclaration : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN

View File

@@ -51,5 +51,18 @@ enum class StandardBoundaries : Boundaries {
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset > editor.caretModel.offset
}
},
CARET_LINE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val document = editor.document
val offset = editor.caretModel.offset
val line = document.getLineNumber(offset)
return document.getLineStartOffset(line)..document.getLineEndOffset(line)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in getOffsetRange(editor, cache)
}
}
}

View File

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

View File

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

View File

@@ -13,12 +13,6 @@ data class AceSettings(
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("advancedModeRGB", converter = ColorConverter::class)
var advancedModeColor: Color = Color(0xFFB700),
@OptionTag("betweenPointsModeRGB", converter = ColorConverter::class)
var betweenPointsModeColor: Color = Color(0x6FC5FF),
@OptionTag("textHighlightRGB", converter = ColorConverter::class)
var textHighlightColor: Color = Color(0x394B58),

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,92 +0,0 @@
package org.acejump.modes
import com.intellij.openapi.editor.CaretState
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class BetweenPointsMode : SessionMode {
private companion object {
private val TYPE_TAG_HINT = arrayOf(
"<b>Type to Search...</b>"
)
private val ACTION_MODE_HINT = arrayOf(
"<f>[C]</f>lone to Caret...",
"<f>[M]</f>ove to Caret..."
)
private val ACTION_MODE_MAP = mapOf(
'C' to (AceTagAction::CloneToCaret),
'M' to (AceTagAction::MoveToCaret)
)
}
override val caretColor
get() = AceConfig.betweenPointsModeColor
private var actionMode: ((AceTagAction.BaseSelectAction) -> AceTagAction)? = null
private var originalCarets: List<CaretState>? = null
private var firstOffset: Int? = null
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
val actionMode = actionMode
if (actionMode == null) {
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing
}
if (acceptedTag == null) {
return state.type(charTyped)
}
if (firstOffset == null) {
val selectAction = AdvancedMode.SELECT_ACTION_MAP[charTyped.toUpperCase()]
if (selectAction != null) {
state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase(), isFinal = true)
return TypeResult.EndSession
}
}
val jumpAction = AdvancedMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
val firstOffset = firstOffset
if (firstOffset == null) {
val caretModel = acceptedTag.editor.caretModel
this.originalCarets = caretModel.caretsAndSelections
state.act(jumpAction, acceptedTag, shiftMode = false, isFinal = false)
this.firstOffset = caretModel.offset
return TypeResult.RestartSearch
}
originalCarets?.let { acceptedTag.editor.caretModel.caretsAndSelections = it }
state.act(
actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)),
acceptedTag,
shiftMode = charTyped.isUpperCase(),
isFinal = true
)
return TypeResult.EndSession
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
return false
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return when {
actionMode == null -> ACTION_MODE_HINT
acceptedTag == null -> TYPE_TAG_HINT.takeUnless { hasQuery }
firstOffset == null -> AdvancedMode.JUMP_ALT_HINT + AdvancedMode.SELECT_HINT
else -> AdvancedMode.JUMP_ALT_HINT
}
}
}

View File

@@ -22,8 +22,4 @@ open class JumpMode : SessionMode {
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
return true
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return null
}
}

View File

@@ -10,5 +10,4 @@ interface SessionMode {
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult
fun accept(state: SessionState, acceptedTag: Tag): Boolean
fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>?
}

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package org.acejump.search
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries
import org.acejump.clone
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.matchesAt
@@ -11,9 +10,7 @@ import org.acejump.matchesAt
/**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character.
*/
class SearchProcessor private constructor(
private val editors: List<Editor>, query: SearchQuery, results: MutableMap<Editor, IntArrayList>
) {
class SearchProcessor private constructor(query: SearchQuery, results: MutableMap<Editor, IntArrayList>) {
companion object {
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
@@ -24,7 +21,7 @@ class SearchProcessor private constructor(
}
}
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) {
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
val regex = query.toRegex()
if (regex != null) {
@@ -149,8 +146,4 @@ class SearchProcessor private constructor(
results[editor] = remaining
}
}
fun clone(): SearchProcessor {
return SearchProcessor(editors, query, results.clone())
}
}

View File

@@ -74,10 +74,6 @@ class Tagger(private val editors: List<Editor>) {
return TaggingResult.Mark(createTagMarkers(resultTags, query.rawText.ifEmpty { null }))
}
fun clone(): Tagger {
return Tagger(editors).also { it.tagMap.putAll(tagMap) }
}
/**
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
*/

View File

@@ -1,25 +1,19 @@
package org.acejump.session
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.codeInsight.hint.HintUtil
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import com.intellij.ui.LightweightHint
import org.acejump.ExternalUsage
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.clone
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.EditorKeyListener
import org.acejump.input.KeyLayoutCache
import org.acejump.modes.AdvancedMode
import org.acejump.modes.BetweenPointsMode
import org.acejump.modes.JumpMode
import org.acejump.modes.SessionMode
import org.acejump.search.*
@@ -68,13 +62,12 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
editorSettings.stopEditing(editor)
when (result) {
TypeResult.Nothing -> updateHint()
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)
updateHint()
}
TypeResult.EndSession -> end()
@@ -114,37 +107,11 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
textHighlighter.renderOccurrences(results, query)
}
}
updateHint()
}
private fun setMode(mode: SessionMode) {
this.mode = mode
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
updateHint()
}
private fun updateHint() {
val acceptedTag = acceptedTag
val editor = mainEditor
val offset = when {
acceptedTag == null -> null
acceptedTag.editor === editor -> acceptedTag.offset
else -> mainEditor.caretModel.offset
}
val hintArray = mode.getHint(offset, state?.currentProcessor.let { it != null && it.query.rawText.isNotEmpty() }) ?: return
val hintText = hintArray
.joinToString("\n")
.replace("<f>", "<span style=\"font-family:'${editor.colorsScheme.editorFontName}';font-weight:bold\">")
.replace("</f>", "</span>")
val hint = LightweightHint(HintUtil.createInformationLabel(hintText))
val pos = offset?.let(editor::offsetToLogicalPosition) ?: editor.caretModel.logicalPosition
val point = HintManagerImpl.getHintPosition(hint, editor, pos, HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, point, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, true, info)
}
fun startJumpMode() {
@@ -165,22 +132,6 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
}
fun startOrCycleSpecialModes() {
if (!this::mode.isInitialized) {
setMode(AdvancedMode())
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
return
}
restart()
setMode(when (mode) {
is AdvancedMode -> BetweenPointsMode()
else -> AdvancedMode()
})
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.
*/
@@ -192,9 +143,8 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary).also {
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary, it)
}
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary)
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary, processor)
updateSearch(processor, markImmediately = true)
}
@@ -206,15 +156,6 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
if (processor != null) {
updateSearch(processor, markImmediately = true)
}
else if (mode is AdvancedMode) {
val offset = mainEditor.caretModel.offset
val result = mainEditor.immutableText.getOrNull(offset)?.let(state::type)
if (result is TypeResult.UpdateResults) {
val tag = Tag(mainEditor, offset).also { acceptedTag = it }
textHighlighter.renderFinal(tag, result.processor.query)
updateHint()
}
}
}
/**

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

@@ -13,6 +13,8 @@
<category>Navigation</category>
<vendor url="https://github.com/acejump/AceJump">AceJump</vendor>
<depends optional="true" config-file="acejump-ideavim.xml">IdeaVIM</depends>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="org.acejump.config.AceConfig"/>
<applicationConfigurable groupId="tools" displayName="AceJump"
@@ -25,16 +27,6 @@
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
<editorActionHandler action="EditorEnter" order="first"
implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/>
<editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$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>
<actions>
@@ -45,71 +37,6 @@
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action>
<action id="AceSpecialAction"
class="org.acejump.action.AceKeyboardAction$ActivateAceJumpSpecial"
text="Activate / Cycle AceJump Special Modes">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl alt SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl alt SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt SEMICOLON"/>
</action>
<action id="AceLineAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode">
<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.AceKeyboardAction$StartAllLineStartsMode"
text="Start AceJump in All Line Starts Mode"/>
<action id="AceLineEndsAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineEndsMode"
text="Start AceJump in All Line Ends Mode"/>
<action id="AceLineIndentsAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineIndentsMode"
text="Start AceJump in All Line Indents Mode"/>
<action id="AceWordAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsMode"
text="Start AceJump in All Words Mode"/>
<action id="AceWordForwardAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsForwardMode"
text="Start AceJump in All Words After Caret Mode"/>
<action id="AceWordBackwardsAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/>
<action id="AceVimAction_JumpToChar"
class="org.acejump.action.AceVimAction$JumpToChar"
text="AceJump Vim - JumpToChar"/>
<action id="AceVimAction_JumpToCharBeforeCaret"
class="org.acejump.action.AceVimAction$JumpToCharBeforeCaret"
text="AceJump Vim - JumpToCharBeforeCaret"/>
<action id="AceVimAction_JumpToCharAfterCaret"
class="org.acejump.action.AceVimAction$JumpToCharAfterCaret"
text="AceJump Vim - JumpToCharAfterCaret"/>
<action id="AceVimAction_LWordsAfterCaret"
class="org.acejump.action.AceVimAction$LWordsAfterCaret"
text="AceJump Vim - LWordsAfterCaret"/>
<action id="AceVimAction_UWordsAfterCaret"
class="org.acejump.action.AceVimAction$UWordsAfterCaret"
text="AceJump Vim - UWordsAfterCaret"/>
<action id="AceVimAction_LWordsBeforeCaret"
class="org.acejump.action.AceVimAction$LWordsBeforeCaret"
text="AceJump Vim - LWordsBeforeCaret"/>
<action id="AceVimAction_UWordsBeforeCaret"
class="org.acejump.action.AceVimAction$UWordsBeforeCaret"
text="AceJump Vim - UWordsBeforeCaret"/>
<action id="AceVimAction_LWordEndsAfterCaret"
class="org.acejump.action.AceVimAction$LWordEndsAfterCaret"
text="AceJump Vim - LWordEndsAfterCaret"/>
<action id="AceVimAction_UWordEndsAfterCaret"
class="org.acejump.action.AceVimAction$UWordEndsAfterCaret"
text="AceJump Vim - UWordEndsAfterCaret"/>
<action id="AceVimAction_LWordEndsBeforeCaret"
class="org.acejump.action.AceVimAction$LWordEndsBeforeCaret"
text="AceJump Vim - LWordEndsBeforeCaret"/>
<action id="AceVimAction_UWordEndsBeforeCaret"
class="org.acejump.action.AceVimAction$UWordEndsBeforeCaret"
text="AceJump Vim - UWordEndsBeforeCaret"/>
<action id="AceVimAction_GoToDeclaration"
class="org.acejump.action.AceVimAction$GoToDeclaration"
text="AceJump Vim - GoToDeclaration"/>

View File

@@ -1,5 +1,4 @@
import org.acejump.action.AceKeyboardAction
import org.acejump.test.util.BaseTest
/**
@@ -34,40 +33,4 @@ class AceTest : BaseTest() {
myFixture.checkResult("testin<caret>g 1234")
}
fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two")
takeAction(AceKeyboardAction.StartAllWordsBackwardsMode)
assertEquals(2, session.tags.size)
}
fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four")
takeAction(AceKeyboardAction.StartAllWordsForwardMode)
assertEquals(4, session.tags.size)
}
fun `test word mode`() {
makeEditor("test word action")
takeAction(AceKeyboardAction.StartAllWordsMode)
assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key)
myFixture.checkResult("test <caret>word action")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines\n")
takeAction(AceKeyboardAction.StartAllLineMarksMode)
assertEquals(9, session.tags.size)
}
}

View File

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