fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / fileEditor / impl / IdeDocumentHistoryImpl.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.fileEditor.impl;
3
4 import com.intellij.ide.ui.UISettings;
5 import com.intellij.openapi.Disposable;
6 import com.intellij.openapi.application.ApplicationManager;
7 import com.intellij.openapi.command.CommandEvent;
8 import com.intellij.openapi.command.CommandListener;
9 import com.intellij.openapi.command.CommandProcessor;
10 import com.intellij.openapi.command.impl.CommandMerger;
11 import com.intellij.openapi.components.PersistentStateComponent;
12 import com.intellij.openapi.components.State;
13 import com.intellij.openapi.components.Storage;
14 import com.intellij.openapi.components.StoragePathMacros;
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.editor.RangeMarker;
20 import com.intellij.openapi.editor.event.CaretEvent;
21 import com.intellij.openapi.editor.event.DocumentEvent;
22 import com.intellij.openapi.editor.event.EditorEventListener;
23 import com.intellij.openapi.editor.event.EditorEventMulticaster;
24 import com.intellij.openapi.fileEditor.*;
25 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
26 import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider;
27 import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
28 import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.project.ProjectUtil;
31 import com.intellij.openapi.util.Disposer;
32 import com.intellij.openapi.util.Pair;
33 import com.intellij.openapi.util.registry.Registry;
34 import com.intellij.openapi.vfs.LocalFileSystem;
35 import com.intellij.openapi.vfs.VfsUtilCore;
36 import com.intellij.openapi.vfs.VirtualFile;
37 import com.intellij.openapi.vfs.VirtualFileManager;
38 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
39 import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent;
40 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
41 import com.intellij.openapi.wm.ToolWindowManager;
42 import com.intellij.psi.ExternalChangeAction;
43 import com.intellij.testFramework.LightVirtualFile;
44 import com.intellij.ui.SimpleColoredComponent;
45 import com.intellij.ui.SimpleTextAttributes;
46 import com.intellij.util.containers.ContainerUtil;
47 import com.intellij.util.io.EnumeratorLongDescriptor;
48 import com.intellij.util.io.EnumeratorStringDescriptor;
49 import com.intellij.util.io.PersistentHashMap;
50 import com.intellij.util.messages.MessageBus;
51 import com.intellij.util.messages.MessageBusConnection;
52 import com.intellij.util.messages.Topic;
53 import com.intellij.util.text.DateFormatUtil;
54 import gnu.trove.THashSet;
55 import org.jetbrains.annotations.NotNull;
56 import org.jetbrains.annotations.Nullable;
57
58 import java.io.File;
59 import java.io.IOException;
60 import java.lang.ref.Reference;
61 import java.lang.ref.WeakReference;
62 import java.util.*;
63
64 @State(name = "IdeDocumentHistory", storages = {
65   @Storage(StoragePathMacros.PRODUCT_WORKSPACE_FILE),
66   @Storage(value = StoragePathMacros.WORKSPACE_FILE, deprecated = true)
67 })
68 public class IdeDocumentHistoryImpl extends IdeDocumentHistory implements Disposable, PersistentStateComponent<IdeDocumentHistoryImpl.RecentlyChangedFilesState> {
69   private static final Logger LOG = Logger.getInstance(IdeDocumentHistoryImpl.class);
70
71   private static final int BACK_QUEUE_LIMIT = Registry.intValue("editor.navigation.history.stack.size");
72   private static final int CHANGE_QUEUE_LIMIT = Registry.intValue("editor.navigation.history.stack.size");
73
74   private final Project myProject;
75
76   private FileDocumentManager myFileDocumentManager;
77
78   private final LinkedList<PlaceInfo> myBackPlaces = new LinkedList<>(); // LinkedList of PlaceInfo's
79   private final LinkedList<PlaceInfo> myForwardPlaces = new LinkedList<>(); // LinkedList of PlaceInfo's
80   private boolean myBackInProgress;
81   private boolean myForwardInProgress;
82   private Object myLastGroupId;
83   private boolean myRegisteredBackPlaceInLastGroup;
84
85   // change's navigation
86   private final LinkedList<PlaceInfo> myChangePlaces = new LinkedList<>(); // LinkedList of PlaceInfo's
87   private int myCurrentIndex;
88
89   private PlaceInfo myCommandStartPlace;
90   private boolean myCurrentCommandIsNavigation;
91   private boolean myCurrentCommandHasChanges;
92   private final Set<VirtualFile> myChangedFilesInCurrentCommand = new THashSet<>();
93   private boolean myCurrentCommandHasMoves;
94
95   private final PersistentHashMap<String, Long> myRecentFilesTimestampsMap;
96
97   private RecentlyChangedFilesState myRecentlyChangedFiles = new RecentlyChangedFilesState();
98
99   public IdeDocumentHistoryImpl(@NotNull Project project) {
100     myProject = project;
101
102     MessageBusConnection busConnection = project.getMessageBus().connect(this);
103     busConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() {
104       @Override
105       public void selectionChanged(@NotNull FileEditorManagerEvent e) {
106         onSelectionChanged();
107       }
108     });
109     busConnection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
110       @Override
111       public void after(@NotNull List<? extends VFileEvent> events) {
112         for (VFileEvent event : events) {
113           if (event instanceof VFileDeleteEvent) {
114             removeInvalidFilesFromStacks();
115             return;
116           }
117         }
118       }
119     });
120     busConnection.subscribe(CommandListener.TOPIC, new CommandListener() {
121       @Override
122       public void commandStarted(@NotNull CommandEvent event) {
123         onCommandStarted();
124       }
125
126       @Override
127       public void commandFinished(@NotNull CommandEvent event) {
128         onCommandFinished(event.getProject(), event.getCommandGroupId());
129       }
130     });
131
132     EditorEventListener listener = new EditorEventListener() {
133       @Override
134       public void documentChanged(@NotNull DocumentEvent e) {
135         Document document = e.getDocument();
136         final VirtualFile file = getFileDocumentManager().getFile(document);
137         if (file != null && !(file instanceof LightVirtualFile) && !ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class)) {
138           if (!ApplicationManager.getApplication().isDispatchThread()) {
139             LOG.error("Document update for physical file not in EDT: " + file);
140           }
141           myCurrentCommandHasChanges = true;
142           myChangedFilesInCurrentCommand.add(file);
143         }
144       }
145
146       @Override
147       public void caretPositionChanged(@NotNull CaretEvent e) {
148         if (e.getOldPosition().line == e.getNewPosition().line) {
149           return;
150         }
151
152         Document document = e.getEditor().getDocument();
153         if (getFileDocumentManager().getFile(document) != null) {
154           myCurrentCommandHasMoves = true;
155         }
156       }
157     };
158     EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster();
159     multicaster.addDocumentListener(listener, this);
160     multicaster.addCaretListener(listener, this);
161
162     myRecentFilesTimestampsMap = initRecentFilesTimestampMap(project);
163   }
164
165   protected FileEditorManagerEx getFileEditorManager() {
166     return FileEditorManagerEx.getInstanceEx(myProject);
167   }
168
169   @NotNull
170   private PersistentHashMap<String, Long> initRecentFilesTimestampMap(@NotNull Project project) {
171     File file = ProjectUtil.getProjectCachePath(project, "recentFilesTimeStamps.dat").toFile();
172     PersistentHashMap<String, Long> map;
173     try {
174       map = new PersistentHashMap<>(file, EnumeratorStringDescriptor.INSTANCE, EnumeratorLongDescriptor.INSTANCE);
175     }
176     catch (IOException e) {
177       LOG.info("Cannot create PersistentHashMap in "+file, e);
178       PersistentHashMap.deleteFilesStartingWith(file);
179       try {
180         map = new PersistentHashMap<>(file, EnumeratorStringDescriptor.INSTANCE, EnumeratorLongDescriptor.INSTANCE);
181       }
182       catch (IOException e1) {
183         LOG.error("Cannot create PersistentHashMap in " + file + " even after deleting old files", e1);
184         throw new RuntimeException(e);
185       }
186     }
187     PersistentHashMap<String, Long> finalMap = map;
188     Disposer.register(this, () -> {
189       try {
190         finalMap.close();
191       }
192       catch (IOException e) {
193         LOG.info("Cannot close persistent viewed files timestamps hash map", e);
194       }
195     });
196     return map;
197   }
198
199   private void registerViewed(@NotNull VirtualFile file) {
200     if (ApplicationManager.getApplication().isUnitTestMode()) {
201       return;
202     }
203
204     try {
205       myRecentFilesTimestampsMap.put(file.getPath(), System.currentTimeMillis());
206     }
207     catch (IOException e) {
208       LOG.info("Cannot put a timestamp from a persistent hash map", e);
209     }
210   }
211
212   public static void appendTimestamp(@NotNull Project project,
213                                      @NotNull SimpleColoredComponent component,
214                                      @NotNull VirtualFile file) {
215     if (!UISettings.getInstance().getShowInplaceComments()) {
216       return;
217     }
218
219     try {
220       Long timestamp = getInstance(project).getRecentFilesTimestamps().get(file.getPath());
221       if (timestamp != null) {
222         component.append(" ").append(DateFormatUtil.formatPrettyDateTime(timestamp), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES);
223       }
224     }
225     catch (IOException e) {
226       LOG.info("Cannot get a timestamp from a persistent hash map", e);
227     }
228   }
229
230   public static class RecentlyChangedFilesState {
231     // don't make it private, see: IDEA-130363 Recently Edited Files list should survive restart
232     @SuppressWarnings("WeakerAccess") public List<String> CHANGED_PATHS = new ArrayList<>();
233
234     public void register(VirtualFile file) {
235       final String path = file.getPath();
236       CHANGED_PATHS.remove(path);
237       CHANGED_PATHS.add(path);
238       trimToSize();
239     }
240
241     private void trimToSize() {
242       final int limit = UISettings.getInstance().getRecentFilesLimit() + 1;
243       while (CHANGED_PATHS.size() > limit) {
244         CHANGED_PATHS.remove(0);
245       }
246     }
247   }
248
249   @Override
250   public RecentlyChangedFilesState getState() {
251     return myRecentlyChangedFiles;
252   }
253
254   @Override
255   public void loadState(@NotNull RecentlyChangedFilesState state) {
256     myRecentlyChangedFiles = state;
257   }
258
259   public final void onSelectionChanged() {
260     myCurrentCommandIsNavigation = true;
261     myCurrentCommandHasMoves = true;
262   }
263
264   final void onCommandStarted() {
265     myCommandStartPlace = getCurrentPlaceInfo();
266     myCurrentCommandIsNavigation = false;
267     myCurrentCommandHasChanges = false;
268     myCurrentCommandHasMoves = false;
269     myChangedFilesInCurrentCommand.clear();
270   }
271
272   @Nullable
273   private PlaceInfo getCurrentPlaceInfo() {
274     FileEditorWithProvider selectedEditorWithProvider = getSelectedEditor();
275     if (selectedEditorWithProvider == null) {
276       return null;
277     }
278     return createPlaceInfo(selectedEditorWithProvider.getFileEditor(), selectedEditorWithProvider.getProvider());
279   }
280
281   @Nullable
282   private static PlaceInfo getPlaceInfoFromFocus() {
283     FileEditor fileEditor = new FocusBasedCurrentEditorProvider().getCurrentEditor();
284     if (fileEditor instanceof TextEditor && fileEditor.isValid()) {
285       VirtualFile file = fileEditor.getFile();
286       if (file != null) {
287         return new PlaceInfo(file,
288                              fileEditor.getState(FileEditorStateLevel.NAVIGATION),
289                              TextEditorProvider.getInstance().getEditorTypeId(),
290                              null,
291                              getCaretPosition(fileEditor), System.currentTimeMillis());
292       }
293     }
294     return null;
295   }
296
297   final void onCommandFinished(Project project, Object commandGroupId) {
298     if (!CommandMerger.canMergeGroup(commandGroupId, myLastGroupId)) myRegisteredBackPlaceInLastGroup = false;
299     myLastGroupId = commandGroupId;
300
301     if (myCommandStartPlace != null && myCurrentCommandIsNavigation && myCurrentCommandHasMoves) {
302       if (!myBackInProgress) {
303         if (!myRegisteredBackPlaceInLastGroup) {
304           myRegisteredBackPlaceInLastGroup = true;
305           putLastOrMerge(myCommandStartPlace, BACK_QUEUE_LIMIT, false);
306           registerViewed(myCommandStartPlace.myFile);
307         }
308         if (!myForwardInProgress) {
309           myForwardPlaces.clear();
310         }
311       }
312       removeInvalidFilesFromStacks();
313     }
314
315     if (myCurrentCommandHasChanges) {
316       setCurrentChangePlace(project == myProject);
317     }
318     else if (myCurrentCommandHasMoves) {
319       myCurrentIndex = myChangePlaces.size();
320     }
321   }
322
323   @Override
324   public final void includeCurrentCommandAsNavigation() {
325     myCurrentCommandIsNavigation = true;
326   }
327
328   @Override
329   public void setCurrentCommandHasMoves() {
330     myCurrentCommandHasMoves = true;
331   }
332
333   @Override
334   public final void includeCurrentPlaceAsChangePlace() {
335     setCurrentChangePlace(false);
336   }
337
338   private void setCurrentChangePlace(boolean acceptPlaceFromFocus) {
339     PlaceInfo placeInfo = getCurrentPlaceInfo();
340     if (placeInfo != null && !myChangedFilesInCurrentCommand.contains(placeInfo.getFile())) {
341       placeInfo = null;
342     }
343     if (placeInfo == null && acceptPlaceFromFocus) {
344       placeInfo = getPlaceInfoFromFocus();
345     }
346     if (placeInfo != null && !myChangedFilesInCurrentCommand.contains(placeInfo.getFile())) {
347       placeInfo = null;
348     }
349     if (placeInfo == null) {
350       return;
351     }
352
353     myRecentlyChangedFiles.register(placeInfo.getFile());
354
355     putLastOrMerge(placeInfo, CHANGE_QUEUE_LIMIT, true);
356     myCurrentIndex = myChangePlaces.size();
357   }
358
359   @Override
360   public VirtualFile[] getChangedFiles() {
361     List<VirtualFile> files = new ArrayList<>();
362
363     final LocalFileSystem lfs = LocalFileSystem.getInstance();
364     final List<String> paths = myRecentlyChangedFiles.CHANGED_PATHS;
365     for (String path : paths) {
366       final VirtualFile file = lfs.findFileByPath(path);
367       if (file != null) {
368         files.add(file);
369       }
370     }
371
372     return VfsUtilCore.toVirtualFileArray(files);
373   }
374
375   @Override
376   public PersistentHashMap<String, Long> getRecentFilesTimestamps() {
377     return myRecentFilesTimestampsMap;
378   }
379
380   boolean isRecentlyChanged(@NotNull VirtualFile file) {
381     return myRecentlyChangedFiles.CHANGED_PATHS.contains(file.getPath());
382   }
383
384   @Override
385   public final void clearHistory() {
386     myBackPlaces.clear();
387     myForwardPlaces.clear();
388     myChangePlaces.clear();
389
390     myLastGroupId = null;
391
392     myCurrentIndex = 0;
393     myCommandStartPlace = null;
394   }
395
396   @Override
397   public final void back() {
398     removeInvalidFilesFromStacks();
399     if (myBackPlaces.isEmpty()) return;
400     final PlaceInfo info = myBackPlaces.removeLast();
401     myProject.getMessageBus().syncPublisher(RecentPlacesListener.TOPIC).recentPlaceRemoved(info, false);
402
403     PlaceInfo current = getCurrentPlaceInfo();
404     if (current != null) myForwardPlaces.add(current);
405
406     myBackInProgress = true;
407     try {
408       executeCommand(() -> gotoPlaceInfo(info), "", null);
409     }
410     finally {
411       myBackInProgress = false;
412     }
413   }
414
415   @Override
416   public final void forward() {
417     removeInvalidFilesFromStacks();
418
419     final PlaceInfo target = getTargetForwardInfo();
420     if (target == null) return;
421
422     myForwardInProgress = true;
423     try {
424       executeCommand(() -> gotoPlaceInfo(target), "", null);
425     }
426     finally {
427       myForwardInProgress = false;
428     }
429   }
430
431   private PlaceInfo getTargetForwardInfo() {
432     if (myForwardPlaces.isEmpty()) return null;
433
434     PlaceInfo target = myForwardPlaces.removeLast();
435     PlaceInfo current = getCurrentPlaceInfo();
436
437     while (!myForwardPlaces.isEmpty()) {
438       if (current != null && isSame(current, target)) {
439         target = myForwardPlaces.removeLast();
440       }
441       else {
442         break;
443       }
444     }
445     return target;
446   }
447
448   @Override
449   public final boolean isBackAvailable() {
450     return !myBackPlaces.isEmpty();
451   }
452
453   @Override
454   public final boolean isForwardAvailable() {
455     return !myForwardPlaces.isEmpty();
456   }
457
458   @Override
459   public final void navigatePreviousChange() {
460     removeInvalidFilesFromStacks();
461     if (myCurrentIndex == 0) return;
462     PlaceInfo currentPlace = getCurrentPlaceInfo();
463     for (int i = myCurrentIndex - 1; i >= 0; i--) {
464       PlaceInfo info = myChangePlaces.get(i);
465       if (currentPlace == null || !isSame(currentPlace, info)) {
466         executeCommand(() -> gotoPlaceInfo(info), "", null);
467         myCurrentIndex = i;
468         break;
469       }
470     }
471   }
472
473   @Override
474   @NotNull
475   public List<PlaceInfo> getBackPlaces() {
476     return ContainerUtil.immutableList(myBackPlaces);
477   }
478
479   @Override
480   public List<PlaceInfo> getChangePlaces() {
481     return ContainerUtil.immutableList(myChangePlaces);
482   }
483
484   @Override
485   public void removeBackPlace(@NotNull PlaceInfo placeInfo) {
486     removePlaceInfo(placeInfo, myBackPlaces, false);
487   }
488
489   @Override
490   public void removeChangePlace(@NotNull PlaceInfo placeInfo) {
491     removePlaceInfo(placeInfo, myChangePlaces, true);
492   }
493
494   private void removePlaceInfo(@NotNull PlaceInfo placeInfo, @NotNull LinkedList<PlaceInfo> places, boolean changed) {
495     boolean removed = places.remove(placeInfo);
496     if (removed) {
497       myProject.getMessageBus().syncPublisher(RecentPlacesListener.TOPIC).recentPlaceRemoved(placeInfo, changed);
498     }
499   }
500
501   @Override
502   public final boolean isNavigatePreviousChangeAvailable() {
503     return myCurrentIndex > 0;
504   }
505
506   void removeInvalidFilesFromStacks() {
507     removeInvalidFilesFrom(myBackPlaces);
508
509     removeInvalidFilesFrom(myForwardPlaces);
510     if (removeInvalidFilesFrom(myChangePlaces)) {
511       myCurrentIndex = myChangePlaces.size();
512     }
513   }
514
515   @Override
516   public void navigateNextChange() {
517     removeInvalidFilesFromStacks();
518     if (myCurrentIndex >= myChangePlaces.size()) return;
519     PlaceInfo currentPlace = getCurrentPlaceInfo();
520     for (int i = myCurrentIndex; i < myChangePlaces.size(); i++) {
521       PlaceInfo info = myChangePlaces.get(i);
522       if (currentPlace == null || !isSame(currentPlace, info)) {
523         executeCommand(() -> gotoPlaceInfo(info), "", null);
524         myCurrentIndex = i + 1;
525         break;
526       }
527     }
528   }
529
530   @Override
531   public boolean isNavigateNextChangeAvailable() {
532     return myCurrentIndex < myChangePlaces.size();
533   }
534
535   private static boolean removeInvalidFilesFrom(@NotNull List<PlaceInfo> backPlaces) {
536     boolean removed = false;
537     for (Iterator<PlaceInfo> iterator = backPlaces.iterator(); iterator.hasNext(); ) {
538       PlaceInfo info = iterator.next();
539       final VirtualFile file = info.myFile;
540       if (!file.isValid()) {
541         iterator.remove();
542         removed = true;
543       }
544     }
545
546     return removed;
547   }
548
549   @Override
550   public void gotoPlaceInfo(@NotNull PlaceInfo info) {
551     final boolean wasActive = ToolWindowManager.getInstance(myProject).isEditorComponentActive();
552     EditorWindow wnd = info.getWindow();
553     FileEditorManagerEx editorManager = getFileEditorManager();
554     final Pair<FileEditor[], FileEditorProvider[]> editorsWithProviders = wnd != null && wnd.isValid()
555                                                                           ? editorManager.openFileWithProviders(info.getFile(), wasActive, wnd)
556                                                                           : editorManager.openFileWithProviders(info.getFile(), wasActive, false);
557
558     editorManager.setSelectedEditor(info.getFile(), info.getEditorTypeId());
559
560     final FileEditor[] editors = editorsWithProviders.getFirst();
561     final FileEditorProvider[] providers = editorsWithProviders.getSecond();
562     for (int i = 0; i < editors.length; i++) {
563       String typeId = providers[i].getEditorTypeId();
564       if (typeId.equals(info.getEditorTypeId())) {
565         editors[i].setState(info.getNavigationState());
566       }
567     }
568   }
569
570   /**
571    * @return currently selected FileEditor or null.
572    */
573   @Nullable
574   protected FileEditorWithProvider getSelectedEditor() {
575     FileEditorManagerEx editorManager = getFileEditorManager();
576     VirtualFile file = editorManager.getCurrentFile();
577     return file == null ? null : editorManager.getSelectedEditorWithProvider(file);
578   }
579
580   protected PlaceInfo createPlaceInfo(@NotNull final FileEditor fileEditor, final FileEditorProvider fileProvider) {
581     if (!fileEditor.isValid()) {
582       return null;
583     }
584
585     FileEditorManagerEx editorManager = getFileEditorManager();
586     final VirtualFile file = editorManager.getFile(fileEditor);
587     LOG.assertTrue(file != null);
588     FileEditorState state = fileEditor.getState(FileEditorStateLevel.NAVIGATION);
589
590     return new PlaceInfo(file, state, fileProvider.getEditorTypeId(), editorManager.getCurrentWindow(), getCaretPosition(fileEditor),
591                          System.currentTimeMillis());
592   }
593
594   @Nullable
595   private static RangeMarker getCaretPosition(@NotNull FileEditor fileEditor) {
596     if (!(fileEditor instanceof TextEditor)) {
597       return null;
598     }
599
600     Editor editor = ((TextEditor)fileEditor).getEditor();
601     int offset = editor.getCaretModel().getOffset();
602
603     return editor.getDocument().createRangeMarker(offset, offset);
604   }
605
606   private void putLastOrMerge(@NotNull PlaceInfo next, int limit, boolean isChanged) {
607     LinkedList<PlaceInfo> list = isChanged ? myChangePlaces : myBackPlaces;
608     MessageBus messageBus = myProject.getMessageBus();
609     RecentPlacesListener listener = messageBus.syncPublisher(RecentPlacesListener.TOPIC);
610     if (!list.isEmpty()) {
611       PlaceInfo prev = list.getLast();
612       if (isSame(prev, next)) {
613         PlaceInfo removed = list.removeLast();
614         listener.recentPlaceRemoved(removed, isChanged);
615       }
616     }
617
618     list.add(next);
619     listener.recentPlaceAdded(next, isChanged);
620     if (list.size() > limit) {
621       PlaceInfo first = list.removeFirst();
622       listener.recentPlaceRemoved(first, isChanged);
623     }
624   }
625
626   private FileDocumentManager getFileDocumentManager() {
627     if (myFileDocumentManager == null) {
628       myFileDocumentManager = FileDocumentManager.getInstance();
629     }
630     return myFileDocumentManager;
631   }
632
633   public static final class PlaceInfo {
634     private final VirtualFile myFile;
635     private final FileEditorState myNavigationState;
636     private final String myEditorTypeId;
637     private final Reference<EditorWindow> myWindow;
638     @Nullable private final RangeMarker myCaretPosition;
639     private final long myTimeStamp;
640
641     public PlaceInfo(@NotNull VirtualFile file,
642                      @NotNull FileEditorState navigationState,
643                      @NotNull String editorTypeId,
644                      @Nullable EditorWindow window,
645                      @Nullable RangeMarker caretPosition) {
646       myNavigationState = navigationState;
647       myFile = file;
648       myEditorTypeId = editorTypeId;
649       myWindow = new WeakReference<>(window);
650       myCaretPosition = caretPosition;
651       myTimeStamp = -1;
652     }
653
654     public PlaceInfo(@NotNull VirtualFile file,
655                      @NotNull FileEditorState navigationState,
656                      @NotNull String editorTypeId,
657                      @Nullable EditorWindow window,
658                      @Nullable RangeMarker caretPosition,
659                      long stamp) {
660       myNavigationState = navigationState;
661       myFile = file;
662       myEditorTypeId = editorTypeId;
663       myWindow = new WeakReference<>(window);
664       myCaretPosition = caretPosition;
665       myTimeStamp = stamp;
666     }
667
668     public EditorWindow getWindow() {
669       return myWindow.get();
670     }
671
672     @NotNull
673     public FileEditorState getNavigationState() {
674       return myNavigationState;
675     }
676
677     @NotNull
678     public VirtualFile getFile() {
679       return myFile;
680     }
681
682     @NotNull
683     public String getEditorTypeId() {
684       return myEditorTypeId;
685     }
686
687     @Override
688     public String toString() {
689       return getFile().getName() + " " + getNavigationState();
690     }
691
692     @Nullable
693     public RangeMarker getCaretPosition() {
694       return myCaretPosition;
695     }
696
697     public long getTimeStamp() {
698       return myTimeStamp;
699     }
700   }
701
702   @Override
703   public final void dispose() {
704     myLastGroupId = null;
705   }
706
707   protected void executeCommand(Runnable runnable, String name, Object groupId) {
708     CommandProcessor.getInstance().executeCommand(myProject, runnable, name, groupId);
709   }
710
711   public static boolean isSame(@NotNull PlaceInfo first, @NotNull PlaceInfo second) {
712     if (first.getFile().equals(second.getFile())) {
713       FileEditorState firstState = first.getNavigationState();
714       FileEditorState secondState = second.getNavigationState();
715       return firstState.equals(secondState) || firstState.canBeMergedWith(secondState, FileEditorStateLevel.NAVIGATION);
716     }
717
718     return false;
719   }
720
721   /**
722    * {@link RecentPlacesListener} listens recently viewed or changed place adding and removing events.
723    */
724   public interface RecentPlacesListener {
725     Topic<RecentPlacesListener> TOPIC = Topic.create("RecentPlacesListener", RecentPlacesListener.class);
726
727     /**
728      * Fires on a new place info adding into {@link #myChangePlaces} or {@link #myBackPlaces} infos list
729      *
730      * @param changePlace new place info
731      * @param isChanged   true if place info was added into the changed infos list {@link #myChangePlaces};
732      *                    false if place info was added into the back infos list {@link #myBackPlaces}
733      */
734     void recentPlaceAdded(@NotNull PlaceInfo changePlace, boolean isChanged);
735
736     /**
737      * Fires on a place info removing from the {@link #myChangePlaces} or the {@link #myBackPlaces} infos list
738      *
739      * @param changePlace place info that was removed
740      * @param isChanged   true if place info was removed from the changed infos list {@link #myChangePlaces};
741      *                    false if place info was removed from the back infos list {@link #myBackPlaces}
742      */
743     void recentPlaceRemoved(@NotNull PlaceInfo changePlace, boolean isChanged);
744   }
745 }