Skip to content

Commit

Permalink
Major AceJump refactoring!
Browse files Browse the repository at this point in the history
See acejump#348 for information on what's changed and what more needs to be done.
  • Loading branch information
chylex committed Mar 28, 2021
1 parent 89af384 commit 7fcc6c4
Show file tree
Hide file tree
Showing 59 changed files with 2,407 additions and 2,895 deletions.
12 changes: 6 additions & 6 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2
max_line_length=80
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
max_line_length = 140
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import org.jetbrains.changelog.closure
import org.jetbrains.intellij.tasks.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
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

plugins {
idea apply true
Expand Down
82 changes: 82 additions & 0 deletions src/main/kotlin/org/acejump/AceUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.acejump

import com.intellij.openapi.editor.Editor

annotation class ExternalUsage

/**
* Returns an immutable version of the currently edited document.
*/
val Editor.immutableText
get() = this.document.immutableCharSequence

/**
* Returns true if [this] contains [otherText] at the specified offset.
*/
fun CharSequence.matchesAt(selfOffset: Int, otherText: String, ignoreCase: Boolean): Boolean {
return this.regionMatches(selfOffset, otherText, 0, otherText.length, ignoreCase)
}

/**
* Calculates the length of a common prefix in [this] starting at index [selfOffset], and [otherText] starting at index 0.
*/
fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): Int {
var i = 0
var o = selfOffset + i

while (i < otherText.length && o < this.length && otherText[i].equals(this[o], ignoreCase = true)) {
i++
o++
}

return i
}

/**
* Determines which characters form a "word" for the purposes of functions below.
*/
val Char.isWordPart
get() = this.isJavaIdentifierPart()

/**
* Finds index of the first character in a word.
*/
inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var start = pos

while (start > 0 && isPartOfWord(this[start - 1])) {
--start
}

return start
}

/**
* Finds index of the last character in a word.
*/
inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos

while (end < length - 1 && isPartOfWord(this[end + 1])) {
++end
}

return end
}

/**
* Finds index of the first word character following a sequence of non-word characters following the end of a word.
*/
inline fun CharSequence.wordEndPlus(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = this.wordEnd(pos, isPartOfWord)

while (end < length - 1 && !isPartOfWord(this[end + 1])) {
++end
}

if (end < length - 1 && isPartOfWord(this[end + 1])) {
++end
}

return end
}
61 changes: 61 additions & 0 deletions src/main/kotlin/org/acejump/action/AceAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.cycleJumpMode()
}

// @formatter:off

object ToggleJumpMode : BaseToggleJumpModeAction(JumpMode.JUMP)
object ToggleJumpEndMode : BaseToggleJumpModeAction(JumpMode.JUMP_END)
object ToggleTargetMode : BaseToggleJumpModeAction(JumpMode.TARGET)
object ToggleDeclarationMode : BaseToggleJumpModeAction(JumpMode.DEFINE)

object ToggleAllLinesMode : BaseRegexSearchAction(Pattern.LINE_MARK, StandardBoundaries.WHOLE_FILE)
object ToggleAllWordsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.WHOLE_FILE)
object ToggleAllWordsBackwardsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.BEFORE_CARET)
object ToggleAllWordsForwardMode : BaseRegexSearchAction(Pattern.ALL_WORDS, StandardBoundaries.AFTER_CARET)

// @formatter:on
}
62 changes: 62 additions & 0 deletions src/main/kotlin/org/acejump/action/AceEditorAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.acejump.action

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager

/**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
*/
sealed class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
}

final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val session = SessionManager[editor]

if (session != null) {
run(session)
}
else if (originalHandler.isEnabled(editor, caret, dataContext)) {
originalHandler.execute(editor, caret, dataContext)
}
}

protected abstract fun run(session: Session)

// Actions

class Reset(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.end()
}

class ClearSearch(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.restart()
}

class SelectBackward(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitPreviousTag()
}

class SelectForward(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.visitNextTag()
}

class SearchCodeIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.CODE_INDENTS, StandardBoundaries.WHOLE_FILE)
}

class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.START_OF_LINE, StandardBoundaries.WHOLE_FILE)
}

class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.END_OF_LINE, StandardBoundaries.WHOLE_FILE)
}
}
108 changes: 108 additions & 0 deletions src/main/kotlin/org/acejump/action/TagJumper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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)
}
}
}
Loading

0 comments on commit 7fcc6c4

Please sign in to comment.