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

15 Commits

37 changed files with 1198 additions and 767 deletions

View File

@@ -43,4 +43,4 @@ intellij {
} }
group = "org.acejump" group = "org.acejump"
version = "4.0" version = "chylex"

View File

@@ -1,6 +1,7 @@
package org.acejump package org.acejump
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actions.EditorActionUtil
annotation class ExternalUsage annotation class ExternalUsage
@@ -64,6 +65,32 @@ inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char
return end 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. * Finds index of the first word character following a sequence of non-word characters following the end of a word.
*/ */

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

@@ -40,23 +40,19 @@ 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) { class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.WHOLE_FILE) override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.WHOLE_FILE) override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.WHOLE_FILE) override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.VISIBLE_ON_SCREEN)
} }
} }

View File

@@ -0,0 +1,51 @@
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.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 AceKeyboardAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
invoke(SessionManager.start(e.getData(EDITOR) ?: return))
}
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.startRegexSearch(pattern, boundaries)
}
/**
* Starts or ends an AceJump session.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.cycleMode()
}
// @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

@@ -0,0 +1,526 @@
package org.acejump.action
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction
import com.intellij.find.actions.FindUsagesAction
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
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.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 com.intellij.refactoring.actions.RefactoringQuickListPopupAction
import com.intellij.refactoring.actions.RenameElementAction
import org.acejump.*
import org.acejump.search.SearchProcessor
import kotlin.math.max
/**
* Base class for actions available after typing a tag.
*/
sealed class AceTagAction {
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
abstract class BaseJumpAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
recordCaretPosition(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
}
abstract class BaseSelectAction : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: 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) {
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
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)
}
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 selectRange(editor: Editor, fromOffset: Int, toOffset: Int, cursorOffset: Int = toOffset) = with(editor) {
selectionModel.removeSelection(true)
selectionModel.setSelection(fromOffset, toOffset)
caretModel.moveToOffset(cursorOffset)
}
fun performAction(action: AnAction) {
ActionManager.getInstance().tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
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 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
* 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, 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)
}
}
}
/**
* 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)
}
}
}
/**
* On default action, selects a word according to [SelectWord], and then extends the selection in either or both directions based
* on the characters around the word. TODO
*
* On shift action, adds the new selection to existing selections.
*/
object SelectAroundWord : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
SelectWord(editor, searchProcessor, offset, shiftMode = false)
val text = editor.immutableText
var selectionStart = editor.selectionModel.selectionStart
var selectionEnd = editor.selectionModel.selectionEnd
val indentStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(editor.document, editor.caretModel.logicalPosition.line)
while (selectionStart > 0 && selectionStart > indentStart && text[selectionStart - 1].let { it == ' ' || it == ',' }) {
--selectionStart
}
while (selectionEnd < text.length && text[selectionEnd].let { it == ' ' || it == ',' }) {
++selectionEnd
}
if (selectionStart > 0 && text[selectionStart - 1] == '!') {
--selectionStart
}
selectRange(editor, selectionStart, selectionEnd)
}
}
/**
* On default action, selects the line at the tag, excluding the indent.
* On 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)
val document = editor.document
val line = editor.caretModel.logicalPosition.line
val lineStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(document, line)
val lineEnd = document.getLineEndOffset(line)
selectRange(editor, lineStart, lineEnd)
}
}
/**
* On default action, places the caret at the last character of the search query, and then performs Extend Selection a set amount of
* times.
*
* On shift action, adds the new selection to existing selections.
*/
class SelectExtended(private val extendCount: Int) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
repeat(extendCount) {
performAction(action)
}
}
}
/**
* 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)
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)
selectRange(editor, firstOffset, editor.caretModel.offset)
}
}
/**
* On default action, selects text based on the provided [selector] action, and deletes it without moving the existing carets.
* On shift action, moves caret to the position where deletion occurred.
*/
class Delete(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Delete").run<Throwable> {
editor.selectionModel.let { editor.document.deleteString(it.selectionStart, it.selectionEnd) }
}
if (!shiftMode) {
editor.caretModel.caretsAndSelections = oldCarets
}
}
}
/**
* Selects text based on the provided [selector] action and clones it at every existing caret, selecting the cloned text. If a caret
* 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`.
* Always places the caret at the start of the word.
*/
object GoToDeclaration : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
}
}
/**
* 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) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) FindUsagesAction() else ShowUsagesAction())
}
}
/**
* 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) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
performAction(ShowIntentionActionsAction())
}
}
/**
* 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) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
if (shiftMode) {
ApplicationManager.getApplication().invokeLater { performAction(RenameElementAction()) }
}
else {
performAction(RefactoringQuickListPopupAction())
}
}
}
}

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)

View File

@@ -19,16 +19,14 @@ 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 jumpModeColor get() = settings.jumpModeColor val jumpModeColor get() = settings.jumpModeColor
val jumpEndModeColor get() = settings.jumpEndModeColor val fromCaretModeColor get() = settings.fromCaretModeColor
val targetModeColor get() = settings.targetModeColor val betweenPointsModeColor get() = settings.betweenPointsModeColor
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 roundedTagCorners get() = settings.roundedTagCorners val acceptedTagColor get() = settings.acceptedTagColor
val searchWholeFile get() = settings.searchWholeFile
} }
override fun getState(): AceSettings { override fun getState(): AceSettings {

View File

@@ -14,36 +14,26 @@ 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.minQueryLengthInt != settings.minQueryLength ||
panel.cycleMode2 != settings.cycleMode2 ||
panel.cycleMode3 != settings.cycleMode3 ||
panel.cycleMode4 != settings.cycleMode4 ||
panel.jumpModeColor != settings.jumpModeColor || panel.jumpModeColor != settings.jumpModeColor ||
panel.jumpEndModeColor != settings.jumpEndModeColor || panel.fromCaretModeColor != settings.fromCaretModeColor ||
panel.targetModeColor != settings.targetModeColor || panel.betweenPointsModeColor != settings.betweenPointsModeColor ||
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.roundedTagCorners != settings.roundedTagCorners || panel.acceptedTagColor != settings.acceptedTagColor
panel.searchWholeFile != settings.searchWholeFile
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.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
settings.cycleMode2 = panel.cycleMode2
settings.cycleMode3 = panel.cycleMode3
settings.cycleMode4 = panel.cycleMode4
panel.jumpModeColor?.let { settings.jumpModeColor = it } panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.jumpEndModeColor?.let { settings.jumpEndModeColor = it } panel.fromCaretModeColor?.let { settings.fromCaretModeColor = it }
panel.targetModeColor?.let { settings.targetModeColor = it } panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = 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.roundedTagCorners = panel.roundedTagCorners panel.acceptedTagColor?.let { settings.acceptedTagColor = it }
settings.searchWholeFile = panel.searchWholeFile
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,32 +8,26 @@ 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 minQueryLength: Int = 1,
var cycleMode2: JumpMode = JumpMode.DEFINE,
var cycleMode3: JumpMode = JumpMode.TARGET,
var cycleMode4: JumpMode = JumpMode.JUMP_END,
@OptionTag("jumpModeRGB", converter = ColorConverter::class) @OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color.BLUE, var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("jumpEndModeRGB", converter = ColorConverter::class) @OptionTag("fromCaretModeRGB", converter = ColorConverter::class)
var jumpEndModeColor: Color = Color.CYAN, var fromCaretModeColor: Color = Color(0xFFB700),
@OptionTag("targetModeRGB", converter = ColorConverter::class) @OptionTag("betweenPointsModeRGB", converter = ColorConverter::class)
var targetModeColor: Color = Color.RED, var betweenPointsModeColor: Color = Color(0x6FC5FF),
@OptionTag("definitionModeRGB", converter = ColorConverter::class)
var definitionModeColor: Color = Color.MAGENTA,
@OptionTag("textHighlightRGB", converter = ColorConverter::class) @OptionTag("textHighlightRGB", converter = ColorConverter::class)
var textHighlightColor: Color = Color.GREEN, var textHighlightColor: Color = Color(0x394B58),
@OptionTag("tagForegroundRGB", converter = ColorConverter::class) @OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var tagForegroundColor: Color = Color.BLACK, var tagForegroundColor: Color = Color(0xFFFFFF),
@OptionTag("tagBackgroundRGB", converter = ColorConverter::class) @OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
var tagBackgroundColor: Color = Color.YELLOW, var tagBackgroundColor: Color = Color(0x008299),
var roundedTagCorners: Boolean = true, @OptionTag("acceptedTagRGB", converter = ColorConverter::class)
var searchWholeFile: Boolean = true 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,19 @@ 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 minQueryLengthField = JBTextField()
private val cycleModeCombo2 = ComboBox<JumpMode>()
private val cycleModeCombo3 = ComboBox<JumpMode>()
private val cycleModeCombo4 = ComboBox<JumpMode>()
private val jumpModeColorWheel = ColorPanel() private val jumpModeColorWheel = ColorPanel()
private val jumpEndModeColorWheel = ColorPanel() private val fromCaretModeColorWheel = ColorPanel()
private val targetModeColorWheel = ColorPanel() private val betweenPointsModeColorWheel = 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 roundedTagCornersCheckBox = JBCheckBox() private val acceptedTagColorWheel = ColorPanel()
private val searchWholeFileCheckBox = JBCheckBox()
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,33 +50,18 @@ 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("Jump mode caret background:") { short(jumpModeColorWheel) }
row("Jump to End mode caret background:") { short(jumpEndModeColorWheel) } row("From Caret mode caret background:") { short(fromCaretModeColorWheel) }
row("Target mode caret background:") { short(targetModeColorWheel) } row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) }
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("Appearance") {
row { short(roundedTagCornersCheckBox.apply { text = "Rounded tag corners" }) }
}
titledRow("Behavior") {
row { short(searchWholeFileCheckBox.apply { text = "Search whole file" }) }
} }
} }
@@ -95,36 +69,30 @@ 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 minQueryLength by minQueryLengthField
internal var cycleMode2 by cycleModeCombo2
internal var cycleMode3 by cycleModeCombo3
internal var cycleMode4 by cycleModeCombo4
internal var jumpModeColor by jumpModeColorWheel internal var jumpModeColor by jumpModeColorWheel
internal var jumpEndModeColor by jumpEndModeColorWheel internal var fromCaretModeColor by fromCaretModeColorWheel
internal var targetModeColor by targetModeColorWheel internal var betweenPointsModeColor by betweenPointsModeColorWheel
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 roundedTagCorners by roundedTagCornersCheckBox internal var acceptedTagColor by acceptedTagColorWheel
internal var searchWholeFile by searchWholeFileCheckBox
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) { minQueryLength = value.toString() }
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars allowedChars = settings.allowedChars
keyboardLayout = settings.layout keyboardLayout = settings.layout
cycleMode1 = settings.cycleMode1 minQueryLength = settings.minQueryLength.toString()
cycleMode2 = settings.cycleMode2
cycleMode3 = settings.cycleMode3
cycleMode4 = settings.cycleMode4
jumpModeColor = settings.jumpModeColor jumpModeColor = settings.jumpModeColor
jumpEndModeColor = settings.jumpEndModeColor fromCaretModeColor = settings.fromCaretModeColor
targetModeColor = settings.targetModeColor betweenPointsModeColor = settings.betweenPointsModeColor
definitionModeColor = settings.definitionModeColor
textHighlightColor = settings.textHighlightColor textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor tagForegroundColor = settings.tagForegroundColor
tagBackgroundColor = settings.tagBackgroundColor tagBackgroundColor = settings.tagBackgroundColor
roundedTagCorners = settings.roundedTagCorners acceptedTagColor = settings.acceptedTagColor
searchWholeFile = settings.searchWholeFile
} }
// 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

@@ -0,0 +1,91 @@
package org.acejump.modes
import com.intellij.openapi.editor.CaretState
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
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>[S]</f>elect... / <f>[F]</f>rom Caret...",
"<f>[D]</f>elete...",
"<f>[C]</f>lone to Caret...",
"<f>[M]</f>ove to Caret..."
)
private const val ACTION_MODE_FROM_CARET = 'F'
private val ACTION_MODE_MAP = mapOf(
'S' to ({ action: AceTagAction.BaseSelectAction -> action }),
'D' to (AceTagAction::Delete),
'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: Int?): TypeResult {
val actionMode = actionMode
if (actionMode == null) {
if (charTyped.equals(ACTION_MODE_FROM_CARET, ignoreCase = true)) {
return TypeResult.ChangeMode(SelectFromCaretMode())
}
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing
}
if (acceptedTag == null) {
return state.type(charTyped)
}
if (firstOffset == null) {
val selectAction = JumpMode.SELECT_ACTION_MAP[charTyped.toUpperCase()]
if (selectAction != null) {
state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
}
val jumpAction = JumpMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
val firstOffset = firstOffset
if (firstOffset == null) {
val caretModel = state.editor.caretModel
this.originalCarets = caretModel.caretsAndSelections
state.act(jumpAction, acceptedTag, shiftMode = false)
this.firstOffset = caretModel.offset
return TypeResult.RestartSearch
}
originalCarets?.let { state.editor.caretModel.caretsAndSelections = it }
state.act(actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
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 -> JumpMode.JUMP_ALT_HINT + JumpMode.SELECT_HINT
else -> JumpMode.JUMP_ALT_HINT
}
}
}

View File

@@ -0,0 +1,75 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class JumpMode : 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 / <f>[A]</f>round",
"Select <f>[Q]</f>uery / <f>[N]</f> Line / <f>[1-9]</f> Expansion"
)
val SELECT_ACTION_MAP = mapOf(
'W' to AceTagAction.SelectWord,
'H' to AceTagAction.SelectHump,
'A' to AceTagAction.SelectAroundWord,
'Q' to AceTagAction.SelectQuery,
'N' to AceTagAction.SelectLine,
*('1'..'9').mapIndexed { index, char -> char to AceTagAction.SelectExtended(index + 1) }.toTypedArray()
)
private val ALL_HINTS = arrayOf(
*JUMP_HINT,
*SELECT_HINT,
"<f>[D]</f>eclaration / <f>[U]</f>sages",
"<f>[I]</f>ntentions / <f>[R]</f>efactor"
)
private val ALL_ACTION_MAP = mapOf(
*JUMP_ACTION_MAP.map { it.key to it.value }.toTypedArray(),
*SELECT_ACTION_MAP.map { it.key to it.value }.toTypedArray(),
'D' to AceTagAction.GoToDeclaration,
'U' to AceTagAction.ShowUsages,
'I' to AceTagAction.ShowIntentions,
'R' to AceTagAction.Refactor
)
}
override val caretColor
get() = AceConfig.jumpModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
if (acceptedTag == null) {
return state.type(charTyped)
}
val action = ALL_ACTION_MAP[charTyped.toUpperCase()]
if (action != null) {
state.act(action, acceptedTag, charTyped.isUpperCase())
return TypeResult.EndSession
}
return TypeResult.Nothing
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return ALL_HINTS.takeIf { acceptedTag != null }
}
}

View File

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

View File

@@ -0,0 +1,12 @@
package org.acejump.modes
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: Int?): TypeResult
fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>?
}

View File

@@ -10,7 +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. * 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(private val editor: Editor, query: SearchQuery) {
companion object { companion object {
fun fromChar(editor: Editor, char: Char, boundaries: Boundaries): SearchProcessor { fun fromChar(editor: Editor, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.Literal(char.toString()), boundaries) return SearchProcessor(editor, SearchQuery.Literal(char.toString()), boundaries)
@@ -21,13 +21,7 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
} }
} }
var query = query private constructor(editor: Editor, query: SearchQuery, boundaries: Boundaries) : this(editor, query) {
private set
var results = IntArrayList(0)
private set
init {
val regex = query.toRegex() val regex = query.toRegex()
if (regex != null) { if (regex != null) {
@@ -36,19 +30,26 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
while (result != null) { while (result != null) {
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception. val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception.
val highlightEnd = index + query.getHighlightLength("", index)
if (boundaries.isOffsetInside(editor, index)) { if (highlightEnd > offsetRange.last) {
results.add(index)
}
else if (index > offsetRange.last) {
break break
} }
else if (boundaries.isOffsetInside(editor, index)) {
results.add(index)
}
result = result.next() result = result.next()
} }
} }
} }
internal var query = query
private set
internal var results = IntArrayList(0)
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
* transitioned the search query from a non-word to a word, it notifies the [Tagger] to reassign all tags. If the new query does not * transitioned the search query from a non-word to a word, it notifies the [Tagger] to reassign all tags. If the new query does not
@@ -119,4 +120,8 @@ internal class SearchProcessor private constructor(private val editor: Editor, q
results = remaining results = remaining
} }
fun clone(): SearchProcessor {
return SearchProcessor(editor, query).also { it.results.addAll(results) }
}
} }

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

@@ -52,18 +52,25 @@ import kotlin.math.max
* at any time during the selection process, so as not to confuse the user. * at any time during the selection process, so as not to confuse the user.
*/ */
internal class Solver private constructor(private val editor: Editor, private val queryLength: Int, private val results: IntList) { internal class Solver private constructor(
private val editor: Editor,
private val queryLength: Int,
private val newResults: IntList,
private val allResults: IntList
) {
companion object { companion object {
fun solve(editor: Editor, query: SearchQuery, results: IntList, tags: List<String>, cache: EditorOffsetCache): Map<String, Int> { fun solve(
return Solver(editor, max(1, query.rawText.length), results).map(tags, cache) editor: Editor, query: SearchQuery, newResults: IntList, allResults: IntList, tags: List<String>, cache: EditorOffsetCache
): Map<String, Int> {
return Solver(editor, max(1, query.rawText.length), newResults, allResults).map(tags, cache)
} }
} }
private var newTags = Object2IntOpenHashMap<String>(KeyLayoutCache.allPossibleTags.size) private var newTags = Object2IntOpenHashMap<String>(KeyLayoutCache.allPossibleTags.size)
private val newTagIndices = IntOpenHashSet() private val newTagIndices = IntOpenHashSet()
private var allWordFragments = HashSet<String>(results.size).apply { private var allWordFragments = HashSet<String>(allResults.size).apply {
val iter = results.iterator() val iter = allResults.iterator()
while (iter.hasNext()) { while (iter.hasNext()) {
forEachWordFragment(iter.nextInt()) { add(it) } forEachWordFragment(iter.nextInt()) { add(it) }
} }
@@ -73,7 +80,7 @@ internal class Solver private constructor(private val editor: Editor, private va
val eligibleSitesByTag = HashMap<String, IntList>(100) val eligibleSitesByTag = HashMap<String, IntList>(100)
val tagsByFirstLetter = availableTags.groupBy { it[0] } val tagsByFirstLetter = availableTags.groupBy { it[0] }
val iter = results.iterator() val iter = newResults.iterator()
while (iter.hasNext()) { while (iter.hasNext()) {
val site = iter.nextInt() val site = iter.nextInt()
@@ -107,7 +114,7 @@ internal class Solver private constructor(private val editor: Editor, private va
var totalAssigned = 0 var totalAssigned = 0
for (tag in sortedTags) { for (tag in sortedTags) {
if (totalAssigned == results.size) { if (totalAssigned == newResults.size) {
break break
} }

View File

@@ -19,28 +19,31 @@ import kotlin.collections.component2
/** /**
* 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.
*/ */
internal class Tagger(private val editor: Editor) { class Tagger(private val editor: Editor) {
private var tagMap = HashBiMap.create<String, Int>() private var tagMap = HashBiMap.create<String, Int>()
val hasTags
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 }
/** /**
* 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: 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()
@@ -49,7 +52,7 @@ 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)
} }
} }
@@ -65,6 +68,10 @@ internal class Tagger(private val editor: Editor) {
return TaggingResult.Mark(createTagMarkers(results, query.rawText.ifEmpty { null })) return TaggingResult.Mark(createTagMarkers(results, query.rawText.ifEmpty { null }))
} }
fun clone(): Tagger {
return Tagger(editor).also { it.tagMap.putAll(tagMap) }
}
/** /**
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags. * Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
*/ */
@@ -103,7 +110,7 @@ internal class Tagger(private val editor: Editor) {
} }
allAssignedTags.putAll(oldCompatibleTags) allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editor, query, vacantResults, availableTags, cache)) allAssignedTags.putAll(Solver.solve(editor, query, vacantResults, results, availableTags, cache))
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) -> return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
// Avoid matching query - will trigger a jump. // Avoid matching query - will trigger a jump.

View File

@@ -3,6 +3,6 @@ package org.acejump.search
import org.acejump.view.Tag import org.acejump.view.Tag
internal sealed class TaggingResult { internal sealed class TaggingResult {
class Jump(val offset: Int) : TaggingResult() class Accept(val offset: Int) : TaggingResult()
class Mark(val tags: List<Tag>) : TaggingResult() class Mark(val tags: List<Tag>) : 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,21 +1,24 @@
package org.acejump.session 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.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 com.intellij.ui.LightweightHint
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.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.immutableText
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.modes.BetweenPointsMode
import org.acejump.modes.JumpMode
import org.acejump.modes.SessionMode
import org.acejump.search.* import org.acejump.search.*
import org.acejump.view.TagCanvas import org.acejump.view.TagCanvas
import org.acejump.view.TextHighlighter import org.acejump.view.TextHighlighter
@@ -24,37 +27,22 @@ 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 editor: Editor) {
private companion object { private val editorSettings = EditorSettings.setup(editor)
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(editor)
private val jumpModeTracker = JumpModeTracker() private var acceptedTag: Int? = null
private var jumpMode = JumpMode.DISABLED
set(value) { set(value) {
field = value field = value
if (value === JumpMode.DISABLED) { if (value != null) {
end() tagCanvas.removeMarkers()
} editorSettings.onTagAccepted(editor)
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 var tagger = Tagger(editor)
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 textHighlighter = TextHighlighter(editor)
private val tagCanvas = TagCanvas(editor) private val tagCanvas = TagCanvas(editor)
@@ -67,58 +55,103 @@ class Session(private val editor: Editor) {
EditorKeyListener.attach(editor, object : TypedActionHandler { EditorKeyListener.attach(editor, object : TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) { override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
var processor = searchProcessor val state = state ?: return
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
}
updateSearch(processor, shiftMode = charTyped.isUpperCase()) when (result) {
TypeResult.Nothing -> updateHint()
TypeResult.RestartSearch -> restart().also { this@Session.state = SessionStateImpl(editor, tagger); updateHint() }
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
is TypeResult.ChangeMode -> setMode(result.mode)
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, 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) }) {
textHighlighter.renderOccurrences(results, query)
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) val offset = result.offset
tagCanvas.removeMarkers() acceptedTag = offset
end() textHighlighter.renderFinal(offset, processor.query)
} }
is TaggingResult.Mark -> { is TaggingResult.Mark -> {
val tags = result.tags val tags = result.tags
tagCanvas.setMarkers(tags, isRegex = query is SearchQuery.RegularExpression) tagCanvas.setMarkers(tags)
textHighlighter.renderOccurrences(results, query)
val cache = EditorOffsetCache.new()
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
if (tags.none { boundaries.isOffsetInside(editor, it.offsetL, cache) || boundaries.isOffsetInside(editor, it.offsetR, cache) }) {
tagVisitor?.scrollToClosest()
}
} }
} }
updateHint()
}
private fun setMode(mode: SessionMode) {
this.mode = mode
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
updateHint()
}
private fun updateHint() {
val hintArray = mode.getHint(acceptedTag, 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 = acceptedTag?.let(editor::offsetToLogicalPosition) ?: editor.caretModel.logicalPosition
val point = HintManagerImpl.getHintPosition(hint, editor, pos, HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, point, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, true, info)
}
fun cycleMode() {
if (!this::mode.isInitialized) {
setMode(JumpMode())
state = SessionStateImpl(editor, tagger)
return
}
restart()
setMode(when (mode) {
is JumpMode -> BetweenPointsMode()
else -> JumpMode()
})
state = SessionStateImpl(editor, tagger)
} }
/** /**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights. * Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/ */
fun startRegexSearch(pattern: String, boundaries: Boundaries) { fun startRegexSearch(pattern: String, boundaries: Boundaries) {
if (!this::mode.isInitialized) {
setMode(JumpMode())
}
tagger = Tagger(editor) tagger = Tagger(editor)
tagCanvas.setMarkers(emptyList(), isRegex = true) tagCanvas.setMarkers(emptyList())
updateSearch(SearchProcessor.fromRegex(editor, pattern, boundaries.intersection(defaultBoundaries)).also { searchProcessor = it })
val processor = SearchProcessor.fromRegex(editor, pattern, boundaries).also { state = SessionStateImpl(editor, tagger, it) }
updateSearch(processor, markImmediately = true)
} }
/** /**
@@ -128,42 +161,21 @@ class Session(private val editor: Editor) {
startRegexSearch(pattern.regex, boundaries) startRegexSearch(pattern.regex, boundaries)
} }
/** fun tagImmediately() {
* See [JumpModeTracker.cycle]. val state = state ?: return
*/ val processor = state.currentProcessor
fun cycleNextJumpMode() {
jumpMode = jumpModeTracker.cycle(forward = true) if (processor != null) {
} updateSearch(processor, markImmediately = 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()
} }
} else if (mode is JumpMode) {
val offset = editor.caretModel.offset
/** val result = editor.immutableText.getOrNull(offset)?.let(state::type)
* See [TagVisitor.visitNext]. If there are no tags, nothing happens. if (result is TypeResult.UpdateResults) {
*/ acceptedTag = offset
fun visitNextTag() { textHighlighter.renderFinal(offset, result.processor.query)
if (tagVisitor?.visitNext() == true) { updateHint()
end() }
} }
} }
@@ -175,13 +187,19 @@ class Session(private val editor: Editor) {
} }
/** /**
* Clears any currently active search, tags, and highlights. Does not reset [JumpMode]. * Clears any currently active search, tags, and highlights.
*/ */
fun restart() { fun restart() {
state = null
tagger = Tagger(editor) tagger = Tagger(editor)
searchProcessor = null acceptedTag = null
tagCanvas.removeMarkers() tagCanvas.removeMarkers()
textHighlighter.reset() textHighlighter.reset()
HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.onTagUnaccepted(editor)
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
editor.contentComponent.repaint()
} }
/** /**
@@ -189,13 +207,14 @@ class Session(private val editor: Editor) {
*/ */
internal fun dispose() { internal fun dispose() {
tagger = Tagger(editor) tagger = Tagger(editor)
EditorKeyListener.detach(editor)
tagCanvas.unbind() tagCanvas.unbind()
textHighlighter.reset() textHighlighter.reset()
EditorKeyListener.detach(editor)
if (!editor.isDisposed) { if (!editor.isDisposed) {
originalSettings.restore(editor) HintManagerImpl.getInstanceImpl().hideAllHints()
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, JumpMode.DISABLED.caretColor) editorSettings.restore(editor)
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
} }
} }

View File

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

View File

@@ -0,0 +1,30 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
internal class SessionStateImpl(override val editor: Editor, private val tagger: Tagger, 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(editor, char, StandardBoundaries.VISIBLE_ON_SCREEN)
return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it })
}
if (processor.type(char, tagger)) {
return TypeResult.UpdateResults(processor)
}
return TypeResult.Nothing
}
override fun act(action: AceTagAction, offset: Int, shiftMode: Boolean) {
currentProcessor?.let { action(editor, it, offset, shiftMode) }
}
}

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()
object RestartSearch : TypeResult()
class UpdateResults(val processor: SearchProcessor) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
object EndSession : TypeResult()
}

