mirror of
				https://github.com/chylex/IntelliJ-IdeaVim.git
				synced 2025-10-31 11:17:13 +01:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			9dbe3c3363
			...
			surround-m
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d6076c719f | |||
|   | a3ca1b965b | ||
|   | dd20b480a7 | ||
|   | 38292e97af | ||
|   | 46ea752164 | ||
|   | 194b744361 | ||
| b50197f7ce | |||
|   | c00703d1d0 | ||
|   | 6e12377116 | ||
|   | b0c4391ad8 | ||
|   | f43ac2538a | ||
|   | 9eaf8b5d2d | ||
|   | e365d0b07c | ||
|   | 69c273c4a5 | ||
|   | f7950e7adb | ||
|   | 7c1ae9812e | ||
|   | 5c794ac40e | 
| @@ -55,6 +55,7 @@ usual beta standards. | ||||
| * [VIM-696](https://youtrack.jetbrains.com/issue/VIM-696/vim-selection-issue-after-undo) Fix selection after undo | ||||
| * [VIM-744](https://youtrack.jetbrains.com/issue/VIM-744/Use-undoredo-with-count-modifier) Add count to undo/redo | ||||
| * [VIM-1862](https://youtrack.jetbrains.com/issue/VIM-1862/Ex-commands-executed-in-keymaps-and-macros-are-added-to-the-command-history) Fix command history | ||||
| * [VIM-2227](https://youtrack.jetbrains.com/issue/VIM-2227) Wrong behavior when deleting / changing surround with invalid character | ||||
|  | ||||
| ### Merged PRs: | ||||
| * [468](https://github.com/JetBrains/ideavim/pull/468) by [Thomas Schouten](https://github.com/PHPirates): Implement UserDataHolder for EditorDataContext | ||||
| @@ -63,6 +64,7 @@ usual beta standards. | ||||
| * [493](https://github.com/JetBrains/ideavim/pull/493) by [Matt Ellis](https://github.com/citizenmatt): Improvements to Commentary extension | ||||
| * [494](https://github.com/JetBrains/ideavim/pull/494) by [Matt Ellis](https://github.com/citizenmatt): Cleanup pre-212 CaretVisualAttributes compatibility code | ||||
| * [504](https://github.com/JetBrains/ideavim/pull/504) by [Matt Ellis](https://github.com/citizenmatt): Minor bug fixes | ||||
| * [519](https://github.com/JetBrains/ideavim/pull/519) by [chylex](https://github.com/chylex): Fix(VIM-2227): Wrong behavior when deleting / changing surround with invalid character | ||||
|  | ||||
| ## 1.10.0, 2022-02-17 | ||||
|  | ||||
|   | ||||
							
								
								
									
										21
									
								
								qodana.yaml
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								qodana.yaml
									
									
									
									
									
								
							| @@ -1,6 +1,8 @@ | ||||
| version: 1.0 | ||||
| profile: | ||||
|   name: Qodana | ||||
| include: | ||||
|   - name: CheckDependencyLicenses | ||||
| exclude: | ||||
|   - name: MoveVariableDeclarationIntoWhen | ||||
|   - name: PluginXmlValidity | ||||
| @@ -9,12 +11,15 @@ exclude: | ||||
|   - name: UnusedReturnValue | ||||
|   - name: All | ||||
|     paths: | ||||
|       - build.gradle | ||||
|       - build.gradle.kts | ||||
|       - gradle/wrapper/gradle-wrapper.properties | ||||
|       - resources/icons/youtrack.svg | ||||
|       - src/com/maddyhome/idea/vim/ex/vimscript/VimScriptCommandHandler.java | ||||
|       - src/com/maddyhome/idea/vim/helper/SearchHelper.java | ||||
|       - src/com/maddyhome/idea/vim/regexp/RegExp.java | ||||
|       - test/org/jetbrains/plugins/ideavim/propertybased/samples/JavaText.kt | ||||
|       - test/org/jetbrains/plugins/ideavim/propertybased/samples/LoremText.kt | ||||
|       - test/org/jetbrains/plugins/ideavim/propertybased/samples/SimpleText.kt | ||||
|       - src/main/resources/icons/youtrack.svg | ||||
|       - src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java | ||||
|       - src/main/java/com/maddyhome/idea/vim/regexp/RegExp.kt | ||||
|       - src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/JavaText.kt | ||||
|       - src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/LoremText.kt | ||||
|       - src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/SimpleText.kt | ||||
|       - src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptListener.java | ||||
|       - src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptLexer.java | ||||
|       - src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptParser.java | ||||
|       - src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptVisitor.java | ||||
| @@ -146,7 +146,7 @@ rShift: GREATER+; | ||||
|  | ||||
| letCommands: | ||||
|     (WS | COLON)* range? (WS | COLON)* LET WS+ expr WS* | ||||
|         assignmentOperator =  (ASSIGN | PLUS_ASSIGN | MINUS_ASSIGN | STAR_ASSIGN | DIV_ASSIGN | MOD_ASSIGN | DOT_ASSIGN) | ||||
|         assignmentOperator | ||||
|         WS* expr WS* ((inline_comment NEW_LINE) | (NEW_LINE | BAR)+) | ||||
|     #Let1Command| | ||||
|  | ||||
| @@ -154,6 +154,21 @@ letCommands: | ||||
|     #Let2Command | ||||
| ; | ||||
|  | ||||
| assignmentOperator: | ||||
|     ASSIGN | plusAssign | minusAssign | startAssign | divAssign | modAssign | dotAssign; | ||||
| plusAssign: | ||||
|     PLUS ASSIGN; | ||||
| minusAssign: | ||||
|     MINUS ASSIGN; | ||||
| startAssign: | ||||
|     STAR ASSIGN; | ||||
| divAssign: | ||||
|     DIV ASSIGN; | ||||
| modAssign: | ||||
|     MOD ASSIGN; | ||||
| dotAssign: | ||||
|     DOT ASSIGN; | ||||
|  | ||||
| shortRange: | ||||
|     ((QUESTION (~QUESTION)* QUESTION?) | (DIV (~DIV)* DIV?)); | ||||
| range: | ||||
| @@ -778,12 +793,12 @@ IS_NOT_CS:              'isnot#'; | ||||
|  | ||||
| // Assignment operators | ||||
| ASSIGN:                 '='; | ||||
| PLUS_ASSIGN:            '+='; | ||||
| MINUS_ASSIGN:           '-='; | ||||
| STAR_ASSIGN:            '*='; | ||||
| DIV_ASSIGN:             '/='; | ||||
| MOD_ASSIGN:             '%='; | ||||
| DOT_ASSIGN:             '.='; | ||||
| //PLUS_ASSIGN:            '+='; | ||||
| //MINUS_ASSIGN:           '-='; | ||||
| //STAR_ASSIGN:            '*='; | ||||
| //DIV_ASSIGN:             '/='; | ||||
| //MOD_ASSIGN:             '%='; | ||||
| //DOT_ASSIGN:             '.='; | ||||
|  | ||||
| // Escaped chars | ||||
| ESCAPED_QUESTION:       '\\?'; | ||||
|   | ||||
| @@ -22,6 +22,7 @@ import com.intellij.openapi.application.runWriteAction | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.maddyhome.idea.vim.VimPlugin | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimChangeGroup | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.CommandState | ||||
| @@ -40,6 +41,7 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegister | ||||
| import com.maddyhome.idea.vim.extension.VimExtensionHandler | ||||
| import com.maddyhome.idea.vim.helper.EditorHelper | ||||
| import com.maddyhome.idea.vim.helper.mode | ||||
| 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 | ||||
| @@ -84,22 +86,20 @@ class VimSurroundExtension : VimExtension { | ||||
|     override val isRepeatable = true | ||||
|  | ||||
|     override fun execute(editor: VimEditor, context: ExecutionContext) { | ||||
|       setOperatorFunction(Operator()) | ||||
|       setOperatorFunction(Operator(supportsMultipleCursors = false)) // TODO | ||||
|       executeNormalWithoutMapping(injector.parser.parseKeys("g@"), editor.ij) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private class VSurroundHandler : VimExtensionHandler { | ||||
|     override fun execute(editor: VimEditor, context: ExecutionContext) { | ||||
|       val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart | ||||
|       // NB: Operator ignores SelectionType anyway | ||||
|       if (!Operator().apply(editor.ij, context.ij, SelectionType.CHARACTER_WISE)) { | ||||
|       if (!Operator(supportsMultipleCursors = true).apply(editor.ij, context.ij, SelectionType.CHARACTER_WISE)) { | ||||
|         return | ||||
|       } | ||||
|       runWriteAction { | ||||
|         // Leave visual mode | ||||
|         executeNormalWithoutMapping(injector.parser.parseKeys("<Esc>"), editor.ij) | ||||
|         editor.ij.caretModel.moveToOffset(selectionStart) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -120,23 +120,32 @@ class VimSurroundExtension : VimExtension { | ||||
|  | ||||
|     companion object { | ||||
|       fun change(editor: Editor, charFrom: Char, newSurround: Pair<String, String>?) { | ||||
|         editor.runWithEveryCaretAndRestore { changeAtCaret(editor, charFrom, newSurround) } | ||||
|       } | ||||
|        | ||||
|       fun changeAtCaret(editor: Editor, charFrom: Char, newSurround: Pair<String, String>?) { | ||||
|         // We take over the " register, so preserve it | ||||
|         val oldValue: List<KeyStroke>? = getRegister(REGISTER) | ||||
|         // Empty the " register | ||||
|         setRegister(REGISTER, null) | ||||
|         // Extract the inner value | ||||
|         perform("di" + pick(charFrom), editor) | ||||
|         val innerValue: MutableList<KeyStroke> = getRegister(REGISTER)?.toMutableList() ?: mutableListOf() | ||||
|         // Delete the surrounding | ||||
|         perform("da" + pick(charFrom), editor) | ||||
|         // Insert the surrounding characters and paste | ||||
|         if (newSurround != null) { | ||||
|           innerValue.addAll(0, injector.parser.parseKeys(newSurround.first)) | ||||
|           innerValue.addAll(injector.parser.parseKeys(newSurround.second)) | ||||
|         // If the surrounding characters were not found, the register will be empty | ||||
|         if (innerValue.isNotEmpty()) { | ||||
|           // Delete the surrounding | ||||
|           perform("da" + pick(charFrom), editor) | ||||
|           // Insert the surrounding characters and paste | ||||
|           if (newSurround != null) { | ||||
|             innerValue.addAll(0, injector.parser.parseKeys(newSurround.first)) | ||||
|             innerValue.addAll(injector.parser.parseKeys(newSurround.second)) | ||||
|           } | ||||
|           pasteSurround(innerValue, editor) | ||||
|           // Jump back to start | ||||
|           executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor) | ||||
|         } | ||||
|         pasteSurround(innerValue, editor) | ||||
|         // Restore the old value | ||||
|         setRegister(REGISTER, oldValue) | ||||
|         // Jump back to start | ||||
|         executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor) | ||||
|       } | ||||
|  | ||||
|       private fun perform(sequence: String, editor: Editor) { | ||||
| @@ -179,25 +188,43 @@ class VimSurroundExtension : VimExtension { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private class Operator : OperatorFunction { | ||||
|   private class Operator(private val supportsMultipleCursors: Boolean) : OperatorFunction { | ||||
|     override fun apply(editor: Editor, context: DataContext, selectionType: SelectionType): Boolean { | ||||
|       val c = getChar(editor) | ||||
|       if (c.code == 0) return true | ||||
|  | ||||
|       val pair = getOrInputPair(c, editor) ?: return false | ||||
|       // XXX: Will it work with line-wise or block-wise selections? | ||||
|       val range = getSurroundRange(editor) ?: return false | ||||
|  | ||||
|       runWriteAction { | ||||
|         val change = VimPlugin.getChange() | ||||
|         val leftSurround = pair.first | ||||
|         val primaryCaret = editor.caretModel.primaryCaret | ||||
|         change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, leftSurround) | ||||
|         change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + leftSurround.length, pair.second) | ||||
|         // Jump back to start | ||||
|         executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor) | ||||
|         if (supportsMultipleCursors) { | ||||
|           editor.runWithEveryCaretAndRestore { | ||||
|             applyOnce(editor, change, pair) | ||||
|           } | ||||
|         } | ||||
|         else { | ||||
|           applyOnce(editor, change, pair) | ||||
|           // Jump back to start | ||||
|           executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor) | ||||
|         } | ||||
|       } | ||||
|       return true | ||||
|     } | ||||
|      | ||||
|     private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: Pair<String, String>) { | ||||
|       // XXX: Will it work with line-wise or block-wise selections? | ||||
|       val range = getSurroundRange(editor) | ||||
|       if (range != null) { | ||||
|         val primaryCaret = editor.caretModel.primaryCaret | ||||
|         change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, pair.first) | ||||
|         change.insertText( | ||||
|           IjVimEditor(editor), | ||||
|           IjVimCaret(primaryCaret), | ||||
|           range.endOffset + pair.first.length, | ||||
|           pair.second | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     private fun getSurroundRange(editor: Editor): TextRange? = when (editor.mode) { | ||||
|       CommandState.Mode.COMMAND -> VimPlugin.getMark().getChangeMarks(editor.vim) | ||||
|   | ||||
| @@ -39,7 +39,6 @@ import com.intellij.psi.codeStyle.CodeStyleManager; | ||||
| import com.intellij.psi.util.PsiUtilBase; | ||||
| import com.intellij.util.containers.ContainerUtil; | ||||
| import com.maddyhome.idea.vim.EventFacade; | ||||
| import com.maddyhome.idea.vim.RegisterActions; | ||||
| import com.maddyhome.idea.vim.VimPlugin; | ||||
| import com.maddyhome.idea.vim.api.*; | ||||
| import com.maddyhome.idea.vim.command.*; | ||||
| @@ -123,10 +122,10 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|    * @param col    The column to indent to | ||||
|    */ | ||||
|   private void insertNewLineBelow(@NotNull VimEditor editor, @NotNull VimCaret caret, int col) { | ||||
|     if (((IjVimEditor) editor).getEditor().isOneLineMode()) return; | ||||
|     if (editor.isOneLineMode()) return; | ||||
|  | ||||
|     injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret)); | ||||
|     UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT); | ||||
|     caret.moveToOffset(injector.getMotion().moveCaretToLineEnd(editor, caret)); | ||||
|     editor.setVimChangeActionSwitchMode(CommandState.Mode.INSERT); | ||||
|     insertText(editor, caret, "\n" + IndentConfig.create(((IjVimEditor) editor).getEditor()).createIndentBySize(col)); | ||||
|   } | ||||
|  | ||||
| @@ -165,41 +164,6 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|   } | ||||
|  | ||||
|  | ||||
|   @Override | ||||
|   public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType(@NotNull VimEditor editor, | ||||
|                                                                         @NotNull VimCaret caret, | ||||
|                                                                         @NotNull ExecutionContext context, | ||||
|                                                                         final @NotNull Argument argument, | ||||
|                                                                         boolean isChange, | ||||
|                                                                         @NotNull OperatorArguments operatorArguments) { | ||||
|     final TextRange range = | ||||
|       injector.getMotion().getMotionRange(editor, caret, context, argument, operatorArguments); | ||||
|     if (range == null) return null; | ||||
|  | ||||
|     // Delete 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 | ||||
|     SelectionType type; | ||||
|     if (argument.getMotion().isLinewiseMotion()) { | ||||
|       type = SelectionType.LINE_WISE; | ||||
|     } | ||||
|     else { | ||||
|       type = SelectionType.CHARACTER_WISE; | ||||
|     } | ||||
|     final Command motion = argument.getMotion(); | ||||
|     if (!isChange && !motion.isLinewiseMotion()) { | ||||
|       VimLogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset()); | ||||
|       VimLogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset()); | ||||
|       if (start.getLine() != end.getLine()) { | ||||
|         if (!SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getStartOffset(), -1) && | ||||
|             !SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getEndOffset(), 1)) { | ||||
|           type = SelectionType.LINE_WISE; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return new Pair<>(range, type); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType2(@NotNull VimEditor editor, | ||||
| @@ -236,60 +200,6 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|     return new Pair<>(range, type); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete the range of text. | ||||
|    * | ||||
|    * @param editor   The editor to delete the text from | ||||
|    * @param caret    The caret to be moved after deletion | ||||
|    * @param range    The range to delete | ||||
|    * @param type     The type of deletion | ||||
|    * @param isChange Is from a change action | ||||
|    * @return true if able to delete the text, false if not | ||||
|    */ | ||||
|   @Override | ||||
|   public boolean deleteRange(@NotNull VimEditor editor, | ||||
|                              @NotNull VimCaret caret, | ||||
|                              @NotNull TextRange range, | ||||
|                              @Nullable SelectionType type, | ||||
|                              boolean isChange) { | ||||
|  | ||||
|     // Update the last column before we delete, or we might be retrieving the data for a line that no longer exists | ||||
|     UserDataManager.setVimLastColumn(((IjVimCaret) caret).getCaret(), InlayHelperKt.getInlayAwareVisualColumn(((IjVimCaret) caret).getCaret())); | ||||
|  | ||||
|     boolean removeLastNewLine = removeLastNewLine(editor, range, type); | ||||
|     final boolean res = deleteText(editor, range, type); | ||||
|     if (removeLastNewLine) { | ||||
|       int textLength = ((IjVimEditor) editor).getEditor().getDocument().getTextLength(); | ||||
|       ((IjVimEditor) editor).getEditor().getDocument().deleteString(textLength - 1, textLength); | ||||
|     } | ||||
|  | ||||
|     if (res) { | ||||
|       int pos = EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), range.getStartOffset(), isChange); | ||||
|       if (type == SelectionType.LINE_WISE) { | ||||
|         pos = VimPlugin.getMotion() | ||||
|           .moveCaretToLineWithStartOfLineOption(editor, editor.offsetToLogicalPosition(pos).getLine(), | ||||
|                                                 caret); | ||||
|       } | ||||
|       injector.getMotion().moveCaret(editor, caret, pos); | ||||
|     } | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   private boolean removeLastNewLine(@NotNull VimEditor editor, @NotNull TextRange range, @Nullable SelectionType type) { | ||||
|     int endOffset = range.getEndOffset(); | ||||
|     int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()); | ||||
|     if (endOffset > fileSize) { | ||||
|       if (injector.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.ideastrictmodeName, OptionConstants.ideastrictmodeName)) { | ||||
|         throw new IllegalStateException("Incorrect offset. File size: " + fileSize + ", offset: " + endOffset); | ||||
|       } | ||||
|       endOffset = fileSize; | ||||
|     } | ||||
|     return type == SelectionType.LINE_WISE && | ||||
|            range.getStartOffset() != 0 && | ||||
|            ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence().charAt(endOffset - 1) != '\n' && | ||||
|            endOffset == fileSize; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void insertLineAround(@NotNull VimEditor editor, @NotNull ExecutionContext context, int shift) { | ||||
|     com.maddyhome.idea.vim.newapi.ChangeGroupKt.insertLineAround(editor, context, shift); | ||||
| @@ -303,48 +213,6 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|     return com.maddyhome.idea.vim.newapi.ChangeGroupKt.deleteRange(editor, caret, range, type); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete count characters and then enter insert mode | ||||
|    * | ||||
|    * @param editor The editor to change | ||||
|    * @param caret  The caret to be moved | ||||
|    * @param count  The number of characters to change | ||||
|    * @return true if able to delete count characters, false if not | ||||
|    */ | ||||
|   @Override | ||||
|   public boolean changeCharacters(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) { | ||||
|     int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor()); | ||||
|     int col = ((IjVimCaret) caret).getCaret().getLogicalPosition().column; | ||||
|     if (col + count >= len) { | ||||
|       return changeEndOfLine(editor, caret, 1); | ||||
|     } | ||||
|  | ||||
|     boolean res = deleteCharacter(editor, caret, count, true); | ||||
|     if (res) { | ||||
|       UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT); | ||||
|     } | ||||
|  | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete from the cursor to the end of count - 1 lines down and enter insert mode | ||||
|    * | ||||
|    * @param editor The editor to change | ||||
|    * @param caret  The caret to perform action on | ||||
|    * @param count  The number of lines to change | ||||
|    * @return true if able to delete count lines, false if not | ||||
|    */ | ||||
|   @Override | ||||
|   public boolean changeEndOfLine(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) { | ||||
|     boolean res = deleteEndOfLine(editor, caret, count); | ||||
|     if (res) { | ||||
|       injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret)); | ||||
|       UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT); | ||||
|     } | ||||
|  | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete the text covered by the motion command argument and enter insert mode | ||||
| @@ -368,9 +236,9 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|     String id = motion.getAction().getId(); | ||||
|     boolean kludge = false; | ||||
|     boolean bigWord = id.equals(VIM_MOTION_BIG_WORD_RIGHT); | ||||
|     final CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence(); | ||||
|     final int offset = ((IjVimCaret) caret).getCaret().getOffset(); | ||||
|     int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()); | ||||
|     final CharSequence chars = editor.text(); | ||||
|     final int offset = caret.getOffset().getPoint(); | ||||
|     int fileSize = ((int)editor.fileSize()); | ||||
|     if (fileSize > 0 && offset < fileSize) { | ||||
|       final CharacterHelper.CharacterType charType = CharacterHelper.charType(chars.charAt(offset), bigWord); | ||||
|       if (charType != CharacterHelper.CharacterType.WHITESPACE) { | ||||
| @@ -379,24 +247,24 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|         if (wordMotions.contains(id) && lastWordChar && motion.getCount() == 1) { | ||||
|           final boolean res = deleteCharacter(editor, caret, 1, true); | ||||
|           if (res) { | ||||
|             UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT); | ||||
|             editor.setVimChangeActionSwitchMode(CommandState.Mode.INSERT); | ||||
|           } | ||||
|           return res; | ||||
|         } | ||||
|         switch (id) { | ||||
|           case VIM_MOTION_WORD_RIGHT: | ||||
|             kludge = true; | ||||
|             motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_WORD_END_RIGHT)); | ||||
|             motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT)); | ||||
|  | ||||
|             break; | ||||
|           case VIM_MOTION_BIG_WORD_RIGHT: | ||||
|             kludge = true; | ||||
|             motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT)); | ||||
|             motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT)); | ||||
|  | ||||
|             break; | ||||
|           case VIM_MOTION_CAMEL_RIGHT: | ||||
|             kludge = true; | ||||
|             motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_CAMEL_END_RIGHT)); | ||||
|             motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT)); | ||||
|  | ||||
|             break; | ||||
|         } | ||||
| @@ -405,8 +273,8 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|  | ||||
|     if (kludge) { | ||||
|       int cnt = operatorArguments.getCount1() * motion.getCount(); | ||||
|       int pos1 = SearchHelper.findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false); | ||||
|       int pos2 = SearchHelper.findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false); | ||||
|       int pos1 = injector.getSearchHelper().findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false); | ||||
|       int pos2 = injector.getSearchHelper().findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false); | ||||
|       if (logger.isDebugEnabled()) { | ||||
|         logger.debug("pos=" + offset); | ||||
|         logger.debug("pos1=" + pos1); | ||||
| @@ -427,11 +295,11 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.experimentalapiName, OptionConstants.experimentalapiName)) { | ||||
|     if (injector.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.experimentalapiName, OptionConstants.experimentalapiName)) { | ||||
|       Pair<TextRange, SelectionType> deleteRangeAndType = | ||||
|         getDeleteRangeAndType2(editor, caret, context, argument, true, operatorArguments.withCount0(count0)); | ||||
|       if (deleteRangeAndType == null) return false; | ||||
|       ChangeGroupKt.changeRange(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), deleteRangeAndType.getFirst(), deleteRangeAndType.getSecond(), ((IjExecutionContext) context).getContext()); | ||||
|       //ChangeGroupKt.changeRange(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), deleteRangeAndType.getFirst(), deleteRangeAndType.getSecond(), ((IjExecutionContext) context).getContext()); | ||||
|       return true; | ||||
|     } | ||||
|     else { | ||||
| @@ -442,23 +310,6 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Counts number of lines in the visual block. | ||||
|    * <p> | ||||
|    * The result includes empty and short lines which does not have explicit start position (caret). | ||||
|    * | ||||
|    * @param editor The editor the block was selected in | ||||
|    * @param range  The range corresponding to the selected block | ||||
|    * @return total number of lines | ||||
|    */ | ||||
|   public static int getLinesCountInVisualBlock(@NotNull VimEditor editor, @NotNull TextRange range) { | ||||
|     final int[] startOffsets = range.getStartOffsets(); | ||||
|     if (startOffsets.length == 0) return 0; | ||||
|     final VimLogicalPosition firstStart = editor.offsetToLogicalPosition(startOffsets[0]); | ||||
|     final VimLogicalPosition lastStart = editor.offsetToLogicalPosition(startOffsets[range.size() - 1]); | ||||
|     return lastStart.getLine() - firstStart.getLine() + 1; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Toggles the case of count characters | ||||
|    * | ||||
| @@ -484,7 +335,7 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|                              @NotNull TextRange range, | ||||
|                              boolean append, | ||||
|                              @NotNull OperatorArguments operatorArguments) { | ||||
|     final int lines = getLinesCountInVisualBlock(editor, range); | ||||
|     final int lines = VimChangeGroupBase.Companion.getLinesCountInVisualBlock(editor, range); | ||||
|     final VimLogicalPosition startPosition = editor.offsetToLogicalPosition(range.getStartOffset()); | ||||
|  | ||||
|     boolean visualBlockMode = operatorArguments.getMode() == CommandState.Mode.VISUAL && | ||||
| @@ -589,24 +440,24 @@ public class ChangeGroup extends VimChangeGroupBase { | ||||
|     int col = 0; | ||||
|     int lines = 0; | ||||
|     if (type == SelectionType.BLOCK_WISE) { | ||||
|       lines = getLinesCountInVisualBlock(editor, range); | ||||
|       lines = VimChangeGroupBase.Companion.getLinesCountInVisualBlock(editor, range); | ||||
|       col = editor.offsetToLogicalPosition(range.getStartOffset()).getColumn(); | ||||
|       if (UserDataManager.getVimLastColumn(((IjVimCaret) caret).getCaret()) == VimMotionGroupBase.LAST_COLUMN) { | ||||
|       if (caret.getVimLastColumn() == VimMotionGroupBase.LAST_COLUMN) { | ||||
|         col = VimMotionGroupBase.LAST_COLUMN; | ||||
|       } | ||||
|     } | ||||
|     boolean after = range.getEndOffset() >= EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()); | ||||
|     boolean after = range.getEndOffset() >= editor.fileSize(); | ||||
|  | ||||
|     final VimLogicalPosition lp = editor.offsetToLogicalPosition(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, caret)); | ||||
|     final VimLogicalPosition lp = editor.offsetToLogicalPosition(injector.getMotion().moveCaretToLineStartSkipLeading(editor, caret)); | ||||
|  | ||||
|     boolean res = deleteRange(editor, caret, range, type, true); | ||||
|     if (res) { | ||||
|       if (type == SelectionType.LINE_WISE) { | ||||
|         // Please don't use `getDocument().getText().isEmpty()` because it converts CharSequence into String | ||||
|         if (((IjVimEditor) editor).getEditor().getDocument().getTextLength() == 0) { | ||||
|         if (editor.fileSize() == 0) { | ||||
|           insertBeforeCursor(editor, context); | ||||
|         } | ||||
|         else if (after && !EditorHelperRt.endsWithNewLine(((IjVimEditor) editor).getEditor())) { | ||||
|         else if (after && !EngineEditorHelperKt.endsWithNewLine(editor)) { | ||||
|           insertNewLineBelow(editor, caret, lp.getColumn()); | ||||
|         } | ||||
|         else { | ||||
|   | ||||
| @@ -890,6 +890,10 @@ public class SearchGroup extends VimSearchGroupBase implements PersistentStateCo | ||||
|   @Override | ||||
|   public void setLastSearchPattern(@Nullable String lastSearchPattern) { | ||||
|     this.lastSearch = lastSearchPattern; | ||||
|     if (showSearchHighlight) { | ||||
|       resetIncsearchHighlights(); | ||||
|       updateSearchHighlights(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   | ||||
| @@ -22,6 +22,7 @@ package com.maddyhome.idea.vim.helper | ||||
|  | ||||
| import com.intellij.codeWithMe.ClientId | ||||
| import com.intellij.openapi.editor.Caret | ||||
| import com.intellij.openapi.editor.CaretState | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.intellij.openapi.editor.ex.util.EditorUtil | ||||
| import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx | ||||
| @@ -106,3 +107,41 @@ val Caret.vimLine: Int | ||||
|  */ | ||||
| val Editor.vimLine: Int | ||||
|   get() = this.caretModel.currentCaret.vimLine | ||||
|  | ||||
| inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) { | ||||
|   val caretModel = this.caretModel | ||||
|   val carets = if (this.inBlockSubMode) 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 | ||||
|   }  | ||||
| } | ||||
|   | ||||
| @@ -21,18 +21,24 @@ package com.maddyhome.idea.vim.helper | ||||
| import com.intellij.openapi.actionSystem.ActionGroup | ||||
| import com.intellij.openapi.actionSystem.ActionManager | ||||
| import com.intellij.openapi.actionSystem.ActionPlaces | ||||
| import com.intellij.openapi.actionSystem.AnAction | ||||
| import com.intellij.openapi.actionSystem.AnActionEvent | ||||
| import com.intellij.openapi.actionSystem.AnActionResult | ||||
| import com.intellij.openapi.actionSystem.DataContext | ||||
| import com.intellij.openapi.actionSystem.IdeActions | ||||
| import com.intellij.openapi.actionSystem.PlatformCoreDataKeys | ||||
| import com.intellij.openapi.actionSystem.PlatformDataKeys | ||||
| import com.intellij.openapi.actionSystem.Presentation | ||||
| import com.intellij.openapi.actionSystem.ex.ActionManagerEx | ||||
| import com.intellij.openapi.actionSystem.ex.ActionUtil | ||||
| import com.intellij.openapi.command.CommandProcessor | ||||
| import com.intellij.openapi.command.UndoConfirmationPolicy | ||||
| import com.intellij.openapi.components.Service | ||||
| import com.intellij.openapi.editor.actionSystem.DocCommandGroupId | ||||
| import com.intellij.openapi.project.IndexNotReadyException | ||||
| import com.intellij.openapi.ui.popup.JBPopupFactory | ||||
| import com.intellij.openapi.util.NlsContexts | ||||
| import com.intellij.util.SlowOperations | ||||
| import com.maddyhome.idea.vim.RegisterActions | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.NativeAction | ||||
| @@ -95,11 +101,48 @@ class IjActionExecutor : VimActionExecutor { | ||||
|       popup.showInFocusCenter() | ||||
|       return true | ||||
|     } else { | ||||
|       ActionUtil.performActionDumbAwareWithCallbacks(ijAction, event) | ||||
|       performDumbAwareWithCallbacks(ijAction, event) { ijAction.actionPerformed(event) } | ||||
|       return true | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // This is taken directly from ActionUtil.performActionDumbAwareWithCallbacks | ||||
|   // But with one check removed. With this check some actions (like `:w` doesn't work) | ||||
|   // https://youtrack.jetbrains.com/issue/VIM-2691/File-is-not-saved-on-w | ||||
|   private fun performDumbAwareWithCallbacks( | ||||
|     action: AnAction, | ||||
|     event: AnActionEvent, | ||||
|     performRunnable: Runnable, | ||||
|   ) { | ||||
|     val project = event.project | ||||
|     var indexError: IndexNotReadyException? = null | ||||
|     val manager = ActionManagerEx.getInstanceEx() | ||||
|     manager.fireBeforeActionPerformed(action, event) | ||||
|     val component = event.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT) | ||||
|     var result: AnActionResult? = null | ||||
|     try { | ||||
|       SlowOperations.allowSlowOperations(SlowOperations.ACTION_PERFORM).use { ignore -> | ||||
|         performRunnable.run() | ||||
|         result = AnActionResult.PERFORMED | ||||
|       } | ||||
|     } catch (ex: IndexNotReadyException) { | ||||
|       indexError = ex | ||||
|       result = AnActionResult.failed(ex) | ||||
|     } catch (ex: RuntimeException) { | ||||
|       result = AnActionResult.failed(ex) | ||||
|       throw ex | ||||
|     } catch (ex: Error) { | ||||
|       result = AnActionResult.failed(ex) | ||||
|       throw ex | ||||
|     } finally { | ||||
|       if (result == null) result = AnActionResult.failed(Throwable()) | ||||
|       manager.fireAfterActionPerformed(action, event, result!!) | ||||
|     } | ||||
|     if (indexError != null) { | ||||
|       ActionUtil.showDumbModeWarning(project, event) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private fun canBePerformed(event: AnActionEvent, action: ActionGroup, context: DataContext): Boolean { | ||||
|     val presentation = event.presentation | ||||
|     return try { | ||||
| @@ -155,6 +198,10 @@ class IjActionExecutor : VimActionExecutor { | ||||
|     return RegisterActions.findAction(id) | ||||
|   } | ||||
|  | ||||
|   override fun findVimActionOrDie(id: String): EditorActionHandlerBase { | ||||
|     return RegisterActions.findActionOrDie(id) | ||||
|   } | ||||
|  | ||||
|   override fun getAction(actionId: String): NativeAction? { | ||||
|     return ActionManager.getInstance().getAction(actionId)?.let { IjNativeAction(it) } | ||||
|   } | ||||
|   | ||||
| @@ -143,4 +143,8 @@ class IjEditorHelper : EngineEditorHelper { | ||||
|   override fun getLeadingWhitespace(editor: VimEditor, line: Int): String { | ||||
|     return EditorHelper.getLeadingWhitespace(editor.ij, line) | ||||
|   } | ||||
|  | ||||
|   override fun anyNonWhitespace(editor: VimEditor, offset: Int, dir: Int): Boolean { | ||||
|     return SearchHelper.anyNonWhitespace(editor.ij, offset, dir) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -99,5 +99,6 @@ class UndoRedoHelper : UndoRedoBase() { | ||||
|     while (check() && !changeListener.hasChanged) { | ||||
|       action.run() | ||||
|     } | ||||
|     vimDocument.removeChangeListener(changeListener) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -32,6 +32,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent | ||||
| import com.intellij.openapi.actionSystem.CommonDataKeys | ||||
| import com.intellij.openapi.actionSystem.DataContext | ||||
| import com.intellij.openapi.actionSystem.ex.AnActionListener | ||||
| import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.intellij.openapi.project.DumbAwareToggleAction | ||||
| import com.maddyhome.idea.vim.KeyHandler | ||||
| @@ -70,7 +71,7 @@ object IdeaSpecifics { | ||||
|       //region Track action id | ||||
|       if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL, OptionConstants.trackactionidsName)) { | ||||
|         if (action !is NotificationService.ActionIdNotifier.CopyActionId && action !is NotificationService.ActionIdNotifier.StopTracking) { | ||||
|           val id: String? = ActionManager.getInstance().getId(action) | ||||
|           val id: String? = ActionManager.getInstance().getId(action) ?: (action.shortcutSet as? ProxyShortcutSet)?.actionId | ||||
|           VimPlugin.getNotifications(dataContext.getData(CommonDataKeys.PROJECT)).notifyActionId(id) | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import com.maddyhome.idea.vim.VimPlugin | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.LineDeleteShift | ||||
| import com.maddyhome.idea.vim.api.VimCaret | ||||
| import com.maddyhome.idea.vim.api.VimChangeGroupBase | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.VimMotionGroupBase | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| @@ -40,7 +41,6 @@ import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.common.VimRange | ||||
| import com.maddyhome.idea.vim.common.including | ||||
| import com.maddyhome.idea.vim.common.offset | ||||
| import com.maddyhome.idea.vim.group.ChangeGroup | ||||
| import com.maddyhome.idea.vim.group.MotionGroup | ||||
| import com.maddyhome.idea.vim.helper.EditorHelper | ||||
| import com.maddyhome.idea.vim.helper.inlayAwareVisualColumn | ||||
| @@ -62,7 +62,7 @@ fun changeRange( | ||||
|   var col = 0 | ||||
|   var lines = 0 | ||||
|   if (type === SelectionType.BLOCK_WISE) { | ||||
|     lines = ChangeGroup.getLinesCountInVisualBlock(IjVimEditor(editor), range) | ||||
|     lines = VimChangeGroupBase.getLinesCountInVisualBlock(IjVimEditor(editor), range) | ||||
|     col = editor.offsetToLogicalPosition(range.startOffset).column | ||||
|     if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) { | ||||
|       col = VimMotionGroupBase.LAST_COLUMN | ||||
|   | ||||
| @@ -54,4 +54,4 @@ class IjVimDocument(private val document: Document) : VimDocument { | ||||
|   override fun getOffsetGuard(offset: Offset): LiveRange? { | ||||
|     return document.getOffsetGuard(offset.point)?.vim | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -110,6 +110,17 @@ class IjVimSearchHelper : VimSearchHelper { | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   override fun findNextWordEnd( | ||||
|     chars: CharSequence, | ||||
|     pos: Int, | ||||
|     size: Int, | ||||
|     count: Int, | ||||
|     bigWord: Boolean, | ||||
|     spaceWords: Boolean, | ||||
|   ): Int { | ||||
|     return SearchHelper.findNextWordEnd(chars, pos, size, count, bigWord, spaceWords) | ||||
|   } | ||||
|  | ||||
|   override fun findNextWord(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean): Int { | ||||
|     return SearchHelper.findNextWord( | ||||
|       (editor as IjVimEditor).editor, | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| /* | ||||
|  * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform | ||||
|  * Copyright (C) 2003-2022 The IdeaVim authors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 2 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package com.maddyhome.idea.vim.vimscript.model.functions.handlers | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.ex.ExException | ||||
| import com.maddyhome.idea.vim.vimscript.model.VimLContext | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimList | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString | ||||
| import com.maddyhome.idea.vim.vimscript.model.expressions.Expression | ||||
| import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler | ||||
|  | ||||
| class JoinFunctionHandler : FunctionHandler() { | ||||
|   override val name: String = "join" | ||||
|   override val minimumNumberOfArguments: Int = 1 | ||||
|   override val maximumNumberOfArguments: Int = 2 | ||||
|  | ||||
|   override fun doFunction( | ||||
|     argumentValues: List<Expression>, | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     vimContext: VimLContext, | ||||
|   ): VimDataType { | ||||
|     val firstArgument = argumentValues[0].evaluate(editor, context, vimContext) | ||||
|     if (firstArgument !is VimList) { | ||||
|       throw ExException("E714: List required") | ||||
|     } | ||||
|     val secondArgument = argumentValues.getOrNull(1)?.evaluate(editor, context, vimContext) ?: VimString(" ") | ||||
|     return VimString(firstArgument.values.joinToString(secondArgument.asString()) { it.toString() }) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,44 @@ | ||||
| /* | ||||
|  * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform | ||||
|  * Copyright (C) 2003-2022 The IdeaVim authors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 2 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package com.maddyhome.idea.vim.vimscript.model.functions.handlers | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.vimscript.model.VimLContext | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString | ||||
| import com.maddyhome.idea.vim.vimscript.model.expressions.Expression | ||||
| import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler | ||||
| import java.util.* | ||||
|  | ||||
| class TolowerFunctionHandler : FunctionHandler() { | ||||
|   override val name: String = "tolower" | ||||
|   override val minimumNumberOfArguments: Int = 1 | ||||
|   override val maximumNumberOfArguments: Int = 1 | ||||
|  | ||||
|   override fun doFunction( | ||||
|     argumentValues: List<Expression>, | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     vimContext: VimLContext, | ||||
|   ): VimDataType { | ||||
|     val argumentString = argumentValues[0].evaluate(editor, context, vimContext).asString() | ||||
|     return VimString(argumentString.lowercase(Locale.getDefault())) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,44 @@ | ||||
| /* | ||||
|  * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform | ||||
|  * Copyright (C) 2003-2022 The IdeaVim authors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 2 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package com.maddyhome.idea.vim.vimscript.model.functions.handlers | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.vimscript.model.VimLContext | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString | ||||
| import com.maddyhome.idea.vim.vimscript.model.expressions.Expression | ||||
| import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler | ||||
| import java.util.* | ||||
|  | ||||
| class ToupperFunctionHandler : FunctionHandler() { | ||||
|   override val name: String = "toupper" | ||||
|   override val minimumNumberOfArguments: Int = 1 | ||||
|   override val maximumNumberOfArguments: Int = 1 | ||||
|  | ||||
|   override fun doFunction( | ||||
|     argumentValues: List<Expression>, | ||||
|     editor: VimEditor, | ||||
|     context: ExecutionContext, | ||||
|     vimContext: VimLContext, | ||||
|   ): VimDataType { | ||||
|     val argumentString = argumentValues[0].evaluate(editor, context, vimContext).asString() | ||||
|     return VimString(argumentString.uppercase(Locale.getDefault())) | ||||
|   } | ||||
| } | ||||
| @@ -180,7 +180,7 @@ object CommandVisitor : VimscriptBaseVisitor<Command>() { | ||||
|   override fun visitLet1Command(ctx: VimscriptParser.Let1CommandContext): Command { | ||||
|     val ranges: Ranges = parseRanges(ctx.range()) | ||||
|     val variable: Expression = expressionVisitor.visit(ctx.expr(0)) | ||||
|     val operator = getByValue(ctx.assignmentOperator.text) | ||||
|     val operator = getByValue(ctx.assignmentOperator().text) | ||||
|     val expression: Expression = expressionVisitor.visit(ctx.expr(1)) | ||||
|     return LetCommand(ranges, variable, operator, expression, true) | ||||
|   } | ||||
|   | ||||
| @@ -11,5 +11,8 @@ | ||||
|     <vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.FuncrefFunctionHandler" name="funcref"/> | ||||
|     <vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler" name="has"/> | ||||
|     <vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler" name="submatch"/> | ||||
|     <vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.TolowerFunctionHandler" name="tolower"/> | ||||
|     <vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.ToupperFunctionHandler" name="toupper"/> | ||||
|     <vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.JoinFunctionHandler" name="join"/> | ||||
|   </extensions> | ||||
| </idea-plugin> | ||||
| @@ -88,7 +88,7 @@ class UndoActionTest : VimTestCase() { | ||||
|     val after = """ | ||||
|                 A Discovery | ||||
|  | ||||
|                 I1 found${c} it in a legendary land | ||||
|                 I1 found$c it in a legendary land | ||||
|                 all rocks and lavender and tufted grass, | ||||
|                 where it was settled on some sodden sand | ||||
|                 hard by the torrent of a mountain pass. | ||||
| @@ -101,4 +101,4 @@ class UndoActionTest : VimTestCase() { | ||||
|     val editor = myFixture.editor | ||||
|     return editor.caretModel.primaryCaret.hasSelection() | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,117 @@ | ||||
| /* | ||||
|  * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform | ||||
|  * Copyright (C) 2003-2022 The IdeaVim authors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 2 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package org.jetbrains.plugins.ideavim.ex.implementation | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import org.jetbrains.plugins.ideavim.VimTestCase | ||||
| import org.junit.Ignore | ||||
|  | ||||
| class LongerFunctionTest : VimTestCase() { | ||||
|   val script = """ | ||||
|       function! IsUppercase(char) | ||||
|         return a:char >=# 'A' && a:char <=# 'Z' | ||||
|       endfunction | ||||
|  | ||||
|       function! IsCaps(word) | ||||
|         return a:word ==# toupper(a:word) | ||||
|       endfunction | ||||
|  | ||||
|       function! Capitalize(word) | ||||
|         return toupper(a:word[0]) .. a:word[1:] | ||||
|       endfunction | ||||
|  | ||||
|       function! Split(text, delimeter) | ||||
|         let parts = [] | ||||
|         let part = '' | ||||
|         for char in a:text | ||||
|           if char ==? a:delimeter | ||||
|             let parts += [part] | ||||
|             let part = '' | ||||
|           else | ||||
|             let part .= char | ||||
|           endif | ||||
|         endfor | ||||
|         let parts += [part] | ||||
|         return parts | ||||
|       endfunction | ||||
|  | ||||
|       function! ToCamelCase() | ||||
|         let capitalize = 0 | ||||
|          | ||||
|         normal gv"wy | ||||
|         let wordUnderCaret = @w | ||||
|         let parts = Split(wordUnderCaret, '_') | ||||
|         let result = tolower(parts[0]) | ||||
|         let counter = 1 | ||||
|         while counter < len(parts) | ||||
|           let result .= Capitalize(tolower(parts[counter]))  | ||||
|           let counter += 1 | ||||
|         endwhile | ||||
|         execute 'normal gvc' .. result  | ||||
|       endfunction | ||||
|  | ||||
|       function! ToSnakeCase() | ||||
|         normal gv"wy | ||||
|         let wordUnderCaret = @w | ||||
|          | ||||
|         let parts = [] | ||||
|         let subword = '' | ||||
|         let atWordStart = 1 | ||||
|         for char in wordUnderCaret | ||||
|           if IsUppercase(char) && !atWordStart | ||||
|             let parts += [toupper(subword)] | ||||
|             let subword = char | ||||
|           else | ||||
|             let subword .= char | ||||
|           endif | ||||
|  | ||||
|           let atWordStart = char ==? ' ' | ||||
|         endfor | ||||
|         let parts += [toupper(subword)] | ||||
|         execute 'normal gvc' .. join(parts, '_') | ||||
|       endfunction | ||||
|        | ||||
|       vnoremap u :<C-u>call ToCamelCase()<CR> | ||||
|       vnoremap U :<C-u>call ToSnakeCase()<CR> | ||||
|     """.trimIndent() | ||||
|  | ||||
|   // todo normal required | ||||
| //  fun `test 1`() { | ||||
| //    configureByText(""" | ||||
| //      const val ${c}VERY_IMPORTANT_VALUE = 42 | ||||
| //    """.trimIndent()) | ||||
| //    injector.vimscriptExecutor.execute(script) | ||||
| //    typeText(injector.parser.parseKeys("veu")) | ||||
| //    assertState(""" | ||||
| //      const val veryImportantValue${c} = 42 | ||||
| //    """.trimIndent()) | ||||
| //  } | ||||
|  | ||||
|   // todo normal required | ||||
| //  fun `test 2`() { | ||||
| //    configureByText(""" | ||||
| //      val ${c}myCamelCaseValue = "Hi, I'm a simple value" | ||||
| //    """.trimIndent()) | ||||
| //    injector.vimscriptExecutor.execute(script) | ||||
| //    typeText(injector.parser.parseKeys("veU")) | ||||
| //    assertState(""" | ||||
| //      val MY_CAMEL_CASE_VALUE${c} = "Hi, I'm a simple value" | ||||
| //    """.trimIndent()) | ||||
| //  } | ||||
| } | ||||
| @@ -0,0 +1,55 @@ | ||||
| /* | ||||
|  * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform | ||||
|  * Copyright (C) 2003-2022 The IdeaVim authors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 2 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package org.jetbrains.plugins.ideavim.ex.implementation.functions | ||||
|  | ||||
| import org.jetbrains.plugins.ideavim.VimTestCase | ||||
|  | ||||
| class BasicStringFunctions : VimTestCase() { | ||||
|  | ||||
|   fun `test toupper`() { | ||||
|     configureByText("\n") | ||||
|     typeText(commandToKeys("echo toupper('Vim is awesome')")) | ||||
|     assertExOutput("VIM IS AWESOME\n") | ||||
|   } | ||||
|  | ||||
|   fun `test tolower`() { | ||||
|     configureByText("\n") | ||||
|     typeText(commandToKeys("echo toupper('Vim is awesome')")) | ||||
|     assertExOutput("vim is awesome\n") | ||||
|   } | ||||
|  | ||||
|   fun `test join`() { | ||||
|     configureByText("\n") | ||||
|     typeText(commandToKeys("echo join(['Vim', 'is', 'awesome'], '_')")) | ||||
|     assertExOutput("Vim_is_awesome\n") | ||||
|   } | ||||
|  | ||||
|   fun `test join without second argument`() { | ||||
|     configureByText("\n") | ||||
|     typeText(commandToKeys("echo join(['Vim', 'is', 'awesome'])")) | ||||
|     assertExOutput("Vim is awesome\n") | ||||
|   } | ||||
|  | ||||
|   fun `test join with wrong first argument type`() { | ||||
|     configureByText("\n") | ||||
|     typeText(commandToKeys("echo join('Vim is awesome')")) | ||||
|     assertPluginError(true) | ||||
|     assertPluginErrorMessageContains("E714: List required") | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,55 @@ | ||||
| /* | ||||
|  * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform | ||||
|  * Copyright (C) 2003-2022 The IdeaVim authors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 2 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package org.jetbrains.plugins.ideavim.ex.parser.commands | ||||
|  | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.vimscript.model.commands.LetCommand | ||||
| import com.maddyhome.idea.vim.vimscript.model.expressions.Register | ||||
| import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression | ||||
| import com.maddyhome.idea.vim.vimscript.model.expressions.operators.AssignmentOperator | ||||
| import junit.framework.TestCase.assertTrue | ||||
| import org.junit.Test | ||||
| import kotlin.test.assertEquals | ||||
|  | ||||
| class LetCommandTest { | ||||
|  | ||||
|   @Test | ||||
|   fun `let with register is parsed correctly`() { | ||||
|     val script = injector.vimscriptParser.parse("let @+=5") | ||||
|     assertEquals(1, script.units.size) | ||||
|     val command = script.units.first() | ||||
|     assertTrue(command is LetCommand) | ||||
|     command as LetCommand | ||||
|     assertEquals(Register('+'), command.variable) | ||||
|     assertEquals(AssignmentOperator.ASSIGNMENT, command.operator) | ||||
|     assertEquals(SimpleExpression(5), command.expression) | ||||
|   } | ||||
|  | ||||
|   @Test | ||||
|   fun `let with register is parsed correctly 2`() { | ||||
|     val script = injector.vimscriptParser.parse("let @--=42") | ||||
|     assertEquals(1, script.units.size) | ||||
|     val command = script.units.first() | ||||
|     assertTrue(command is LetCommand) | ||||
|     command as LetCommand | ||||
|     assertEquals(Register('-'), command.variable) | ||||
|     assertEquals(AssignmentOperator.SUBTRACTION, command.operator) | ||||
|     assertEquals(SimpleExpression(42), command.expression) | ||||
|   } | ||||
| } | ||||
| @@ -313,6 +313,17 @@ class VimSurroundExtensionTest : VimTestCase() { | ||||
|     doTest(listOf("dsb"), before, after, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|   } | ||||
|  | ||||
|   // VIM-2227 | ||||
|   @TestWithoutNeovim(SkipNeovimReason.PLUGIN) | ||||
|   fun testDeleteInvalidSurroundingCharacter() { | ||||
|     val text = "if (${c}condition) {" | ||||
|      | ||||
|     doTest("yibds]", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|     doTest("yibds[", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|     doTest("yibds}", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|     doTest("yibds{", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|   } | ||||
|  | ||||
|   @TestWithoutNeovim(SkipNeovimReason.PLUGIN) | ||||
|   fun testRepeatDeleteSurroundParens() { | ||||
|     val before = "if ((${c}condition)) {\n}\n" | ||||
| @@ -371,6 +382,17 @@ class VimSurroundExtensionTest : VimTestCase() { | ||||
|     doTest(listOf("csbrE."), before, after, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|   } | ||||
|  | ||||
|   // VIM-2227 | ||||
|   @TestWithoutNeovim(SkipNeovimReason.PLUGIN) | ||||
|   fun testChangeInvalidSurroundingCharacter() { | ||||
|     val text = "if (${c}condition) {" | ||||
|  | ||||
|     doTest("yibcs]}", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|     doTest("yibcs[}", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|     doTest("yibcs}]", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|     doTest("yibcs{]", text, text, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) | ||||
|   } | ||||
|  | ||||
|   @VimBehaviorDiffers( | ||||
|     """ | ||||
|       <h1>Title</h1> | ||||
|   | ||||
| @@ -23,11 +23,12 @@ import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.command.Argument | ||||
| import com.maddyhome.idea.vim.command.Command | ||||
| import com.maddyhome.idea.vim.command.OperatorArguments | ||||
| import com.maddyhome.idea.vim.command.SelectionType | ||||
| import com.maddyhome.idea.vim.ex.ExException | ||||
| import com.maddyhome.idea.vim.handler.VimActionHandler | ||||
| import com.maddyhome.idea.vim.put.PutData | ||||
| import com.maddyhome.idea.vim.register.Register | ||||
| import com.maddyhome.idea.vim.vimscript.model.Script | ||||
| import javax.swing.KeyStroke | ||||
|  | ||||
| class InsertRegisterAction : VimActionHandler.SingleExecution() { | ||||
|   override val type: Command.Type = Command.Type.INSERT | ||||
| @@ -84,10 +85,10 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() { | ||||
| private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean { | ||||
|   val register: Register? = injector.registerGroup.getRegister(key) | ||||
|   if (register != null) { | ||||
|     val keys: List<KeyStroke> = register.keys | ||||
|     for (k in keys) { | ||||
|       injector.changeGroup.processKey(editor, context, k) | ||||
|     } | ||||
|     val text = register.rawText ?: injector.parser.toPrintableString(register.keys) | ||||
|     val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList()) | ||||
|     val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true) | ||||
|     injector.put.putText(editor, context, putData) | ||||
|     return true | ||||
|   } | ||||
|   return false | ||||
|   | ||||
| @@ -33,4 +33,11 @@ interface EngineEditorHelper { | ||||
|   fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition | ||||
|   fun getVisualLineLength(editor: VimEditor, line: Int): Int | ||||
|   fun getLeadingWhitespace(editor: VimEditor, line: Int): String | ||||
| } | ||||
|   fun anyNonWhitespace(editor: VimEditor, offset: Int, dir: Int): Boolean | ||||
| } | ||||
|  | ||||
| fun VimEditor.endsWithNewLine(): Boolean { | ||||
|   val textLength = this.fileSize().toInt() | ||||
|   if (textLength == 0) return false | ||||
|   return this.text()[textLength - 1] == '\n' | ||||
| } | ||||
|   | ||||
| @@ -65,6 +65,7 @@ interface VimActionExecutor { | ||||
|   ) | ||||
|  | ||||
|   fun findVimAction(id: String): EditorActionHandlerBase? | ||||
|   fun findVimActionOrDie(id: String): EditorActionHandlerBase | ||||
|  | ||||
|   fun getAction(actionId: String): NativeAction? | ||||
|   fun getActionIdList(idPrefix: String): List<String> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package com.maddyhome.idea.vim.api | ||||
|  | ||||
| import com.maddyhome.idea.vim.KeyHandler | ||||
| import com.maddyhome.idea.vim.command.Argument | ||||
| import com.maddyhome.idea.vim.command.Command | ||||
| import com.maddyhome.idea.vim.command.CommandFlags | ||||
| import com.maddyhome.idea.vim.command.CommandState | ||||
| @@ -24,6 +25,8 @@ import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor | ||||
| import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_END | ||||
| import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS | ||||
| import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_START | ||||
| import com.maddyhome.idea.vim.options.OptionConstants | ||||
| import com.maddyhome.idea.vim.options.OptionScope | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants.LAST_INSERTED_TEXT_REGISTER | ||||
| import java.awt.event.KeyEvent | ||||
| import javax.swing.KeyStroke | ||||
| @@ -798,6 +801,134 @@ abstract class VimChangeGroupBase : VimChangeGroup { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun getDeleteRangeAndType( | ||||
|     editor: VimEditor, | ||||
|     caret: VimCaret, | ||||
|     context: ExecutionContext, | ||||
|     argument: Argument, | ||||
|     isChange: Boolean, | ||||
|     operatorArguments: OperatorArguments, | ||||
|   ): Pair<TextRange, SelectionType>? { | ||||
|     val range = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) ?: return null | ||||
|  | ||||
|     // Delete 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 | ||||
|     var type: SelectionType = if (argument.motion.isLinewiseMotion()) { | ||||
|       SelectionType.LINE_WISE | ||||
|     } else { | ||||
|       SelectionType.CHARACTER_WISE | ||||
|     } | ||||
|     val motion = argument.motion | ||||
|     if (!isChange && !motion.isLinewiseMotion()) { | ||||
|       val start = editor.offsetToLogicalPosition(range.startOffset) | ||||
|       val end = editor.offsetToLogicalPosition(range.endOffset) | ||||
|       if (start.line != end.line) { | ||||
|         if (!injector.engineEditorHelper.anyNonWhitespace(editor, range.startOffset, -1) && | ||||
|           !injector.engineEditorHelper.anyNonWhitespace(editor, range.endOffset, 1) | ||||
|         ) { | ||||
|           type = SelectionType.LINE_WISE | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return Pair(range, type) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete the range of text. | ||||
|    * | ||||
|    * @param editor   The editor to delete the text from | ||||
|    * @param caret    The caret to be moved after deletion | ||||
|    * @param range    The range to delete | ||||
|    * @param type     The type of deletion | ||||
|    * @param isChange Is from a change action | ||||
|    * @return true if able to delete the text, false if not | ||||
|    */ | ||||
|   override fun deleteRange( | ||||
|     editor: VimEditor, | ||||
|     caret: VimCaret, | ||||
|     range: TextRange, | ||||
|     type: SelectionType?, | ||||
|     isChange: Boolean, | ||||
|   ): Boolean { | ||||
|  | ||||
|     // Update the last column before we delete, or we might be retrieving the data for a line that no longer exists | ||||
|     caret.vimLastColumn = caret.inlayAwareVisualColumn | ||||
|     val removeLastNewLine = removeLastNewLine(editor, range, type) | ||||
|     val res = deleteText(editor, range, type) | ||||
|     if (removeLastNewLine) { | ||||
|       val textLength = editor.fileSize().toInt() | ||||
|       editor.deleteString(TextRange(textLength - 1, textLength)) | ||||
|     } | ||||
|     if (res) { | ||||
|       var pos = injector.engineEditorHelper.normalizeOffset(editor, range.startOffset, isChange) | ||||
|       if (type === SelectionType.LINE_WISE) { | ||||
|         pos = injector.motion | ||||
|           .moveCaretToLineWithStartOfLineOption( | ||||
|             editor, editor.offsetToLogicalPosition(pos).line, | ||||
|             caret | ||||
|           ) | ||||
|       } | ||||
|       injector.motion.moveCaret(editor, caret, pos) | ||||
|     } | ||||
|     return res | ||||
|   } | ||||
|  | ||||
|   private fun removeLastNewLine(editor: VimEditor, range: TextRange, type: SelectionType?): Boolean { | ||||
|     var endOffset = range.endOffset | ||||
|     val fileSize = editor.fileSize().toInt() | ||||
|     if (endOffset > fileSize) { | ||||
|       check( | ||||
|         !injector.optionService.isSet( | ||||
|           OptionScope.GLOBAL, | ||||
|           OptionConstants.ideastrictmodeName, | ||||
|           OptionConstants.ideastrictmodeName | ||||
|         ) | ||||
|       ) { "Incorrect offset. File size: $fileSize, offset: $endOffset" } | ||||
|       endOffset = fileSize | ||||
|     } | ||||
|     return (type === SelectionType.LINE_WISE) && range.startOffset != 0 && editor.text()[endOffset - 1] != '\n' && endOffset == fileSize | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete from the cursor to the end of count - 1 lines down and enter insert mode | ||||
|    * | ||||
|    * @param editor The editor to change | ||||
|    * @param caret  The caret to perform action on | ||||
|    * @param count  The number of lines to change | ||||
|    * @return true if able to delete count lines, false if not | ||||
|    */ | ||||
|   override fun changeEndOfLine(editor: VimEditor, caret: VimCaret, count: Int): Boolean { | ||||
|     val res = deleteEndOfLine(editor, caret, count) | ||||
|     if (res) { | ||||
|       caret.moveToOffset(injector.motion.moveCaretToLineEnd(editor, caret)) | ||||
|       editor.vimChangeActionSwitchMode = CommandState.Mode.INSERT | ||||
|     } | ||||
|     return res | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete count characters and then enter insert mode | ||||
|    * | ||||
|    * @param editor The editor to change | ||||
|    * @param caret  The caret to be moved | ||||
|    * @param count  The number of characters to change | ||||
|    * @return true if able to delete count characters, false if not | ||||
|    */ | ||||
|   override fun changeCharacters(editor: VimEditor, caret: VimCaret, count: Int): Boolean { | ||||
|     val len = injector.engineEditorHelper.getLineLength(editor) | ||||
|     val col = caret.getLogicalPosition().column | ||||
|     if (col + count >= len) { | ||||
|       return changeEndOfLine(editor, caret, 1) | ||||
|     } | ||||
|     val res = deleteCharacter(editor, caret, count, true) | ||||
|     if (res) { | ||||
|       editor.vimChangeActionSwitchMode = CommandState.Mode.INSERT | ||||
|     } | ||||
|     return res | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Clears all the keystrokes from the current insert command | ||||
|    * | ||||
| @@ -871,6 +1002,24 @@ abstract class VimChangeGroupBase : VimChangeGroup { | ||||
|   companion object { | ||||
|     private const val MAX_REPEAT_CHARS_COUNT = 10000 | ||||
|     private val logger = vimLogger<VimChangeGroupBase>() | ||||
|  | ||||
|     /** | ||||
|      * Counts number of lines in the visual block. | ||||
|      * | ||||
|      * | ||||
|      * The result includes empty and short lines which does not have explicit start position (caret). | ||||
|      * | ||||
|      * @param editor The editor the block was selected in | ||||
|      * @param range  The range corresponding to the selected block | ||||
|      * @return total number of lines | ||||
|      */ | ||||
|     fun getLinesCountInVisualBlock(editor: VimEditor, range: TextRange): Int { | ||||
|       val startOffsets = range.startOffsets | ||||
|       if (startOffsets.isEmpty()) return 0 | ||||
|       val firstStart = editor.offsetToLogicalPosition(startOffsets[0]) | ||||
|       val lastStart = editor.offsetToLogicalPosition(startOffsets[range.size() - 1]) | ||||
|       return lastStart.line - firstStart.line + 1 | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -75,6 +75,15 @@ interface VimSearchHelper { | ||||
|     bigWord: Boolean, | ||||
|   ): Int | ||||
|  | ||||
|   fun findNextWordEnd( | ||||
|     chars: CharSequence, | ||||
|     pos: Int, | ||||
|     size: Int, | ||||
|     count: Int, | ||||
|     bigWord: Boolean, | ||||
|     spaceWords: Boolean, | ||||
|   ): Int | ||||
|  | ||||
|   fun findNextWord(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean): Int | ||||
|  | ||||
|   fun findPattern( | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import com.maddyhome.idea.vim.command.isChar | ||||
| import com.maddyhome.idea.vim.command.isLine | ||||
| import com.maddyhome.idea.vim.common.TextRange | ||||
| import com.maddyhome.idea.vim.helper.firstOrNull | ||||
| import com.maddyhome.idea.vim.helper.mode | ||||
| import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS | ||||
| import java.util.* | ||||
| import kotlin.math.abs | ||||
| @@ -140,7 +141,9 @@ abstract class VimPutBase : VimPut { | ||||
|       } | ||||
|       "postEndOffset" -> caret.moveToOffset(endOffset + 1) | ||||
|       "preLineEndOfEndOffset" -> { | ||||
|         val pos = min(endOffset, editor.getLineEndForOffset(endOffset - 1) - 1) | ||||
|         var rightestPosition = editor.getLineEndForOffset(endOffset - 1) | ||||
|         if (editor.mode != CommandState.Mode.INSERT) --rightestPosition // it's not possible to place a caret at the end of the line in any mode except insert | ||||
|         val pos = min(endOffset, rightestPosition) | ||||
|         caret.moveToOffset(pos) | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -48,6 +48,11 @@ interface VimRegisterGroup { | ||||
|     isDelete: Boolean, | ||||
|   ): Boolean | ||||
|  | ||||
|   /** | ||||
|    * Stores text to any writable register (used for the let command) | ||||
|    */ | ||||
|   fun storeText(register: Char, text: String): Boolean | ||||
|  | ||||
|   /** | ||||
|    * Stores text, character wise, in the given special register | ||||
|    * | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import com.maddyhome.idea.vim.register.RegisterConstants.RECORDABLE_REGISTERS | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants.SMALL_DELETION_REGISTER | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants.UNNAMED_REGISTER | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants.VALID_REGISTERS | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants.WRITABLE_REGISTERS | ||||
| import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString | ||||
| import javax.swing.KeyStroke | ||||
|  | ||||
| @@ -292,6 +293,24 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   override fun storeText(register: Char, text: String): 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.CHARACTER_WISE, textToStore, ArrayList()) | ||||
|     saveRegister(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 | ||||
|   } | ||||
|   | ||||
| @@ -21,9 +21,11 @@ package com.maddyhome.idea.vim.vimscript.model.commands | ||||
| import com.maddyhome.idea.vim.api.ExecutionContext | ||||
| import com.maddyhome.idea.vim.api.VimEditor | ||||
| import com.maddyhome.idea.vim.api.injector | ||||
| import com.maddyhome.idea.vim.diagnostic.vimLogger | ||||
| import com.maddyhome.idea.vim.ex.ExException | ||||
| import com.maddyhome.idea.vim.ex.ranges.Ranges | ||||
| import com.maddyhome.idea.vim.options.OptionScope | ||||
| import com.maddyhome.idea.vim.register.RegisterConstants | ||||
| import com.maddyhome.idea.vim.vimscript.model.ExecutionResult | ||||
| import com.maddyhome.idea.vim.vimscript.model.Script | ||||
| import com.maddyhome.idea.vim.vimscript.model.VimLContext | ||||
| @@ -56,8 +58,12 @@ data class LetCommand( | ||||
|   val isSyntaxSupported: Boolean, | ||||
| ) : Command.SingleExecution(ranges) { | ||||
|  | ||||
|   companion object { | ||||
|     private val logger = vimLogger<LetCommand>() | ||||
|   } | ||||
|   override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY) | ||||
|  | ||||
|  | ||||
|   @Throws(ExException::class) | ||||
|   override fun processCommand(editor: VimEditor, context: ExecutionContext): ExecutionResult { | ||||
|     if (!isSyntaxSupported) return ExecutionResult.Error | ||||
| @@ -194,13 +200,19 @@ data class LetCommand( | ||||
|       is EnvVariableExpression -> TODO() | ||||
|  | ||||
|       is Register -> { | ||||
|         if (!(variable.char.isLetter() || variable.char.isDigit() || variable.char == '"')) { | ||||
|           throw ExException("Let command supports only 0-9a-zA-Z\" registers at the moment") | ||||
|         if (RegisterConstants.WRITABLE_REGISTERS.contains(variable.char)) { | ||||
|           val result = injector.registerGroup.storeText(variable.char, expression.evaluate(editor, context, vimContext).asString()) | ||||
|           if (!result) { | ||||
|             logger.error(""" | ||||
|               Error during `let ${variable.originalString} ${operator.value} ${expression.originalString}` command execution. | ||||
|               Could not set register value | ||||
|             """.trimIndent()) | ||||
|           } | ||||
|         } else if (RegisterConstants.VALID_REGISTERS.contains(variable.char)) { | ||||
|           throw ExException("E354: Invalid register name: '${variable.char}'") | ||||
|         } else { | ||||
|           throw ExException("E18: Unexpected characters in :let") | ||||
|         } | ||||
|  | ||||
|         injector.registerGroup.startRecording(editor, variable.char) | ||||
|         injector.registerGroup.recordText(expression.evaluate(editor, context, vimContext).asString()) | ||||
|         injector.registerGroup.finishRecording(editor) | ||||
|       } | ||||
|  | ||||
|       else -> throw ExException("E121: Undefined variable") | ||||
|   | ||||
| @@ -61,7 +61,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH | ||||
|         ) | ||||
|       ) | ||||
|     } | ||||
|     initializeFunctionVariables(argumentValues, editor, context) | ||||
|     initializeFunctionVariables(argumentValues, editor, context, vimContext) | ||||
|  | ||||
|     if (function.flags.contains(FunctionFlag.RANGE)) { | ||||
|       val line = (injector.variableService.getNonNullVariableValue(Variable(Scope.FUNCTION_VARIABLE, "firstline"), editor, context, function) as VimInt).value | ||||
| @@ -131,12 +131,12 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH | ||||
|     return returnValue | ||||
|   } | ||||
|  | ||||
|   private fun initializeFunctionVariables(argumentValues: List<Expression>, editor: VimEditor, context: ExecutionContext) { | ||||
|   private fun initializeFunctionVariables(argumentValues: List<Expression>, editor: VimEditor, context: ExecutionContext, functionCallContext: VimLContext) { | ||||
|     // non-optional function arguments | ||||
|     for ((index, name) in function.args.withIndex()) { | ||||
|       injector.variableService.storeVariable( | ||||
|         Variable(Scope.FUNCTION_VARIABLE, name), | ||||
|         argumentValues[index].evaluate(editor, context, function.vimContext), | ||||
|         argumentValues[index].evaluate(editor, context, functionCallContext), | ||||
|         editor, | ||||
|         context, | ||||
|         function | ||||
| @@ -147,7 +147,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH | ||||
|       val expressionToStore = if (index + function.args.size < argumentValues.size) argumentValues[index + function.args.size] else function.defaultArgs[index].second | ||||
|       injector.variableService.storeVariable( | ||||
|         Variable(Scope.FUNCTION_VARIABLE, function.defaultArgs[index].first), | ||||
|         expressionToStore.evaluate(editor, context, function.vimContext), | ||||
|         expressionToStore.evaluate(editor, context, functionCallContext), | ||||
|         editor, | ||||
|         context, | ||||
|         function | ||||
| @@ -158,7 +158,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH | ||||
|       val remainingArgs = if (function.args.size + function.defaultArgs.size < argumentValues.size) { | ||||
|         VimList( | ||||
|           argumentValues.subList(function.args.size + function.defaultArgs.size, argumentValues.size) | ||||
|             .map { it.evaluate(editor, context, function.vimContext) }.toMutableList() | ||||
|             .map { it.evaluate(editor, context, functionCallContext) }.toMutableList() | ||||
|         ) | ||||
|       } else { | ||||
|         VimList(mutableListOf()) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user