diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorMultiCaretPerformanceTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorMultiCaretPerformanceTest.java index 0e104ae14bfd..6f7b6519e802 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorMultiCaretPerformanceTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorMultiCaretPerformanceTest.java @@ -18,6 +18,7 @@ package com.intellij.openapi.editor; import com.intellij.openapi.editor.impl.AbstractEditorTest; import com.intellij.openapi.util.text.StringUtil; import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.testFramework.TestFileType; public class EditorMultiCaretPerformanceTest extends AbstractEditorTest { public void testTyping() { @@ -50,4 +51,18 @@ public class EditorMultiCaretPerformanceTest extends AbstractEditorTest { }).attempts(1).assertTiming(); checkResultByText(StringUtil.repeat(' ' + StringUtil.repeat("a", charactersToType) + " \n", caretCount)); } + + public void testTypingInXml() { + int caretCount = 1000; + int charactersToType = 100; + String initialText = "\n" + StringUtil.repeat(" \n", caretCount) + ""; + init(initialText, TestFileType.XML); + PlatformTestUtil.startPerformanceTest("Typing in XML with large number of carets", 100_000, () -> { + for (int i = 0; i < charactersToType; i++) { + type('a'); + } + }).attempts(1).assertTiming(); + checkResultByText("\n" + StringUtil.repeat(" " + StringUtil.repeat("a", charactersToType) + "\n", caretCount) + + ""); + } } diff --git a/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java b/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java index c6787d88082c..bac3ac37a6c8 100644 --- a/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java +++ b/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java @@ -38,13 +38,12 @@ import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; import com.intellij.psi.templateLanguages.TemplateLanguage; import com.intellij.psi.xml.XmlTokenType; -import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; import com.intellij.xml.util.HtmlUtil; import com.intellij.xml.util.XmlUtil; import org.jetbrains.annotations.NotNull; -import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -73,12 +72,12 @@ public class XmlTagNameSynchronizer implements CommandListener { private void installSynchronizer(final Editor editor) { final Project project = editor.getProject(); - if (project == null) return; + if (project == null || !(editor instanceof EditorImpl)) return; final Document document = editor.getDocument(); final VirtualFile file = myFileDocumentManager.getFile(document); final Language language = findXmlLikeLanguage(project, file); - if (language != null) new TagNameSynchronizer(editor, project, language); + if (language != null) new TagNameSynchronizer((EditorImpl)editor, project, language); } private static Language findXmlLikeLanguage(Project project, VirtualFile file) { @@ -121,20 +120,17 @@ public class XmlTagNameSynchronizer implements CommandListener { } private static class TagNameSynchronizer implements DocumentListener { - public static final TagNameSynchronizer[] EMPTY = new TagNameSynchronizer[0]; + private static final Key> MARKERS_KEY = Key.create("tag.name.synchronizer.markers"); + private static final TagNameSynchronizer[] EMPTY = new TagNameSynchronizer[0]; private final PsiDocumentManagerBase myDocumentManager; private final Language myLanguage; + private final EditorImpl myEditor; + private boolean myApplying; - private enum State {INITIAL, TRACKING, APPLYING} - - private final Editor myEditor; - private State myState = State.INITIAL; - private final List> myMarkers = new SmartList<>(); - - private TagNameSynchronizer(Editor editor, Project project, Language language) { + private TagNameSynchronizer(EditorImpl editor, Project project, Language language) { myEditor = editor; myLanguage = language; - final Disposable disposable = ((EditorImpl)editor).getDisposable(); + final Disposable disposable = editor.getDisposable(); final Document document = editor.getDocument(); document.addDocumentListener(this, disposable); editor.putUserData(SYNCHRONIZER_KEY, this); @@ -146,7 +142,7 @@ public class XmlTagNameSynchronizer implements CommandListener { if (!WebEditorOptions.getInstance().isSyncTagEditing()) return; final Document document = event.getDocument(); - if (myState == State.APPLYING || UndoManager.getInstance(myEditor.getProject()).isUndoInProgress() || + if (myApplying || UndoManager.getInstance(Objects.requireNonNull(myEditor.getProject())).isUndoInProgress() || !PomModelImpl.isAllowPsiModification() || ((DocumentEx)document).isInBulkUpdate()) { return; } @@ -163,83 +159,55 @@ public class XmlTagNameSynchronizer implements CommandListener { return; } + Caret caret = myEditor.getCaretModel().getCurrentCaret(); + for (int i = 0; i < newLength; i++) { if (!XmlUtil.isValidTagNameChar(fragment.charAt(i))) { - clearMarkers(); + clearMarkers(caret); return; } } - if (myState == State.INITIAL) { + Couple markers = caret.getUserData(MARKERS_KEY); + if (markers != null && !fitsInMarker(markers, offset, oldLength)) { + clearMarkers(caret); + markers = null; + } + if (markers == null) { final PsiFile file = myDocumentManager.getPsiFile(document); if (file == null || myDocumentManager.getSynchronizer().isInSynchronization(document)) return; - final SmartList leaders = new SmartList<>(); - for (Caret caret : myEditor.getCaretModel().getAllCarets()) { - final RangeMarker leader = createTagNameMarker(caret); - if (leader == null) { - for (RangeMarker marker : leaders) { - marker.dispose(); - } - return; - } - leader.setGreedyToLeft(true); - leader.setGreedyToRight(true); - leaders.add(leader); - } - if (leaders.isEmpty()) return; + final RangeMarker leader = createTagNameMarker(caret); + if (leader == null) return; + leader.setGreedyToLeft(true); + leader.setGreedyToRight(true); if (myDocumentManager.isUncommited(document)) { myDocumentManager.commitDocument(document); } - for (RangeMarker leader : leaders) { - final RangeMarker support = findSupport(leader, file, document); - if (support == null) { - clearMarkers(); - return; - } - support.setGreedyToLeft(true); - support.setGreedyToRight(true); - myMarkers.add(Couple.of(leader, support)); - } - - if (!fitsInMarker(offset, oldLength)) { - clearMarkers(); - return; - } - - myState = State.TRACKING; - } - if (myMarkers.isEmpty()) return; - - boolean fitsInMarker = fitsInMarker(offset, oldLength); - if (!fitsInMarker || myMarkers.size() != myEditor.getCaretModel().getCaretCount()) { - clearMarkers(); - beforeDocumentChange(event); + final RangeMarker support = findSupport(leader, file, document); + if (support == null) return; + support.setGreedyToLeft(true); + support.setGreedyToRight(true); + markers = Couple.of(leader, support); + if (!fitsInMarker(markers, offset, oldLength)) return; + caret.putUserData(MARKERS_KEY, markers); } } - public boolean fitsInMarker(int offset, int oldLength) { - boolean fitsInMarker = false; - for (Couple leaderAndSupport : myMarkers) { - final RangeMarker leader = leaderAndSupport.first; - if (!leader.isValid()) { - fitsInMarker = false; - break; - } - fitsInMarker |= offset >= leader.getStartOffset() && offset + oldLength <= leader.getEndOffset(); - } - return fitsInMarker; + private static boolean fitsInMarker(Couple markers, int offset, int oldLength) { + RangeMarker leader = markers.first; + return leader.isValid() && offset >= leader.getStartOffset() && offset + oldLength <= leader.getEndOffset(); } - public void clearMarkers() { - for (Couple leaderAndSupport : myMarkers) { - leaderAndSupport.first.dispose(); - leaderAndSupport.second.dispose(); + private static void clearMarkers(Caret caret) { + Couple markers = caret.getUserData(MARKERS_KEY); + if (markers != null) { + markers.first.dispose(); + markers.second.dispose(); + caret.putUserData(MARKERS_KEY, null); } - myMarkers.clear(); - myState = State.INITIAL; } private RangeMarker createTagNameMarker(Caret caret) { @@ -278,15 +246,13 @@ public class XmlTagNameSynchronizer implements CommandListener { } public void beforeCommandFinished() { - if (myMarkers.isEmpty()) return; - - myState = State.APPLYING; - - final Document document = myEditor.getDocument(); - final Runnable apply = () -> { - for (Couple couple : myMarkers) { - final RangeMarker leader = couple.first; - final RangeMarker support = couple.second; + CaretAction action = caret -> { + Couple markers = caret.getUserData(MARKERS_KEY); + if (markers == null || !markers.first.isValid() || !markers.second.isValid()) return; + final Document document = myEditor.getDocument(); + final Runnable apply = () -> { + final RangeMarker leader = markers.first; + final RangeMarker support = markers.second; if (document.getTextLength() < leader.getEndOffset()) { return; } @@ -295,19 +261,29 @@ public class XmlTagNameSynchronizer implements CommandListener { !name.equals(document.getText(new TextRange(support.getStartOffset(), support.getEndOffset())))) { document.replaceString(support.getStartOffset(), support.getEndOffset(), name); } - } + }; + ApplicationManager.getApplication().runWriteAction(() -> { + final LookupImpl lookup = (LookupImpl)LookupManager.getActiveLookup(myEditor); + if (lookup != null) { + lookup.performGuardedChange(apply); + } + else { + apply.run(); + } + }); }; - ApplicationManager.getApplication().runWriteAction(() -> { - final LookupImpl lookup = (LookupImpl)LookupManager.getActiveLookup(myEditor); - if (lookup != null) { - lookup.performGuardedChange(apply); + myApplying = true; + try { + if (myEditor.getCaretModel().isIteratingOverCarets()) { + action.perform(myEditor.getCaretModel().getCurrentCaret()); } else { - apply.run(); + myEditor.getCaretModel().runForEachCaret(action); } - }); - - myState = State.TRACKING; + } + finally { + myApplying = false; + } } private RangeMarker findSupport(RangeMarker leader, PsiFile file, Document document) {