1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-10-15 13:12:01 +02:00

Compare commits

..

26 Commits

Author SHA1 Message Date
1d4a14defd Set plugin version to chylex-52 2025-10-08 05:36:15 +02:00
021a94d9bb Improve support for recording macros with code completion
Fixes wrong recorded inputs when code completion introduces an import.

Fixes wrong recorded inputs when completing a static member with a partial type name. Example: `WiE.A` -> `WindowEvent.ACTION_EVENT_MASK`
2025-10-08 05:36:01 +02:00
9f6bffcf7d Preserve visual mode after executing IDE action 2025-10-06 22:19:03 +02:00
80222af0bf Make g0/g^/g$ work with soft wraps 2025-10-06 22:19:03 +02:00
57ea1ecb69 Make gj/gk jump over soft wraps 2025-10-06 22:19:03 +02:00
3b65b55929 Make camelCase motions adjust based on direction of visual selection 2025-10-06 22:19:02 +02:00
097924e078 Make search highlights temporary 2025-10-06 22:19:02 +02:00
8d092693b9 Exit insert mode after refactoring 2025-10-06 22:19:02 +02:00
42780c052b Add action to run last macro in all opened files 2025-10-06 22:19:02 +02:00
bf5eb879f9 Stop macro execution after a failed search 2025-10-06 22:19:02 +02:00
51e9c9be1c Revert per-caret registers 2025-10-06 22:19:02 +02:00
f4137d2769 Apply scrolloff after executing native IDEA actions 2025-10-06 22:19:02 +02:00
9c9284a201 Stay on same line after reindenting 2025-10-06 22:19:02 +02:00
84c30d1afc Update search register when using f/t 2025-10-06 22:19:02 +02:00
0de4b4fdde Automatically add unambiguous imports after running a macro 2025-10-06 22:19:02 +02:00
1ccb75e6b8 Fix(VIM-3986): Exception when pasting register contents containing new line 2025-10-06 22:19:02 +02:00
6671642428 Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2025-10-06 22:19:02 +02:00
6cef05bfbb Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2025-10-06 22:19:02 +02:00
e961dce249 Add support for count for visual and line motion surround 2025-10-06 22:19:00 +02:00
47937cb382 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2025-10-06 22:18:55 +02:00
10552bef28 Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2025-10-05 01:25:09 +02:00
140fe1ec6c Respect count with <Action> mappings 2025-10-05 01:25:09 +02:00
ecb2416457 Change matchit plugin to use HTML patterns in unrecognized files 2025-10-05 01:25:09 +02:00
a8de488629 Reset insert mode when switching active editor 2025-10-05 01:25:09 +02:00
ba55ffe7e4 Remove notifications about configuration options 2025-10-05 01:25:09 +02:00
a6ba575ef9 Set custom plugin version 2025-10-05 01:25:08 +02:00
80 changed files with 968 additions and 874 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -138,6 +138,7 @@ dependencies {
// AceJump is an optional dependency. We use their SessionManager class to check if it's active // AceJump is an optional dependency. We use their SessionManager class to check if it's active
plugin("AceJump", "3.8.19") plugin("AceJump", "3.8.19")
plugin("com.intellij.classic.ui", "251.23774.318")
bundledPlugins("org.jetbrains.plugins.terminal") bundledPlugins("org.jetbrains.plugins.terminal")
@@ -236,6 +237,7 @@ tasks {
// a custom task (see below) // a custom task (see below)
runIde { runIde {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true) systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
systemProperty("idea.trust.all.projects", "true")
} }
// Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies // Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies

View File

@@ -20,7 +20,7 @@ ideaVersion=2025.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IC ideaType=IC
instrumentPluginCode=true instrumentPluginCode=true
version=SNAPSHOT version=chylex-52
javaVersion=21 javaVersion=21
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1
@@ -41,7 +41,6 @@ youtrackToken=
# Gradle settings # Gradle settings
org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.configuration-cache=true
org.gradle.caching=true org.gradle.caching=true
# Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary # Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary

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

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

View File

@@ -230,7 +230,7 @@ private object FileTypePatterns {
} else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") { } else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
this.cMakePatterns this.cMakePatterns
} else { } 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.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret 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.VimEditor
import com.maddyhome.idea.vim.api.endsWithNewLine import com.maddyhome.idea.vim.api.endsWithNewLine
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset 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.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.findBlockRange import com.maddyhome.idea.vim.group.findBlockRange
import com.maddyhome.idea.vim.helper.exitVisualMode 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.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.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
@@ -78,7 +82,7 @@ internal class VimSurroundExtension : VimExtension {
putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true) putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true)
} }
VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator()) VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false, count = 1)) // TODO
} }
private class YSurroundHandler : ExtensionHandler { private class YSurroundHandler : ExtensionHandler {
@@ -105,7 +109,7 @@ internal class VimSurroundExtension : VimExtension {
val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset) val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
if (lastNonWhiteSpaceOffset != null) { if (lastNonWhiteSpaceOffset != null) {
val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1) val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
performSurround(pair, range, it) performSurround(pair, range, it, count = operatorArguments.count1)
} }
// it.moveToOffset(lineStartOffset) // it.moveToOffset(lineStartOffset)
} }
@@ -128,15 +132,13 @@ internal class VimSurroundExtension : VimExtension {
private class VSurroundHandler : ExtensionHandler { private class VSurroundHandler : ExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// NB: Operator ignores SelectionType anyway // 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 return
} }
runWriteAction { runWriteAction {
// Leave visual mode // Leave visual mode
editor.exitVisualMode() editor.exitVisualMode()
editor.ij.caretModel.moveToOffset(selectionStart)
// Reset the key handler so that the command trie is updated for the new mode (Normal) // 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 // TODO: This should probably be handled by ToHandlerMapping.execute
@@ -159,6 +161,10 @@ internal class VimSurroundExtension : VimExtension {
companion object { companion object {
fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) { 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 // Save old register values for carets
val surroundings = editor.sortedCarets() val surroundings = editor.sortedCarets()
.map { .map {
@@ -201,7 +207,7 @@ internal class VimSurroundExtension : VimExtension {
val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue
it.first + trimmedValue + it.second it.first + trimmedValue + it.second
} ?: innerValue } ?: innerValue
val textData = PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.CHARACTER_WISE) val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), null)
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false) val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = false)
surrounding.caret to putData surrounding.caret to putData
@@ -278,20 +284,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 { override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
val ijEditor = editor.ij val ijEditor = editor.ij
val c = injector.keyGroup.getChar(editor) ?: return true val c = injector.keyGroup.getChar(editor) ?: return true
val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false 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 runWriteAction {
performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE) val change = VimPlugin.getChange()
// Jump back to start if (supportsMultipleCursors) {
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor) ijEditor.runWithEveryCaretAndRestore {
applyOnce(ijEditor, change, pair, count)
}
}
else {
applyOnce(ijEditor, change, pair, count)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
}
}
return true 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? { private fun getSurroundRange(caret: VimCaret): TextRange? {
val editor = caret.editor val editor = caret.editor
if (editor.mode is Mode.CMD_LINE) { if (editor.mode is Mode.CMD_LINE) {
@@ -380,15 +407,15 @@ 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) {
runWriteAction { runWriteAction {
val editor = caret.editor val editor = caret.editor
val change = VimPlugin.getChange() 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 isEOF = range.endOffset == editor.text().length
val hasNewLine = editor.endsWithNewLine() val hasNewLine = editor.endsWithNewLine()
val rightSurround = if (tagsOnNewLines) { val rightSurround = (if (tagsOnNewLines) {
if (isEOF && !hasNewLine) { if (isEOF && !hasNewLine) {
"\n" + pair.second "\n" + pair.second
} else { } else {
@@ -396,7 +423,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare
} }
} else { } else {
pair.second pair.second
} }).let { RepeatedCharSequence.of(it, count) }
change.insertText(editor, caret, range.startOffset, leftSurround) change.insertText(editor, caret, range.startOffset, leftSurround)
change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround) change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)

View File

@@ -43,7 +43,6 @@ import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
import kotlin.math.min
/** /**
* Provides all the insert/replace related functionality * Provides all the insert/replace related functionality
@@ -156,6 +155,7 @@ class ChangeGroup : VimChangeGroupBase() {
context: ExecutionContext, context: ExecutionContext,
range: TextRange, range: TextRange,
) { ) {
val startPos = editor.offsetToBufferPosition(caret.offset)
val startOffset = editor.getLineStartForOffset(range.startOffset) val startOffset = editor.getLineStartForOffset(range.startOffset)
val endOffset = editor.getLineEndForOffset(range.endOffset) val endOffset = editor.getLineEndForOffset(range.endOffset)
val ijEditor = (editor as IjVimEditor).editor val ijEditor = (editor as IjVimEditor).editor
@@ -165,7 +165,7 @@ class ChangeGroup : VimChangeGroupBase() {
var copiedText: IjVimCopiedText? = null var copiedText: IjVimCopiedText? = null
try { try {
if (injector.registerGroup.isPrimaryRegisterSupported()) { if (injector.registerGroup.isPrimaryRegisterSupported()) {
copiedText = injector.clipboardManager.getPrimaryContent(editor, context) as IjVimCopiedText copiedText = injector.clipboardManager.getPrimaryContent() as IjVimCopiedText
} }
} catch (e: Exception) { } catch (e: Exception) {
// FIXME: [isPrimaryRegisterSupported()] is not implemented perfectly, so there might be thrown an exception after trying to access the primary selection // FIXME: [isPrimaryRegisterSupported()] is not implemented perfectly, so there might be thrown an exception after trying to access the primary selection
@@ -180,11 +180,7 @@ class ChangeGroup : VimChangeGroupBase() {
} }
} }
val afterAction = { val afterAction = {
val firstLine = editor.offsetToBufferPosition( caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, startPos.line))
min(startOffset.toDouble(), endOffset.toDouble()).toInt()
).line
val newOffset = injector.motion.moveCaretToLineStartSkipLeading(editor, firstLine)
caret.moveToOffset(newOffset)
restoreCursor(editor, caret, (caret as IjVimCaret).caret.logicalPosition.line) restoreCursor(editor, caret, (caret as IjVimCaret).caret.logicalPosition.line)
} }
if (project != null) { if (project != null) {

View File

@@ -141,7 +141,7 @@ object IjOptions {
// Temporary feature flags during development, not really intended for external use // Temporary feature flags during development, not really intended for external use
val closenotebooks: ToggleOption = val closenotebooks: ToggleOption =
addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true)) 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)) val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
val vimHints: ToggleOption = addOption(ToggleOption("vimhints", GLOBAL, "vimhints", false, isHidden = true)) val vimHints: ToggleOption = addOption(ToggleOption("vimhints", GLOBAL, "vimhints", false, isHidden = true))

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

@@ -21,6 +21,7 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.macro.VimMacroBase import com.maddyhome.idea.vim.macro.VimMacroBase
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
/** /**
* Used to handle playback of macros * Used to handle playback of macros
@@ -89,6 +90,9 @@ internal class MacroGroup : VimMacroBase() {
} finally { } finally {
keyStack.removeFirst() keyStack.removeFirst()
} }
if (!isInternalMacro) {
MacroAutoImport.run(editor.ij, context.ij)
}
} }
if (isInternalMacro) { if (isInternalMacro) {

View File

@@ -87,6 +87,9 @@ internal class MotionGroup : VimMotionGroupBase() {
} }
override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion { override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineStart)
}
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, false) return moveCaretToColumn(editor, caret, col, false)
} }
@@ -95,6 +98,15 @@ internal class MotionGroup : VimMotionGroupBase() {
editor: VimEditor, editor: VimEditor,
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int { ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
val offset = caret.ij.visualLineStart
val line = editor.offsetToBufferPosition(offset).line
return if (offset == editor.getLineStartOffset(line)) {
editor.getLeadingCharacterOffset(line, 0)
} else {
offset
}
}
val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
val bufferLine = caret.getLine() val bufferLine = caret.getLine()
return editor.getLeadingCharacterOffset(bufferLine, col) return editor.getLeadingCharacterOffset(bufferLine, col)
@@ -105,6 +117,9 @@ internal class MotionGroup : VimMotionGroupBase() {
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
allowEnd: Boolean, allowEnd: Boolean,
): Motion { ): Motion {
if (editor.ij.softWrapModel.isSoftWrappingEnabled) {
return AbsoluteOffset(caret.ij.visualLineEnd - 1)
}
val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line) val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line)
return moveCaretToColumn(editor, caret, col, allowEnd) return moveCaretToColumn(editor, caret, col, allowEnd)
} }

View File

@@ -33,7 +33,6 @@ import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.handler.KeyMapIssue import com.maddyhome.idea.vim.handler.KeyMapIssue
import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.MessageHelper
@@ -41,8 +40,6 @@ import com.maddyhome.idea.vim.icons.VimIcons
import com.maddyhome.idea.vim.key.ShortcutOwner import com.maddyhome.idea.vim.key.ShortcutOwner
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ijOptions
import com.maddyhome.idea.vim.options.OptionConstants
import com.maddyhome.idea.vim.statistic.ActionTracker import com.maddyhome.idea.vim.statistic.ActionTracker
import com.maddyhome.idea.vim.ui.VimEmulationConfigurable import com.maddyhome.idea.vim.ui.VimEmulationConfigurable
import com.maddyhome.idea.vim.vimscript.services.VimRcService import com.maddyhome.idea.vim.vimscript.services.VimRcService
@@ -62,55 +59,11 @@ internal class NotificationService(private val project: Project?) {
@Suppress("unused") @Suppress("unused")
constructor() : this(null) constructor() : this(null)
fun notifyAboutIdeaPut() { fun notifyAboutNewUndo() {}
val notification = Notification(
IDEAVIM_NOTIFICATION_ID,
IDEAVIM_NOTIFICATION_TITLE,
"""Add <code>ideaput</code> to <code>clipboard</code> option to perform a put via the IDE<br/><b><code>set clipboard+=ideaput</code></b>""",
NotificationType.INFORMATION,
)
notification.addAction(OpenIdeaVimRcAction(notification)) fun notifyAboutIdeaPut() {}
notification.addAction( fun notifyAboutIdeaJoin(editor: VimEditor) {}
AppendToIdeaVimRcAction(
notification,
"set clipboard^=ideaput",
"ideaput",
) {
// Technically, we're supposed to prepend values to clipboard so that it's not added to the "exclude" item.
// Since we don't handle exclude, it's safe to append. But let's be clean.
injector.globalOptions().clipboard.prependValue(OptionConstants.clipboard_ideaput)
},
)
notification.notify(project)
}
fun notifyAboutIdeaJoin(editor: VimEditor) {
val notification = Notification(
IDEAVIM_NOTIFICATION_ID,
IDEAVIM_NOTIFICATION_TITLE,
"""Put <b><code>set ideajoin</code></b> into your <code>~/.ideavimrc</code> to perform a join via the IDE""",
NotificationType.INFORMATION,
)
notification.addAction(OpenIdeaVimRcAction(notification))
notification.addAction(
AppendToIdeaVimRcAction(
notification,
"set ideajoin",
"ideajoin"
) {
// This is a global-local option. Setting it will always set the global value
injector.ijOptions(editor).ideajoin = true
},
)
notification.addAction(HelpLink(ideajoinExamplesUrl))
notification.notify(project)
}
fun enableRepeatingMode() = Messages.showYesNoDialog( fun enableRepeatingMode() = Messages.showYesNoDialog(
"Do you want to enable repeating keys in macOS on press and hold?\n\n" + "Do you want to enable repeating keys in macOS on press and hold?\n\n" +
@@ -218,7 +171,7 @@ internal class NotificationService(private val project: Project?) {
is KeyMapIssue.AddShortcut -> { is KeyMapIssue.AddShortcut -> {
appendLine("- ${it.key} key is not assigned to the ${it.action} action.<br/>") appendLine("- ${it.key} key is not assigned to the ${it.action} action.<br/>")
} }
is KeyMapIssue.RemoveShortcut -> { is KeyMapIssue.RemoveShortcut -> {
appendLine("- ${it.shortcut} key is incorrectly assigned to the ${it.action} action.<br/>") appendLine("- ${it.shortcut} key is incorrectly assigned to the ${it.action} action.<br/>")
} }
@@ -305,16 +258,16 @@ internal class NotificationService(private val project: Project?) {
notification = notification =
Notification(IDEAVIM_NOTIFICATION_ID, IDEAVIM_NOTIFICATION_TITLE, content, NotificationType.INFORMATION).also { Notification(IDEAVIM_NOTIFICATION_ID, IDEAVIM_NOTIFICATION_TITLE, content, NotificationType.INFORMATION).also {
it.whenExpired { notification = null } it.whenExpired { notification = null }
it.addAction(StopTracking()) it.addAction(StopTracking())
if (id != null || possibleIDs?.size == 1) { if (id != null || possibleIDs?.size == 1) {
it.addAction(CopyActionId(id ?: possibleIDs?.get(0), project)) it.addAction(CopyActionId(id ?: possibleIDs?.get(0), project))
}
it.notify(project)
} }
it.notify(project)
}
if (id != null) { if (id != null) {
ActionTracker.Util.logTrackedAction(id) ActionTracker.Util.logTrackedAction(id)
} }

View File

@@ -25,10 +25,9 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
/** /**
* This group works with command associated with copying and pasting text * This group works with command associated with copying and pasting text
*/ */
@@ -128,7 +127,7 @@ public class RegisterGroup extends VimRegisterGroupBase implements PersistentSta
final String text = VimPlugin.getXML().getSafeXmlText(textElement); final String text = VimPlugin.getXML().getSafeXmlText(textElement);
if (text != null) { if (text != null) {
logger.trace("Register data parsed"); logger.trace("Register data parsed");
register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type); register = new Register(key, type, text, Collections.emptyList());
} }
else { else {
logger.trace("Cannot parse register data"); 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.ide.isRider
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import com.maddyhome.idea.vim.newapi.IjVimCaret 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.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
@@ -128,7 +127,7 @@ internal class PutGroup : VimPutBase() {
point.dispose() point.dispose()
if (!caret.isValid) return@forEach 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) { val endOffset = if (data.indent) {
doIndent( doIndent(
vimEditor, vimEditor,
@@ -180,10 +179,12 @@ internal class PutGroup : VimPutBase() {
val allContentsBefore = CopyPasteManager.getInstance().allContents val allContentsBefore = CopyPasteManager.getInstance().allContents
val sizeBeforeInsert = allContentsBefore.size val sizeBeforeInsert = allContentsBefore.size
val firstItemBefore = allContentsBefore.firstOrNull() val firstItemBefore = allContentsBefore.firstOrNull()
logger.debug { "Copied text: ${text.copiedText}" } logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" }
val (textContent, transferableData) = text.copiedText as IjVimCopiedText
val origContent: TextBlockTransferable = 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 allContentsAfter = CopyPasteManager.getInstance().allContents
val sizeAfterInsert = allContentsAfter.size val sizeAfterInsert = allContentsAfter.size
try { try {
@@ -191,7 +192,7 @@ internal class PutGroup : VimPutBase() {
} finally { } finally {
val textInClipboard = (firstItemBefore as? TextBlockTransferable) val textInClipboard = (firstItemBefore as? TextBlockTransferable)
?.getTransferData(DataFlavor.stringFlavor) as? String ?.getTransferData(DataFlavor.stringFlavor) as? String
val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.text val textOnTop = textInClipboard != null && textInClipboard != text.text
if (sizeBeforeInsert != sizeAfterInsert || textOnTop) { if (sizeBeforeInsert != sizeAfterInsert || textOnTop) {
// Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register // Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) } (CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }

View File

@@ -352,7 +352,7 @@ public class EditorHelper {
final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight); final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
final @NotNull VimEditor editor1 = new IjVimEditor(editor); 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); 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. // 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,7 +12,9 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.util.ui.table.JBTableRowEditor import com.intellij.util.ui.table.JBTableRowEditor
@@ -21,6 +23,8 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.group.IjOptionConstants import com.maddyhome.idea.vim.group.IjOptionConstants
import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint
import com.maddyhome.idea.vim.newapi.globalIjOptions 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 java.awt.Component
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JTable import javax.swing.JTable
@@ -98,8 +102,7 @@ internal fun Editor.isPrimaryEditor(): Boolean {
internal fun Editor.isTerminalEditor(): Boolean { internal fun Editor.isTerminalEditor(): Boolean {
return !isViewer return !isViewer
&& document.isWritable && document.isWritable
&& !EditorHelper.isFileEditor(this) && this.editorKind == EditorKind.CONSOLE
&& !EditorHelper.isDiffEditor(this)
} }
// Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor // Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor
@@ -132,3 +135,41 @@ internal val Caret.vimLine: Int
*/ */
internal val Editor.vimLine: Int internal val Editor.vimLine: Int
get() = this.caretModel.currentCaret.vimLine get() = this.caretModel.currentCaret.vimLine
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

@@ -25,15 +25,19 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.progress.util.ProgressIndicatorUtils import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.refactoring.actions.BaseRefactoringAction
import com.maddyhome.idea.vim.RegisterActions import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction import com.maddyhome.idea.vim.api.NativeAction
import com.maddyhome.idea.vim.api.VimActionExecutor import com.maddyhome.idea.vim.api.VimActionExecutor
import com.maddyhome.idea.vim.api.VimEditor 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.command.OperatorArguments
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.newapi.IjNativeAction import com.maddyhome.idea.vim.newapi.IjNativeAction
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.awt.Component import java.awt.Component
import javax.swing.JComponent import javax.swing.JComponent
@@ -70,6 +74,12 @@ internal class IjActionExecutor : VimActionExecutor {
thisLogger().error("Actions cannot be updated when write-action is running or pending") thisLogger().error("Actions cannot be updated when write-action is running or pending")
} }
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 val ijAction = (action as IjNativeAction).action
try { try {
isRunningActionFromVim = true isRunningActionFromVim = true
@@ -79,6 +89,20 @@ internal class IjActionExecutor : VimActionExecutor {
val place = ijAction.choosePlace() val place = ijAction.choosePlace()
val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true) val res = ActionManager.getInstance().tryToExecute(ijAction, null, contextComponent, place, true)
res.waitFor(5_000) res.waitFor(5_000)
if (startVisualModeType != null && startVisualCaretSelection != null) {
val primaryCaret = editor.primaryCaret()
val endVisualCaretOffset = primaryCaret.offset
if (startVisualCaretSelection.first != endVisualCaretOffset) {
if (!editor.inVisualMode || (editor.mode as Mode.VISUAL).selectionType != startVisualModeType) {
injector.visualMotionGroup.toggleVisual(editor, 1, 0, startVisualModeType)
}
primaryCaret.moveToOffset(startVisualCaretSelection.first)
primaryCaret.setSelection(startVisualCaretSelection.second, startVisualCaretSelection.third)
primaryCaret.moveToOffset(endVisualCaretOffset)
}
}
return res.isDone return res.isDone
} finally { } finally {
isRunningActionFromVim = false isRunningActionFromVim = false

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. // that this needs to be replaced as a more or less dumb line for line rewrite.
val topLine = getVisualLineAtTopOfScreen(editor) val topLine = getVisualLineAtTopOfScreen(editor)
val bottomLine = getVisualLineAtBottomOfScreen(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 // 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 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.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes 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.VimEditor
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector 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 org.jetbrains.annotations.Contract
import java.awt.Font import java.awt.Font
import java.util.* import java.util.*
import javax.swing.Timer
internal fun updateSearchHighlights( internal fun updateSearchHighlights(
pattern: String?, pattern: String?,
@@ -84,6 +86,12 @@ internal fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, en
) )
} }
val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = Timer(400) {
removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear()
}
/** /**
* Refreshes current search highlights for all visible editors * Refreshes current search highlights for all visible editors
*/ */
@@ -125,27 +133,43 @@ private fun updateSearchHighlights(
// hlsearch (+ incsearch/noincsearch) // hlsearch (+ incsearch/noincsearch)
// Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given // 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 // `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows
val vimEditor = editor.vim val isSearching = injector.commandLine.getActiveCommandLine() != null
val editorLastLine = vimEditor.lineCount() - 1 application.invokeLater {
val searchStartLine = searchRange?.startLine ?: 0 val vimEditor = editor.vim
val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine) val editorLastLine = vimEditor.lineCount() - 1
if (searchStartLine <= editorLastLine) { val searchStartLine = searchRange?.startLine ?: 0
val results = val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine)
injector.searchHelper.findAll( if (searchStartLine <= editorLastLine) {
vimEditor, val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished
pattern, val visibleTopLeft = visibleArea.location
searchStartLine, val visibleBottomRight = visibleArea.location.apply { translate(visibleArea.width, visibleArea.height) }
searchEndLine, val visibleStartOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleTopLeft))
shouldIgnoreCase(pattern, shouldIgnoreSmartCase) val visibleEndOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleBottomRight))
) val visibleStartLine = editor.document.getLineNumber(visibleStartOffset)
if (results.isNotEmpty()) { val visibleEndLine = editor.document.getLineNumber(visibleEndOffset)
if (editor === currentEditor?.ij) { removeSearchHighlights(editor)
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
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)) { } else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) {
// nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor // 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 // 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 return currentEditorCurrentMatchOffset
} }
@@ -204,7 +229,7 @@ private fun removeSearchHighlights(editor: Editor) {
*/ */
@Contract("_, _, false -> false; _, null, true -> false") @Contract("_, _, false -> false; _, null, true -> false")
private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean { 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( private fun findClosestMatch(

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.registry.Registry
import com.intellij.util.PlatformUtils import com.intellij.util.PlatformUtils
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
@@ -29,6 +30,8 @@ import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.newapi.IjVimCaret import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij 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 import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
/** /**
@@ -82,15 +85,7 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService {
// TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
editor.runWithChangeTracking { editor.runWithChangeTracking {
undoManager.undo(fileEditor) undoManager.undo(fileEditor)
restoreVisualMode(editor)
// 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)
} }
} else { } else {
runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) { runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
@@ -241,4 +236,21 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService {
val hasChanges: Boolean val hasChanges: Boolean
get() = changeListener.hasChanged || initialPath != editor.getPath() get() = changeListener.hasChanged || initialPath != editor.getPath()
} }
private fun restoreVisualMode(editor: VimEditor) {
if (!editor.inVisualMode && editor.getSelectionModel().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.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolder
import com.maddyhome.idea.vim.api.CaretRegisterStorageBase
import com.maddyhome.idea.vim.api.LocalMarkStorage import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo import com.maddyhome.idea.vim.api.SelectionInfo
import com.maddyhome.idea.vim.common.InsertSequence import com.maddyhome.idea.vim.common.InsertSequence
@@ -98,7 +97,6 @@ internal var Caret.vimInsertStart: RangeMarker by userDataOr {
} }
// TODO: Data could be lost during visual block motion // TODO: Data could be lost during visual block motion
internal var Caret.registerStorage: CaretRegisterStorageBase? by userDataCaretToEditor()
internal var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor() internal var Caret.markStorage: LocalMarkStorage? by userDataCaretToEditor()
internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor() internal var Caret.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor()

View File

@@ -64,8 +64,10 @@ class IJEditorFocusListener : EditorListener {
VimPlugin.getChange().insertBeforeCaret(editor, context) VimPlugin.getChange().insertBeforeCaret(editor, context)
KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true) KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true)
} }
if (isCurrentEditorTerminal && !ijEditor.inInsertMode) { if (isCurrentEditorTerminal) {
switchToInsertMode.run() if (!ijEditor.inInsertMode) {
switchToInsertMode.run()
}
} else if (ijEditor.isInsertMode && (oldEditorInfo.isInsertModeForced || !ijEditor.document.isWritable)) { } else if (ijEditor.isInsertMode && (oldEditorInfo.isInsertModeForced || !ijEditor.document.isWritable)) {
val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor) val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor)
val mode = injector.vimState.mode val mode = injector.vimState.mode

View File

@@ -16,7 +16,9 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
import com.intellij.codeInsight.template.Template import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManagerListener import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener import com.intellij.find.FindModelListener
import com.intellij.ide.actions.ApplyIntentionAction import com.intellij.ide.actions.ApplyIntentionAction
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
@@ -31,6 +33,7 @@ import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actions.EnterAction import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.keymap.KeymapManager import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
@@ -63,6 +66,7 @@ internal object IdeaSpecifics {
private val surrounderAction = private val surrounderAction =
"com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction" "com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
private var editor: Editor? = null private var editor: Editor? = null
private var caretOffset = -1
private var completionData: CompletionData? = null private var completionData: CompletionData? = null
override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) { override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
@@ -71,6 +75,7 @@ internal object IdeaSpecifics {
val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR) val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
if (hostEditor != null) { if (hostEditor != null) {
editor = hostEditor editor = hostEditor
caretOffset = hostEditor.caretModel.offset
} }
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
@@ -147,26 +152,46 @@ internal object IdeaSpecifics {
if (VimPlugin.isNotEnabled()) return if (VimPlugin.isNotEnabled()) return
val editor = editor val editor = editor
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) { if (editor != null) {
completionData?.recordCompletion(editor, VimPlugin.getRegister()) if (action is ChooseItemAction && injector.registerGroup.isRecording) {
} completionData?.recordCompletion(editor, VimPlugin.getRegister()
)
}
//region Enter insert mode after surround with if //region Enter insert mode after surround with if
if (surrounderAction == action.javaClass.name && surrounderItems.any { if (surrounderAction == action.javaClass.name && surrounderItems.any {
action.templatePresentation.text.endsWith( action.templatePresentation.text.endsWith(
it, it,
) )
}
) {
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?.let { editor.vim.exitInsertMode(event.dataContext.vim)
it.vim.mode = Mode.NORMAL() KeyHandler.getInstance().reset(editor.vim)
VimPlugin.getChange().insertBeforeCaret(it.vim, event.dataContext.vim) }
KeyHandler.getInstance().reset(it.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.editor = null
this.caretOffset = -1
this.completionData?.dispose() this.completionData?.dispose()
this.completionData = null this.completionData = null

View File

@@ -97,6 +97,7 @@ import com.maddyhome.idea.vim.newapi.IjVimSearchGroup
import com.maddyhome.idea.vim.newapi.InsertTimeRecorder import com.maddyhome.idea.vim.newapi.InsertTimeRecorder
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.selectionType import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
@@ -410,10 +411,21 @@ internal object VimListenerManager {
override fun selectionChanged(event: FileEditorManagerEvent) { override fun selectionChanged(event: FileEditorManagerEvent) {
// We can't rely on being passed a non-null editor, so check for Code With Me scenarios explicitly // We can't rely on being passed a non-null editor, so check for Code With Me scenarios explicitly
if (VimPlugin.isNotEnabled() || !ClientId.isCurrentlyUnderLocalId) return if (VimPlugin.isNotEnabled() || !ClientId.isCurrentlyUnderLocalId) return
val newEditor = event.newEditor
if (newEditor is TextEditor) {
val editor = newEditor.editor
if (editor.isInsertMode) {
editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim)
}
// Breaks relativenumber for some reason
// injector.scroll.scrollCaretIntoView(editor.vim)
}
MotionGroup.fileEditorManagerSelectionChangedCallback(event) MotionGroup.fileEditorManagerSelectionChangedCallback(event)
FileGroup.fileEditorManagerSelectionChangedCallback(event) FileGroup.fileEditorManagerSelectionChangedCallback(event)
VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event) // VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event) IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event)
VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor) VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor)
} }

View File

@@ -39,7 +39,7 @@ import java.io.IOException
@Service @Service
internal class IjClipboardManager : VimClipboardManager { internal class IjClipboardManager : VimClipboardManager {
override fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): IjVimCopiedText? { override fun getPrimaryContent(): IjVimCopiedText? {
val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null
val contents = clipboard.getContents(null) ?: return null val contents = clipboard.getContents(null) ?: return null
val (text, transferableData) = getTextAndTransferableData(contents) ?: return null val (text, transferableData) = getTextAndTransferableData(contents) ?: return null
@@ -242,6 +242,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) 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.LogicalPosition
import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.editor.VisualPosition
import com.maddyhome.idea.vim.api.BufferPosition 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.ImmutableVimCaret
import com.maddyhome.idea.vim.api.LocalMarkStorage import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo import com.maddyhome.idea.vim.api.SelectionInfo
@@ -21,6 +19,7 @@ import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimCaretBase import com.maddyhome.idea.vim.api.VimCaretBase
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimVisualPosition 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.InsertSequence
import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.group.visual.VisualChange 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.lastSelectionInfo
import com.maddyhome.idea.vim.helper.markStorage import com.maddyhome.idea.vim.helper.markStorage
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset 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.resetVimLastColumn
import com.maddyhome.idea.vim.helper.vimInsertStart import com.maddyhome.idea.vim.helper.vimInsertStart
import com.maddyhome.idea.vim.helper.vimLastColumn 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.vimLine
import com.maddyhome.idea.vim.helper.vimSelectionStart import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimSelectionStartClear import com.maddyhome.idea.vim.helper.vimSelectionStartClear
import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
internal class IjVimCaret(val caret: Caret) : VimCaretBase() { internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
override val registerStorage: CaretRegisterStorage override val registerStorage: VimRegisterGroup
get() { get() = injector.registerGroup
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 markStorage: LocalMarkStorage override val markStorage: LocalMarkStorage
get() { get() {
var storage = this.caret.markStorage var storage = this.caret.markStorage

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.editor.ex.ScrollingModelEx
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.impl.CaretModelImpl import com.intellij.openapi.editor.impl.CaretModelImpl
import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.VirtualFileManager
import com.maddyhome.idea.vim.api.BufferPosition import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
@@ -149,7 +150,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
} }
} }
} }
editor.document.insertString(atPosition, text) editor.document.insertString(atPosition, StringUtil.convertLineSeparators(text, "\n"))
} }
override fun replaceString(start: Int, end: Int, newString: String) { override fun replaceString(start: Int, end: Int, newString: String) {
@@ -178,21 +179,38 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
return editor.caretModel.allCarets.map { IjVimCaret(it) } return editor.caretModel.allCarets.map { IjVimCaret(it) }
} }
override var isFirstCaret = true
override var isReversingCarets = false
@Suppress("ideavimRunForEachCaret") @Suppress("ideavimRunForEachCaret")
override fun forEachCaret(action: (VimCaret) -> Unit) { override fun forEachCaret(action: (VimCaret) -> Unit) {
if (editor.vim.inBlockSelection) { if (editor.vim.inBlockSelection) {
action(IjVimCaret(editor.caretModel.primaryCaret)) action(IjVimCaret(editor.caretModel.primaryCaret))
} else { } else {
editor.caretModel.runForEachCaret({ try {
if (it.isValid) { editor.caretModel.runForEachCaret({
action(IjVimCaret(it)) if (it.isValid) {
} action(IjVimCaret(it))
}, false) isFirstCaret = false
}
}, false)
} finally {
isFirstCaret = true
}
} }
} }
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) { 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 { override fun isInForEachCaretScope(): Boolean {
@@ -502,6 +520,10 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
} }
} }
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return editor.softWrapModel.getSoftWrap(offset)?.start
}
override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T { override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T {
return caret return caret
} }

View File

@@ -283,7 +283,7 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
.calculateCount0Snapshot() .calculateCount0Snapshot()
) )
if (labelText == "/" || labelText == "?" || searchCommand) { if ((labelText == "/" || labelText == "?" || searchCommand) && !injector.macro.isExecutingMacro) {
val forwards = labelText != "?" // :s, :g, :v are treated as forwards val forwards = labelText != "?" // :s, :g, :v are treated as forwards
val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0) val patternEnd: Int = injector.searchGroup.findEndOfPattern(searchText, separator, 0)
val pattern = searchText.take(patternEnd) val pattern = searchText.take(patternEnd)

View File

@@ -1,12 +1,4 @@
<!-- <idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
~ 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
~ https://opensource.org/licenses/MIT.
-->
<idea-plugin url="https://plugins.jetbrains.com/plugin/164">
<name>IdeaVim</name> <name>IdeaVim</name>
<id>IdeaVIM</id> <id>IdeaVIM</id>
<description><![CDATA[ <description><![CDATA[
@@ -21,7 +13,7 @@
<li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li> <li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li>
</ul> </ul>
]]></description> ]]></description>
<version>SNAPSHOT</version> <version>chylex</version>
<vendor>JetBrains</vendor> <vendor>JetBrains</vendor>
<!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) --> <!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) -->
@@ -246,6 +238,7 @@
</group> </group>
<action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/> <action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
<action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
</actions> </actions>
<extensions defaultExtensionNs="IdeaVIM"> <extensions defaultExtensionNs="IdeaVIM">

View File

@@ -78,5 +78,10 @@
"keys": "gJ", "keys": "gJ",
"class": "com.maddyhome.idea.vim.action.change.delete.DeleteJoinVisualLinesAction", "class": "com.maddyhome.idea.vim.action.change.delete.DeleteJoinVisualLinesAction",
"modes": "X" "modes": "X"
},
{
"keys": "z@",
"class": "com.maddyhome.idea.vim.action.macro.PlaybackRegisterInOpenFilesAction",
"modes": "N"
} }
] ]

View File

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

View File

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

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.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler 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() { class RedoAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED 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.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler 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() { class UndoAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion 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.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -21,12 +20,8 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/** /**
* @author vlan * @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() { class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.action.change.change package com.maddyhome.idea.vim.action.change.change
import com.intellij.vim.annotations.CommandOrMotion 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.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroup import com.maddyhome.idea.vim.api.VimChangeGroup
@@ -21,12 +20,8 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
/** /**
* @author vlan * @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() { class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE

View File

@@ -70,15 +70,10 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
*/ */
@VimLockLabel.SelfSynchronized @VimLockLabel.SelfSynchronized
private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean { 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) { if (register != null) {
val textData = PutData.TextData( val textData = PutData.TextData(register.text, SelectionType.CHARACTER_WISE, emptyList(), register.name)
register.name, val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
injector.clipboardManager.dumbCopiedText(register.text),
SelectionType.CHARACTER_WISE
)
val putData =
PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
injector.put.putText(editor, context, putData) injector.put.putText(editor, context, putData)
return true 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.CommandOrMotion
import com.intellij.vim.annotations.Mode import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext 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.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
@@ -36,33 +35,40 @@ sealed class PutTextBaseAction(
val count = operatorArguments.count1 val count = operatorArguments.count1
val sortedCarets = editor.sortedCarets() val sortedCarets = editor.sortedCarets()
return if (sortedCarets.size > 1) { 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 var result = true
caretToPutData.forEach { caretToPutData.forEach {
result = injector.put.putTextForCaret(editor, it.key, context, it.value) && result result = injector.put.putTextForCaret(editor, it.key, context, it.value) && result
} }
result result
} else { } else {
val putData = getPutDataForCaret(editor, context, sortedCarets.single(), count) injector.put.putText(editor, context, getPutData(count))
injector.put.putText(editor, context, putData)
} }
} }
private fun getPutDataForCaret( private fun getPutData(count: Int,
editor: VimEditor,
context: ExecutionContext,
caret: ImmutableVimCaret,
count: Int,
): PutData { ): PutData {
val registerService = injector.registerGroup return PutData(getRegisterTextData(), null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1)
val registerChar = if (caret.editor.carets().size == 1) { }
registerService.currentRegister }
} else {
registerService.getCurrentRegisterForMulticaret() fun getRegisterTextData(): TextData? {
} val register = injector.registerGroup.getRegister(injector.registerGroup.currentRegister)
val register = caret.registerStorage.getRegister(editor, context, registerChar) return register?.let {
val textData = register?.let { TextData(register) } TextData(
return PutData(textData, null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1) register.text ?: injector.parser.toPrintableString(register.keys),
register.type,
register.transferableData,
register.name,
)
} }
} }

View File

@@ -41,8 +41,22 @@ sealed class PutVisualTextBaseAction(
): Boolean { ): Boolean {
if (caretsAndSelections.isEmpty()) return false if (caretsAndSelections.isEmpty()) return false
val count = cmd.count val count = cmd.count
val caretToPutData = val sortedCarets =
editor.sortedCarets().associateWith { getPutDataForCaret(editor, context, it, caretsAndSelections[it], count) } 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() injector.registerGroup.resetRegister()
var result = true var result = true
caretToPutData.forEach { caretToPutData.forEach {
@@ -50,17 +64,11 @@ sealed class PutVisualTextBaseAction(
} }
return result return result
} }
private fun getPutDataForCaret( private fun getPutDataForCaret(textData: PutData.TextData?,
editor: VimEditor,
context: ExecutionContext,
caret: VimCaret, caret: VimCaret,
selection: VimSelection?, selection: VimSelection?,
count: Int, count: Int,): PutData {
): PutData {
val lastRegisterChar = injector.registerGroup.lastRegisterChar
val register = caret.registerStorage.getRegister(editor, context, lastRegisterChar)
val textData = register?.let { PutData.TextData(register) }
val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) } val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) }
return PutData(textData, visualSelection, count, insertTextBeforeCaret, indent, caretAfterInsertedText) 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) replayKeys(editor, context, register.keys)
} }
else { else {
insertText(commandLine, commandLine.caret.offset, register.text) insertText(commandLine, commandLine.caret.offset, register.text ?: return false)
} }
return true return true
} }

View File

@@ -88,8 +88,7 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
else -> throw ExException("Unexpected search label ${argument.label}") 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.Error
if (offsetAndMotion == null) return Motion.NoMotion
parentAction.motionType = offsetAndMotion.second parentAction.motionType = offsetAndMotion.second
return offsetAndMotion.first.toMotionOrError() return offsetAndMotion.first.toMotionOrError()
} }

View File

@@ -76,6 +76,13 @@ sealed class TillCharacterMotion(
) )
} }
injector.motion.setLastFTCmd(tillCharacterMotionType, argument.character) 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() return res.toMotionOrError()
} }
} }

