All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition platform-impl library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * Copyright 2000-2015 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.openapi.command.CommandAdapter;
import com.intellij.openapi.command.CommandEvent;
import com.intellij.openapi.command.CommandListener;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.impl.CommandMerger;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.wm.ToolWindowManager;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.lang.ref.WeakReference;
import java.util.*;

@State(
    name = "IdeDocumentHistory",
    storages = {@Storage(file = StoragePathMacros.WORKSPACE_FILE)}
)
public class IdeDocumentHistoryImpl extends IdeDocumentHistory implements ProjectComponent, PersistentStateComponent {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl");

  private static final int BACK_QUEUE_LIMIT = Registry.intValue("editor.navigation.history.stack.size");
  private static final int CHANGE_QUEUE_LIMIT = Registry.intValue("editor.navigation.history.stack.size");

  private final Project myProject;

  private final EditorFactory myEditorFactory;
  private FileDocumentManager myFileDocumentManager;
  private FileEditorManagerEx myEditorManager;
  private final VirtualFileManager myVfManager;
  private final CommandProcessor myCmdProcessor;
  private final ToolWindowManager myToolWindowManager;

  private final LinkedList myBackPlaces = new LinkedList(); // LinkedList of PlaceInfo's
  private final LinkedList myForwardPlaces = new LinkedList(); // LinkedList of PlaceInfo's
  private boolean myBackInProgress = false;
  private boolean myForwardInProgress = false;
  private Object myLastGroupId = null;

  // change's navigation
  private final LinkedList myChangePlaces = new LinkedList(); // LinkedList of PlaceInfo's
  private int myStartIndex = 0;
  private int myCurrentIndex = 0;
  private PlaceInfo myCurrentChangePlace = null;

  private PlaceInfo myCommandStartPlace = null;
  private boolean myCurrentCommandIsNavigation = false;
  private boolean myCurrentCommandHasChanges = false;
  private final Set myChangedFilesInCurrentCommand = new THashSet();
  private boolean myCurrentCommandHasMoves = false;

  private final CommandListener myCommandListener = new CommandAdapter() {
    @Override
    public void commandStarted(CommandEvent event) {
      onCommandStarted();
    }

    @Override
    public void commandFinished(CommandEvent event) {
      onCommandFinished(event.getCommandGroupId());
    }
  };

  private RecentlyChangedFilesState myRecentlyChangedFiles = new RecentlyChangedFilesState();

  public IdeDocumentHistoryImpl(@NotNull Project project,
                                @NotNull EditorFactory editorFactory,
                                @NotNull FileEditorManager editorManager,
                                @NotNull VirtualFileManager vfManager,
                                @NotNull CommandProcessor cmdProcessor,
                                @NotNull ToolWindowManager toolWindowManager) {
    myProject = project;
    myEditorFactory = editorFactory;
    myEditorManager = (FileEditorManagerEx)editorManager;
    myVfManager = vfManager;
    myCmdProcessor = cmdProcessor;
    myToolWindowManager = toolWindowManager;
  }

  @Override
  public final void projectOpened() {
    myEditorManager = (FileEditorManagerEx)FileEditorManager.getInstance(myProject);
    EditorEventMulticaster eventMulticaster = myEditorFactory.getEventMulticaster();

    DocumentListener documentListener = new DocumentAdapter() {
      @Override
      public void documentChanged(DocumentEvent e) {
        onDocumentChanged(e);
      }
    };
    eventMulticaster.addDocumentListener(documentListener, myProject);

    CaretListener caretListener = new CaretAdapter() {
      @Override
      public void caretPositionChanged(CaretEvent e) {
        onCaretPositionChanged(e);
      }
    };
    eventMulticaster.addCaretListener(caretListener,myProject);

    myProject.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerAdapter() {
      @Override
      public void selectionChanged(@NotNull FileEditorManagerEvent e) {
        onSelectionChanged();
      }
    });