View File

@@ -27,6 +27,8 @@ internal class Tag(
private val length = tag.length private val length = tag.length
companion object { companion object {
private const val ARC = 1
/** /**
* 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.
@@ -47,18 +49,27 @@ internal class Tag(
/** /**
* Renders the tag background. * Renders the tag background.
*/ */
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color, arc: Int) { private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color g.color = color
g.fillRoundRect(rect.x, rect.y + 1, rect.width, rect.height - 1, arc, arc) g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
} }
/** /**
* Renders the tag text. * Renders the tag text.
*/ */
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) { private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) {
val x = point.x + 2
val y = point.y + font.baselineDistance
g.font = font.tagFont g.font = font.tagFont
if (!ColorUtil.isDark(AceConfig.tagForegroundColor)) {
g.color = Color(0F, 0F, 0F, 0.35F)
g.drawString(text, x + 1, y + 1)
}
g.color = AceConfig.tagForegroundColor g.color = AceConfig.tagForegroundColor
g.drawString(text, point.x, point.y + font.baselineDistance) g.drawString(text, x, y)
} }
} }
@@ -70,56 +81,36 @@ internal class Tag(
return offsetL in range return offsetL in range
} }
/**
* Determines on which side of the target character the tag is positioned.
*/
enum class TagAlignment {
LEFT,
RIGHT
}
/** /**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags. * Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
* Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap. * Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap.
*/ */
fun paint( fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? {
g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>, isRegex: Boolean val rect = alignTag(editor, cache, font, occupied) ?: return null
): Rectangle? {
val (rect, alignment) = alignTag(editor, cache, font, occupied) ?: return null
val highlightColor = when { drawHighlight(g, rect, AceConfig.tagBackgroundColor)
alignment != TagAlignment.RIGHT || hasSpaceRight || isRegex -> AceConfig.tagBackgroundColor
else -> ColorUtil.darker(AceConfig.tagBackgroundColor, 3)
}
drawHighlight(g, rect, highlightColor, font.tagCornerArc)
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) })
return rect return rect
} }
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Pair<Rectangle, TagAlignment>? { private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) { if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) {
val rectR = createRightAlignedTagRect(editor, cache, font) val rectR = createRightAlignedTagRect(editor, cache, font)
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects) }
return (rectR to TagAlignment.RIGHT).takeIf {
boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects)
}
} }
val rectL = createLeftAlignedTagRect(editor, cache, font) val rectL = createLeftAlignedTagRect(editor, cache, font)
if (occupied.none(rectL::intersects)) { if (occupied.none(rectL::intersects)) {
return (rectL to TagAlignment.LEFT).takeIf { boundaries.isOffsetInside(editor, offsetL, cache) } return rectL.takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
} }
val rectR = createRightAlignedTagRect(editor, cache, font) val rectR = createRightAlignedTagRect(editor, cache, font)
if (occupied.none(rectR::intersects)) { if (occupied.none(rectR::intersects)) {
return (rectR to TagAlignment.RIGHT).takeIf { boundaries.isOffsetInside(editor, offsetR, cache) } return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
} }
return null return null
@@ -128,12 +119,12 @@ internal class Tag(
private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle { private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetR) val pos = cache.offsetToXY(editor, offsetR)
val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR) val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR)
return Rectangle(pos.x + shift, pos.y, font.tagCharWidth * length, font.lineHeight) return Rectangle(pos.x + shift, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
} }
private fun createLeftAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle { private fun createLeftAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetL) val pos = cache.offsetToXY(editor, offsetL)
val shift = -(font.tagCharWidth * length) val shift = -(font.tagCharWidth * length)
return Rectangle(pos.x + shift, pos.y, font.tagCharWidth * length, font.lineHeight) return Rectangle(pos.x + shift - 4, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
} }
} }

