Compare commits

...
14 Commits
27 changed files with 324 additions and 42 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ ideaVersion=2026.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IU ideaType=IU
instrumentPluginCode=true instrumentPluginCode=true
version=chylex-1 version=9999.59-chylex
javaVersion=21 javaVersion=21
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1
@@ -0,0 +1,67 @@
package com.maddyhome.idea.vim.action.macro
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.impl.FinishMarkAction
import com.intellij.openapi.command.impl.StartMarkAction
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
@CommandOrMotion(keys = ["z@"], modes = [Mode.NORMAL])
class PlaybackRegisterInOpenFilesAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override val argumentType: Argument.Type = Argument.Type.CHARACTER
private val playbackRegisterAction = PlaybackRegisterAction()
override fun execute(
editor: VimEditor,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
val argument = cmd.argument as? Argument.Character ?: return false
val project = editor.ij.project ?: return false
val fileEditorManager = FileEditorManagerEx.getInstanceExIfCreated(project) ?: return false
val register = argument.character.let { if (it == '@') injector.macro.lastRegister else it }
val commandName = "Execute Macro '$register' in All Open Files"
val action = Runnable {
CommandProcessor.getInstance().markCurrentCommandAsGlobal(project)
for (textEditor in fileEditorManager.allEditors.filterIsInstance<TextEditor>()) {
fileEditorManager.openFile(textEditor.file, true)
val editor = textEditor.editor
val vimEditor = editor.vim
vimEditor.mode = com.maddyhome.idea.vim.state.mode.Mode.NORMAL()
KeyHandler.Companion.getInstance().reset(vimEditor)
val startMarkAction = StartMarkAction.start(editor, project, commandName)
playbackRegisterAction.execute(vimEditor, context, cmd, operatorArguments)
FinishMarkAction.finish(project, editor, startMarkAction)
}
}
CommandProcessor.getInstance()
.executeCommand(project, action, commandName, null, UndoConfirmationPolicy.REQUEST_CONFIRMATION)
return true
}
}
@@ -24,12 +24,11 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimChangeGroupBase import com.maddyhome.idea.vim.api.VimChangeGroupBase
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.VirtualBufferKind
import com.maddyhome.idea.vim.api.anyNonWhitespace import com.maddyhome.idea.vim.api.anyNonWhitespace
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
import com.maddyhome.idea.vim.api.getVisualLineCount import com.maddyhome.idea.vim.api.getVisualLineCount
import com.maddyhome.idea.vim.api.VirtualBufferKind
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.CmdwinKeys
import com.maddyhome.idea.vim.api.lineLength import com.maddyhome.idea.vim.api.lineLength
import com.maddyhome.idea.vim.api.normalizeOffset import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.api.normalizeVisualLine import com.maddyhome.idea.vim.api.normalizeVisualLine
@@ -44,6 +43,7 @@ import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import com.maddyhome.idea.vim.helper.CmdwinKeys
import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.getNormalizedScrollOffset import com.maddyhome.idea.vim.helper.getNormalizedScrollOffset
@@ -126,6 +126,9 @@ class MotionGroup : VimMotionGroupBase() {
} }
override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion { override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineStart)
}
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, false) return moveCaretToColumn(editor, caret, col, false)
} }
@@ -134,6 +137,15 @@ class MotionGroup : VimMotionGroupBase() {
editor: VimEditor, editor: VimEditor,
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int { ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
val offset = caret.ij.visualLineStart
val line = editor.offsetToBufferPosition(offset).line
return if (offset == editor.getLineStartOffset(line)) {
editor.getLeadingCharacterOffset(line, 0)
} else {
offset
}
}
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
val bufferLine = caret.getLine() val bufferLine = caret.getLine()
return editor.getLeadingCharacterOffset(bufferLine, col) return editor.getLeadingCharacterOffset(bufferLine, col)
@@ -144,6 +156,9 @@ class MotionGroup : VimMotionGroupBase() {
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
allowEnd: Boolean, allowEnd: Boolean,
): Motion { ): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineEnd - 1)
}
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, allowEnd) return moveCaretToColumn(editor, caret, col, allowEnd)
} }
@@ -9,7 +9,10 @@
package com.maddyhome.idea.vim.helper package com.maddyhome.idea.vim.helper
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.KeyStrokeAdapter import com.intellij.ui.KeyStrokeAdapter
import java.awt.event.InputEvent
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@@ -24,12 +27,30 @@ internal class ActionEventKeyStrokeExtractor {
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent) val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
val strokeCache = keyStrokeCache val strokeCache = keyStrokeCache
if (defaultKeyStroke != null) { if (defaultKeyStroke != null) {
keyStrokeCache = inputEvent.`when` to defaultKeyStroke val fixedKeyStroke = fixKeyStroke(defaultKeyStroke)
return defaultKeyStroke keyStrokeCache = inputEvent.`when` to fixedKeyStroke
return fixedKeyStroke
} else if (strokeCache.first == inputEvent.`when`) { } else if (strokeCache.first == inputEvent.`when`) {
keyStrokeCache = null to null keyStrokeCache = null to null
return strokeCache.second return strokeCache.second
} }
return KeyStroke.getKeyStrokeForEvent(inputEvent) return KeyStroke.getKeyStrokeForEvent(inputEvent)
} }
private fun fixKeyStroke(key: KeyStroke): KeyStroke {
return if (
key.modifiers and CTRL_ALT_MASK != 0 &&
key.isOnKeyRelease &&
SystemInfoRt.isWindows &&
Registry.`is`("actionSystem.fix.alt.gr", true)
) {
KeyStroke.getKeyStroke(key.keyCode, key.modifiers)
} else {
key
}
}
companion object {
private const val CTRL_ALT_MASK = InputEvent.CTRL_DOWN_MASK or InputEvent.ALT_DOWN_MASK
}
} }
@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.ui.popup.util.PopupUtil
import com.intellij.util.ui.table.JBTableRowEditor import com.intellij.util.ui.table.JBTableRowEditor
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.StringListOptionValue import com.maddyhome.idea.vim.api.StringListOptionValue
@@ -71,6 +72,10 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
* Enter and arrow keys (see KeyGroup and ToolWindowNavEverywhere). * Enter and arrow keys (see KeyGroup and ToolWindowNavEverywhere).
*/ */
private fun Editor.isAllowedFileEditor(): Boolean { private fun Editor.isAllowedFileEditor(): Boolean {
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) {
return PopupUtil.getPopupContainerFor(component) == null
}
return EditorHelper.isCommitWindowEditor(this) return EditorHelper.isCommitWindowEditor(this)
|| EditorHelper.isKotlinClassDecompiledToJavaFile(this) || EditorHelper.isKotlinClassDecompiledToJavaFile(this)
|| EditorHelper.isDiffEditor(this) || EditorHelper.isDiffEditor(this)
@@ -24,6 +24,7 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.progress.util.ProgressIndicatorUtils import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.refactoring.actions.BaseRefactoringAction
import com.maddyhome.idea.vim.RegisterActions import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction import com.maddyhome.idea.vim.api.NativeAction
@@ -35,6 +36,8 @@ import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.newapi.IjNativeAction import com.maddyhome.idea.vim.newapi.IjNativeAction
import com.maddyhome.idea.vim.newapi.IjVimDocument import com.maddyhome.idea.vim.newapi.IjVimDocument
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.awt.Component import java.awt.Component
import javax.swing.JComponent import javax.swing.JComponent
@@ -70,15 +73,35 @@ class IjActionExecutor : VimActionExecutor {
thisLogger().error("Actions cannot be updated when write-action is running or pending") thisLogger().error("Actions cannot be updated when write-action is running or pending")
} }
return executeAction((action as IjNativeAction).action, editor?.ij?.contentComponent) return executeAction(editor, (action as IjNativeAction).action, editor?.ij?.contentComponent)
} }
private fun executeAction(ijAction: AnAction, contextComponent: Component?): Boolean { private fun executeAction(editor: VimEditor?, ijAction: AnAction, contextComponent: Component?): Boolean {
val startVisualModeType = (editor?.mode as? Mode.VISUAL)?.selectionType
val startVisualCaretSelection = if (editor != null && startVisualModeType != null && ijAction !is BaseRefactoringAction)
editor.primaryCaret().let { Triple(it.offset, it.selectionStart, it.selectionEnd) }
else
null
try { try {
isRunningActionFromVim = true isRunningActionFromVim = true
val place = ijAction.choosePlace() val place = ijAction.choosePlace()
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true) val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
res.waitFor(5_000) res.waitFor(5_000)
if (startVisualModeType != null && startVisualCaretSelection != null) {
val primaryCaret = editor.primaryCaret()
val endVisualCaretOffset = primaryCaret.offset
if (startVisualCaretSelection.first != endVisualCaretOffset) {
if (!editor.inVisualMode || (editor.mode as Mode.VISUAL).selectionType != startVisualModeType) {
injector.visualMotionGroup.toggleVisual(editor, 1, 0, startVisualModeType)
}
primaryCaret.moveToOffset(startVisualCaretSelection.first)
primaryCaret.setSelection(startVisualCaretSelection.second, startVisualCaretSelection.third)
primaryCaret.moveToOffset(endVisualCaretOffset)
}
}
return res.isDone return res.isDone
} finally { } finally {
isRunningActionFromVim = false isRunningActionFromVim = false
@@ -88,7 +111,7 @@ class IjActionExecutor : VimActionExecutor {
override fun executeAction(editor: VimEditor?, action: NativeAction, context: ExecutionContext): Boolean { override fun executeAction(editor: VimEditor?, action: NativeAction, context: ExecutionContext): Boolean {
if (editor != null && editor === injector.fallbackWindow) { if (editor != null && editor === injector.fallbackWindow) {
val contextComponent = context.ij.getData(PlatformDataKeys.CONTEXT_COMPONENT) val contextComponent = context.ij.getData(PlatformDataKeys.CONTEXT_COMPONENT)
return executeAction((action as IjNativeAction).action, contextComponent) return executeAction(editor, (action as IjNativeAction).action, contextComponent)
} }
return executeAction(editor, action) return executeAction(editor, action)
} }
@@ -198,6 +198,11 @@ private fun updateSearchHighlights(
currentMatchOffset, currentMatchOffset,
editor editor
) )
if (!isIncsearch) {
removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart()
}
} }
return currentEditorCurrentMatchOffset return currentEditorCurrentMatchOffset
@@ -512,3 +517,9 @@ private fun shouldAddCurrentMatchSearchHighlight(pattern: String?, hlSearch: Boo
*/ */
@Contract(pure = true) @Contract(pure = true)
private fun isIncrementalSearchHighlights(initialOffset: Int) = initialOffset != -1 private fun isIncrementalSearchHighlights(initialOffset: Int) = initialOffset != -1
val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = javax.swing.Timer(450) {
removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear()
}
@@ -18,6 +18,7 @@ import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
import com.maddyhome.idea.vim.api.CaretRegisterStorageBase import com.maddyhome.idea.vim.api.CaretRegisterStorageBase
import com.maddyhome.idea.vim.api.LocalMarkStorage import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo import com.maddyhome.idea.vim.api.SelectionInfo
@@ -97,7 +98,7 @@ var Caret.vimInsertStart: RangeMarker by userDataOr {
} }
// TODO: Data could be lost during visual block motion // TODO: Data could be lost during visual block motion
var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor() var Caret.registerStorage: CaretRegisterStorageBase? by GlobalCaretData.userData()
var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor() var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor() var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
@@ -144,6 +145,39 @@ fun <T> userData(): ReadWriteProperty<UserDataHolder, T?> =
} }
} }
object GlobalCaretData {
private val holders = mutableListOf<UserDataHolderBase>()
private fun getHolder(caretIndex: Int): UserDataHolderBase {
while (holders.lastIndex < caretIndex) {
holders.add(UserDataHolderBase())
}
return holders[caretIndex]
}
fun <T> userData(): ReadWriteProperty<Caret, T?> = object : UserDataReadWriteProperty<Caret, T?>() {
override fun getValue(thisRef: Caret, property: KProperty<*>): T? {
return getCaretData(thisRef).getUserData(getKey(property))
}
override fun setValue(thisRef: Caret, property: KProperty<*>, value: T?) {
getCaretData(thisRef).putUserData(getKey(property), value)
}
private fun getCaretData(caret: Caret): UserDataHolder {
if (caret == caret.caretModel.primaryCaret) {
return caret.editor
}
val caretIndex = caret.caretModel.allCarets.indexOf(caret)
return if (caretIndex == -1)
caret.editor
else
getHolder(caretIndex)
}
}
}
/** /**
* Function for delegated properties. * Function for delegated properties.
* The property will be saved to caret if this caret is not primary * The property will be saved to caret if this caret is not primary
@@ -19,6 +19,7 @@ import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateImpl import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TemplateManagerImpl import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction import com.intellij.ide.actions.ApplyIntentionAction
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
@@ -52,7 +53,6 @@ import com.maddyhome.idea.vim.group.NotificationService
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
import com.maddyhome.idea.vim.helper.exitSelectMode import com.maddyhome.idea.vim.helper.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.hasVisualSelection
import com.maddyhome.idea.vim.helper.isEndAllowed import com.maddyhome.idea.vim.helper.isEndAllowed
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.helper.vimInitialised import com.maddyhome.idea.vim.helper.vimInitialised
@@ -211,6 +211,10 @@ internal object IdeaSpecifics {
KeyHandler.getInstance().reset(it.vim) KeyHandler.getInstance().reset(it.vim)
} }
} }
else if (action is NextVariableAction && TemplateManagerImpl.getTemplateState(editor) == null) {
editor.vim.exitInsertMode(event.dataContext.vim)
KeyHandler.getInstance().reset(editor.vim)
}
//endregion //endregion
if (caretOffset != -1 && caretOffset != editor.caretModel.offset) { if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {
@@ -413,23 +417,6 @@ internal object IdeaSpecifics {
vimEditor.exitMode() vimEditor.exitMode()
vimEditor.mode = Mode.NORMAL() vimEditor.mode = Mode.NORMAL()
} }
} else {
// IdeaSelectionControl will not be called if we're moving to a new variable with no change in selection.
// And if we're moving to the end of the template, the change in selection will reset us to Normal because
// IdeaSelectionControl will be called when the template is no longer active.
if ((!editor.selectionModel.hasSelection() && !vimEditor.mode.hasVisualSelection) || newIndex == -1) {
if (vimEditor.isIdeaRefactorModeSelect) {
if (vimEditor.mode !is Mode.INSERT) {
vimEditor.exitMode()
injector.application.runReadAction {
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
VimPlugin.getChange().insertBeforeCaret(editor.vim, context)
}
}
} else {
vimEditor.mode = Mode.NORMAL()
}
}
} }
} }
} }
@@ -405,16 +405,18 @@ object VimListenerManager {
editorGroup?.editorDeinit(editor.vim) editorGroup?.editorDeinit(editor.vim)
} }
} }
ApplicationManager.getApplication().invokeLater {
if (vimDisabled(editor)) {
remove(editor)
}
}
} }
fun remove(editor: Editor) { fun remove(editor: Editor) {
val editorDisposable = editor.removeUserData(editorListenersDisposableKey) val editorDisposable = editor.removeUserData(editorListenersDisposableKey)
if (editorDisposable != null) { if (editorDisposable != null) {
Disposer.dispose(editorDisposable) Disposer.dispose(editorDisposable)
} else if (!EditorHelper.isCommandHistoryWindow(editor)) {
// Cmdwin editors (q:, q/, q?) may bypass `add` in some test paths; that's expected.
// We definitely do not expect this to happen for any other editor.
StrictMode.fail("Editor doesn't have disposable attached. $editor")
} }
} }
} }
@@ -529,7 +531,7 @@ object VimListenerManager {
MotionGroup.fileEditorManagerSelectionChangedCallback(event) MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event) FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event) // VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event) IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor) VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
} }
@@ -657,6 +657,10 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
get() = ijFoldRegion.endOffset get() = ijFoldRegion.endOffset
} }
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return editor.softWrapModel.getSoftWrap(offset)?.start
}
override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T { override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T {
return caret return caret
} }
@@ -0,0 +1,43 @@
package com.maddyhome.idea.vim.vimscript.model.functions.handlers
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.impl.PresentationFactory
import com.intellij.openapi.actionSystem.impl.Utils
import com.intellij.openapi.keymap.impl.ActionProcessor
import com.intellij.vim.annotations.VimscriptFunction
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.vimscript.model.VimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
import com.maddyhome.idea.vim.vimscript.model.datatypes.asVimInt
import com.maddyhome.idea.vim.vimscript.model.functions.BuiltinFunctionHandler
import java.awt.event.KeyEvent
@VimscriptFunction(name = "isactionenabled")
internal class IsActionEnabled : BuiltinFunctionHandler<VimInt>() {
override fun doFunction(
arguments: Arguments,
editor: VimEditor,
context: ExecutionContext,
vimContext: VimLContext,
): VimInt {
val action = ActionManager.getInstance().getAction(arguments.getString(0).value)
if (action == null) {
return false.asVimInt()
}
val presentationFactory = PresentationFactory()
val wrappedContext = Utils.createAsyncDataContext(context.ij)
val actionProcessor = object : ActionProcessor() {}
val inputEventAdjusted = KeyEvent(editor.ij.contentComponent, KeyEvent.KEY_PRESSED, 0L, 0, KeyEvent.VK_UNDEFINED, '\u0000')
val updateEvent = Utils.runUpdateSessionForInputEvent(listOf(action), inputEventAdjusted, wrappedContext, "IdeaVim", actionProcessor, presentationFactory) { _, updater, events ->
val presentation = updater(action)
events[presentation]
}
val result = updateEvent != null && updateEvent.presentation.isEnabled
return result.asVimInt()
}
}
@@ -118,5 +118,10 @@
"keys": "gJ", "keys": "gJ",
"class": "com.maddyhome.idea.vim.action.change.delete.DeleteJoinVisualLinesAction", "class": "com.maddyhome.idea.vim.action.change.delete.DeleteJoinVisualLinesAction",
"modes": "X" "modes": "X"
},
{
"keys": "z@",
"class": "com.maddyhome.idea.vim.action.macro.PlaybackRegisterInOpenFilesAction",
"modes": "N"
} }
] ]
@@ -1,4 +1,5 @@
{ {
"has": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler", "has": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler",
"isactionenabled": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.IsActionEnabled",
"pumvisible": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.PopupMenuVisibleFunctionHandler" "pumvisible": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.PopupMenuVisibleFunctionHandler"
} }
@@ -115,8 +115,7 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
val offsetAndMotion = injector.searchGroup.processSearchCommand( val offsetAndMotion = injector.searchGroup.processSearchCommand(
editor, argument.string, caret.offset, count, direction editor, argument.string, caret.offset, count, direction
) )
// Vim doesn't treat not finding something as an error, although it might report either an error or warning message if (offsetAndMotion == null) return Motion.Error
if (offsetAndMotion == null) return Motion.NoMotion
parentAction.motionType = offsetAndMotion.second parentAction.motionType = offsetAndMotion.second
return offsetAndMotion.first.toMotionOrError() return offsetAndMotion.first.toMotionOrError()
} }
@@ -31,6 +31,12 @@ class MotionCamelLeftAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (caret.hasSelection() && caret.offset > caret.vimSelectionStart) {
val target = injector.searchHelper.findPreviousCamelEnd(editor.text(), caret.offset, operatorArguments.count1)
if (target != null && target > caret.vimSelectionStart) {
return target.toMotionOrError()
}
}
return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1) return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error ?.toMotionOrError() ?: Motion.Error
} }
@@ -47,6 +53,10 @@ class MotionCamelRightAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (caret.hasSelection() && caret.offset >= caret.vimSelectionStart) {
return injector.searchHelper.findNextCamelEnd(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error
}
return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1) return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error ?.toMotionOrError() ?: Motion.Error
} }
@@ -70,6 +70,6 @@ class MotionDownNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1) return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1, bufferLines = true)
} }
} }
@@ -70,6 +70,6 @@ class MotionUpNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1) return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1, bufferLines = true)
} }
} }
@@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.register.RegisterConstants import com.maddyhome.idea.vim.register.RegisterConstants
import com.maddyhome.idea.vim.register.VimRegisterGroupBase import com.maddyhome.idea.vim.register.VimRegisterGroupBase
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.yank.MultiCaretJoinedText
import javax.swing.KeyStroke import javax.swing.KeyStroke
abstract class VimCaretBase : VimCaret abstract class VimCaretBase : VimCaret
@@ -67,6 +68,11 @@ open class CaretRegisterStorageBase(override var caret: ImmutableVimCaret) : Car
} }
override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? { override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
val multiCaretJoinRegister = injector.registerGroup.getRegister(editor, context, RegisterConstants.MULTICARET_JOIN_REGISTER)
val multiCaretJoinedText = multiCaretJoinRegister?.copiedText as? MultiCaretJoinedText
if (multiCaretJoinedText != null && multiCaretJoinedText.carets.let { it > 0 && it != editor.carets().size }) {
return multiCaretJoinRegister
}
if (caret.isPrimary || !RegisterConstants.RECORDABLE_REGISTERS.contains(r)) { if (caret.isPrimary || !RegisterConstants.RECORDABLE_REGISTERS.contains(r)) {
return injector.registerGroup.getRegister(editor, context, r) return injector.registerGroup.getRegister(editor, context, r)
} }
@@ -314,6 +314,7 @@ interface VimEditor {
} }
fun createIndentBySize(size: Int): String fun createIndentBySize(size: Int): String
fun getSoftWrapStartAtOffset(offset: Int): Int?
/** /**
* Returns the collapsed fold region at the specified offset, if any. * Returns the collapsed fold region at the specified offset, if any.
@@ -25,7 +25,7 @@ interface VimMotionGroup {
allowWrap: Boolean = false, allowWrap: Boolean = false,
): Motion ): Motion
fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean = false): Motion
// TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion // TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion
@@ -38,14 +38,18 @@ abstract class VimMotionGroupBase : VimMotionGroup {
override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T
override var lastFTChar: Char = ' ' override var lastFTChar: Char = ' '
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion { override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean): Motion {
val pos = caret.getVisualPosition() val pos = caret.getVisualPosition()
if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) { if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) {
return Motion.Error return Motion.Error
} }
val intendedColumn = caret.vimLastColumn val intendedColumn = caret.vimLastColumn
val line = editor.normalizeVisualLine(pos.line + count) val line = if (bufferLines)
// TODO Does not work with folds, but I don't use those.
editor.normalizeVisualLine(editor.bufferLineToVisualLine(editor.visualLineToBufferLine(pos.line) + count))
else
editor.normalizeVisualLine(pos.line + count)
if (intendedColumn == LAST_COLUMN) { if (intendedColumn == LAST_COLUMN) {
val normalisedColumn = injector.engineEditorHelper.normalizeVisualColumn( val normalisedColumn = injector.engineEditorHelper.normalizeVisualColumn(
@@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretListener import com.maddyhome.idea.vim.api.VimCaretListener
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.normalizeOffset import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
@@ -247,7 +248,15 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret") StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret")
} }
val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset) val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset).let {
if (offset.intendedColumn == VimMotionGroupBase.LAST_COLUMN) {
val softWrapStart = editor.getSoftWrapStartAtOffset(it)
if (softWrapStart != null) softWrapStart - 1 else it
}
else {
it
}
}
StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action") StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action")
// Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode // Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode
@@ -295,7 +304,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
foldRegion.isExpanded = true foldRegion.isExpanded = true
} }
} }
return resultOffset return resultOffset
} }
@@ -792,6 +792,10 @@ class VimRegex(pattern: String) {
return null return null
} }
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return null
}
override fun getAllFoldRegions(): List<VimFoldRegion> { override fun getAllFoldRegions(): List<VimFoldRegion> {
return emptyList() return emptyList()
} }
@@ -24,7 +24,8 @@ object RegisterConstants {
const val CLIPBOARD_REGISTERS: String = CLIPBOARD_REGISTER.toString() + PRIMARY_REGISTER const val CLIPBOARD_REGISTERS: String = CLIPBOARD_REGISTER.toString() + PRIMARY_REGISTER
const val NUMBERED_REGISTERS: String = "0123456789" const val NUMBERED_REGISTERS: String = "0123456789"
const val NAMED_REGISTERS: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const val MULTICARET_JOIN_REGISTER: Char = '\u0000'
const val NAMED_REGISTERS: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + MULTICARET_JOIN_REGISTER
const val WRITABLE_REGISTERS: String = ( const val WRITABLE_REGISTERS: String = (
NUMBERED_REGISTERS + NAMED_REGISTERS + CLIPBOARD_REGISTERS + NUMBERED_REGISTERS + NAMED_REGISTERS + CLIPBOARD_REGISTERS +
@@ -0,0 +1,13 @@
package com.maddyhome.idea.vim.yank
import com.maddyhome.idea.vim.common.VimCopiedText
data class MultiCaretJoinedText(val carets: Int, override val text: String): VimCopiedText {
companion object {
val EMPTY = MultiCaretJoinedText(0, "")
}
override fun updateText(newText: String): VimCopiedText {
return MultiCaretJoinedText(carets, newText)
}
}
@@ -16,12 +16,15 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.anyNonWhitespace import com.maddyhome.idea.vim.api.anyNonWhitespace
import com.maddyhome.idea.vim.api.getLineEndForOffset import com.maddyhome.idea.vim.api.getLineEndForOffset
import com.maddyhome.idea.vim.api.getLineStartForOffset import com.maddyhome.idea.vim.api.getLineStartForOffset
import com.maddyhome.idea.vim.api.getText
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.register.RegisterConstants
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import kotlin.math.min import kotlin.math.min
@@ -42,6 +45,20 @@ open class YankGroupBase : VimYankGroup {
for ((caret, myRange) in caretToRange) { for ((caret, myRange) in caretToRange) {
result = caret.registerStorage.storeText(editor, context, myRange.first, myRange.second, false) && result result = caret.registerStorage.storeText(editor, context, myRange.first, myRange.second, false) && result
} }
val selectionType = caretToRange.values.first().second
val multiCaretJoinedText: MultiCaretJoinedText
if (result && caretToRange.size > 1) {
val joinedText = caretToRange.entries.sortedBy { it.key.offset }.joinToString("\n") { editor.getText(it.value.first).trimEnd('\n') }
multiCaretJoinedText = MultiCaretJoinedText(caretToRange.size, joinedText)
}
else {
multiCaretJoinedText = MultiCaretJoinedText.EMPTY
}
injector.registerGroup.saveRegister(editor, context, RegisterConstants.MULTICARET_JOIN_REGISTER, Register(
RegisterConstants.MULTICARET_JOIN_REGISTER, multiCaretJoinedText, selectionType)
)
return result return result
} }