    VirtualFileListener fileListener = new VirtualFileAdapter() {
      @Override
      public void fileDeleted(@NotNull VirtualFileEvent event) {
        onFileDeleted();
      }
    };
    myVfManager.addVirtualFileListener(fileListener,myProject);
    myCmdProcessor.addCommandListener(myCommandListener,myProject);
  }

  public static class RecentlyChangedFilesState {
    // don't make it private, see: IDEA-130363 Recently Edited Files list should survive restart
    public List CHANGED_PATHS = new ArrayList();

    public void register(VirtualFile file) {
      final String path = file.getPath();
      CHANGED_PATHS.remove(path);
      CHANGED_PATHS.add(path);
      trimToSize();
    }

    private void trimToSize(){
      final int limit = UISettings.getInstance().RECENT_FILES_LIMIT + 1;
      while(CHANGED_PATHS.size()>limit){
        CHANGED_PATHS.remove(0);
      }
    }
  }

  @Override
  public RecentlyChangedFilesState getState() {
    return myRecentlyChangedFiles;
  }

  @Override
  public void loadState(RecentlyChangedFilesState state) {
    myRecentlyChangedFiles = state;
  }

  final void onFileDeleted() {
    removeInvalidFilesFromStacks();
  }

  public final void onSelectionChanged() {
    myCurrentCommandIsNavigation = true;
    myCurrentCommandHasMoves = true;
  }

  private void onCaretPositionChanged(CaretEvent e) {
    if (e.getOldPosition().line == e.getNewPosition().line) return;
    Document document = e.getEditor().getDocument();
    if (getFileDocumentManager().getFile(document) != null) {
      myCurrentCommandHasMoves = true;
    }
  }

  private void onDocumentChanged(DocumentEvent e) {
    Document document = e.getDocument();
    final VirtualFile file = getFileDocumentManager().getFile(document);
    if (file != null) {
      myCurrentCommandHasChanges = true;
      myChangedFilesInCurrentCommand.add(file);
    }
  }

  final void onCommandStarted() {
    myCommandStartPlace = getCurrentPlaceInfo();
    myCurrentCommandIsNavigation = false;
    myCurrentCommandHasChanges = false;
    myCurrentCommandHasMoves = false;
    myChangedFilesInCurrentCommand.clear();
  }

  private PlaceInfo getCurrentPlaceInfo() {
    final Pair selectedEditorWithProvider = getSelectedEditor();
    if (selectedEditorWithProvider != null) {
      return createPlaceInfo(selectedEditorWithProvider.getFirst (), selectedEditorWithProvider.getSecond ());
    }
    return null;
  }

  final void onCommandFinished(Object commandGroupId) {
    if (myCommandStartPlace != null) {
      if (myCurrentCommandIsNavigation && myCurrentCommandHasMoves) {
        if (!myBackInProgress) {
          if (!CommandMerger.canMergeGroup(commandGroupId, myLastGroupId)) {
            putLastOrMerge(myBackPlaces, myCommandStartPlace, BACK_QUEUE_LIMIT);
          }
          if (!myForwardInProgress) {
            myForwardPlaces.clear();
          }
        }
        removeInvalidFilesFromStacks();
      }
    }
    myLastGroupId = commandGroupId;

    if (myCurrentCommandHasChanges) {
      setCurrentChangePlace();
    }
    else if (myCurrentCommandHasMoves) {
      pushCurrentChangePlace();
    }
  }


  @Override
  public final void projectClosed() {
  }

  @Override
  public final void includeCurrentCommandAsNavigation() {
    myCurrentCommandIsNavigation = true;
  }

  @Override
  public final void includeCurrentPlaceAsChangePlace() {
    setCurrentChangePlace();
    pushCurrentChangePlace();
  }

  private void setCurrentChangePlace() {
    final PlaceInfo placeInfo = getCurrentPlaceInfo();
    if (placeInfo == null) {
      return;
    }

    final VirtualFile file = placeInfo.getFile();
    if (myChangedFilesInCurrentCommand.contains(file)) {
      myRecentlyChangedFiles.register(file);

      myCurrentChangePlace = placeInfo;
      if (!myChangePlaces.isEmpty()) {
        final PlaceInfo lastInfo = myChangePlaces.getLast();
        if (isSame(placeInfo, lastInfo)) {
          myChangePlaces.removeLast();
        }
      }
      myCurrentIndex = myStartIndex + myChangePlaces.size();
    }
  }

  private void pushCurrentChangePlace() {
    if (myCurrentChangePlace != null) {
      myChangePlaces.add(myCurrentChangePlace);
      if (myChangePlaces.size() > CHANGE_QUEUE_LIMIT) {
        myChangePlaces.removeFirst();
        myStartIndex++;
      }
      myCurrentChangePlace = null;
    }
    myCurrentIndex = myStartIndex + myChangePlaces.size();
  }

  @Override
  public VirtualFile[] getChangedFiles() {
    List files = new ArrayList();

    final LocalFileSystem lfs = LocalFileSystem.getInstance();
    final List paths = myRecentlyChangedFiles.CHANGED_PATHS;
    for (String path : paths) {
      final VirtualFile file = lfs.findFileByPath(path);
      if (file != null) {
        files.add(file);
      }
    }

    return VfsUtilCore.toVirtualFileArray(files);
  }

  @Override
  public final void clearHistory() {
    myBackPlaces.clear();
    myForwardPlaces.clear();
    myChangePlaces.clear();

    myLastGroupId = null;

    myStartIndex = 0;
    myCurrentIndex = 0;
    myCurrentChangePlace = null;
    myCommandStartPlace = null;
  }

  @Override
  public final void back() {
    removeInvalidFilesFromStacks();
    if (myBackPlaces.isEmpty()) return;
    final PlaceInfo info = myBackPlaces.removeLast();

    PlaceInfo current = getCurrentPlaceInfo();
    if (current != null) {
      if (!isSame(current, info)) {
        putLastOrMerge(myForwardPlaces, current, Integer.MAX_VALUE);
      }
    }
    putLastOrMerge(myForwardPlaces, info, Integer.MAX_VALUE);

    myBackInProgress = true;

    executeCommand(new Runnable() {
      @Override
      public void run() {
        gotoPlaceInfo(info);
      }
    }, "", null);

    myBackInProgress = false;
  }

  @Override
  public final void forward() {
    removeInvalidFilesFromStacks();

    final PlaceInfo target = getTargetForwardInfo();
    if (target == null) return;

    myForwardInProgress = true;
    executeCommand(new Runnable() {
      @Override
      public void run() {
        gotoPlaceInfo(target);
      }
    }, "", null);
    myForwardInProgress = false;
  }

  private PlaceInfo getTargetForwardInfo() {
    if (myForwardPlaces.isEmpty()) return null;

    PlaceInfo target = myForwardPlaces.removeLast();
    PlaceInfo current = getCurrentPlaceInfo();

    while (!myForwardPlaces.isEmpty()) {
      if (current != null && isSame(current, target)) {
        target = myForwardPlaces.removeLast();
      }
      else {
        break;
      }
    }
    return target;
  }

  @Override
  public final boolean isBackAvailable() {
    return !myBackPlaces.isEmpty();
  }

  @Override
  public final boolean isForwardAvailable() {
    return !myForwardPlaces.isEmpty();
  }

  @Override
  public final void navigatePreviousChange() {
    removeInvalidFilesFromStacks();
    if (myCurrentIndex == myStartIndex) return;
    int index = myCurrentIndex - 1;
    final PlaceInfo info = myChangePlaces.get(index - myStartIndex);

    executeCommand(new Runnable() {
      @Override
      public void run() {
        gotoPlaceInfo(info);
      }
    }, "", null);
    myCurrentIndex = index;
  }

  @Override
  public final boolean isNavigatePreviousChangeAvailable() {
    return myCurrentIndex > myStartIndex;
  }

  private void removeInvalidFilesFromStacks() {
    removeInvalidFilesFrom(myBackPlaces);

    removeInvalidFilesFrom(myForwardPlaces);
    if (removeInvalidFilesFrom(myChangePlaces)) {
      myCurrentIndex = myStartIndex + myChangePlaces.size();
    }
  }

  @Override
  public void navigateNextChange() {
    removeInvalidFilesFromStacks();
    if (myCurrentIndex >= myStartIndex + myChangePlaces.size() - 1) return;
    int index = myCurrentIndex + 1;
    final PlaceInfo info = myChangePlaces.get(index - myStartIndex);

    executeCommand(new Runnable() {
      @Override
      public void run() {
        gotoPlaceInfo(info);
      }
    }, "", null);
    myCurrentIndex = index;
  }

  @Override
  public boolean isNavigateNextChangeAvailable() {
    return myCurrentIndex < myStartIndex + myChangePlaces.size() - 1;
  }

  private static boolean removeInvalidFilesFrom(@NotNull List backPlaces) {
    boolean removed = false;
    for (Iterator iterator = backPlaces.iterator(); iterator.hasNext();) {
      PlaceInfo info = iterator.next();
      final VirtualFile file = info.myFile;
      if (!file.isValid()) {
        iterator.remove();
        removed = true;
      }
    }

    return removed;
  }

  private void gotoPlaceInfo(@NotNull PlaceInfo info) { // TODO: Msk
    final boolean wasActive = myToolWindowManager.isEditorComponentActive();
    EditorWindow wnd = info.getWindow();
    final Pair editorsWithProviders;
    if (wnd != null && wnd.isValid()) {
      editorsWithProviders = myEditorManager.openFileWithProviders(info.getFile(), wasActive, wnd);
    } else {
      editorsWithProviders = myEditorManager.openFileWithProviders(info.getFile(), wasActive, false);
    }

    myEditorManager.setSelectedEditor(info.getFile(), info.getEditorTypeId());

    final FileEditor[] editors = editorsWithProviders.getFirst();
    final FileEditorProvider[] providers = editorsWithProviders.getSecond();
    for (int i = 0; i < editors.length; i++) {
      String typeId = providers [i].getEditorTypeId();
      if (typeId.equals(info.getEditorTypeId())) {
        editors[i].setState(info.getNavigationState());
      }
    }
  }

  /**
   * @return currently selected FileEditor or null.
   */
  protected Pair getSelectedEditor() {
    VirtualFile file = myEditorManager.getCurrentFile();
    return file != null ? myEditorManager.getSelectedEditorWithProvider(file) : null;
  }

  private PlaceInfo createPlaceInfo(@NotNull final FileEditor fileEditor, final FileEditorProvider fileProvider) {
    final VirtualFile file = myEditorManager.getFile(fileEditor);
    LOG.assertTrue(file != null);

    final FileEditorState state = fileEditor.getState(FileEditorStateLevel.NAVIGATION);

    return new PlaceInfo(file, state, fileProvider.getEditorTypeId(), myEditorManager.getCurrentWindow());
  }


  @Override
  @NotNull
  public final String getComponentName() {
    return "IdeDocumentHistory";
  }

  private static void putLastOrMerge(@NotNull LinkedList list, @NotNull PlaceInfo next, int limitSizeLimit) {
    if (!list.isEmpty()) {
      PlaceInfo prev = list.getLast();
      if (isSame(prev, next)) {
        list.removeLast();
      }
    }

    list.add(next);
    if (list.size() > limitSizeLimit) {
      list.removeFirst();
    }
  }

  private FileDocumentManager getFileDocumentManager() {
    if (myFileDocumentManager == null) {
      myFileDocumentManager = FileDocumentManager.getInstance();
    }
    return myFileDocumentManager;
  }

  private static final class PlaceInfo {

    private final VirtualFile myFile;
    private final FileEditorState myNavigationState;
    private final String myEditorTypeId;
    private final WeakReference myWindow;

    public PlaceInfo(@NotNull VirtualFile file, @NotNull FileEditorState navigationState, @NotNull String editorTypeId, @Nullable EditorWindow window) {
      myNavigationState = navigationState;
      myFile = file;
      myEditorTypeId = editorTypeId;
      myWindow = new WeakReference(window);
    }

    public EditorWindow getWindow() {
      return myWindow.get();
    }

    @NotNull
    private FileEditorState getNavigationState() {
      return myNavigationState;
    }

    @NotNull
    public VirtualFile getFile() {
      return myFile;
    }

    @NotNull
    public String getEditorTypeId() {
      return myEditorTypeId;
    }

    @Override
    public String toString() {
      return getFile().getName() + " " + getNavigationState();
    }

  }

  @NotNull
  @TestOnly
  List getBackPlaces() {
    return myBackPlaces;
  }

  @Override
  public final void initComponent() { }

  @Override
  public final void disposeComponent() {
    myLastGroupId = null;
  }

  protected void executeCommand(Runnable runnable, String name, Object groupId) {
    myCmdProcessor.executeCommand(myProject, runnable, name, groupId);
  }

  private static boolean isSame(@NotNull PlaceInfo first, @NotNull PlaceInfo second) {
    if (first.getFile().equals(second.getFile())) {
      FileEditorState firstState = first.getNavigationState();
      FileEditorState secondState = second.getNavigationState();
      return firstState.equals(secondState) || firstState.canBeMergedWith(secondState, FileEditorStateLevel.NAVIGATION);
    }

    return false;
  }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy