e97e800f7115999c3946e12e721dad97610948ec
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / bookmarks / BookmarkManager.java
1 /*
2  * Copyright 2000-2012 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.intellij.ide.bookmarks;
18
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;
46
47 import java.awt.*;
48 import java.awt.event.InputEvent;
49 import java.util.*;
50 import java.util.List;
51
52 @State(
53   name = "BookmarkManager",
54   storages = {
55     @Storage(file = StoragePathMacros.WORKSPACE_FILE)
56   }
57 )
58 public class BookmarkManager extends AbstractProjectComponent implements PersistentStateComponent<Element> {
59   private static final int MAX_AUTO_DESCRIPTION_SIZE = 50;
60
61   private final List<Bookmark> myBookmarks = new ArrayList<Bookmark>();
62
63   private final MessageBus myBus;
64
65   public static BookmarkManager getInstance(Project project) {
66     return project.getComponent(BookmarkManager.class);
67   }
68
69   public BookmarkManager(Project project, MessageBus bus, PsiDocumentManager documentManager) {
70     super(project);
71     myBus = bus;
72     EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster();
73     multicaster.addDocumentListener(new MyDocumentListener(), myProject);
74     multicaster.addEditorMouseListener(new MyEditorMouseListener(), myProject);
75
76     documentManager.addListener(new PsiDocumentManager.Listener() {
77       @Override
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() {
84               @Override
85               public void run() {
86                 if (myProject.isDisposed()) return;
87                 bookmark.createHighlighter((MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true));
88               }
89             });
90           }
91         }
92       }
93
94       @Override
95       public void fileCreated(@NotNull PsiFile file, @NotNull Document document) {
96       }
97     });
98   }
99
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() {
105         @Override
106         public boolean checkInput(String inputString) {
107           return true;
108         }
109
110         @Override
111         public boolean canClose(String inputString) {
112           return true;
113         }
114       });
115     if (description != null) {
116       setDescription(bookmark, description);
117     }
118   }
119
120   @NotNull
121   @Override
122   public String getComponentName() {
123     return "BookmarkManager";
124   }
125
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;
130
131     final VirtualFile virtualFile = psiFile.getVirtualFile();
132     if (virtualFile == null) return;
133
134     addTextBookmark(virtualFile, lineIndex, getAutoDescription(editor, lineIndex));
135   }
136
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);
141     return b;
142   }
143
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();
150     }
151     if ( autoDescription.length () > MAX_AUTO_DESCRIPTION_SIZE) {
152       return autoDescription.substring(0, MAX_AUTO_DESCRIPTION_SIZE)+"...";
153     }
154     return autoDescription;
155   }
156
157   @Nullable
158   public Bookmark addFileBookmark(VirtualFile file, String description) {
159     if (file == null) return null;
160     if (findFileBookmark(file) != null) return null;
161
162     Bookmark b = new Bookmark(myProject, file, -1, description);
163     myBookmarks.add(0, b);
164     myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkAdded(b);
165     return b;
166   }
167
168
169   @NotNull
170   public List<Bookmark> getValidBookmarks() {
171     List<Bookmark> answer = new ArrayList<Bookmark>();
172     for (Bookmark bookmark : myBookmarks) {
173       if (bookmark.isValid()) answer.add(bookmark);
174     }
175     return answer;
176   }
177
178
179   @Nullable
180   public Bookmark findEditorBookmark(@NotNull Document document, int line) {
181     for (Bookmark bookmark : myBookmarks) {
182       if (bookmark.getDocument() == document && bookmark.getLine() == line) {
183         return bookmark;
184       }
185     }
186
187     return null;
188   }
189
190   @Nullable
191   public Bookmark findFileBookmark(@NotNull VirtualFile file) {
192     for (Bookmark bookmark : myBookmarks) {
193       if (Comparing.equal(bookmark.getFile(), file) && bookmark.getLine() == -1) return bookmark;
194     }
195
196     return null;
197   }
198
199   @Nullable
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;
204     }
205     return null;
206   }
207
208   public boolean hasBookmarksWithMnemonics() {
209     for (Bookmark bookmark : myBookmarks) {
210       if (bookmark.getMnemonic() != 0) return true;
211     }
212
213     return false;
214   }
215
216   public void removeBookmark(@NotNull Bookmark bookmark) {
217     myBookmarks.remove(bookmark);
218     bookmark.release();
219     myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkRemoved(bookmark);
220   }
221
222   @Override
223   public Element getState() {
224     Element container = new Element("BookmarkManager");
225     writeExternal(container);
226     return container;
227   }
228
229   @Override
230   public void loadState(final Element state) {
231     StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new DumbAwareRunnable() {
232       @Override
233       public void run() {
234         BookmarksListener publisher = myBus.syncPublisher(BookmarksListener.TOPIC);
235         for (Bookmark bookmark : myBookmarks) {
236           bookmark.release();
237           publisher.bookmarkRemoved(bookmark);
238         }
239         myBookmarks.clear();
240
241         readExternal(state);
242       }
243     });
244   }
245
246   private void readExternal(Element element) {
247     for (final Object o : element.getChildren()) {
248       Element bookmarkElement = (Element)o;
249
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");
255
256         Bookmark b = null;
257         VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url);
258         if (file != null) {
259           if (line != null) {
260             try {
261               int lineIndex = Integer.parseInt(line);
262               b = addTextBookmark(file, lineIndex, description);
263             }
264             catch (NumberFormatException e) {
265               // Ignore. Will miss bookmark if line number cannot be parsed
266             }
267           }
268           else {
269             b = addFileBookmark(file, description);
270           }
271         }
272
273         if (b != null && mnemonic != null && mnemonic.length() == 1) {
274           setMnemonic(b, mnemonic.charAt(0));
275         }
276       }
277     }
278   }
279
280   private void writeExternal(Element element) {
281     List<Bookmark> reversed = new ArrayList<Bookmark>(myBookmarks);
282     Collections.reverse(reversed);
283
284     for (Bookmark bookmark : reversed) {
285       if (!bookmark.isValid()) continue;
286       Element bookmarkElement = new Element("bookmark");
287
288       bookmarkElement.setAttribute("url", bookmark.getFile().getUrl());
289
290       String description = bookmark.getNotEmptyDescription();
291       if (description != null) {
292         bookmarkElement.setAttribute("description", description);
293       }
294
295       int line = bookmark.getLine();
296       if (line >= 0) {
297         bookmarkElement.setAttribute("line", String.valueOf(line));
298       }
299
300       char mnemonic = bookmark.getMnemonic();
301       if (mnemonic != 0) {
302         bookmarkElement.setAttribute("mnemonic", String.valueOf(mnemonic));
303       }
304
305       element.addContent(bookmarkElement);
306     }
307   }
308
309   /**
310    * Try to move bookmark one position up in the list
311    *
312    * @return bookmark list after moving
313    */
314   @NotNull
315   public List<Bookmark> moveBookmarkUp(@NotNull Bookmark bookmark) {
316     final int index = myBookmarks.indexOf(bookmark);
317     if (index > 0) {
318       Collections.swap(myBookmarks, index, index - 1);
319       EventQueue.invokeLater(new Runnable() {
320         @Override
321         public void run() {
322           myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index));
323           myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index - 1));
324         }
325       });
326     }
327     return myBookmarks;
328   }
329
330
331   /**
332    * Try to move bookmark one position down in the list
333    *
334    * @return bookmark list after moving
335    */
336   @NotNull
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() {
342         @Override
343         public void run() {
344           myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index));
345           myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(myBookmarks.get(index + 1));
346         }
347       });
348     }
349
350     return myBookmarks;
351   }
352
353   @Nullable
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;
359     }
360     if (isWrapped && bookmarksForDocument.length > 0) {
361       return bookmarksForDocument[0];
362     }
363     return null;
364   }
365
366   @Nullable
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;
373     }
374     if (isWrapped && bookmarksForDocument.length > 0) {
375       return bookmarksForDocument[bookmarksForDocument.length - 1];
376     }
377     return null;
378   }
379
380   @NotNull
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);
386       }
387     }
388
389     Bookmark[] bookmarks = answer.toArray(new Bookmark[answer.size()]);
390     Arrays.sort(bookmarks, new Comparator<Bookmark>() {
391       @Override
392       public int compare(final Bookmark o1, final Bookmark o2) {
393         return o1.getLine() - o2.getLine();
394       }
395     });
396     return bookmarks;
397   }
398
399   public void setMnemonic(@NotNull Bookmark bookmark, char c) {
400     final Bookmark old = findBookmarkForMnemonic(c);
401     if (old != null) removeBookmark(old);
402
403     bookmark.setMnemonic(c);
404     myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(bookmark);
405   }
406
407   public void setDescription(@NotNull Bookmark bookmark, String description) {
408     bookmark.setDescription(description);
409     myBus.syncPublisher(BookmarksListener.TOPIC).bookmarkChanged(bookmark);
410   }
411
412   public void colorsChanged() {
413     for (Bookmark bookmark : myBookmarks) {
414       bookmark.updateHighlighter();
415     }
416   }
417
418
419   private class MyEditorMouseListener extends EditorMouseAdapter {
420     @Override
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;
425
426       Editor editor = e.getEditor();
427       int line = editor.xyToLogicalPosition(new Point(e.getMouseEvent().getX(), e.getMouseEvent().getY())).line;
428       if (line < 0) return;
429
430       Document document = editor.getDocument();
431
432       Bookmark bookmark = findEditorBookmark(document, line);
433       if (bookmark == null) {
434         addEditorBookmark(editor, line);
435       }
436       else {
437         removeBookmark(bookmark);
438       }
439       e.consume();
440     }
441   }
442
443   private class MyDocumentListener extends DocumentAdapter {
444     @Override
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;
451
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>();
457           }
458           bookmarksToRemove.add(bookmark);
459         }
460       }
461       if (bookmarksToRemove != null) {
462         for (Bookmark bookmark : bookmarksToRemove) {
463           removeBookmark(bookmark);
464         }
465       }
466     }
467
468     @Override
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>();
475           }
476           bookmarksToRemove.add(bookmark);
477         }
478       }
479
480       if (bookmarksToRemove != null) {
481         for (Bookmark bookmark : bookmarksToRemove) {
482           removeBookmark(bookmark);
483         }
484       }
485     }
486   }
487 }
488