fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / command / impl / UndoManagerImpl.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.command.impl;
3
4 import com.intellij.ide.DataManager;
5 import com.intellij.idea.ActionsBundle;
6 import com.intellij.openapi.Disposable;
7 import com.intellij.openapi.actionSystem.CommonDataKeys;
8 import com.intellij.openapi.application.Application;
9 import com.intellij.openapi.application.ApplicationManager;
10 import com.intellij.openapi.command.CommandEvent;
11 import com.intellij.openapi.command.CommandListener;
12 import com.intellij.openapi.command.CommandProcessor;
13 import com.intellij.openapi.command.UndoConfirmationPolicy;
14 import com.intellij.openapi.command.undo.*;
15 import com.intellij.openapi.diagnostic.Logger;
16 import com.intellij.openapi.editor.Document;
17 import com.intellij.openapi.editor.Editor;
18 import com.intellij.openapi.editor.EditorFactory;
19 import com.intellij.openapi.fileEditor.*;
20 import com.intellij.openapi.fileEditor.impl.CurrentEditorProvider;
21 import com.intellij.openapi.fileEditor.impl.FocusBasedCurrentEditorProvider;
22 import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
23 import com.intellij.openapi.ide.CopyPasteManager;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.project.ex.ProjectEx;
26 import com.intellij.openapi.util.*;
27 import com.intellij.openapi.util.registry.Registry;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.openapi.wm.ex.WindowManagerEx;
31 import com.intellij.psi.ExternalChangeAction;
32 import com.intellij.util.ObjectUtils;
33 import com.intellij.util.messages.MessageBus;
34 import gnu.trove.THashSet;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37 import org.jetbrains.annotations.TestOnly;
38
39 import java.awt.*;
40 import java.util.List;
41 import java.util.*;
42
43 public final class UndoManagerImpl extends UndoManager implements Disposable {
44   private static final Logger LOG = Logger.getInstance(UndoManagerImpl.class);
45
46   @TestOnly
47   public static boolean ourNeverAskUser;
48
49   private static final int COMMANDS_TO_KEEP_LIVE_QUEUES = 100;
50   private static final int COMMAND_TO_RUN_COMPACT = 20;
51   private static final int FREE_QUEUES_LIMIT = 30;
52
53   @Nullable private final ProjectEx myProject;
54
55   private final NotNullLazyValue<List<UndoProvider>> myUndoProviders = new AtomicNotNullLazyValue<List<UndoProvider>>() {
56     @NotNull
57     @Override
58     protected List<UndoProvider> compute() {
59       List<UndoProvider> list = myProject == null
60                                 ? UndoProvider.EP_NAME.getExtensionList()
61                                 : UndoProvider.PROJECT_EP_NAME.getExtensionList(myProject);
62       for (UndoProvider undoProvider : list) {
63         if (undoProvider instanceof Disposable) {
64           Disposer.register(UndoManagerImpl.this, (Disposable)undoProvider);
65         }
66       }
67       return list;
68     }
69   };
70
71   private CurrentEditorProvider myEditorProvider;
72
73   private final UndoRedoStacksHolder myUndoStacksHolder = new UndoRedoStacksHolder(true);
74   private final UndoRedoStacksHolder myRedoStacksHolder = new UndoRedoStacksHolder(false);
75
76   private final CommandMerger myMerger;
77
78   private CommandMerger myCurrentMerger;
79   private Project myCurrentActionProject = DummyProject.getInstance();
80
81   private int myCommandTimestamp = 1;
82
83   private int myCommandLevel;
84   private enum OperationState { NONE, UNDO, REDO }
85   private OperationState myCurrentOperationState = OperationState.NONE;
86
87   private DocumentReference myOriginatorReference;
88
89   public static boolean isRefresh() {
90     return ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class);
91   }
92
93   public static int getGlobalUndoLimit() {
94     return Registry.intValue("undo.globalUndoLimit");
95   }
96
97   public static int getDocumentUndoLimit() {
98     return Registry.intValue("undo.documentUndoLimit");
99   }
100
101   public UndoManagerImpl() {
102     this(null);
103   }
104
105   public UndoManagerImpl(@Nullable Project project) {
106     myProject = (ProjectEx)project;
107     myMerger = new CommandMerger(this);
108
109     if (myProject != null && myProject.isDefault()) {
110       return;
111     }
112
113     myEditorProvider = new FocusBasedCurrentEditorProvider();
114
115     MessageBus messageBus = myProject == null ? ApplicationManager.getApplication().getMessageBus() : myProject.getMessageBus();
116     messageBus.connect(this).subscribe(CommandListener.TOPIC, new CommandListener() {
117       private boolean myStarted;
118
119       @Override
120       public void commandStarted(@NotNull CommandEvent event) {
121         if (myProject != null && myProject.isDisposed() || myStarted) return;
122         onCommandStarted(event.getProject(), event.getUndoConfirmationPolicy(), event.shouldRecordActionForOriginalDocument());
123       }
124
125       @Override
126       public void commandFinished(@NotNull CommandEvent event) {
127         if (myProject != null && myProject.isDisposed() || myStarted) return;
128         onCommandFinished(event.getProject(), event.getCommandName(), event.getCommandGroupId());
129       }
130
131       @Override
132       public void undoTransparentActionStarted() {
133         if (myProject != null && myProject.isDisposed()) return;
134         if (!isInsideCommand()) {
135           myStarted = true;
136           onCommandStarted(myProject, UndoConfirmationPolicy.DEFAULT, true);
137         }
138       }
139
140       @Override
141       public void undoTransparentActionFinished() {
142         if (myProject != null && myProject.isDisposed()) return;
143         if (myStarted) {
144           myStarted = false;
145           onCommandFinished(myProject, "", null);
146         }
147       }
148     });
149   }
150
151   @Nullable
152   public Project getProject() {
153     return myProject;
154   }
155
156   @Override
157   public void dispose() {
158   }
159
160   public boolean isActive() {
161     return Comparing.equal(myProject, myCurrentActionProject) || myProject == null && myCurrentActionProject.isDefault();
162   }
163
164   private boolean isInsideCommand() {
165     return myCommandLevel > 0;
166   }
167
168   private void onCommandStarted(final Project project, UndoConfirmationPolicy undoConfirmationPolicy, boolean recordOriginalReference) {
169     if (myCommandLevel == 0) {
170       for (UndoProvider undoProvider : myUndoProviders.getValue()) {
171         undoProvider.commandStarted(project);
172       }
173       myCurrentActionProject = project;
174     }
175
176     commandStarted(undoConfirmationPolicy, myProject == project && recordOriginalReference);
177
178     LOG.assertTrue(myCommandLevel == 0 || !(myCurrentActionProject instanceof DummyProject));
179   }
180
181   private void onCommandFinished(final Project project, final String commandName, final Object commandGroupId) {
182     commandFinished(commandName, commandGroupId);
183     if (myCommandLevel == 0) {
184       for (UndoProvider undoProvider : myUndoProviders.getValue()) {
185         undoProvider.commandFinished(project);
186       }
187       myCurrentActionProject = DummyProject.getInstance();
188     }
189     LOG.assertTrue(myCommandLevel == 0 || !(myCurrentActionProject instanceof DummyProject));
190   }
191
192   private void commandStarted(UndoConfirmationPolicy undoConfirmationPolicy, boolean recordOriginalReference) {
193     if (myCommandLevel == 0) {
194       myCurrentMerger = new CommandMerger(this, CommandProcessor.getInstance().isUndoTransparentActionInProgress());
195
196       if (recordOriginalReference && myProject != null) {
197         Editor editor = null;
198         final Application application = ApplicationManager.getApplication();
199         if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
200           editor = CommonDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext());
201         }
202         else {
203           Component component = WindowManagerEx.getInstanceEx().getFocusedComponent(myProject);
204           if (component != null) {
205             editor = CommonDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext(component));
206           }
207         }
208
209         if (editor != null) {
210           Document document = editor.getDocument();
211           VirtualFile file = FileDocumentManager.getInstance().getFile(document);
212           if (file != null && file.isValid()) {
213             myOriginatorReference = DocumentReferenceManager.getInstance().create(file);
214           }
215         }
216       }
217     }
218     LOG.assertTrue(myCurrentMerger != null, String.valueOf(myCommandLevel));
219     myCurrentMerger.setBeforeState(getCurrentState());
220     myCurrentMerger.mergeUndoConfirmationPolicy(undoConfirmationPolicy);
221
222     myCommandLevel++;
223
224   }
225
226   private void commandFinished(String commandName, Object groupId) {
227     if (myCommandLevel == 0) return; // possible if command listener was added within command
228     myCommandLevel--;
229     if (myCommandLevel > 0) return;
230
231     if (myProject != null && myCurrentMerger.hasActions() && !myCurrentMerger.isTransparent() && myCurrentMerger.isPhysical()) {
232       addFocusedDocumentAsAffected();
233     }
234     myOriginatorReference = null;
235
236     myCurrentMerger.setAfterState(getCurrentState());
237     myMerger.commandFinished(commandName, groupId, myCurrentMerger);
238
239     disposeCurrentMerger();
240   }
241
242   private void addFocusedDocumentAsAffected() {
243     if (myOriginatorReference == null || myCurrentMerger.hasChangesOf(myOriginatorReference, true)) return;
244
245     final DocumentReference[] refs = {myOriginatorReference};
246     myCurrentMerger.addAction(new MentionOnlyUndoableAction(refs));
247   }
248
249   private EditorAndState getCurrentState() {
250     FileEditor editor = myEditorProvider.getCurrentEditor();
251     if (editor == null) {
252       return null;
253     }
254     if (!editor.isValid()) {
255       return null;
256     }
257     return new EditorAndState(editor, editor.getState(FileEditorStateLevel.UNDO));
258   }
259
260   private void disposeCurrentMerger() {
261     LOG.assertTrue(myCommandLevel == 0);
262     if (myCurrentMerger != null) {
263       myCurrentMerger = null;
264     }
265   }
266
267   @Override
268   public void nonundoableActionPerformed(@NotNull final DocumentReference ref, final boolean isGlobal) {
269     ApplicationManager.getApplication().assertIsDispatchThread();
270     if (myProject != null && myProject.isDisposed()) return;
271     undoableActionPerformed(new NonUndoableAction(ref, isGlobal));
272   }
273
274   @Override
275   public void undoableActionPerformed(@NotNull UndoableAction action) {
276     ApplicationManager.getApplication().assertIsDispatchThread();
277     if (myProject != null && myProject.isDisposed()) return;
278
279     if (myCurrentOperationState != OperationState.NONE) return;
280
281     if (myCommandLevel == 0) {
282       LOG.assertTrue(action instanceof NonUndoableAction,
283                      "Undoable actions allowed inside commands only (see com.intellij.openapi.command.CommandProcessor.executeCommand())");
284       commandStarted(UndoConfirmationPolicy.DEFAULT, false);
285       myCurrentMerger.addAction(action);
286       commandFinished("", null);
287       return;
288     }
289
290     if (isRefresh()) myOriginatorReference = null;
291
292     myCurrentMerger.addAction(action);
293   }
294
295   public void markCurrentCommandAsGlobal() {
296     myCurrentMerger.markAsGlobal();
297   }
298
299   void addAffectedDocuments(@NotNull Document... docs) {
300     if (!isInsideCommand()) {
301       LOG.error("Must be called inside command");
302       return;
303     }
304     List<DocumentReference> refs = new ArrayList<>(docs.length);
305     for (Document each : docs) {
306       // is document's file still valid
307       VirtualFile file = FileDocumentManager.getInstance().getFile(each);
308       if (file != null && !file.isValid()) continue;
309
310       refs.add(DocumentReferenceManager.getInstance().create(each));
311     }
312     myCurrentMerger.addAdditionalAffectedDocuments(refs);
313   }
314
315   public void addAffectedFiles(@NotNull VirtualFile... files) {
316     if (!isInsideCommand()) {
317       LOG.error("Must be called inside command");
318       return;
319     }
320     List<DocumentReference> refs = new ArrayList<>(files.length);
321     for (VirtualFile each : files) {
322       refs.add(DocumentReferenceManager.getInstance().create(each));
323     }
324     myCurrentMerger.addAdditionalAffectedDocuments(refs);
325   }
326
327   public void invalidateActionsFor(@NotNull DocumentReference ref) {
328     ApplicationManager.getApplication().assertIsDispatchThread();
329     myMerger.invalidateActionsFor(ref);
330     if (myCurrentMerger != null) myCurrentMerger.invalidateActionsFor(ref);
331     myUndoStacksHolder.invalidateActionsFor(ref);
332     myRedoStacksHolder.invalidateActionsFor(ref);
333   }
334
335   @Override
336   public void undo(@Nullable FileEditor editor) {
337     ApplicationManager.getApplication().assertIsDispatchThread();
338     LOG.assertTrue(isUndoAvailable(editor));
339     undoOrRedo(editor, true);
340   }
341
342   @Override
343   public void redo(@Nullable FileEditor editor) {
344     ApplicationManager.getApplication().assertIsDispatchThread();
345     LOG.assertTrue(isRedoAvailable(editor));
346     undoOrRedo(editor, false);
347   }
348
349   private void undoOrRedo(final FileEditor editor, final boolean isUndo) {
350     myCurrentOperationState = isUndo ? OperationState.UNDO : OperationState.REDO;
351
352     final RuntimeException[] exception = new RuntimeException[1];
353     Runnable executeUndoOrRedoAction = () -> {
354       try {
355         CopyPasteManager.getInstance().stopKillRings();
356         myMerger.undoOrRedo(editor, isUndo);
357       }
358       catch (RuntimeException ex) {
359         exception[0] = ex;
360       }
361       finally {
362         myCurrentOperationState = OperationState.NONE;
363       }
364     };
365
366     String name = getUndoOrRedoActionNameAndDescription(editor, isUndoInProgress()).second;
367     CommandProcessor.getInstance()
368       .executeCommand(myProject, executeUndoOrRedoAction, name, null, myMerger.getUndoConfirmationPolicy());
369     if (exception[0] != null) throw exception[0];
370   }
371
372   @Override
373   public boolean isUndoInProgress() {
374     return myCurrentOperationState == OperationState.UNDO;
375   }
376
377   @Override
378   public boolean isRedoInProgress() {
379     return myCurrentOperationState == OperationState.REDO;
380   }
381
382   @Override
383   public boolean isUndoAvailable(@Nullable FileEditor editor) {
384     return isUndoOrRedoAvailable(editor, true);
385   }
386
387   @Override
388   public boolean isRedoAvailable(@Nullable FileEditor editor) {
389     return isUndoOrRedoAvailable(editor, false);
390   }
391
392   boolean isUndoOrRedoAvailable(@Nullable FileEditor editor, boolean undo) {
393     ApplicationManager.getApplication().assertIsDispatchThread();
394
395     Collection<DocumentReference> refs = getDocRefs(editor);
396     return refs != null && isUndoOrRedoAvailable(refs, undo);
397   }
398
399   boolean isUndoOrRedoAvailable(@NotNull DocumentReference ref) {
400     Set<DocumentReference> refs = Collections.singleton(ref);
401     return isUndoOrRedoAvailable(refs, true) || isUndoOrRedoAvailable(refs, false);
402   }
403
404   private boolean isUndoOrRedoAvailable(@NotNull Collection<? extends DocumentReference> refs, boolean isUndo) {
405     if (isUndo && myMerger.isUndoAvailable(refs)) return true;
406     UndoRedoStacksHolder stackHolder = getStackHolder(isUndo);
407     return stackHolder.canBeUndoneOrRedone(refs);
408   }
409
410   private static Collection<DocumentReference> getDocRefs(@Nullable FileEditor editor) {
411     if (editor instanceof TextEditor && ((TextEditor)editor).getEditor().isViewer()) {
412       return null;
413     }
414     if (editor == null) {
415       return Collections.emptyList();
416     }
417     return getDocumentReferences(editor);
418   }
419
420   @NotNull
421   static Set<DocumentReference> getDocumentReferences(@NotNull FileEditor editor) {
422     Set<DocumentReference> result = new THashSet<>();
423
424     if (editor instanceof DocumentReferenceProvider) {
425       result.addAll(((DocumentReferenceProvider)editor).getDocumentReferences());
426       return result;
427     }
428
429     Document[] documents = TextEditorProvider.getDocuments(editor);
430     if (documents != null) {
431       for (Document each : documents) {
432         Document original = getOriginal(each);
433         // KirillK : in AnAction.update we may have an editor with an invalid file
434         VirtualFile f = FileDocumentManager.getInstance().getFile(each);
435         if (f != null && !f.isValid()) continue;
436         result.add(DocumentReferenceManager.getInstance().create(original));
437       }
438     }
439     return result;
440   }
441
442   @NotNull
443   private UndoRedoStacksHolder getStackHolder(boolean isUndo) {
444     return isUndo ? myUndoStacksHolder : myRedoStacksHolder;
445   }
446
447   @NotNull
448   @Override
449   public Pair<String, String> getUndoActionNameAndDescription(FileEditor editor) {
450     return getUndoOrRedoActionNameAndDescription(editor, true);
451   }
452
453   @NotNull
454   @Override
455   public Pair<String, String> getRedoActionNameAndDescription(FileEditor editor) {
456     return getUndoOrRedoActionNameAndDescription(editor, false);
457   }
458
459   @NotNull
460   private Pair<String, String> getUndoOrRedoActionNameAndDescription(FileEditor editor, boolean undo) {
461     String desc = isUndoOrRedoAvailable(editor, undo) ? doFormatAvailableUndoRedoAction(editor, undo) : null;
462     if (desc == null) desc = "";
463     String shortActionName = StringUtil.first(desc, 30, true);
464
465     if (desc.isEmpty()) {
466       desc = undo
467              ? ActionsBundle.message("action.undo.description.empty")
468              : ActionsBundle.message("action.redo.description.empty");
469     }
470
471     return Pair.create((undo ? ActionsBundle.message("action.undo.text", shortActionName)
472                              : ActionsBundle.message("action.redo.text", shortActionName)).trim(),
473                        (undo ? ActionsBundle.message("action.undo.description", desc)
474                              : ActionsBundle.message("action.redo.description", desc)).trim());
475   }
476
477   @Nullable
478   private String doFormatAvailableUndoRedoAction(FileEditor editor, boolean isUndo) {
479     Collection<DocumentReference> refs = getDocRefs(editor);
480     if (refs == null) return null;
481     if (isUndo && myMerger.isUndoAvailable(refs)) return myMerger.getCommandName();
482     return getStackHolder(isUndo).getLastAction(refs).getCommandName();
483   }
484
485   @NotNull
486   UndoRedoStacksHolder getUndoStacksHolder() {
487     return myUndoStacksHolder;
488   }
489
490   @NotNull
491   UndoRedoStacksHolder getRedoStacksHolder() {
492     return myRedoStacksHolder;
493   }
494
495   int nextCommandTimestamp() {
496     return ++myCommandTimestamp;
497   }
498
499   @NotNull
500   private static Document getOriginal(@NotNull Document document) {
501     Document result = document.getUserData(ORIGINAL_DOCUMENT);
502     return result == null ? document : result;
503   }
504
505   static boolean isCopy(@NotNull Document d) {
506     return d.getUserData(ORIGINAL_DOCUMENT) != null;
507   }
508
509   protected void compact() {
510     if (myCurrentOperationState == OperationState.NONE && myCommandTimestamp % COMMAND_TO_RUN_COMPACT == 0) {
511       doCompact();
512     }
513   }
514
515   private void doCompact() {
516     Collection<DocumentReference> refs = collectReferencesWithoutMergers();
517
518     Collection<DocumentReference> openDocs = new HashSet<>();
519     for (DocumentReference each : refs) {
520       VirtualFile file = each.getFile();
521       if (file == null) {
522         Document document = each.getDocument();
523         if (document != null && EditorFactory.getInstance().getEditors(document, myProject).length > 0) {
524           openDocs.add(each);
525         }
526       }
527       else {
528         if (myProject != null && FileEditorManager.getInstance(myProject).isFileOpen(file)) {
529           openDocs.add(each);
530         }
531       }
532     }
533     refs.removeAll(openDocs);
534
535     if (refs.size() <= FREE_QUEUES_LIMIT) return;
536
537     DocumentReference[] backSorted = refs.toArray(DocumentReference.EMPTY_ARRAY);
538     Arrays.sort(backSorted, Comparator.comparingInt(this::getLastCommandTimestamp));
539
540     for (int i = 0; i < backSorted.length - FREE_QUEUES_LIMIT; i++) {
541       DocumentReference each = backSorted[i];
542       if (getLastCommandTimestamp(each) + COMMANDS_TO_KEEP_LIVE_QUEUES > myCommandTimestamp) break;
543       clearUndoRedoQueue(each);
544     }
545   }
546
547   private int getLastCommandTimestamp(@NotNull DocumentReference ref) {
548     return Math.max(myUndoStacksHolder.getLastCommandTimestamp(ref), myRedoStacksHolder.getLastCommandTimestamp(ref));
549   }
550
551   @NotNull
552   private Collection<DocumentReference> collectReferencesWithoutMergers() {
553     Set<DocumentReference> result = new THashSet<>();
554     myUndoStacksHolder.collectAllAffectedDocuments(result);
555     myRedoStacksHolder.collectAllAffectedDocuments(result);
556     return result;
557   }
558
559   private void clearUndoRedoQueue(@NotNull DocumentReference docRef) {
560     myMerger.flushCurrentCommand();
561     disposeCurrentMerger();
562
563     myUndoStacksHolder.clearStacks(false, Collections.singleton(docRef));
564     myRedoStacksHolder.clearStacks(false, Collections.singleton(docRef));
565   }
566
567   @TestOnly
568   public void setEditorProvider(@NotNull CurrentEditorProvider p) {
569     myEditorProvider = p;
570   }
571
572   @TestOnly
573   @NotNull
574   public CurrentEditorProvider getEditorProvider() {
575     return myEditorProvider;
576   }
577
578   @TestOnly
579   public void dropHistoryInTests() {
580     flushMergers();
581     LOG.assertTrue(myCommandLevel == 0, myCommandLevel);
582
583     myUndoStacksHolder.clearAllStacksInTests();
584     myRedoStacksHolder.clearAllStacksInTests();
585   }
586
587   @TestOnly
588   private void flushMergers() {
589     assert myProject == null || !myProject.isDisposed() : myProject;
590     // Run dummy command in order to flush all mergers...
591     CommandProcessor.getInstance().executeCommand(myProject, EmptyRunnable.getInstance(), "Dummy", null);
592   }
593
594   @TestOnly
595   public void flushCurrentCommandMerger() {
596     myMerger.flushCurrentCommand();
597   }
598
599   @TestOnly
600   public void clearUndoRedoQueueInTests(@NotNull VirtualFile file) {
601     clearUndoRedoQueue(DocumentReferenceManager.getInstance().create(file));
602   }
603
604   @TestOnly
605   public void clearUndoRedoQueueInTests(@NotNull Document document) {
606     clearUndoRedoQueue(DocumentReferenceManager.getInstance().create(document));
607   }
608
609   @Override
610   public String toString() {
611     return "UndoManager for " + ObjectUtils.notNull(myProject, "application");
612   }
613 }