diff --git a/java/java-tests/testData/codeInsight/completion/normal/MulticaretTyping.java b/java/java-tests/testData/codeInsight/completion/normal/MulticaretTyping.java new file mode 100644 index 000000000000..00d3186f4ef2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/completion/normal/MulticaretTyping.java @@ -0,0 +1,4 @@ +class Foo {{ + System.out.p + System.out.p +}} diff --git a/java/java-tests/testData/codeInsight/completion/normal/MulticaretTyping_after.java b/java/java-tests/testData/codeInsight/completion/normal/MulticaretTyping_after.java new file mode 100644 index 000000000000..87debd854955 --- /dev/null +++ b/java/java-tests/testData/codeInsight/completion/normal/MulticaretTyping_after.java @@ -0,0 +1,4 @@ +class Foo {{ + System.out.append() + System.out.append() +}} diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/completion/JavaAutoPopupTest.groovy b/java/java-tests/testSrc/com/intellij/codeInsight/completion/JavaAutoPopupTest.groovy index 4e8961dbe192..0b12b544bbfd 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/completion/JavaAutoPopupTest.groovy +++ b/java/java-tests/testSrc/com/intellij/codeInsight/completion/JavaAutoPopupTest.groovy @@ -52,6 +52,7 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiJavaFile import com.intellij.psi.statistics.StatisticsManager import com.intellij.psi.statistics.impl.StatisticsManagerImpl +import com.intellij.testFramework.EditorTestUtil import com.intellij.util.containers.ContainerUtil import org.jetbrains.annotations.NotNull @@ -1141,6 +1142,56 @@ class Foo {{ }}''' } + public void testMulticaret() { + doTestMulticaret """ +class Foo {{ + tx; + tx; +}}""", '\n', ''' +class Foo {{ + toString()x; + toString()x; +}}''' + } + + public void testMulticaretTab() { + doTestMulticaret """ +class Foo {{ + tx; + tx; +}}""", '\t', ''' +class Foo {{ + toString(); + toString(); +}}''' + } + + public void testMulticaretBackspace() { + doTestMulticaret """ +class Foo {{ + t; + t; +}}""", '\b\t', ''' +class Foo {{ + toString(); + toString(); +}}''' + } + + private doTestMulticaret(final String textBefore, final String toType, final String textAfter) { + EditorTestUtil.enableMultipleCarets() + try { + myFixture.configureByText "a.java", textBefore + type 'toStr' + assert lookup + type toType + myFixture.checkResult textAfter + } + finally { + EditorTestUtil.disableMultipleCarets() + } + } + private doTestBlockSelection(final String textBefore, final String toType, final String textAfter) { myFixture.configureByText "a.java", textBefore edt { diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/completion/NormalCompletionTest.groovy b/java/java-tests/testSrc/com/intellij/codeInsight/completion/NormalCompletionTest.groovy index f07a9635da0c..31a146b70531 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/completion/NormalCompletionTest.groovy +++ b/java/java-tests/testSrc/com/intellij/codeInsight/completion/NormalCompletionTest.groovy @@ -1403,6 +1403,21 @@ class Foo {{ } } + public void testMulticaretTyping() { + EditorTestUtil.enableMultipleCarets() + try { + configure() + assert lookup + type('p') + assert lookup + type('\n') + checkResult() + } + finally { + EditorTestUtil.disableMultipleCarets() + } + } + public void "test complete lowercase class name"() { myFixture.addClass("package foo; public class myClass {}") myFixture.configureByText "a.java", """ diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java index 216227d7744c..767256f2fb99 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java @@ -135,10 +135,7 @@ public class TypedHandler extends TypedActionHandlerBase { Project project = CommonDataKeys.PROJECT.getData(dataContext); PsiFile file; - if (project == null - || editor.isColumnMode() - || editor.getCaretModel().getAllCarets().size() > 1 - || (file = PsiUtilBase.getPsiFileInEditor(editor, project)) == null) { + if (project == null || editor.isColumnMode() || (file = PsiUtilBase.getPsiFileInEditor(editor, project)) == null) { if (myOriginalHandler != null){ myOriginalHandler.execute(editor, charTyped, dataContext); } @@ -174,13 +171,13 @@ public class TypedHandler extends TypedActionHandlerBase { } if (!editor.isInsertMode()){ - myOriginalHandler.execute(originalEditor, charTyped, dataContext); + if (myOriginalHandler != null) { + myOriginalHandler.execute(originalEditor, charTyped, dataContext); + } return; } - if (editor.getSelectionModel().hasSelection()){ - EditorModificationUtil.deleteSelectedText(editor); - } + EditorModificationUtil.deleteSelectedTextForAllCarets(editor); FileType fileType = getFileType(file, editor); @@ -194,7 +191,7 @@ public class TypedHandler extends TypedActionHandlerBase { } } - if (!editor.getSelectionModel().hasBlockSelection()) { + if (!editor.getSelectionModel().hasBlockSelection() && editor.getCaretModel().getAllCarets().size() == 1) { if (')' == charTyped || ']' == charTyped || '}' == charTyped) { if (FileTypes.PLAIN_TEXT != fileType) { if (handleRParen(editor, fileType, charTyped)) return; @@ -206,12 +203,14 @@ public class TypedHandler extends TypedActionHandlerBase { } long modificationStampBeforeTyping = editor.getDocument().getModificationStamp(); - myOriginalHandler.execute(originalEditor, charTyped, dataContext); + if (myOriginalHandler != null) { + myOriginalHandler.execute(originalEditor, charTyped, dataContext); + } AutoHardWrapHandler.getInstance().wrapLineIfNecessary(editor, dataContext, modificationStampBeforeTyping); if (('(' == charTyped || '[' == charTyped || '{' == charTyped) && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET && - !editor.getSelectionModel().hasBlockSelection() && fileType != FileTypes.PLAIN_TEXT) { + !editor.getSelectionModel().hasBlockSelection() && editor.getCaretModel().getAllCarets().size() == 1 && fileType != FileTypes.PLAIN_TEXT) { handleAfterLParen(editor, fileType, charTyped); } else if ('}' == charTyped) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupTypedHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupTypedHandler.java index be95eed73160..9d8c67938907 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupTypedHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupTypedHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2014 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ public class LookupTypedHandler extends TypedHandlerDelegate { if (!lookup.performGuardedChange(new Runnable() { @Override public void run() { - EditorModificationUtil.typeInStringAtCaretHonorBlockSelection(editor, String.valueOf(charTyped), true); + EditorModificationUtil.typeInStringAtCaretHonorMultipleCarets(editor, String.valueOf(charTyped), true); } })) { return Result.STOP; diff --git a/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java b/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java index cf674d6abe72..cf36d1831b53 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java @@ -25,6 +25,7 @@ import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.psi.PsiDocumentManager; import com.intellij.util.Producer; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.datatransfer.DataFlavor; @@ -51,6 +52,15 @@ public class EditorModificationUtil { editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); } + public static void deleteSelectedTextForAllCarets(@NotNull final Editor editor) { + editor.getCaretModel().runForEachCaret(new CaretAction() { + @Override + public void perform(Caret caret) { + deleteSelectedText(editor); + } + }); + } + public static void deleteBlockSelection(Editor editor) { SelectionModel selectionModel = editor.getSelectionModel(); if (!selectionModel.hasBlockSelection()) return; @@ -81,11 +91,15 @@ public class EditorModificationUtil { editor.getSelectionModel().setBlockSelection(new LogicalPosition(startLine, caretColumn), new LogicalPosition(endLine, caretColumn)); } - public static void insertStringAtCaret(Editor editor, String s) { + public static void insertStringAtCaret(Editor editor, @NotNull String s) { insertStringAtCaret(editor, s, false, true); } - public static int insertStringAtCaret(Editor editor, String s, boolean toProcessOverwriteMode, boolean toMoveCaret) { + public static int insertStringAtCaret(Editor editor, @NotNull String s, boolean toProcessOverwriteMode, boolean toMoveCaret) { + return insertStringAtCaret(editor, s, toProcessOverwriteMode, toMoveCaret, s.length()); + } + + public static int insertStringAtCaret(Editor editor, @NotNull String s, boolean toProcessOverwriteMode, boolean toMoveCaret, int caretShift) { final SelectionModel selectionModel = editor.getSelectionModel(); if (selectionModel.hasSelection()) { editor.getCaretModel().moveToOffset(selectionModel.getSelectionStart(), true); @@ -120,7 +134,7 @@ public class EditorModificationUtil { document.replaceString(oldOffset, Math.min(endOffset, oldOffset + s.length()), s); } - int offset = oldOffset + s.length(); + int offset = oldOffset + filler.length() + caretShift; if (toMoveCaret){ editor.getCaretModel().moveToOffset(offset, true); editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); @@ -365,6 +379,62 @@ public class EditorModificationUtil { } } + public static void typeInStringAtCaretHonorMultipleCarets(final Editor editor, @NotNull final String str, final boolean toProcessOverwriteMode) { + typeInStringAtCaretHonorMultipleCarets(editor, str, toProcessOverwriteMode, str.length()); + } + + /** + * Inserts given string at each caret's position. Effective caret shift will be equal to caretShift for each caret. + */ + public static void typeInStringAtCaretHonorMultipleCarets(final Editor editor, @NotNull final String str, final boolean toProcessOverwriteMode, final int caretShift) + throws ReadOnlyFragmentModificationException + { + Document doc = editor.getDocument(); + final SelectionModel selectionModel = editor.getSelectionModel(); + if (selectionModel.hasBlockSelection()) { + RangeMarker guard = selectionModel.getBlockSelectionGuard(); + if (guard != null) { + DocumentEvent evt = new MockDocumentEvent(doc, editor.getCaretModel().getOffset()); + ReadOnlyFragmentModificationException e = new ReadOnlyFragmentModificationException(evt, guard); + EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(doc).handle(e); + } + else { + final LogicalPosition start = selectionModel.getBlockStart(); + final LogicalPosition end = selectionModel.getBlockEnd(); + assert start != null; + assert end != null; + + int column = Math.min(start.column, end.column); + int startLine = Math.min(start.line, end.line); + int endLine = Math.max(start.line, end.line); + deleteBlockSelection(editor); + for (int i = startLine; i <= endLine; i++) { + editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(i, column)); + insertStringAtCaret(editor, str, toProcessOverwriteMode, true, caretShift); + } + selectionModel.setBlockSelection(new LogicalPosition(startLine, column + str.length()), + new LogicalPosition(endLine, column + str.length())); + } + } + else { + editor.getCaretModel().runForEachCaret(new CaretAction() { + @Override + public void perform(Caret caret) { + insertStringAtCaret(editor, str, toProcessOverwriteMode, true, caretShift); + } + }); + } + } + + public static void moveAllCaretsRelatively(@NotNull Editor editor, final int caretShift) { + editor.getCaretModel().runForEachCaret(new CaretAction() { + @Override + public void perform(Caret caret) { + caret.moveToOffset(caret.getOffset() + caretShift); + } + }); + } + /** @deprecated use {@link #pasteTransferable(Editor, Producer)} (to remove in IDEA 14) */ @SuppressWarnings("UnusedDeclaration") public static TextRange pasteFromClipboard(Editor editor) { diff --git a/platform/platform-api/src/com/intellij/openapi/editor/actionSystem/TypedAction.java b/platform/platform-api/src/com/intellij/openapi/editor/actionSystem/TypedAction.java index 28bc0f0510ed..1e49937e7765 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/actionSystem/TypedAction.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/actionSystem/TypedAction.java @@ -64,18 +64,7 @@ public class TypedAction { try { final String str = String.valueOf(charTyped); CommandProcessor.getInstance().setCurrentCommandName(EditorBundle.message("typing.in.editor.command.name")); - - if (editor.getCaretModel().getAllCarets().size() == 1) { // temporary fix for completion - going forward we shouldn't use this check - EditorModificationUtil.typeInStringAtCaretHonorBlockSelection(editor, str, true); - } - else { - editor.getCaretModel().runForEachCaret(new CaretAction() { - @Override - public void perform(Caret caret) { - EditorModificationUtil.typeInStringAtCaretHonorBlockSelection(editor, str, true); - } - }); - } + EditorModificationUtil.typeInStringAtCaretHonorMultipleCarets(editor, str, true); } catch (ReadOnlyFragmentModificationException e) { EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(doc).handle(e); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutAction.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutAction.java index 4fc378e566c6..dd54b1de2087 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutAction.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutAction.java @@ -53,12 +53,7 @@ public class CutAction extends EditorAction { }); } editor.getSelectionModel().copySelectionToClipboard(); - editor.getCaretModel().runForEachCaret(new CaretAction() { - @Override - public void perform(Caret caret) { - EditorModificationUtil.deleteSelectedText(editor); - } - }); + EditorModificationUtil.deleteSelectedTextForAllCarets(editor); } } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java b/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java index f30febe2988d..15b29c1c584a 100644 --- a/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java +++ b/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2014 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,8 +82,7 @@ public class PyMethodNameTypedHandler extends TypedHandlerDelegate { if (caretOffset == chars.length() || chars.charAt(caretOffset) != ':') { textToType += ':'; } - EditorModificationUtil.typeInStringAtCaretHonorBlockSelection(editor, textToType, true); - editor.getCaretModel().moveToOffset(offset + 1 + pname.length()); // right after param name + EditorModificationUtil.typeInStringAtCaretHonorMultipleCarets(editor, textToType, true, 1 + pname.length()); // right after param name return Result.STOP; } }