1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2026-04-09 22:13:06 +02:00

Compare commits

...

26 Commits

Author SHA1 Message Date
078ddaf3ca Set plugin version to chylex-56 2026-04-09 21:16:06 +02:00
94a7e1d303 Add 'isactionenabled' function 2026-04-09 21:16:06 +02:00
3de7743f56 Fix Ex commands not working 2026-04-09 19:22:04 +02:00
8636717dea Preserve visual mode after executing IDE action 2026-04-09 19:22:04 +02:00
22dfdd8ca6 Make g0/g^/g$ work with soft wraps 2026-04-09 19:22:03 +02:00
49f9f16f0d Make gj/gk jump over soft wraps 2026-04-09 19:22:03 +02:00
9bfc5d72ce Make camelCase motions adjust based on direction of visual selection 2026-04-09 19:22:03 +02:00
84c227122a Make search highlights temporary 2026-04-09 19:22:03 +02:00
1b9ff4c94a Do not switch to normal mode after inserting a live template 2026-04-09 19:22:03 +02:00
bdecbb5ef0 Exit insert mode after refactoring 2026-04-09 19:22:03 +02:00
7dfd8e6cff Add action to run last macro in all opened files 2026-04-09 19:22:03 +02:00
31e76f0fcf Stop macro execution after a failed search 2026-04-09 19:22:03 +02:00
2aadbdc8f0 Revert per-caret registers 2026-04-09 19:22:03 +02:00
627d65e528 Apply scrolloff after executing native IDEA actions 2026-04-09 19:22:03 +02:00
e77871796e Automatically add unambiguous imports after running a macro 2026-04-09 19:22:03 +02:00
c6e993dcbd Fix(VIM-3986): Exception when pasting register contents containing new line 2026-04-09 19:22:03 +02:00
341ba1ba1f Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2026-04-09 19:22:03 +02:00
f3d7ad55f6 Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2026-04-09 19:22:03 +02:00
5480b99898 Update search register when using f/t 2026-04-09 19:22:03 +02:00
5734a13ea0 Add support for count for visual and line motion surround 2026-04-09 19:22:02 +02:00
582e6bdcd8 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2026-04-09 19:22:02 +02:00
7414c3d3ed Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2026-04-09 19:22:02 +02:00
8fa5bec363 Respect count with <Action> mappings 2026-04-09 19:22:02 +02:00
aea54bdf81 Change matchit plugin to use HTML patterns in unrecognized files 2026-04-09 19:22:02 +02:00
79aca4497e Fix ex command panel causing Undock tool window to hide 2026-04-09 19:22:02 +02:00
50976ea9da Revert "VIM-4120 display multiple lines in OutputPanel with different styles"
This reverts commit 5e20bbf1
2026-04-09 19:22:02 +02:00
99 changed files with 1350 additions and 1244 deletions

View File

@@ -20,7 +20,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=chylex-56
javaVersion=21
remoteRobotVersion=0.11.23
antlrVersion=4.10.1

View File

@@ -311,6 +311,7 @@
</group>
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
</actions>
<!-- Frontend vim extensions (editor/text manipulation, no PSI/file-system dependency) -->

View File

@@ -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
}
}

View File

@@ -220,7 +220,7 @@ object VimExtensionFacade {
caret: ImmutableVimCaret,
keys: List<KeyStroke?>?,
) {
caret.registerStorage.setKeys(editor, context, register, keys?.filterNotNull() ?: emptyList())
caret.registerStorage.setKeys(register, keys?.filterNotNull() ?: emptyList())
}
/** Set the current contents of the given register */

View File

@@ -21,9 +21,7 @@ import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.Disposer
import com.intellij.util.Alarm
import com.intellij.util.Alarm.ThreadToUse
import com.jetbrains.rd.util.first
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.ModeChangeListener
@@ -123,9 +121,9 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
initialised = false
}
override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
override fun yankPerformed(editor: VimEditor, range: TextRange) {
ensureInitialised()
highlightHandler.highlightYankRange(caretToRange)
highlightHandler.highlightYankRange(editor.ij, range)
}
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
@@ -146,25 +144,22 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis
private var lastEditor: Editor? = null
private val highlighters = mutableSetOf<RangeHighlighter>()
fun highlightYankRange(caretToRange: Map<ImmutableVimCaret, TextRange>) {
fun highlightYankRange(editor: Editor, range: TextRange) {
// from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted
clearYankHighlighters()
val editor = caretToRange.first().key.editor.ij
lastEditor = editor
val attributes = getHighlightTextAttributes(editor)
for (range in caretToRange.values) {
for (i in 0 until range.size()) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.startOffsets[i],
range.endOffsets[i],
HighlighterLayer.SELECTION,
attributes,
HighlighterTargetArea.EXACT_RANGE,
)
highlighters.add(highlighter)
}
for (i in 0 until range.size()) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.startOffsets[i],
range.endOffsets[i],
HighlighterLayer.SELECTION,
attributes,
HighlighterTargetArea.EXACT_RANGE,
)
highlighters.add(highlighter)
}
// from vim-highlightedyank docs: A negative number makes the highlight persistent.

View File

@@ -268,7 +268,7 @@ private object FileTypePatterns {
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
this.cMakePatterns
} else {
return null
this.htmlPatterns
}
}

View File

@@ -0,0 +1,30 @@
package com.maddyhome.idea.vim.extension.surround
import com.intellij.util.text.CharSequenceSubSequence
internal data class RepeatedCharSequence(val text: CharSequence, val count: Int) : CharSequence {
override val length = text.length * count
override fun get(index: Int): Char {
if (index < 0 || index >= length) throw IndexOutOfBoundsException()
return text[index % text.length]
}
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
return CharSequenceSubSequence(this, startIndex, endIndex)
}
override fun toString(): String {
return text.repeat(count)
}
companion object {
fun of(text: CharSequence, count: Int): CharSequence {
return when (count) {
0 -> ""
1 -> text
else -> RepeatedCharSequence(text, count)
}
}
}
}

View File

@@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.endsWithNewLine
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
@@ -36,7 +37,10 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
import com.maddyhome.idea.vim.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.findBlockRange
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
@@ -139,7 +143,7 @@ internal class VimSurroundExtension : VimExtension {
)
}
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator())
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false, count = 1)) // TODO
}
private class YSurroundHandler : ExtensionHandler {
@@ -166,7 +170,7 @@ internal class VimSurroundExtension : VimExtension {
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
if (lastNonWhiteSpaceOffset != null) {
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
performSurround(pair, range, it)
performSurround(pair, range, it, count = operatorArguments.count1)
}
// it.moveToOffset(lineStartOffset)
}
@@ -189,15 +193,13 @@ internal class VimSurroundExtension : VimExtension {
private class VSurroundHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// NB: Operator ignores SelectionType anyway
if (!Operator().apply(editor, context, editor.mode.selectionType)) {
if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
return
}
runWriteAction {
// Leave visual mode
editor.exitVisualMode()
editor.ij.caretModel.moveToOffset(selectionStart)
// Reset the key handler so that the command trie is updated for the new mode (Normal)
// TODO: This should probably be handled by ToHandlerMapping.execute
@@ -220,6 +222,10 @@ internal class VimSurroundExtension : VimExtension {
companion object {
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) }
}
fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) {
// Save old register values for carets
val surroundings = editor.sortedCarets()
.map {
@@ -263,7 +269,7 @@ internal class VimSurroundExtension : VimExtension {
it.first + trimmedValue + it.second
} ?: innerValue
val textData =
PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.CHARACTER_WISE)
PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), null)
val putData =
PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false)
@@ -342,20 +348,41 @@ internal class VimSurroundExtension : VimExtension {
}
}
private class Operator : OperatorFunction {
private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val ijEditor = editor.ij
val c = injector.keyGroup.getChar(editor) ?: return true
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
// XXX: Will it work with line-wise or block-wise selections?
val range = getSurroundRange(editor.currentCaret()) ?: return false
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
runWriteAction {
val change = VimPlugin.getChange()
if (supportsMultipleCursors) {
ijEditor.runWithEveryCaretAndRestore {
applyOnce(ijEditor, change, pair, count)
}
}
else {
applyOnce(ijEditor, change, pair, count)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
}
}
return true
}
private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: SurroundPair, count: Int) {
// XXX: Will it work with line-wise or block-wise selections?
val primaryCaret = editor.caretModel.primaryCaret
val range = getSurroundRange(primaryCaret.vim)
if (range != null) {
val start = RepeatedCharSequence.of(pair.first, count)
val end = RepeatedCharSequence.of(pair.second, count)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end)
}
}
private fun getSurroundRange(caret: VimCaret): TextRange? {
val editor = caret.editor
if (editor.mode is Mode.CMD_LINE) {
@@ -444,14 +471,14 @@ private fun getOrInputPair(c: Char, editor: Editor, context: DataContext): Surro
}
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, tagsOnNewLines: Boolean = false) {
private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCaret, count: Int, tagsOnNewLines: Boolean = false) {
val editor = caret.editor
val change = VimPlugin.getChange()
val leftSurround = pair.first + if (tagsOnNewLines) "\n" else ""
val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
val isEOF = range.endOffset == editor.text().length
val hasNewLine = editor.endsWithNewLine()
val rightSurround = if (tagsOnNewLines) {
val rightSurround = (if (tagsOnNewLines) {
if (isEOF && !hasNewLine) {
"\n" + pair.second
} else {
@@ -459,7 +486,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare
}
} else {
pair.second
}
}).let { RepeatedCharSequence.of(it, count) }
change.insertText(editor, caret, range.startOffset, leftSurround)
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)

View File

@@ -141,7 +141,7 @@ object IjOptions {
// Temporary feature flags during development, not really intended for external use
val closenotebooks: ToggleOption =
addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true))
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true))
val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isHidden = true))
val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
// This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which
// derives from Option<VimInt>

View File

@@ -0,0 +1,68 @@
package com.maddyhome.idea.vim.group
import com.intellij.codeInsight.daemon.ReferenceImporter
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
import java.util.function.BooleanSupplier
internal object MacroAutoImport {
fun run(editor: Editor, dataContext: DataContext) {
val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
if (!FileDocumentManager.getInstance().requestWriting(editor.document, project)) {
return
}
val importers = ReferenceImporter.EP_NAME.extensionList
if (importers.isEmpty()) {
return
}
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Auto import", true) {
override fun run(indicator: ProgressIndicator) {
val fixes = ReadAction.nonBlocking<List<BooleanSupplier>> {
val fixes = mutableListOf<BooleanSupplier>()
file.accept(object : PsiRecursiveElementWalkingVisitor() {
override fun visitElement(element: PsiElement) {
for (reference in element.references) {
if (reference.resolve() != null) {
continue
}
for (importer in importers) {
importer.computeAutoImportAtOffset(editor, file, element.textRange.startOffset, true)
?.let(fixes::add)
}
}
super.visitElement(element)
}
})
return@nonBlocking fixes
}.executeSynchronously()
ApplicationManager.getApplication().invokeAndWait {
WriteCommandAction.writeCommandAction(project)
.withName("Auto Import")
.withGroupId("IdeaVimAutoImportAfterMacro")
.shouldRecordActionForActiveDocument(true)
.run<RuntimeException> {
fixes.forEach { it.asBoolean }
}
}
}
})
}
}

View File

@@ -92,6 +92,9 @@ class MacroGroup : VimMacroBase() {
} finally {
keyStack.removeFirst()
}
if (!isInternalMacro) {
MacroAutoImport.run(editor.ij, context.ij)
}
}
if (isInternalMacro) {

View File

@@ -86,6 +86,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)
}
@@ -94,6 +97,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)
@@ -104,6 +116,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)
}

View File

