blob: ddd5395805ff71f788eb6936b6d216b1a47b39a8 [file] [log] [blame]
/*
* Copyright 2000-2014 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.AbstractProjectComponent;
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.JDOMExternalizable;
import com.intellij.openapi.util.Pair;
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;
public final class EditorHistoryManager extends AbstractProjectComponent implements JDOMExternalizable {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.EditorHistoryManager");
private Element myElement;
public static EditorHistoryManager getInstance(final Project project){
return project.getComponent(EditorHistoryManager.class);
}
/**
* State corresponding to the most recent file is the last
*/
private final ArrayList<HistoryEntry> myEntriesList = new ArrayList<HistoryEntry>();
/** Invoked by reflection */
EditorHistoryManager(final Project project, final UISettings uiSettings){
super(project);
uiSettings.addUISettingsListener(new MyUISettingsListener(), project);
}
@Override
public void projectOpened(){
MessageBusConnection connection = myProject.getMessageBus().connect();
connection.subscribe(FileEditorManagerListener.Before.FILE_EDITOR_MANAGER, new MyEditorManagerBeforeListener());
connection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new MyEditorManagerListener());
StartupManager.getInstance(myProject).runWhenProjectIsInitialized(
new DumbAwareRunnable() {
@Override
public void run() {
// myElement may be null if node that corresponds to this manager does not exist
if (myElement != null) {
final List children = myElement.getChildren(HistoryEntry.TAG);
myElement = null;
//noinspection unchecked
for (final Element e : (Iterable<Element>)children) {
try {
myEntriesList.add(new HistoryEntry(myProject, e));
}
catch (InvalidDataException e1) {
// OK here
}
catch (ProcessCanceledException e1) {
// OK here
}
catch (Exception anyException) {
LOG.error(anyException);
}
}
trimToSize();
}
}
}
);
}
@Override
@NotNull
public String getComponentName(){
return "editorHistoryManager";
}
private void fileOpenedImpl(@NotNull final VirtualFile file) {
fileOpenedImpl(file, null, null);
}
/**
* 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){
myEntriesList.remove(entry);
myEntriesList.add(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);
providers[i] = provider;
states[i] = editors[i].getState(FileEditorStateLevel.FULL);
}
myEntriesList.add(new HistoryEntry(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.mySelectedProvider = selectedEditorWithProvider.getSecond ();
LOG.assertTrue(entry.mySelectedProvider != null);
if(changeEntryOrderOnly){
myEntriesList.remove(entry);
myEntriesList.add(entry);
}
}
}
/**
* Removes all entries that correspond to invalid files
*/
private void validateEntries(){
for(int i=myEntriesList.size()-1; i>=0; i--){
final HistoryEntry entry = myEntriesList.get(i);
if(!entry.myFile.isValid()){
myEntriesList.remove(i);
}
}
}
/**
* @return array of valid files that are in the history, oldest first. May contain duplicates.
*/
public VirtualFile[] getFiles(){
validateEntries();
final VirtualFile[] result = new VirtualFile[myEntriesList.size()];
for(int i=myEntriesList.size()-1; i>=0 ;i--){
result[i] = myEntriesList.get(i).myFile;
}
return 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 boolean hasBeenOpen(@NotNull VirtualFile f) {
for (HistoryEntry each : myEntriesList) {
if (Comparing.equal(each.myFile, 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.
*
* @exception java.lang.IllegalArgumentException if <code>file</code>
* is <code>null</code>
*/
public void removeFile(@NotNull final VirtualFile file){
final HistoryEntry entry = getEntry(file);
if(entry != null){
myEntriesList.remove(entry);
}
}
public FileEditorState getState(@NotNull VirtualFile file, final FileEditorProvider provider) {
validateEntries();
final HistoryEntry entry = getEntry(file);
return entry != null ? entry.getState(provider) : null;
}
/**
* @return may be null
*/
public FileEditorProvider getSelectedProvider(final VirtualFile file) {
validateEntries();
final HistoryEntry entry = getEntry(file);
return entry != null ? entry.mySelectedProvider : null;
}
private HistoryEntry getEntry(@NotNull VirtualFile file){
validateEntries();
for (int i = myEntriesList.size() - 1; i >= 0; i--) {
final HistoryEntry entry = myEntriesList.get(i);
if(file.equals(entry.myFile)){
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 void trimToSize(){
final int limit = UISettings.getInstance().RECENT_FILES_LIMIT + 1;
while(myEntriesList.size()>limit){
myEntriesList.remove(0);
}
}
@Override
public void readExternal(final 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
myElement = element.clone();
}
@Override
public void writeExternal(final Element element){
// 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];
if(getEntry(file) != null){ // we have to update only files that are in history
updateHistoryEntry(file, false);
}
}
for (final HistoryEntry entry : myEntriesList) {
entry.writeExternal(element, myProject);
}
}
/**
* Updates history
*/
private final class MyEditorManagerListener extends FileEditorManagerAdapter{
@Override
public void fileOpened(@NotNull final FileEditorManager source, @NotNull final VirtualFile file){
fileOpenedImpl(file);
}
@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(new Runnable() {
@Override
public void run() {
updateHistoryEntry(event.getOldFile(), event.getOldEditor(), event.getOldProvider(), false);
updateHistoryEntry(event.getNewFile(), true);
}
});
}
}
private final class MyEditorManagerBeforeListener extends FileEditorManagerListener.Before.Adapter {
@Override
public void beforeFileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
updateHistoryEntry(file, false);
}
}
/**
* Cuts/extends history length
*/
private final class MyUISettingsListener implements UISettingsListener{
@Override
public void uiSettingsChanged(final UISettings source) {
trimToSize();
}
}
}