2 * Copyright 2000-2012 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.intellij.ide.bookmarks;
19 import com.intellij.ide.IdeBundle;
20 import com.intellij.openapi.components.*;
21 import com.intellij.openapi.components.StoragePathMacros;
22 import com.intellij.openapi.editor.Document;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.editor.EditorFactory;
25 import com.intellij.openapi.editor.event.*;
26 import com.intellij.openapi.editor.ex.MarkupModelEx;
27 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
28 import com.intellij.openapi.fileEditor.FileDocumentManager;
29 import com.intellij.openapi.project.DumbAwareRunnable;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.startup.StartupManager;
32 import com.intellij.openapi.ui.InputValidator;
33 import com.intellij.openapi.ui.Messages;
34 import com.intellij.openapi.util.Comparing;
35 import com.intellij.openapi.util.SystemInfo;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.openapi.vfs.VirtualFileManager;
39 import com.intellij.psi.PsiDocumentManager;
40 import com.intellij.psi.PsiFile;
41 import com.intellij.util.messages.MessageBus;
42 import com.intellij.util.ui.UIUtil;
43 import org.jdom.Element;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
48 import java.awt.event.InputEvent;
50 import java.util.List;
53 name = "BookmarkManager",
55 @Storage(file = StoragePathMacros.WORKSPACE_FILE)
58 public class BookmarkManager extends AbstractProjectComponent implements PersistentStateComponent<Element> {
59 private static final int MAX_AUTO_DESCRIPTION_SIZE = 50;
61 private final List<Bookmark> myBookmarks = new ArrayList<Bookmark>();
63 private final MessageBus myBus;
65 public static BookmarkManager getInstance(Project project) {
66 return project.getComponent(BookmarkManager.class);
69 public BookmarkManager(Project project, MessageBus bus, PsiDocumentManager documentManager) {
72 EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster();
73 multicaster.addDocumentListener(new MyDocumentListener(), myProject);
74 multicaster.addEditorMouseListener(new MyEditorMouseListener(), myProject);
76 documentManager.addListener(new PsiDocumentManager.Listener() {
78 public void documentCreated(@NotNull final Document document, PsiFile psiFile) {
79 final VirtualFile file = FileDocumentManager.getInstance().getFile(document);
80 if (file == null) return;
81 for (final Bookmark bookmark : myBookmarks) {
82 if (Comparing.equal(bookmark.getFile(), file)) {
83 UIUtil.invokeLaterIfNeeded(new Runnable() {
86 if (myProject.isDisposed()) return;
87 bookmark.createHighlighter((MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true));
95 public void fileCreated(@NotNull PsiFile file, @NotNull Document document) {
100 public void editDescription(@NotNull Bookmark bookmark) {
101 String description = Messages
102 .showInputDialog(myProject, IdeBundle.message("action.bookmark.edit.description.dialog.message"),
103 IdeBundle.message("action.bookmark.edit.description.dialog.title"), Messages.getQuestionIcon(),
104 bookmark.getDescription(), new InputValidator() {
106 public boolean checkInput(String inputString) {
111 public boolean canClose(String inputString) {
115 if (description != null) {
116 setDescription(bookmark, description);
122 public String getComponentName() {
123 return "BookmarkManager";
126 public void addEditorBookmark(Editor editor, int lineIndex) {
127 Document document = editor.getDocument();
128 PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
129 if (psiFile == null) return;
131 final VirtualFile virtualFile = psiFile.getVirtualFile();
132 if (virtualFile == null) return;
134 addTextBookmark(virtualFile, lineIndex, getAutoDescription(editor, lineIndex));
137 public Bookmark addTextBookmark(VirtualFile file, int lineIndex, String description) {
138 Bookmark b = new Bookmark(myProject, file, lineIndex, description);
139 myBookmarks.add(0, b);
140 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkAdded(b);
144 public static String getAutoDescription(final Editor editor, final int lineIndex) {
145 String autoDescription = editor.getSelectionModel().getSelectedText();
146 if ( autoDescription == null ) {
147 Document document = editor.getDocument();
148 autoDescription = document.getCharsSequence()
149 .subSequence(document.getLineStartOffset(lineIndex), document.getLineEndOffset(lineIndex)).toString().trim();
151 if ( autoDescription.length () > MAX_AUTO_DESCRIPTION_SIZE) {
152 return autoDescription.substring(0, MAX_AUTO_DESCRIPTION_SIZE)+"...";
154 return autoDescription;
158 public Bookmark addFileBookmark(VirtualFile file, String description) {
159 if (file == null) return null;
160 if (findFileBookmark(file) != null) return null;
162 Bookmark b = new Bookmark(myProject, file, -1, description);
163 myBookmarks.add(0, b);
164 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkAdded(b);
170 public List<Bookmark> getValidBookmarks() {
171 List<Bookmark> answer = new ArrayList<Bookmark>();
172 for (Bookmark bookmark : myBookmarks) {
173 if (bookmark.isValid()) answer.add(bookmark);
180 public Bookmark findEditorBookmark(@NotNull Document document, int line) {
181 for (Bookmark bookmark : myBookmarks) {
182 if (bookmark.getDocument() == document && bookmark.getLine() == line) {
191 public Bookmark findFileBookmark(@NotNull VirtualFile file) {
192 for (Bookmark bookmark : myBookmarks) {
193 if (Comparing.equal(bookmark.getFile(), file) && bookmark.getLine() == -1) return bookmark;
200 public Bookmark findBookmarkForMnemonic(char m) {
201 final char mm = Character.toUpperCase(m);
202 for (Bookmark bookmark : myBookmarks) {
203 if (mm == bookmark.getMnemonic()) return bookmark;
208 public boolean hasBookmarksWithMnemonics() {
209 for (Bookmark bookmark : myBookmarks) {
210 if (bookmark.getMnemonic() != 0) return true;
216 public void removeBookmark(@NotNull Bookmark bookmark) {
217 myBookmarks.remove(bookmark);
219 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkRemoved(bookmark);
223 public Element getState() {
224 Element container = new Element("BookmarkManager");
225 writeExternal(container);
230 public void loadState(final Element state) {
231 StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new DumbAwareRunnable() {
234 BookmarksListener publisher = myBus.syncPublisher(BookmarksListener.TOPIC);
235 for (Bookmark bookmark : myBookmarks) {
237 publisher.bookmarkRemoved(bookmark);
246 private void readExternal(Element element) {
247 for (final Object o : element.getChildren()) {
248 Element bookmarkElement = (Element)o;
250 if ("bookmark".equals(bookmarkElement.getName())) {
251 String url = bookmarkElement.getAttributeValue("url");
252 String line = bookmarkElement.getAttributeValue("line");
253 String description = StringUtil.notNullize(bookmarkElement.getAttributeValue("description"));
254 String mnemonic = bookmarkElement.getAttributeValue("mnemonic");
257 VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url);
261 int lineIndex = Integer.parseInt(line);
262 b = addTextBookmark(file, lineIndex, description);
264 catch (NumberFormatException e) {
265 // Ignore. Will miss bookmark if line number cannot be parsed
269 b = addFileBookmark(file, description);
273 if (b != null && mnemonic != null && mnemonic.length() == 1) {
274 setMnemonic(b, mnemonic.charAt(0));
280 private void writeExternal(Element element) {
281 List<Bookmark> reversed = new ArrayList<Bookmark>(myBookmarks);
282 Collections.reverse(reversed);
284 for (Bookmark bookmark : reversed) {
285 if (!bookmark.isValid()) continue;
286 Element bookmarkElement = new Element("bookmark");
288 bookmarkElement.setAttribute("url", bookmark.getFile().getUrl());
290 String description = bookmark.getNotEmptyDescription();
291 if (description != null) {
292 bookmarkElement.setAttribute("description", description);
295 int line = bookmark.getLine();
297 bookmarkElement.setAttribute("line", String.valueOf(line));
300 char mnemonic = bookmark.getMnemonic();
302 bookmarkElement.setAttribute("mnemonic", String.valueOf(mnemonic));
305 element.addContent(bookmarkElement);
310 * Try to move bookmark one position up in the list
312 * @return bookmark list after moving
315 public List<Bookmark> moveBookmarkUp(@NotNull Bookmark bookmark) {
316 final int index = myBookmarks.indexOf(bookmark);
318 Collections.swap(myBookmarks, index, index - 1);
319 EventQueue.invokeLater(new Runnable() {
322 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index));
323 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index - 1));
332 * Try to move bookmark one position down in the list
334 * @return bookmark list after moving
337 public List<Bookmark> moveBookmarkDown(@NotNull Bookmark bookmark) {
338 final int index = myBookmarks.indexOf(bookmark);
339 if (index < myBookmarks.size() - 1) {
340 Collections.swap(myBookmarks, index, index + 1);
341 EventQueue.invokeLater(new Runnable() {
344 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index));
345 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index + 1));
354 public Bookmark getNextBookmark(@NotNull Editor editor, boolean isWrapped) {
355 Bookmark[] bookmarksForDocument = getBookmarksForDocument(editor.getDocument());
356 int lineNumber = editor.getCaretModel().getLogicalPosition().line;
357 for (Bookmark bookmark : bookmarksForDocument) {
358 if (bookmark.getLine() > lineNumber) return bookmark;
360 if (isWrapped && bookmarksForDocument.length > 0) {
361 return bookmarksForDocument[0];
367 public Bookmark getPreviousBookmark(@NotNull Editor editor, boolean isWrapped) {
368 Bookmark[] bookmarksForDocument = getBookmarksForDocument(editor.getDocument());
369 int lineNumber = editor.getCaretModel().getLogicalPosition().line;
370 for (int i = bookmarksForDocument.length - 1; i >= 0; i--) {
371 Bookmark bookmark = bookmarksForDocument[i];
372 if (bookmark.getLine() < lineNumber) return bookmark;
374 if (isWrapped && bookmarksForDocument.length > 0) {
375 return bookmarksForDocument[bookmarksForDocument.length - 1];
381 private Bookmark[] getBookmarksForDocument(@NotNull Document document) {
382 ArrayList<Bookmark> answer = new ArrayList<Bookmark>();
383 for (Bookmark bookmark : getValidBookmarks()) {
384 if (document.equals(bookmark.getDocument())) {
385 answer.add(bookmark);
389 Bookmark[] bookmarks = answer.toArray(new Bookmark[answer.size()]);
390 Arrays.sort(bookmarks, new Comparator<Bookmark>() {
392 public int compare(final Bookmark o1, final Bookmark o2) {
393 return o1.getLine() - o2.getLine();
399 public void setMnemonic(@NotNull Bookmark bookmark, char c) {
400 final Bookmark old = findBookmarkForMnemonic(c);
401 if (old != null) removeBookmark(old);
403 bookmark.setMnemonic(c);
404 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(bookmark);
407 public void setDescription(@NotNull Bookmark bookmark, String description) {
408 bookmark.setDescription(description);
409 myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(bookmark);
412 public void colorsChanged() {
413 for (Bookmark bookmark : myBookmarks) {
414 bookmark.updateHighlighter();
419 private class MyEditorMouseListener extends EditorMouseAdapter {
421 public void mouseClicked(final EditorMouseEvent e) {
422 if (e.getArea() != EditorMouseEventArea.LINE_MARKERS_AREA) return;
423 if (e.getMouseEvent().isPopupTrigger()) return;
424 if ((e.getMouseEvent().getModifiers() & (SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK)) == 0) return;
426 Editor editor = e.getEditor();
427 int line = editor.xyToLogicalPosition(new Point(e.getMouseEvent().getX(), e.getMouseEvent().getY())).line;
428 if (line < 0) return;
430 Document document = editor.getDocument();
432 Bookmark bookmark = findEditorBookmark(document, line);
433 if (bookmark == null) {
434 addEditorBookmark(editor, line);
437 removeBookmark(bookmark);
443 private class MyDocumentListener extends DocumentAdapter {
445 public void beforeDocumentChange(DocumentEvent e) {
446 List<Bookmark> bookmarksToRemove = null;
447 for (Bookmark bookmark : myBookmarks) {
448 Document document = FileDocumentManager.getInstance().getCachedDocument(bookmark.getFile());
449 if (document == null || document != e.getDocument()) continue;
450 if (bookmark.getLine() ==-1) continue;
452 int start = bookmark.getDocument().getLineStartOffset(bookmark.getLine());
453 int end = bookmark.getDocument().getLineEndOffset(bookmark.getLine());
454 if (start >= e.getOffset() && end <= e.getOffset() + e.getOldLength() ) {
455 if (bookmarksToRemove == null) {
456 bookmarksToRemove = new ArrayList<Bookmark>();
458 bookmarksToRemove.add(bookmark);
461 if (bookmarksToRemove != null) {
462 for (Bookmark bookmark : bookmarksToRemove) {
463 removeBookmark(bookmark);
469 public void documentChanged(DocumentEvent e) {
470 List<Bookmark> bookmarksToRemove = null;
471 for (Bookmark bookmark : myBookmarks) {
472 if (!bookmark.isValid()) {
473 if (bookmarksToRemove == null) {
474 bookmarksToRemove = new ArrayList<Bookmark>();
476 bookmarksToRemove.add(bookmark);
480 if (bookmarksToRemove != null) {
481 for (Bookmark bookmark : bookmarksToRemove) {
482 removeBookmark(bookmark);