View File

@@ -31,6 +31,12 @@ class MotionCamelLeftAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (caret.hasSelection() && caret.offset > caret.vimSelectionStart) {
val target = injector.searchHelper.findPreviousCamelEnd(editor.text(), caret.offset, operatorArguments.count1)
if (target != null && target > caret.vimSelectionStart) {
return target.toMotionOrError()
}
}
return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1) return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error ?.toMotionOrError() ?: Motion.Error
} }
@@ -47,6 +53,10 @@ class MotionCamelRightAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (caret.hasSelection() && caret.offset >= caret.vimSelectionStart) {
return injector.searchHelper.findNextCamelEnd(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error
}
return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1) return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1)
?.toMotionOrError() ?: Motion.Error ?.toMotionOrError() ?: Motion.Error
} }

View File

@@ -70,6 +70,6 @@ class MotionDownNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1) return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1, bufferLines = true)
} }
} }

View File

@@ -70,6 +70,6 @@ class MotionUpNotLineWiseAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1) return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1, bufferLines = true)
} }
} }

View File

@@ -9,7 +9,6 @@
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.LiveRange 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.VisualChange
import com.maddyhome.idea.vim.group.visual.vimMoveBlockSelectionToOffset import com.maddyhome.idea.vim.group.visual.vimMoveBlockSelectionToOffset
import com.maddyhome.idea.vim.group.visual.vimMoveSelectionToCaret 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.VimLockLabel
import com.maddyhome.idea.vim.helper.StrictMode import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.register.Register import com.maddyhome.idea.vim.register.VimRegisterGroup
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inBlockSelection import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.inVisualMode 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. * Immutable interface of the caret. Immutable caret is an important concept of Fleet.
@@ -66,7 +63,7 @@ interface ImmutableVimCaret {
fun hasSelection(): Boolean fun hasSelection(): Boolean
var lastSelectionInfo: SelectionInfo var lastSelectionInfo: SelectionInfo
val registerStorage: CaretRegisterStorage val registerStorage: VimRegisterGroup
val markStorage: LocalMarkStorage val markStorage: LocalMarkStorage
} }
@@ -152,19 +149,3 @@ fun VimCaret.moveToMotion(motion: Motion): VimCaret {
this 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 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 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

@@ -231,7 +231,7 @@ interface VimChangeGroup {
operatorArguments: OperatorArguments, 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 fun insertText(editor: VimEditor, caret: VimCaret, str: String): VimCaret

View File

@@ -179,31 +179,41 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return false return false
} }
} }
val mode = editor.mode
val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE if (type == null ||
val shouldYank = type != null && !isInsertMode && saveToRegister (mode == Mode.INSERT || mode == Mode.REPLACE) ||
if (shouldYank && !caret.registerStorage.storeText(editor, context, updatedRange, type, isDelete = true)) { !saveToRegister ||
return false injector.registerGroup.storeText(
}
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, editor,
LineDeleteShift.NL_ON_END context,
) ?: continue caret,
injector.application.runWriteAction { 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)) 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) { return false
val start = updatedRange.startOffset
injector.markService.setMark(caret, MARK_CHANGE_POS, start)
injector.markService.setChangeMarks(caret, TextRange(start, start + 1))
}
return true
} }
/** /**
@@ -213,7 +223,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param caret The caret to start insertion in * @param caret The caret to start insertion in
* @param str The text to insert * @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 { injector.application.runWriteAction {
(editor as MutableVimEditor).insertText(caret, offset, str) (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. * - **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 { interface VimClipboardManager {
fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): VimCopiedText? fun getPrimaryContent(): VimCopiedText?
fun getClipboardContent(editor: VimEditor, context: ExecutionContext): 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 * This method should perform caret merging after the operations. This is similar to IJ runForEachCaret
* TODO review * TODO review
*/ */
val isFirstCaret: Boolean
val isReversingCarets: Boolean
fun forEachCaret(action: (VimCaret) -> Unit) fun forEachCaret(action: (VimCaret) -> Unit)
fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false) fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false)
fun isInForEachCaretScope(): Boolean fun isInForEachCaretScope(): Boolean
@@ -211,6 +212,7 @@ interface VimEditor {
fun createIndentBySize(size: Int): String fun createIndentBySize(size: Int): String
fun getFoldRegionAtOffset(offset: Int): VimFoldRegion? fun getFoldRegionAtOffset(offset: Int): VimFoldRegion?
fun getSoftWrapStartAtOffset(offset: Int): Int?
/** /**
* Mostly related to Fleet. After the editor is modified, the carets are modified. You can't use the old caret * Mostly related to Fleet. After the editor is modified, the carets are modified. You can't use the old caret

View File

@@ -25,7 +25,7 @@ interface VimMotionGroup {
allowWrap: Boolean = false, allowWrap: Boolean = false,
): Motion ): Motion
fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean = false): Motion
// TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion // TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion

View File

@@ -33,14 +33,18 @@ abstract class VimMotionGroupBase : VimMotionGroup {
override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T
override var lastFTChar: Char = ' ' override var lastFTChar: Char = ' '
override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion { override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean): Motion {
val pos = caret.getVisualPosition() val pos = caret.getVisualPosition()
if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) { if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) {
return Motion.Error return Motion.Error
} }
val intendedColumn = caret.vimLastColumn val intendedColumn = caret.vimLastColumn
val line = editor.normalizeVisualLine(pos.line + count) val line = if (bufferLines)
// TODO Does not work with folds, but I don't use those.
editor.normalizeVisualLine(editor.bufferLineToVisualLine(editor.visualLineToBufferLine(pos.line) + count))
else
editor.normalizeVisualLine(pos.line + count)
if (intendedColumn == LAST_COLUMN) { if (intendedColumn == LAST_COLUMN) {
val normalisedColumn = injector.engineEditorHelper.normalizeVisualColumn( val normalisedColumn = injector.engineEditorHelper.normalizeVisualColumn(

View File

@@ -208,4 +208,17 @@ interface VimSearchGroup {
fun isSomeTextHighlighted(): Boolean fun isSomeTextHighlighted(): Boolean
fun getCurrentIncsearchResultRange(editor: VimEditor): TextRange? 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 patternOffset The pattern offset, e.g. `/{pattern}/{offset}`
* @param direction The direction to search * @param direction The direction to search
*/ */
@TestOnly override fun setLastSearchState(
fun setLastSearchState(
pattern: String, pattern: String,
patternOffset: String, patternOffset: String,
direction: Direction, direction: Direction,

View File

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

View File

@@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.common 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.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
@@ -72,9 +71,9 @@ class VimListenersNotifier {
isReplaceCharListeners.forEach { it.isReplaceCharChanged(editor) } 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 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 package com.maddyhome.idea.vim.common
import com.maddyhome.idea.vim.api.ImmutableVimCaret import com.maddyhome.idea.vim.api.VimEditor
interface VimYankListener: Listener { 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.VimCaret
import com.maddyhome.idea.vim.api.VimCaretListener import com.maddyhome.idea.vim.api.VimCaretListener
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.normalizeOffset import com.maddyhome.idea.vim.api.normalizeOffset
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
@@ -226,7 +227,15 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret") StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret")
} }
val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset) val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset).let {
if (offset.intendedColumn == VimMotionGroupBase.LAST_COLUMN) {
val softWrapStart = editor.getSoftWrapStartAtOffset(it)
if (softWrapStart != null) softWrapStart - 1 else it
}
else {
it
}
}
StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action") StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action")
// Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode // Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode
@@ -274,7 +283,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) {
foldRegion.isExpanded = true foldRegion.isExpanded = true
} }
} }
return resultOffset return resultOffset
} }