@@ -24,10 +24,9 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
/**
* This group works with command associated with copying and pasting text
*/
@@ -128,7 +127,7 @@ public class RegisterGroup extends VimRegisterGroupBase
final String text = XMLGroup.getInstance().getSafeXmlText(textElement);
if (text != null) {
logger.trace("Register data parsed");
register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type);
register = new Register(key, type, text, Collections.emptyList());
}
else {
logger.trace("Cannot parse register data");

View File

@@ -37,7 +37,6 @@ import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.ide.isRider
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimCopiedText
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
@@ -127,7 +126,7 @@ internal class PutGroup : VimPutBase() {
point.dispose()
if (!caret.isValid) return@forEach
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.copiedText.text.length)
val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.text.length)
val endOffset = if (data.indent) {
doIndent(
vimEditor,
@@ -179,10 +178,12 @@ internal class PutGroup : VimPutBase() {
val allContentsBefore = CopyPasteManager.getInstance().allContents
val sizeBeforeInsert = allContentsBefore.size
val firstItemBefore = allContentsBefore.firstOrNull()
logger.debug { "Copied text: ${text.copiedText}" }
val (textContent, transferableData) = text.copiedText as IjVimCopiedText
logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" }
val origContent: TextBlockTransferable =
injector.clipboardManager.setClipboardText(textContent, textContent, transferableData) as TextBlockTransferable
injector.clipboardManager.setClipboardText(
text.text,
transferableData = text.transferableData,
) as TextBlockTransferable
val allContentsAfter = CopyPasteManager.getInstance().allContents
val sizeAfterInsert = allContentsAfter.size
try {
@@ -190,7 +191,7 @@ internal class PutGroup : VimPutBase() {
} finally {
val textInClipboard = (firstItemBefore as? TextBlockTransferable)
?.getTransferData(DataFlavor.stringFlavor) as? String
val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.text
val textOnTop = textInClipboard != null && textInClipboard != text.text
if (sizeBeforeInsert != sizeAfterInsert || textOnTop) {
// Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }

View File

@@ -351,7 +351,7 @@ public class EditorHelper {
final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
final @NotNull VimEditor editor1 = new IjVimEditor(editor);
final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) - 1;
final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) + editor.getSettings().getAdditionalLinesCount();
final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine);
// For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen.

View File

@@ -12,6 +12,7 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.util.EditorUtil
@@ -23,6 +24,8 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import java.awt.Component
import javax.swing.JComponent
import javax.swing.JTable
@@ -141,3 +144,41 @@ private fun vimEnabled(editor: Editor?): Boolean {
if (editor != null && editor.isIdeaVimDisabledHere) return false
return true
}
internal inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) {
val caretModel = this.caretModel
val carets = if (this.vim.inBlockSelection) null else caretModel.allCarets
if (carets == null || carets.size == 1) {
action()
}
else {
var initialDocumentSize = this.document.textLength
var documentSizeDifference = 0
val caretOffsets = carets.map { it.selectionStart to it.selectionEnd }
val restoredCarets = mutableListOf<CaretState>()
caretModel.removeSecondaryCarets()
for ((selectionStart, selectionEnd) in caretOffsets) {
if (selectionStart == selectionEnd) {
caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference)
}
else {
caretModel.primaryCaret.setSelection(
selectionStart + documentSizeDifference,
selectionEnd + documentSizeDifference
)
}
action()
restoredCarets.add(caretModel.caretsAndSelections.single())
val documentLength = this.document.textLength
documentSizeDifference += documentLength - initialDocumentSize
initialDocumentSize = documentLength
}
caretModel.caretsAndSelections = restoredCarets
}
}

View File

@@ -24,15 +24,19 @@ 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
import com.maddyhome.idea.vim.api.VimActionExecutor
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.newapi.IjNativeAction
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
@@ -68,6 +72,12 @@ class IjActionExecutor : VimActionExecutor {
thisLogger().error("Actions cannot be updated when write-action is running or pending")
}
val startVisualModeType = (editor?.mode as? Mode.VISUAL)?.selectionType
val startVisualCaretSelection = if (editor != null && startVisualModeType != null && action.action !is BaseRefactoringAction)
editor.primaryCaret().let { Triple(it.offset, it.selectionStart, it.selectionEnd) }
else
null
val ijAction = (action as IjNativeAction).action
try {
isRunningActionFromVim = true
@@ -77,6 +87,20 @@ class IjActionExecutor : VimActionExecutor {
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

View File

@@ -58,7 +58,7 @@ internal object ScrollViewHelper {
// that this needs to be replaced as a more or less dumb line for line rewrite.
val topLine = getVisualLineAtTopOfScreen(editor)
val bottomLine = getVisualLineAtBottomOfScreen(editor)
val lastLine = vimEditor.getVisualLineCount() - 1
val lastLine = vimEditor.getVisualLineCount() + editor.settings.additionalLinesCount
// We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred
val scrollOffset = injector.options(vimEditor).scrolloff

View File

@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.application
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
@@ -30,6 +31,7 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.Contract
import java.awt.Font
import java.util.*
import javax.swing.Timer
fun updateSearchHighlights(
pattern: String?,
@@ -84,6 +86,12 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
)
}
val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = Timer(400) {
removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear()
}
/**
* Refreshes current search highlights for all visible editors
*/
@@ -125,27 +133,43 @@ private fun updateSearchHighlights(
// hlsearch (+ incsearch/noincsearch)
// Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given
// `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows
val vimEditor = editor.vim
val editorLastLine = vimEditor.lineCount() - 1
val searchStartLine = searchRange?.startLine ?: 0
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
if (searchStartLine <= editorLastLine) {
val results =
injector.searchHelper.findAll(
vimEditor,
pattern,
searchStartLine,
searchEndLine,
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
)
if (results.isNotEmpty()) {
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
val isSearching = injector.commandLine.getActiveCommandLine() != null
application.invokeLater {
val vimEditor = editor.vim
val editorLastLine = vimEditor.lineCount() - 1
val searchStartLine = searchRange?.startLine ?: 0
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
if (searchStartLine <= editorLastLine) {
val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished
val visibleTopLeft = visibleArea.location
val visibleBottomRight = visibleArea.location.apply { translate(visibleArea.width, visibleArea.height) }
val visibleStartOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleTopLeft))
val visibleEndOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleBottomRight))
val visibleStartLine = editor.document.getLineNumber(visibleStartOffset)
val visibleEndLine = editor.document.getLineNumber(visibleEndOffset)
removeSearchHighlights(editor)
val results =
injector.searchHelper.findAll(
vimEditor,
pattern,
searchStartLine.coerceAtLeast(visibleStartLine),
searchEndLine.coerceAtMost(visibleEndLine),
shouldIgnoreCase(pattern, shouldIgnoreSmartCase)
)
if (results.isNotEmpty()) {
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
}
highlightSearchResults(editor, pattern, results, currentMatchOffset)
if (!isSearching) {
removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart()
}
}
highlightSearchResults(editor, pattern, results, currentMatchOffset)
}
editor.vimLastSearch = pattern
}
editor.vimLastSearch = pattern
} else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
// nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor
// only), because 'incsearch' is active. But we don't show a search if Visual is active (behind Command-line of
@@ -179,6 +203,7 @@ private fun updateSearchHighlights(
}
}
removeHighlightsTimer.restart()
return currentEditorCurrentMatchOffset
}
@@ -204,7 +229,7 @@ private fun removeSearchHighlights(editor: Editor) {
*/
@Contract("_, _, false -> false; _, null, true -> false")
private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean {
return hlSearch && newPattern != null && newPattern != editor.vimLastSearch && newPattern != ""
return hlSearch && newPattern != null && newPattern != ""
}
private fun findClosestMatch(

View File

@@ -19,6 +19,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.PlatformUtils
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
@@ -28,6 +29,8 @@ import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
/**
@@ -81,15 +84,7 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking {
undoManager.undo(fileEditor)
// We execute undo one more time if the previous one just restored selection
if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
undoManager.undo(fileEditor)
}
}
CommandProcessor.getInstance().runUndoTransparentAction {
removeSelections(editor)
restoreVisualMode(editor)
}
} else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
@@ -240,4 +235,21 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
val hasChanges: Boolean
get() = changeListener.hasChanged || initialPath != editor.getPath()
}
private fun restoreVisualMode(editor: VimEditor) {
if (!editor.inVisualMode && editor.ij.selectionModel.hasSelection()) {
val detectedMode = VimPlugin.getVisualMotion().detectSelectionType(editor)
// Visual block selection is restored into multiple carets, so multi-carets that form a block are always
// identified as visual block mode, leading to false positives.
// Since I use visual block mode much less often than multi-carets, this is a judgment call to never restore
// visual block mode.
val wantedMode = if (detectedMode == SelectionType.BLOCK_WISE)
SelectionType.CHARACTER_WISE
else
detectedMode
VimPlugin.getVisualMotion().enterVisualMode(editor, wantedMode)
}
}
}

View File

@@ -18,7 +18,6 @@ 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.maddyhome.idea.vim.api.CaretRegisterStorageBase
import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo
import com.maddyhome.idea.vim.api.VimOutputPanel
@@ -97,9 +96,8 @@ var Caret.vimInsertStart: RangeMarker by userDataOr {
}
// TODO: Data could be lost during visual block motion
var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor()
var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
internal var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()
var Editor.vimInitialised: Boolean by userDataOr { false }

View File

@@ -17,7 +17,9 @@ import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter
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
@@ -32,6 +34,7 @@ import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange
@@ -46,7 +49,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.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.ide.isRider
@@ -73,6 +75,7 @@ internal object IdeaSpecifics {
private val surrounderAction =
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
private var editor: Editor? = null
private var caretOffset = -1
private var completionData: CompletionData? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
@@ -81,6 +84,7 @@ internal object IdeaSpecifics {
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) {
editor = hostEditor
caretOffset = hostEditor.caretModel.offset
}
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
@@ -157,26 +161,46 @@ internal object IdeaSpecifics {
if (VimPlugin.isNotEnabled()) return
val editor = editor
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
completionData?.recordCompletion(editor, VimPlugin.getRegister())
}
if (editor != null) {
if (action is ChooseItemAction && injector.registerGroup.isRecording) {
completionData?.recordCompletion(editor, VimPlugin.getRegister()
)
}
//region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith(
it,
)
//region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith(
it,
)
}
) {
editor?.let {
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim)
KeyHandler.getInstance().reset(it.vim)
}
}
) {
editor?.let {
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.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
if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {
val scrollModel = editor.scrollingModel as ScrollingModelImpl
if (scrollModel.isScrollingNow) {
val v = scrollModel.verticalScrollOffset
val h = scrollModel.horizontalScrollOffset
scrollModel.finishAnimation()
scrollModel.scroll(h, v)
scrollModel.finishAnimation()
}
injector.scroll.scrollCaretIntoView(editor.vim)
}
}
//endregion
this.editor = null
this.caretOffset = -1
this.completionData?.dispose()
this.completionData = null
@@ -310,23 +334,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()
}
}
}
}
}

View File

@@ -420,12 +420,14 @@ object VimListenerManager {
editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim)
}
// Breaks relativenumber for some reason
// injector.scroll.scrollCaretIntoView(editor.vim)
}
injector.outputPanel.getCurrentOutputPanel()?.close()
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
(VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
// (VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
}

View File

@@ -38,7 +38,7 @@ import java.io.IOException
internal class IjClipboardManager : VimClipboardManager {
override fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): IjVimCopiedText? {
override fun getPrimaryContent(): IjVimCopiedText? {
val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null
val contents = clipboard.getContents(null) ?: return null
val (text, transferableData) = getTextAndTransferableData(contents) ?: return null
@@ -241,6 +241,6 @@ internal class IjClipboardManager : VimClipboardManager {
}
}
data class IjVimCopiedText(override val text: String, val transferableData: List<Any>) : VimCopiedText {
data class IjVimCopiedText(override val text: String, override val transferableData: List<Any>) : VimCopiedText {
override fun updateText(newText: String): VimCopiedText = IjVimCopiedText(newText, transferableData)
}

View File

@@ -12,8 +12,6 @@ import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.VisualPosition
import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.CaretRegisterStorage
import com.maddyhome.idea.vim.api.CaretRegisterStorageBase
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo
@@ -21,6 +19,7 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretBase
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.group.visual.VisualChange
@@ -29,7 +28,6 @@ import com.maddyhome.idea.vim.helper.insertHistory
import com.maddyhome.idea.vim.helper.lastSelectionInfo
import com.maddyhome.idea.vim.helper.markStorage
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
import com.maddyhome.idea.vim.helper.registerStorage
import com.maddyhome.idea.vim.helper.resetVimLastColumn
import com.maddyhome.idea.vim.helper.vimInsertStart
import com.maddyhome.idea.vim.helper.vimLastColumn
@@ -37,22 +35,14 @@ import com.maddyhome.idea.vim.helper.vimLastVisualOperatorRange
import com.maddyhome.idea.vim.helper.vimLine
import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimSelectionStartClear
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.SelectionType
class IjVimCaret(val caret: Caret) : VimCaretBase() {
override val registerStorage: CaretRegisterStorage
get() {
var storage = this.caret.registerStorage
if (storage == null) {
initInjector() // To initialize injector used in CaretRegisterStorageBase
storage = CaretRegisterStorageBase(this)
this.caret.registerStorage = storage
} else if (storage.caret != this) {
storage.caret = this
}
return storage
}
override val registerStorage: VimRegisterGroup
get() = injector.registerGroup
override val markStorage: LocalMarkStorage
get() {
var storage = this.caret.markStorage

View File

@@ -22,6 +22,7 @@ import com.intellij.openapi.editor.ex.ScrollingModelEx
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.impl.CaretModelImpl
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.platform.project.projectId
import com.maddyhome.idea.vim.api.BufferPosition
@@ -150,7 +151,7 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
}
}
if (!isWritable()) return
editor.document.insertString(atPosition, text)
editor.document.insertString(atPosition, StringUtil.convertLineSeparators(text, "\n"))
}
override fun replaceString(start: Int, end: Int, newString: String) {
@@ -179,21 +180,38 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
return editor.caretModel.allCarets.map { IjVimCaret(it) }
}
override var isFirstCaret = true
override var isReversingCarets = false
@Suppress("ideavimRunForEachCaret")
override fun forEachCaret(action: (VimCaret) -> Unit) {
if (editor.vim.inBlockSelection) {
action(IjVimCaret(editor.caretModel.primaryCaret))
} else {
editor.caretModel.runForEachCaret({
if (it.isValid) {
action(IjVimCaret(it))
}
}, false)
try {
editor.caretModel.runForEachCaret({
if (it.isValid) {
action(IjVimCaret(it))
isFirstCaret = false
}
}, false)
} finally {
isFirstCaret = true
}
}
}
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {
editor.caretModel.runForEachCaret({ action(IjVimCaret(it)) }, reverse)
isReversingCarets = reverse
try {
editor.caretModel.runForEachCaret({
action(IjVimCaret(it))
isFirstCaret = false
}, reverse)
} finally {
isFirstCaret = true
isReversingCarets = false
}
}
override fun isInForEachCaretScope(): Boolean {
@@ -613,6 +631,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
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -9,7 +9,9 @@
package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.application.ApplicationManager
import com.maddyhome.idea.vim.api.MessageType
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMessagesBase
import com.maddyhome.idea.vim.api.globalOptions
@@ -23,43 +25,56 @@ internal class IjVimMessages : VimMessagesBase() {
private var message: String? = null
private var error = false
private var lastBeepTimeMillis = 0L
private var allowClearStatusBarMessage = true
override fun showMessage(editor: VimEditor, message: String?) {
showMessageInternal(editor, message, MessageType.STANDARD)
}
override fun showErrorMessage(editor: VimEditor, message: String?) {
showMessageInternal(editor, message, MessageType.ERROR)
indicateError()
}
private fun showMessageInternal(editor: VimEditor, message: String?, messageType: MessageType) {
this.message = message
if (message.isNullOrBlank()) {
clearStatusBarMessage()
return
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
fun setStatusBarMessage(project: Project, message: String?) {
WindowManager.getInstance().getStatusBar(project)?.let {
it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
}
}
val context = injector.executionContextManager.getEditorExecutionContext(editor)
injector.outputPanel.output(editor, context, message, messageType)
}
this.message = message
@Suppress("DEPRECATION")
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
if (editor != null) {
showMessage(editor, message)
val project = editor?.ij?.project
if (project != null) {
setStatusBarMessage(project, message)
} else {
// Legacy path for when editor is null - just store the message
this.message = message
// TODO: We really shouldn't set the status bar text for other projects. That's rude.
ProjectManager.getInstance().openProjects.forEach {
setStatusBarMessage(it, message)
}
}
// Redraw happens automatically based on changes or scrolling. If we've just set the message (e.g., searching for a
// string, hitting the bottom and scrolling to the top), make sure we don't immediately clear it when scrolling.
allowClearStatusBarMessage = false
ApplicationManager.getApplication().invokeLater {
allowClearStatusBarMessage = true
}
}
override fun getStatusBarMessage(): String? = message
// Vim doesn't appear to have a policy about clearing the status bar, other than on "redraw". This can be forced with
// <C-L> or the `:redraw` command, but also happens as the screen changes, e.g., when inserting or deleting lines,
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
// things happen.
override fun clearStatusBarMessage() {
if (message.isNullOrEmpty()) return
injector.outputPanel.getCurrentOutputPanel()?.close()
val currentMessage = message
if (currentMessage.isNullOrEmpty()) return
// Don't clear the status bar message if we've only just set it
if (!allowClearStatusBarMessage) return
ProjectManager.getInstance().openProjects.forEach { project ->
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
// Only clear the status bar if it's showing our last message
if (statusBar.info?.contains(currentMessage) == true) {
statusBar.info = ""
}
}
}
message = null
}

View File

@@ -12,8 +12,8 @@ import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.ClientProperty
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBPanel
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.IJSwingUtilities
@@ -24,6 +24,7 @@ import com.maddyhome.idea.vim.api.MessageType
import com.maddyhome.idea.vim.api.VimOutputPanel
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.helper.requestFocus
import com.maddyhome.idea.vim.helper.selectEditorFont
import com.maddyhome.idea.vim.helper.vimMorePanel
@@ -35,165 +36,121 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.lang.ref.WeakReference
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JRootPane
import javax.swing.JScrollPane
import javax.swing.JTextPane
import javax.swing.JTextArea
import javax.swing.KeyStroke
import javax.swing.SwingUtilities
import javax.swing.text.DefaultCaret
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.StyleConstants
import javax.swing.text.StyledDocument
import kotlin.math.ceil
import kotlin.math.min
/**
* Panel that displays text in a `more` like window overlaid on the editor.
* This panel displays text in a `more` like window and implements [VimOutputPanel].
*/
class OutputPanel private constructor(
private val editor: Editor,
) : JBPanel<OutputPanel>(), VimOutputPanel {
class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), VimOutputPanel {
private val myEditorRef: WeakReference<Editor> = editorRef
val editor: Editor? get() = myEditorRef.get()
private val textPane = JTextPane()
private val resizeAdapter: ComponentAdapter
private var defaultForeground: Color? = null
val myLabel: JLabel = JLabel("more")
private val myText = JTextArea()
private val myScrollPane: JScrollPane =
JBScrollPane(myText, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private val myAdapter: ComponentAdapter
private var myLineHeight = 0
private var glassPane: JComponent? = null
private var originalLayout: LayoutManager? = null
private var wasOpaque = false
private var myOldGlass: JComponent? = null
private var myOldLayout: LayoutManager? = null
private var myWasOpaque = false
var active: Boolean = false
private val segments = mutableListOf<TextLine>()
var myActive: Boolean = false
private val labelComponent: JLabel = JLabel("more")
private val scrollPane: JScrollPane =
JBScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private var cachedLineHeight = 0
private var isSingleLine = false
val isActive: Boolean
get() = myActive
init {
textPane.isEditable = false
textPane.caret = object : DefaultCaret() {
override fun setVisible(v: Boolean) {
super.setVisible(false)
}
}
textPane.highlighter = null
// Create a text editor for the text and a label for the prompt
val layout = BorderLayout(0, 0)
setLayout(layout)
add(myScrollPane, BorderLayout.CENTER)
add(myLabel, BorderLayout.SOUTH)
resizeAdapter = object : ComponentAdapter() {
// Set the text area read only, and support wrap
myText.isEditable = false
myText.setLineWrap(true)
myAdapter = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) {
positionPanel()
}
}
// Suppress the fancy frame background used in the Islands theme
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
// Setup some listeners to handle keystrokes
val moreKeyListener = MoreKeyListener()
addKeyListener(moreKeyListener)
myText.addKeyListener(moreKeyListener)
// Initialize panel
setLayout(BorderLayout(0, 0))
add(scrollPane, BorderLayout.CENTER)
add(labelComponent, BorderLayout.SOUTH)
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
// Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation
// applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by
// JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting.
ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true)
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
updateUI()
}
// Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler
override fun updateUI() {
super.updateUI()
setBorder(ExPanelBorder())
// Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null
@Suppress("SENSELESS_COMPARISON")
if (textPane != null && labelComponent != null && scrollPane != null) {
if (myText != null && myLabel != null && myScrollPane != null) {
setFontForElements()
textPane.setBorder(null)
scrollPane.setBorder(null)
labelComponent.setForeground(textPane.getForeground())
myText.setBorder(null)
myScrollPane.setBorder(null)
myLabel.setForeground(myText.getForeground())
// Make sure the panel is positioned correctly in case we're changing font size
positionPanel()
}
}
override var text: String
get() = textPane.getText() ?: ""
get() = myText.text
set(value) {
// ExOutputPanel will strip a trailing newline. We'll do it now so that tests have the same behaviour.
val newValue = value.removeSuffix("\n")
segments.clear()
if (newValue.isEmpty()) return
segments.add(TextLine(newValue, null))
myText.text = newValue
val ed = editor
if (ed != null) {
myText.setFont(selectEditorFont(ed, newValue))
}
myText.setCaretPosition(0)
if (newValue.isNotEmpty()) {
activate()
}
}
override var label: String
get() = labelComponent.text
get() = myLabel.text ?: ""
set(value) {
labelComponent.text = value
myLabel.text = value
val ed = editor
if (ed != null) {
myLabel.setFont(selectEditorFont(ed, value))
}
}
/**
* Sets styled text with multiple segments, each potentially having a different color.
*/
fun setStyledText(lines: List<TextLine>) {
val doc = textPane.styledDocument
doc.remove(0, doc.length)
if (defaultForeground == null) {
defaultForeground = textPane.foreground
}
if (lines.size > 1) {
setMultiLineText(lines, doc)
} else {
doc.insertString(doc.length, lines[0].text.removeSuffix("\n"), getLineColor(lines[0]))
}
val fullText = doc.getText(0, doc.length)
textPane.setFont(selectEditorFont(editor, fullText))
textPane.setCaretPosition(0)
if (fullText.isNotEmpty()) {
activate()
}
}
private fun setMultiLineText(
lines: List<TextLine>,
doc: StyledDocument,
) {
for ((index, line) in lines.withIndex()) {
val text = line.text.removeSuffix("\n")
val attrs = getLineColor(line)
val separator = if (index < lines.size - 1) "\n" else ""
doc.insertString(doc.length, text + separator, attrs)
}
}
private fun getLineColor(segment: TextLine): SimpleAttributeSet {
val attrs = SimpleAttributeSet()
val color = segment.color ?: defaultForeground
if (color != null) {
StyleConstants.setForeground(attrs, color)
}
return attrs
}
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
val color = when (messageType) {
MessageType.ERROR -> JBColor.RED
MessageType.STANDARD -> null
}
segments.add(TextLine(text, color))
}
override fun show() {
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
if (currentPanel != null && currentPanel != this) currentPanel.close()
setStyledText(segments)
if (!active) {
activate()
if (this.text.isNotEmpty() && isNewLine) {
this.text += "\n$text"
} else {
this.text += text
}
}
@@ -202,15 +159,20 @@ class OutputPanel private constructor(
}
override fun clearText() {
segments.clear()
}
fun clear() {
text = ""
}
override fun handleKey(key: KeyStroke) {
override fun show() {
editor ?: return
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
if (currentPanel != null && currentPanel != this) currentPanel.close()
if (!myActive) {
activate()
}
}
override fun handleKey(key: KeyStroke) {
if (isAtEnd) {
close(key)
return
@@ -235,262 +197,214 @@ class OutputPanel private constructor(
override fun getForeground(): Color? {
@Suppress("SENSELESS_COMPARISON")
if (textPane == null) {
if (myText == null) {
// Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null.
return super.getForeground()
}
return textPane.getForeground()
return myText.getForeground()
}
override fun getBackground(): Color? {
@Suppress("SENSELESS_COMPARISON")
if (textPane == null) {
if (myText == null) {
// Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null.
return super.getBackground()
}
return textPane.getBackground()
return myText.getBackground()
}
/**
* Turns off the output panel and optionally puts the focus back to the original component.
* Turns off the ex entry field and optionally puts the focus back to the original component
*/
fun deactivate(refocusOwningEditor: Boolean) {
if (!active) return
active = false
clearText()
textPane.text = ""
if (refocusOwningEditor) {
requestFocus(editor.contentComponent)
if (!myActive) return
myActive = false
myText.text = ""
val ed = editor
if (refocusOwningEditor && ed != null) {
requestFocus(ed.contentComponent)
}
if (glassPane != null) {
glassPane!!.removeComponentListener(resizeAdapter)
glassPane!!.isVisible = false
glassPane!!.remove(this)
glassPane!!.setOpaque(wasOpaque)
glassPane!!.setLayout(originalLayout)
if (myOldGlass != null) {
myOldGlass!!.removeComponentListener(myAdapter)
myOldGlass!!.isVisible = false
myOldGlass!!.remove(this)
myOldGlass!!.setOpaque(myWasOpaque)
myOldGlass!!.setLayout(myOldLayout)
}
}
/**
* Turns on the output panel for the given editor.
* Turns on the more window for the given editor
*/
fun activate() {
disableOldGlass()
val ed = editor ?: return
val root = SwingUtilities.getRootPane(ed.contentComponent)
deactivateOldGlass(root)
setFontForElements()
positionPanel()
if (glassPane != null) {
glassPane!!.isVisible = true
if (myOldGlass != null) {
myOldGlass!!.isVisible = true
}
active = true
requestFocus(textPane)
myActive = true
requestFocus(myText)
}
private fun disableOldGlass() {
val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return
glassPane = root.getGlassPane() as JComponent?
if (glassPane == null) {
private fun deactivateOldGlass(root: JRootPane?) {
if (root == null) return
myOldGlass = root.getGlassPane() as JComponent?
if (myOldGlass != null) {
myOldLayout = myOldGlass!!.layout
myWasOpaque = myOldGlass!!.isOpaque
myOldGlass!!.setLayout(null)
myOldGlass!!.setOpaque(false)
myOldGlass!!.add(this)
myOldGlass!!.addComponentListener(myAdapter)
}
}
private fun setFontForElements() {
val ed = editor ?: return
myText.setFont(selectEditorFont(ed, myText.getText()))
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
override fun scrollLine() {
scrollOffset(myLineHeight)
}
override fun scrollPage() {
scrollOffset(myScrollPane.getVerticalScrollBar().visibleAmount)
}
override fun scrollHalfPage() {
val sa = myScrollPane.getVerticalScrollBar().visibleAmount / 2.0
val offset = ceil(sa / myLineHeight) * myLineHeight
scrollOffset(offset.toInt())
}
fun onBadKey() {
val ed = editor ?: return
myLabel.setText(injector.messages.message("message.ex.output.more.prompt.full"))
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
private fun scrollOffset(more: Int) {
val ed = editor ?: return
val `val` = myScrollPane.getVerticalScrollBar().value
myScrollPane.getVerticalScrollBar().setValue(`val` + more)
myScrollPane.getHorizontalScrollBar().setValue(0)
if (isAtEnd) {
myLabel.setText(injector.messages.message("message.ex.output.end.prompt"))
} else {
myLabel.setText(injector.messages.message("message.ex.output.more.prompt"))
}
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
val isAtEnd: Boolean
get() {
val isSingleLine = myText.getLineCount() == 1
if (isSingleLine) return true
val scrollBar = myScrollPane.getVerticalScrollBar()
val value = scrollBar.value
if (!scrollBar.isVisible) {
return true
}
return value >= scrollBar.maximum - scrollBar.visibleAmount ||
scrollBar.maximum <= scrollBar.visibleAmount
}
private fun positionPanel() {
val ed = editor ?: return
val contentComponent = ed.contentComponent
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent)
val rootPane = SwingUtilities.getRootPane(contentComponent)
if (scroll == null || rootPane == null) {
// These might be null if we're invoked during component initialisation and before it's been added to the tree
return
}
originalLayout = glassPane!!.layout
wasOpaque = glassPane!!.isOpaque
glassPane!!.setLayout(null)
glassPane!!.setOpaque(false)
glassPane!!.add(this)
glassPane!!.addComponentListener(resizeAdapter)
size = scroll.size
myLineHeight = myText.getFontMetrics(myText.getFont()).height
val count: Int = countLines(myText.getText())
val visLines = size.height / myLineHeight - 1
val lines = min(count, visLines)
setSize(
size.width,
lines * myLineHeight + myLabel.getPreferredSize().height + border.getBorderInsets(this).top * 2
)
val height = size.height
val bounds = scroll.bounds
bounds.translate(0, scroll.getHeight() - height)
bounds.height = height
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
bounds.location = pos
setBounds(bounds)
myScrollPane.getVerticalScrollBar().setValue(0)
if (!injector.globalOptions().more) {
// FIX
scrollOffset(100000)
} else {
scrollOffset(0)
}
}
fun close(key: KeyStroke? = null) {
val ed = editor ?: return
ApplicationManager.getApplication().invokeLater {
deactivate(true)
val project = ed.project
if (project != null && key != null && key.keyChar != '\n') {
val keys: MutableList<KeyStroke> = ArrayList(1)
keys.add(key)
if (LOG.isTrace()) {
LOG.trace(
"Adding new keys to keyStack as part of playback. State before adding keys: " +
getInstance().keyStack.dump()
)
}
getInstance().keyStack.addKeys(keys)
val context: ExecutionContext =
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(ed))
VimPlugin.getMacro().playbackKeys(IjVimEditor(ed), context, 1)
}
}
}
override fun close() {
close(null)
}
fun close(key: KeyStroke?) {
val passKeyBack = isSingleLine
ApplicationManager.getApplication().invokeLater {
deactivate(true)
val project = editor.project
// For single line messages, pass any key back to the editor (including Enter)
// For multi-line messages, don't pass Enter back (it was used to dismiss)
if (project != null && key != null && (passKeyBack || key.keyChar != '\n')) {
val keys: MutableList<KeyStroke> = ArrayList(1)
keys.add(key)
getInstance().keyStack.addKeys(keys)
val context: ExecutionContext =
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(editor))
VimPlugin.getMacro().playbackKeys(IjVimEditor(editor), context, 1)
}
}
}
private fun setFontForElements() {
textPane.setFont(selectEditorFont(editor, textPane.getText()))
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
private fun positionPanel() {
val scroll = positionPanelStart() ?: return
val lineHeight = textPane.getFontMetrics(textPane.getFont()).height
val count = countLines(textPane.getText())
val visLines = size.height / lineHeight - 1
val lines = min(count, visLines)
// Simple output: single line that fits entirely - no label needed
isSingleLine = count == 1 && count <= visLines
labelComponent.isVisible = !isSingleLine
val extraHeight = if (isSingleLine) 0 else labelComponent.getPreferredSize().height
setSize(
size.width,
lines * lineHeight + extraHeight + border.getBorderInsets(this).top * 2
)
finishPositioning(scroll)
// Force layout so that viewport sizes are valid before checking scroll state
validate()
// onPositioned
cachedLineHeight = lineHeight
scrollPane.getVerticalScrollBar().setValue(0)
if (!isSingleLine) {
if (!injector.globalOptions().more) {
scrollOffset(100000)
} else {
scrollOffset(0)
}
}
}
private fun positionPanelStart(): JScrollPane? {
val contentComponent = editor.contentComponent
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent) as? JScrollPane
val rootPane = SwingUtilities.getRootPane(contentComponent)
if (scroll == null || rootPane == null) {
return null
}
size = scroll.size
return scroll
}
private fun finishPositioning(scroll: JScrollPane) {
val rootPane = SwingUtilities.getRootPane(editor.contentComponent)
val bounds = scroll.bounds
bounds.translate(0, scroll.getHeight() - size.height)
bounds.height = size.height
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
bounds.location = pos
setBounds(bounds)
}
private fun countLines(text: String): Int {
if (text.isEmpty()) {
return 1
}
var count = 0
var pos = -1
while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) {
count++
}
if (text[text.length - 1] != '\n') {
count++
}
return count
}
override fun scrollLine() {
scrollOffset(cachedLineHeight)
}
override fun scrollPage() {
scrollOffset(scrollPane.getVerticalScrollBar().visibleAmount)
}
override fun scrollHalfPage() {
val sa = scrollPane.getVerticalScrollBar().visibleAmount / 2.0
val offset = ceil(sa / cachedLineHeight) * cachedLineHeight
scrollOffset(offset.toInt())
}
fun onBadKey() {
labelComponent.setText(injector.messages.message("message.ex.output.more.prompt.full"))
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
private fun scrollOffset(more: Int) {
scrollPane.validate()
val scrollBar = scrollPane.getVerticalScrollBar()
val value = scrollBar.value
scrollBar.setValue(value + more)
scrollPane.getHorizontalScrollBar().setValue(0)
// Check if we're at the end or if content fits entirely (nothing to scroll)
if (isAtEnd) {
labelComponent.setText(injector.messages.message("message.ex.output.end.prompt"))
} else {
labelComponent.setText(injector.messages.message("message.ex.output.more.prompt"))
}
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
val isAtEnd: Boolean
get() {
if (isSingleLine) return true
val contentHeight = textPane.preferredSize.height
val viewportHeight = scrollPane.viewport.height
if (contentHeight <= viewportHeight) return true
val scrollBar = scrollPane.getVerticalScrollBar()
return scrollBar.value >= scrollBar.maximum - scrollBar.visibleAmount
}
private inner class OutputPanelKeyListener : KeyAdapter() {
private class MoreKeyListener : KeyAdapter() {
/**
* Invoked when a key has been pressed.
*/
override fun keyTyped(e: KeyEvent) {
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
val keyChar = e.keyChar
val keyCode = e.getKeyCode()
val keyChar = e.getKeyChar()
val modifiers = e.modifiersEx
val keyStroke = KeyStroke.getKeyStroke(keyChar, modifiers)
val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED)
KeyStroke.getKeyStroke(keyCode, modifiers)
else
KeyStroke.getKeyStroke(keyChar, modifiers)
currentPanel.handleKey(keyStroke)
}
override fun keyPressed(e: KeyEvent) {
if (!e.isActionKey && e.keyCode != KeyEvent.VK_ENTER) return
val currentPanel = injector.outputPanel.getCurrentOutputPanel() as? OutputPanel ?: return
val keyCode = e.keyCode
val modifiers = e.modifiersEx
val keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers)
if (isSingleLine) {
currentPanel.close(keyStroke)
e.consume()
return
}
// Multi-line mode: arrow keys scroll, down/right at end closes
when (keyCode) {
KeyEvent.VK_ENTER -> {
if (currentPanel.isAtEnd) currentPanel.close() else currentPanel.scrollLine()
e.consume()
}
KeyEvent.VK_DOWN -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollLine()
KeyEvent.VK_RIGHT -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollLine()
KeyEvent.VK_UP -> currentPanel.scrollOffset(-cachedLineHeight)
KeyEvent.VK_LEFT -> currentPanel.scrollOffset(-cachedLineHeight)
KeyEvent.VK_PAGE_DOWN -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollPage()
KeyEvent.VK_PAGE_UP -> currentPanel.scrollOffset(-scrollPane.verticalScrollBar.visibleAmount)
}
}
}
class LafListener : LafManagerListener {
override fun lookAndFeelChanged(source: LafManager) {
if (VimPlugin.isNotEnabled()) return
// This listener is only invoked for local scenarios, and we only need to update local editor UI. This will invoke
// updateUI on the output pane and it's child components
for (vimEditor in injector.editorGroup.getEditors()) {
val editor = (vimEditor as IjVimEditor).editor
if (!isPanelActive(editor)) continue
@@ -500,24 +414,41 @@ class OutputPanel private constructor(
}
companion object {
private val LOG: VimLogger = injector.getLogger<OutputPanel>(OutputPanel::class.java)
fun getNullablePanel(editor: Editor): OutputPanel? {
return editor.vimMorePanel as OutputPanel?
return editor.vimMorePanel as? OutputPanel
}
fun isPanelActive(editor: Editor): Boolean {
return getNullablePanel(editor) != null
return getNullablePanel(editor)?.myActive ?: false
}
fun getInstance(editor: Editor): OutputPanel {
var panel: OutputPanel? = getNullablePanel(editor)
if (panel == null) {
panel = OutputPanel(editor)
panel = OutputPanel(WeakReference(editor))
editor.vimMorePanel = panel
}
return panel
}
private fun countLines(text: String): Int {
if (text.isEmpty()) {
return 0
}
var count = 0
var pos = -1
while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) {
count++
}
if (text[text.length - 1] != '\n') {
count++
}
return count
}
}
}
data class TextLine(val text: String, val color: Color?)

View File

@@ -15,6 +15,7 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.DocumentAdapter
import com.intellij.util.IJSwingUtilities
import com.maddyhome.idea.vim.EventFacade
@@ -144,6 +145,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
glassPane.addComponentListener(resizePanelListener)
positionPanel()
glassPane.isVisible = true
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, parent)
entry.requestFocusInWindow()
}
this.isActive = true
@@ -191,6 +193,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
requestFocus(parent!!)
}
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, null)
oldGlass!!.removeComponentListener(resizePanelListener)
oldGlass!!.isVisible = false
oldGlass!!.remove(this)
@@ -284,7 +287,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
.calculateCount0Snapshot()
)
if (labelText == "/" || labelText == "?" || searchCommand) {
if ((labelText == "/" || labelText == "?" || searchCommand) && !injector.macro.isExecutingMacro) {
val forwards = labelText != "?" // :s, :g, :v are treated as forwards
val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0)
val pattern = searchText.take(patternEnd)

View File

@@ -22,11 +22,11 @@ class IjOutputPanelService : VimOutputPanelServiceBase() {
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
override fun getCurrentOutputPanel(): VimOutputPanel? {
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).active }
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).isActive }
}
override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel {
val panel = OutputPanel.getInstance(editor.ij)
val panel = OutputPanel(WeakReference(editor.ij))
activeOutputPanel = WeakReference(panel)
return panel
}

View File

@@ -96,7 +96,7 @@ data class ReadCommand(val range: Range, val modifier: CommandModifier, val argu
val copiedText = injector.clipboardManager.dumbCopiedText(content)
val caret = editor.currentCaret()
val address = if (range.addresses.isEmpty()) -1 else range.addresses.last().getLine1(editor, caret)
val textData = PutData.TextData(null, copiedText, SelectionType.LINE_WISE)
val textData = PutData.TextData(null, SelectionType.LINE_WISE, copiedText.transferableData, null)
return PutData(
textData,
null,

View File

@@ -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()
}
}

View File

@@ -78,5 +78,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"
}
]

View File

@@ -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"
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -134,7 +134,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases()
configureByText("\n")
typeText(commandToKeys("command! -range Error echo <args>"))
assertPluginError(true)
assertPluginError(false)
kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage())
}
@@ -143,7 +143,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases()
configureByText("\n")
typeText(commandToKeys("command! -complete=color Error echo <args>"))
assertPluginError(true)
assertPluginError(false)
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -24,6 +24,7 @@ class ExecuteCommandTest : VimTestCase() {
fun `test execute with range`() {
configureByText("\n")
typeText(commandToKeys("1,2execute 'echo 42'"))
assertNoExOutput()
assertPluginError(true)
}

View File

@@ -72,6 +72,7 @@ class HistoryCommandTest : VimTestCase() {
fun `test history with 'history' option set to 0 shows nothing`() {
enterCommand("set history=0")
enterCommand("history")
assertNoExOutput()
assertPluginError(false)
assertPluginErrorMessage("'history' option is zero")
}

View File

@@ -241,12 +241,7 @@ class RegistersCommandTest : VimTestCase() {
val vimEditor = fixture.editor.vim
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
injector.registerGroup.saveRegister(
vimEditor,
context,
'+',
Register('+', injector.clipboardManager.dumbCopiedText("Lorem ipsum dolor"), SelectionType.LINE_WISE)
)
injector.registerGroup.saveRegister(vimEditor, context, '+', Register('+', SelectionType.LINE_WISE, "Lorem ipsum dolor", mutableListOf()))
val clipboardContent = injector.clipboardManager.dumbCopiedText("clipboard content")
injector.clipboardManager.setClipboardContent(vimEditor, context, clipboardContent)
typeText("V<Esc>")
@@ -453,12 +448,7 @@ class RegistersCommandTest : VimTestCase() {
val vimEditor = fixture.editor.vim
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
val clipboardContent = injector.clipboardManager.dumbCopiedText("clipboard content")
injector.registerGroup.saveRegister(
vimEditor,
context,
'+',
Register('+', injector.clipboardManager.dumbCopiedText("Lorem ipsum dolor"), SelectionType.LINE_WISE)
)
injector.registerGroup.saveRegister(vimEditor, context, '+', Register('+', SelectionType.LINE_WISE, "Lorem ipsum dolor", mutableListOf()))
injector.clipboardManager.setClipboardContent(vimEditor, context, clipboardContent)
typeText("V<Esc>")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class AndFunctionTest : VimTestCase() {
@Test
fun `test and function with list causes error`() {
enterCommand("echo and([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -50,6 +51,7 @@ class AndFunctionTest : VimTestCase() {
@Test
fun `test and function with dict causes error`() {
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -57,6 +59,7 @@ class AndFunctionTest : VimTestCase() {
@Test
fun `test and function with float causes error`() {
enterCommand("echo and(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -28,6 +28,7 @@ class InvertFunctionTest : VimTestCase() {
@Test
fun `test invert function with list causes error`() {
enterCommand("echo invert([1, 2, 3])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -35,6 +36,7 @@ class InvertFunctionTest : VimTestCase() {
@Test
fun `test invert function with dict causes error`() {
enterCommand("echo invert({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -42,6 +44,7 @@ class InvertFunctionTest : VimTestCase() {
@Test
fun `test invert function with float causes error`() {
enterCommand("echo invert(1.5)")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class OrFunctionTest : VimTestCase() {
@Test
fun `test or function with list causes error`() {
enterCommand("echo or([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -50,6 +51,7 @@ class OrFunctionTest : VimTestCase() {
@Test
fun `test or function with dict causes error`() {
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -57,6 +59,7 @@ class OrFunctionTest : VimTestCase() {
@Test
fun `test or function with float causes error`() {
enterCommand("echo or(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class XorFunctionTest : VimTestCase() {
@Test
fun `test xor function with list causes error`() {
enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -50,6 +51,7 @@ class XorFunctionTest : VimTestCase() {
@Test
fun `test xor function with dict causes error`() {
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -57,6 +59,7 @@ class XorFunctionTest : VimTestCase() {
@Test
fun `test xor function with float causes error`() {
enterCommand("echo xor(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -33,6 +33,7 @@ class ToLowerFunctionTest : VimTestCase() {
@Test
fun `test tolower with list causes error`() {
enterCommand("echo tolower([1, 2, 3])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String")
}
@@ -40,6 +41,7 @@ class ToLowerFunctionTest : VimTestCase() {
@Test
fun `test tolower with dict causes error`() {
enterCommand("echo tolower({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E731: Using a Dictionary as a String")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -33,6 +33,7 @@ class ToUpperFunctionTest : VimTestCase() {
@Test
fun `test toupper with list causes error`() {
enterCommand("echo toupper([1, 2, 3])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String")
}
@@ -40,6 +41,7 @@ class ToUpperFunctionTest : VimTestCase() {
@Test
fun `test toupper with dict causes error`() {
enterCommand("echo toupper({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E731: Using a Dictionary as a String")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -207,12 +207,7 @@ class FunctionDeclarationTest : VimTestCase() {
typeText(commandToKeys("echo F1()"))
assertPluginError(true)
assertPluginErrorMessage("E121: Undefined variable: x")
assertExOutput(
"""
E121: Undefined variable: x
0
""".trimIndent()
)
assertExOutput("0")
typeText(commandToKeys("delf! F1"))
typeText(commandToKeys("delf! F2"))

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -154,12 +154,7 @@ class TryCatchTest : VimTestCase() {
),
)
assertPluginError(true)
assertExOutput(
"""
finally block
my exception
""".trimIndent()
)
assertExOutput("finally block")
}
@Test

View File

@@ -50,4 +50,4 @@ class RegisterVariableTest : VimTestCase() {
assertEquals("ab", register.text)
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
) {
enterCommand("set nowrapscan")
}
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
}
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
three
""".trimIndent()
)
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E486: Pattern not found: banana")
}
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
) {
enterCommand("set nowrapscan")
}
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E384: Search hit TOP without match for: three")
}
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
three
""".trimIndent()
)
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E486: Pattern not found: banana")
}
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
)
enterCommand("set nowrapscan")
typeText("10", "/", searchCommand("one"))
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
assertPosition(2, 0)
}
@@ -679,7 +679,7 @@ class SearchGroupTest : VimTestCase() {
)
enterCommand("set nowrapscan")
typeText("12", "?one<CR>")
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E384: Search hit TOP without match for: one")
assertPosition(8, 0)
}

View File

@@ -608,7 +608,7 @@ class NavigateBetweenFoldsTest : FoldActionTestBase() {
typeText("yzj")
val context = injector.executionContextManager.getEditorExecutionContext(fixture.editor.vim)
val regText = injector.registerGroup.getRegister(fixture.editor.vim, context, '0')!!.text
val regText = injector.registerGroup.getRegister(fixture.editor.vim, context, '0')!!.text!!
// Linewise yank includes all lines from cursor to fold start (inclusive)
assertEquals(true, regText.contains("int x = 5"))
assertEquals(true, regText.contains("int y = 10"))

View File

@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["<C-R>"], modes = [Mode.NORMAL])
@CommandOrMotion(keys = ["U", "<C-R>"], modes = [Mode.NORMAL, Mode.VISUAL])
class RedoAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED

View File

@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL])
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL, Mode.VISUAL])
class UndoAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -21,12 +20,8 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/**
* @author vlan
*
* Note: This implementation assumes that the 'gu' command in visual mode is equivalent to 'u'.
* While 'v_gu' is not explicitly documented in Vim help, we treat these commands as identical
* based on observed behavior, without examining Vim's source code.
*/
@CommandOrMotion(keys = ["u", "gu"], modes = [Mode.VISUAL])
@CommandOrMotion(keys = [], modes = [])
class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -21,12 +20,8 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/**
* @author vlan
*
* Note: This implementation assumes that the 'gU' command in visual mode is equivalent to 'U'.
* While 'v_gU' is not explicitly documented in Vim help, we treat these commands as identical
* based on observed behavior, without examining Vim's source code.
*/
@CommandOrMotion(keys = ["U", "gU"], modes = [Mode.VISUAL])
@CommandOrMotion(keys = [], modes = [])
class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE

View File

@@ -69,15 +69,10 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
*/
@VimLockLabel.SelfSynchronized
private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean {
val register: Register? = injector.registerGroup.getRegister(editor, context, key)
val register: Register? = injector.registerGroup.getRegister(key)
if (register != null) {
val textData = PutData.TextData(
register.name,
injector.clipboardManager.dumbCopiedText(register.text),
SelectionType.CHARACTER_WISE
)
val putData =
PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
val textData = PutData.TextData(register.text, SelectionType.CHARACTER_WISE, emptyList(), register.name)
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
injector.put.putText(editor, context, putData)
return true
}

View File

@@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.action.copy
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
@@ -36,33 +35,40 @@ sealed class PutTextBaseAction(
val count = operatorArguments.count1
val sortedCarets = editor.sortedCarets()
return if (sortedCarets.size > 1) {
val caretToPutData = sortedCarets.associateWith { getPutDataForCaret(editor, context, it, count) }
val putData = getPutData(count)
val splitText = putData.textData?.rawText?.split('\n')?.dropLastWhile(String::isEmpty)
val caretToPutData = if (splitText != null && splitText.size == sortedCarets.size) {
sortedCarets.mapIndexed { index, caret -> caret to putData.copy(textData = putData.textData.copy(rawText = splitText[splitText.lastIndex - index])) }.toMap()
} else {
sortedCarets.associateWith { putData }
}
var result = true
caretToPutData.forEach {
result = injector.put.putTextForCaret(editor, it.key, context, it.value) && result
}
result
} else {
val putData = getPutDataForCaret(editor, context, sortedCarets.single(), count)
injector.put.putText(editor, context, putData)
injector.put.putText(editor, context, getPutData(count))
}
}
private fun getPutDataForCaret(
editor: VimEditor,
context: ExecutionContext,
caret: ImmutableVimCaret,
count: Int,
private fun getPutData(count: Int,
): PutData {
val registerService = injector.registerGroup
val registerChar = if (caret.editor.carets().size == 1) {
registerService.currentRegister
} else {
registerService.getCurrentRegisterForMulticaret()
}
val register = caret.registerStorage.getRegister(editor, context, registerChar)
val textData = register?.let { TextData(register) }
return PutData(textData, null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
return PutData(getRegisterTextData(), null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
}
}
fun getRegisterTextData(): TextData? {
val register = injector.registerGroup.getRegister(injector.registerGroup.currentRegister)
return register?.let {
TextData(
register.text ?: injector.parser.toPrintableString(register.keys),
register.type,
register.transferableData,
register.name,
)
}
}

View File

@@ -41,8 +41,22 @@ sealed class PutVisualTextBaseAction(
): Boolean {
if (caretsAndSelections.isEmpty()) return false
val count = cmd.count
val caretToPutData =
editor.sortedCarets().associateWith { getPutDataForCaret(editor, context, it, caretsAndSelections[it], count) }
val sortedCarets =
editor.sortedCarets()
val textData = getRegisterTextData()
val splitText = textData?.rawText?.split('\n')?.dropLastWhile(String::isEmpty)
val caretToTextData = if (splitText != null && splitText.size == sortedCarets.size) {
sortedCarets.mapIndexed { index, caret -> caret to textData.copy(rawText = splitText[splitText.lastIndex - index]) }.toMap()
} else {
sortedCarets.associateWith { textData }
}
val caretToPutData = caretToTextData.mapValues { (caret, textData) ->
getPutDataForCaret(textData, caret, caretsAndSelections[caret], count)
}
injector.registerGroup.resetRegister()
var result = true
caretToPutData.forEach {
@@ -50,17 +64,11 @@ sealed class PutVisualTextBaseAction(
}
return result
}
private fun getPutDataForCaret(
editor: VimEditor,
context: ExecutionContext,
private fun getPutDataForCaret(textData: PutData.TextData?,
caret: VimCaret,
selection: VimSelection?,
count: Int,
): PutData {
val lastRegisterChar = injector.registerGroup.lastRegisterChar
val register = caret.registerStorage.getRegister(editor, context, lastRegisterChar)
val textData = register?.let { PutData.TextData(register) }
count: Int,): PutData {
val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) }
return PutData(textData, visualSelection, count, insertTextBeforeCaret, indent, caretAfterInsertedText)
}

View File

@@ -118,7 +118,7 @@ open class InsertRegisterActionBase(insertLiterally: Boolean) : InsertCommandLin
replayKeys(editor, context, register.keys)
}
else {
insertText(commandLine, commandLine.caret.offset, register.text)
insertText(commandLine, commandLine.caret.offset, register.text ?: return false)
}
return true
}

View File

@@ -88,8 +88,7 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
else -> throw ExException("Unexpected search label ${argument.label}")
}
// 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()
}

View File

@@ -76,6 +76,13 @@ sealed class TillCharacterMotion(
)
}
injector.motion.setLastFTCmd(tillCharacterMotionType, argument.character)
val offset = if (!finishBeforeCharacter) ""
else if (direction == Direction.FORWARDS) "s-1"
else "s+1"
injector.searchGroup.setLastSearchState(argument.character.let { if (it in "`^$.*[~/\\") "\\$it" else it.toString() }, offset, direction)
return res.toMotionOrError()
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -9,7 +9,6 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.group.visual.VisualChange
import com.maddyhome.idea.vim.group.visual.vimMoveBlockSelectionToOffset
import com.maddyhome.idea.vim.group.visual.vimMoveSelectionToCaret
@@ -17,13 +16,11 @@ import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.helper.VimLockLabel
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.inVisualMode
import javax.swing.KeyStroke
/**
* Immutable interface of the caret. Immutable caret is an important concept of Fleet.
@@ -66,7 +63,7 @@ interface ImmutableVimCaret {
fun hasSelection(): Boolean
var lastSelectionInfo: SelectionInfo
val registerStorage: CaretRegisterStorage
val registerStorage: VimRegisterGroup
val markStorage: LocalMarkStorage
}
@@ -152,19 +149,3 @@ fun VimCaret.moveToMotion(motion: Motion): VimCaret {
this
}
}
interface CaretRegisterStorage {
val caret: ImmutableVimCaret
fun storeText(
editor: VimEditor,
context: ExecutionContext,
range: TextRange,
type: SelectionType,
isDelete: Boolean,
): Boolean
fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register?
fun setKeys(editor: VimEditor, context: ExecutionContext, register: Char, keys: List<KeyStroke>)
fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register)
}

View File

@@ -8,94 +8,4 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.TextRange
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 javax.swing.KeyStroke
abstract class VimCaretBase : VimCaret
open class CaretRegisterStorageBase(override var caret: ImmutableVimCaret) : CaretRegisterStorage,
VimRegisterGroupBase() {
companion object {
private const val ALLOWED_TO_STORE_REGISTERS = RegisterConstants.RECORDABLE_REGISTERS +
RegisterConstants.SMALL_DELETION_REGISTER +
RegisterConstants.BLACK_HOLE_REGISTER +
RegisterConstants.LAST_INSERTED_TEXT_REGISTER +
RegisterConstants.LAST_SEARCH_REGISTER
}
override var lastRegisterChar: Char
get() {
return injector.registerGroup.lastRegisterChar
}
set(_) {}
override var isRegisterSpecifiedExplicitly: Boolean
get() {
return injector.registerGroup.isRegisterSpecifiedExplicitly
}
set(_) {}
override fun storeText(
editor: VimEditor,
context: ExecutionContext,
range: TextRange,
type: SelectionType,
isDelete: Boolean,
): Boolean {
val registerChar = if (caret.editor.carets().size == 1) currentRegister else getCurrentRegisterForMulticaret()
if (caret.isPrimary) {
val registerService = injector.registerGroup
registerService.lastRegisterChar = registerChar
return registerService.storeText(editor, context, caret, range, type, isDelete)
} else {
if (!ALLOWED_TO_STORE_REGISTERS.contains(registerChar)) {
return false
}
val text = preprocessTextBeforeStoring(editor.getText(range), type)
return storeTextInternal(editor, context, range, text, type, registerChar, isDelete)
}
}
override fun getRegister(r: Char): Register? {
val editorStub = injector.fallbackWindow
val contextStub = injector.executionContextManager.getEditorExecutionContext(editorStub)
return getRegister(editorStub, contextStub, r)
}
override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
if (caret.isPrimary || !RegisterConstants.RECORDABLE_REGISTERS.contains(r)) {
return injector.registerGroup.getRegister(editor, context, r)
}
return super.getRegister(editor, context, r) ?: injector.registerGroup.getRegister(editor, context, r)
}
override fun setKeys(register: Char, keys: List<KeyStroke>) {
val editorStub = injector.fallbackWindow
val contextStub = injector.executionContextManager.getEditorExecutionContext(editorStub)
setKeys(editorStub, contextStub, register, keys)
}
override fun setKeys(editor: VimEditor, context: ExecutionContext, register: Char, keys: List<KeyStroke>) {
if (caret.isPrimary) {
injector.registerGroup.setKeys(register, keys)
}
if (!RegisterConstants.RECORDABLE_REGISTERS.contains(register)) {
return
}
return super.setKeys(register, keys)
}
override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) {
if (caret.isPrimary) {
injector.registerGroup.saveRegister(editor, context, r, register)
}
if (!RegisterConstants.RECORDABLE_REGISTERS.contains(r)) {
return
}
return super.saveRegister(editor, context, r, register)
}
}

View File

@@ -236,7 +236,7 @@ interface VimChangeGroup {
operatorArguments: OperatorArguments,
)
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret
fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret
fun insertText(editor: VimEditor, caret: VimCaret, str: String): VimCaret

View File

@@ -179,31 +179,41 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return false
}
}
val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE
val shouldYank = type != null && !isInsertMode && saveToRegister
if (shouldYank && !caret.registerStorage.storeText(editor, context, updatedRange, type, isDelete = true)) {
return false
}
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
val mode = editor.mode
if (type == null ||
(mode == Mode.INSERT || mode == Mode.REPLACE) ||
!saveToRegister ||
injector.registerGroup.storeText(
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
context,
caret,
updatedRange,
type,
true,
!editor.isFirstCaret,
editor.isReversingCarets
)
) {
val startOffsets = updatedRange.startOffsets
val endOffsets = updatedRange.endOffsets
for (i in updatedRange.size() - 1 downTo 0) {
val (newRange, _) = editor.search(
startOffsets[i] to endOffsets[i],
editor,
LineDeleteShift.NL_ON_END
) ?: continue
injector.application.runWriteAction {
editor.deleteString(TextRange(newRange.first, newRange.second))
}
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
}
if (type != null) {
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
return false
}
/**
@@ -213,7 +223,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param caret The caret to start insertion in
* @param str The text to insert
*/
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret {
override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret {
injector.application.runWriteAction {
(editor as MutableVimEditor).insertText(caret, offset, str)
}

View File

@@ -20,7 +20,7 @@ import java.awt.datatransfer.Transferable
* - **Clipboard**: This is supported by all operating systems. It functions as a storage for the common 'copy and paste' operations typically done with Ctrl-C and Ctrl-V.
*/
interface VimClipboardManager {
fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): VimCopiedText?
fun getPrimaryContent(): VimCopiedText?
fun getClipboardContent(editor: VimEditor, context: ExecutionContext): VimCopiedText?

View File

@@ -111,7 +111,8 @@ interface VimEditor {
* This method should perform caret merging after the operations. This is similar to IJ runForEachCaret
* TODO review
*/
val isFirstCaret: Boolean
val isReversingCarets: Boolean
fun forEachCaret(action: (VimCaret) -> Unit)
fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false)
fun isInForEachCaretScope(): Boolean
@@ -209,6 +210,7 @@ interface VimEditor {
}
fun createIndentBySize(size: Int): String
fun getSoftWrapStartAtOffset(offset: Int): Int?
/**
* Returns the collapsed fold region at the specified offset, if any.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -12,25 +12,7 @@ import com.maddyhome.idea.vim.helper.EngineMessageHelper
import org.jetbrains.annotations.PropertyKey
interface VimMessages {
/**
* Displays an informational message to the user.
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showMessage(editor: VimEditor, message: String?)
/**
* Displays an error message to the user (typically in red).
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showErrorMessage(editor: VimEditor, message: String?)
/**
* Legacy method for displaying messages.
* @deprecated Use [showMessage] or [showErrorMessage] instead.
*/
@Deprecated("Use showMessage or showErrorMessage instead", ReplaceWith("showMessage(editor, message)"))
fun showStatusBarMessage(editor: VimEditor?, message: String?)
fun getStatusBarMessage(): String?
fun clearStatusBarMessage()
fun indicateError()
@@ -46,4 +28,13 @@ interface VimMessages {
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
fun updateStatusBar(editor: VimEditor)
fun showMessage(editor: VimEditor, message: String) {
showStatusBarMessage(editor, message)
}
fun showErrorMessage(editor: VimEditor, message: String?) {
showStatusBarMessage(editor, message)
indicateError()
}
}

View File

@@ -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

View File

@@ -33,14 +33,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(

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -29,8 +29,7 @@ interface VimOutputPanel {
* Note: The full text content is not updated in the display until [show] is invoked.
*
* @param text The text to append.
* @param isNewLine Whether to start the appended text on a new line.
* @param messageType The type of message, used to determine text styling.
* @param isNewLine Whether to start the appended text on a new line. Defaults to true.
*/
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
@@ -52,4 +51,4 @@ interface VimOutputPanel {
fun setContent(text: String)
fun clearText()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -26,12 +26,8 @@ interface VimOutputPanelService {
fun getCurrentOutputPanel(): VimOutputPanel?
/**
* Appends text to the existing output panel or creates a new one with the given text and message type.
* Appends text to the existing output panel or creates a new one with the given text.
* Basic method that should be sufficient in most cases.
*/
fun output(
editor: VimEditor,
context: ExecutionContext,
text: String,
messageType: MessageType = MessageType.STANDARD,
)
}
fun output(editor: VimEditor, context: ExecutionContext, text: String)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -13,9 +13,9 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
return getCurrentOutputPanel() ?: create(editor, context)
}
override fun output(editor: VimEditor, context: ExecutionContext, text: String, messageType: MessageType) {
override fun output(editor: VimEditor, context: ExecutionContext, text: String) {
val panel = getOrCreate(editor, context)
panel.addText(text, true, messageType)
panel.addText(text)
panel.show()
}
}
}

View File

@@ -208,4 +208,17 @@ interface VimSearchGroup {
fun isSomeTextHighlighted(): Boolean
fun getCurrentIncsearchResultRange(editor: VimEditor): TextRange?
/**
* Sets the last search state purely for tests
*
* @param pattern The pattern to save. This is the last search pattern, not the last substitute pattern
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search
*/
fun setLastSearchState(
pattern: String,
patternOffset: String,
direction: Direction,
)
}

View File

@@ -1425,8 +1425,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
* @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search
*/
@TestOnly
fun setLastSearchState(
override fun setLastSearchState(
pattern: String,
patternOffset: String,
direction: Direction,

View File

@@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.common
interface VimCopiedText {
val text: String
val transferableData: List<Any>
// TODO Looks like sticky tape, I'm not sure that we need to modify already stored text
fun updateText(newText: String): VimCopiedText

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.common
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
@@ -72,9 +71,9 @@ class VimListenersNotifier {
isReplaceCharListeners.forEach { it.isReplaceCharChanged(editor) }
}
fun notifyYankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
fun notifyYankPerformed(editor: VimEditor, range: TextRange) {
if (!injector.enabler.isEnabled()) return // we remove all the listeners when turning the plugin off, but let's do it just in case
yankListeners.forEach { it.yankPerformed(caretToRange) }
yankListeners.forEach { it.yankPerformed(editor, range) }
}
/**

View File

@@ -8,8 +8,8 @@
package com.maddyhome.idea.vim.common
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
interface VimYankListener: Listener {
fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>)
}
fun yankPerformed(editor: VimEditor, range: TextRange)
}

View File

@@ -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
@@ -226,7 +227,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
@@ -274,7 +283,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
foldRegion.isExpanded = true
}
}
return resultOffset
}

View File

@@ -272,7 +272,11 @@ class ToActionMappingInfo(
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToAction' mapping...")
injector.actionExecutor.executeAction(editor, name = action, context = context)
val commandBuilder = KeyHandler.getInstance().keyHandlerState.commandBuilder
for (i in 0 until commandBuilder.calculateCount0Snapshot().coerceAtLeast(1)) {
injector.actionExecutor.executeAction(editor, name = action, context = context)
}
commandBuilder.resetCount()
}
companion object {

View File

@@ -8,11 +8,11 @@
package com.maddyhome.idea.vim.put
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.state.mode.SelectionType
data class ProcessedTextData(
val registerChar: Char?,
val copiedText: VimCopiedText,
val text: String,
val typeInRegister: SelectionType,
val transferableData: List<Any>,
val registerChar: Char?,
)

View File

@@ -9,9 +9,7 @@
package com.maddyhome.idea.vim.put
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.state.mode.SelectionType
/**
@@ -37,12 +35,9 @@ data class PutData(
)
data class TextData(
val registerChar: Char?,
val copiedText: VimCopiedText,
val rawText: String?,
val typeInRegister: SelectionType,
) {
constructor(register: Register) : this(register.name, register.copiedText, register.type)
val rawText = copiedText.text // TODO do not call it raw text...
}
val transferableData: List<Any>,
val registerChar: Char?,
)
}

View File

@@ -141,7 +141,6 @@ abstract class VimPutBase : VimPut {
if (data.visualSelection?.typeInEditor?.isLine == true && data.textData.typeInRegister.isChar) text += "\n"
// TODO: shouldn't it be adjusted when we are storing the text?
if (data.textData.typeInRegister.isLine && text.isNotEmpty() && text.last() != '\n') text += '\n'
if (data.textData.typeInRegister.isChar && text.lastOrNull() == '\n' && data.visualSelection?.typeInEditor?.isLine == false) {
@@ -150,9 +149,10 @@ abstract class VimPutBase : VimPut {
}
return ProcessedTextData(
data.textData.registerChar,
data.textData.copiedText.updateText(text),
text,
data.textData.typeInRegister,
data.textData.transferableData,
data.textData.registerChar,
)
}
@@ -515,7 +515,7 @@ abstract class VimPutBase : VimPut {
startOffsets.forEach { startOffset ->
val selectionType = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE
val (endOffset, updatedCaret) = putTextInternal(
editor, updated, context, text.copiedText.text, text.typeInRegister, selectionType,
editor, updated, context, text.text, text.typeInRegister, selectionType,
startOffset, data.count, data.indent, data.caretAfterInsertedText,
)
updated = updatedCaret

View File

@@ -620,6 +620,12 @@ class VimRegex(pattern: String) {
override fun nativeCarets(): List<VimCaret> = emptyList()
override val isFirstCaret: Boolean
get() = false
override val isReversingCarets: Boolean
get() = false
override fun forEachCaret(action: (VimCaret) -> Unit) {}
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {}
@@ -787,6 +793,10 @@ class VimRegex(pattern: String) {
return null
}
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return null
}
override fun getAllFoldRegions(): List<VimFoldRegion> {
return emptyList()
}

View File

@@ -8,40 +8,86 @@
package com.maddyhome.idea.vim.register
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.helper.EngineStringHelper
import com.maddyhome.idea.vim.state.mode.SelectionType
import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
// TODO should we prefer keys over text, as they are more informative?
// TODO e.g.  could be both <Esc> and <C-[> after trying to restore original keys
data class Register(
val name: Char,
val keys: List<KeyStroke>,
val type: SelectionType,
val copiedText: VimCopiedText,
) {
val text = copiedText.text
val printableString: String =
EngineStringHelper.toPrintableCharacters(keys) // should be the same as [text], but we can't render control notation properly
class Register {
var name: Char
val type: SelectionType
val keys: MutableList<KeyStroke>
val transferableData: MutableList<out Any>
val rawText: String?
constructor(name: Char, type: SelectionType, keys: MutableList<KeyStroke>) {
this.name = name
this.type = type
this.keys = keys
this.transferableData = mutableListOf()
this.rawText = text
}
constructor(name: Char, type: SelectionType, keys: List<KeyStroke>) : this(
name,
keys,
type,
injector.clipboardManager.dumbCopiedText(injector.parser.toPrintableString(keys))
)
constructor(
name: Char,
type: SelectionType,
text: String,
transferableData: MutableList<out Any>,
) {
this.name = name
this.type = type
this.keys = injector.parser.stringToKeys(text).toMutableList()
this.transferableData = transferableData
this.rawText = text
}
constructor(name: Char, copiedText: VimCopiedText, type: SelectionType) : this(
name,
injector.parser.stringToKeys(copiedText.text),
type,
copiedText
)
constructor(
name: Char,
type: SelectionType,
text: String,
transferableData: MutableList<out Any>,
rawText: String,
) {
this.name = name
this.type = type
this.keys = injector.parser.stringToKeys(text).toMutableList()
this.transferableData = transferableData
this.rawText = rawText
}
override fun toString(): String = "@$name = $printableString"
val text: String?
get() {
val builder = StringBuilder()
for (key in keys) {
val c = key.keyChar
if (c == KeyEvent.CHAR_UNDEFINED) {
return null
}
builder.append(c)
}
return builder.toString()
}
val printableString: String
get() = EngineStringHelper.toPrintableCharacters(keys) // should be the same as [text], but we can't render control notation properly
/**
* Append the supplied text to any existing text.
*/
fun addTextAndResetTransferableData(text: String) {
addKeys(injector.parser.stringToKeys(text))
transferableData.clear()
}
fun prependTextAndResetTransferableData(text: String) {
this.keys.addAll(0, injector.parser.stringToKeys(text))
transferableData.clear()
}
fun addKeys(keys: List<KeyStroke>) {
this.keys.addAll(keys)
}
object KeySorter : Comparator<Register> {
@NonNls
@@ -52,41 +98,3 @@ data class Register(
}
}
}
/**
* Imagine you yanked two lines and have the following content in your register a - foo\nbar\n (register type is line-wise)
* Now, there are three different ways to append content, each with a different outcome:
* - If you append a macro qAbazq, you'll get foo\nbarbaz\n in register `a` and it stays line-wise
* - If you use Vim script and execute let @A = "baz", the result will be foo\nbar\nbaz and the register becomes character-wise
* - If you copy "baz" to register A, it becomes foo\nbar\nbaz\n and stays line-wise
*
* At the moment, we will stick to the third option to not overcomplicate the plugin
* (until there is a user who notices the difference)
*/
fun Register.addText(text: String): Register {
return when (this.type) {
SelectionType.CHARACTER_WISE -> {
Register(
this.name,
injector.clipboardManager.dumbCopiedText(this.text + text),
SelectionType.CHARACTER_WISE
) // todo it's empty for historical reasons, but should we really clear transferable data?
}
SelectionType.LINE_WISE -> {
Register(
this.name,
injector.clipboardManager.dumbCopiedText(this.text + text + (if (text.endsWith('\n')) "" else "\n")),
SelectionType.LINE_WISE
) // todo it's empty for historical reasons, but should we really clear transferable data?
}
SelectionType.BLOCK_WISE -> {
Register(
this.name,
injector.clipboardManager.dumbCopiedText(this.text + "\n" + text),
SelectionType.BLOCK_WISE
) // todo it's empty for historical reasons, but should we really clear transferable data?
}
}
}

View File

@@ -17,6 +17,13 @@ import javax.swing.KeyStroke
interface VimRegisterGroup {
/**
* Get the last register selected by the user
*
* @return The register, null if no such register
*/
@Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getLastRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext)")
val lastRegister: Register?
var lastRegisterChar: Char
val currentRegister: Char
@@ -32,7 +39,6 @@ interface VimRegisterGroup {
val isRegisterSpecifiedExplicitly: Boolean
val defaultRegister: Char
fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register?
fun isValid(reg: Char): Boolean
fun selectRegister(reg: Char): Boolean
fun resetRegister()
@@ -41,6 +47,7 @@ interface VimRegisterGroup {
fun isRegisterWritable(): Boolean
fun isRegisterWritable(reg: Char): Boolean
/** Store text into the last register. */
fun storeText(
editor: VimEditor,
context: ExecutionContext,
@@ -48,18 +55,20 @@ interface VimRegisterGroup {
range: TextRange,
type: SelectionType,
isDelete: Boolean,
forceAppend: Boolean = false,
prependInsteadOfAppend: Boolean = false
): Boolean
/**
* Stores text to any writable register (used for the let command)
*/
fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean
/**
* Stores text to any writable register (used for multicaret tests)
*/
@TestOnly
fun storeText(
editor: VimEditor,
context: ExecutionContext,
register: Char,
text: String,
selectionType: SelectionType,
): Boolean
fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType): Boolean
/**
* Stores text, character wise, in the given special register
@@ -75,7 +84,6 @@ interface VimRegisterGroup {
* preferable to yank from the fixture editor.
*/
fun storeTextSpecial(register: Char, text: String): Boolean
@Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext, char)")
fun getRegister(r: Char): Register?
fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register?

View File

@@ -17,7 +17,6 @@ import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimCopiedText
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger
@@ -75,9 +74,13 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
override val defaultRegister: Char
get() = defaultRegisterChar
override fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register? {
return getRegister(editor, context, lastRegisterChar)
}
/**
* Get the last register selected by the user
*
* @return The register, null if no such register
*/
override val lastRegister: Register?
get() = getRegister(lastRegisterChar)
private val onClipboardChanged: () -> Unit = {
val clipboardOptionValue = injector.globalOptions().clipboard
@@ -113,13 +116,20 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return if (isValid(reg)) {
isRegisterSpecifiedExplicitly = true
lastRegisterChar = reg
logger.debug { "register selected: $lastRegisterChar" }
logger.debug { "register selected: $lastRegister" }
true
} else {
false
}
}
override fun getRegister(r: Char): Register? {
val dummyEditor = injector.fallbackWindow
val dummyContext = injector.executionContextManager.getEditorExecutionContext(dummyEditor)
return getRegister(dummyEditor, dummyContext, r)
}
/**
* Reset the selected register back to the default register.
*/
@@ -175,6 +185,8 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
type: SelectionType,
register: Char,
isDelete: Boolean,
forceAppend: Boolean,
prependInsteadOfAppend: Boolean,
): Boolean {
// Null register doesn't get saved, but acts like it was
if (lastRegisterChar == BLACK_HOLE_REGISTER) return true
@@ -193,50 +205,62 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
end = t
}
val copiedText =
if (start != -1) { // FIXME: so, we had invalid ranges all the time?.. I've never handled such cases
injector.clipboardManager.collectCopiedText(editor, context, range, text)
} else {
injector.clipboardManager.dumbCopiedText(text)
}
logger.debug { "Copy to '$lastRegisterChar' with copied text: $copiedText" }
// If this is an uppercase register, we need to append the text to the corresponding lowercase register
if (Character.isUpperCase(register)) {
val transferableData: List<Any> =
if (start != -1) injector.clipboardManager.getTransferableData(editor, range) else ArrayList()
var processedText =
if (start != -1) injector.clipboardManager.preprocessText(editor, range, text, transferableData) else text
logger.debug {
val transferableClasses = transferableData.joinToString(",") { it.javaClass.name }
"Copy to '$lastRegister' with transferable data: $transferableClasses"
}
if (Character.isUpperCase(register) || forceAppend) {
if (forceAppend && type == SelectionType.CHARACTER_WISE) {
processedText = if (prependInsteadOfAppend)
processedText + '\n'
else
'\n' + processedText
}
val lreg = Character.toLowerCase(register)
val r = myRegisters[lreg]
// Append the text if the lowercase register existed
if (r != null) {
myRegisters[lreg] = r.addText(copiedText.text)
if (prependInsteadOfAppend) {
r.prependTextAndResetTransferableData(processedText)
}
else {
r.addTextAndResetTransferableData(processedText)
}
} else {
myRegisters[lreg] = Register(lreg, copiedText, type)
logger.debug { "register '$register' contains: \"$copiedText\"" }
myRegisters[lreg] = Register(lreg, type, processedText, ArrayList(transferableData))
logger.debug { "register '$register' contains: \"$processedText\"" }
} // Set the text if the lowercase register didn't exist yet
} else {
myRegisters[register] = Register(register, copiedText, type)
logger.debug { "register '$register' contains: \"$copiedText\"" }
myRegisters[register] = Register(register, type, processedText, ArrayList(transferableData))
logger.debug { "register '$register' contains: \"$processedText\"" }
} // Put the text in the specified register
if (register == CLIPBOARD_REGISTER) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
if (!isRegisterSpecifiedExplicitly && !isDelete && isPrimaryRegisterSupported() && OptionConstants.clipboard_unnamedplus in injector.globalOptions().clipboard) {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
}
}
if (register == PRIMARY_REGISTER) {
if (isPrimaryRegisterSupported()) {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
if (!isRegisterSpecifiedExplicitly && !isDelete && OptionConstants.clipboard_unnamed in injector.globalOptions().clipboard) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
}
} else {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
}
}
// Also add it to the unnamed register if the default wasn't specified
if (register != UNNAMED_REGISTER && ".:/".indexOf(register) == -1) {
myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, copiedText, type)
logger.debug { "register '$UNNAMED_REGISTER' contains: \"$copiedText\"" }
myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, type, processedText, ArrayList(transferableData))
logger.debug { "register '$UNNAMED_REGISTER' contains: \"$processedText\"" }
}
if (isDelete) {
@@ -256,26 +280,26 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
while (d >= '1') {
val t = myRegisters[d]
if (t != null) {
val incName = (d.code + 1).toChar()
myRegisters[incName] = Register(incName, t.copiedText, t.type)
t.name = (d.code + 1).toChar()
myRegisters[(d.code + 1).toChar()] = t
}
d--
}
myRegisters['1'] = Register('1', copiedText, type)
myRegisters['1'] = Register('1', type, processedText, ArrayList(transferableData))
}
// Deletes smaller than one line and without specified register go the the "-" register
if (smallInlineDeletion && register == defaultRegister) {
myRegisters[SMALL_DELETION_REGISTER] =
Register(SMALL_DELETION_REGISTER, copiedText, type)
Register(SMALL_DELETION_REGISTER, type, processedText, ArrayList(transferableData))
}
} else if (register == defaultRegister) {
myRegisters['0'] = Register('0', copiedText, type)
logger.debug { "register '0' contains: \"$copiedText\"" }
myRegisters['0'] = Register('0', type, processedText, ArrayList(transferableData))
logger.debug { "register '0' contains: \"$processedText\"" }
} // Yanks also go to register 0 if the default register was used
return true
}
/**
* Store text into the last register.
*
@@ -292,10 +316,12 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
range: TextRange,
type: SelectionType,
isDelete: Boolean,
forceAppend: Boolean,
prependInsteadOfAppend: Boolean
): Boolean {
if (isRegisterWritable()) {
val text = preprocessTextBeforeStoring(editor.getText(range), type)
return storeTextInternal(editor, context, range, text, type, lastRegisterChar, isDelete)
return storeTextInternal(editor, context, range, text, type, lastRegisterChar, isDelete, forceAppend, prependInsteadOfAppend)
}
return false
@@ -330,53 +356,35 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
if (READONLY_REGISTERS.indexOf(register) == -1 && register != LAST_SEARCH_REGISTER && register != UNNAMED_REGISTER) {
return false
}
myRegisters[register] = Register(
register,
injector.clipboardManager.dumbCopiedText(text),
myRegisters[register] = Register(register,
SelectionType.CHARACTER_WISE
) // TODO why transferable data is not collected?
, text, ArrayList())
logger.debug { "register '$register' contains: \"$text\"" }
return true
}
@Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext, char)")
override fun getRegister(r: Char): Register? {
val dummyEditor = injector.fallbackWindow
val dummyContext = injector.executionContextManager.getEditorExecutionContext(dummyEditor)
return getRegister(dummyEditor, dummyContext, r)
}
override fun storeText(
editor: VimEditor,
context: ExecutionContext,
register: Char,
text: String,
selectionType: SelectionType,
): Boolean {
if (!WRITABLE_REGISTERS.contains(register)) {
return false
}
logger.debug { "register '$register' contains: \"$text\"" }
val oldRegister = getRegister(editor, context, register.lowercaseChar())
val newRegister = if (register.isUpperCase() && oldRegister != null) {
oldRegister.addText(text)
} else {
Register(
register,
injector.clipboardManager.dumbCopiedText(text),
selectionType
) // FIXME why don't we collect transferable data?
}
saveRegister(editor, context, register, newRegister)
if (register == '/') {
injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register
}
return true
}
override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean {
return storeText(editor, context, register, text, SelectionType.CHARACTER_WISE)
}
override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType,
): Boolean {
if (!WRITABLE_REGISTERS.contains(register)) {
return false
}
logger.debug { "register '$register' contains: \"$text\"" }
val textToStore = if (register.isUpperCase()) {
(getRegister(register.lowercaseChar())?.rawText ?: "") + text
} else {
text
}
val reg = Register(register, selectionType, textToStore, ArrayList())
saveRegister(editor, context, register, reg)
if (register == '/') {
injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register
}
return true
}
private fun guessSelectionType(text: String): SelectionType {
return if (text.endsWith("\n")) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE
@@ -389,10 +397,10 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
* @param r - the register character corresponding to either the primary selection (*) or clipboard selection (+)
* @return the content of the selection, if available, otherwise null
*/
private fun refreshClipboardRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
private fun refreshClipboardRegister(r: Char): Register? {
return when (r) {
PRIMARY_REGISTER -> refreshPrimaryRegister(editor, context)
CLIPBOARD_REGISTER -> refreshClipboardRegister(editor, context)
PRIMARY_REGISTER -> refreshPrimaryRegister()
CLIPBOARD_REGISTER -> refreshClipboardRegister()
else -> throw RuntimeException("Clipboard register expected, got $r")
}
}
@@ -401,56 +409,60 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return System.getenv("DISPLAY") != null && injector.systemInfoService.isXWindow
}
private fun setSystemPrimaryRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) {
logger.trace("Setting text: $copiedText to primary selection...")
private fun setSystemPrimaryRegisterText(text: String, rawText: String, transferableData: List<Any>) {
logger.trace("Setting text: $text to primary selection...")
if (isPrimaryRegisterSupported()) {
try {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText)
injector.clipboardManager.setClipboardText(text, rawText, transferableData)
} catch (e: Exception) {
logger.warn("False positive X11 primary selection support")
logger.trace("Setting text to primary selection failed. Setting it to clipboard selection instead")
setSystemClipboardRegisterText(editor, context, copiedText)
setSystemClipboardRegisterText(text, rawText, transferableData)
}
} else {
logger.trace("X11 primary selection is not supporting. Setting clipboard selection instead")
setSystemClipboardRegisterText(editor, context, copiedText)
setSystemClipboardRegisterText(text, rawText, transferableData)
}
}
private fun setSystemClipboardRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText)
private fun setSystemClipboardRegisterText(text: String, rawText: String, transferableData: List<Any>) {
injector.clipboardManager.setClipboardText(text, rawText, transferableData)
}
private fun refreshPrimaryRegister(editor: VimEditor, context: ExecutionContext): Register? {
private fun refreshPrimaryRegister(): Register? {
logger.trace("Syncing cached primary selection value..")
if (!isPrimaryRegisterSupported()) {
logger.trace("X11 primary selection is not supported. Syncing clipboard selection..")
return refreshClipboardRegister(editor, context)
return refreshClipboardRegister()
}
try {
val clipboardData = injector.clipboardManager.getPrimaryContent(editor, context) ?: return null
val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null
val currentRegister = myRegisters[PRIMARY_REGISTER]
if (currentRegister != null && clipboardData.text == currentRegister.text) {
val text = clipboardData.text
val transferableData = clipboardData.transferableData.toMutableList()
if (currentRegister != null && text == currentRegister.text) {
return currentRegister
}
return Register(PRIMARY_REGISTER, clipboardData, guessSelectionType(clipboardData.text))
return transferableData?.let { Register(PRIMARY_REGISTER, guessSelectionType(text), text, it) }
} catch (e: Exception) {
logger.warn("False positive X11 primary selection support")
logger.trace("Syncing primary selection failed. Syncing clipboard selection instead")
return refreshClipboardRegister(editor, context)
return refreshClipboardRegister()
}
}
private fun refreshClipboardRegister(editor: VimEditor, context: ExecutionContext): Register? {
private fun refreshClipboardRegister(): Register? {
// for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage
val systemAwareClipboardRegister = if (isPrimaryRegisterSupported()) CLIPBOARD_REGISTER else PRIMARY_REGISTER
val clipboardData = injector.clipboardManager.getClipboardContent(editor, context) ?: return null
val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null
val currentRegister = myRegisters[systemAwareClipboardRegister]
if (currentRegister != null && clipboardData.text == currentRegister.text) {
val text = clipboardData.text
val transferableData = clipboardData.transferableData.toMutableList()
if (currentRegister != null && text == currentRegister.text) {
return currentRegister
}
return Register(systemAwareClipboardRegister, clipboardData, guessSelectionType(clipboardData.text))
return transferableData?.let { Register(systemAwareClipboardRegister, guessSelectionType(text), text, it) }
}
override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
@@ -460,36 +472,34 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
myR = Character.toLowerCase(myR)
}
return if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) refreshClipboardRegister(
editor,
context,
myR
) else myRegisters[myR]
myR) else myRegisters[myR]
}
override fun getRegisters(editor: VimEditor, context: ExecutionContext): List<Register> {
val filteredRegisters = myRegisters.values.filterNot { CLIPBOARD_REGISTERS.contains(it.name) }.toMutableList()
val clipboardRegisters = CLIPBOARD_REGISTERS
.filterNot { it == CLIPBOARD_REGISTER && !isPrimaryRegisterSupported() } // for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage
.mapNotNull { refreshClipboardRegister(editor, context, it) }
.mapNotNull { refreshClipboardRegister(it) }
return (filteredRegisters + clipboardRegisters).sortedWith(Register.KeySorter)
}
override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) {
var myR = if (Character.isUpperCase(r)) Character.toLowerCase(r) else r
val text = register.text
val rawText = register.rawText
if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) {
if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0 && text != null && rawText != null) {
when (myR) {
CLIPBOARD_REGISTER -> {
if (!isPrimaryRegisterSupported()) {
// it looks wrong, but for some reason non-X systems use the * register to store the clipboard content
myR = PRIMARY_REGISTER
}
setSystemClipboardRegisterText(editor, context, register.copiedText)
setSystemClipboardRegisterText(text, rawText, ArrayList(register.transferableData))
}
PRIMARY_REGISTER -> {
setSystemPrimaryRegisterText(editor, context, register.copiedText)
setSystemPrimaryRegisterText(text, rawText, ArrayList(register.transferableData))
}
}
}
@@ -507,7 +517,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
}
override fun getPlaybackRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
return if (PLAYBACK_REGISTERS.indexOf(r) != 0) getRegister(editor, context, r) else null
return if (PLAYBACK_REGISTERS.indexOf(r) != 0) getRegister(r) else null
}
override fun recordText(text: String) {
@@ -530,7 +540,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
if (register != null) {
var reg: Register? = null
if (Character.isUpperCase(register)) {
reg = getRegister(editor, context, register)
reg = getRegister(register)
}
val myRecordList = recordList
@@ -539,7 +549,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
reg = Register(Character.toLowerCase(register), SelectionType.CHARACTER_WISE, myRecordList)
myRegisters[Character.toLowerCase(register)] = reg
} else {
myRegisters[reg.name.lowercaseChar()] = reg.addText(injector.parser.toPrintableString(myRecordList))
reg.addKeys(myRecordList)
}
}
}

View File

@@ -1,187 +0,0 @@
/*
* Copyright 2003-2025 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.thinapi
import com.intellij.vim.api.VimApi
import com.intellij.vim.api.models.CaretId
import com.intellij.vim.api.models.Mode
import com.intellij.vim.api.models.Range
import com.intellij.vim.api.scopes.ListenersScope
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.EditorListener
import com.maddyhome.idea.vim.common.Listener
import com.maddyhome.idea.vim.common.ListenerOwner
import com.maddyhome.idea.vim.common.MacroRecordingListener
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimPluginListener
import com.maddyhome.idea.vim.common.VimYankListener
import com.maddyhome.idea.vim.key.MappingOwner
import kotlinx.coroutines.runBlocking
import com.maddyhome.idea.vim.state.mode.Mode as EngineMode
class ListenerScopeImpl(
private val listenerOwner: ListenerOwner,
private val mappingOwner: MappingOwner,
private val projectId: String,
) : ListenersScope {
private abstract class ListenerBase(listenerOwner: ListenerOwner) : Listener {
final override val owner: ListenerOwner = listenerOwner
fun launch(block: suspend () -> Unit) {
runBlocking { block() }
}
}
override fun onModeChange(callback: suspend VimApi.(Mode) -> Unit) {
val listener = object : ModeChangeListener, ListenerBase(listenerOwner) {
override fun modeChanged(
editor: VimEditor,
oldMode: EngineMode,
) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback(oldMode.toMode())
}
}
}
injector.listenersNotifier.modeChangeListeners.add(listener)
}
override fun onYank(callback: suspend VimApi.(Map<CaretId, Range.Simple>) -> Unit) {
val listener = object : VimYankListener, ListenerBase(listenerOwner) {
override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) {
val caretToRangeMap: Map<CaretId, Range.Simple> =
caretToRange.map { (caret, range) ->
CaretId(caret.id) to Range.Simple(range.startOffset, range.endOffset)
}.toMap()
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback(caretToRangeMap)
}
}
}
injector.listenersNotifier.yankListeners.add(listener)
}
override fun onEditorCreate(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun created(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onEditorRelease(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun released(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onEditorFocusGain(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun focusGained(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onEditorFocusLost(callback: suspend VimApi.() -> Unit) {
val listener = object : EditorListener, ListenerBase(listenerOwner) {
override fun focusLost(editor: VimEditor) {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.myEditorListeners.add(listener)
}
override fun onMacroRecordingStart(callback: suspend VimApi.() -> Unit) {
val listener = object : MacroRecordingListener, ListenerBase(listenerOwner) {
override fun recordingStarted() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
override fun recordingFinished() {
// Not used in this listener
}
}
injector.listenersNotifier.macroRecordingListeners.add(listener)
}
override fun onMacroRecordingFinish(callback: suspend VimApi.() -> Unit) {
val listener = object : MacroRecordingListener, ListenerBase(listenerOwner) {
override fun recordingStarted() {
// Not used in this listener
}
override fun recordingFinished() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.macroRecordingListeners.add(listener)
}
override fun onIdeaVimEnabled(callback: suspend VimApi.() -> Unit) {
val listener = object : VimPluginListener, ListenerBase(listenerOwner) {
override fun turnedOn() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
override fun turnedOff() {
// Not used in this listener
}
}
injector.listenersNotifier.vimPluginListeners.add(listener)
}
override fun onIdeaVimDisabled(callback: suspend VimApi.() -> Unit) {
val listener = object : VimPluginListener, ListenerBase(listenerOwner) {
override fun turnedOn() {
// Not used in this listener
}
override fun turnedOff() {
val vimApi = VimApiImpl(listenerOwner, mappingOwner, projectId)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.vimPluginListeners.add(listener)
}
}

View File

@@ -132,7 +132,7 @@ class CaretReadImpl(
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
val caret: VimCaret = vimEditor.carets().find { it.id == caretId.id } ?: return null
val register: Register = caret.registerStorage.getRegister(vimEditor, context, register) ?: return null
return RegisterData(register.text, register.type.toTextSelectionType())
return RegisterData(register.text ?: return null, register.type.toTextSelectionType())
}
override fun getReg(register: Char): String? {

View File

@@ -81,7 +81,7 @@ class CaretTransactionImpl(
beforeCaret: Boolean,
): Boolean {
val copiedText = injector.clipboardManager.dumbCopiedText(text)
val textData = PutData.TextData(null, copiedText, SelectionType.CHARACTER_WISE)
val textData = PutData.TextData(null, SelectionType.CHARACTER_WISE, copiedText.transferableData, null)
val putData = PutData(
textData = textData,

View File

@@ -83,7 +83,7 @@ sealed class Command(
if (Flag.SAVE_SELECTION !in argFlags.flags) {
// Editor.inBlockSelection is not available, because we're not in Visual mode anymore. Check if the primary caret
// currently has a selection and if (when we still in Visual) it was a block selection.
injector.application.runReadAction {
injector.application.runWriteAction {
if (editor.primaryCaret().hasSelection() && editor.primaryCaret().lastSelectionInfo.selectionType.isBlock) {
editor.removeSecondaryCarets()
}

View File

@@ -11,6 +11,7 @@ package com.maddyhome.idea.vim.vimscript.model.commands
import com.intellij.vim.annotations.ExCommand
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getText
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range
@@ -37,7 +38,7 @@ data class CopyTextCommand(val range: Range, val modifier: CommandModifier, val
val carets = editor.sortedCarets()
for (caret in carets) {
val range = getLineRange(editor, caret).toTextRange(editor)
val copiedText = injector.clipboardManager.collectCopiedText(editor, context, range)
val text = editor.getText(range)
// Copy is defined as:
// :[range]co[py] {address}
@@ -46,7 +47,8 @@ data class CopyTextCommand(val range: Range, val modifier: CommandModifier, val
// the line _before_ the first line (i.e., copy to above the first line).
val address1 = getAddressFromArgument(editor)
val textData = PutData.TextData(null, copiedText, SelectionType.LINE_WISE)
val transferableData = injector.clipboardManager.getTransferableData(editor, range)
val textData = PutData.TextData(text, SelectionType.LINE_WISE, transferableData, null)
var mutableCaret = caret
val putData = if (address1 == 0) {
// TODO: This should maintain current column location

View File

@@ -89,7 +89,7 @@ data class MoveTextCommand(val range: Range, val modifier: CommandModifier, val
val selectionEndOffset = lastSelectionInfo.end?.let { editor.bufferPositionToOffset(it) }
val text = editor.getText(sourceRange)
val textData = PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.LINE_WISE)
val textData = PutData.TextData(text, SelectionType.LINE_WISE, emptyList(), null)
val dropNewLineInEnd = (targetLineAfterDeletion + linesMoved == editor.lineCount() - 1 && text.last() == '\n') ||
(sourceLineRange.endLine == editor.lineCount() - 1)

View File

@@ -50,7 +50,12 @@ data class PutLinesCommand(val range: Range, val modifier: CommandModifier, val
val line = if (range.size() == 0) -1 else getLine(editor)
val textData = registerGroup.getRegister(editor, context, registerGroup.lastRegisterChar)?.let {
PutData.TextData(null, it.copiedText, SelectionType.LINE_WISE)
PutData.TextData(
it.text ?: injector.parser.toKeyNotation(it.keys),
SelectionType.LINE_WISE,
it.transferableData,
null,
)
}
val putData = PutData(
textData,

View File

@@ -14,6 +14,7 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.helper.EngineStringHelper
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
@@ -41,7 +42,8 @@ data class RegistersCommand(val range: Range, val modifier: CommandModifier, val
SelectionType.CHARACTER_WISE -> "c"
SelectionType.BLOCK_WISE -> "b"
}
" $type \"${reg.name} ${reg.printableString.take(200)}"
val text = reg.rawText?.let { injector.parser.parseKeys(it) } ?: reg.keys
" $type \"${reg.name} ${EngineStringHelper.toPrintableCharacters(text).take(200)}"
}
injector.outputPanel.output(editor, context, regs)

View File

@@ -31,8 +31,23 @@ interface VimYankGroup {
operatorArguments: OperatorArguments,
): Boolean
/**
* This yanks count lines of text
*
* @param editor The editor to yank from
* @param count The number of lines to yank
* @return true if able to yank the lines, false if not
*/
fun yankLine(editor: VimEditor, context: ExecutionContext, count: Int): Boolean
/**
* This yanks a range of text
*
* @param editor The editor to yank from
* @param range The range of text to yank
* @param type The type of yank
* @return true if able to yank the range, false if not
*/
fun yankRange(
editor: VimEditor,
context: ExecutionContext,

View File

@@ -10,39 +10,59 @@ package com.maddyhome.idea.vim.yank
import com.maddyhome.idea.vim.action.motion.updown.MotionDownLess1FirstNonSpaceAction
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret
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.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.state.mode.SelectionType
import org.jetbrains.annotations.Contract
import kotlin.math.min
open class YankGroupBase : VimYankGroup {
private fun yankRange(
editor: VimEditor,
context: ExecutionContext,
caretToRange: Map<ImmutableVimCaret, Pair<TextRange, SelectionType>>,
range: TextRange,
type: SelectionType,
startOffsets: Map<VimCaret, Int>?,
): Boolean {
startOffsets?.forEach { (caret, offset) ->
caret.moveToOffset(offset)
}
injector.listenersNotifier.notifyYankPerformed(caretToRange.mapValues { it.value.first })
injector.listenersNotifier.notifyYankPerformed(editor, range)
return injector.registerGroup.storeText(editor, context, editor.primaryCaret(), range, type, false)
}
var result = true
for ((caret, myRange) in caretToRange) {
result = caret.registerStorage.storeText(editor, context, myRange.first, myRange.second, false) && result
@Contract("_, _ -> new")
protected fun getTextRange(ranges: List<Pair<Int, Int>>, type: SelectionType): TextRange? {
if (ranges.isEmpty()) return null
val size = ranges.size
val starts = IntArray(size)
val ends = IntArray(size)
if (type == SelectionType.LINE_WISE) {
starts[size - 1] = ranges[size - 1].first
ends[size - 1] = ranges[size - 1].second
for (i in 0 until size - 1) {
val range = ranges[i]
starts[i] = range.first
ends[i] = range.second - 1
}
} else {
for (i in 0 until size) {
val range = ranges[i]
starts[i] = range.first
ends[i] = range.second
}
}
return result
return TextRange(starts, ends)
}
/**
@@ -55,11 +75,12 @@ open class YankGroupBase : VimYankGroup {
operatorArguments: OperatorArguments,
): Boolean {
val motion = argument as? Argument.Motion ?: return false
val motionType = motion.getMotionType()
val nativeCaretCount = editor.nativeCarets().size
if (nativeCaretCount <= 0) return false
val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>(nativeCaretCount)
val ranges = ArrayList<Pair<Int, Int>>(nativeCaretCount)
// This logic is from original vim
val startOffsets =
@@ -69,39 +90,24 @@ open class YankGroupBase : VimYankGroup {
HashMap<VimCaret, Int>(nativeCaretCount)
}
for (caret in editor.nativeCarets()) {
var motionType = motion.getMotionType()
val motionRange = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments)
?: continue
assert(motionRange.size() == 1)
ranges.add(motionRange.startOffset to motionRange.endOffset)
startOffsets?.put(caret, motionRange.normalize().startOffset)
// Yank motion commands that are not linewise become linewise if all the following are true:
// 1) The range is across multiple lines
// 2) There is only whitespace before the start of the range
// 3) There is only whitespace after the end of the range
if (argument.motion is MotionActionHandler && argument.motion.motionType == MotionType.EXCLUSIVE) {
val start = editor.offsetToBufferPosition(motionRange.startOffset)
val end = editor.offsetToBufferPosition(motionRange.endOffset)
if (start.line != end.line
&& !editor.anyNonWhitespace(motionRange.startOffset, -1)
&& !editor.anyNonWhitespace(motionRange.endOffset, 1)
) {
motionType = SelectionType.LINE_WISE
}
}
caretToRange[caret] = TextRange(motionRange.startOffset, motionRange.endOffset) to motionType
}
if (caretToRange.isEmpty()) return false
val range = getTextRange(ranges, motionType) ?: return false
if (range.size() == 0) return false
return yankRange(
editor,
context,
caretToRange,
range,
motionType,
startOffsets,
)
}
@@ -115,18 +121,18 @@ open class YankGroupBase : VimYankGroup {
*/
override fun yankLine(editor: VimEditor, context: ExecutionContext, count: Int): Boolean {
val caretCount = editor.nativeCarets().size
val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>(caretCount)
val ranges = ArrayList<Pair<Int, Int>>(caretCount)
for (caret in editor.nativeCarets()) {
val start = injector.motion.moveCaretToCurrentLineStart(editor, caret)
val end =
min(injector.motion.moveCaretToRelativeLineEnd(editor, caret, count - 1, true) + 1, editor.fileSize().toInt())
val end = min(injector.motion.moveCaretToRelativeLineEnd(editor, caret, count - 1, true) + 1, editor.fileSize().toInt())
if (end == -1) continue
caretToRange[caret] = TextRange(start, end) to SelectionType.LINE_WISE
ranges.add(start to end)
}
return yankRange(editor, context, caretToRange, null)
val range = getTextRange(ranges, SelectionType.LINE_WISE) ?: return false
return yankRange(editor, context, range, SelectionType.LINE_WISE, null)
}
/**
@@ -137,15 +143,8 @@ open class YankGroupBase : VimYankGroup {
* @param type The type of yank
* @return true if able to yank the range, false if not
*/
override fun yankRange(
editor: VimEditor,
context: ExecutionContext,
range: TextRange?,
type: SelectionType,
moveCursor: Boolean,
): Boolean {
override fun yankRange(editor: VimEditor, context: ExecutionContext, range: TextRange?, type: SelectionType, moveCursor: Boolean): Boolean {
range ?: return false
val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>()
if (type == SelectionType.LINE_WISE) {
for (i in 0 until range.size()) {
@@ -165,19 +164,17 @@ open class YankGroupBase : VimYankGroup {
val startOffsets = HashMap<VimCaret, Int>(editor.nativeCarets().size)
if (type == SelectionType.BLOCK_WISE) {
startOffsets[editor.primaryCaret()] = range.normalize().startOffset
caretToRange[editor.primaryCaret()] = range to type
} else {
for ((i, caret) in editor.nativeCarets().withIndex()) {
val textRange = TextRange(rangeStartOffsets[i], rangeEndOffsets[i])
startOffsets[caret] = textRange.normalize().startOffset
caretToRange[caret] = textRange to type
}
}
return if (moveCursor) {
yankRange(editor, context, caretToRange, startOffsets)
yankRange(editor, context, range, type, startOffsets)
} else {
yankRange(editor, context, caretToRange, null)
yankRange(editor, context, range, type, null)
}
}
}

View File

@@ -427,7 +427,7 @@
{
"keys": "<C-R>",
"class": "com.maddyhome.idea.vim.action.change.RedoAction",
"modes": "N"
"modes": "NX"
},
{
"keys": "<C-R>",
@@ -1042,7 +1042,7 @@
{
"keys": "<Undo>",
"class": "com.maddyhome.idea.vim.action.change.UndoAction",
"modes": "N"
"modes": "NX"
},
{
"keys": "<Up>",
@@ -1261,8 +1261,8 @@
},
{
"keys": "U",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction",
"modes": "X"
"class": "com.maddyhome.idea.vim.action.change.RedoAction",
"modes": "NX"
},
{
"keys": "V",
@@ -1744,11 +1744,6 @@
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperMotionAction",
"modes": "N"
},
{
"keys": "gU",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction",
"modes": "X"
},
{
"keys": "g^",
"class": "com.maddyhome.idea.vim.action.motion.leftright.MotionFirstScreenNonSpaceAction",
@@ -1859,11 +1854,6 @@
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerMotionAction",
"modes": "N"
},
{
"keys": "gu",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction",
"modes": "X"
},
{
"keys": "gv",
"class": "com.maddyhome.idea.vim.action.motion.visual.VisualSelectPreviousAction",
@@ -2067,12 +2057,7 @@
{
"keys": "u",
"class": "com.maddyhome.idea.vim.action.change.UndoAction",
"modes": "N"
},
{
"keys": "u",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction",
"modes": "X"
"modes": "NX"
},
{
"keys": "v",