blob: 99532035d64c28ecedd550227a6e1d161d0f3d99 [file] [log] [blame]
/*
* Copyright 2000-2012 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.ide.bookmarks;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.components.*;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.DumbAwareRunnable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.ui.UIUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.event.InputEvent;
import java.util.*;
import java.util.List;
@State(
name = "BookmarkManager",
storages = {
@Storage( file = StoragePathMacros.WORKSPACE_FILE)
}
)
public class BookmarkManager extends AbstractProjectComponent implements PersistentStateComponent<Element> {
private static final int MAX_AUTO_DESCRIPTION_SIZE = 50;
private final List<Bookmark> myBookmarks = new ArrayList<Bookmark>();
private final MessageBus myBus;
public static BookmarkManager getInstance(Project project) {
return project.getComponent(BookmarkManager.class);
}
public BookmarkManager(Project project, MessageBus bus, PsiDocumentManager documentManager) {
super(project);
myBus = bus;
EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster();
multicaster.addDocumentListener(new MyDocumentListener(), myProject);
multicaster.addEditorMouseListener(new MyEditorMouseListener(), myProject);
documentManager.addListener(new PsiDocumentManager.Listener() {
@Override
public void documentCreated(@NotNull final Document document, PsiFile psiFile) {
final VirtualFile file = FileDocumentManager.getInstance().getFile(document);
if (file == null) return;
for (final Bookmark bookmark : myBookmarks) {
if (Comparing.equal(bookmark.getFile(), file)) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (myProject.isDisposed()) return;
bookmark.createHighlighter((MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true));
}
});
}
}
}
@Override
public void fileCreated(@NotNull PsiFile file, @NotNull Document document) {
}
});
}
public void editDescription(@NotNull Bookmark bookmark) {
String description = Messages
.showInputDialog(myProject, IdeBundle.message("action.bookmark.edit.description.dialog.message"),
IdeBundle.message("action.bookmark.edit.description.dialog.title"), Messages.getQuestionIcon(),
bookmark.getDescription(), new InputValidator() {
@Override
public boolean checkInput(String inputString) {
return true;
}
@Override
public boolean canClose(String inputString) {
return true;
}
});
if (description != null) {
setDescription(bookmark, description);
}
}
@NotNull
@Override
public String getComponentName() {
return "BookmarkManager";
}
public void addEditorBookmark(Editor editor, int lineIndex) {
Document document = editor.getDocument();
PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
if (psiFile == null) return;
final VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile == null) return;
addTextBookmark(virtualFile, lineIndex, getAutoDescription(editor, lineIndex));
}
public Bookmark addTextBookmark(VirtualFile file, int lineIndex, String description) {
Bookmark b = new Bookmark(myProject, file, lineIndex, description);
myBookmarks.add(0, b);
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkAdded(b);
return b;
}
public static String getAutoDescription(final Editor editor, final int lineIndex) {
String autoDescription = editor.getSelectionModel().getSelectedText();
if ( autoDescription == null ) {
Document document = editor.getDocument();
autoDescription = document.getCharsSequence()
.subSequence(document.getLineStartOffset(lineIndex), document.getLineEndOffset(lineIndex)).toString().trim();
}
if ( autoDescription.length () > MAX_AUTO_DESCRIPTION_SIZE) {
return autoDescription.substring(0, MAX_AUTO_DESCRIPTION_SIZE)+"...";
}
return autoDescription;
}
@Nullable
public Bookmark addFileBookmark(VirtualFile file, String description) {
if (file == null) return null;
if (findFileBookmark(file) != null) return null;
Bookmark b = new Bookmark(myProject, file, -1, description);
myBookmarks.add(0, b);
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkAdded(b);
return b;
}
@NotNull
public List<Bookmark> getValidBookmarks() {
List<Bookmark> answer = new ArrayList<Bookmark>();
for (Bookmark bookmark : myBookmarks) {
if (bookmark.isValid()) answer.add(bookmark);
}
return answer;
}
@Nullable
public Bookmark findEditorBookmark(@NotNull Document document, int line) {
for (Bookmark bookmark : myBookmarks) {
if (bookmark.getDocument() == document && bookmark.getLine() == line) {
return bookmark;
}
}
return null;
}
@Nullable
public Bookmark findFileBookmark(@NotNull VirtualFile file) {
for (Bookmark bookmark : myBookmarks) {
if (Comparing.equal(bookmark.getFile(), file) && bookmark.getLine() == -1) return bookmark;
}
return null;
}
@Nullable
public Bookmark findBookmarkForMnemonic(char m) {
final char mm = Character.toUpperCase(m);
for (Bookmark bookmark : myBookmarks) {
if (mm == bookmark.getMnemonic()) return bookmark;
}
return null;
}
public boolean hasBookmarksWithMnemonics() {
for (Bookmark bookmark : myBookmarks) {
if (bookmark.getMnemonic() != 0) return true;
}
return false;
}
public void removeBookmark(@NotNull Bookmark bookmark) {
myBookmarks.remove(bookmark);
bookmark.release();
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkRemoved(bookmark);
}
@Override
public Element getState() {
Element container = new Element("BookmarkManager");
writeExternal(container);
return container;
}
@Override
public void loadState(final Element state) {
StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new DumbAwareRunnable() {
@Override
public void run() {
BookmarksListener publisher = myBus.syncPublisher(BookmarksListener.TOPIC);
for (Bookmark bookmark : myBookmarks) {
bookmark.release();
publisher.bookmarkRemoved(bookmark);
}
myBookmarks.clear();
readExternal(state);
}
});
}
private void readExternal(Element element) {
for (final Object o : element.getChildren()) {
Element bookmarkElement = (Element)o;
if ("bookmark".equals(bookmarkElement.getName())) {
String url = bookmarkElement.getAttributeValue("url");
String line = bookmarkElement.getAttributeValue("line");
String description = StringUtil.notNullize(bookmarkElement.getAttributeValue("description"));
String mnemonic = bookmarkElement.getAttributeValue("mnemonic");
Bookmark b = null;
VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url);
if (file != null) {
if (line != null) {
try {
int lineIndex = Integer.parseInt(line);
b = addTextBookmark(file, lineIndex, description);
}
catch (NumberFormatException e) {
// Ignore. Will miss bookmark if line number cannot be parsed
}
}
else {
b = addFileBookmark(file, description);
}
}
if (b != null && mnemonic != null && mnemonic.length() == 1) {
setMnemonic(b, mnemonic.charAt(0));
}
}
}
}
private void writeExternal(Element element) {
List<Bookmark> reversed = new ArrayList<Bookmark>(myBookmarks);
Collections.reverse(reversed);
for (Bookmark bookmark : reversed) {
if (!bookmark.isValid()) continue;
Element bookmarkElement = new Element("bookmark");
bookmarkElement.setAttribute("url", bookmark.getFile().getUrl());
String description = bookmark.getNotEmptyDescription();
if (description != null) {
bookmarkElement.setAttribute("description", description);
}
int line = bookmark.getLine();
if (line >= 0) {
bookmarkElement.setAttribute("line", String.valueOf(line));
}
char mnemonic = bookmark.getMnemonic();
if (mnemonic != 0) {
bookmarkElement.setAttribute("mnemonic", String.valueOf(mnemonic));
}
element.addContent(bookmarkElement);
}
}
/**
* Try to move bookmark one position up in the list
*
* @return bookmark list after moving
*/
@NotNull
public List<Bookmark> moveBookmarkUp(@NotNull Bookmark bookmark) {
final int index = myBookmarks.indexOf(bookmark);
if (index > 0) {
Collections.swap(myBookmarks, index, index - 1);
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index));
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index - 1));
}
});
}
return myBookmarks;
}
/**
* Try to move bookmark one position down in the list
*
* @return bookmark list after moving
*/
@NotNull
public List<Bookmark> moveBookmarkDown(@NotNull Bookmark bookmark) {
final int index = myBookmarks.indexOf(bookmark);
if (index < myBookmarks.size() - 1) {
Collections.swap(myBookmarks, index, index + 1);
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index));
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index + 1));
}
});
}
return myBookmarks;
}
@Nullable
public Bookmark getNextBookmark(@NotNull Editor editor, boolean isWrapped) {
Bookmark[] bookmarksForDocument = getBookmarksForDocument(editor.getDocument());
int lineNumber = editor.getCaretModel().getLogicalPosition().line;
for (Bookmark bookmark : bookmarksForDocument) {
if (bookmark.getLine() > lineNumber) return bookmark;
}
if (isWrapped && bookmarksForDocument.length > 0) {
return bookmarksForDocument[0];
}
return null;
}
@Nullable
public Bookmark getPreviousBookmark(@NotNull Editor editor, boolean isWrapped) {
Bookmark[] bookmarksForDocument = getBookmarksForDocument(editor.getDocument());
int lineNumber = editor.getCaretModel().getLogicalPosition().line;
for (int i = bookmarksForDocument.length - 1; i >= 0; i--) {
Bookmark bookmark = bookmarksForDocument[i];
if (bookmark.getLine() < lineNumber) return bookmark;
}
if (isWrapped && bookmarksForDocument.length > 0) {
return bookmarksForDocument[bookmarksForDocument.length - 1];
}
return null;
}
@NotNull
private Bookmark[] getBookmarksForDocument(@NotNull Document document) {
ArrayList<Bookmark> answer = new ArrayList<Bookmark>();
for (Bookmark bookmark : getValidBookmarks()) {
if (document.equals(bookmark.getDocument())) {
answer.add(bookmark);
}
}
Bookmark[] bookmarks = answer.toArray(new Bookmark[answer.size()]);
Arrays.sort(bookmarks, new Comparator<Bookmark>() {
@Override
public int compare(final Bookmark o1, final Bookmark o2) {
return o1.getLine() - o2.getLine();
}
});
return bookmarks;
}
public void setMnemonic(@NotNull Bookmark bookmark, char c) {
final Bookmark old = findBookmarkForMnemonic(c);
if (old != null) removeBookmark(old);
bookmark.setMnemonic(c);
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(bookmark);
}
public void setDescription(@NotNull Bookmark bookmark, String description) {
bookmark.setDescription(description);
myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(bookmark);
}
public void colorsChanged() {
for (Bookmark bookmark : myBookmarks) {
bookmark.updateHighlighter();
}
}
private class MyEditorMouseListener extends EditorMouseAdapter {
@Override
public void mouseClicked(final EditorMouseEvent e) {
if (e.getArea() != EditorMouseEventArea.LINE_MARKERS_AREA) return;
if (e.getMouseEvent().isPopupTrigger()) return;
if ((e.getMouseEvent().getModifiers() & (SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK)) == 0) return;
Editor editor = e.getEditor();
int line = editor.xyToLogicalPosition(new Point(e.getMouseEvent().getX(), e.getMouseEvent().getY())).line;
if (line < 0) return;
Document document = editor.getDocument();
Bookmark bookmark = findEditorBookmark(document, line);
if (bookmark == null) {
addEditorBookmark(editor, line);
}
else {
removeBookmark(bookmark);
}
e.consume();
}
}
private class MyDocumentListener extends DocumentAdapter {
@Override
public void documentChanged(DocumentEvent e) {
List<Bookmark> bookmarksToRemove = null;
for (Bookmark bookmark : myBookmarks) {
if (!bookmark.isValid()) {
if (bookmarksToRemove == null) {
bookmarksToRemove = new ArrayList<Bookmark>();
}
bookmarksToRemove.add(bookmark);
}
}
if (bookmarksToRemove != null) {
for (Bookmark bookmark : bookmarksToRemove) {
removeBookmark(bookmark);
}
}
}
}
}