mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
43db2dec54
commit
44239585c3
+15
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user