1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2025-05-31 09:34:06 +02:00

[WIP] Add interactive modes for selection and deletion

This commit is contained in:
chylex 2020-12-21 06:03:54 +01:00
parent 402517c412
commit 658c2062a7
15 changed files with 276 additions and 95 deletions

View File

@ -5,10 +5,10 @@ import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries
import org.acejump.interact.VisitDirection
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
import org.acejump.session.mode.VisitDirection
/**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].

View File

@ -5,11 +5,11 @@ 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.interact.mode.DefaultMode
import org.acejump.interact.mode.MultiCaretMode
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
import org.acejump.session.mode.MultiCaretSessionMode
import org.acejump.session.mode.SingleCaretSessionMode
/**
* Base class for keyboard-activated actions that create or update an AceJump [Session].
@ -36,14 +36,14 @@ sealed class AceKeyboardAction : DumbAwareAction() {
* Starts or ends an AceJump session.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.setMode(SingleCaretSessionMode())
override fun invoke(session: Session) = session.toggleMode(DefaultMode)
}
/**
* Starts or ends an AceJump session in multicaret mode.
*/
object ActivateAceJumpMultiCaret : AceKeyboardAction() {
override fun invoke(session: Session) = session.setMode(MultiCaretSessionMode())
override fun invoke(session: Session) = session.toggleMode(MultiCaretMode())
}
// @formatter:off

View File

