/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.fileEditor.impl; import com.intellij.ide.ui.UISettings; import com.intellij.ide.ui.UISettingsListener; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.*; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.DumbAwareRunnable; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiDocumentManager; import com.intellij.util.ArrayUtilRt; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.messages.MessageBusConnection; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @State(name = "EditorHistoryManager", storages = @Storage(file = StoragePathMacros.WORKSPACE_FILE)) public final class EditorHistoryManager implements PersistentStateComponent<Element>, ProjectComponent { private static final Logger LOG = Logger.getInstance(EditorHistoryManager.class); private final Project myProject; public static EditorHistoryManager getInstance(@NotNull Project project) { return project.getComponent(EditorHistoryManager.class); } /** * State corresponding to the most recent file is the last */ private final List<HistoryEntry> myEntriesList = new ArrayList<>(); EditorHistoryManager(@NotNull Project project) { myProject = project; MessageBusConnection connection = project.getMessageBus().connect(); connection.subscribe(UISettingsListener.TOPIC, new UISettingsListener() { @Override public void uiSettingsChanged(UISettings uiSettings) { trimToSize(); } }); connection.subscribe(FileEditorManagerListener.Before.FILE_EDITOR_MANAGER, new FileEditorManagerListener.Before.Adapter() { @Override public void beforeFileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { updateHistoryEntry(file, false); } }); connection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new MyEditorManagerListener()); } private synchronized void addEntry(HistoryEntry entry) { myEntriesList.add(entry); } private synchronized void removeEntry(HistoryEntry entry) { boolean removed = myEntriesList.remove(entry); if (removed) entry.destroy(); } private synchronized void moveOnTop(HistoryEntry entry) { myEntriesList.remove(entry); myEntriesList.add(entry); } /** * Makes file most recent one */ private void fileOpenedImpl(@NotNull final VirtualFile file, @Nullable final FileEditor fallbackEditor, @Nullable FileEditorProvider fallbackProvider) { ApplicationManager.getApplication().assertIsDispatchThread(); // don't add files that cannot be found via VFM (light & etc.) if (VirtualFileManager.getInstance().findFileByUrl(file.getUrl()) == null) return; final FileEditorManagerEx editorManager = FileEditorManagerEx.getInstanceEx(myProject); final Pair<FileEditor[], FileEditorProvider[]> editorsWithProviders = editorManager.getEditorsWithProviders(file); FileEditor[] editors = editorsWithProviders.getFirst(); FileEditorProvider[] oldProviders = editorsWithProviders.getSecond(); if (editors.length <= 0 && fallbackEditor != null) { editors = new FileEditor[]{fallbackEditor}; } if (oldProviders.length <= 0 && fallbackProvider != null) { oldProviders = new FileEditorProvider[]{fallbackProvider}; } if (editors.length <= 0) { LOG.error("No editors for file " + file.getPresentableUrl()); } FileEditor selectedEditor = editorManager.getSelectedEditor(file); if (selectedEditor == null) { selectedEditor = fallbackEditor; } LOG.assertTrue(selectedEditor != null); final int selectedProviderIndex = ArrayUtilRt.find(editors, selectedEditor); LOG.assertTrue(selectedProviderIndex != -1, "Can't find " + selectedEditor + " among " + Arrays.asList(editors)); final HistoryEntry entry = getEntry(file); if (entry != null) { moveOnTop(entry); } else { final FileEditorState[] states = new FileEditorState[editors.length]; final FileEditorProvider[] providers = new FileEditorProvider[editors.length]; for (int i = states.length - 1; i >= 0; i--) { final FileEditorProvider provider = oldProviders[i]; LOG.assertTrue(provider != null); FileEditor editor = editors[i]; if (!editor.isValid()) continue; providers[i] = provider; states[i] = editor.getState(FileEditorStateLevel.FULL); } addEntry(HistoryEntry.createHeavy(myProject, file, providers, states, providers[selectedProviderIndex])); trimToSize(); } } public void updateHistoryEntry(@Nullable final VirtualFile file, final boolean changeEntryOrderOnly) { updateHistoryEntry(file, null, null, changeEntryOrderOnly); } private void updateHistoryEntry(@Nullable final VirtualFile file, @Nullable final FileEditor fallbackEditor, @Nullable FileEditorProvider fallbackProvider, final boolean changeEntryOrderOnly) { if (file == null) { return; } final FileEditorManagerEx editorManager = FileEditorManagerEx.getInstanceEx(myProject); final Pair<FileEditor[], FileEditorProvider[]> editorsWithProviders = editorManager.getEditorsWithProviders(file); FileEditor[] editors = editorsWithProviders.getFirst(); FileEditorProvider[] providers = editorsWithProviders.getSecond(); if (editors.length <= 0 && fallbackEditor != null) { editors = new FileEditor[]{fallbackEditor}; providers = new FileEditorProvider[]{fallbackProvider}; } if (editors.length == 0) { // obviously not opened in any editor at the moment, // makes no sense to put the file in the history return; } final HistoryEntry entry = getEntry(file); if (entry == null) { // Size of entry list can be less than number of opened editors (some entries can be removed) if (file.isValid()) { // the file could have been deleted, so the isValid() check is essential fileOpenedImpl(file, fallbackEditor, fallbackProvider); } return; } if (!changeEntryOrderOnly) { // update entry state //LOG.assertTrue(editors.length > 0); for (int i = editors.length - 1; i >= 0; i--) { final FileEditor editor = editors[i]; final FileEditorProvider provider = providers[i]; if (!editor.isValid()) { // this can happen for example if file extension was changed // and this method was called during corresponding myEditor close up continue; } final FileEditorState oldState = entry.getState(provider); final FileEditorState newState = editor.getState(FileEditorStateLevel.FULL); if (!newState.equals(oldState)) { entry.putState(provider, newState); } } } final Pair<FileEditor, FileEditorProvider> selectedEditorWithProvider = editorManager.getSelectedEditorWithProvider(file); if (selectedEditorWithProvider != null) { //LOG.assertTrue(selectedEditorWithProvider != null); entry.setSelectedProvider(selectedEditorWithProvider.getSecond()); LOG.assertTrue(entry.getSelectedProvider() != null); if (changeEntryOrderOnly) { moveOnTop(entry); } } } /** * @return array of valid files that are in the history, oldest first. May contain duplicates. */ public synchronized VirtualFile[] getFiles() { final List<VirtualFile> result = new ArrayList<>(myEntriesList.size()); for (HistoryEntry entry : myEntriesList) { VirtualFile file = entry.getFile(); if (file != null) result.add(file); } return VfsUtilCore.toVirtualFileArray(result); } /** * @return a set of valid files that are in the history, oldest first. */ public LinkedHashSet<VirtualFile> getFileSet() { LinkedHashSet<VirtualFile> result = ContainerUtil.newLinkedHashSet(); for (VirtualFile file : getFiles()) { // if the file occurs several times in the history, only its last occurrence counts result.remove(file); result.add(file); } return result; } public synchronized boolean hasBeenOpen(@NotNull VirtualFile f) { for (HistoryEntry each : myEntriesList) { if (Comparing.equal(each.getFile(), f)) return true; } return false; } /** * Removes specified <code>file</code> from history. The method does * nothing if <code>file</code> is not in the history. * * @throws IllegalArgumentException if <code>file</code> * is <code>null</code> */ public synchronized void removeFile(@NotNull final VirtualFile file) { final HistoryEntry entry = getEntry(file); if (entry != null) { removeEntry(entry); } } public FileEditorState getState(@NotNull VirtualFile file, final FileEditorProvider provider) { final HistoryEntry entry = getEntry(file); return entry != null ? entry.getState(provider) : null; } /** * @return may be null */ public FileEditorProvider getSelectedProvider(final VirtualFile file) { final HistoryEntry entry = getEntry(file); return entry != null ? entry.getSelectedProvider() : null; } private synchronized HistoryEntry getEntry(@NotNull VirtualFile file) { for (int i = myEntriesList.size() - 1; i >= 0; i--) { final HistoryEntry entry = myEntriesList.get(i); VirtualFile entryFile = entry.getFile(); if (file.equals(entryFile)) { return entry; } } return null; } /** * If total number of files in history more then <code>UISettings.RECENT_FILES_LIMIT</code> * then removes the oldest ones to fit the history to new size. */ private synchronized void trimToSize() { final int limit = UISettings.getInstance().RECENT_FILES_LIMIT + 1; while (myEntriesList.size() > limit) { HistoryEntry removed = myEntriesList.remove(0); removed.destroy(); } } @Override public void loadState(@NotNull Element element) { // we have to delay xml processing because history entries require EditorStates to be created // which is done via corresponding EditorProviders, those are not accessible before their // is initComponent() called final Element state = element.clone(); StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new DumbAwareRunnable() { @Override public void run() { for (Element e : state.getChildren(HistoryEntry.TAG)) { try { addEntry(HistoryEntry.createHeavy(myProject, e)); } catch (InvalidDataException e1) { // OK here } catch (ProcessCanceledException e1) { // OK here } catch (Exception anyException) { LOG.error(anyException); } } trimToSize(); } }); } @Override public synchronized Element getState() { Element element = new Element("state"); // update history before saving final VirtualFile[] openFiles = FileEditorManager.getInstance(myProject).getOpenFiles(); for (int i = openFiles.length - 1; i >= 0; i--) { final VirtualFile file = openFiles[i]; // we have to update only files that are in history if (getEntry(file) != null) { updateHistoryEntry(file, false); } } for (final HistoryEntry entry : myEntriesList) { entry.writeExternal(element, myProject); } return element; } @Override public void projectOpened() { } @Override public void projectClosed() { } @Override public void initComponent() { } @Override public synchronized void disposeComponent() { for (HistoryEntry entry : myEntriesList) { entry.destroy(); } myEntriesList.clear(); } @NotNull @Override public String getComponentName() { return "editorHistoryManager"; } /** * Updates history */ private final class MyEditorManagerListener extends FileEditorManagerAdapter { @Override public void fileOpened(@NotNull final FileEditorManager source, @NotNull final VirtualFile file) { fileOpenedImpl(file, null, null); } @Override public void selectionChanged(@NotNull final FileEditorManagerEvent event) { // updateHistoryEntry does commitDocument which is 1) very expensive and 2) cannot be performed from within PSI change listener // so defer updating history entry until documents committed to improve responsiveness PsiDocumentManager.getInstance(myProject).performWhenAllCommitted(() -> { updateHistoryEntry(event.getOldFile(), event.getOldEditor(), event.getOldProvider(), false); updateHistoryEntry(event.getNewFile(), true); }); } } }