View File

@@ -4,10 +4,8 @@ import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.event.CaretListener
import com.intellij.ui.ColorUtil
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 java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.Rectangle import java.awt.Rectangle
@@ -20,7 +18,6 @@ import javax.swing.SwingUtilities
*/ */
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: List<Tag>? = null
private var isRegex = false
init { init {
val contentComponent = editor.contentComponent val contentComponent = editor.contentComponent
@@ -48,9 +45,8 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
repaint() repaint()
} }
fun setMarkers(markers: List<Tag>, isRegex: Boolean) { fun setMarkers(markers: List<Tag>) {
this.markers = markers this.markers = markers
this.isRegex = isRegex
repaint() repaint()
} }
@@ -84,18 +80,12 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
val caretOffset = editor.caretModel.offset val caretOffset = editor.caretModel.offset
val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset } val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset }
val caretRect = caretMarker?.paint(g, editor, cache, font, occupied, isRegex) caretMarker?.paint(g, editor, cache, font, occupied)
for (marker in markers) { for (marker in markers) {
if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) { if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
marker.paint(g, editor, cache, font, occupied, isRegex) marker.paint(g, editor, cache, font, occupied)
} }
} }
if (caretRect != null) {
g.color = ColorUtil.brighter(AceConfig.tagBackgroundColor, 10)
// Only adding 1 to width because it seems the right side of the tag highlight is slightly off.
g.drawRoundRect(caretRect.x - 1, caretRect.y, caretRect.width + 1, caretRect.height, font.tagCornerArc, font.tagCornerArc)
}
} }
} }

