Merge branch 'svn-history-regex-filter' of https://github.com/pkrcah/intellij-community
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / committed / CommittedChangesPanel.java
1 /*
2  * Copyright 2000-2011 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 /*
18  * Created by IntelliJ IDEA.
19  * User: yole
20  * Date: 05.12.2006
21  * Time: 19:39:22
22  */
23 package com.intellij.openapi.vcs.changes.committed;
24
25 import com.intellij.openapi.Disposable;
26 import com.intellij.openapi.actionSystem.*;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.application.ModalityState;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.progress.ProgressIndicator;
31 import com.intellij.openapi.progress.ProgressManager;
32 import com.intellij.openapi.progress.Task;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.ui.Messages;
35 import com.intellij.openapi.util.Disposer;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.openapi.vcs.*;
38 import com.intellij.openapi.vcs.changes.BackgroundFromStartOption;
39 import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
40 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
41 import com.intellij.openapi.vfs.VirtualFile;
42 import com.intellij.ui.FilterComponent;
43 import com.intellij.ui.LightColors;
44 import com.intellij.util.AsynchConsumer;
45 import com.intellij.util.BufferedListConsumer;
46 import com.intellij.util.Consumer;
47 import com.intellij.util.WaitForProgressToShow;
48 import com.intellij.util.containers.ContainerUtil;
49 import com.intellij.util.ui.UIUtil;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import javax.swing.*;
54 import javax.swing.event.ChangeEvent;
55 import javax.swing.event.ChangeListener;
56 import java.awt.*;
57 import java.util.ArrayList;
58 import java.util.Collections;
59 import java.util.LinkedList;
60 import java.util.List;
61 import java.util.regex.Pattern;
62 import java.util.regex.PatternSyntaxException;
63
64 public class CommittedChangesPanel extends JPanel implements TypeSafeDataProvider, Disposable {
65   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.committed.CommittedChangesPanel");
66
67   private final CommittedChangesTreeBrowser myBrowser;
68   private final Project myProject;
69   private CommittedChangesProvider myProvider;
70   private ChangeBrowserSettings mySettings;
71   private final RepositoryLocation myLocation;
72   private int myMaxCount = 0;
73   private final MyFilterComponent myFilterComponent = new MyFilterComponent();
74   private final JCheckBox myRegexCheckbox;
75   private final List<Runnable> myShouldBeCalledOnDispose;
76   private volatile boolean myDisposed;
77   private volatile boolean myInLoad;
78   private Consumer<String> myIfNotCachedReloader;
79   private boolean myChangesLoaded;
80
81   public CommittedChangesPanel(Project project, final CommittedChangesProvider provider, final ChangeBrowserSettings settings,
82                                @Nullable final RepositoryLocation location, @Nullable ActionGroup extraActions) {
83     super(new BorderLayout());
84     mySettings = settings;
85     myProject = project;
86     myProvider = provider;
87     myLocation = location;
88     myShouldBeCalledOnDispose = new ArrayList<Runnable>();
89     myBrowser = new CommittedChangesTreeBrowser(project, new ArrayList<CommittedChangeList>());
90     Disposer.register(this, myBrowser);
91     add(myBrowser, BorderLayout.CENTER);
92
93     final VcsCommittedViewAuxiliary auxiliary = provider.createActions(myBrowser, location);
94
95     JPanel toolbarPanel = new JPanel();
96     toolbarPanel.setLayout(new BoxLayout(toolbarPanel, BoxLayout.X_AXIS));
97
98     ActionGroup group = (ActionGroup) ActionManager.getInstance().getAction("CommittedChangesToolbar");
99
100     ActionToolbar toolBar = myBrowser.createGroupFilterToolbar(project, group, extraActions,
101                                                                auxiliary != null ? auxiliary.getToolbarActions() : Collections.<AnAction>emptyList());
102     toolbarPanel.add(toolBar.getComponent());
103     toolbarPanel.add(Box.createHorizontalGlue());
104     myRegexCheckbox = new JCheckBox(VcsBundle.message("committed.changes.regex.title"));
105     myRegexCheckbox.setSelected(false);
106     myRegexCheckbox.getModel().addChangeListener(new ChangeListener() {
107       @Override
108       public void stateChanged(ChangeEvent e) {
109         myFilterComponent.filter();
110       }
111     });
112     toolbarPanel.add(myFilterComponent);
113     toolbarPanel.add(myRegexCheckbox);
114     myFilterComponent.setMinimumSize(myFilterComponent.getPreferredSize());
115     myFilterComponent.setMaximumSize(myFilterComponent.getPreferredSize());
116     myBrowser.setToolBar(toolbarPanel);
117
118     if (auxiliary != null) {
119       myShouldBeCalledOnDispose.add(auxiliary.getCalledOnViewDispose());
120       myBrowser.setTableContextMenu(group, auxiliary.getPopupActions());
121     } else {
122       myBrowser.setTableContextMenu(group, Collections.<AnAction>emptyList());
123     }
124     
125     final AnAction anAction = ActionManager.getInstance().getAction("CommittedChanges.Refresh");
126     anAction.registerCustomShortcutSet(CommonShortcuts.getRerun(), this);
127     myBrowser.addFilter(myFilterComponent);
128     myIfNotCachedReloader = myLocation == null ? null : new Consumer<String>() {
129       @Override
130       public void consume(String s) {
131         refreshChanges(false);
132       }
133     };
134   }
135
136   public RepositoryLocation getRepositoryLocation() {
137     return myLocation;
138   }
139
140   public void setMaxCount(final int maxCount) {
141     myMaxCount = maxCount;
142   }
143
144   public void setProvider(final CommittedChangesProvider provider) {
145     if (myProvider != provider) {
146       myProvider = provider;
147       mySettings = provider.createDefaultSettings(); 
148     }
149   }
150
151   public void refreshChanges(final boolean cacheOnly) {
152     if (myLocation != null) {
153       refreshChangesFromLocation();
154     }
155     else {
156       refreshChangesFromCache(cacheOnly);
157     }
158   }
159
160   private void refreshChangesFromLocation() {
161     myBrowser.reset();
162
163     myInLoad = true;
164     myBrowser.setLoading(true);
165     ProgressManager.getInstance().run(new Task.Backgroundable(myProject, "Loading changes", true, BackgroundFromStartOption.getInstance()) {
166       
167       public void run(@NotNull final ProgressIndicator indicator) {
168         try {
169           final AsynchConsumer<List<CommittedChangeList>> appender = new AsynchConsumer<List<CommittedChangeList>>() {
170             public void finished() {
171             }
172
173             public void consume(final List<CommittedChangeList> list) {
174               new AbstractCalledLater(myProject, ModalityState.stateForComponent(myBrowser)) {
175                 public void run() {
176                   myBrowser.append(list);
177                 }
178               }.callMe();
179             }
180           };
181           final BufferedListConsumer<CommittedChangeList> bufferedListConsumer = new BufferedListConsumer<CommittedChangeList>(30, appender,-1);
182
183           myProvider.loadCommittedChanges(mySettings, myLocation, myMaxCount, new AsynchConsumer<CommittedChangeList>() {
184             public void finished() {
185               bufferedListConsumer.flush();
186             }
187             public void consume(CommittedChangeList committedChangeList) {
188               if (myDisposed) {
189                 indicator.cancel();
190               }
191               ProgressManager.checkCanceled();
192               bufferedListConsumer.consumeOne(committedChangeList);
193             }
194           });
195         }
196         catch (final VcsException e) {
197           LOG.info(e);
198           WaitForProgressToShow.runOrInvokeLaterAboveProgress(new Runnable() {
199             public void run() {
200               Messages.showErrorDialog(myProject, "Error refreshing view: " + StringUtil.join(e.getMessages(), "\n"), "Committed Changes");
201             }
202           }, null, myProject);
203         } finally {
204           myInLoad = false;
205           myBrowser.setLoading(false);
206         }
207       }
208     });
209   }
210
211   public void clearCaches() {
212     final CommittedChangesCache cache = CommittedChangesCache.getInstance(myProject);
213     cache.clearCaches(new Runnable() {
214       @Override
215       public void run() {
216         ApplicationManager.getApplication().invokeLater(new Runnable() {
217           @Override
218           public void run() {
219             updateFilteredModel(Collections.<CommittedChangeList>emptyList(), true);
220           }
221         }, ModalityState.NON_MODAL, myProject.getDisposed());
222       }
223     });
224   }
225
226   private void refreshChangesFromCache(final boolean cacheOnly) {
227     final CommittedChangesCache cache = CommittedChangesCache.getInstance(myProject);
228     cache.hasCachesForAnyRoot(new Consumer<Boolean>() {
229       public void consume(final Boolean notEmpty) {
230         if (! notEmpty) {
231           if (cacheOnly) {
232             myBrowser.getEmptyText().setText(VcsBundle.message("committed.changes.not.loaded.message"));
233             return;
234           }
235           if (!CacheSettingsDialog.showSettingsDialog(myProject)) return;
236         }
237         cache.getProjectChangesAsync(mySettings, myMaxCount, cacheOnly,
238                                      new Consumer<List<CommittedChangeList>>() {
239                                        public void consume(final List<CommittedChangeList> committedChangeLists) {
240                                          updateFilteredModel(committedChangeLists, false);
241                                          }
242                                        },
243                                      new Consumer<List<VcsException>>() {
244                                        public void consume(final List<VcsException> vcsExceptions) {
245                                          AbstractVcsHelper.getInstance(myProject).showErrors(vcsExceptions, "Error refreshing VCS history");
246                                        }
247                                      });
248       }
249     });
250   }
251
252   private interface FilterHelper {
253     boolean filter(@NotNull final CommittedChangeList cl);
254   }
255
256   private class RegexFilterHelper implements FilterHelper {
257     private final Pattern myPattern;
258
259     RegexFilterHelper(@NotNull final String regex) {
260       Pattern pattern;
261       try {
262         pattern = Pattern.compile(regex);
263       } catch (PatternSyntaxException e) {
264         pattern = null;
265         myBrowser.getEmptyText().setText(VcsBundle.message("committed.changes.incorrect.regex.message"));
266       }
267       this.myPattern = pattern;
268     }
269
270     @Override
271     public boolean filter(@NotNull CommittedChangeList cl) {
272       return changeListMatches(cl);
273     }
274
275     private boolean changeListMatches(@NotNull CommittedChangeList cl) {
276       if (myPattern == null) {
277         return false;
278       }
279       boolean commentMatches = myPattern.matcher(cl.getComment()).find();
280       boolean committerMatches = myPattern.matcher(cl.getCommitterName()).find();
281       boolean revisionMatches = myPattern.matcher(Long.toString(cl.getNumber())).find();
282       return commentMatches || committerMatches || revisionMatches;
283     }
284   }
285
286   private static class WordMatchFilterHelper implements FilterHelper {
287     private final String[] myParts;
288
289     WordMatchFilterHelper(final String filterString) {
290       myParts = filterString.split(" ");
291       for(int i = 0; i < myParts.length; ++ i) {
292         myParts [i] = myParts [i].toLowerCase();
293       }
294     }
295
296     public boolean filter(@NotNull final CommittedChangeList cl) {
297       return changeListMatches(cl, myParts);
298     }
299
300     private static boolean changeListMatches(@NotNull final CommittedChangeList changeList, final String[] filterWords) {
301       for(String word: filterWords) {
302         final String comment = changeList.getComment();
303         final String committer = changeList.getCommitterName();
304         if ((comment != null && comment.toLowerCase().contains(word)) ||
305             (committer != null && committer.toLowerCase().contains(word)) ||
306             Long.toString(changeList.getNumber()).contains(word)) {
307           return true;
308         }
309       }
310       return false;
311     }
312   }
313
314   private void updateFilteredModel(List<CommittedChangeList> committedChangeLists, final boolean reset) {
315     if (committedChangeLists == null) {
316       return;
317     }
318     myChangesLoaded = !reset;
319     setEmptyMessage(myChangesLoaded);
320     myBrowser.setItems(committedChangeLists, CommittedChangesBrowserUseCase.COMMITTED);
321   }
322
323   private void setEmptyMessage(boolean changesLoaded) {
324     String emptyText;
325     if (!changesLoaded) {
326       emptyText = VcsBundle.message("committed.changes.not.loaded.message");
327     } else {
328       emptyText = VcsBundle.message("committed.changes.empty.message");
329     }
330     myBrowser.getEmptyText().setText(emptyText);
331   }
332
333   public void setChangesFilter() {
334     CommittedChangesFilterDialog filterDialog = new CommittedChangesFilterDialog(myProject, myProvider.createFilterUI(true), mySettings);
335     filterDialog.show();
336     if (filterDialog.isOK()) {
337       mySettings = filterDialog.getSettings();
338       refreshChanges(false);
339     }
340   }
341
342   public void calcData(DataKey key, DataSink sink) {
343     if (key.equals(VcsDataKeys.REMOTE_HISTORY_CHANGED_LISTENER)) {
344       sink.put(VcsDataKeys.REMOTE_HISTORY_CHANGED_LISTENER, myIfNotCachedReloader);
345     } else if (VcsDataKeys.REMOTE_HISTORY_LOCATION.equals(key)) {
346       sink.put(VcsDataKeys.REMOTE_HISTORY_LOCATION, myLocation);
347     }
348     //if (key.equals(VcsDataKeys.CHANGES) || key.equals(VcsDataKeys.CHANGE_LISTS)) {
349       myBrowser.calcData(key, sink);
350     //}
351   }
352
353   public void dispose() {
354     for (Runnable runnable : myShouldBeCalledOnDispose) {
355       runnable.run();
356     }
357     myDisposed = true;
358   }
359
360   private void setRegularFilterBackground() {
361     myFilterComponent.getTextEditor().setBackground(UIUtil.getTextFieldBackground());
362   }
363
364   private void setNotFoundFilterBackground() {
365     myFilterComponent.getTextEditor().setBackground(LightColors.RED);
366   }
367
368   private class MyFilterComponent extends FilterComponent implements ChangeListFilteringStrategy {
369     private final List<ChangeListener> myList = ContainerUtil.createLockFreeCopyOnWriteList();
370
371     public MyFilterComponent() {
372       super("COMMITTED_CHANGES_FILTER_HISTORY", 20);
373     }
374
375     @Override
376     public CommittedChangesFilterKey getKey() {
377       return new CommittedChangesFilterKey("text", CommittedChangesFilterPriority.TEXT);
378     }
379
380     public void filter() {
381       for (ChangeListener changeListener : myList) {
382         changeListener.stateChanged(new ChangeEvent(this));
383       }
384     }
385     public JComponent getFilterUI() {
386       return null;
387     }
388     public void setFilterBase(List<CommittedChangeList> changeLists) {
389     }
390     public void addChangeListener(ChangeListener listener) {
391       myList.add(listener);
392     }
393     public void removeChangeListener(ChangeListener listener) {
394       myList.remove(listener);
395     }
396     public void resetFilterBase() {
397     }
398     public void appendFilterBase(List<CommittedChangeList> changeLists) {
399     }
400     @NotNull
401     public List<CommittedChangeList> filterChangeLists(List<CommittedChangeList> changeLists) {
402       final FilterHelper filterHelper;
403       setEmptyMessage(myChangesLoaded);
404       if (myRegexCheckbox.isSelected()) {
405         filterHelper = new RegexFilterHelper(myFilterComponent.getFilter());
406       } else {
407         filterHelper = new WordMatchFilterHelper(myFilterComponent.getFilter());
408       }
409       final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
410       for (CommittedChangeList list : changeLists) {
411         if (filterHelper.filter(list)) {
412           result.add(list);
413         }
414       }
415       if (result.size() == 0 && !myFilterComponent.getFilter().isEmpty()) {
416         setNotFoundFilterBackground();
417       } else {
418         setRegularFilterBackground();
419       }
420       return result;
421     }
422   }
423
424   public void passCachedListsToListener(final VcsConfigurationChangeListener.DetailedNotification notification,
425                                         final Project project, final VirtualFile root) {
426     final LinkedList<CommittedChangeList> resultList = new LinkedList<CommittedChangeList>();
427     myBrowser.reportLoadedLists(new CommittedChangeListsListener() {
428       public void onBeforeStartReport() {
429       }
430       public boolean report(CommittedChangeList list) {
431         resultList.add(list);
432         return false;
433       }
434       public void onAfterEndReport() {
435         if (! resultList.isEmpty()) {
436           notification.execute(project, root, resultList);
437         }
438       }
439     });
440   }
441
442   public boolean isInLoad() {
443     return myInLoad;
444   }
445 }