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