@ -7,8 +7,10 @@ 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.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
@ -196,6 +198,89 @@ internal sealed class AceTagAction {
}
}
object SelectQuery : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
recordCaretPosition(editor)
val startOffset = JumpToSearchStart.getCaretOffset(editor, searchProcessor, offset)
val endOffset = JumpPastSearchEnd.getCaretOffset(editor, searchProcessor, offset)
selectRange(editor, startOffset, endOffset)
}
}
object SelectWord : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = JumpToWordStartTag.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val endOffset = JumpToWordEndTag.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode)
}
}
}
object SelectHump : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
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)
}
}
}
object SelectLine : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
editor.selectionModel.selectLineAtCaret()
}
}
class SelectExtended(private val extendCount: Int) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false)
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
repeat(extendCount) {
performAction(action)
}
}
}
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)
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
}
}
}
/**
* On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
* On shift action, performs the Go To Type Declaration action, available via `Navigate | Type Declaration`.

View File

@ -1,10 +1,12 @@
package org.acejump.session.mode
package org.acejump.interact
import org.acejump.session.SessionMode
import org.acejump.session.SessionSnapshot
sealed class TypeResult {
object Nothing : TypeResult()
object UpdateResults : TypeResult()
class LoadSnapshot(val snapshot: SessionSnapshot) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
object EndSession : TypeResult()
}

View File

@ -1,4 +1,4 @@
package org.acejump.session.mode
package org.acejump.interact
enum class VisitDirection {
BACKWARD,

View File

@ -1,4 +1,4 @@
package org.acejump.session.mode
package org.acejump.interact
sealed class VisitResult {
object Nothing : VisitResult()

View File

@ -0,0 +1,53 @@
package org.acejump.interact.mode
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.action.TagVisitor
import org.acejump.interact.TypeResult
import org.acejump.interact.VisitDirection
import org.acejump.interact.VisitResult
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
import org.acejump.session.SessionMode
internal abstract class AbstractNavigableMode : SessionMode {
abstract val actionMap: Map<Char, AceTagAction>
abstract val modeMap: Map<Char, () -> SessionMode>
override fun type(editor: Editor, processor: SearchProcessor, tagger: Tagger, charTyped: Char, acceptedTag: Int?): TypeResult {
if (acceptedTag != null) {
val action = actionMap[charTyped.toUpperCase()]
if (action != null) {
action(editor, processor, acceptedTag, charTyped.isUpperCase())
return TypeResult.EndSession
}
val mode = modeMap[charTyped.toUpperCase()]
if (mode != null) {
return TypeResult.ChangeMode(mode())
}
return TypeResult.Nothing
}
if (processor.type(charTyped, tagger)) {
return TypeResult.UpdateResults
}
return TypeResult.Nothing
}
override fun visit(editor: Editor, processor: SearchProcessor, direction: VisitDirection, acceptedTag: Int?): VisitResult {
if (acceptedTag != null) {
AceTagAction.JumpToSearchStart(editor, processor, acceptedTag, shiftMode = false)
return VisitResult.EndSession
}
val onlyTagOffset = when (direction) {
VisitDirection.BACKWARD -> TagVisitor(editor, processor).visitPrevious()
VisitDirection.FORWARD -> TagVisitor(editor, processor).visitNext()
}
return onlyTagOffset?.let(VisitResult::SetAcceptedTag) ?: VisitResult.Nothing
}
}

View File

@ -0,0 +1,33 @@
package org.acejump.interact.mode
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
internal object DefaultMode : AbstractNavigableMode() {
override val actionMap = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'K' to AceTagAction.JumpToSearchEnd,
'L' to AceTagAction.JumpPastSearchEnd,
'W' to AceTagAction.JumpToWordStartTag,
'E' to AceTagAction.JumpToWordEndTag,
'B' to AceTagAction.GoToDeclaration,
'U' to AceTagAction.ShowUsages,
'I' to AceTagAction.ShowIntentions,
'R' to AceTagAction.Refactor
)
override val modeMap = mapOf(
'S' to { SelectMode },
'D' to { DeleteMode }
)
override val caretColor
get() = AceConfig.singleCaretModeColor
override val actionHint = arrayOf(
"<f>[J]</f>ump to Tag / <f>[K]</f> to Query / <f>[L]</f> past Query",
"<f>[W]</f>ord Start / Word <f>[E]</f>nd",
"<f>[S]</f>elect... / <f>[D]</f>elete...",
"<f>[B]</f> Declaration / <f>[U]</f>sages / <f>[I]</f>ntentions / <f>[R]</f>efactor"
)
}

View File

@ -0,0 +1,17 @@
package org.acejump.interact.mode
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
internal object DeleteMode : AbstractNavigableMode() {
override val actionMap = SelectMode.actionMap.mapValues { AceTagAction.Delete(it.value) }
override val modeMap
get() = SelectMode.modeMap
override val caretColor
get() = AceConfig.singleCaretModeColor
override val actionHint
get() = SelectMode.actionHint
}

View File

@ -1,13 +1,16 @@
package org.acejump.session.mode
package org.acejump.interact.mode
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.interact.TypeResult
import org.acejump.interact.VisitDirection
import org.acejump.interact.VisitResult
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
import org.acejump.session.SessionMode
import org.acejump.session.SessionSnapshot
internal class MultiCaretSessionMode : SessionMode {
internal class MultiCaretMode : SessionMode {
private companion object {
private val actionMap = mapOf(
'J' to AceTagAction.JumpToSearchStart,

View File

@ -0,0 +1,27 @@
package org.acejump.interact.mode
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionMode
internal object SelectMode : AbstractNavigableMode() {
override val actionMap = mapOf(
'Q' to AceTagAction.SelectQuery,
'W' to AceTagAction.SelectWord,
'H' to AceTagAction.SelectHump,
'L' to AceTagAction.SelectLine,
*('1'..'9').mapIndexed { index, char -> char to AceTagAction.SelectExtended(index + 1) }.toTypedArray()
)
override val modeMap
get() = emptyMap<Char, () -> SessionMode>()
override val caretColor
get() = AceConfig.singleCaretModeColor
override val actionHint = arrayOf(
"<f>[Q]</f>uery / <f>[L]</f>ine",
"<f>[W]</f>ord / <f>[H]</f>ump",
"<f>[1-9] Extend Selection</f>"
)
}

View File

@ -26,6 +26,14 @@ 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
}

View File

@ -18,11 +18,11 @@ import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.input.EditorKeyListener
import org.acejump.input.KeyLayoutCache
import org.acejump.interact.TypeResult
import org.acejump.interact.VisitDirection
import org.acejump.interact.VisitResult
import org.acejump.interact.mode.DefaultMode
import org.acejump.search.*
import org.acejump.session.mode.SingleCaretSessionMode
import org.acejump.session.mode.TypeResult
import org.acejump.session.mode.VisitDirection
import org.acejump.session.mode.VisitResult
import org.acejump.view.TagCanvas
import org.acejump.view.TextHighlighter
@ -53,17 +53,7 @@ class Session(private val editor: Editor) {
if (processor != null) {
textHighlighter.renderFinal(value, processor.query)
val hintText = mode.actionHint
.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 = HintManagerImpl.getHintPosition(hint, editor, editor.offsetToLogicalPosition(value), HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, pos, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, pos, flags, 0, true, info)
setMode(DefaultMode)
}
}
}
@ -88,11 +78,18 @@ class Session(private val editor: Editor) {
if (processor == null) {
processor = SearchProcessor.fromChar(editor, charTyped, defaultBoundaries).also { searchProcessor = it }
updateSearch(processor)
return
}
else when (val result = mode.type(editor, processor, tagger, charTyped, acceptedTag)) {
editorSettings.startEditing(editor)
val result = mode.type(editor, processor, tagger, charTyped, acceptedTag)
editorSettings.stopEditing(editor)
when (result) {
TypeResult.UpdateResults -> updateSearch(processor)
TypeResult.EndSession -> end()
is TypeResult.LoadSnapshot -> loadSnapshot(result.snapshot)
is TypeResult.ChangeMode -> setMode(result.mode)
else -> return
}
}
@ -136,13 +133,31 @@ class Session(private val editor: Editor) {
searchProcessor = snapshot.savedProcessor?.also(::updateSearch)
}
private fun setMode(mode: SessionMode) {
this.mode = mode
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
val acceptedTag = acceptedTag
if (acceptedTag != null) {
val hintText = mode.actionHint
.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 = HintManagerImpl.getHintPosition(hint, editor, editor.offsetToLogicalPosition(acceptedTag), HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, pos, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, pos, flags, 0, true, info)
}
}
/**
* Sets AceJump mode or ends the session if the mode is the same.
*/
fun setMode(mode: SessionMode) {
fun toggleMode(mode: SessionMode) {
if (!this::mode.isInitialized) {
this.mode = mode
editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
setMode(mode)
}
else if (!this.mode.isSame(mode)) {
this.mode = mode
@ -157,7 +172,7 @@ class Session(private val editor: Editor) {
* 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) {
setMode(SingleCaretSessionMode())
toggleMode(DefaultMode)
tagger = Tagger(editor)
tagCanvas.setMarkers(emptyList(), isRegex = true)
updateSearch(SearchProcessor.fromRegex(editor, pattern, boundaries.intersection(defaultBoundaries)).also { searchProcessor = it })

View File

@ -1,11 +1,11 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.interact.TypeResult
import org.acejump.interact.VisitDirection
import org.acejump.interact.VisitResult
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
import org.acejump.session.mode.TypeResult
import org.acejump.session.mode.VisitDirection
import org.acejump.session.mode.VisitResult
import java.awt.Color
interface SessionMode {

View File

@ -1,62 +0,0 @@
package org.acejump.session.mode
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.action.TagVisitor
import org.acejump.config.AceConfig
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
import org.acejump.session.SessionMode
internal class SingleCaretSessionMode : SessionMode {
private companion object {
private val actionMap = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'K' to AceTagAction.JumpToSearchEnd,
'L' to AceTagAction.JumpPastSearchEnd,
'W' to AceTagAction.JumpToWordStartTag,
'E' to AceTagAction.JumpToWordEndTag,
'S' to AceTagAction.SelectWordOrHump,
'D' to AceTagAction.GoToDeclaration,
'U' to AceTagAction.ShowUsages,
'I' to AceTagAction.ShowIntentions,
'R' to AceTagAction.Refactor
)
}
override val caretColor
get() = AceConfig.singleCaretModeColor
override val actionHint = arrayOf(
"<f>[J]</f>ump to Tag / <f>[K]</f> to Query / <f>[L]</f> past Query",
"<f>[W]</f>ord Start / Word <f>[E]</f>nd / <f>[S]</f>elect Word",
"<f>[D]</f>eclaration / <f>[U]</f>sages / <f>[I]</f>ntentions / <f>[R]</f>efactor"
)
override fun type(editor: Editor, processor: SearchProcessor, tagger: Tagger, charTyped: Char, acceptedTag: Int?): TypeResult {
if (acceptedTag != null) {
val action = actionMap[charTyped.toUpperCase()] ?: return TypeResult.Nothing
action(editor, processor, acceptedTag, charTyped.isUpperCase())
return TypeResult.EndSession
}
else if (processor.type(charTyped, tagger)) {
return TypeResult.UpdateResults
}
return TypeResult.Nothing
}
override fun visit(editor: Editor, processor: SearchProcessor, direction: VisitDirection, acceptedTag: Int?): VisitResult {
if (acceptedTag != null) {
AceTagAction.JumpToSearchStart(editor, processor, acceptedTag, shiftMode = false)
return VisitResult.EndSession
}
val onlyTagOffset = when (direction) {
VisitDirection.BACKWARD -> TagVisitor(editor, processor).visitPrevious()
VisitDirection.FORWARD -> TagVisitor(editor, processor).visitNext()
}
return onlyTagOffset?.let(VisitResult::SetAcceptedTag) ?: VisitResult.Nothing
}
}