View File

@@ -1,8 +1,7 @@
package org.acejump.view; 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 org.acejump.config.AceConfig
import java.awt.Font import java.awt.Font
import java.awt.FontMetrics import java.awt.FontMetrics
@@ -11,8 +10,7 @@ import java.awt.FontMetrics
*/ */
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 tagCornerArc = if (AceConfig.roundedTagCorners) editor.colorsScheme.editorFontSize - 3 else 1
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

View File

@@ -6,45 +6,42 @@ 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 java.awt.Color
import org.acejump.wordStart
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 val editor: Editor) {
private companion object {
private const val LAYER = HighlighterLayer.LAST + 1
private var ARC = 0
}
private var previousHighlights: Array<RangeHighlighter>? = null 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(offsets: IntList, query: SearchQuery) {
render(offsets, when (query) {
is SearchQuery.RegularExpression -> RegexRenderer
else -> SearchedWordRenderer
}, query::getHighlightLength)
}
/**
* Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
*/
fun renderFinal(offset: Int, query: SearchQuery) {
render(IntArrayList(intArrayOf(offset)), AcceptedTagRenderer, query::getHighlightLength)
}
private inline fun render(offsets: IntList, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
val markup = editor.markupModel val markup = editor.markupModel
val chars = editor.immutableText val chars = editor.immutableText
ARC = TagFont(editor).tagCornerArc
val renderer = when {
query is SearchQuery.RegularExpression -> RegexRenderer
jumpMode === JumpMode.TARGET -> SearchedWordWithOutlineRenderer
else -> SearchedWordRenderer
}
val modifications = (previousHighlights?.size ?: 0) + offsets.size val modifications = (previousHighlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000 val enableBulkEditing = modifications > 1000
@@ -58,7 +55,7 @@ internal class TextHighlighter(private val editor: Editor) {
previousHighlights?.forEach(markup::removeHighlighter) previousHighlights?.forEach(markup::removeHighlighter)
previousHighlights = Array(offsets.size) { index -> previousHighlights = 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
@@ -81,40 +78,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.fillRoundRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1, ARC, ARC)
}
}
/**
* 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.drawRoundRect(max(0, start.x - JBUI.scale(1)), start.y, end.x - start.x + JBUI.scale(2), editor.lineHeight, ARC, ARC)
} }
} }
@@ -123,17 +87,44 @@ 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)
}
}
/**
* 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) { 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.fillRoundRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1, ARC, ARC) g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
} }
} }
} }

View File

@@ -23,16 +23,14 @@
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" <editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/> implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
<editorActionHandler action="EditorLeft" order="first" <editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/> implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorLineStart" order="first" <editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/> implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorRight" order="first" <editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/> implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
<editorActionHandler action="EditorLineEnd" order="first" <editorActionHandler action="EditorLineEnd" order="first"
@@ -41,55 +39,36 @@
<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 Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action> </action>
<action id="AceReverseAction"
class="org.acejump.action.AceAction$ActivateOrReverseCycleMode"
text="Activate / Reverse Cycle AceJump Mode"/>
<action id="AceWordStartAction"
class="org.acejump.action.AceAction$ToggleJumpMode"
text="Start AceJump in Jump Mode"/>
<action id="AceWordEndAction"
class="org.acejump.action.AceAction$ToggleJumpEndMode"
text="Start AceJump in Jump End Mode"/>
<action id="AceTargetAction"
class="org.acejump.action.AceAction$ToggleTargetMode"
text="Start AceJump in Target Mode">
<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="AceDeclarationAction"
class="org.acejump.action.AceAction$ToggleDeclarationMode"
text="Start AceJump in Declaration Mode"/>
<action id="AceLineAction" <action id="AceLineAction"
class="org.acejump.action.AceAction$StartAllLineMarksMode" class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode"> 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" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" 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"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
</action> </action>
<action id="AceLineStartsAction" <action id="AceLineStartsAction"
class="org.acejump.action.AceAction$StartAllLineStartsMode" class="org.acejump.action.AceKeyboardAction$StartAllLineStartsMode"
text="Start AceJump in All Line Starts Mode"/> text="Start AceJump in All Line Starts Mode"/>
<action id="AceLineEndsAction" <action id="AceLineEndsAction"
class="org.acejump.action.AceAction$StartAllLineEndsMode" class="org.acejump.action.AceKeyboardAction$StartAllLineEndsMode"
text="Start AceJump in All Line Ends Mode"/> text="Start AceJump in All Line Ends Mode"/>
<action id="AceLineIndentsAction" <action id="AceLineIndentsAction"
class="org.acejump.action.AceAction$StartAllLineIndentsMode" class="org.acejump.action.AceKeyboardAction$StartAllLineIndentsMode"
text="Start AceJump in All Line Indents Mode"/> text="Start AceJump in All Line Indents Mode"/>
<action id="AceWordAction" <action id="AceWordAction"
class="org.acejump.action.AceAction$StartAllWordsMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsMode"
text="Start AceJump in All Words Mode"/> text="Start AceJump in All Words Mode"/>
<action id="AceWordForwardAction" <action id="AceWordForwardAction"
class="org.acejump.action.AceAction$StartAllWordsForwardMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsForwardMode"
text="Start AceJump in All Words After Caret Mode"/> text="Start AceJump in All Words After Caret Mode"/>
<action id="AceWordBackwardsAction" <action id="AceWordBackwardsAction"
class="org.acejump.action.AceAction$StartAllWordsBackwardsMode" class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/> text="Start AceJump in All Words Before Caret Mode"/>
</actions> </actions>
</idea-plugin> </idea-plugin>

View File

@@ -1,6 +1,6 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
import org.acejump.action.AceAction import org.acejump.action.AceKeyboardAction
import org.acejump.test.util.BaseTest import org.acejump.test.util.BaseTest
/** /**
@@ -56,6 +56,7 @@ class AceTest : BaseTest() {
"<caret>testing 1234".search("g") "<caret>testing 1234".search("g")
typeAndWaitForResults(session.tags[0].key) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("j")
myFixture.checkResult("testin<caret>g 1234") myFixture.checkResult("testin<caret>g 1234")
} }
@@ -63,7 +64,8 @@ class AceTest : BaseTest() {
fun `test shift selection`() { fun `test shift selection`() {
"<caret>testing 1234".search("4") "<caret>testing 1234".search("4")
typeAndWaitForResults(session.tags[0].key.toUpperCase()) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("J")
myFixture.checkResult("<selection>testing 123<caret></selection>4") myFixture.checkResult("<selection>testing 123<caret></selection>4")
} }
@@ -71,7 +73,7 @@ class AceTest : BaseTest() {
fun `test words before caret action`() { fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two") makeEditor("test words <caret> before caret is two")
takeAction(AceAction.StartAllWordsBackwardsMode) takeAction(AceKeyboardAction.StartAllWordsBackwardsMode)
assertEquals(2, session.tags.size) assertEquals(2, session.tags.size)
} }
@@ -79,7 +81,7 @@ class AceTest : BaseTest() {
fun `test words after caret action`() { fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four") makeEditor("test words <caret> after caret is four")
takeAction(AceAction.StartAllWordsForwardMode) takeAction(AceKeyboardAction.StartAllWordsForwardMode)
assertEquals(4, session.tags.size) assertEquals(4, session.tags.size)
} }
@@ -87,11 +89,12 @@ class AceTest : BaseTest() {
fun `test word mode`() { fun `test word mode`() {
makeEditor("test word action") makeEditor("test word action")
takeAction(AceAction.StartAllWordsMode) takeAction(AceKeyboardAction.StartAllWordsMode)
assertEquals(3, session.tags.size) assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key) typeAndWaitForResults(session.tags[1].key)
typeAndWaitForResults("j")
myFixture.checkResult("test <caret>word action") myFixture.checkResult("test <caret>word action")
} }
@@ -99,8 +102,8 @@ class AceTest : BaseTest() {
fun `test target mode`() { fun `test target mode`() {
"<caret>test target action".search("target") "<caret>test target action".search("target")
takeAction(AceAction.ToggleTargetMode)
typeAndWaitForResults(session.tags[0].key) typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("s")
myFixture.checkResult("test <selection>target<caret></selection> action") myFixture.checkResult("test <selection>target<caret></selection> action")
} }
@@ -108,7 +111,7 @@ class AceTest : BaseTest() {
fun `test line mode`() { fun `test line mode`() {
makeEditor(" test\n three\n lines\n") makeEditor(" test\n three\n lines\n")
takeAction(AceAction.StartAllLineMarksMode) takeAction(AceKeyboardAction.StartAllLineMarksMode)
assertEquals(9, session.tags.size) assertEquals(9, session.tags.size)
} }

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()
@@ -42,4 +42,4 @@ class LatencyTest : BaseTest() {
javaClass.classLoader.getResource("lipsum.txt")!!.file javaClass.classLoader.getResource("lipsum.txt")!!.file
).readText() ).readText()
) )
} }

View File

@@ -6,7 +6,7 @@ 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() {
@@ -55,7 +55,7 @@ abstract class BaseTest : BasePlatformTestCase() {
fun String.executeQuery(query: String) { 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)
} }
} }