View File

@@ -271,7 +271,11 @@ class ToActionMappingInfo(
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToAction' mapping...") 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 { companion object {

View File

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

View File

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

View File

@@ -141,7 +141,6 @@ abstract class VimPutBase : VimPut {
if (data.visualSelection?.typeInEditor?.isLine == true && data.textData.typeInRegister.isChar) text += "\n" 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.isLine && text.isNotEmpty() && text.last() != '\n') text += '\n'
if (data.textData.typeInRegister.isChar && text.lastOrNull() == '\n' && data.visualSelection?.typeInEditor?.isLine == false) { if (data.textData.typeInRegister.isChar && text.lastOrNull() == '\n' && data.visualSelection?.typeInEditor?.isLine == false) {
@@ -150,9 +149,10 @@ abstract class VimPutBase : VimPut {
} }
return ProcessedTextData( return ProcessedTextData(
data.textData.registerChar, text,
data.textData.copiedText.updateText(text),
data.textData.typeInRegister, data.textData.typeInRegister,
data.textData.transferableData,
data.textData.registerChar,
) )
} }
@@ -512,7 +512,7 @@ abstract class VimPutBase : VimPut {
startOffsets.forEach { startOffset -> startOffsets.forEach { startOffset ->
val selectionType = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE val selectionType = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE
val (endOffset, updatedCaret) = putTextInternal( 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, startOffset, data.count, data.indent, data.caretAfterInsertedText,
) )
updated = updatedCaret updated = updatedCaret

View File

@@ -21,8 +21,8 @@ import com.maddyhome.idea.vim.api.VimFoldRegion
import com.maddyhome.idea.vim.api.VimIndentConfig import com.maddyhome.idea.vim.api.VimIndentConfig
import com.maddyhome.idea.vim.api.VimScrollingModel import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.VimVirtualFile import com.maddyhome.idea.vim.api.VimVirtualFile
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimEditorReplaceMask import com.maddyhome.idea.vim.common.VimEditorReplaceMask
@@ -620,7 +620,13 @@ class VimRegex(pattern: String) {
override fun carets(): List<VimCaret> = emptyList() override fun carets(): List<VimCaret> = emptyList()
override fun nativeCarets(): List<VimCaret> = emptyList() 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 forEachCaret(action: (VimCaret) -> Unit) {}
override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {} override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {}
@@ -784,6 +790,10 @@ class VimRegex(pattern: String) {
return null return null
} }
override fun getSoftWrapStartAtOffset(offset: Int): Int? {
return null
}
override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T? { override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T? {
return null return null
} }

View File

@@ -8,40 +8,86 @@
package com.maddyhome.idea.vim.register package com.maddyhome.idea.vim.register
import com.maddyhome.idea.vim.api.injector 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.helper.EngineStringHelper
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
// TODO should we prefer keys over text, as they are more informative? class Register {
// TODO e.g.  could be both <Esc> and <C-[> after trying to restore original keys var name: Char
data class Register( val type: SelectionType
val name: Char, val keys: MutableList<KeyStroke>
val keys: List<KeyStroke>, val transferableData: MutableList<out Any>
val type: SelectionType, val rawText: String?
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
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( constructor(
name, name: Char,
keys, type: SelectionType,
type, text: String,
injector.clipboardManager.dumbCopiedText(injector.parser.toPrintableString(keys)) 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( constructor(
name, name: Char,
injector.parser.stringToKeys(copiedText.text), type: SelectionType,
type, text: String,
copiedText 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> { object KeySorter : Comparator<Register> {
@NonNls @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 { 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 var lastRegisterChar: Char
val currentRegister: Char val currentRegister: Char
@@ -32,7 +39,6 @@ interface VimRegisterGroup {
val isRegisterSpecifiedExplicitly: Boolean val isRegisterSpecifiedExplicitly: Boolean
val defaultRegister: Char val defaultRegister: Char
fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register?
fun isValid(reg: Char): Boolean fun isValid(reg: Char): Boolean
fun selectRegister(reg: Char): Boolean fun selectRegister(reg: Char): Boolean
fun resetRegister() fun resetRegister()
@@ -41,6 +47,7 @@ interface VimRegisterGroup {
fun isRegisterWritable(): Boolean fun isRegisterWritable(): Boolean
fun isRegisterWritable(reg: Char): Boolean fun isRegisterWritable(reg: Char): Boolean
/** Store text into the last register. */
fun storeText( fun storeText(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
@@ -48,18 +55,20 @@ interface VimRegisterGroup {
range: TextRange, range: TextRange,
type: SelectionType, type: SelectionType,
isDelete: Boolean, isDelete: Boolean,
forceAppend: Boolean = false,
prependInsteadOfAppend: Boolean = false
): Boolean ): Boolean
/**
* Stores text to any writable register (used for the let command)
*/
fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean
/**
* Stores text to any writable register (used for multicaret tests)
*/
@TestOnly @TestOnly
fun storeText( fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType): Boolean
editor: VimEditor,
context: ExecutionContext,
register: Char,
text: String,
selectionType: SelectionType,
): Boolean
/** /**
* Stores text, character wise, in the given special register * Stores text, character wise, in the given special register
@@ -75,7 +84,6 @@ interface VimRegisterGroup {
* preferable to yank from the fixture editor. * preferable to yank from the fixture editor.
*/ */
fun storeTextSpecial(register: Char, text: String): Boolean 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)") @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(r: Char): Register?
fun getRegister(editor: VimEditor, context: ExecutionContext, 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.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.common.TextRange 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.VimLogger
import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger
@@ -75,9 +74,13 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
override val defaultRegister: Char override val defaultRegister: Char
get() = defaultRegisterChar 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 = { private val onClipboardChanged: () -> Unit = {
val clipboardOptionValue = injector.globalOptions().clipboard val clipboardOptionValue = injector.globalOptions().clipboard
@@ -113,13 +116,20 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return if (isValid(reg)) { return if (isValid(reg)) {
isRegisterSpecifiedExplicitly = true isRegisterSpecifiedExplicitly = true
lastRegisterChar = reg lastRegisterChar = reg
logger.debug { "register selected: $lastRegisterChar" } logger.debug { "register selected: $lastRegister" }
true true
} else { } else {
false 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. * Reset the selected register back to the default register.
*/ */
@@ -175,6 +185,8 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
type: SelectionType, type: SelectionType,
register: Char, register: Char,
isDelete: Boolean, isDelete: Boolean,
forceAppend: Boolean,
prependInsteadOfAppend: Boolean,
): Boolean { ): Boolean {
// Null register doesn't get saved, but acts like it was // Null register doesn't get saved, but acts like it was
if (lastRegisterChar == BLACK_HOLE_REGISTER) return true if (lastRegisterChar == BLACK_HOLE_REGISTER) return true
@@ -193,50 +205,62 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
end = t 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 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 lreg = Character.toLowerCase(register)
val r = myRegisters[lreg] val r = myRegisters[lreg]
// Append the text if the lowercase register existed // Append the text if the lowercase register existed
if (r != null) { if (r != null) {
myRegisters[lreg] = r.addText(copiedText.text) if (prependInsteadOfAppend) {
r.prependTextAndResetTransferableData(processedText)
}
else {
r.addTextAndResetTransferableData(processedText)
}
} else { } else {
myRegisters[lreg] = Register(lreg, copiedText, type) myRegisters[lreg] = Register(lreg, type, processedText, ArrayList(transferableData))
logger.debug { "register '$register' contains: \"$copiedText\"" } logger.debug { "register '$register' contains: \"$processedText\"" }
} // Set the text if the lowercase register didn't exist yet } // Set the text if the lowercase register didn't exist yet
} else { } else {
myRegisters[register] = Register(register, copiedText, type) myRegisters[register] = Register(register, type, processedText, ArrayList(transferableData))
logger.debug { "register '$register' contains: \"$copiedText\"" } logger.debug { "register '$register' contains: \"$processedText\"" }
} // Put the text in the specified register } // Put the text in the specified register
if (register == CLIPBOARD_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) { 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 (register == PRIMARY_REGISTER) {
if (isPrimaryRegisterSupported()) { 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) { if (!isRegisterSpecifiedExplicitly && !isDelete && OptionConstants.clipboard_unnamed in injector.globalOptions().clipboard) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText) injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData))
} }
} else { } 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 // Also add it to the unnamed register if the default wasn't specified
if (register != UNNAMED_REGISTER && ".:/".indexOf(register) == -1) { if (register != UNNAMED_REGISTER && ".:/".indexOf(register) == -1) {
myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, copiedText, type) myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, type, processedText, ArrayList(transferableData))
logger.debug { "register '$UNNAMED_REGISTER' contains: \"$copiedText\"" } logger.debug { "register '$UNNAMED_REGISTER' contains: \"$processedText\"" }
} }
if (isDelete) { if (isDelete) {
@@ -256,26 +280,26 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
while (d >= '1') { while (d >= '1') {
val t = myRegisters[d] val t = myRegisters[d]
if (t != null) { if (t != null) {
val incName = (d.code + 1).toChar() t.name = (d.code + 1).toChar()
myRegisters[incName] = Register(incName, t.copiedText, t.type) myRegisters[(d.code + 1).toChar()] = t
} }
d-- 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 // Deletes smaller than one line and without specified register go the the "-" register
if (smallInlineDeletion && register == defaultRegister) { if (smallInlineDeletion && register == defaultRegister) {
myRegisters[SMALL_DELETION_REGISTER] = myRegisters[SMALL_DELETION_REGISTER] =
Register(SMALL_DELETION_REGISTER, copiedText, type) Register(SMALL_DELETION_REGISTER, type, processedText, ArrayList(transferableData))
} }
} else if (register == defaultRegister) { } else if (register == defaultRegister) {
myRegisters['0'] = Register('0', copiedText, type) myRegisters['0'] = Register('0', type, processedText, ArrayList(transferableData))
logger.debug { "register '0' contains: \"$copiedText\"" } logger.debug { "register '0' contains: \"$processedText\"" }
} // Yanks also go to register 0 if the default register was used } // Yanks also go to register 0 if the default register was used
return true return true
} }
/** /**
* Store text into the last register. * Store text into the last register.
* *
@@ -292,10 +316,12 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
range: TextRange, range: TextRange,
type: SelectionType, type: SelectionType,
isDelete: Boolean, isDelete: Boolean,
forceAppend: Boolean,
prependInsteadOfAppend: Boolean
): Boolean { ): Boolean {
if (isRegisterWritable()) { if (isRegisterWritable()) {
val text = preprocessTextBeforeStoring(editor.getText(range), type) 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 return false
@@ -330,53 +356,35 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
if (READONLY_REGISTERS.indexOf(register) == -1 && register != LAST_SEARCH_REGISTER && register != UNNAMED_REGISTER) { if (READONLY_REGISTERS.indexOf(register) == -1 && register != LAST_SEARCH_REGISTER && register != UNNAMED_REGISTER) {
return false return false
} }
myRegisters[register] = Register( myRegisters[register] = Register(register,
register,
injector.clipboardManager.dumbCopiedText(text),
SelectionType.CHARACTER_WISE SelectionType.CHARACTER_WISE
) // TODO why transferable data is not collected? , text, ArrayList())
logger.debug { "register '$register' contains: \"$text\"" } logger.debug { "register '$register' contains: \"$text\"" }
return true 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 { override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean {
return storeText(editor, context, register, text, SelectionType.CHARACTER_WISE) 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 { private fun guessSelectionType(text: String): SelectionType {
return if (text.endsWith("\n")) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE 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 (+) * @param r - the register character corresponding to either the primary selection (*) or clipboard selection (+)
* @return the content of the selection, if available, otherwise null * @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) { return when (r) {
PRIMARY_REGISTER -> refreshPrimaryRegister(editor, context) PRIMARY_REGISTER -> refreshPrimaryRegister()
CLIPBOARD_REGISTER -> refreshClipboardRegister(editor, context) CLIPBOARD_REGISTER -> refreshClipboardRegister()
else -> throw RuntimeException("Clipboard register expected, got $r") else -> throw RuntimeException("Clipboard register expected, got $r")
} }
} }
@@ -401,56 +409,60 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return System.getenv("DISPLAY") != null && injector.systemInfoService.isXWindow return System.getenv("DISPLAY") != null && injector.systemInfoService.isXWindow
} }
private fun setSystemPrimaryRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) { private fun setSystemPrimaryRegisterText(text: String, rawText: String, transferableData: List<Any>) {
logger.trace("Setting text: $copiedText to primary selection...") logger.trace("Setting text: $text to primary selection...")
if (isPrimaryRegisterSupported()) { if (isPrimaryRegisterSupported()) {
try { try {
injector.clipboardManager.setPrimaryContent(editor, context, copiedText) injector.clipboardManager.setClipboardText(text, rawText, transferableData)
} catch (e: Exception) { } catch (e: Exception) {
logger.warn("False positive X11 primary selection support") logger.warn("False positive X11 primary selection support")
logger.trace("Setting text to primary selection failed. Setting it to clipboard selection instead") logger.trace("Setting text to primary selection failed. Setting it to clipboard selection instead")
setSystemClipboardRegisterText(editor, context, copiedText) setSystemClipboardRegisterText(text, rawText, transferableData)
} }
} else { } else {
logger.trace("X11 primary selection is not supporting. Setting clipboard selection instead") 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) { private fun setSystemClipboardRegisterText(text: String, rawText: String, transferableData: List<Any>) {
injector.clipboardManager.setClipboardContent(editor, context, copiedText) 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..") logger.trace("Syncing cached primary selection value..")
if (!isPrimaryRegisterSupported()) { if (!isPrimaryRegisterSupported()) {
logger.trace("X11 primary selection is not supported. Syncing clipboard selection..") logger.trace("X11 primary selection is not supported. Syncing clipboard selection..")
return refreshClipboardRegister(editor, context) return refreshClipboardRegister()
} }
try { try {
val clipboardData = injector.clipboardManager.getPrimaryContent(editor, context) ?: return null val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null
val currentRegister = myRegisters[PRIMARY_REGISTER] 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 currentRegister
} }
return Register(PRIMARY_REGISTER, clipboardData, guessSelectionType(clipboardData.text)) return transferableData?.let { Register(PRIMARY_REGISTER, guessSelectionType(text), text, it) }
} catch (e: Exception) { } catch (e: Exception) {
logger.warn("False positive X11 primary selection support") logger.warn("False positive X11 primary selection support")
logger.trace("Syncing primary selection failed. Syncing clipboard selection instead") 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 // for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage
val systemAwareClipboardRegister = if (isPrimaryRegisterSupported()) CLIPBOARD_REGISTER else PRIMARY_REGISTER 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] 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 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? { override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? {
@@ -460,36 +472,34 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
myR = Character.toLowerCase(myR) myR = Character.toLowerCase(myR)
} }
return if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) refreshClipboardRegister( return if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) refreshClipboardRegister(
editor, myR) else myRegisters[myR]
context,
myR
) else myRegisters[myR]
} }
override fun getRegisters(editor: VimEditor, context: ExecutionContext): List<Register> { override fun getRegisters(editor: VimEditor, context: ExecutionContext): List<Register> {
val filteredRegisters = myRegisters.values.filterNot { CLIPBOARD_REGISTERS.contains(it.name) }.toMutableList() val filteredRegisters = myRegisters.values.filterNot { CLIPBOARD_REGISTERS.contains(it.name) }.toMutableList()
val clipboardRegisters = CLIPBOARD_REGISTERS val clipboardRegisters = CLIPBOARD_REGISTERS
.filterNot { it == CLIPBOARD_REGISTER && !isPrimaryRegisterSupported() } // for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage .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) return (filteredRegisters + clipboardRegisters).sortedWith(Register.KeySorter)
} }
override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) { override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) {
var myR = if (Character.isUpperCase(r)) Character.toLowerCase(r) else r 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) { when (myR) {
CLIPBOARD_REGISTER -> { CLIPBOARD_REGISTER -> {
if (!isPrimaryRegisterSupported()) { if (!isPrimaryRegisterSupported()) {
// it looks wrong, but for some reason non-X systems use the * register to store the clipboard content // it looks wrong, but for some reason non-X systems use the * register to store the clipboard content
myR = PRIMARY_REGISTER myR = PRIMARY_REGISTER
} }
setSystemClipboardRegisterText(editor, context, register.copiedText) setSystemClipboardRegisterText(text, rawText, ArrayList(register.transferableData))
} }
PRIMARY_REGISTER -> { 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? { 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) { override fun recordText(text: String) {
@@ -530,7 +540,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
if (register != null) { if (register != null) {
var reg: Register? = null var reg: Register? = null
if (Character.isUpperCase(register)) { if (Character.isUpperCase(register)) {
reg = getRegister(editor, context, register) reg = getRegister(register)
} }
val myRecordList = recordList val myRecordList = recordList
@@ -539,7 +549,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
reg = Register(Character.toLowerCase(register), SelectionType.CHARACTER_WISE, myRecordList) reg = Register(Character.toLowerCase(register), SelectionType.CHARACTER_WISE, myRecordList)
myRegisters[Character.toLowerCase(register)] = reg myRegisters[Character.toLowerCase(register)] = reg
} else { } else {
myRegisters[reg.name.lowercaseChar()] = reg.addText(injector.parser.toPrintableString(myRecordList)) reg.addKeys(myRecordList)
} }
} }
} }

View File

@@ -1,186 +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,
) : 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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
launch {
vimApi.callback()
}
}
}
injector.listenersNotifier.vimPluginListeners.add(listener)
}
}

View File

@@ -107,7 +107,7 @@ class CaretReadImpl(
val isVisualBlockMode = mode is Mode.VISUAL && mode.selectionType == SelectionType.BLOCK_WISE val isVisualBlockMode = mode is Mode.VISUAL && mode.selectionType == SelectionType.BLOCK_WISE
return if (isVisualBlockMode) { return if (isVisualBlockMode) {
val ranges = vimEditor.nativeCarets().mapNotNull { val ranges = vimEditor.nativeCarets().mapNotNull {
val marks = injector.markService.getVisualSelectionMarks(it) ?: return@mapNotNull null val marks = injector.markService.getVisualSelectionMarks(it) ?: return@mapNotNull null
Range.Simple(marks.startOffset, marks.endOffset) Range.Simple(marks.startOffset, marks.endOffset)
}.toTypedArray() }.toTypedArray()
@@ -132,7 +132,7 @@ class CaretReadImpl(
val context = injector.executionContextManager.getEditorExecutionContext(vimEditor) val context = injector.executionContextManager.getEditorExecutionContext(vimEditor)
val caret: VimCaret = vimEditor.carets().find { it.id == caretId.id } ?: return null val caret: VimCaret = vimEditor.carets().find { it.id == caretId.id } ?: return null
val register: Register = caret.registerStorage.getRegister(vimEditor, context, register) ?: 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? { override fun getReg(register: Char): String? {

View File

@@ -76,7 +76,7 @@ class CaretTransactionImpl(
beforeCaret: Boolean, beforeCaret: Boolean,
): Boolean { ): Boolean {
val copiedText = injector.clipboardManager.dumbCopiedText(text) 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( val putData = PutData(
textData = textData, textData = textData,

View File

@@ -11,6 +11,7 @@ package com.maddyhome.idea.vim.vimscript.model.commands
import com.intellij.vim.annotations.ExCommand import com.intellij.vim.annotations.ExCommand
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor 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.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range 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() val carets = editor.sortedCarets()
for (caret in carets) { for (caret in carets) {
val range = getLineRange(editor, caret).toTextRange(editor) val range = getLineRange(editor, caret).toTextRange(editor)
val copiedText = injector.clipboardManager.collectCopiedText(editor, context, range) val text = editor.getText(range)
// Copy is defined as: // Copy is defined as:
// :[range]co[py] {address} // :[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). // the line _before_ the first line (i.e., copy to above the first line).
val address1 = getAddressFromArgument(editor) 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 var mutableCaret = caret
val putData = if (address1 == 0) { val putData = if (address1 == 0) {
// TODO: This should maintain current column location // 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 selectionEndOffset = lastSelectionInfo.end?.let { editor.bufferPositionToOffset(it) }
val text = editor.getText(sourceRange) 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') || val dropNewLineInEnd = (targetLineAfterDeletion + linesMoved == editor.lineCount() - 1 && text.last() == '\n') ||
(sourceLineRange.endLine == editor.lineCount() - 1) (sourceLineRange.endLine == editor.lineCount() - 1)

View File

@@ -47,7 +47,12 @@ data class PutLinesCommand(val range: Range, val modifier: CommandModifier, val
val line = if (range.size() == 0) -1 else getLine(editor) val line = if (range.size() == 0) -1 else getLine(editor)
val textData = registerGroup.getRegister(editor, context, registerGroup.lastRegisterChar)?.let { 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( val putData = PutData(
textData, 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.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range 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.state.mode.SelectionType
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult 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.CHARACTER_WISE -> "c"
SelectionType.BLOCK_WISE -> "b" 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) injector.outputPanel.output(editor, context, regs)

View File

@@ -31,8 +31,23 @@ interface VimYankGroup {
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean ): 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 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( fun yankRange(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, 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.action.motion.updown.MotionDownLess1FirstNonSpaceAction
import com.maddyhome.idea.vim.api.ExecutionContext 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.VimCaret
import com.maddyhome.idea.vim.api.VimEditor 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.getLineEndForOffset
import com.maddyhome.idea.vim.api.getLineStartForOffset import com.maddyhome.idea.vim.api.getLineStartForOffset
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import org.jetbrains.annotations.Contract
import kotlin.math.min import kotlin.math.min
open class YankGroupBase : VimYankGroup { open class YankGroupBase : VimYankGroup {
private fun yankRange( private fun yankRange(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
caretToRange: Map<ImmutableVimCaret, Pair<TextRange, SelectionType>>, range: TextRange,
type: SelectionType,
startOffsets: Map<VimCaret, Int>?, startOffsets: Map<VimCaret, Int>?,
): Boolean { ): Boolean {
startOffsets?.forEach { (caret, offset) -> startOffsets?.forEach { (caret, offset) ->
caret.moveToOffset(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 @Contract("_, _ -> new")
for ((caret, myRange) in caretToRange) { protected fun getTextRange(ranges: List<Pair<Int, Int>>, type: SelectionType): TextRange? {
result = caret.registerStorage.storeText(editor, context, myRange.first, myRange.second, false) && result 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, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val motion = argument as? Argument.Motion ?: return false val motion = argument as? Argument.Motion ?: return false
val motionType = motion.getMotionType()
val nativeCaretCount = editor.nativeCarets().size val nativeCaretCount = editor.nativeCarets().size
if (nativeCaretCount <= 0) return false 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 // This logic is from original vim
val startOffsets = val startOffsets =
@@ -69,39 +90,24 @@ open class YankGroupBase : VimYankGroup {
HashMap<VimCaret, Int>(nativeCaretCount) HashMap<VimCaret, Int>(nativeCaretCount)
} }
for (caret in editor.nativeCarets()) { for (caret in editor.nativeCarets()) {
var motionType = motion.getMotionType()
val motionRange = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) val motionRange = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments)
?: continue ?: continue
assert(motionRange.size() == 1) assert(motionRange.size() == 1)
ranges.add(motionRange.startOffset to motionRange.endOffset)
startOffsets?.put(caret, motionRange.normalize().startOffset) 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( return yankRange(
editor, editor,
context, context,
caretToRange, range,
motionType,
startOffsets, startOffsets,
) )
} }
@@ -115,18 +121,18 @@ open class YankGroupBase : VimYankGroup {
*/ */
override fun yankLine(editor: VimEditor, context: ExecutionContext, count: Int): Boolean { override fun yankLine(editor: VimEditor, context: ExecutionContext, count: Int): Boolean {
val caretCount = editor.nativeCarets().size 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()) { for (caret in editor.nativeCarets()) {
val start = injector.motion.moveCaretToCurrentLineStart(editor, caret) val start = injector.motion.moveCaretToCurrentLineStart(editor, caret)
val end = val end = min(injector.motion.moveCaretToRelativeLineEnd(editor, caret, count - 1, true) + 1, editor.fileSize().toInt())
min(injector.motion.moveCaretToRelativeLineEnd(editor, caret, count - 1, true) + 1, editor.fileSize().toInt())
if (end == -1) continue 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 * @param type The type of yank
* @return true if able to yank the range, false if not * @return true if able to yank the range, false if not
*/ */
override fun yankRange( override fun yankRange(editor: VimEditor, context: ExecutionContext, range: TextRange?, type: SelectionType, moveCursor: Boolean): Boolean {
editor: VimEditor,
context: ExecutionContext,
range: TextRange?,
type: SelectionType,
moveCursor: Boolean,
): Boolean {
range ?: return false range ?: return false
val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>()
if (type == SelectionType.LINE_WISE) { if (type == SelectionType.LINE_WISE) {
for (i in 0 until range.size()) { for (i in 0 until range.size()) {
@@ -165,19 +164,17 @@ open class YankGroupBase : VimYankGroup {
val startOffsets = HashMap<VimCaret, Int>(editor.nativeCarets().size) val startOffsets = HashMap<VimCaret, Int>(editor.nativeCarets().size)
if (type == SelectionType.BLOCK_WISE) { if (type == SelectionType.BLOCK_WISE) {
startOffsets[editor.primaryCaret()] = range.normalize().startOffset startOffsets[editor.primaryCaret()] = range.normalize().startOffset
caretToRange[editor.primaryCaret()] = range to type
} else { } else {
for ((i, caret) in editor.nativeCarets().withIndex()) { for ((i, caret) in editor.nativeCarets().withIndex()) {
val textRange = TextRange(rangeStartOffsets[i], rangeEndOffsets[i]) val textRange = TextRange(rangeStartOffsets[i], rangeEndOffsets[i])
startOffsets[caret] = textRange.normalize().startOffset startOffsets[caret] = textRange.normalize().startOffset
caretToRange[caret] = textRange to type
} }
} }
return if (moveCursor) { return if (moveCursor) {
yankRange(editor, context, caretToRange, startOffsets) yankRange(editor, context, range, type, startOffsets)
} else { } else {
yankRange(editor, context, caretToRange, null) yankRange(editor, context, range, type, null)
} }
} }
} }

View File

@@ -417,7 +417,7 @@
{ {
"keys": "<C-R>", "keys": "<C-R>",
"class": "com.maddyhome.idea.vim.action.change.RedoAction", "class": "com.maddyhome.idea.vim.action.change.RedoAction",
"modes": "N" "modes": "NX"
}, },
{ {
"keys": "<C-R>", "keys": "<C-R>",
@@ -1027,7 +1027,7 @@
{ {
"keys": "<Undo>", "keys": "<Undo>",
"class": "com.maddyhome.idea.vim.action.change.UndoAction", "class": "com.maddyhome.idea.vim.action.change.UndoAction",
"modes": "N" "modes": "NX"
}, },
{ {
"keys": "<Up>", "keys": "<Up>",
@@ -1246,8 +1246,8 @@
}, },
{ {
"keys": "U", "keys": "U",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction", "class": "com.maddyhome.idea.vim.action.change.RedoAction",
"modes": "X" "modes": "NX"
}, },
{ {
"keys": "V", "keys": "V",
@@ -1729,11 +1729,6 @@
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperMotionAction", "class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperMotionAction",
"modes": "N" "modes": "N"
}, },
{
"keys": "gU",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction",
"modes": "X"
},
{ {
"keys": "g^", "keys": "g^",
"class": "com.maddyhome.idea.vim.action.motion.leftright.MotionFirstScreenNonSpaceAction", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionFirstScreenNonSpaceAction",
@@ -1844,11 +1839,6 @@
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerMotionAction", "class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerMotionAction",
"modes": "N" "modes": "N"
}, },
{
"keys": "gu",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction",
"modes": "X"
},
{ {
"keys": "gv", "keys": "gv",
"class": "com.maddyhome.idea.vim.action.motion.visual.VisualSelectPreviousAction", "class": "com.maddyhome.idea.vim.action.motion.visual.VisualSelectPreviousAction",
@@ -2052,12 +2042,7 @@
{ {
"keys": "u", "keys": "u",
"class": "com.maddyhome.idea.vim.action.change.UndoAction", "class": "com.maddyhome.idea.vim.action.change.UndoAction",
"modes": "N" "modes": "NX"
},
{
"keys": "u",
"class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction",
"modes": "X"
}, },
{ {
"keys": "v", "keys": "v",