IDEA-211201 Typing with multiple carets is slow in XML files

get rid of N^2 editing operations time on the number of carets

GitOrigin-RevId: 6ad2c69da54f23ec01c1caafa235601e11edc18d
This commit is contained in:
Dmitry Batrak
2019-04-28 17:04:29 +03:00
committed by intellij-monorepo-bot
parent 43db2dec54
commit 44239585c3
2 changed files with 80 additions and 89 deletions
@@ -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) + "<caret> \n", caretCount));
}
public void testTypingInXml() {
int caretCount = 1000;
int charactersToType = 100;
String initialText = "<root>\n" + StringUtil.repeat(" <node><caret></node>\n", caretCount) + "</root>";
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("<root>\n" + StringUtil.repeat(" <node>" + StringUtil.repeat("a", charactersToType) + "<caret></node>\n", caretCount)
+ "</root>");
}
}
@@ -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<Couple<RangeMarker>> 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<Couple<RangeMarker>> 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<RangeMarker> 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<RangeMarker> 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<RangeMarker> 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<RangeMarker> markers, int offset, int oldLength) {
RangeMarker leader = markers.first;
return leader.isValid() && offset >= leader.getStartOffset() && offset + oldLength <= leader.getEndOffset();
}
public void clearMarkers() {
for (Couple<RangeMarker> leaderAndSupport : myMarkers) {
leaderAndSupport.first.dispose();
leaderAndSupport.second.dispose();
private static void clearMarkers(Caret caret) {
Couple<RangeMarker> 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<RangeMarker> couple : myMarkers) {
final RangeMarker leader = couple.first;
final RangeMarker support = couple.second;
CaretAction action = caret -> {
Couple<RangeMarker> 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) {