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
ideaType=IU
instrumentPluginCode=true
version=chylex-1
version=9999.59-chylex
javaVersion=21
remoteRobotVersion=0.11.23
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.VimEditor
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.getLeadingCharacterOffset
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.helper.CmdwinKeys
import com.maddyhome.idea.vim.api.lineLength
import com.maddyhome.idea.vim.api.normalizeOffset
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.MotionActionHandler
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.exitVisualMode
import com.maddyhome.idea.vim.helper.getNormalizedScrollOffset
@@ -126,6 +126,9 @@ class MotionGroup : VimMotionGroupBase() {
}
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)
return moveCaretToColumn(editor, caret, col, false)
}
@@ -134,6 +137,15 @@ class MotionGroup : VimMotionGroupBase() {
editor: VimEditor,
caret: ImmutableVimCaret,
): @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 bufferLine = caret.getLine()
return editor.getLeadingCharacterOffset(bufferLine, col)
@@ -144,6 +156,9 @@ class MotionGroup : VimMotionGroupBase() {
caret: ImmutableVimCaret,
allowEnd: Boolean,
): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineEnd - 1)
}
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, allowEnd)
}
@@ -9,7 +9,10 @@
package com.maddyhome.idea.vim.helper
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 java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@@ -24,12 +27,30 @@ internal class ActionEventKeyStrokeExtractor {
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
val strokeCache = keyStrokeCache
if (defaultKeyStroke != null) {
keyStrokeCache = inputEvent.`when` to defaultKeyStroke
return defaultKeyStroke
val fixedKeyStroke = fixKeyStroke(defaultKeyStroke)
keyStrokeCache = inputEvent.`when` to fixedKeyStroke
return fixedKeyStroke
} else if (strokeCache.first == inputEvent.`when`) {
keyStrokeCache = null to null
return strokeCache.second
}
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.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.ui.popup.util.PopupUtil
import com.intellij.util.ui.table.JBTableRowEditor
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.StringListOptionValue
@@ -71,6 +72,10 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
* Enter and arrow keys (see KeyGroup and ToolWindowNavEverywhere).
*/
private fun Editor.isAllowedFileEditor(): Boolean {
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) {
return PopupUtil.getPopupContainerFor(component) == null
}
return EditorHelper.isCommitWindowEditor(this)
|| EditorHelper.isKotlinClassDecompiledToJavaFile(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.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.util.NlsContexts
import com.intellij.refactoring.actions.BaseRefactoringAction
import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext
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.IjVimDocument
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 java.awt.Component
import javax.swing.JComponent
@@ -70,15 +73,35 @@ class IjActionExecutor : VimActionExecutor {
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 {
isRunningActionFromVim = true
val place = ijAction.choosePlace()
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
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
} finally {
isRunningActionFromVim = false
@@ -88,7 +111,7 @@ class IjActionExecutor : VimActionExecutor {
override fun executeAction(editor: VimEditor?, action: NativeAction, context: ExecutionContext): Boolean {
if (editor != null && editor === injector.fallbackWindow) {
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)
}
@@ -198,6 +198,11 @@ private fun updateSearchHighlights(
currentMatchOffset,
editor
)
if (!isIncsearch) {
removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart()
}
}
return currentEditorCurrentMatchOffset
@@ -512,3 +517,9 @@ private fun shouldAddCurrentMatchSearchHighlight(pattern: String?, hlSearch: Boo
*/
@Contract(pure = true)
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.util.Key
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.LocalMarkStorage
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
var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor()
var Caret.registerStorage: CaretRegisterStorageBase? by GlobalCaretData.userData()
var Caret.markStorage: LocalMarkStorage? 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.
* 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.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction
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.helper.exitSelectMode
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.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.helper.vimInitialised
@@ -211,6 +211,10 @@ internal object IdeaSpecifics {
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
if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {
@@ -413,23 +417,6 @@ internal object IdeaSpecifics {
vimEditor.exitMode()
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)
}
}
ApplicationManager.getApplication().invokeLater {
if (vimDisabled(editor)) {
remove(editor)
}
}
}
fun remove(editor: Editor) {
val editorDisposable = editor.removeUserData(editorListenersDisposableKey)
if (editorDisposable != null) {
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)
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
// VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
}
@@ -657,6 +657,10 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
get() = ijFoldRegion.endOffset
}
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return editor.softWrapModel.getSoftWrap(offset)?.start
}
override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T {
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",
"class": "com.maddyhome.idea.vim.action.change.delete.DeleteJoinVisualLinesAction",
"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",
"isactionenabled": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.IsActionEnabled",
"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(
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.NoMotion
if (offsetAndMotion == null) return Motion.Error
parentAction.motionType = offsetAndMotion.second
return offsetAndMotion.first.toMotionOrError()
}
@@ -31,6 +31,12 @@ class MotionCamelLeftAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): 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)
?.toMotionOrError() ?: Motion.Error
}
@@ -47,6 +53,10 @@ class MotionCamelRightAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): 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)
?.toMotionOrError() ?: Motion.Error
}
@@ -70,6 +70,6 @@ class MotionDownNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): 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?,
operatorArguments: OperatorArguments,
): 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.VimRegisterGroupBase
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.yank.MultiCaretJoinedText
import javax.swing.KeyStroke
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? {
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)) {
return injector.registerGroup.getRegister(editor, context, r)
}
@@ -314,6 +314,7 @@ interface VimEditor {
}
fun createIndentBySize(size: Int): String
fun getSoftWrapStartAtOffset(offset: Int): Int?
/**
* Returns the collapsed fold region at the specified offset, if any.
@@ -25,7 +25,7 @@ interface VimMotionGroup {
allowWrap: Boolean = false,
): 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
@@ -38,14 +38,18 @@ abstract class VimMotionGroupBase : VimMotionGroup {
override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T
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()
if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) {
return Motion.Error
}
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) {
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.VimCaretListener
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.normalizeOffset
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")
}
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")
// Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode
@@ -792,6 +792,10 @@ class VimRegex(pattern: String) {
return null
}
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return null
}
override fun getAllFoldRegions(): List<VimFoldRegion> {
return emptyList()
}
@@ -24,7 +24,8 @@ object RegisterConstants {
const val CLIPBOARD_REGISTERS: String = CLIPBOARD_REGISTER.toString() + PRIMARY_REGISTER
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 = (
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.getLineEndForOffset
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.command.Argument
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange
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 kotlin.math.min
@@ -42,6 +45,20 @@ open class YankGroupBase : VimYankGroup {
for ((caret, myRange) in caretToRange) {
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
}