cleanup
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / fileEditor / impl / EditorHistoryManager.java
1 // Copyright 2000-2020 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.google.common.annotations.VisibleForTesting;
5 import com.intellij.ide.ui.UISettings;
6 import com.intellij.ide.ui.UISettingsListener;
7 import com.intellij.openapi.Disposable;
8 import com.intellij.openapi.application.ApplicationManager;
9 import com.intellij.openapi.components.*;
10 import com.intellij.openapi.diagnostic.Logger;
11 import com.intellij.openapi.fileEditor.*;
12 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
13 import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider;
14 import com.intellij.openapi.progress.ProcessCanceledException;
15 import com.intellij.openapi.project.DumbAware;
16 import com.intellij.openapi.project.Project;
17 import com.intellij.openapi.startup.StartupActivity;
18 import com.intellij.openapi.util.Pair;
19 import com.intellij.openapi.vfs.VfsUtilCore;
20 import com.intellij.openapi.vfs.VirtualFile;
21 import com.intellij.openapi.vfs.VirtualFileManager;
22 import com.intellij.psi.PsiDocumentManager;
23 import com.intellij.util.ArrayUtilRt;
24 import com.intellij.util.messages.MessageBusConnection;
25 import org.jdom.Element;
26 import org.jetbrains.annotations.NotNull;
27 import org.jetbrains.annotations.Nullable;
28
29 import java.util.*;
30
31 @State(name = "editorHistoryManager", storages = {
32   @Storage(StoragePathMacros.PRODUCT_WORKSPACE_FILE),
33   @Storage(value = StoragePathMacros.WORKSPACE_FILE, deprecated = true)
34 })
35 public final class EditorHistoryManager implements PersistentStateComponent<Element>, Disposable {
36   private static final Logger LOG = Logger.getInstance(EditorHistoryManager.class);
37
38   private final Project myProject;
39
40   public static EditorHistoryManager getInstance(@NotNull Project project){
41     return ServiceManager.getService(project, EditorHistoryManager.class);
42   }
43
44   /**
45    * State corresponding to the most recent file is the last
46    */
47   private final List<HistoryEntry> myEntriesList = new ArrayList<>();
48
49   EditorHistoryManager(@NotNull Project project) {
50     myProject = project;
51
52     MessageBusConnection connection = project.getMessageBus().connect();
53     connection.subscribe(UISettingsListener.TOPIC, uiSettings -> trimToSize());
54     connection.subscribe(FileEditorManagerListener.Before.FILE_EDITOR_MANAGER, new FileEditorManagerListener.Before() {
55       @Override
56       public void beforeFileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
57         updateHistoryEntry(file, false);
58       }
59     });
60     connection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new MyEditorManagerListener());
61   }
62
63   static class EditorHistoryManagerStartUpActivity implements DumbAware, StartupActivity {
64     @Override
65     public void runActivity(@NotNull Project project) {
66       getInstance(project);
67     }
68   }
69
70   private synchronized void removeEntry(@NotNull HistoryEntry entry) {
71     if (myEntriesList.remove(entry)) {
72       entry.destroy();
73     }
74   }
75
76   private synchronized void moveOnTop(@NotNull HistoryEntry entry) {
77     myEntriesList.remove(entry);
78     myEntriesList.add(entry);
79   }
80
81   /**
82    * Makes file most recent one
83    */
84   private void fileOpenedImpl(@NotNull VirtualFile file, @Nullable FileEditor fallbackEditor, @Nullable FileEditorProvider fallbackProvider) {
85     ApplicationManager.getApplication().assertIsDispatchThread();
86     // don't add files that cannot be found via VFM (light & etc.)
87     if (VirtualFileManager.getInstance().findFileByUrl(file.getUrl()) == null) {
88       return;
89     }
90
91     FileEditorManagerEx editorManager = FileEditorManagerEx.getInstanceEx(myProject);
92
93     Pair<FileEditor[], FileEditorProvider[]> editorsWithProviders = editorManager.getEditorsWithProviders(file);
94     FileEditor[] editors = editorsWithProviders.getFirst();
95     FileEditorProvider[] oldProviders = editorsWithProviders.getSecond();
96     LOG.assertTrue(editors.length == oldProviders.length, "Different number of editors and providers");
97     if (editors.length <= 0 && fallbackEditor != null && fallbackProvider != null) {
98       editors = new FileEditor[] { fallbackEditor };
99       oldProviders = new FileEditorProvider[] { fallbackProvider };
100     }
101     if (editors.length <= 0) {
102       // fileOpened notification is asynchronous, file could have been closed by now due to some reason
103       return;
104     }
105     FileEditor selectedEditor = editorManager.getSelectedEditor(file);
106     if (selectedEditor == null) {
107       selectedEditor = fallbackEditor;
108     }
109     LOG.assertTrue(selectedEditor != null);
110     int selectedProviderIndex = ArrayUtilRt.find(editors, selectedEditor);
111     LOG.assertTrue(selectedProviderIndex != -1, "Can't find " + selectedEditor + " among " + Arrays.asList(editors));
112
113     HistoryEntry entry = getEntry(file);
114     if (entry != null) {
115       moveOnTop(entry);
116     }
117     else {
118       FileEditorState[] states = new FileEditorState[editors.length];
119       FileEditorProvider[] providers = new FileEditorProvider[editors.length];
120       for (int i = states.length - 1; i >= 0; i--) {
121         FileEditorProvider provider = oldProviders[i];
122         LOG.assertTrue(provider != null);
123         providers[i] = provider;
124         FileEditor editor = editors[i];
125         if (editor.isValid()) {
126           states[i] = editor.getState(FileEditorStateLevel.FULL);
127         }
128       }
129       //noinspection SynchronizeOnThis
130       synchronized (this) {
131         myEntriesList.add(HistoryEntry.createHeavy(myProject, file, providers, states, providers[selectedProviderIndex]));
132       }
133       trimToSize();
134     }
135   }
136
137   public void updateHistoryEntry(@NotNull VirtualFile file, boolean changeEntryOrderOnly) {
138     updateHistoryEntry(file, null, null, changeEntryOrderOnly);
139   }
140
141   private void updateHistoryEntry(@NotNull VirtualFile file,
142                                   @Nullable FileEditor fileEditor,
143                                   @Nullable FileEditorProvider fileEditorProvider,
144                                   boolean changeEntryOrderOnly) {
145     FileEditorManagerEx editorManager = FileEditorManagerEx.getInstanceEx(myProject);
146     FileEditor[] editors;
147     FileEditorProvider[] providers;
148     if (fileEditor == null || fileEditorProvider == null) {
149       Pair<FileEditor[], FileEditorProvider[]> editorsWithProviders = editorManager.getEditorsWithProviders(file);
150       editors = editorsWithProviders.getFirst();
151       providers = editorsWithProviders.getSecond();
152     }
153     else {
154       editors = new FileEditor[] {fileEditor};
155       providers = new FileEditorProvider[] {fileEditorProvider};
156     }
157
158     if (editors.length == 0) {
159       // obviously not opened in any editor at the moment,
160       // makes no sense to put the file in the history
161       return;
162     }
163     HistoryEntry entry = getEntry(file);
164     if(entry == null){
165       // Size of entry list can be less than number of opened editors (some entries can be removed)
166       if (file.isValid()) {
167         // the file could have been deleted, so the isValid() check is essential
168         fileOpenedImpl(file, fileEditor, fileEditorProvider);
169       }
170       return;
171     }
172
173     if (!changeEntryOrderOnly) { // update entry state
174       //LOG.assertTrue(editors.length > 0);
175       for (int i = editors.length - 1; i >= 0; i--) {
176         FileEditor           editor = editors   [i];
177         FileEditorProvider provider = providers [i];
178         if (provider == null) continue; // can happen if fileEditorProvider is null
179         if (!editor.isValid()) {
180           // this can happen for example if file extension was changed
181           // and this method was called during corresponding myEditor close up
182           continue;
183         }
184
185         FileEditorState oldState = entry.getState(provider);
186         FileEditorState newState = editor.getState(FileEditorStateLevel.FULL);
187         if (!newState.equals(oldState)) {
188           entry.putState(provider, newState);
189         }
190       }
191     }
192     FileEditorWithProvider selectedEditorWithProvider = editorManager.getSelectedEditorWithProvider(file);
193     if (selectedEditorWithProvider != null) {
194       //LOG.assertTrue(selectedEditorWithProvider != null);
195       entry.setSelectedProvider(selectedEditorWithProvider.getProvider());
196       LOG.assertTrue(entry.getSelectedProvider() != null);
197
198       if (changeEntryOrderOnly) {
199         moveOnTop(entry);
200       }
201     }
202   }
203
204   /**
205    * @return array of valid files that are in the history, oldest first.
206    */
207   public synchronized VirtualFile @NotNull [] getFiles() {
208     List<VirtualFile> result = new ArrayList<>(myEntriesList.size());
209     for (HistoryEntry entry : myEntriesList) {
210       VirtualFile file = entry.getFile();
211       if (file != null) result.add(file);
212     }
213     return VfsUtilCore.toVirtualFileArray(result);
214   }
215
216   /**
217    * For internal or test-only usage.
218    */
219   @VisibleForTesting
220   public synchronized void removeAllFiles() {
221     for (HistoryEntry entry : myEntriesList) {
222       entry.destroy();
223     }
224     myEntriesList.clear();
225   }
226
227   /**
228    * @return a set of valid files that are in the history, oldest first.
229    */
230   @NotNull
231   public synchronized List<VirtualFile> getFileList() {
232     List<VirtualFile> result = new ArrayList<>();
233     for (HistoryEntry entry : myEntriesList) {
234       VirtualFile file = entry.getFile();
235       if (file != null) {
236         result.add(file);
237       }
238     }
239     return result;
240   }
241
242   /**
243    * @deprecated use {@link #getFileList()}
244    */
245   @NotNull
246   @Deprecated
247   public synchronized LinkedHashSet<VirtualFile> getFileSet() {
248     return new LinkedHashSet<>(getFileList());
249   }
250
251   public synchronized boolean hasBeenOpen(@NotNull VirtualFile f) {
252     for (HistoryEntry each : myEntriesList) {
253       if (f.equals(each.getFile())) {
254         return true;
255       }
256     }
257     return false;
258   }
259
260   /**
261    * Removes specified {@code file} from history. The method does
262    * nothing if {@code file} is not in the history.
263    *
264    * @exception IllegalArgumentException if {@code file}
265    * is {@code null}
266    */
267   public synchronized void removeFile(@NotNull VirtualFile file){
268     HistoryEntry entry = getEntry(file);
269     if(entry != null){
270       removeEntry(entry);
271     }
272   }
273
274   public FileEditorState getState(@NotNull VirtualFile file, @NotNull FileEditorProvider provider) {
275     HistoryEntry entry = getEntry(file);
276     return entry != null ? entry.getState(provider) : null;
277   }
278
279   /**
280    * @return may be null
281    */
282   FileEditorProvider getSelectedProvider(@NotNull VirtualFile file) {
283     HistoryEntry entry = getEntry(file);
284     return entry != null ? entry.getSelectedProvider() : null;
285   }
286
287   private synchronized HistoryEntry getEntry(@NotNull VirtualFile file) {
288     for (int i = myEntriesList.size() - 1; i >= 0; i--) {
289       HistoryEntry entry = myEntriesList.get(i);
290       VirtualFile entryFile = entry.getFile();
291       if (file.equals(entryFile)) {
292         return entry;
293       }
294     }
295     return null;
296   }
297
298   /**
299    * If total number of files in history more then {@code UISettings.RECENT_FILES_LIMIT}
300    * then removes the oldest ones to fit the history to new size.
301    */
302   private synchronized void trimToSize() {
303     int limit = UISettings.getInstance().getRecentFilesLimit() + 1;
304     while (myEntriesList.size() > limit) {
305       HistoryEntry removed = myEntriesList.remove(0);
306       removed.destroy();
307     }
308   }
309
310   @Override
311   public synchronized void loadState(@NotNull Element state) {
312     // each HistoryEntry contains myDisposable that must be disposed to dispose corresponding virtual file pointer
313     removeAllFiles();
314
315     // backward compatibility - previously entry maybe duplicated
316     Map<String, Element> fileToElement = new LinkedHashMap<>();
317     for (Element e : state.getChildren(HistoryEntry.TAG)) {
318       String file = e.getAttributeValue(HistoryEntry.FILE_ATTR);
319       fileToElement.remove(file);
320       // last is the winner
321       fileToElement.put(file, e);
322     }
323
324     for (Element e : fileToElement.values()) {
325       try {
326         myEntriesList.add(HistoryEntry.createHeavy(myProject, e));
327       }
328       catch (ProcessCanceledException ignored) {
329       }
330       catch (Exception anyException) {
331         LOG.error(anyException);
332       }
333     }
334   }
335
336   @Override
337   public synchronized Element getState() {
338     Element element = new Element("state");
339     // update history before saving
340     VirtualFile[] openFiles = FileEditorManager.getInstance(myProject).getOpenFiles();
341     for (int i = openFiles.length - 1; i >= 0; i--) {
342       VirtualFile file = openFiles[i];
343       // we have to update only files that are in history
344       if (getEntry(file) != null) {
345         updateHistoryEntry(file, false);
346       }
347     }
348
349     for (HistoryEntry entry : myEntriesList) {
350       entry.writeExternal(element, myProject);
351     }
352     return element;
353   }
354
355   @Override
356   public synchronized void dispose() {
357     removeAllFiles();
358   }
359
360   /**
361    * Updates history
362    */
363   private final class MyEditorManagerListener implements FileEditorManagerListener {
364     @Override
365     public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file){
366       fileOpenedImpl(file, null, null);
367     }
368
369     @Override
370     public void selectionChanged(@NotNull FileEditorManagerEvent event){
371       // updateHistoryEntry does commitDocument which is 1) very expensive and 2) cannot be performed from within PSI change listener
372       // so defer updating history entry until documents committed to improve responsiveness
373       PsiDocumentManager.getInstance(myProject).performWhenAllCommitted(() -> {
374         FileEditor newEditor = event.getNewEditor();
375         if(newEditor != null && !newEditor.isValid())
376           return;
377
378         VirtualFile oldFile = event.getOldFile();
379         if (oldFile != null) {
380           updateHistoryEntry(oldFile, event.getOldEditor(), event.getOldProvider(), false);
381         }
382         VirtualFile newFile = event.getNewFile();
383         if (newFile != null) {
384           updateHistoryEntry(newFile, true);
385         }
386       });
387     }
388   }
389 }