diff --git a/build.gradle.kts b/build.gradle.kts index a9b3241330..6d0f8eb834 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -147,7 +147,8 @@ intellij { downloadSources.set(downloadIdeaSources.toBoolean()) instrumentCode.set(instrumentPluginCode.toBoolean()) intellijRepository.set("https://www.jetbrains.com/intellij-repository") - plugins.set(listOf("java", "AceJump:3.8.4")) + // Yaml is only used for testing. It's part of the IdeaIC distribution, but needs to be included as a reference + plugins.set(listOf("java", "AceJump:3.8.4", "yaml")) } tasks { diff --git a/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt b/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt index c38385c3c2..5454c4d583 100644 --- a/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt +++ b/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt @@ -20,56 +20,105 @@ package com.maddyhome.idea.vim.action.change 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.VimEditor 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.OperatorArguments import com.maddyhome.idea.vim.command.SelectionType +import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.argumentCaptured import com.maddyhome.idea.vim.group.MotionGroup +import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.commandState +import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.newapi.ij +import java.util.* + +private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: SelectionType): Boolean { + val operatorFunction = VimPlugin.getKey().operatorFunction + if (operatorFunction == null) { + VimPlugin.showMessage(MessageHelper.message("E774")) + return false + } + + val saveRepeatHandler = VimRepeater.repeatHandler + VimPlugin.getMark().setChangeMarks(editor, textRange) + KeyHandler.getInstance().reset(editor) + val result = operatorFunction.apply(editor.ij, context.ij, selectionType) + VimRepeater.repeatHandler = saveRepeatHandler + return result +} -/** - * @author vlan - */ class OperatorAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED override val argumentType: Argument.Type = Argument.Type.MOTION override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { - val operatorFunction = VimPlugin.getKey().operatorFunction - if (operatorFunction != null) { - val argument = cmd.argument - if (argument != null) { - if (!editor.commandState.isDotRepeatInProgress) { - argumentCaptured = argument - } - val saveRepeatHandler = VimRepeater.repeatHandler - val motion = argument.motion - val range = MotionGroup - .getMotionRange( - editor.ij, - editor.ij.caretModel.primaryCaret, - context.ij, - argument, - operatorArguments - ) - if (range != null) { - VimPlugin.getMark().setChangeMarks(editor, range) - val selectionType = if (motion.isLinewiseMotion()) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE - KeyHandler.getInstance().reset(editor) - val result = operatorFunction.apply(editor.ij, context.ij, selectionType) - VimRepeater.repeatHandler = saveRepeatHandler - return result - } + val argument = cmd.argument ?: return false + if (!editor.commandState.isDotRepeatInProgress) { + argumentCaptured = argument + } + val range = getMotionRange(editor, context, argument, operatorArguments) + + if (range != null) { + val selectionType = if (argument.motion.isLinewiseMotion()) { + SelectionType.LINE_WISE } - return false + else { + SelectionType.CHARACTER_WISE + } + return doOperatorAction(editor, context, range, selectionType) } - VimPlugin.showMessage(MessageHelper.message("E774")) return false } + + private fun getMotionRange( + editor: VimEditor, + context: ExecutionContext, + argument: Argument, + operatorArguments: OperatorArguments): TextRange? { + + // Note that we're using getMotionRange2 in order to avoid normalising the linewise range into line start + // offsets that will be used to set the change marks. This affects things like the location of the caret in the + // Commentary extension + val ijEditor = editor.ij + return MotionGroup.getMotionRange2( + ijEditor, + ijEditor.caretModel.primaryCaret, + context.ij, + argument, + operatorArguments + )?.normalize()?.let { + + // If we're linewise, make sure the end offset isn't just the EOL char + if (argument.motion.isLinewiseMotion() && it.endOffset < editor.fileSize()) { + TextRange(it.startOffset, it.endOffset + 1) + } else { + it + } + } + } } + +class VisualOperatorAction: VisualOperatorActionHandler.ForEachCaret() { + override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED + + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_EXIT_VISUAL) + + override fun executeAction( + editor: VimEditor, + caret: VimCaret, + context: ExecutionContext, + cmd: Command, + range: VimSelection, + operatorArguments: OperatorArguments, + ): Boolean { + return doOperatorAction(editor, context, range.toVimTextRange(), range.type) + } +} \ No newline at end of file diff --git a/src/main/java/com/maddyhome/idea/vim/common/CommandAlias.kt b/src/main/java/com/maddyhome/idea/vim/common/CommandAlias.kt index 35c9252ea4..bb3329493d 100644 --- a/src/main/java/com/maddyhome/idea/vim/common/CommandAlias.kt +++ b/src/main/java/com/maddyhome/idea/vim/common/CommandAlias.kt @@ -18,9 +18,10 @@ package com.maddyhome.idea.vim.common -import com.intellij.openapi.actionSystem.DataContext -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.VimEditor +import com.maddyhome.idea.vim.ex.ranges.Ranges import com.maddyhome.idea.vim.helper.MessageHelper import org.jetbrains.annotations.NonNls @@ -125,5 +126,5 @@ sealed class GoalCommand { } interface CommandAliasHandler { - fun execute(editor: Editor, context: DataContext) + fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) } diff --git a/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionFacade.kt b/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionFacade.kt index 5c64a54a3b..c94b3bba9e 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionFacade.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionFacade.kt @@ -24,6 +24,8 @@ import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.action.change.Extension import com.maddyhome.idea.vim.command.SelectionType +import com.maddyhome.idea.vim.common.CommandAlias +import com.maddyhome.idea.vim.common.CommandAliasHandler import com.maddyhome.idea.vim.common.MappingMode import com.maddyhome.idea.vim.helper.CommandLineHelper import com.maddyhome.idea.vim.helper.EditorDataContext @@ -81,6 +83,29 @@ object VimExtensionFacade { VimPlugin.getKey().putKeyMapping(filteredModes, fromKeys, pluginOwner, toKeys, recursive) } + /** + * Equivalent to calling 'command' to set up a user-defined command or alias + */ + fun addCommand( + name: String, + handler: CommandAliasHandler + ) { + addCommand(name, 0, 0, handler) + } + + /** + * Equivalent to calling 'command' to set up a user-defined command or alias + */ + @JvmStatic + fun addCommand( + name: String, + minimumNumberOfArguments: Int, + maximumNumberOfArguments: Int, + handler: CommandAliasHandler + ) { + VimPlugin.getCommand().setAlias(name, CommandAlias.Call(minimumNumberOfArguments, maximumNumberOfArguments, name, handler)) + } + /** Sets the value of 'operatorfunc' to be used as the operator function in 'g@'. */ @JvmStatic fun setOperatorFunction(function: OperatorFunction) { diff --git a/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.java b/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.java deleted file mode 100644 index 7d6df171e6..0000000000 --- a/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 . - */ - -package com.maddyhome.idea.vim.extension.commentary; - -import com.intellij.codeInsight.actions.MultiCaretCodeInsightActionHandler; -import com.intellij.codeInsight.generation.CommentByBlockCommentHandler; -import com.intellij.codeInsight.generation.CommentByLineCommentHandler; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.application.WriteAction; -import com.intellij.openapi.editor.Caret; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiFile; -import com.maddyhome.idea.vim.VimPlugin; -import com.maddyhome.idea.vim.command.CommandState; -import com.maddyhome.idea.vim.command.SelectionType; -import com.maddyhome.idea.vim.common.MappingMode; -import com.maddyhome.idea.vim.common.TextRange; -import com.maddyhome.idea.vim.extension.VimExtension; -import com.maddyhome.idea.vim.extension.VimExtensionHandler; -import com.maddyhome.idea.vim.key.OperatorFunction; -import com.maddyhome.idea.vim.newapi.IjVimEditor; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import static com.maddyhome.idea.vim.extension.VimExtensionFacade.*; -import static com.maddyhome.idea.vim.helper.StringHelper.parseKeys; - -/** - * @author dhleong - */ -public class CommentaryExtension implements VimExtension { - - @Override - public @NotNull String getName() { - return "commentary"; - } - - @Override - public void init() { - putExtensionHandlerMapping(MappingMode.N, parseKeys("(CommentMotion)"), getOwner(), - new CommentMotionHandler(), false); - putExtensionHandlerMapping(MappingMode.N, parseKeys("(CommentLine)"), getOwner(), new CommentLineHandler(), - false); - putExtensionHandlerMapping(MappingMode.XO, parseKeys("(CommentMotionV)"), getOwner(), - new CommentMotionVHandler(), false); - - putKeyMappingIfMissing(MappingMode.N, parseKeys("gc"), getOwner(), parseKeys("(CommentMotion)"), true); - putKeyMappingIfMissing(MappingMode.N, parseKeys("gcc"), getOwner(), parseKeys("(CommentLine)"), true); - putKeyMappingIfMissing(MappingMode.XO, parseKeys("gc"), getOwner(), parseKeys("(CommentMotionV)"), true); - } - - private static class CommentMotionHandler implements VimExtensionHandler { - @Override - public boolean isRepeatable() { - return true; - } - - @Override - public void execute(@NotNull Editor editor, @NotNull DataContext context) { - setOperatorFunction(new Operator()); - executeNormalWithoutMapping(parseKeys("g@"), editor); - } - } - - private static class CommentMotionVHandler implements VimExtensionHandler { - @Override - public void execute(@NotNull Editor editor, @NotNull DataContext context) { - if (!editor.getCaretModel().getPrimaryCaret().hasSelection()) { - return; - } - - // always use line-wise comments - if (!new Operator().apply(editor, context, SelectionType.LINE_WISE)) { - return; - } - - WriteAction.run(() -> { - // Leave visual mode - executeNormalWithoutMapping(parseKeys(""), editor); - editor.getCaretModel().moveToOffset(editor.getCaretModel().getPrimaryCaret().getSelectionStart()); - }); - } - } - - private static class Operator implements OperatorFunction { - @Override - public boolean apply(@NotNull Editor editor, @NotNull DataContext context, @NotNull SelectionType selectionType) { - final TextRange range = getCommentRange(editor); - if (range == null) return false; - - if (CommandState.getInstance(new IjVimEditor(editor)).getMode() != CommandState.Mode.VISUAL) { - editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); - } - - final MultiCaretCodeInsightActionHandler handler = selectionType == SelectionType.CHARACTER_WISE - ? new CommentByBlockCommentHandler() - : new CommentByLineCommentHandler(); - - return WriteAction.compute(() -> { - try { - Project proj = editor.getProject(); - if (proj == null) return false; - - PsiFile file = PsiDocumentManager.getInstance(proj).getPsiFile(editor.getDocument()); - if (file == null) return false; - - handler.invoke(editor.getProject(), editor, editor.getCaretModel().getCurrentCaret(), file); - handler.postInvoke(); - - // Jump back to start if in block mode - if (selectionType == SelectionType.CHARACTER_WISE) { - executeNormalWithoutMapping(parseKeys("`["), editor); - } - return true; - } - finally { - // remove the selection - editor.getSelectionModel().removeSelection(); - } - }); - } - - private @Nullable TextRange getCommentRange(@NotNull Editor editor) { - final CommandState.Mode mode = CommandState.getInstance(new IjVimEditor(editor)).getMode(); - switch (mode) { - case COMMAND: - return VimPlugin.getMark().getChangeMarks(new IjVimEditor(editor)); - case VISUAL: - Caret primaryCaret = editor.getCaretModel().getPrimaryCaret(); - return new TextRange(primaryCaret.getSelectionStart(), primaryCaret.getSelectionEnd()); - default: - return null; - } - } - } - - private static class CommentLineHandler implements VimExtensionHandler { - @Override - public boolean isRepeatable() { - return true; - } - - @Override - public void execute(@NotNull Editor editor, @NotNull DataContext context) { - final int offset = editor.getCaretModel().getOffset(); - final int line = editor.getDocument().getLineNumber(offset); - final int lineStart = editor.getDocument().getLineStartOffset(line); - final int lineEnd = editor.getDocument().getLineEndOffset(line); - VimPlugin.getMark().setChangeMarks(new IjVimEditor(editor), new TextRange(lineStart, lineEnd)); - new Operator().apply(editor, context, SelectionType.LINE_WISE); - } - } -} diff --git a/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt b/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt new file mode 100644 index 0000000000..3f7a72bffe --- /dev/null +++ b/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt @@ -0,0 +1,222 @@ +/* + * 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 . + */ +package com.maddyhome.idea.vim.extension.commentary + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.util.PsiTreeUtil +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 +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.CommandFlags +import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.command.SelectionType +import com.maddyhome.idea.vim.command.TextObjectVisualType +import com.maddyhome.idea.vim.common.CommandAliasHandler +import com.maddyhome.idea.vim.common.MappingMode +import com.maddyhome.idea.vim.common.TextRange +import com.maddyhome.idea.vim.ex.ranges.Ranges +import com.maddyhome.idea.vim.extension.VimExtension +import com.maddyhome.idea.vim.extension.VimExtensionFacade.addCommand +import com.maddyhome.idea.vim.extension.VimExtensionFacade.executeNormalWithoutMapping +import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping +import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping +import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing +import com.maddyhome.idea.vim.extension.VimExtensionFacade.setOperatorFunction +import com.maddyhome.idea.vim.extension.VimExtensionHandler +import com.maddyhome.idea.vim.handler.TextObjectActionHandler +import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.PsiHelper +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.commandState +import com.maddyhome.idea.vim.key.OperatorFunction +import com.maddyhome.idea.vim.newapi.IjVimEditor +import com.maddyhome.idea.vim.newapi.ij +import com.maddyhome.idea.vim.newapi.vim +import java.util.* + +class CommentaryExtension : VimExtension { + + companion object { + fun doCommentary(editor: VimEditor, context: ExecutionContext, range: TextRange, selectionType: SelectionType, resetCaret: Boolean): Boolean { + val mode = editor.commandState.mode + if (mode !== CommandState.Mode.VISUAL) { + editor.ij.selectionModel.setSelection(range.startOffset, range.endOffset) + } + + return runWriteAction { + try { + // Treat block- and character-wise selections as block comments. Be ready to fall back to if the first action + // isn't available + val actions = if (selectionType === SelectionType.LINE_WISE) { + listOf(IdeActions.ACTION_COMMENT_LINE, IdeActions.ACTION_COMMENT_BLOCK) + } else { + listOf(IdeActions.ACTION_COMMENT_BLOCK, IdeActions.ACTION_COMMENT_LINE) + } + + injector.actionExecutor.executeAction(actions[0], context) + || injector.actionExecutor.executeAction(actions[1], context) + } finally { + // Remove the selection, if we added it + if (mode !== CommandState.Mode.VISUAL) { + editor.removeSelection() + } + + // Put the caret back at the start of the range, as though it was moved by the operator's motion argument. + // This is what Vim does. If IntelliJ is configured to add comments at the start of the line, this might put + // the caret in the "wrong" place. E.g. gc_ should put the caret on the first non-whitespace character. This + // is calculated by the motion, saved in the marks, and then we insert the comment. If it's inserted at the + // first non-whitespace character, then the caret is in the right place. If it's inserted at the first column, + // then the caret is now in a bit of a weird place. We can't detect this scenario, so we just have to accept + // the difference + if (resetCaret) { + editor.primaryCaret().moveToOffset(range.startOffset) + } + } + } + } + } + + override fun getName() = "commentary" + + override fun init() { + val plugCommentaryKeys = parseKeys("Commentary") + val plugCommentaryLineKeys = parseKeys("CommentaryLine") + putExtensionHandlerMapping(MappingMode.NX, plugCommentaryKeys, owner, CommentaryOperatorHandler(), false) + putExtensionHandlerMapping(MappingMode.O, plugCommentaryKeys, owner, CommentaryTextObjectMotionHandler(), false) + putKeyMappingIfMissing(MappingMode.N, plugCommentaryLineKeys, owner, parseKeys("gc_"), true) + + putKeyMappingIfMissing(MappingMode.NXO, parseKeys("gc"), owner, plugCommentaryKeys, true) + putKeyMappingIfMissing(MappingMode.N, parseKeys("gcc"), owner, plugCommentaryLineKeys, true) + putKeyMappingIfMissing(MappingMode.N, parseKeys("gcu"), owner, parseKeys("CommentaryCommentary"), true) + + // Previous versions of IdeaVim used different mappings to Vim's Commentary. Make sure everything works if someone + // is still using the old mapping + putKeyMapping(MappingMode.N, parseKeys("(CommentMotion)"), owner, plugCommentaryKeys, true) + putKeyMapping(MappingMode.XO, parseKeys("(CommentMotionV)"), owner, plugCommentaryKeys, true) + putKeyMapping(MappingMode.N, parseKeys("(CommentLine)"), owner, plugCommentaryLineKeys, true) + + addCommand("Commentary", CommentaryCommandAliasHandler()) + } + + /** + * Sets up the operator, pending a motion + * + * E.g. handles the `gc` in `gc_`, by setting the operator function, then invoking `g@` to receive the `_` motion to + * invoke the operator. This object is both the mapping handler and the operator function. + */ + private class CommentaryOperatorHandler : OperatorFunction, VimExtensionHandler { + override fun isRepeatable() = true + + override fun execute(editor: Editor, context: DataContext) { + setOperatorFunction(this) + executeNormalWithoutMapping(parseKeys("g@"), editor) + } + + override fun apply(editor: Editor, context: DataContext, selectionType: SelectionType): Boolean { + val range = VimPlugin.getMark().getChangeMarks(editor.vim) ?: return false + return doCommentary(editor.vim, context.vim, range, selectionType, true) + } + } + + /** + * The text object handler that provides the motion in e.g. `dgc` + * + * This object is both the `Commentary` mapping handler and the text object handler + */ + private class CommentaryTextObjectMotionHandler: TextObjectActionHandler(), VimExtensionHandler { + override fun isRepeatable() = true + + override fun execute(editor: Editor, context: DataContext) { + val commandState = editor.vim.commandState + val count = maxOf(1, commandState.commandBuilder.count) + + val textObjectHandler = this + commandState.commandBuilder.completeCommandPart(Argument(Command(count, textObjectHandler, Command.Type.MOTION, + EnumSet.noneOf(CommandFlags::class.java)))) + } + + override val visualType: TextObjectVisualType = TextObjectVisualType.LINE_WISE + + override fun getRange( + editor: VimEditor, + caret: VimCaret, + context: ExecutionContext, + count: Int, + rawCount: Int, + argument: Argument? + ): TextRange? { + + val nativeEditor = (editor as IjVimEditor).editor + val file = PsiHelper.getFile(nativeEditor) ?: return null + val lastLine = editor.lineCount() + + var startLine = caret.getLogicalPosition().line + while (startLine > 0 && isCommentLine(file, nativeEditor, startLine - 1)) startLine-- + var endLine = caret.getLogicalPosition().line - 1 + while (endLine < lastLine && isCommentLine(file, nativeEditor, endLine + 1)) endLine++ + + if (startLine <= endLine) { + val startOffset = EditorHelper.getLineStartOffset(nativeEditor, startLine) + val endOffset = EditorHelper.getLineStartOffset(nativeEditor, endLine + 1) + return TextRange(startOffset, endOffset) + } + + return null + } + + // Check all leaf nodes in the given line are whitespace, comments, or are owned by comments + private fun isCommentLine(file: PsiFile, editor: Editor, logicalLine: Int): Boolean { + val startOffset = EditorHelper.getLineStartOffset(editor, logicalLine) + val endOffset = EditorHelper.getLineEndOffset(editor, logicalLine, true) + val startElement = file.findElementAt(startOffset) ?: return false + var next: PsiElement? = startElement + while (next != null && next.textRange.startOffset <= endOffset) { + if (next !is PsiWhiteSpace && !isComment(next)) + return false + next = PsiTreeUtil.nextLeaf(next, true) + } + + return true + } + + private fun isComment(element: PsiElement) = + PsiTreeUtil.getParentOfType(element, PsiComment::class.java, false) != null + } + + /** + * The handler for the `Commentary` user defined command + * + * Used like `:1,3Commentary` or `g/fun/Commentary` + */ + private class CommentaryCommandAliasHandler: CommandAliasHandler { + override fun execute(command:String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) { + doCommentary(editor, context, ranges.getTextRange(editor, -1), SelectionType.LINE_WISE, false) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt b/src/main/java/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt index ee3d7fabc1..23d93dab2e 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt @@ -49,7 +49,6 @@ import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.subMode import com.maddyhome.idea.vim.key.OperatorFunction import com.maddyhome.idea.vim.mark.Mark -import com.maddyhome.idea.vim.mark.VimMark import com.maddyhome.idea.vim.mark.VimMarkConstants import com.maddyhome.idea.vim.newapi.vim import org.jetbrains.annotations.NonNls @@ -313,13 +312,6 @@ class VimExchangeExtension : VimExtension { } private fun getExchange(editor: Editor, isVisual: Boolean, selectionType: SelectionType): Exchange { - fun getEndCol(selectionEnd: Mark, type: CommandState.SubMode): Int { - return if (type == CommandState.SubMode.VISUAL_LINE) { - EditorHelper.getLineLength(editor, selectionEnd.logicalLine) - } else { - selectionEnd.col - } - } // TODO: improve KeyStroke list to sting conversion fun getRegisterText(reg: Char): String = getRegister(reg)?.map { it.keyChar }?.joinToString("") ?: "" @@ -338,22 +330,13 @@ class VimExchangeExtension : VimExtension { val starRegText = getRegister('*') val plusRegText = getRegister('+') - var (selectionStart, selectionEnd) = getMarks(isVisual) + val (selectionStart, selectionEnd) = getMarks(isVisual) if (isVisual) { executeNormalWithoutMapping(parseKeys("gvy"), editor) // TODO: handle // if &selection ==# 'exclusive' && start != end // let end.column -= len(matchstr(@@, '\_.$')) } else { - selectionEnd = selectionEnd.let { - VimMark.create( - it.key, - it.logicalLine, - getEndCol(it, selectionType.toSubMode()), - it.filename, - it.protocol - )!! - } when (selectionType) { SelectionType.LINE_WISE -> executeNormalWithoutMapping(stringToKeys("`[V`]y"), editor) SelectionType.BLOCK_WISE -> executeNormalWithoutMapping(stringToKeys("""`[`]y"""), editor) diff --git a/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt b/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt index 03b0a67ec6..796727fd5a 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt @@ -23,10 +23,8 @@ import com.intellij.ide.projectView.impl.ProjectViewImpl import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project @@ -41,6 +39,8 @@ import com.intellij.ui.TreeExpandCollapse import com.intellij.ui.speedSearch.SpeedSearchSupply import com.intellij.util.ui.tree.TreeUtil import com.maddyhome.idea.vim.VimPlugin +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.common.CommandAlias import com.maddyhome.idea.vim.common.CommandAliasHandler @@ -49,6 +49,7 @@ import com.maddyhome.idea.vim.common.CommandPartNode import com.maddyhome.idea.vim.common.Node import com.maddyhome.idea.vim.common.RootNode import com.maddyhome.idea.vim.common.addLeafs +import com.maddyhome.idea.vim.ex.ranges.Ranges import com.maddyhome.idea.vim.extension.VimExtension import com.maddyhome.idea.vim.group.KeyGroup import com.maddyhome.idea.vim.helper.MessageHelper @@ -56,6 +57,7 @@ import com.maddyhome.idea.vim.helper.StringHelper import com.maddyhome.idea.vim.helper.runAfterGotFocus import com.maddyhome.idea.vim.key.MappingOwner import com.maddyhome.idea.vim.key.RequiredShortcut +import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString import java.awt.event.KeyEvent @@ -143,14 +145,14 @@ class NerdTree : VimExtension { } class IjCommandHandler(private val actionId: String) : CommandAliasHandler { - override fun execute(editor: Editor, context: DataContext) { + override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) { callAction(actionId, context) } } class ToggleHandler : CommandAliasHandler { - override fun execute(editor: Editor, context: DataContext) { - val project = editor.project ?: return + override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) { + val project = editor.ij.project ?: return val toolWindow = ToolWindowManagerEx.getInstanceEx(project).getToolWindow(ToolWindowId.PROJECT_VIEW) ?: return if (toolWindow.isVisible) { toolWindow.hide() @@ -161,8 +163,8 @@ class NerdTree : VimExtension { } class CloseHandler : CommandAliasHandler { - override fun execute(editor: Editor, context: DataContext) { - val project = editor.project ?: return + override fun execute(command: String, ranges: Ranges, editor: VimEditor, context: ExecutionContext) { + val project = editor.ij.project ?: return val toolWindow = ToolWindowManagerEx.getInstanceEx(project).getToolWindow(ToolWindowId.PROJECT_VIEW) ?: return if (toolWindow.isVisible) { toolWindow.hide() @@ -217,7 +219,7 @@ class NerdTree : VimExtension { val action = nextNode.actionHolder when (action) { - is NerdAction.ToIj -> callAction(action.name, e.dataContext) + is NerdAction.ToIj -> callAction(action.name, e.dataContext.vim) is NerdAction.Code -> e.project?.let { action.action(it, e.dataContext, e) } } } @@ -353,7 +355,7 @@ class NerdTree : VimExtension { currentWindow.split(SwingConstants.VERTICAL, true, file, true) // FIXME: 22.01.2021 This solution bouncing a bit - callAction("ActivateProjectToolWindow", context) + callAction("ActivateProjectToolWindow", context.vim) } ) registerCommand( @@ -364,7 +366,7 @@ class NerdTree : VimExtension { val currentWindow = splitters.currentWindow currentWindow.split(SwingConstants.HORIZONTAL, true, file, true) - callAction("ActivateProjectToolWindow", context) + callAction("ActivateProjectToolWindow", context.vim) } ) registerCommand( @@ -502,19 +504,19 @@ class NerdTree : VimExtension { private val LOG = logger() - fun callAction(name: String, context: DataContext) { + fun callAction(name: String, context: ExecutionContext) { val action = ActionManager.getInstance().getAction(name) ?: run { VimPlugin.showMessage(MessageHelper.message("action.not.found.0", name)) return } val application = ApplicationManager.getApplication() if (application.isUnitTestMode) { - injector.actionExecutor.executeAction(action.vim, context.vim) + injector.actionExecutor.executeAction(action.vim, context) } else { runAfterGotFocus { injector.actionExecutor.executeAction( action.vim, - context.vim, + context, ) } } diff --git a/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt b/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt index f9973a9100..c8070ba357 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt +++ b/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt @@ -63,8 +63,6 @@ fun Editor.exitVisualMode() { VimPlugin.getMark().setVisualSelectionMarks(this.vim, TextRange(vimSelectionStart, primaryCaret.offset)) this.caretModel.allCarets.forEach { it.vimSelectionStartClear() } - this.subMode = CommandState.SubMode.NONE - this.vim.commandState.popModes() } } diff --git a/src/main/java/com/maddyhome/idea/vim/key/MappingInfo.kt b/src/main/java/com/maddyhome/idea/vim/key/MappingInfo.kt index ef376144ba..5a27c13f2d 100644 --- a/src/main/java/com/maddyhome/idea/vim/key/MappingInfo.kt +++ b/src/main/java/com/maddyhome/idea/vim/key/MappingInfo.kt @@ -158,10 +158,6 @@ class ToHandlerMappingInfo( LOG.debug("Executing 'ToHandler' mapping info...") val commandState = CommandState.getInstance(editor) - // Cache isOperatorPending in case the extension changes the mode while moving the caret - // See CommonExtensionTest - // TODO: Is this legal? Should we assert in this case? - // Cache isOperatorPending in case the extension changes the mode while moving the caret // See CommonExtensionTest // TODO: Is this legal? Should we assert in this case? diff --git a/src/main/java/com/maddyhome/idea/vim/package-info.java b/src/main/java/com/maddyhome/idea/vim/package-info.java index 25643a6f2a..9e905518c8 100644 --- a/src/main/java/com/maddyhome/idea/vim/package-info.java +++ b/src/main/java/com/maddyhome/idea/vim/package-info.java @@ -558,6 +558,7 @@ * |v_gq| {@link com.maddyhome.idea.vim.action.change.change.ReformatCodeVisualAction} * |v_gv| {@link com.maddyhome.idea.vim.action.motion.visual.VisualSwapSelectionsAction} * |v_g`| {@link com.maddyhome.idea.vim.action.motion.mark.MotionGotoFileMarkNoSaveJumpAction} + * |v_g@| {@link com.maddyhome.idea.vim.action.change.VisualOperatorAction} * |v_iquote| {@link com.maddyhome.idea.vim.action.motion.object.MotionInnerBlockDoubleQuoteAction} * |v_i'| {@link com.maddyhome.idea.vim.action.motion.object.MotionInnerBlockSingleQuoteAction} * |v_i(| {@link com.maddyhome.idea.vim.action.motion.object.MotionInnerBlockParenAction} diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/UnknownCommand.kt b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/UnknownCommand.kt index 7ae90e18d5..70735285c3 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/UnknownCommand.kt +++ b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/UnknownCommand.kt @@ -27,7 +27,6 @@ import com.maddyhome.idea.vim.ex.InvalidCommandException import com.maddyhome.idea.vim.ex.ranges.Ranges import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.Msg -import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.vimscript.model.ExecutionResult import com.maddyhome.idea.vim.vimscript.model.commands.UnknownCommand.Constants.MAX_RECURSION import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser @@ -67,7 +66,7 @@ data class UnknownCommand(val ranges: Ranges, val name: String, val argument: St } } is GoalCommand.Call -> { - commandAlias.handler.execute(editor.ij, context.ij) + commandAlias.handler.execute(name, ranges, editor, context) return ExecutionResult.Success } } diff --git a/src/main/resources/META-INF/includes/VimActions.xml b/src/main/resources/META-INF/includes/VimActions.xml index 9d83c2c9f3..21fb24a8b7 100644 --- a/src/main/resources/META-INF/includes/VimActions.xml +++ b/src/main/resources/META-INF/includes/VimActions.xml @@ -326,6 +326,7 @@ + diff --git a/src/test/java/org/jetbrains/plugins/ideavim/extension/commentary/CommentaryExtensionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/extension/commentary/CommentaryExtensionTest.kt index dfc2f24271..28a35b7200 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/extension/commentary/CommentaryExtensionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/extension/commentary/CommentaryExtensionTest.kt @@ -1,6 +1,6 @@ /* * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform - * Copyright (C) 2003-2022 The IdeaVim authors + * Copyright (C) 2003-2021 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 @@ -19,57 +19,58 @@ package org.jetbrains.plugins.ideavim.extension.commentary import com.intellij.ide.highlighter.HtmlFileType +import com.intellij.ide.highlighter.JavaFileType import com.maddyhome.idea.vim.command.CommandState -import com.maddyhome.idea.vim.helper.StringHelper -import org.jetbrains.plugins.ideavim.JavaVimTestCase -import org.jetbrains.plugins.ideavim.VimTestCase.Companion.c +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import org.jetbrains.plugins.ideavim.VimTestCase +import org.jetbrains.yaml.YAMLFileType -/** - * @author dhleong - */ -class CommentaryExtensionTest : JavaVimTestCase() { +@Suppress("SpellCheckingInspection") +class CommentaryExtensionTest : VimTestCase() { override fun setUp() { super.setUp() enableExtensions("commentary") } - // |gc| |l| + // |gc| |l| + move caret fun testBlockCommentSingle() { doTest( - StringHelper.parseKeys("gcll"), + "gcll", "if (condition) {\n" + "}\n", - "/*i*/f (condition) {\n" + "}\n" + "/*i*/f (condition) {\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) - assertMode(CommandState.Mode.COMMAND) assertSelection(null) } // |gc| |iw| fun testBlockCommentInnerWord() { doTest( - StringHelper.parseKeys("gciw"), + "gciw", "if (condition) {\n" + "}\n", - "/*if*/ (condition) {\n" + "}\n" + "/*if*/ (condition) {\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) - assertMode(CommandState.Mode.COMMAND) assertSelection(null) } // |gc| |iw| fun testBlockCommentTillForward() { doTest( - StringHelper.parseKeys("gct{"), + "gct{", "if (condition) {\n" + "}\n", - "/*if (condition) */{\n" + "}\n" + "/*if (condition) */{\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } // |gc| |ab| fun testBlockCommentOuterParens() { doTest( - StringHelper.parseKeys("gcab"), + "gcab", "if (condition) {\n" + "}\n", - "if /*(condition)*/ {\n" + "}\n" + "if /*(condition)*/ {\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } @@ -80,120 +81,140 @@ class CommentaryExtensionTest : JavaVimTestCase() { // |gc| |j| fun testLineCommentDown() { doTest( - StringHelper.parseKeys("gcj"), + "gcj", "if (condition) {\n" + "}\n", - "//if (condition) {\n" + - "//}\n" + "//if (condition) {\n" + + "//}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun testLineCommentDownPreservesAbsoluteCaretLocation() { + doTest( + "gcj", + "if (condition) {\n" + "}\n", + "//if (condition) {\n" + + "//}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } // |gc| |ip| fun testLineCommentInnerParagraph() { doTest( - StringHelper.parseKeys("gcip"), + "gcip", "if (condition) {\n" + "}\n", "//if (condition) {\n" + - "//}\n" + "//}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } // |gc| |ip| fun testLineCommentSingleLineInnerParagraph() { doTest( - StringHelper.parseKeys("gcip"), + "gcip", "${c}if (condition) {}", - "//if (condition) {}" + "//if (condition) {}", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } /* Ensure uncommenting works as well */ // |gc| |ip| fun testLineUncommentInnerParagraph() { doTest( - StringHelper.parseKeys("gcip"), + "gcip", "//if (condition) {\n" + "//}\n", "if (condition) {\n" + - "}\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) - assertMode(CommandState.Mode.COMMAND) assertSelection(null) } // |gc| |ip| fun testLineUncommentSingleLineInnerParagraph() { doTest( - StringHelper.parseKeys("gcip"), + "gcip", "$c//if (condition) {}", - "if (condition) {}" + "if (condition) {}", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } /* Visual mode */ // |gc| |ip| fun testLineCommentVisualInnerParagraph() { doTest( - StringHelper.parseKeys("vipgc"), + "vipgc", "if (condition) {\n" + "}\n", "//if (condition) {\n" + - "//}\n" + "//}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } // |gc| |ip| fun testLineUncommentVisualInnerParagraph() { doTest( - StringHelper.parseKeys("vipgc"), + "vipgc", "//if (condition) {\n" + "//}\n", "if (condition) {\n" + - "}\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } /* Special shortcut gcc is always linewise */ // |gcc| fun testLineCommentShortcut() { doTest( - StringHelper.parseKeys("gccj"), + "gccj", "if (condition) {\n" + "}\n", "//if (condition) {\n" + - "}\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) - assertMode(CommandState.Mode.COMMAND) assertSelection(null) } // |gcc| - fun testLineCommentShortcutPreservesCaret() { + fun testLineCommentShortcutSetsCaretToMotionLocation() { doTest( - StringHelper.parseKeys("gcc"), + "gcc", "if (condition) {\n" + "}\n", - "//if (condition) {\n" + "}\n" + "//if (condition) {\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) - assertMode(CommandState.Mode.COMMAND) assertSelection(null) } // |gcc| fun testLineUncommentShortcut() { doTest( - StringHelper.parseKeys("gcc"), + "gcc", "//if (condition) {\n" + "}\n", "if (condition) {\n" + - "}\n" + "}\n", + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) - assertMode(CommandState.Mode.COMMAND) assertSelection(null) } // |gcc| fun testHTMLCommentShortcut() { - myFixture.configureByText(HtmlFileType.INSTANCE, "
") - typeText(StringHelper.parseKeys("gcc")) - myFixture.checkResult("") - assertMode(CommandState.Mode.COMMAND) + doTest( + "gcc", + "
", + "", + CommandState.Mode.COMMAND, + CommandState.SubMode.NONE, + HtmlFileType.INSTANCE + ) assertSelection(null) } fun `test comment motion repeat`() { doTest( - StringHelper.parseKeys("gcj", "jj."), + "gcj" + "jj.", """ if (condition) { } @@ -205,13 +226,14 @@ class CommentaryExtensionTest : JavaVimTestCase() { //} //if (condition) { //} - """.trimIndent() + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } fun `test comment motion right repeat`() { doTest( - StringHelper.parseKeys("gciw", "jj."), + "gciw" + "jj.", """ if (condition) { } @@ -223,13 +245,14 @@ class CommentaryExtensionTest : JavaVimTestCase() { } /*if*/ (condition) { } - """.trimIndent() + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE ) } fun `test comment line repeat`() { doTest( - StringHelper.parseKeys("gcc", "j."), + "gcc" + "j.", """ if (condition) { } @@ -237,7 +260,557 @@ class CommentaryExtensionTest : JavaVimTestCase() { """ //if (condition) { //} - """.trimIndent() + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + @VimBehaviorDiffers(description = "IntelliJ's uncomment leaves the leading whitespace") + fun `test uncomment with gcgc`() { + doTest( + "gcgc", + """ + // final Int value1 = 42; + // final Int value2 = 42; + // final Int value3 = 42; + final Int value4 = 42; + """.trimIndent(), + """ + final Int value1 = 42; + final Int value2 = 42; + final Int value3 = 42; + final Int value4 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + @VimBehaviorDiffers(description = "IntelliJ's uncomment leaves the leading whitespace") + fun `test uncomment with gcu`() { + doTest( + "gcu", + """ + // final Int value1 = 42; + // final Int value2 = 42; + // final Int value3 = 42; + final Int value4 = 42; + """.trimIndent(), + """ + final Int value1 = 42; + final Int value2 = 42; + final Int value3 = 42; + final Int value4 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test comment line with count`() { + // Caret position is kept as the position *before* the commenting. This is how Vim works + doTest( + "4gcc", + """ + final Int value1 = 42; + final Int value2 = 42; + final Int value3 = 42; + final Int value4 = 42; + final Int value5 = 42; + final Int value6 = 42; + """.trimIndent(), + """ + final Int value1 = 42; + //final Int value2 = 42; + //final Int value3 = 42; + //final Int value4 = 42; + //final Int value5 = 42; + final Int value6 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment`() { + doTest( + "dgc", + """ + // Comment 1 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes multiple line comments`() { + doTest( + "dgc", + """ + // Comment 1 + // Comment 2 + // Comment 3 + // Comment 4 + // Comment 5 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes multiple line comments 2`() { + doTest( + "dgc", + """ + // Comment 1 + // Comment 2 + // Comment 3 + // Comment 4 + // Comment 5 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment from leading whitespace`() { + doTest( + "dgc", + """ + // Comment 1 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment from leading whitespace 2`() { + doTest( + "dgc", + """ + + + // Comment 1 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment from leading whitespace 3`() { + doTest( + "dgc", + """ + final Int value1 = 42; + + + // Comment 1 + final Int value2 = 42; + """.trimIndent(), + """ + final Int value1 = 42; + final Int value2 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment from trailing whitespace`() { + doTest( + "dgc", + """ + + // Comment 1 + + + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comments separated by whitespace`() { + doTest( + "dgc", + """ + // Comment 1 + + // Comment 2 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes disjointed single line comments from whitespace`() { + doTest( + "dgc", + """ + // Comment 1 + + // Comment 2 + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment from current line`() { + doTest( + "dgc", + """ + // Comment + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes single line comment from current line 2`() { + doTest( + "dgc", + """ + // Comment + final Int value = 42; + final Int value2 = 42; + """.trimIndent(), + """ + final Int value = 42; + final Int value2 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object does not delete line with comment and text`() { + doTest( + "dgc", + """ + final Int value = 42; // Comment + """.trimIndent(), + """ + final Int value = 42; // Comment + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes block comment`() { + doTest( + "dgc", + """ + /* Comment 1 */ + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes multi-line block comment`() { + doTest( + "dgc", + """ + /* Comment 1 + * Comment 2 + * Comment 3 */ + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes adjoining multi-line block comments`() { + doTest( + "dgc", + """ + /* Comment 1 + * Comment 2 + * Comment 3 */ + /* Comment 1 + * Comment 2 + * Comment 3 */ + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes adjoining multi-line block comments 2`() { + doTest( + "dgc", + """ + /* Comment 1 + * Comment 2 + * Comment 3 */ + + /* Comment 1 + * Comment 2 + * Comment 3 */ + final Int value = 42; + """.trimIndent(), + """ + final Int value = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object does not delete line with text and block comment`() { + doTest( + "dgc", + """ + final Int value /* Block comment */ = 42; + """.trimIndent(), + """ + final Int value /* Block comment */ = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes JavaDoc comment`() { + doTest( + "dgc", + """ + /** + * Cool summary, dude + * @param value the value, innit + * @param name what's your name? + */ + public void something(int value, String name) { + } + """.trimIndent(), + """ + public void something(int value, String name) { + } + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes JavaDoc comment from leading whitespace`() { + doTest( + "dgc", + """ + + /** + * Cool summary, dude + * @param value the value, innit + * @param name what's your name? + */ + public void something(int value, String name) { + } + """.trimIndent(), + """ + public void something(int value, String name) { + } + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes JavaDoc comment and adjoining comments`() { + doTest( + "dgc", + """ + // This should be deleted too + /** + * Cool summary, dude + * @param value the value, innit + * @param name what's your name? + */ + public void something(int value, String name) { + } + """.trimIndent(), + """ + public void something(int value, String name) { + } + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test text object deletes JavaDoc comment and adjoining comments separated by whitespace`() { + doTest( + "dgc", + """ + // This should be deleted too + + /* Block comment */ + + /** + * Cool summary, dude + * @param value the value, innit + * @param name what's your name? + */ + public void something(int value, String name) { + } + """.trimIndent(), + """ + public void something(int value, String name) { + } + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test Commentary command comments current line`() { + doTest( + ":Commentary", + """ + final int var value1 = 42; + final int var value2 = 42; + final int var value3 = 42; + """.trimIndent(), + """ + final int var value1 = 42; + //final int var value2 = 42; + final int var value3 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test Commentary command comments simple line range`() { + doTest( + ":2Commentary", + """ + final int var value1 = 42; + final int var value2 = 42; + final int var value3 = 42; + """.trimIndent(), + """ + final int var value1 = 42; + //final int var value2 = 42; + final int var value3 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test Commentary command comments line range`() { + doTest( + ":1,3Commentary", + """ + final int var value1 = 42; + final int var value2 = 42; + final int var value3 = 42; + """.trimIndent(), + """ + //final int var value1 = 42; + //final int var value2 = 42; + //final int var value3 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + @VimBehaviorDiffers( + """ + //final int var value1 = 42; + //final int var value2 = 42; + //final int var value3 = 42; + """, + description = "Vim exits Visual mode before entering Command mode, and resets the caret to the start of the visual selection." + + "When executing the Commentary command, we don't move the caret, so it should be end up at the start of the visual selection." + + "Note that Escape exits Visual mode, but leaves the caret where it is", + shouldBeFixed = true + ) + fun `test Commentary command comments visual range`() { + doTest( + "Vjj" + ":Commentary", + """ + final int var value1 = 42; + final int var value2 = 42; + final int var value3 = 42; + """.trimIndent(), + """ + //final int var value1 = 42; + //final int var value2 = 42; + //final int var value3 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test Commentary command comments search range`() { + doTest( + ":g/value2/Commentary", + """ + final int var value1 = 42; + final int var value2 = 42; + final int var value3 = 42; + final int var value21 = 42; + final int var value22 = 42; + """.trimIndent(), + """ + final int var value1 = 42; + //final int var value2 = 42; + final int var value3 = 42; + //final int var value21 = 42; + //final int var value22 = 42; + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, JavaFileType.INSTANCE + ) + } + + fun `test block comment falls back to line comment when not available`() { + doTest( + "gcw", + """ + american: + - Boston Red Sox + - Detroit Tigers + - New York Yankees + national: + - New York Mets + - Chicago Cubs + - Atlanta Braves + """.trimIndent(), + """ + american: + #- Boston Red Sox + - Detroit Tigers + - New York Yankees + national: + - New York Mets + - Chicago Cubs + - Atlanta Braves + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE, YAMLFileType.YML ) } } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/extension/exchange/VimExchangeExtensionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/extension/exchange/VimExchangeExtensionTest.kt index 366cd107ee..502fd29f7d 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/extension/exchange/VimExchangeExtensionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/extension/exchange/VimExchangeExtensionTest.kt @@ -413,7 +413,10 @@ class VimExchangeExtensionTest : VimTestCase() { configureByText(before) typeText(StringHelper.parseKeys("cxj")) - assertHighlighter(0, 19, HighlighterTargetArea.LINES_IN_RANGE) + // Note that this is the range of the motion. The implementation will select the text with linewise 'V', so the + // correct text is yanked. The LINES_IN_RANGE means the highlight is drawn across the whole line + // so the correct text is + assertHighlighter(4, 14, HighlighterTargetArea.LINES_IN_RANGE) // Exit vim-exchange exitExchange() @@ -430,7 +433,9 @@ class VimExchangeExtensionTest : VimTestCase() { configureByText(before) typeText(StringHelper.parseKeys("cxx")) - assertHighlighter(0, 9, HighlighterTargetArea.LINES_IN_RANGE) + // Note that this is the range of the motion. The implementation will select the text with linewise 'V', so the + // correct text is yanked. The LINES_IN_RANGE means the highlight is drawn across the whole line + assertHighlighter(0, 4, HighlighterTargetArea.LINES_IN_RANGE) // Exit vim-exchange exitExchange()