mirror of
				https://github.com/chylex/IntelliJ-IdeaVim.git
				synced 2025-10-31 02:17:13 +01:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			customized
			...
			0f0a73c139
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0f0a73c139 | |||
| 9eccf626db | |||
| f53f980a01 | |||
| 54e3f7053c | |||
| c7a8f6b4aa | |||
| f680f0b2cd | |||
| 10131a8d57 | |||
| f5b194002c | |||
| 41e4adbd6a | |||
| 191bf9967e | |||
| ed84d20d4a | |||
| 3e829b8949 | |||
| 28b511131b | |||
| cc2a8d25e2 | |||
| fb9f83821b | |||
| fcb9fd8169 | |||
| 8793889f05 | |||
| f294059e81 | |||
| c7a9fbf374 | |||
| 9b04739f15 | |||
| f68c56b968 | |||
| 0282d99692 | |||
| 03be3e1be2 | |||
| 7c233f5e1a | 
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| * text=auto eol=lf | ||||
| @@ -137,6 +137,7 @@ dependencies { | ||||
|  | ||||
|     // AceJump is an optional dependency. We use their SessionManager class to check if it's active | ||||
|     plugin("AceJump", "3.8.19") | ||||
|     plugin("com.intellij.classic.ui", "251.23774.318") | ||||
|  | ||||
|     bundledPlugins("org.jetbrains.plugins.terminal") | ||||
|   } | ||||
| @@ -239,6 +240,8 @@ tasks { | ||||
|   } | ||||
|  | ||||
|   compileTestKotlin { | ||||
|     enabled = false | ||||
|      | ||||
|     kotlinOptions { | ||||
|       jvmTarget = javaVersion | ||||
|       apiVersion = "2.0" | ||||
| @@ -254,6 +257,7 @@ tasks { | ||||
|   // a custom task (see below) | ||||
|   runIde { | ||||
|     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 | ||||
|   | ||||
| @@ -20,7 +20,7 @@ ideaVersion=2025.1 | ||||
| # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type | ||||
| ideaType=IC | ||||
| instrumentPluginCode=true | ||||
| version=SNAPSHOT | ||||
| version=chylex-49 | ||||
| javaVersion=21 | ||||
| remoteRobotVersion=0.11.23 | ||||
| antlrVersion=4.10.1 | ||||
| @@ -41,7 +41,6 @@ youtrackToken= | ||||
|  | ||||
| # Gradle settings | ||||
| org.gradle.jvmargs='-Dfile.encoding=UTF-8' | ||||
| org.gradle.configuration-cache=true | ||||
| org.gradle.caching=true | ||||
|  | ||||
| # Disable warning from gradle-intellij-plugin. Kotlin stdlib is included as compileOnly, so the warning is unnecessary | ||||
|   | ||||
| @@ -0,0 +1,52 @@ | ||||
| package com.maddyhome.idea.vim.action | ||||
|  | ||||
| import com.intellij.openapi.actionSystem.ActionUpdateThread | ||||
| import com.intellij.openapi.actionSystem.AnActionEvent | ||||
| import com.intellij.openapi.command.UndoConfirmationPolicy | ||||
| import com.intellij.openapi.command.WriteCommandAction | ||||
| import com.intellij.openapi.fileEditor.TextEditor | ||||
| import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx | ||||
| import com.intellij.openapi.project.DumbAwareAction | ||||
| import com.maddyhome.idea.vim.KeyHandler | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext | ||||
| import com.maddyhome.idea.vim.newapi.vim | ||||
| import com.maddyhome.idea.vim.state.mode.Mode | ||||
|  | ||||
| class VimRunLastMacroInOpenFiles : DumbAwareAction() { | ||||
|   override fun update(e: AnActionEvent) { | ||||
|     val lastRegister = injector.macro.lastRegister | ||||
|     val isEnabled = lastRegister != 0.toChar() | ||||
|  | ||||
|     e.presentation.isEnabled = isEnabled | ||||
|     e.presentation.text = if (isEnabled) "Run Macro '${lastRegister}' in Open Files" else "Run Last Macro in Open Files" | ||||
|   } | ||||
|  | ||||
|   override fun getActionUpdateThread(): ActionUpdateThread { | ||||
|     return ActionUpdateThread.EDT | ||||
|   } | ||||
|  | ||||
|   override fun actionPerformed(e: AnActionEvent) { | ||||
|     val project = e.project ?: return | ||||
|     val fileEditorManager = FileEditorManagerEx.getInstanceExIfCreated(project) ?: return | ||||
|     val editors = fileEditorManager.allEditors.filterIsInstance<TextEditor>() | ||||
|      | ||||
|     WriteCommandAction.writeCommandAction(project) | ||||
|       .withName(e.presentation.text) | ||||
|       .withGlobalUndo() | ||||
|       .withUndoConfirmationPolicy(UndoConfirmationPolicy.REQUEST_CONFIRMATION) | ||||
|       .run<RuntimeException> { | ||||
|         val reg = injector.macro.lastRegister | ||||
|          | ||||
|         for (editor in editors) { | ||||
|           fileEditorManager.openFile(editor.file, true) | ||||
|            | ||||
|           val vimEditor = editor.editor.vim | ||||
|           vimEditor.mode = Mode.NORMAL() | ||||
|           KeyHandler.getInstance().reset(vimEditor) | ||||
|            | ||||
|           injector.macro.playbackRegister(vimEditor, IjEditorExecutionContext(e.dataContext), reg, 1) | ||||
|         } | ||||
|       } | ||||
|   } | ||||
| } | ||||
| @@ -221,7 +221,7 @@ object VimExtensionFacade { | ||||
|     caret: ImmutableVimCaret, | ||||
|     keys: List<KeyStroke?>?, | ||||
|   ) { | ||||
|     caret.registerStorage.setKeys(editor, context, register, keys?.filterNotNull() ?: emptyList()) | ||||
|     caret.registerStorage.setKeys(register, keys?.filterNotNull() ?: emptyList()) | ||||
|   } | ||||
|  | ||||
|   /** Set the current contents of the given register */ | ||||
|   | ||||
| @@ -21,9 +21,7 @@ import com.intellij.openapi.editor.markup.TextAttributes | ||||
| import com.intellij.openapi.util.Disposer | ||||
| import com.intellij.util.Alarm | ||||
| import com.intellij.util.Alarm.ThreadToUse | ||||
| import com.jetbrains.rd.util.first | ||||
| import com.maddyhome.idea.vim.VimPlugin | ||||
| import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.common.ModeChangeListener | ||||
| @@ -123,9 +121,9 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis | ||||
|     initialised = false | ||||
|   } | ||||
|  | ||||
|   override fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) { | ||||
|   override fun yankPerformed(editor: VimEditor, range: TextRange) { | ||||
|     ensureInitialised() | ||||
|     highlightHandler.highlightYankRange(caretToRange) | ||||
|     highlightHandler.highlightYankRange(editor.ij, range) | ||||
|   } | ||||
|  | ||||
|   override fun modeChanged(editor: VimEditor, oldMode: Mode) { | ||||
| @@ -146,15 +144,13 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis | ||||
|     private var lastEditor: Editor? = null | ||||
|     private val highlighters = mutableSetOf<RangeHighlighter>() | ||||
|  | ||||
|     fun highlightYankRange(caretToRange: Map<ImmutableVimCaret, TextRange>) { | ||||
|     fun highlightYankRange(editor: Editor, range: TextRange) { | ||||
|       // from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted | ||||
|       clearYankHighlighters() | ||||
|  | ||||
|       val editor = caretToRange.first().key.editor.ij | ||||
|       lastEditor = editor | ||||
|  | ||||
|       val attributes = getHighlightTextAttributes(editor) | ||||
|       for (range in caretToRange.values) { | ||||
|       for (i in 0 until range.size()) { | ||||
|         val highlighter = editor.markupModel.addRangeHighlighter( | ||||
|           range.startOffsets[i], | ||||
| @@ -165,7 +161,6 @@ internal class VimHighlightedYank : VimExtension, VimYankListener, ModeChangeLis | ||||
|         ) | ||||
|         highlighters.add(highlighter) | ||||
|       } | ||||
|       } | ||||
|  | ||||
|       // from vim-highlightedyank docs: A negative number makes the highlight persistent. | ||||
|       val timeout = extractUsersHighlightDuration() | ||||
|   | ||||
| @@ -230,7 +230,7 @@ private object FileTypePatterns { | ||||
|     } else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") { | ||||
|       this.cMakePatterns | ||||
|     } else { | ||||
|       return null | ||||
|       this.htmlPatterns | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,6 @@ import com.maddyhome.idea.vim.extension.exportOperatorFunction | ||||
| import com.maddyhome.idea.vim.group.visual.VimSelection | ||||
| import com.maddyhome.idea.vim.helper.exitVisualMode | ||||
| import com.maddyhome.idea.vim.key.OperatorFunction | ||||
| import com.maddyhome.idea.vim.newapi.IjVimCopiedText | ||||
| import com.maddyhome.idea.vim.newapi.IjVimEditor | ||||
| import com.maddyhome.idea.vim.newapi.ij | ||||
| import com.maddyhome.idea.vim.newapi.vim | ||||
| @@ -154,8 +153,7 @@ private fun doReplace(editor: Editor, context: DataContext, caret: ImmutableVimC | ||||
|     usedType = SelectionType.CHARACTER_WISE | ||||
|   } | ||||
|  | ||||
|   val copiedText = IjVimCopiedText(usedText, (savedRegister.copiedText as IjVimCopiedText).transferableData) | ||||
|   val textData = PutData.TextData(savedRegister.name, copiedText, usedType) | ||||
|   val textData = PutData.TextData(usedText, usedType, savedRegister.transferableData, savedRegister.name) | ||||
|  | ||||
|   val putData = PutData( | ||||
|     textData, | ||||
|   | ||||
| @@ -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) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.KeyHandler | ||||
| import com.maddyhome.idea.vim.VimPlugin | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimChangeGroup | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.endsWithNewLine | ||||
| import com.maddyhome.idea.vim.api.getLeadingCharacterOffset | ||||
| @@ -37,7 +38,10 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret | ||||
| import com.maddyhome.idea.vim.extension.exportOperatorFunction | ||||
| import com.maddyhome.idea.vim.group.findBlockRange | ||||
| import com.maddyhome.idea.vim.helper.exitVisualMode | ||||
| import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore | ||||
| import com.maddyhome.idea.vim.key.OperatorFunction | ||||
| import com.maddyhome.idea.vim.newapi.IjVimCaret | ||||
| import com.maddyhome.idea.vim.newapi.IjVimEditor | ||||
| import com.maddyhome.idea.vim.newapi.ij | ||||
| import com.maddyhome.idea.vim.newapi.vim | ||||
| import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper | ||||
| @@ -80,7 +84,7 @@ internal class VimSurroundExtension : VimExtension { | ||||
|       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 { | ||||
| @@ -108,7 +112,7 @@ internal class VimSurroundExtension : VimExtension { | ||||
|         val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset) | ||||
|         if (lastNonWhiteSpaceOffset != null) { | ||||
|           val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1) | ||||
|           performSurround(pair, range, it) | ||||
|           performSurround(pair, range, it, count = operatorArguments.count1) | ||||
|         } | ||||
| //        it.moveToOffset(lineStartOffset) | ||||
|       } | ||||
| @@ -131,15 +135,13 @@ internal class VimSurroundExtension : VimExtension { | ||||
|  | ||||
|   private class VSurroundHandler : ExtensionHandler { | ||||
|     override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { | ||||
|       val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart | ||||
|       // NB: Operator ignores SelectionType anyway | ||||
|       if (!Operator().apply(editor, context, editor.mode.selectionType)) { | ||||
|       if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) { | ||||
|         return | ||||
|       } | ||||
|       runWriteAction { | ||||
|         // Leave visual mode | ||||
|         editor.exitVisualMode() | ||||
|         editor.ij.caretModel.moveToOffset(selectionStart) | ||||
|          | ||||
|         // Reset the key handler so that the command trie is updated for the new mode (Normal) | ||||
|         // TODO: This should probably be handled by ToHandlerMapping.execute | ||||
| @@ -164,6 +166,10 @@ internal class VimSurroundExtension : VimExtension { | ||||
|  | ||||
|     companion object { | ||||
|       fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) { | ||||
|         editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) } | ||||
|       } | ||||
|        | ||||
|       fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) { | ||||
|         // Save old register values for carets | ||||
|         val surroundings = editor.sortedCarets() | ||||
|           .map { | ||||
| @@ -206,7 +212,7 @@ internal class VimSurroundExtension : VimExtension { | ||||
|               val trimmedValue = if (newSurround.shouldTrim) innerValue.trim() else innerValue | ||||
|               it.first + trimmedValue + it.second | ||||
|             } ?: 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) | ||||
|  | ||||
|             surrounding.caret to putData | ||||
| @@ -284,21 +290,42 @@ internal class VimSurroundExtension : VimExtension { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private class Operator : OperatorFunction { | ||||
|     override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean { | ||||
|       val ijEditor = editor.ij | ||||
|   private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction { | ||||
|     override fun apply(vimEditor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean { | ||||
|       val ijEditor = vimEditor.ij | ||||
|       val c = getChar(ijEditor) | ||||
|       if (c.code == 0) return true | ||||
|  | ||||
|       val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false | ||||
|       // XXX: Will it work with line-wise or block-wise selections? | ||||
|       val range = getSurroundRange(editor.currentCaret()) ?: return false | ||||
|       performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE) | ||||
|  | ||||
|       runWriteAction { | ||||
|         val change = VimPlugin.getChange() | ||||
|         if (supportsMultipleCursors) { | ||||
|           ijEditor.runWithEveryCaretAndRestore { | ||||
|             applyOnce(ijEditor, change, pair, count) | ||||
|           } | ||||
|         } | ||||
|         else { | ||||
|           applyOnce(ijEditor, change, pair, count) | ||||
|           // Jump back to start | ||||
|           executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor) | ||||
|         } | ||||
|       } | ||||
|       return true | ||||
|     } | ||||
|      | ||||
|     private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: SurroundPair, count: Int) { | ||||
|       // XXX: Will it work with line-wise or block-wise selections? | ||||
|       val primaryCaret = editor.caretModel.primaryCaret | ||||
|       val range = getSurroundRange(primaryCaret.vim) | ||||
|       if (range != null) { | ||||
|         val start = RepeatedCharSequence.of(pair.first, count) | ||||
|         val end = RepeatedCharSequence.of(pair.second, count) | ||||
|         change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start) | ||||
|         change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     private fun getSurroundRange(caret: VimCaret): TextRange? { | ||||
|       val editor = caret.editor | ||||
|       if (editor.mode is Mode.CMD_LINE) { | ||||
| @@ -398,15 +425,15 @@ private fun getChar(editor: Editor): Char { | ||||
|   return res | ||||
| } | ||||
|  | ||||
| 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 { | ||||
|     val editor = caret.editor | ||||
|     val change = VimPlugin.getChange() | ||||
|     val leftSurround = pair.first + if (tagsOnNewLines) "\n" else "" | ||||
|     val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count) | ||||
|  | ||||
|     val isEOF = range.endOffset == editor.text().length | ||||
|     val hasNewLine = editor.endsWithNewLine() | ||||
|     val rightSurround = if (tagsOnNewLines) { | ||||
|     val rightSurround = (if (tagsOnNewLines) { | ||||
|       if (isEOF && !hasNewLine) { | ||||
|         "\n" + pair.second | ||||
|       } else { | ||||
| @@ -414,7 +441,7 @@ private fun performSurround(pair: SurroundPair, range: TextRange, caret: VimCare | ||||
|       } | ||||
|     } else { | ||||
|       pair.second | ||||
|     } | ||||
|     }).let { RepeatedCharSequence.of(it, count) } | ||||
|  | ||||
|     change.insertText(editor, caret, range.startOffset, leftSurround) | ||||
|     change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround) | ||||
|   | ||||
| @@ -43,7 +43,6 @@ import com.maddyhome.idea.vim.newapi.ij | ||||
| import com.maddyhome.idea.vim.state.mode.Mode | ||||
| import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService | ||||
| import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService | ||||
| import kotlin.math.min | ||||
|  | ||||
| /** | ||||
|  * Provides all the insert/replace related functionality | ||||
| @@ -156,6 +155,7 @@ class ChangeGroup : VimChangeGroupBase() { | ||||
|     context: ExecutionContext, | ||||
|     range: TextRange, | ||||
|   ) { | ||||
|     val startPos = editor.offsetToBufferPosition(caret.offset) | ||||
|     val startOffset = editor.getLineStartForOffset(range.startOffset) | ||||
|     val endOffset = editor.getLineEndForOffset(range.endOffset) | ||||
|     val ijEditor = (editor as IjVimEditor).editor | ||||
| @@ -165,7 +165,7 @@ class ChangeGroup : VimChangeGroupBase() { | ||||
|     var copiedText: IjVimCopiedText? = null | ||||
|     try { | ||||
|       if (injector.registerGroup.isPrimaryRegisterSupported()) { | ||||
|         copiedText = injector.clipboardManager.getPrimaryContent(editor, context) as IjVimCopiedText | ||||
|         copiedText = injector.clipboardManager.getPrimaryContent() as IjVimCopiedText | ||||
|       } | ||||
|     } catch (e: Exception) { | ||||
|       // 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 firstLine = editor.offsetToBufferPosition( | ||||
|         min(startOffset.toDouble(), endOffset.toDouble()).toInt() | ||||
|       ).line | ||||
|       val newOffset = injector.motion.moveCaretToLineStartSkipLeading(editor, firstLine) | ||||
|       caret.moveToOffset(newOffset) | ||||
|       caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, startPos.line)) | ||||
|       restoreCursor(editor, caret, (caret as IjVimCaret).caret.logicalPosition.line) | ||||
|     } | ||||
|     if (project != null) { | ||||
|   | ||||
| @@ -141,7 +141,7 @@ object IjOptions { | ||||
|   // Temporary feature flags during development, not really intended for external use | ||||
|   val closenotebooks: ToggleOption = | ||||
|     addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true)) | ||||
|   val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true)) | ||||
|   val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isHidden = true)) | ||||
|   val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true)) | ||||
|  | ||||
|   // This needs to be Option<out VimDataType> so that it can work with derived option types, such as NumberOption, which | ||||
|   | ||||
| @@ -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 } | ||||
|             } | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| @@ -21,6 +21,7 @@ import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.helper.MessageHelper.message | ||||
| import com.maddyhome.idea.vim.macro.VimMacroBase | ||||
| import com.maddyhome.idea.vim.newapi.IjVimEditor | ||||
| import com.maddyhome.idea.vim.newapi.ij | ||||
|  | ||||
| /** | ||||
|  * Used to handle playback of macros | ||||
| @@ -89,6 +90,9 @@ internal class MacroGroup : VimMacroBase() { | ||||
|         } finally { | ||||
|           keyStack.removeFirst() | ||||
|         } | ||||
|         if (!isInternalMacro) { | ||||
|           MacroAutoImport.run(editor.ij, context.ij) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (isInternalMacro) { | ||||
|   | ||||
| @@ -89,6 +89,9 @@ internal class MotionGroup : VimMotionGroupBase() { | ||||
|   } | ||||
|  | ||||
|   override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion { | ||||
|     if (editor.ij.softWrapModel.isSoftWrappingEnabled) { | ||||
|       return AbsoluteOffset(caret.ij.visualLineStart) | ||||
|     } | ||||
|     val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) | ||||
|     return moveCaretToColumn(editor, caret, col, false) | ||||
|   } | ||||
| @@ -97,6 +100,15 @@ internal class MotionGroup : VimMotionGroupBase() { | ||||
|     editor: VimEditor, | ||||
|     caret: ImmutableVimCaret, | ||||
|   ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int { | ||||
|     if (editor.ij.softWrapModel.isSoftWrappingEnabled) { | ||||
|       val offset = caret.ij.visualLineStart | ||||
|       val line = editor.offsetToBufferPosition(offset).line | ||||
|       return if (offset == editor.getLineStartOffset(line)) { | ||||
|         editor.getLeadingCharacterOffset(line, 0) | ||||
|       } else { | ||||
|         offset | ||||
|       } | ||||
|     } | ||||
|     val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line) | ||||
|     val bufferLine = caret.getLine() | ||||
|     return editor.getLeadingCharacterOffset(bufferLine, col) | ||||
| @@ -107,6 +119,9 @@ internal class MotionGroup : VimMotionGroupBase() { | ||||
|     caret: ImmutableVimCaret, | ||||
|     allowEnd: Boolean, | ||||
|   ): Motion { | ||||
|     if (editor.ij.softWrapModel.isSoftWrappingEnabled) { | ||||
|       return AbsoluteOffset(caret.ij.visualLineEnd - 1) | ||||
|     } | ||||
|     val col = EditorHelper.getVisualColumnAtRightOfDisplay(editor.ij, caret.getVisualPosition().line) | ||||
|     return moveCaretToColumn(editor, caret, col, allowEnd) | ||||
|   } | ||||
|   | ||||
| @@ -33,7 +33,6 @@ import com.intellij.openapi.ui.Messages | ||||
| import com.intellij.openapi.util.SystemInfo | ||||
| import com.maddyhome.idea.vim.VimPlugin | ||||
| 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.handler.KeyMapIssue | ||||
| 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.ShortcutOwnerInfo | ||||
| 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.ui.VimEmulationConfigurable | ||||
| import com.maddyhome.idea.vim.vimscript.services.VimRcService | ||||
| @@ -62,55 +59,11 @@ internal class NotificationService(private val project: Project?) { | ||||
|   @Suppress("unused") | ||||
|   constructor() : this(null) | ||||
|  | ||||
|   fun notifyAboutIdeaPut() { | ||||
|     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, | ||||
|     ) | ||||
|   fun notifyAboutNewUndo() {} | ||||
|  | ||||
|     notification.addAction(OpenIdeaVimRcAction(notification)) | ||||
|   fun notifyAboutIdeaPut() {} | ||||
|  | ||||
|     notification.addAction( | ||||
|       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 notifyAboutIdeaJoin(editor: VimEditor) {} | ||||
|  | ||||
|   fun enableRepeatingMode() = Messages.showYesNoDialog( | ||||
|     "Do you want to enable repeating keys in macOS on press and hold?\n\n" + | ||||
|   | ||||
| @@ -25,10 +25,9 @@ import org.jetbrains.annotations.Nullable; | ||||
| import javax.swing.*; | ||||
| import java.awt.event.KeyEvent; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import static com.maddyhome.idea.vim.api.VimInjectorKt.injector; | ||||
|  | ||||
| /** | ||||
|  * This group works with command associated with copying and pasting text | ||||
|  */ | ||||
| @@ -128,7 +127,7 @@ public class RegisterGroup extends VimRegisterGroupBase implements PersistentSta | ||||
|           final String text = VimPlugin.getXML().getSafeXmlText(textElement); | ||||
|           if (text != null) { | ||||
|             logger.trace("Register data parsed"); | ||||
|             register = new Register(key, injector.getClipboardManager().dumbCopiedText(text), type); | ||||
|             register = new Register(key, type, text, Collections.emptyList()); | ||||
|           } | ||||
|           else { | ||||
|             logger.trace("Cannot parse register data"); | ||||
|   | ||||
| @@ -37,7 +37,6 @@ import com.maddyhome.idea.vim.ide.isClionNova | ||||
| import com.maddyhome.idea.vim.ide.isRider | ||||
| import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS | ||||
| import com.maddyhome.idea.vim.newapi.IjVimCaret | ||||
| import com.maddyhome.idea.vim.newapi.IjVimCopiedText | ||||
| import com.maddyhome.idea.vim.newapi.IjVimEditor | ||||
| import com.maddyhome.idea.vim.newapi.ij | ||||
| import com.maddyhome.idea.vim.newapi.vim | ||||
| @@ -128,7 +127,7 @@ internal class PutGroup : VimPutBase() { | ||||
|       point.dispose() | ||||
|       if (!caret.isValid) return@forEach | ||||
|  | ||||
|       val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.copiedText.text.length) | ||||
|       val caretPossibleEndOffset = lastPastedRegion?.endOffset ?: (startOffset + text.text.length) | ||||
|       val endOffset = if (data.indent) { | ||||
|         doIndent( | ||||
|           vimEditor, | ||||
| @@ -180,10 +179,12 @@ internal class PutGroup : VimPutBase() { | ||||
|     val allContentsBefore = CopyPasteManager.getInstance().allContents | ||||
|     val sizeBeforeInsert = allContentsBefore.size | ||||
|     val firstItemBefore = allContentsBefore.firstOrNull() | ||||
|     logger.debug { "Copied text: ${text.copiedText}" } | ||||
|     val (textContent, transferableData) = text.copiedText as IjVimCopiedText | ||||
|     logger.debug { "Transferable classes: ${text.transferableData.joinToString { it.javaClass.name }}" } | ||||
|     val origContent: TextBlockTransferable = | ||||
|       injector.clipboardManager.setClipboardText(textContent, textContent, transferableData) as TextBlockTransferable | ||||
|       injector.clipboardManager.setClipboardText( | ||||
|       text.text, | ||||
|       transferableData = text.transferableData, | ||||
|     ) as TextBlockTransferable | ||||
|     val allContentsAfter = CopyPasteManager.getInstance().allContents | ||||
|     val sizeAfterInsert = allContentsAfter.size | ||||
|     try { | ||||
| @@ -191,7 +192,7 @@ internal class PutGroup : VimPutBase() { | ||||
|     } finally { | ||||
|       val textInClipboard = (firstItemBefore as? TextBlockTransferable) | ||||
|         ?.getTransferData(DataFlavor.stringFlavor) as? String | ||||
|       val textOnTop = textInClipboard != null && textInClipboard != text.copiedText.text | ||||
|       val textOnTop = textInClipboard != null && textInClipboard != text.text | ||||
|       if (sizeBeforeInsert != sizeAfterInsert || textOnTop) { | ||||
|         // Sometimes an inserted text replaces an existing one. E.g. on insert with + or * register | ||||
|         (CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) } | ||||
|   | ||||
| @@ -344,7 +344,7 @@ public class EditorHelper { | ||||
|  | ||||
|     final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight); | ||||
|     final @NotNull VimEditor editor1 = new IjVimEditor(editor); | ||||
|     final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) - 1; | ||||
|     final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) + editor.getSettings().getAdditionalLinesCount(); | ||||
|     final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine); | ||||
|  | ||||
|     // For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen. | ||||
|   | ||||
| @@ -12,7 +12,9 @@ package com.maddyhome.idea.vim.helper | ||||
|  | ||||
| import com.intellij.codeWithMe.ClientId | ||||
| import com.intellij.openapi.editor.Caret | ||||
| import com.intellij.openapi.editor.CaretState | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.intellij.openapi.editor.EditorKind | ||||
| import com.intellij.openapi.editor.ex.util.EditorUtil | ||||
| import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx | ||||
| 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.key.IdeaVimDisablerExtensionPoint | ||||
| import com.maddyhome.idea.vim.newapi.globalIjOptions | ||||
| import com.maddyhome.idea.vim.newapi.vim | ||||
| import com.maddyhome.idea.vim.state.mode.inBlockSelection | ||||
| import java.awt.Component | ||||
| import javax.swing.JComponent | ||||
| import javax.swing.JTable | ||||
| @@ -102,8 +106,7 @@ internal fun Editor.isPrimaryEditor(): Boolean { | ||||
| internal fun Editor.isTerminalEditor(): Boolean { | ||||
|   return !isViewer | ||||
|     && document.isWritable | ||||
|     && !EditorHelper.isFileEditor(this) | ||||
|     && !EditorHelper.isDiffEditor(this) | ||||
|     && this.editorKind == EditorKind.CONSOLE | ||||
| } | ||||
|  | ||||
| // Optimized clone of com.intellij.ide.ui.laf.darcula.DarculaUIUtil.isTableCellEditor | ||||
| @@ -136,3 +139,41 @@ internal val Caret.vimLine: Int | ||||
|  */ | ||||
| internal val Editor.vimLine: Int | ||||
|   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 | ||||
|   }  | ||||
| } | ||||
|   | ||||
| @@ -59,7 +59,7 @@ internal object ScrollViewHelper { | ||||
|     // that this needs to be replaced as a more or less dumb line for line rewrite. | ||||
|     val topLine = getVisualLineAtTopOfScreen(editor) | ||||
|     val bottomLine = getVisualLineAtBottomOfScreen(editor) | ||||
|     val lastLine = vimEditor.getVisualLineCount() - 1 | ||||
|     val lastLine = vimEditor.getVisualLineCount() + editor.settings.additionalLinesCount | ||||
|  | ||||
|     // We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred | ||||
|     val scrollOffset = injector.options(vimEditor).scrolloff | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import com.intellij.openapi.editor.markup.HighlighterLayer | ||||
| import com.intellij.openapi.editor.markup.HighlighterTargetArea | ||||
| import com.intellij.openapi.editor.markup.RangeHighlighter | ||||
| import com.intellij.openapi.editor.markup.TextAttributes | ||||
| import com.intellij.util.application | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.globalOptions | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| @@ -30,6 +31,7 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode | ||||
| import org.jetbrains.annotations.Contract | ||||
| import java.awt.Font | ||||
| import java.util.* | ||||
| import javax.swing.Timer | ||||
|  | ||||
| internal fun updateSearchHighlights( | ||||
|   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 | ||||
|  */ | ||||
| @@ -125,17 +133,28 @@ private fun updateSearchHighlights( | ||||
|       // hlsearch (+ incsearch/noincsearch) | ||||
|       // Make sure the range fits this editor. Note that Vim will use the same range for all windows. E.g., given | ||||
|       // `:1,5s/foo`, Vim will highlight all occurrences of `foo` in the first five lines of all visible windows | ||||
|       val isSearching = injector.commandLine.getActiveCommandLine() != null | ||||
|       application.invokeLater { | ||||
|         val vimEditor = editor.vim | ||||
|         val editorLastLine = vimEditor.lineCount() - 1 | ||||
|         val searchStartLine = searchRange?.startLine ?: 0 | ||||
|         val searchEndLine = (searchRange?.endLine ?: -1).coerceAtMost(editorLastLine) | ||||
|         if (searchStartLine <= editorLastLine) { | ||||
|           val visibleArea = editor.scrollingModel.visibleAreaOnScrollingFinished | ||||
|           val visibleTopLeft = visibleArea.location | ||||
|           val visibleBottomRight = visibleArea.location.apply { translate(visibleArea.width, visibleArea.height) } | ||||
|           val visibleStartOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleTopLeft)) | ||||
|           val visibleEndOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(visibleBottomRight)) | ||||
|           val visibleStartLine = editor.document.getLineNumber(visibleStartOffset) | ||||
|           val visibleEndLine = editor.document.getLineNumber(visibleEndOffset) | ||||
|           removeSearchHighlights(editor) | ||||
|  | ||||
|           val results = | ||||
|             injector.searchHelper.findAll( | ||||
|               vimEditor, | ||||
|               pattern, | ||||
|             searchStartLine, | ||||
|             searchEndLine, | ||||
|               searchStartLine.coerceAtLeast(visibleStartLine), | ||||
|               searchEndLine.coerceAtMost(visibleEndLine), | ||||
|               shouldIgnoreCase(pattern, shouldIgnoreSmartCase) | ||||
|             ) | ||||
|           if (results.isNotEmpty()) { | ||||
| @@ -143,9 +162,14 @@ private fun updateSearchHighlights( | ||||
|               currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards) | ||||
|             } | ||||
|             highlightSearchResults(editor, pattern, results, currentMatchOffset) | ||||
|             if (!isSearching) { | ||||
|               removeHighlightsEditors.add(editor) | ||||
|               removeHighlightsTimer.restart() | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         editor.vimLastSearch = pattern | ||||
|       } | ||||
|     } else if (shouldAddCurrentMatchSearchHighlight(pattern, showHighlights, initialOffset)) { | ||||
|       // nohlsearch + incsearch. Even though search highlights are disabled, we still show a highlight (current editor | ||||
|       // only), because 'incsearch' is active. But we don't show a search if Visual is active (behind Command-line of | ||||
| @@ -179,6 +203,7 @@ private fun updateSearchHighlights( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   removeHighlightsTimer.restart() | ||||
|   return currentEditorCurrentMatchOffset | ||||
| } | ||||
|  | ||||
| @@ -204,7 +229,7 @@ private fun removeSearchHighlights(editor: Editor) { | ||||
|  */ | ||||
| @Contract("_, _, false -> false; _, null, true -> false") | ||||
| private fun shouldAddAllSearchHighlights(editor: Editor, newPattern: String?, hlSearch: Boolean): Boolean { | ||||
|   return hlSearch && newPattern != null && newPattern != editor.vimLastSearch && newPattern != "" | ||||
|   return hlSearch && newPattern != null && newPattern != "" | ||||
| } | ||||
|  | ||||
| private fun findClosestMatch( | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import com.intellij.openapi.fileEditor.TextEditorWithPreview | ||||
| import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider | ||||
| import com.intellij.openapi.util.registry.Registry | ||||
| import com.intellij.util.PlatformUtils | ||||
| import com.maddyhome.idea.vim.VimPlugin | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| @@ -29,6 +30,8 @@ import com.maddyhome.idea.vim.common.InsertSequence | ||||
| import com.maddyhome.idea.vim.newapi.IjVimCaret | ||||
| import com.maddyhome.idea.vim.newapi.globalIjOptions | ||||
| import com.maddyhome.idea.vim.newapi.ij | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
| import com.maddyhome.idea.vim.state.mode.inVisualMode | ||||
| import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService | ||||
|  | ||||
| /** | ||||
| @@ -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 | ||||
|       editor.runWithChangeTracking { | ||||
|         undoManager.undo(fileEditor) | ||||
|  | ||||
|         // We execute undo one more time if the previous one just restored selection | ||||
|         if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) { | ||||
|           undoManager.undo(fileEditor) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       CommandProcessor.getInstance().runUndoTransparentAction { | ||||
|         removeSelections(editor) | ||||
|         restoreVisualMode(editor) | ||||
|       } | ||||
|     } else { | ||||
|       runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) { | ||||
| @@ -241,4 +236,21 @@ internal class UndoRedoHelper : VimTimestampBasedUndoService { | ||||
|     val hasChanges: Boolean | ||||
|       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) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,6 @@ import com.intellij.openapi.editor.VisualPosition | ||||
| import com.intellij.openapi.editor.markup.RangeHighlighter | ||||
| import com.intellij.openapi.util.Key | ||||
| import com.intellij.openapi.util.UserDataHolder | ||||
| import com.maddyhome.idea.vim.api.CaretRegisterStorageBase | ||||
| import com.maddyhome.idea.vim.api.LocalMarkStorage | ||||
| import com.maddyhome.idea.vim.api.SelectionInfo | ||||
| import com.maddyhome.idea.vim.common.InsertSequence | ||||
| @@ -98,7 +97,6 @@ internal var Caret.vimInsertStart: RangeMarker by userDataOr { | ||||
| } | ||||
|  | ||||
| // 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.lastSelectionInfo: SelectionInfo? by userDataCaretToEditor() | ||||
|  | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| package com.maddyhome.idea.vim.helper | ||||
|  | ||||
| import com.intellij.ide.plugins.StandalonePluginUpdateChecker | ||||
| import com.intellij.openapi.components.Service | ||||
| import com.intellij.openapi.components.service | ||||
| import com.maddyhome.idea.vim.VimPlugin | ||||
| import com.maddyhome.idea.vim.group.NotificationService | ||||
| import com.maddyhome.idea.vim.icons.VimIcons | ||||
|  | ||||
| @Service(Service.Level.APP) | ||||
| internal class VimStandalonePluginUpdateChecker : StandalonePluginUpdateChecker( | ||||
|   VimPlugin.getPluginId(), | ||||
|   updateTimestampProperty = PROPERTY_NAME, | ||||
|   NotificationService.IDEAVIM_STICKY_GROUP, | ||||
|   VimIcons.IDEAVIM, | ||||
| ) { | ||||
|  | ||||
|   override fun skipUpdateCheck(): Boolean = VimPlugin.isNotEnabled() || "dev" in VimPlugin.getVersion() | ||||
|  | ||||
|   companion object { | ||||
|     private const val PROPERTY_NAME = "ideavim.statistics.timestamp" | ||||
|     fun getInstance(): VimStandalonePluginUpdateChecker = service() | ||||
|   } | ||||
| } | ||||
| @@ -64,8 +64,10 @@ class IJEditorFocusListener : EditorListener { | ||||
|       VimPlugin.getChange().insertBeforeCursor(editor, context) | ||||
|       KeyHandler.getInstance().lastUsedEditorInfo = LastUsedEditorInfo(currentEditorHashCode, true) | ||||
|     } | ||||
|     if (isCurrentEditorTerminal && !ijEditor.inInsertMode) { | ||||
|     if (isCurrentEditorTerminal) { | ||||
|       if (!ijEditor.inInsertMode) { | ||||
|         switchToInsertMode.run() | ||||
|       } | ||||
|     } else if (ijEditor.isInsertMode && (oldEditorInfo.isInsertModeForced || !ijEditor.document.isWritable)) { | ||||
|       val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor) | ||||
|       val mode = injector.vimState.mode | ||||
|   | ||||
| @@ -16,7 +16,9 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction | ||||
| import com.intellij.codeInsight.template.Template | ||||
| import com.intellij.codeInsight.template.TemplateEditingAdapter | ||||
| 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.actions.NextVariableAction | ||||
| import com.intellij.find.FindModelListener | ||||
| import com.intellij.ide.actions.ApplyIntentionAction | ||||
| import com.intellij.openapi.actionSystem.ActionManager | ||||
| @@ -30,6 +32,7 @@ import com.intellij.openapi.actionSystem.ex.AnActionListener | ||||
| import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.intellij.openapi.editor.actions.EnterAction | ||||
| import com.intellij.openapi.editor.impl.ScrollingModelImpl | ||||
| import com.intellij.openapi.keymap.KeymapManager | ||||
| import com.intellij.openapi.project.DumbAwareToggleAction | ||||
| import com.intellij.openapi.util.TextRange | ||||
| @@ -61,6 +64,7 @@ internal object IdeaSpecifics { | ||||
|     private val surrounderAction = | ||||
|       "com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction" | ||||
|     private var editor: Editor? = null | ||||
|     private var caretOffset = -1 | ||||
|     private var completionPrevDocumentLength: Int? = null | ||||
|     private var completionPrevDocumentOffset: Int? = null | ||||
|  | ||||
| @@ -70,6 +74,7 @@ internal object IdeaSpecifics { | ||||
|       val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR) | ||||
|       if (hostEditor != null) { | ||||
|         editor = hostEditor | ||||
|         caretOffset = hostEditor.caretModel.offset | ||||
|       } | ||||
|  | ||||
|       val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction | ||||
| @@ -127,7 +132,8 @@ internal object IdeaSpecifics { | ||||
|       if (VimPlugin.isNotEnabled()) return | ||||
|  | ||||
|       val editor = editor | ||||
|       if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) { | ||||
|       if (editor != null) { | ||||
|         if (action is ChooseItemAction && injector.registerGroup.isRecording) { | ||||
|           val prevDocumentLength = completionPrevDocumentLength | ||||
|           val prevDocumentOffset = completionPrevDocumentOffset | ||||
|  | ||||
| @@ -167,9 +173,27 @@ internal object IdeaSpecifics { | ||||
|             KeyHandler.getInstance().reset(it.vim) | ||||
|           } | ||||
|         } | ||||
|         else if (action is NextVariableAction && TemplateManagerImpl.getTemplateState(editor) == null) { | ||||
|           editor.vim.exitInsertMode(event.dataContext.vim) | ||||
|           KeyHandler.getInstance().reset(editor.vim) | ||||
|         } | ||||
|         //endregion | ||||
|  | ||||
|         if (caretOffset != -1 && caretOffset != editor.caretModel.offset) { | ||||
|           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) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       this.editor = null | ||||
|       this.caretOffset = -1 | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -81,7 +81,6 @@ import com.maddyhome.idea.vim.handler.keyCheckRequests | ||||
| import com.maddyhome.idea.vim.helper.CaretVisualAttributesListener | ||||
| import com.maddyhome.idea.vim.helper.GuicursorChangeListener | ||||
| import com.maddyhome.idea.vim.helper.StrictMode | ||||
| import com.maddyhome.idea.vim.helper.VimStandalonePluginUpdateChecker | ||||
| import com.maddyhome.idea.vim.helper.exitSelectMode | ||||
| import com.maddyhome.idea.vim.helper.exitVisualMode | ||||
| import com.maddyhome.idea.vim.helper.forceBarCursor | ||||
| @@ -98,6 +97,7 @@ import com.maddyhome.idea.vim.newapi.IjVimSearchGroup | ||||
| import com.maddyhome.idea.vim.newapi.InsertTimeRecorder | ||||
| import com.maddyhome.idea.vim.newapi.ij | ||||
| 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.selectionType | ||||
| import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener | ||||
| @@ -412,9 +412,20 @@ internal object VimListenerManager { | ||||
|       // 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 | ||||
|        | ||||
|       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) | ||||
|       FileGroup.fileEditorManagerSelectionChangedCallback(event) | ||||
|       VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event) | ||||
| //      VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event) | ||||
|       IjVimRedrawService.fileEditorManagerSelectionChangedCallback(event) | ||||
|       VimLastSelectedEditorTracker.setLastSelectedEditor(event.newEditor) | ||||
|     } | ||||
| @@ -487,8 +498,6 @@ internal object VimListenerManager { | ||||
|           OpeningEditor(openingEditor, owningEditorWindow, isPreview, canBeReused) | ||||
|         ) | ||||
|       } | ||||
|  | ||||
|       VimStandalonePluginUpdateChecker.getInstance().pluginUsed() | ||||
|     } | ||||
|  | ||||
|     override fun editorReleased(event: EditorFactoryEvent) { | ||||
|   | ||||
| @@ -39,7 +39,7 @@ import java.io.IOException | ||||
|  | ||||
| @Service | ||||
| internal class IjClipboardManager : VimClipboardManager { | ||||
|   override fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): IjVimCopiedText? { | ||||
|   override fun getPrimaryContent(): IjVimCopiedText? { | ||||
|     val clipboard = Toolkit.getDefaultToolkit()?.systemSelection ?: return null | ||||
|     val contents = clipboard.getContents(null) ?: return null | ||||
|     val (text, transferableData) = getTextAndTransferableData(contents) ?: return null | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -12,8 +12,6 @@ import com.intellij.openapi.editor.Caret | ||||
| import com.intellij.openapi.editor.LogicalPosition | ||||
| import com.intellij.openapi.editor.VisualPosition | ||||
| import com.maddyhome.idea.vim.api.BufferPosition | ||||
| import com.maddyhome.idea.vim.api.CaretRegisterStorage | ||||
| import com.maddyhome.idea.vim.api.CaretRegisterStorageBase | ||||
| import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.LocalMarkStorage | ||||
| import com.maddyhome.idea.vim.api.SelectionInfo | ||||
| @@ -21,6 +19,7 @@ import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimCaretBase | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.VimVisualPosition | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.common.InsertSequence | ||||
| import com.maddyhome.idea.vim.common.LiveRange | ||||
| import com.maddyhome.idea.vim.group.visual.VisualChange | ||||
| @@ -29,7 +28,6 @@ import com.maddyhome.idea.vim.helper.insertHistory | ||||
| import com.maddyhome.idea.vim.helper.lastSelectionInfo | ||||
| import com.maddyhome.idea.vim.helper.markStorage | ||||
| import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset | ||||
| import com.maddyhome.idea.vim.helper.registerStorage | ||||
| import com.maddyhome.idea.vim.helper.resetVimLastColumn | ||||
| import com.maddyhome.idea.vim.helper.vimInsertStart | ||||
| import com.maddyhome.idea.vim.helper.vimLastColumn | ||||
| @@ -37,22 +35,14 @@ import com.maddyhome.idea.vim.helper.vimLastVisualOperatorRange | ||||
| import com.maddyhome.idea.vim.helper.vimLine | ||||
| import com.maddyhome.idea.vim.helper.vimSelectionStart | ||||
| import com.maddyhome.idea.vim.helper.vimSelectionStartClear | ||||
| import com.maddyhome.idea.vim.register.VimRegisterGroup | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
|  | ||||
| internal class IjVimCaret(val caret: Caret) : VimCaretBase() { | ||||
|  | ||||
|   override val registerStorage: CaretRegisterStorage | ||||
|     get() { | ||||
|       var storage = this.caret.registerStorage | ||||
|       if (storage == null) { | ||||
|         initInjector() // To initialize injector used in CaretRegisterStorageBase | ||||
|         storage = CaretRegisterStorageBase(this) | ||||
|         this.caret.registerStorage = storage | ||||
|       } else if (storage.caret != this) { | ||||
|         storage.caret = this | ||||
|       } | ||||
|       return storage | ||||
|     } | ||||
|   override val registerStorage: VimRegisterGroup | ||||
|     get() = injector.registerGroup | ||||
|   | ||||
|   override val markStorage: LocalMarkStorage | ||||
|     get() { | ||||
|       var storage = this.caret.markStorage | ||||
|   | ||||
| @@ -35,8 +35,8 @@ import com.maddyhome.idea.vim.api.VimFoldRegion | ||||
| import com.maddyhome.idea.vim.api.VimIndentConfig | ||||
| import com.maddyhome.idea.vim.api.VimScrollingModel | ||||
| 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.VimVisualPosition | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.common.IndentConfig | ||||
| import com.maddyhome.idea.vim.common.LiveRange | ||||
| @@ -179,21 +179,38 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase( | ||||
|     return editor.caretModel.allCarets.map { IjVimCaret(it) } | ||||
|   } | ||||
|  | ||||
|   override var isFirstCaret = true | ||||
|   override var isReversingCarets = false | ||||
|    | ||||
|   @Suppress("ideavimRunForEachCaret") | ||||
|   override fun forEachCaret(action: (VimCaret) -> Unit) { | ||||
|     if (editor.vim.inBlockSelection) { | ||||
|       action(IjVimCaret(editor.caretModel.primaryCaret)) | ||||
|     } else { | ||||
|       try { | ||||
|         editor.caretModel.runForEachCaret({ | ||||
|           if (it.isValid) { | ||||
|             action(IjVimCaret(it)) | ||||
|             isFirstCaret = false | ||||
|           } | ||||
|         }, false) | ||||
|       } finally { | ||||
|         isFirstCaret = true | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) { | ||||
|     editor.caretModel.runForEachCaret({ action(IjVimCaret(it)) }, reverse) | ||||
|     isReversingCarets = reverse | ||||
|     try { | ||||
|       editor.caretModel.runForEachCaret({ | ||||
|         action(IjVimCaret(it)) | ||||
|         isFirstCaret = false | ||||
|       }, reverse) | ||||
|     } finally { | ||||
|       isFirstCaret = true | ||||
|       isReversingCarets = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun isInForEachCaretScope(): Boolean { | ||||
| @@ -497,6 +514,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 { | ||||
|     return caret | ||||
|   } | ||||
|   | ||||
| @@ -353,7 +353,7 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { | ||||
|         int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder() | ||||
|           .calculateCount0Snapshot()); | ||||
|  | ||||
|         if (labelText.equals("/") || labelText.equals("?") || searchCommand) { | ||||
|         if ((labelText.equals("/") || labelText.equals("?") || searchCommand) && !injector.getMacro().isExecutingMacro()) { | ||||
|           final boolean forwards = !labelText.equals("?");  // :s, :g, :v are treated as forwards | ||||
|           int patternEnd = injector.getSearchGroup().findEndOfPattern(searchText, separator, 0); | ||||
|           final String pattern = searchText.substring(0, patternEnd); | ||||
|   | ||||
| @@ -1,12 +1,4 @@ | ||||
| <!-- | ||||
|   ~ 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" xmlns:xi="http://www.w3.org/2001/XInclude"> | ||||
| <idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude"> | ||||
|   <name>IdeaVim</name> | ||||
|   <id>IdeaVIM</id> | ||||
|   <description><![CDATA[ | ||||
| @@ -21,7 +13,7 @@ | ||||
|         <li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li> | ||||
|       </ul> | ||||
|     ]]></description> | ||||
|   <version>SNAPSHOT</version> | ||||
|   <version>chylex</version> | ||||
|   <vendor>JetBrains</vendor> | ||||
|  | ||||
|   <!-- Mark the plugin as compatible with RubyMine and other products based on the IntelliJ platform (including CWM) --> | ||||
| @@ -150,9 +142,11 @@ | ||||
|   <xi:include href="/META-INF/includes/VimListeners.xml" xpointer="xpointer(/idea-plugin/*)"/> | ||||
|  | ||||
|   <actions resource-bundle="messages.IdeaVimBundle"> | ||||
|     <action id="VimPluginToggle" class="com.maddyhome.idea.vim.action.VimPluginToggleAction"> | ||||
|     <group id="com.chylex.intellij.vim" text="Vim" popup="true"> | ||||
|       <add-to-group group-id="ToolsMenu" anchor="last"/> | ||||
|     </action> | ||||
|       <action id="VimPluginToggle" class="com.maddyhome.idea.vim.action.VimPluginToggleAction"/> | ||||
|       <action id="VimRunLastMacroInOpenFiles" class="com.maddyhome.idea.vim.action.VimRunLastMacroInOpenFiles"/> | ||||
|     </group> | ||||
|      | ||||
|     <!-- Internal --> | ||||
|     <!--suppress PluginXmlI18n --> | ||||
| @@ -171,5 +165,6 @@ | ||||
|     </group> | ||||
|  | ||||
|     <action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/> | ||||
|     <action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" /> | ||||
|   </actions> | ||||
| </idea-plugin> | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command | ||||
| import com.maddyhome.idea.vim.command.OperatorArguments | ||||
| import com.maddyhome.idea.vim.handler.VimActionHandler | ||||
|  | ||||
| @CommandOrMotion(keys = ["<C-R>"], modes = [Mode.NORMAL]) | ||||
| @CommandOrMotion(keys = ["U", "<C-R>"], modes = [Mode.NORMAL, Mode.VISUAL]) | ||||
| class RedoAction : VimActionHandler.SingleExecution() { | ||||
|   override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command | ||||
| import com.maddyhome.idea.vim.command.OperatorArguments | ||||
| import com.maddyhome.idea.vim.handler.VimActionHandler | ||||
|  | ||||
| @CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL]) | ||||
| @CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL, Mode.VISUAL]) | ||||
| class UndoAction : VimActionHandler.SingleExecution() { | ||||
|   override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
| package com.maddyhome.idea.vim.action.change.change | ||||
|  | ||||
| import com.intellij.vim.annotations.CommandOrMotion | ||||
| import com.intellij.vim.annotations.Mode | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimChangeGroup | ||||
| @@ -22,7 +21,7 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler | ||||
| /** | ||||
|  * @author vlan | ||||
|  */ | ||||
| @CommandOrMotion(keys = ["u"], modes = [Mode.VISUAL]) | ||||
| @CommandOrMotion(keys = [], modes = []) | ||||
| class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() { | ||||
|   override val type: Command.Type = Command.Type.CHANGE | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
| package com.maddyhome.idea.vim.action.change.change | ||||
|  | ||||
| import com.intellij.vim.annotations.CommandOrMotion | ||||
| import com.intellij.vim.annotations.Mode | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimChangeGroup | ||||
| @@ -22,7 +21,7 @@ import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler | ||||
| /** | ||||
|  * @author vlan | ||||
|  */ | ||||
| @CommandOrMotion(keys = ["U"], modes = [Mode.VISUAL]) | ||||
| @CommandOrMotion(keys = [], modes = []) | ||||
| class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() { | ||||
|   override val type: Command.Type = Command.Type.CHANGE | ||||
|  | ||||
|   | ||||
| @@ -69,15 +69,10 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() { | ||||
|  */ | ||||
| @RWLockLabel.SelfSynchronized | ||||
| private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean { | ||||
|   val register: Register? = injector.registerGroup.getRegister(editor, context, key) | ||||
|   val register: Register? = injector.registerGroup.getRegister(key) | ||||
|   if (register != null) { | ||||
|     val textData = PutData.TextData( | ||||
|       register.name, | ||||
|       injector.clipboardManager.dumbCopiedText(register.text), | ||||
|       SelectionType.CHARACTER_WISE | ||||
|     ) | ||||
|     val putData = | ||||
|       PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true) | ||||
|     val textData = PutData.TextData(register.text, SelectionType.CHARACTER_WISE, emptyList(), register.name) | ||||
|     val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true) | ||||
|     injector.put.putText(editor, context, putData) | ||||
|     return true | ||||
|   } | ||||
|   | ||||
| @@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.action.copy | ||||
| import com.intellij.vim.annotations.CommandOrMotion | ||||
| import com.intellij.vim.annotations.Mode | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.Argument | ||||
| @@ -36,33 +35,40 @@ sealed class PutTextBaseAction( | ||||
|     val count = operatorArguments.count1 | ||||
|     val sortedCarets = editor.sortedCarets() | ||||
|     return if (sortedCarets.size > 1) { | ||||
|       val caretToPutData = sortedCarets.associateWith { getPutDataForCaret(editor, context, it, count) } | ||||
|       val putData = getPutData(count) | ||||
|  | ||||
|       val splitText = putData.textData?.rawText?.split('\n')?.dropLastWhile(String::isEmpty) | ||||
|       val caretToPutData = if (splitText != null && splitText.size == sortedCarets.size) { | ||||
|         sortedCarets.mapIndexed { index, caret -> caret to putData.copy(textData = putData.textData.copy(rawText = splitText[splitText.lastIndex - index])) }.toMap() | ||||
|       } else { | ||||
|         sortedCarets.associateWith { putData } | ||||
|       } | ||||
|        | ||||
|       var result = true | ||||
|       caretToPutData.forEach { | ||||
|         result = injector.put.putTextForCaret(editor, it.key, context, it.value) && result | ||||
|       } | ||||
|       result | ||||
|     } else { | ||||
|       val putData = getPutDataForCaret(editor, context, sortedCarets.single(), count) | ||||
|       injector.put.putText(editor, context, putData) | ||||
|       injector.put.putText(editor, context, getPutData(count)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private fun getPutDataForCaret( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     caret: ImmutableVimCaret, | ||||
|     count: Int, | ||||
|   private fun getPutData(count: Int, | ||||
|   ): PutData { | ||||
|     val registerService = injector.registerGroup | ||||
|     val registerChar = if (caret.editor.carets().size == 1) { | ||||
|       registerService.currentRegister | ||||
|     } else { | ||||
|       registerService.getCurrentRegisterForMulticaret() | ||||
|     return PutData(getRegisterTextData(), null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1) | ||||
|   } | ||||
|     val register = caret.registerStorage.getRegister(editor, context, registerChar) | ||||
|     val textData = register?.let { TextData(register) } | ||||
|     return PutData(textData, null, count, insertTextBeforeCaret, indent, caretAfterInsertedText, -1) | ||||
| } | ||||
|  | ||||
| fun getRegisterTextData(): TextData? { | ||||
|   val register = injector.registerGroup.getRegister(injector.registerGroup.currentRegister) | ||||
|   return register?.let { | ||||
|     TextData( | ||||
|       register.text ?: injector.parser.toPrintableString(register.keys), | ||||
|       register.type, | ||||
|       register.transferableData, | ||||
|       register.name, | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -41,8 +41,22 @@ sealed class PutVisualTextBaseAction( | ||||
|   ): Boolean { | ||||
|     if (caretsAndSelections.isEmpty()) return false | ||||
|     val count = cmd.count | ||||
|     val caretToPutData = | ||||
|       editor.sortedCarets().associateWith { getPutDataForCaret(editor, context, it, caretsAndSelections[it], count) } | ||||
|     val sortedCarets = | ||||
|       editor.sortedCarets() | ||||
|  | ||||
|     val textData = getRegisterTextData() | ||||
|     val splitText = textData?.rawText?.split('\n')?.dropLastWhile(String::isEmpty) | ||||
|  | ||||
|     val caretToTextData = if (splitText != null && splitText.size == sortedCarets.size) { | ||||
|       sortedCarets.mapIndexed { index, caret -> caret to textData.copy(rawText = splitText[splitText.lastIndex - index]) }.toMap() | ||||
|     } else { | ||||
|       sortedCarets.associateWith { textData } | ||||
|     } | ||||
|      | ||||
|     val caretToPutData = caretToTextData.mapValues { (caret, textData) -> | ||||
|       getPutDataForCaret(textData, caret, caretsAndSelections[caret], count) | ||||
|     } | ||||
|      | ||||
|     injector.registerGroup.resetRegister() | ||||
|     var result = true | ||||
|     caretToPutData.forEach { | ||||
| @@ -51,16 +65,10 @@ sealed class PutVisualTextBaseAction( | ||||
|     return result | ||||
|   } | ||||
|    | ||||
|   private fun getPutDataForCaret( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|   private fun getPutDataForCaret(textData: PutData.TextData?, | ||||
|     caret: VimCaret, | ||||
|     selection: VimSelection?, | ||||
|     count: Int, | ||||
|   ): PutData { | ||||
|     val lastRegisterChar = injector.registerGroup.lastRegisterChar | ||||
|     val register = caret.registerStorage.getRegister(editor, context, lastRegisterChar) | ||||
|     val textData = register?.let { PutData.TextData(register) } | ||||
|     count: Int,): PutData { | ||||
|     val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) } | ||||
|     return PutData(textData, visualSelection, count, insertTextBeforeCaret, indent, caretAfterInsertedText) | ||||
|   } | ||||
|   | ||||
| @@ -88,8 +88,7 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) : | ||||
|  | ||||
|       else -> throw ExException("Unexpected search label ${argument.label}") | ||||
|     } | ||||
|     // Vim doesn't treat not finding something as an error, although it might report either an error or warning message | ||||
|     if (offsetAndMotion == null) return Motion.NoMotion | ||||
|     if (offsetAndMotion == null) return Motion.Error | ||||
|     parentAction.motionType = offsetAndMotion.second | ||||
|     return offsetAndMotion.first.toMotionOrError() | ||||
|   } | ||||
|   | ||||
| @@ -76,6 +76,13 @@ sealed class TillCharacterMotion( | ||||
|       ) | ||||
|     } | ||||
|     injector.motion.setLastFTCmd(tillCharacterMotionType, argument.character) | ||||
|      | ||||
|     val offset = if (!finishBeforeCharacter) "" | ||||
|     else if (direction == Direction.FORWARDS) "s-1" | ||||
|     else "s+1" | ||||
|      | ||||
|     injector.searchGroup.setLastSearchState(argument.character.let { if (it in "`^$.*[~/\\") "\\$it" else it.toString() }, offset, direction) | ||||
|      | ||||
|     return res.toMotionOrError() | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,12 @@ class MotionCamelLeftAction : MotionActionHandler.ForEachCaret() { | ||||
|     argument: Argument?, | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Motion { | ||||
|     if (caret.hasSelection() && caret.offset > caret.vimSelectionStart) { | ||||
|       val target = injector.searchHelper.findPreviousCamelEnd(editor.text(), caret.offset, operatorArguments.count1) | ||||
|       if (target != null && target > caret.vimSelectionStart) { | ||||
|         return target.toMotionOrError() | ||||
|       } | ||||
|     } | ||||
|     return injector.searchHelper.findPreviousCamelStart(editor.text(), caret.offset, operatorArguments.count1) | ||||
|       ?.toMotionOrError() ?: Motion.Error | ||||
|   } | ||||
| @@ -47,6 +53,10 @@ class MotionCamelRightAction : MotionActionHandler.ForEachCaret() { | ||||
|     argument: Argument?, | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Motion { | ||||
|     if (caret.hasSelection() && caret.offset >= caret.vimSelectionStart) { | ||||
|       return injector.searchHelper.findNextCamelEnd(editor.text(), caret.offset + 1, operatorArguments.count1) | ||||
|         ?.toMotionOrError() ?: Motion.Error | ||||
|     } | ||||
|     return injector.searchHelper.findNextCamelStart(editor.text(), caret.offset + 1, operatorArguments.count1) | ||||
|       ?.toMotionOrError() ?: Motion.Error | ||||
|   } | ||||
|   | ||||
| @@ -70,6 +70,6 @@ class MotionDownNotLineWiseAction : MotionActionHandler.ForEachCaret() { | ||||
|     argument: Argument?, | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Motion { | ||||
|     return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1) | ||||
|     return injector.motion.getVerticalMotionOffset(editor, caret, operatorArguments.count1, bufferLines = true) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -70,6 +70,6 @@ class MotionUpNotLineWiseAction : MotionActionHandler.ForEachCaret() { | ||||
|     argument: Argument?, | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Motion { | ||||
|     return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1) | ||||
|     return injector.motion.getVerticalMotionOffset(editor, caret, -operatorArguments.count1, bufferLines = true) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,6 @@ | ||||
| package com.maddyhome.idea.vim.api | ||||
|  | ||||
| import com.maddyhome.idea.vim.common.LiveRange | ||||
| import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.group.visual.VisualChange | ||||
| import com.maddyhome.idea.vim.group.visual.vimMoveBlockSelectionToOffset | ||||
| import com.maddyhome.idea.vim.group.visual.vimMoveSelectionToCaret | ||||
| @@ -17,13 +16,11 @@ import com.maddyhome.idea.vim.handler.Motion | ||||
| import com.maddyhome.idea.vim.helper.RWLockLabel | ||||
| import com.maddyhome.idea.vim.helper.StrictMode | ||||
| import com.maddyhome.idea.vim.helper.exitVisualMode | ||||
| import com.maddyhome.idea.vim.register.Register | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
| import com.maddyhome.idea.vim.register.VimRegisterGroup | ||||
| import com.maddyhome.idea.vim.state.mode.inBlockSelection | ||||
| import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual | ||||
| import com.maddyhome.idea.vim.state.mode.inSelectMode | ||||
| import com.maddyhome.idea.vim.state.mode.inVisualMode | ||||
| import javax.swing.KeyStroke | ||||
|  | ||||
| /** | ||||
|  * Immutable interface of the caret. Immutable caret is an important concept of Fleet. | ||||
| @@ -65,7 +62,7 @@ interface ImmutableVimCaret { | ||||
|   fun hasSelection(): Boolean | ||||
|  | ||||
|   var lastSelectionInfo: SelectionInfo | ||||
|   val registerStorage: CaretRegisterStorage | ||||
|   val registerStorage: VimRegisterGroup | ||||
|   val markStorage: LocalMarkStorage | ||||
| } | ||||
|  | ||||
| @@ -151,19 +148,3 @@ fun VimCaret.moveToMotion(motion: Motion): VimCaret { | ||||
|     this | ||||
|   } | ||||
| } | ||||
|  | ||||
| interface CaretRegisterStorage { | ||||
|   val caret: ImmutableVimCaret | ||||
|  | ||||
|   fun storeText( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     range: TextRange, | ||||
|     type: SelectionType, | ||||
|     isDelete: Boolean, | ||||
|   ): Boolean | ||||
|  | ||||
|   fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? | ||||
|   fun setKeys(editor: VimEditor, context: ExecutionContext, register: Char, keys: List<KeyStroke>) | ||||
|   fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) | ||||
| } | ||||
|   | ||||
| @@ -8,94 +8,4 @@ | ||||
|  | ||||
| package com.maddyhome.idea.vim.api | ||||
|  | ||||
| import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.register.Register | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants | ||||
| import com.maddyhome.idea.vim.register.VimRegisterGroupBase | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
| import javax.swing.KeyStroke | ||||
|  | ||||
| abstract class VimCaretBase : VimCaret | ||||
|  | ||||
| open class CaretRegisterStorageBase(override var caret: ImmutableVimCaret) : CaretRegisterStorage, | ||||
|   VimRegisterGroupBase() { | ||||
|   companion object { | ||||
|     private const val ALLOWED_TO_STORE_REGISTERS = RegisterConstants.RECORDABLE_REGISTERS + | ||||
|       RegisterConstants.SMALL_DELETION_REGISTER + | ||||
|       RegisterConstants.BLACK_HOLE_REGISTER + | ||||
|       RegisterConstants.LAST_INSERTED_TEXT_REGISTER + | ||||
|       RegisterConstants.LAST_SEARCH_REGISTER | ||||
|   } | ||||
|  | ||||
|   override var lastRegisterChar: Char | ||||
|     get() { | ||||
|       return injector.registerGroup.lastRegisterChar | ||||
|     } | ||||
|     set(_) {} | ||||
|  | ||||
|   override var isRegisterSpecifiedExplicitly: Boolean | ||||
|     get() { | ||||
|       return injector.registerGroup.isRegisterSpecifiedExplicitly | ||||
|     } | ||||
|     set(_) {} | ||||
|  | ||||
|   override fun storeText( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     range: TextRange, | ||||
|     type: SelectionType, | ||||
|     isDelete: Boolean, | ||||
|   ): Boolean { | ||||
|     val registerChar = if (caret.editor.carets().size == 1) currentRegister else getCurrentRegisterForMulticaret() | ||||
|     if (caret.isPrimary) { | ||||
|       val registerService = injector.registerGroup | ||||
|       registerService.lastRegisterChar = registerChar | ||||
|       return registerService.storeText(editor, context, caret, range, type, isDelete) | ||||
|     } else { | ||||
|       if (!ALLOWED_TO_STORE_REGISTERS.contains(registerChar)) { | ||||
|         return false | ||||
|       } | ||||
|       val text = preprocessTextBeforeStoring(editor.getText(range), type) | ||||
|       return storeTextInternal(editor, context, range, text, type, registerChar, isDelete) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun getRegister(r: Char): Register? { | ||||
|     val editorStub = injector.fallbackWindow | ||||
|     val contextStub = injector.executionContextManager.getEditorExecutionContext(editorStub) | ||||
|     return getRegister(editorStub, contextStub, r) | ||||
|   } | ||||
|  | ||||
|   override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? { | ||||
|     if (caret.isPrimary || !RegisterConstants.RECORDABLE_REGISTERS.contains(r)) { | ||||
|       return injector.registerGroup.getRegister(editor, context, r) | ||||
|     } | ||||
|     return super.getRegister(editor, context, r) ?: injector.registerGroup.getRegister(editor, context, r) | ||||
|   } | ||||
|  | ||||
|   override fun setKeys(register: Char, keys: List<KeyStroke>) { | ||||
|     val editorStub = injector.fallbackWindow | ||||
|     val contextStub = injector.executionContextManager.getEditorExecutionContext(editorStub) | ||||
|     setKeys(editorStub, contextStub, register, keys) | ||||
|   } | ||||
|  | ||||
|   override fun setKeys(editor: VimEditor, context: ExecutionContext, register: Char, keys: List<KeyStroke>) { | ||||
|     if (caret.isPrimary) { | ||||
|       injector.registerGroup.setKeys(register, keys) | ||||
|     } | ||||
|     if (!RegisterConstants.RECORDABLE_REGISTERS.contains(register)) { | ||||
|       return | ||||
|     } | ||||
|     return super.setKeys(register, keys) | ||||
|   } | ||||
|  | ||||
|   override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) { | ||||
|     if (caret.isPrimary) { | ||||
|       injector.registerGroup.saveRegister(editor, context, r, register) | ||||
|     } | ||||
|     if (!RegisterConstants.RECORDABLE_REGISTERS.contains(r)) { | ||||
|       return | ||||
|     } | ||||
|     return super.saveRegister(editor, context, r, register) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -231,7 +231,7 @@ interface VimChangeGroup { | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ) | ||||
|  | ||||
|   fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret | ||||
|   fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret | ||||
|  | ||||
|   fun insertText(editor: VimEditor, caret: VimCaret, str: String): VimCaret | ||||
|  | ||||
|   | ||||
| @@ -182,13 +182,21 @@ abstract class VimChangeGroupBase : VimChangeGroup { | ||||
|         return false | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE | ||||
|     val shouldYank = type != null && !isInsertMode && saveToRegister | ||||
|     if (shouldYank && !caret.registerStorage.storeText(editor, context, updatedRange, type, isDelete = true)) { | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     val mode = editor.mode | ||||
|     if (type == null || | ||||
|       (mode == Mode.INSERT || mode == Mode.REPLACE) || | ||||
|       !saveToRegister || | ||||
|       injector.registerGroup.storeText( | ||||
|         editor, | ||||
|         context, | ||||
|         caret, | ||||
|         updatedRange, | ||||
|         type, | ||||
|         true, | ||||
|         !editor.isFirstCaret, | ||||
|         editor.isReversingCarets | ||||
|       ) | ||||
|     ) { | ||||
|       val startOffsets = updatedRange.startOffsets | ||||
|       val endOffsets = updatedRange.endOffsets | ||||
|       for (i in updatedRange.size() - 1 downTo 0) { | ||||
| @@ -208,6 +216,8 @@ abstract class VimChangeGroupBase : VimChangeGroup { | ||||
|       } | ||||
|       return true | ||||
|     } | ||||
|     return false | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Inserts text into the document | ||||
| @@ -216,7 +226,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { | ||||
|    * @param caret  The caret to start insertion in | ||||
|    * @param str    The text to insert | ||||
|    */ | ||||
|   override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: String): VimCaret { | ||||
|   override fun insertText(editor: VimEditor, caret: VimCaret, offset: Int, str: CharSequence): VimCaret { | ||||
|     injector.application.runWriteAction { | ||||
|       (editor as MutableVimEditor).insertText(caret, offset, str) | ||||
|     } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import java.awt.datatransfer.Transferable | ||||
|  * - **Clipboard**: This is supported by all operating systems. It functions as a storage for the common 'copy and paste' operations typically done with Ctrl-C and Ctrl-V. | ||||
|  */ | ||||
| interface VimClipboardManager { | ||||
|   fun getPrimaryContent(editor: VimEditor, context: ExecutionContext): VimCopiedText? | ||||
|   fun getPrimaryContent(): VimCopiedText? | ||||
|  | ||||
|   fun getClipboardContent(editor: VimEditor, context: ExecutionContext): VimCopiedText? | ||||
|  | ||||
|   | ||||
| @@ -111,7 +111,8 @@ interface VimEditor { | ||||
|    * This method should perform caret merging after the operations. This is similar to IJ runForEachCaret | ||||
|    * TODO review | ||||
|    */ | ||||
|  | ||||
|   val isFirstCaret: Boolean | ||||
|   val isReversingCarets: Boolean | ||||
|   fun forEachCaret(action: (VimCaret) -> Unit) | ||||
|   fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean = false) | ||||
|   fun isInForEachCaretScope(): Boolean | ||||
| @@ -210,6 +211,7 @@ interface VimEditor { | ||||
|  | ||||
|   fun createIndentBySize(size: Int): String | ||||
|   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 | ||||
|   | ||||
| @@ -17,8 +17,6 @@ import com.maddyhome.idea.vim.common.VimListenersNotifier | ||||
| import com.maddyhome.idea.vim.diagnostic.VimLogger | ||||
| import com.maddyhome.idea.vim.diagnostic.vimLogger | ||||
| import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl | ||||
| import com.maddyhome.idea.vim.register.VimRegisterGroup | ||||
| import com.maddyhome.idea.vim.register.VimRegisterGroupBase | ||||
| import com.maddyhome.idea.vim.state.VimStateMachine | ||||
| import com.maddyhome.idea.vim.vimscript.services.VariableService | ||||
| import com.maddyhome.idea.vim.vimscript.services.VimVariableServiceBase | ||||
| @@ -28,7 +26,6 @@ import com.maddyhome.idea.vim.yank.YankGroupBase | ||||
| abstract class VimInjectorBase : VimInjector { | ||||
|   companion object { | ||||
|     val logger: VimLogger by lazy { vimLogger<VimInjectorBase>() } | ||||
|     val registerGroupStub: VimRegisterGroupBase by lazy { object : VimRegisterGroupBase() {} } | ||||
|   } | ||||
|  | ||||
|   override val vimState: VimStateMachine = VimStateMachineImpl() | ||||
| @@ -38,8 +35,6 @@ abstract class VimInjectorBase : VimInjector { | ||||
|  | ||||
|   override val variableService: VariableService by lazy { object : VimVariableServiceBase() {} } | ||||
|  | ||||
|   override val registerGroup: VimRegisterGroup by lazy { registerGroupStub } | ||||
|   override val registerGroupIfCreated: VimRegisterGroup? by lazy { registerGroupStub } | ||||
|   override val messages: VimMessages by lazy { VimMessagesStub() } | ||||
|   override val processGroup: VimProcessGroup by lazy { VimProcessGroupStub() } | ||||
|   override val application: VimApplication by lazy { VimApplicationStub() } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ interface VimMotionGroup { | ||||
|     allowWrap: Boolean = false, | ||||
|   ): Motion | ||||
|  | ||||
|   fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion | ||||
|   fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean = false): Motion | ||||
|  | ||||
| // TODO: Consider naming. These don't move the caret, but calculate offsets. Also consider returning Motion | ||||
|  | ||||
|   | ||||
| @@ -33,14 +33,18 @@ abstract class VimMotionGroupBase : VimMotionGroup { | ||||
|   override var lastFTCmd: TillCharacterMotionType = TillCharacterMotionType.LAST_SMALL_T | ||||
|   override var lastFTChar: Char = ' ' | ||||
|  | ||||
|   override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion { | ||||
|   override fun getVerticalMotionOffset(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bufferLines: Boolean): Motion { | ||||
|     val pos = caret.getVisualPosition() | ||||
|     if ((pos.line == 0 && count < 0) || (pos.line >= editor.getVisualLineCount() - 1 && count > 0)) { | ||||
|       return Motion.Error | ||||
|     } | ||||
|  | ||||
|     val intendedColumn = caret.vimLastColumn | ||||
|     val line = editor.normalizeVisualLine(pos.line + count) | ||||
|     val line = if (bufferLines) | ||||
|       // TODO Does not work with folds, but I don't use those. | ||||
|       editor.normalizeVisualLine(editor.bufferLineToVisualLine(editor.visualLineToBufferLine(pos.line) + count)) | ||||
|     else | ||||
|       editor.normalizeVisualLine(pos.line + count) | ||||
|  | ||||
|     if (intendedColumn == LAST_COLUMN) { | ||||
|       val normalisedColumn = editor.normalizeVisualColumn( | ||||
|   | ||||
| @@ -206,4 +206,17 @@ interface VimSearchGroup { | ||||
|    * Returns true if any text is selected in the visible editors, false otherwise. | ||||
|    */ | ||||
|   fun isSomeTextHighlighted(): Boolean | ||||
|  | ||||
|   /** | ||||
|    * 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, | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1425,8 +1425,7 @@ abstract class VimSearchGroupBase : VimSearchGroup { | ||||
|    * @param patternOffset   The pattern offset, e.g. `/{pattern}/{offset}` | ||||
|    * @param direction       The direction to search | ||||
|    */ | ||||
|   @TestOnly | ||||
|   fun setLastSearchState( | ||||
|   override fun setLastSearchState( | ||||
|     pattern: String, | ||||
|     patternOffset: String, | ||||
|     direction: Direction, | ||||
|   | ||||
| @@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.common | ||||
|  | ||||
| interface VimCopiedText { | ||||
|   val text: String | ||||
|   val transferableData: List<Any> | ||||
|  | ||||
|   // TODO Looks like sticky tape, I'm not sure that we need to modify already stored text | ||||
|   fun updateText(newText: String): VimCopiedText | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
|  | ||||
| package com.maddyhome.idea.vim.common | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.state.mode.Mode | ||||
| @@ -72,9 +71,9 @@ class VimListenersNotifier { | ||||
|     isReplaceCharListeners.forEach { it.isReplaceCharChanged(editor) } | ||||
|   } | ||||
|  | ||||
|   fun notifyYankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) { | ||||
|   fun notifyYankPerformed(editor: VimEditor, range: TextRange) { | ||||
|     if (!injector.enabler.isEnabled()) return // we remove all the listeners when turning the plugin off, but let's do it just in case | ||||
|     yankListeners.forEach { it.yankPerformed(caretToRange) } | ||||
|     yankListeners.forEach { it.yankPerformed(editor, range) } | ||||
|   } | ||||
|  | ||||
|   fun reset() { | ||||
|   | ||||
| @@ -8,8 +8,8 @@ | ||||
|  | ||||
| package com.maddyhome.idea.vim.common | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
|  | ||||
| interface VimYankListener { | ||||
|   fun yankPerformed(caretToRange: Map<ImmutableVimCaret, TextRange>) | ||||
|   fun yankPerformed(editor: VimEditor, range: TextRange) | ||||
| } | ||||
| @@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimCaretListener | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.VimMotionGroupBase | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.api.normalizeOffset | ||||
| import com.maddyhome.idea.vim.command.Argument | ||||
| @@ -226,7 +227,15 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) { | ||||
|       StrictMode.assert(caret.isPrimary, "Block selection mode must only operate on primary caret") | ||||
|     } | ||||
|  | ||||
|     val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset) | ||||
|     val normalisedOffset = prepareMoveToAbsoluteOffset(editor, cmd, offset).let { | ||||
|       if (offset.intendedColumn == VimMotionGroupBase.LAST_COLUMN) { | ||||
|         val softWrapStart = editor.getSoftWrapStartAtOffset(it) | ||||
|         if (softWrapStart != null) softWrapStart - 1 else it | ||||
|       } | ||||
|       else { | ||||
|         it | ||||
|       } | ||||
|     } | ||||
|     StrictMode.assert(normalisedOffset == offset.offset, "Adjusted offset should be normalised by action") | ||||
|  | ||||
|     // Set before moving, so it can be applied during move, especially important for LAST_COLUMN and visual block mode | ||||
|   | ||||
| @@ -272,8 +272,12 @@ class ToActionMappingInfo( | ||||
|  | ||||
|   override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { | ||||
|     LOG.debug("Executing 'ToAction' mapping...") | ||||
|     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 { | ||||
|     private val LOG = vimLogger<ToActionMappingInfo>() | ||||
|   | ||||
| @@ -8,11 +8,11 @@ | ||||
|  | ||||
| package com.maddyhome.idea.vim.put | ||||
|  | ||||
| import com.maddyhome.idea.vim.common.VimCopiedText | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
|  | ||||
| data class ProcessedTextData( | ||||
|   val registerChar: Char?, | ||||
|   val copiedText: VimCopiedText, | ||||
|   val text: String, | ||||
|   val typeInRegister: SelectionType, | ||||
|   val transferableData: List<Any>, | ||||
|   val registerChar: Char?, | ||||
| ) | ||||
|   | ||||
| @@ -9,9 +9,7 @@ | ||||
| package com.maddyhome.idea.vim.put | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.common.VimCopiedText | ||||
| import com.maddyhome.idea.vim.group.visual.VimSelection | ||||
| import com.maddyhome.idea.vim.register.Register | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
|  | ||||
| /** | ||||
| @@ -35,12 +33,9 @@ data class PutData( | ||||
|   ) | ||||
|  | ||||
|   data class TextData( | ||||
|     val registerChar: Char?, | ||||
|     val copiedText: VimCopiedText, | ||||
|     val rawText: String?, | ||||
|     val typeInRegister: SelectionType, | ||||
|   ) { | ||||
|     constructor(register: Register) : this(register.name, register.copiedText, register.type) | ||||
|  | ||||
|     val rawText = copiedText.text // TODO do not call it raw text... | ||||
|   } | ||||
|     val transferableData: List<Any>, | ||||
|     val registerChar: Char?, | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -141,7 +141,6 @@ abstract class VimPutBase : VimPut { | ||||
|  | ||||
|     if (data.visualSelection?.typeInEditor?.isLine == true && data.textData.typeInRegister.isChar) text += "\n" | ||||
|  | ||||
|     // TODO: shouldn't it be adjusted when we are storing the text? | ||||
|     if (data.textData.typeInRegister.isLine && text.isNotEmpty() && text.last() != '\n') text += '\n' | ||||
|  | ||||
|     if (data.textData.typeInRegister.isChar && text.lastOrNull() == '\n' && data.visualSelection?.typeInEditor?.isLine == false) { | ||||
| @@ -150,9 +149,10 @@ abstract class VimPutBase : VimPut { | ||||
|     } | ||||
|  | ||||
|     return ProcessedTextData( | ||||
|       data.textData.registerChar, | ||||
|       data.textData.copiedText.updateText(text), | ||||
|       text, | ||||
|       data.textData.typeInRegister, | ||||
|       data.textData.transferableData, | ||||
|       data.textData.registerChar, | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| @@ -512,7 +512,7 @@ abstract class VimPutBase : VimPut { | ||||
|     startOffsets.forEach { startOffset -> | ||||
|       val selectionType = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE | ||||
|       val (endOffset, updatedCaret) = putTextInternal( | ||||
|         editor, updated, context, text.copiedText.text, text.typeInRegister, selectionType, | ||||
|         editor, updated, context, text.text, text.typeInRegister, selectionType, | ||||
|         startOffset, data.count, data.indent, data.caretAfterInsertedText, | ||||
|       ) | ||||
|       updated = updatedCaret | ||||
|   | ||||
| @@ -21,8 +21,8 @@ import com.maddyhome.idea.vim.api.VimFoldRegion | ||||
| import com.maddyhome.idea.vim.api.VimIndentConfig | ||||
| import com.maddyhome.idea.vim.api.VimScrollingModel | ||||
| 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.VimVisualPosition | ||||
| import com.maddyhome.idea.vim.common.LiveRange | ||||
| import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.common.VimEditorReplaceMask | ||||
| @@ -621,6 +621,12 @@ class VimRegex(pattern: String) { | ||||
|  | ||||
|     override fun nativeCarets(): List<VimCaret> = emptyList() | ||||
|      | ||||
|     override val isFirstCaret: Boolean | ||||
|       get() = false | ||||
|  | ||||
|     override val isReversingCarets: Boolean | ||||
|       get() = false | ||||
|      | ||||
|     override fun forEachCaret(action: (VimCaret) -> Unit) {} | ||||
|  | ||||
|     override fun forEachNativeCaret(action: (VimCaret) -> Unit, reverse: Boolean) {} | ||||
| @@ -782,6 +788,10 @@ class VimRegex(pattern: String) { | ||||
|       return null | ||||
|     } | ||||
|  | ||||
|     override fun getSoftWrapStartAtOffset(offset: Int): Int? { | ||||
|       return null | ||||
|     } | ||||
|  | ||||
|     override fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T? { | ||||
|       return null | ||||
|     } | ||||
|   | ||||
| @@ -8,40 +8,86 @@ | ||||
| package com.maddyhome.idea.vim.register | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.common.VimCopiedText | ||||
| import com.maddyhome.idea.vim.helper.EngineStringHelper | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
| import org.jetbrains.annotations.NonNls | ||||
| import java.awt.event.KeyEvent | ||||
| import javax.swing.KeyStroke | ||||
|  | ||||
| // TODO should we prefer keys over text, as they are more informative? | ||||
| // TODO e.g.  could be both <Esc> and <C-[> after trying to restore original keys | ||||
| data class Register( | ||||
|   val name: Char, | ||||
|   val keys: List<KeyStroke>, | ||||
|   val type: SelectionType, | ||||
|   val copiedText: VimCopiedText, | ||||
| class Register { | ||||
|   var name: Char | ||||
|   val type: SelectionType | ||||
|   val keys: MutableList<KeyStroke> | ||||
|   val transferableData: MutableList<out Any> | ||||
|   val rawText: String? | ||||
|  | ||||
|   constructor(name: Char, type: SelectionType, keys: MutableList<KeyStroke>) { | ||||
|     this.name = name | ||||
|     this.type = type | ||||
|     this.keys = keys | ||||
|     this.transferableData = mutableListOf() | ||||
|     this.rawText = text | ||||
|   } | ||||
|  | ||||
|   constructor( | ||||
|     name: Char, | ||||
|     type: SelectionType, | ||||
|     text: String, | ||||
|     transferableData: MutableList<out Any>, | ||||
|   ) { | ||||
|   val text = copiedText.text | ||||
|   val printableString: String = | ||||
|     EngineStringHelper.toPrintableCharacters(keys) // should be the same as [text], but we can't render control notation properly | ||||
|     this.name = name | ||||
|     this.type = type | ||||
|     this.keys = injector.parser.stringToKeys(text).toMutableList() | ||||
|     this.transferableData = transferableData | ||||
|     this.rawText = text | ||||
|   } | ||||
|  | ||||
|   constructor( | ||||
|     name: Char, | ||||
|     type: SelectionType, | ||||
|     text: String, | ||||
|     transferableData: MutableList<out Any>, | ||||
|     rawText: String, | ||||
|   ) { | ||||
|     this.name = name | ||||
|     this.type = type | ||||
|     this.keys = injector.parser.stringToKeys(text).toMutableList() | ||||
|     this.transferableData = transferableData | ||||
|     this.rawText = rawText | ||||
|   } | ||||
|  | ||||
|   constructor(name: Char, type: SelectionType, keys: List<KeyStroke>) : this( | ||||
|     name, | ||||
|     keys, | ||||
|     type, | ||||
|     injector.clipboardManager.dumbCopiedText(injector.parser.toPrintableString(keys)) | ||||
|   ) | ||||
|   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() | ||||
|     } | ||||
|  | ||||
|   constructor(name: Char, copiedText: VimCopiedText, type: SelectionType) : this( | ||||
|     name, | ||||
|     injector.parser.stringToKeys(copiedText.text), | ||||
|     type, | ||||
|     copiedText | ||||
|   ) | ||||
|   val printableString: String | ||||
|     get() = EngineStringHelper.toPrintableCharacters(keys) // should be the same as [text], but we can't render control notation properly | ||||
|    | ||||
|   override fun toString(): String = "@$name = $printableString" | ||||
|   /** | ||||
|    * Append the supplied text to any existing text. | ||||
|    */ | ||||
|   fun addTextAndResetTransferableData(text: String) { | ||||
|     addKeys(injector.parser.stringToKeys(text)) | ||||
|     transferableData.clear() | ||||
|   } | ||||
|    | ||||
|   fun prependTextAndResetTransferableData(text: String) { | ||||
|     this.keys.addAll(0, injector.parser.stringToKeys(text)) | ||||
|     transferableData.clear() | ||||
|   } | ||||
|  | ||||
|   fun addKeys(keys: List<KeyStroke>) { | ||||
|     this.keys.addAll(keys) | ||||
|   } | ||||
|  | ||||
|   object KeySorter : Comparator<Register> { | ||||
|     @NonNls | ||||
| @@ -52,41 +98,3 @@ data class Register( | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Imagine you yanked two lines and have the following content in your register a - foo\nbar\n (register type is line-wise) | ||||
|  * Now, there are three different ways to append content, each with a different outcome: | ||||
|  * - If you append a macro qAbazq, you'll get foo\nbarbaz\n in register `a` and it stays line-wise | ||||
|  * - If you use Vim script and execute let @A = "baz", the result will be foo\nbar\nbaz and the register becomes character-wise | ||||
|  * - If you copy "baz" to register A, it becomes foo\nbar\nbaz\n and stays line-wise | ||||
|  * | ||||
|  * At the moment, we will stick to the third option to not overcomplicate the plugin | ||||
|  * (until there is a user who notices the difference) | ||||
|  */ | ||||
| fun Register.addText(text: String): Register { | ||||
|   return when (this.type) { | ||||
|     SelectionType.CHARACTER_WISE -> { | ||||
|       Register( | ||||
|         this.name, | ||||
|         injector.clipboardManager.dumbCopiedText(this.text + text), | ||||
|         SelectionType.CHARACTER_WISE | ||||
|       ) // todo it's empty for historical reasons, but should we really clear transferable data? | ||||
|     } | ||||
|  | ||||
|     SelectionType.LINE_WISE -> { | ||||
|       Register( | ||||
|         this.name, | ||||
|         injector.clipboardManager.dumbCopiedText(this.text + text + (if (text.endsWith('\n')) "" else "\n")), | ||||
|         SelectionType.LINE_WISE | ||||
|       ) // todo it's empty for historical reasons, but should we really clear transferable data? | ||||
|     } | ||||
|  | ||||
|     SelectionType.BLOCK_WISE -> { | ||||
|       Register( | ||||
|         this.name, | ||||
|         injector.clipboardManager.dumbCopiedText(this.text + "\n" + text), | ||||
|         SelectionType.BLOCK_WISE | ||||
|       ) // todo it's empty for historical reasons, but should we really clear transferable data? | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -17,6 +17,13 @@ import javax.swing.KeyStroke | ||||
|  | ||||
| interface VimRegisterGroup { | ||||
|  | ||||
|   /** | ||||
|    * Get the last register selected by the user | ||||
|    * | ||||
|    * @return The register, null if no such register | ||||
|    */ | ||||
|   @Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getLastRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext)") | ||||
|   val lastRegister: Register? | ||||
|   var lastRegisterChar: Char | ||||
|   val currentRegister: Char | ||||
|  | ||||
| @@ -32,7 +39,6 @@ interface VimRegisterGroup { | ||||
|   val isRegisterSpecifiedExplicitly: Boolean | ||||
|   val defaultRegister: Char | ||||
|  | ||||
|   fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register? | ||||
|   fun isValid(reg: Char): Boolean | ||||
|   fun selectRegister(reg: Char): Boolean | ||||
|   fun resetRegister() | ||||
| @@ -41,6 +47,7 @@ interface VimRegisterGroup { | ||||
|   fun isRegisterWritable(): Boolean | ||||
|   fun isRegisterWritable(reg: Char): Boolean | ||||
|  | ||||
|   /** Store text into the last register. */ | ||||
|   fun storeText( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
| @@ -48,18 +55,20 @@ interface VimRegisterGroup { | ||||
|     range: TextRange, | ||||
|     type: SelectionType, | ||||
|     isDelete: Boolean, | ||||
|     forceAppend: Boolean = false, | ||||
|     prependInsteadOfAppend: Boolean = false | ||||
|   ): Boolean | ||||
|  | ||||
|   /** | ||||
|    * Stores text to any writable register (used for the let command) | ||||
|    */ | ||||
|   fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean | ||||
|  | ||||
|   /** | ||||
|    * Stores text to any writable register (used for multicaret tests) | ||||
|    */ | ||||
|   @TestOnly | ||||
|   fun storeText( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     register: Char, | ||||
|     text: String, | ||||
|     selectionType: SelectionType, | ||||
|   ): Boolean | ||||
|   fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType): Boolean | ||||
|  | ||||
|   /** | ||||
|    * Stores text, character wise, in the given special register | ||||
| @@ -75,7 +84,6 @@ interface VimRegisterGroup { | ||||
|    * preferable to yank from the fixture editor. | ||||
|    */ | ||||
|   fun storeTextSpecial(register: Char, text: String): Boolean | ||||
|  | ||||
|   @Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext, char)") | ||||
|   fun getRegister(r: Char): Register? | ||||
|   fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? | ||||
|   | ||||
| @@ -17,7 +17,6 @@ import com.maddyhome.idea.vim.api.globalOptions | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.Argument | ||||
| import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.common.VimCopiedText | ||||
| import com.maddyhome.idea.vim.diagnostic.VimLogger | ||||
| import com.maddyhome.idea.vim.diagnostic.debug | ||||
| import com.maddyhome.idea.vim.diagnostic.vimLogger | ||||
| @@ -75,9 +74,13 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|   override val defaultRegister: Char | ||||
|     get() = defaultRegisterChar | ||||
|  | ||||
|   override fun getLastRegister(editor: VimEditor, context: ExecutionContext): Register? { | ||||
|     return getRegister(editor, context, lastRegisterChar) | ||||
|   } | ||||
|   /** | ||||
|    * Get the last register selected by the user | ||||
|    * | ||||
|    * @return The register, null if no such register | ||||
|    */ | ||||
|   override val lastRegister: Register? | ||||
|     get() = getRegister(lastRegisterChar) | ||||
|  | ||||
|   private val onClipboardChanged: () -> Unit = { | ||||
|     val clipboardOptionValue = injector.globalOptions().clipboard | ||||
| @@ -113,13 +116,20 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     return if (isValid(reg)) { | ||||
|       isRegisterSpecifiedExplicitly = true | ||||
|       lastRegisterChar = reg | ||||
|       logger.debug { "register selected: $lastRegisterChar" } | ||||
|       logger.debug { "register selected: $lastRegister" } | ||||
|  | ||||
|       true | ||||
|     } else { | ||||
|       false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun getRegister(r: Char): Register? { | ||||
|     val dummyEditor = injector.fallbackWindow | ||||
|     val dummyContext = injector.executionContextManager.getEditorExecutionContext(dummyEditor) | ||||
|     return getRegister(dummyEditor, dummyContext, r) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Reset the selected register back to the default register. | ||||
|    */ | ||||
| @@ -175,6 +185,8 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     type: SelectionType, | ||||
|     register: Char, | ||||
|     isDelete: Boolean, | ||||
|     forceAppend: Boolean, | ||||
|     prependInsteadOfAppend: Boolean, | ||||
|   ): Boolean { | ||||
|     // Null register doesn't get saved, but acts like it was | ||||
|     if (lastRegisterChar == BLACK_HOLE_REGISTER) return true | ||||
| @@ -193,50 +205,62 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|       end = t | ||||
|     } | ||||
|  | ||||
|     val copiedText = | ||||
|       if (start != -1) { // FIXME: so, we had invalid ranges all the time?.. I've never handled such cases | ||||
|         injector.clipboardManager.collectCopiedText(editor, context, range, text) | ||||
|       } else { | ||||
|         injector.clipboardManager.dumbCopiedText(text) | ||||
|       } | ||||
|     logger.debug { "Copy to '$lastRegisterChar' with copied text: $copiedText" } | ||||
|     // If this is an uppercase register, we need to append the text to the corresponding lowercase register | ||||
|     if (Character.isUpperCase(register)) { | ||||
|     val transferableData: List<Any> = | ||||
|       if (start != -1) injector.clipboardManager.getTransferableData(editor, range) else ArrayList() | ||||
|     var processedText = | ||||
|       if (start != -1) injector.clipboardManager.preprocessText(editor, range, text, transferableData) else text | ||||
|     logger.debug { | ||||
|       val transferableClasses = transferableData.joinToString(",") { it.javaClass.name } | ||||
|       "Copy to '$lastRegister' with transferable data: $transferableClasses" | ||||
|     } | ||||
|     if (Character.isUpperCase(register) || forceAppend) { | ||||
|       if (forceAppend && type == SelectionType.CHARACTER_WISE) { | ||||
|         processedText = if (prependInsteadOfAppend) | ||||
|           processedText + '\n' | ||||
|         else | ||||
|           '\n' + processedText | ||||
|       } | ||||
|       val lreg = Character.toLowerCase(register) | ||||
|       val r = myRegisters[lreg] | ||||
|       // Append the text if the lowercase register existed | ||||
|       if (r != null) { | ||||
|         myRegisters[lreg] = r.addText(copiedText.text) | ||||
|         if (prependInsteadOfAppend) { | ||||
|           r.prependTextAndResetTransferableData(processedText) | ||||
|         } | ||||
|         else { | ||||
|           r.addTextAndResetTransferableData(processedText) | ||||
|         } | ||||
|       } else { | ||||
|         myRegisters[lreg] = Register(lreg, copiedText, type) | ||||
|         logger.debug { "register '$register' contains: \"$copiedText\"" } | ||||
|         myRegisters[lreg] = Register(lreg, type, processedText, ArrayList(transferableData)) | ||||
|         logger.debug { "register '$register' contains: \"$processedText\"" } | ||||
|       } // Set the text if the lowercase register didn't exist yet | ||||
|     } else { | ||||
|       myRegisters[register] = Register(register, copiedText, type) | ||||
|       logger.debug { "register '$register' contains: \"$copiedText\"" } | ||||
|       myRegisters[register] = Register(register, type, processedText, ArrayList(transferableData)) | ||||
|       logger.debug { "register '$register' contains: \"$processedText\"" } | ||||
|     } // Put the text in the specified register | ||||
|  | ||||
|     if (register == CLIPBOARD_REGISTER) { | ||||
|       injector.clipboardManager.setClipboardContent(editor, context, copiedText) | ||||
|       injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData)) | ||||
|       if (!isRegisterSpecifiedExplicitly && !isDelete && isPrimaryRegisterSupported() && OptionConstants.clipboard_unnamedplus in injector.globalOptions().clipboard) { | ||||
|         injector.clipboardManager.setPrimaryContent(editor, context, copiedText) | ||||
|         injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData)) | ||||
|       } | ||||
|     } | ||||
|     if (register == PRIMARY_REGISTER) { | ||||
|       if (isPrimaryRegisterSupported()) { | ||||
|         injector.clipboardManager.setPrimaryContent(editor, context, copiedText) | ||||
|         injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData)) | ||||
|         if (!isRegisterSpecifiedExplicitly && !isDelete && OptionConstants.clipboard_unnamed in injector.globalOptions().clipboard) { | ||||
|           injector.clipboardManager.setClipboardContent(editor, context, copiedText) | ||||
|           injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData)) | ||||
|         } | ||||
|       } else { | ||||
|         injector.clipboardManager.setClipboardContent(editor, context, copiedText) | ||||
|         injector.clipboardManager.setClipboardText(processedText, text, ArrayList(transferableData)) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Also add it to the unnamed register if the default wasn't specified | ||||
|     if (register != UNNAMED_REGISTER && ".:/".indexOf(register) == -1) { | ||||
|       myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, copiedText, type) | ||||
|       logger.debug { "register '$UNNAMED_REGISTER' contains: \"$copiedText\"" } | ||||
|       myRegisters[UNNAMED_REGISTER] = Register(UNNAMED_REGISTER, type, processedText, ArrayList(transferableData)) | ||||
|       logger.debug { "register '$UNNAMED_REGISTER' contains: \"$processedText\"" } | ||||
|     } | ||||
|  | ||||
|     if (isDelete) { | ||||
| @@ -256,22 +280,22 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|         while (d >= '1') { | ||||
|           val t = myRegisters[d] | ||||
|           if (t != null) { | ||||
|             val incName = (d.code + 1).toChar() | ||||
|             myRegisters[incName] = Register(incName, t.copiedText, t.type) | ||||
|             t.name = (d.code + 1).toChar() | ||||
|             myRegisters[(d.code + 1).toChar()] = t | ||||
|           } | ||||
|           d-- | ||||
|         } | ||||
|         myRegisters['1'] = Register('1', copiedText, type) | ||||
|         myRegisters['1'] = Register('1', type, processedText, ArrayList(transferableData)) | ||||
|       } | ||||
|  | ||||
|       // Deletes smaller than one line and without specified register go the the "-" register | ||||
|       if (smallInlineDeletion && register == defaultRegister) { | ||||
|         myRegisters[SMALL_DELETION_REGISTER] = | ||||
|           Register(SMALL_DELETION_REGISTER, copiedText, type) | ||||
|           Register(SMALL_DELETION_REGISTER, type, processedText, ArrayList(transferableData)) | ||||
|       } | ||||
|     } else if (register == defaultRegister) { | ||||
|       myRegisters['0'] = Register('0', copiedText, type) | ||||
|       logger.debug { "register '0' contains: \"$copiedText\"" } | ||||
|       myRegisters['0'] = Register('0', type, processedText, ArrayList(transferableData)) | ||||
|       logger.debug { "register '0' contains: \"$processedText\"" } | ||||
|     } // Yanks also go to register 0 if the default register was used | ||||
|     return true | ||||
|   } | ||||
| @@ -292,10 +316,12 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     range: TextRange, | ||||
|     type: SelectionType, | ||||
|     isDelete: Boolean, | ||||
|     forceAppend: Boolean, | ||||
|     prependInsteadOfAppend: Boolean | ||||
|   ): Boolean { | ||||
|     if (isRegisterWritable()) { | ||||
|       val text = preprocessTextBeforeStoring(editor.getText(range), type) | ||||
|       return storeTextInternal(editor, context, range, text, type, lastRegisterChar, isDelete) | ||||
|       return storeTextInternal(editor, context, range, text, type, lastRegisterChar, isDelete, forceAppend, prependInsteadOfAppend) | ||||
|     } | ||||
|  | ||||
|     return false | ||||
| @@ -330,54 +356,36 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     if (READONLY_REGISTERS.indexOf(register) == -1 && register != LAST_SEARCH_REGISTER && register != UNNAMED_REGISTER) { | ||||
|       return false | ||||
|     } | ||||
|     myRegisters[register] = Register( | ||||
|       register, | ||||
|       injector.clipboardManager.dumbCopiedText(text), | ||||
|     myRegisters[register] = Register(register, | ||||
|       SelectionType.CHARACTER_WISE | ||||
|     ) // TODO why transferable data is not collected? | ||||
|     , text, ArrayList()) | ||||
|     logger.debug { "register '$register' contains: \"$text\"" } | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   @Deprecated("Please use com.maddyhome.idea.vim.register.VimRegisterGroup#getRegister(com.maddyhome.idea.vim.api.VimEditor, com.maddyhome.idea.vim.api.ExecutionContext, char)") | ||||
|   override fun getRegister(r: Char): Register? { | ||||
|     val dummyEditor = injector.fallbackWindow | ||||
|     val dummyContext = injector.executionContextManager.getEditorExecutionContext(dummyEditor) | ||||
|     return getRegister(dummyEditor, dummyContext, r) | ||||
|   } | ||||
|  | ||||
|   override fun storeText( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     register: Char, | ||||
|     text: String, | ||||
|     selectionType: SelectionType, | ||||
|   ): Boolean { | ||||
|     if (!WRITABLE_REGISTERS.contains(register)) { | ||||
|       return false | ||||
|     } | ||||
|     logger.debug { "register '$register' contains: \"$text\"" } | ||||
|     val oldRegister = getRegister(editor, context, register.lowercaseChar()) | ||||
|     val newRegister = if (register.isUpperCase() && oldRegister != null) { | ||||
|       oldRegister.addText(text) | ||||
|     } else { | ||||
|       Register( | ||||
|         register, | ||||
|         injector.clipboardManager.dumbCopiedText(text), | ||||
|         selectionType | ||||
|       ) // FIXME why don't we collect transferable data? | ||||
|     } | ||||
|     saveRegister(editor, context, register, newRegister) | ||||
|     if (register == '/') { | ||||
|       injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register | ||||
|     } | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String): Boolean { | ||||
|     return storeText(editor, context, register, text, SelectionType.CHARACTER_WISE) | ||||
|   } | ||||
|    | ||||
|   override fun storeText(editor: VimEditor, context: ExecutionContext, register: Char, text: String, selectionType: SelectionType, | ||||
|   ): Boolean { | ||||
|     if (!WRITABLE_REGISTERS.contains(register)) { | ||||
|       return false | ||||
|     } | ||||
|     logger.debug { "register '$register' contains: \"$text\"" } | ||||
|     val textToStore = if (register.isUpperCase()) { | ||||
|       (getRegister(register.lowercaseChar())?.rawText ?: "") + text | ||||
|     } else { | ||||
|       text | ||||
|     } | ||||
|     val reg = Register(register, selectionType, textToStore, ArrayList()) | ||||
|     saveRegister(editor, context, register, reg) | ||||
|     if (register == '/') { | ||||
|       injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register | ||||
|     } | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   private fun guessSelectionType(text: String): SelectionType { | ||||
|     return if (text.endsWith("\n")) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE | ||||
|   } | ||||
| @@ -389,10 +397,10 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|    * @param r - the register character corresponding to either the primary selection (*) or clipboard selection (+) | ||||
|    * @return the content of the selection, if available, otherwise null | ||||
|    */ | ||||
|   private fun refreshClipboardRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? { | ||||
|   private fun refreshClipboardRegister(r: Char): Register? { | ||||
|     return when (r) { | ||||
|       PRIMARY_REGISTER -> refreshPrimaryRegister(editor, context) | ||||
|       CLIPBOARD_REGISTER -> refreshClipboardRegister(editor, context) | ||||
|       PRIMARY_REGISTER -> refreshPrimaryRegister() | ||||
|       CLIPBOARD_REGISTER -> refreshClipboardRegister() | ||||
|       else -> throw RuntimeException("Clipboard register expected, got $r") | ||||
|     } | ||||
|   } | ||||
| @@ -401,56 +409,60 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     return System.getenv("DISPLAY") != null && injector.systemInfoService.isXWindow | ||||
|   } | ||||
|  | ||||
|   private fun setSystemPrimaryRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) { | ||||
|     logger.trace("Setting text: $copiedText to primary selection...") | ||||
|   private fun setSystemPrimaryRegisterText(text: String, rawText: String, transferableData: List<Any>) { | ||||
|     logger.trace("Setting text: $text to primary selection...") | ||||
|     if (isPrimaryRegisterSupported()) { | ||||
|       try { | ||||
|         injector.clipboardManager.setPrimaryContent(editor, context, copiedText) | ||||
|         injector.clipboardManager.setClipboardText(text, rawText, transferableData) | ||||
|       } catch (e: Exception) { | ||||
|         logger.warn("False positive X11 primary selection support") | ||||
|         logger.trace("Setting text to primary selection failed. Setting it to clipboard selection instead") | ||||
|         setSystemClipboardRegisterText(editor, context, copiedText) | ||||
|         setSystemClipboardRegisterText(text, rawText, transferableData) | ||||
|       } | ||||
|     } else { | ||||
|       logger.trace("X11 primary selection is not supporting. Setting clipboard selection instead") | ||||
|       setSystemClipboardRegisterText(editor, context, copiedText) | ||||
|       setSystemClipboardRegisterText(text, rawText, transferableData) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private fun setSystemClipboardRegisterText(editor: VimEditor, context: ExecutionContext, copiedText: VimCopiedText) { | ||||
|     injector.clipboardManager.setClipboardContent(editor, context, copiedText) | ||||
|   private fun setSystemClipboardRegisterText(text: String, rawText: String, transferableData: List<Any>) { | ||||
|     injector.clipboardManager.setClipboardText(text, rawText, transferableData) | ||||
|   } | ||||
|  | ||||
|   private fun refreshPrimaryRegister(editor: VimEditor, context: ExecutionContext): Register? { | ||||
|   private fun refreshPrimaryRegister(): Register? { | ||||
|     logger.trace("Syncing cached primary selection value..") | ||||
|     if (!isPrimaryRegisterSupported()) { | ||||
|       logger.trace("X11 primary selection is not supported. Syncing clipboard selection..") | ||||
|       return refreshClipboardRegister(editor, context) | ||||
|       return refreshClipboardRegister() | ||||
|     } | ||||
|     try { | ||||
|       val clipboardData = injector.clipboardManager.getPrimaryContent(editor, context) ?: return null | ||||
|       val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null | ||||
|       val currentRegister = myRegisters[PRIMARY_REGISTER] | ||||
|       if (currentRegister != null && clipboardData.text == currentRegister.text) { | ||||
|       val text = clipboardData.text | ||||
|       val transferableData = clipboardData.transferableData.toMutableList() | ||||
|       if (currentRegister != null && text == currentRegister.text) { | ||||
|         return currentRegister | ||||
|       } | ||||
|       return Register(PRIMARY_REGISTER, clipboardData, guessSelectionType(clipboardData.text)) | ||||
|       return transferableData?.let { Register(PRIMARY_REGISTER, guessSelectionType(text), text, it) } | ||||
|     } catch (e: Exception) { | ||||
|       logger.warn("False positive X11 primary selection support") | ||||
|       logger.trace("Syncing primary selection failed. Syncing clipboard selection instead") | ||||
|       return refreshClipboardRegister(editor, context) | ||||
|       return refreshClipboardRegister() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private fun refreshClipboardRegister(editor: VimEditor, context: ExecutionContext): Register? { | ||||
|   private fun refreshClipboardRegister(): Register? { | ||||
|     // for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage | ||||
|     val systemAwareClipboardRegister = if (isPrimaryRegisterSupported()) CLIPBOARD_REGISTER else PRIMARY_REGISTER | ||||
|  | ||||
|     val clipboardData = injector.clipboardManager.getClipboardContent(editor, context) ?: return null | ||||
|     val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null | ||||
|     val currentRegister = myRegisters[systemAwareClipboardRegister] | ||||
|     if (currentRegister != null && clipboardData.text == currentRegister.text) { | ||||
|     val text = clipboardData.text | ||||
|     val transferableData = clipboardData.transferableData.toMutableList() | ||||
|     if (currentRegister != null && text == currentRegister.text) { | ||||
|       return currentRegister | ||||
|     } | ||||
|     return Register(systemAwareClipboardRegister, clipboardData, guessSelectionType(clipboardData.text)) | ||||
|     return transferableData?.let { Register(systemAwareClipboardRegister, guessSelectionType(text), text, it) } | ||||
|   } | ||||
|  | ||||
|   override fun getRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? { | ||||
| @@ -460,36 +472,34 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|       myR = Character.toLowerCase(myR) | ||||
|     } | ||||
|     return if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) refreshClipboardRegister( | ||||
|       editor, | ||||
|       context, | ||||
|       myR | ||||
|     ) else myRegisters[myR] | ||||
|       myR) else myRegisters[myR] | ||||
|   } | ||||
|  | ||||
|   override fun getRegisters(editor: VimEditor, context: ExecutionContext): List<Register> { | ||||
|     val filteredRegisters = myRegisters.values.filterNot { CLIPBOARD_REGISTERS.contains(it.name) }.toMutableList() | ||||
|     val clipboardRegisters = CLIPBOARD_REGISTERS | ||||
|       .filterNot { it == CLIPBOARD_REGISTER && !isPrimaryRegisterSupported() } // for some reason non-X systems use PRIMARY_REGISTER as a clipboard storage | ||||
|       .mapNotNull { refreshClipboardRegister(editor, context, it) } | ||||
|       .mapNotNull { refreshClipboardRegister(it) } | ||||
|  | ||||
|     return (filteredRegisters + clipboardRegisters).sortedWith(Register.KeySorter) | ||||
|   } | ||||
|  | ||||
|   override fun saveRegister(editor: VimEditor, context: ExecutionContext, r: Char, register: Register) { | ||||
|     var myR = if (Character.isUpperCase(r)) Character.toLowerCase(r) else r | ||||
|     val text = register.text | ||||
|     val rawText = register.rawText | ||||
|  | ||||
|     if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0) { | ||||
|     if (CLIPBOARD_REGISTERS.indexOf(myR) >= 0 && text != null && rawText != null) { | ||||
|       when (myR) { | ||||
|         CLIPBOARD_REGISTER -> { | ||||
|           if (!isPrimaryRegisterSupported()) { | ||||
|             // it looks wrong, but for some reason non-X systems use the * register to store the clipboard content | ||||
|             myR = PRIMARY_REGISTER | ||||
|           } | ||||
|           setSystemClipboardRegisterText(editor, context, register.copiedText) | ||||
|           setSystemClipboardRegisterText(text, rawText, ArrayList(register.transferableData)) | ||||
|         } | ||||
|  | ||||
|         PRIMARY_REGISTER -> { | ||||
|           setSystemPrimaryRegisterText(editor, context, register.copiedText) | ||||
|           setSystemPrimaryRegisterText(text, rawText, ArrayList(register.transferableData)) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -507,7 +517,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|   } | ||||
|  | ||||
|   override fun getPlaybackRegister(editor: VimEditor, context: ExecutionContext, r: Char): Register? { | ||||
|     return if (PLAYBACK_REGISTERS.indexOf(r) != 0) getRegister(editor, context, r) else null | ||||
|     return if (PLAYBACK_REGISTERS.indexOf(r) != 0) getRegister(r) else null | ||||
|   } | ||||
|  | ||||
|   override fun recordText(text: String) { | ||||
| @@ -530,7 +540,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     if (register != null) { | ||||
|       var reg: Register? = null | ||||
|       if (Character.isUpperCase(register)) { | ||||
|         reg = getRegister(editor, context, register) | ||||
|         reg = getRegister(register) | ||||
|       } | ||||
|  | ||||
|       val myRecordList = recordList | ||||
| @@ -539,7 +549,7 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|           reg = Register(Character.toLowerCase(register), SelectionType.CHARACTER_WISE, myRecordList) | ||||
|           myRegisters[Character.toLowerCase(register)] = reg | ||||
|         } else { | ||||
|           myRegisters[reg.name.lowercaseChar()] = reg.addText(injector.parser.toPrintableString(myRecordList)) | ||||
|           reg.addKeys(myRecordList) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ package com.maddyhome.idea.vim.vimscript.model.commands | ||||
| import com.intellij.vim.annotations.ExCommand | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.getText | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.OperatorArguments | ||||
| import com.maddyhome.idea.vim.ex.ranges.Range | ||||
| @@ -37,7 +38,7 @@ data class CopyTextCommand(val range: Range, val modifier: CommandModifier, val | ||||
|     val carets = editor.sortedCarets() | ||||
|     for (caret in carets) { | ||||
|       val range = getLineRange(editor, caret).toTextRange(editor) | ||||
|       val copiedText = injector.clipboardManager.collectCopiedText(editor, context, range) | ||||
|       val text = editor.getText(range) | ||||
|  | ||||
|       // Copy is defined as: | ||||
|       // :[range]co[py] {address} | ||||
| @@ -46,7 +47,8 @@ data class CopyTextCommand(val range: Range, val modifier: CommandModifier, val | ||||
|       // the line _before_ the first line (i.e., copy to above the first line). | ||||
|       val address1 = getAddressFromArgument(editor) | ||||
|  | ||||
|       val textData = PutData.TextData(null, copiedText, SelectionType.LINE_WISE) | ||||
|       val transferableData = injector.clipboardManager.getTransferableData(editor, range) | ||||
|       val textData = PutData.TextData(text, SelectionType.LINE_WISE, transferableData, null) | ||||
|       var mutableCaret = caret | ||||
|       val putData = if (address1 == 0) { | ||||
|         // TODO: This should maintain current column location | ||||
|   | ||||
| @@ -90,7 +90,7 @@ data class MoveTextCommand(val range: Range, val modifier: CommandModifier, val | ||||
|     val selectionEndOffset = lastSelectionInfo.end?.let { editor.bufferPositionToOffset(it) } | ||||
|  | ||||
|     val text = editor.getText(sourceRange) | ||||
|     val textData = PutData.TextData(null, injector.clipboardManager.dumbCopiedText(text), SelectionType.LINE_WISE) | ||||
|     val textData = PutData.TextData(text, SelectionType.LINE_WISE, emptyList(), null) | ||||
|  | ||||
|     val dropNewLineInEnd = (targetLineAfterDeletion + linesMoved == editor.lineCount() - 1 && text.last() == '\n') || | ||||
|       (sourceLineRange.endLine == editor.lineCount() - 1) | ||||
|   | ||||
| @@ -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 textData = registerGroup.getRegister(editor, context, registerGroup.lastRegisterChar)?.let { | ||||
|       PutData.TextData(null, it.copiedText, SelectionType.LINE_WISE) | ||||
|       PutData.TextData( | ||||
|         it.text ?: injector.parser.toKeyNotation(it.keys), | ||||
|         SelectionType.LINE_WISE, | ||||
|         it.transferableData, | ||||
|         null, | ||||
|       ) | ||||
|     } | ||||
|     val putData = PutData( | ||||
|       textData, | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.OperatorArguments | ||||
| import com.maddyhome.idea.vim.ex.ranges.Range | ||||
| import com.maddyhome.idea.vim.helper.EngineStringHelper | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
| import com.maddyhome.idea.vim.vimscript.model.ExecutionResult | ||||
|  | ||||
| @@ -41,7 +42,8 @@ data class RegistersCommand(val range: Range, val modifier: CommandModifier, val | ||||
|           SelectionType.CHARACTER_WISE -> "c" | ||||
|           SelectionType.BLOCK_WISE -> "b" | ||||
|         } | ||||
|         "  $type  \"${reg.name}   ${reg.printableString.take(200)}" | ||||
|         val text = reg.rawText?.let { injector.parser.parseKeys(it) } ?: reg.keys | ||||
|         "  $type  \"${reg.name}   ${EngineStringHelper.toPrintableCharacters(text).take(200)}" | ||||
|       } | ||||
|  | ||||
|     injector.outputPanel.output(editor, context, regs) | ||||
|   | ||||
| @@ -31,8 +31,23 @@ interface VimYankGroup { | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Boolean | ||||
|  | ||||
|   /** | ||||
|    * This yanks count lines of text | ||||
|    * | ||||
|    * @param editor The editor to yank from | ||||
|    * @param count  The number of lines to yank | ||||
|    * @return true if able to yank the lines, false if not | ||||
|    */ | ||||
|   fun yankLine(editor: VimEditor, context: ExecutionContext, count: Int): Boolean | ||||
|  | ||||
|   /** | ||||
|    * This yanks a range of text | ||||
|    * | ||||
|    * @param editor The editor to yank from | ||||
|    * @param range  The range of text to yank | ||||
|    * @param type   The type of yank | ||||
|    * @return true if able to yank the range, false if not | ||||
|    */ | ||||
|   fun yankRange( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|   | ||||
| @@ -10,39 +10,59 @@ package com.maddyhome.idea.vim.yank | ||||
|  | ||||
| import com.maddyhome.idea.vim.action.motion.updown.MotionDownLess1FirstNonSpaceAction | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.ImmutableVimCaret | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.anyNonWhitespace | ||||
| import com.maddyhome.idea.vim.api.getLineEndForOffset | ||||
| import com.maddyhome.idea.vim.api.getLineStartForOffset | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.Argument | ||||
| import com.maddyhome.idea.vim.command.MotionType | ||||
| import com.maddyhome.idea.vim.command.OperatorArguments | ||||
| import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.handler.MotionActionHandler | ||||
| import com.maddyhome.idea.vim.state.mode.SelectionType | ||||
| import org.jetbrains.annotations.Contract | ||||
| import kotlin.math.min | ||||
|  | ||||
| open class YankGroupBase : VimYankGroup { | ||||
|   private fun yankRange( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     caretToRange: Map<ImmutableVimCaret, Pair<TextRange, SelectionType>>, | ||||
|     range: TextRange, | ||||
|     type: SelectionType, | ||||
|     startOffsets: Map<VimCaret, Int>?, | ||||
|   ): Boolean { | ||||
|     startOffsets?.forEach { (caret, offset) -> | ||||
|       caret.moveToOffset(offset) | ||||
|     } | ||||
|  | ||||
|     injector.listenersNotifier.notifyYankPerformed(caretToRange.mapValues { it.value.first }) | ||||
|  | ||||
|     var result = true | ||||
|     for ((caret, myRange) in caretToRange) { | ||||
|       result = caret.registerStorage.storeText(editor, context, myRange.first, myRange.second, false) && result | ||||
|     injector.listenersNotifier.notifyYankPerformed(editor, range) | ||||
|     return injector.registerGroup.storeText(editor, context, editor.primaryCaret(), range, type, false) | ||||
|   } | ||||
|     return result | ||||
|  | ||||
|   @Contract("_, _ -> new") | ||||
|   protected fun getTextRange(ranges: List<Pair<Int, Int>>, type: SelectionType): TextRange? { | ||||
|     if (ranges.isEmpty()) return null | ||||
|  | ||||
|     val size = ranges.size | ||||
|     val starts = IntArray(size) | ||||
|     val ends = IntArray(size) | ||||
|  | ||||
|     if (type == SelectionType.LINE_WISE) { | ||||
|       starts[size - 1] = ranges[size - 1].first | ||||
|       ends[size - 1] = ranges[size - 1].second | ||||
|       for (i in 0 until size - 1) { | ||||
|         val range = ranges[i] | ||||
|         starts[i] = range.first | ||||
|         ends[i] = range.second - 1 | ||||
|       } | ||||
|     } else { | ||||
|       for (i in 0 until size) { | ||||
|         val range = ranges[i] | ||||
|         starts[i] = range.first | ||||
|         ends[i] = range.second | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return TextRange(starts, ends) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -55,11 +75,12 @@ open class YankGroupBase : VimYankGroup { | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Boolean { | ||||
|     val motion = argument as? Argument.Motion ?: return false | ||||
|     val motionType = motion.getMotionType() | ||||
|  | ||||
|     val nativeCaretCount = editor.nativeCarets().size | ||||
|     if (nativeCaretCount <= 0) return false | ||||
|  | ||||
|     val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>(nativeCaretCount) | ||||
|     val ranges = ArrayList<Pair<Int, Int>>(nativeCaretCount) | ||||
|  | ||||
|     // This logic is from original vim | ||||
|     val startOffsets = | ||||
| @@ -69,39 +90,24 @@ open class YankGroupBase : VimYankGroup { | ||||
|         HashMap<VimCaret, Int>(nativeCaretCount) | ||||
|       } | ||||
|  | ||||
|  | ||||
|     for (caret in editor.nativeCarets()) { | ||||
|       var motionType = motion.getMotionType() | ||||
|       val motionRange = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) | ||||
|         ?: continue | ||||
|  | ||||
|       assert(motionRange.size() == 1) | ||||
|       ranges.add(motionRange.startOffset to motionRange.endOffset) | ||||
|       startOffsets?.put(caret, motionRange.normalize().startOffset) | ||||
|  | ||||
|       // Yank motion commands that are not linewise become linewise if all the following are true: | ||||
|       // 1) The range is across multiple lines | ||||
|       // 2) There is only whitespace before the start of the range | ||||
|       // 3) There is only whitespace after the end of the range | ||||
|       if (argument.motion is MotionActionHandler && argument.motion.motionType == MotionType.EXCLUSIVE) { | ||||
|         val start = editor.offsetToBufferPosition(motionRange.startOffset) | ||||
|         val end = editor.offsetToBufferPosition(motionRange.endOffset) | ||||
|         if (start.line != end.line | ||||
|           && !editor.anyNonWhitespace(motionRange.startOffset, -1) | ||||
|           && !editor.anyNonWhitespace(motionRange.endOffset, 1) | ||||
|         ) { | ||||
|           motionType = SelectionType.LINE_WISE | ||||
|         } | ||||
|     } | ||||
|  | ||||
|       caretToRange[caret] = TextRange(motionRange.startOffset, motionRange.endOffset) to motionType | ||||
|     } | ||||
|     val range = getTextRange(ranges, motionType) ?: return false | ||||
|  | ||||
|     if (caretToRange.isEmpty()) return false | ||||
|     if (range.size() == 0) return false | ||||
|  | ||||
|     return yankRange( | ||||
|       editor, | ||||
|       context, | ||||
|       caretToRange, | ||||
|       range, | ||||
|       motionType, | ||||
|       startOffsets, | ||||
|     ) | ||||
|   } | ||||
| @@ -115,18 +121,18 @@ open class YankGroupBase : VimYankGroup { | ||||
|    */ | ||||
|   override fun yankLine(editor: VimEditor, context: ExecutionContext, count: Int): Boolean { | ||||
|     val caretCount = editor.nativeCarets().size | ||||
|     val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>(caretCount) | ||||
|     val ranges = ArrayList<Pair<Int, Int>>(caretCount) | ||||
|     for (caret in editor.nativeCarets()) { | ||||
|       val start = injector.motion.moveCaretToCurrentLineStart(editor, caret) | ||||
|       val end = | ||||
|         min(injector.motion.moveCaretToRelativeLineEnd(editor, caret, count - 1, true) + 1, editor.fileSize().toInt()) | ||||
|       val end = min(injector.motion.moveCaretToRelativeLineEnd(editor, caret, count - 1, true) + 1, editor.fileSize().toInt()) | ||||
|  | ||||
|       if (end == -1) continue | ||||
|  | ||||
|       caretToRange[caret] = TextRange(start, end) to SelectionType.LINE_WISE | ||||
|       ranges.add(start to end) | ||||
|     } | ||||
|  | ||||
|     return yankRange(editor, context, caretToRange, null) | ||||
|     val range = getTextRange(ranges, SelectionType.LINE_WISE) ?: return false | ||||
|     return yankRange(editor, context, range, SelectionType.LINE_WISE, null) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -137,15 +143,8 @@ open class YankGroupBase : VimYankGroup { | ||||
|    * @param type   The type of yank | ||||
|    * @return true if able to yank the range, false if not | ||||
|    */ | ||||
|   override fun yankRange( | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     range: TextRange?, | ||||
|     type: SelectionType, | ||||
|     moveCursor: Boolean, | ||||
|   ): Boolean { | ||||
|   override fun yankRange(editor: VimEditor, context: ExecutionContext, range: TextRange?, type: SelectionType, moveCursor: Boolean): Boolean { | ||||
|     range ?: return false | ||||
|     val caretToRange = HashMap<ImmutableVimCaret, Pair<TextRange, SelectionType>>() | ||||
|  | ||||
|     if (type == SelectionType.LINE_WISE) { | ||||
|       for (i in 0 until range.size()) { | ||||
| @@ -165,19 +164,17 @@ open class YankGroupBase : VimYankGroup { | ||||
|     val startOffsets = HashMap<VimCaret, Int>(editor.nativeCarets().size) | ||||
|     if (type == SelectionType.BLOCK_WISE) { | ||||
|       startOffsets[editor.primaryCaret()] = range.normalize().startOffset | ||||
|       caretToRange[editor.primaryCaret()] = range to type | ||||
|     } else { | ||||
|       for ((i, caret) in editor.nativeCarets().withIndex()) { | ||||
|         val textRange = TextRange(rangeStartOffsets[i], rangeEndOffsets[i]) | ||||
|         startOffsets[caret] = textRange.normalize().startOffset | ||||
|         caretToRange[caret] = textRange to type | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return if (moveCursor) { | ||||
|       yankRange(editor, context, caretToRange, startOffsets) | ||||
|       yankRange(editor, context, range, type, startOffsets) | ||||
|     } else { | ||||
|       yankRange(editor, context, caretToRange, null) | ||||
|       yankRange(editor, context, range, type, null) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -417,7 +417,7 @@ | ||||
|     { | ||||
|         "keys": "<C-R>", | ||||
|         "class": "com.maddyhome.idea.vim.action.change.RedoAction", | ||||
|         "modes": "N" | ||||
|         "modes": "NX" | ||||
|     }, | ||||
|     { | ||||
|         "keys": "<C-R>", | ||||
| @@ -957,7 +957,7 @@ | ||||
|     { | ||||
|         "keys": "<Undo>", | ||||
|         "class": "com.maddyhome.idea.vim.action.change.UndoAction", | ||||
|         "modes": "N" | ||||
|         "modes": "NX" | ||||
|     }, | ||||
|     { | ||||
|         "keys": "<Up>", | ||||
| @@ -1176,8 +1176,8 @@ | ||||
|     }, | ||||
|     { | ||||
|         "keys": "U", | ||||
|         "class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseUpperVisualAction", | ||||
|         "modes": "X" | ||||
|         "class": "com.maddyhome.idea.vim.action.change.RedoAction", | ||||
|         "modes": "NX" | ||||
|     }, | ||||
|     { | ||||
|         "keys": "V", | ||||
| @@ -1972,12 +1972,7 @@ | ||||
|     { | ||||
|         "keys": "u", | ||||
|         "class": "com.maddyhome.idea.vim.action.change.UndoAction", | ||||
|         "modes": "N" | ||||
|     }, | ||||
|     { | ||||
|         "keys": "u", | ||||
|         "class": "com.maddyhome.idea.vim.action.change.change.ChangeCaseLowerVisualAction", | ||||
|         "modes": "X" | ||||
|         "modes": "NX" | ||||
|     }, | ||||
|     { | ||||
|         "keys": "v", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user