diff --git a/platform/vcs-log/api/src/com/intellij/vcs/log/VcsLogProvider.java b/platform/vcs-log/api/src/com/intellij/vcs/log/VcsLogProvider.java index f3281bb43c0d..0a730270c4a3 100644 --- a/platform/vcs-log/api/src/com/intellij/vcs/log/VcsLogProvider.java +++ b/platform/vcs-log/api/src/com/intellij/vcs/log/VcsLogProvider.java @@ -74,7 +74,11 @@ public interface VcsLogProvider { void subscribeToRootRefreshEvents(@NotNull Collection roots, @NotNull VcsLogRefresher refresher); /** - * Return commits with full details, which correspond to the given filters. + *

Return commits with full details, which correspond to the given filters.

+ * + *

There can be several filters of a single type (e.g. several filters by users).
+ * Filters of different types are concatenated with {@code AND}, while filters of a single type are concatenated with {@code OR}. + * E.g.: (branch1 OR branch2) AND (user1 OR user2).

* * @param maxCount maximum number of commits to request from the VCS, or -1 for unlimited. */ diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUiProperties.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUiProperties.java index be746e6b30e8..b3638aaa143d 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUiProperties.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUiProperties.java @@ -19,14 +19,13 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.components.StoragePathMacros; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; import com.intellij.vcs.log.VcsLogSettings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; +import java.util.*; /** * Stores UI configuration based on user activity and preferences. @@ -42,7 +41,7 @@ public class VcsLogUiProperties implements PersistentStateComponent RECENTLY_FILTERED_USERS = new ArrayDeque(); + public Deque RECENTLY_FILTERED_USER_GROUPS = new ArrayDeque(); } @Nullable @@ -68,19 +67,44 @@ public class VcsLogUiProperties implements PersistentStateComponent usersInGroup) { + UserGroup group = new UserGroup(); + group.users = usersInGroup; + if (myState.RECENTLY_FILTERED_USER_GROUPS.contains(group)) { return; } - myState.RECENTLY_FILTERED_USERS.addFirst(username); - if (myState.RECENTLY_FILTERED_USERS.size() > RECENTLY_FILTERED_USERS_AMOUNT) { - myState.RECENTLY_FILTERED_USERS.removeLast(); + myState.RECENTLY_FILTERED_USER_GROUPS.addFirst(group); + if (myState.RECENTLY_FILTERED_USER_GROUPS.size() > RECENTLY_FILTERED_USERS_AMOUNT) { + myState.RECENTLY_FILTERED_USER_GROUPS.removeLast(); } } @NotNull - public List getRecentlyFilteredUsers() { - return new ArrayList(myState.RECENTLY_FILTERED_USERS); + public List> getRecentlyFilteredUserGroups() { + return ContainerUtil.map2List(myState.RECENTLY_FILTERED_USER_GROUPS, new Function>() { + @Override + public List fun(UserGroup group) { + return group.users; + } + }); + } + + public static class UserGroup { + public List users = new ArrayList(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserGroup group = (UserGroup)o; + if (!users.equals(group.users)) return false; + return true; + } + + @Override + public int hashCode() { + return users.hashCode(); + } } } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java index 3438de616064..4318e92e2433 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java @@ -124,11 +124,10 @@ class BranchFilterPopupComponent extends FilterPopupComponent { @Nullable @Override - protected VcsLogFilter getFilter() { + protected Collection getFilters() { String value = getValue(); - return value == ALL - ? null - : new VcsLogBranchFilter(myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(), value); + Collection allBranches = myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(); + return value == ALL ? null : Collections.singleton(new VcsLogBranchFilter(allBranches, value)); } } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/DateFilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/DateFilterPopupComponent.java index 87d93a16720b..4c98df60957a 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/DateFilterPopupComponent.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/DateFilterPopupComponent.java @@ -29,6 +29,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; import java.util.Date; class DateFilterPopupComponent extends FilterPopupComponent { @@ -66,8 +68,8 @@ class DateFilterPopupComponent extends FilterPopupComponent { @Nullable @Override - protected VcsLogFilter getFilter() { - return myAfter == null && myBefore == null ? null : new VcsLogDateFilter(myAfter, myBefore); + protected Collection getFilters() { + return myAfter == null && myBefore == null ? null : Collections.singleton(new VcsLogDateFilter(myAfter, myBefore)); } private void setOnlyAfter(Date after) { diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java index 838a488b3252..f8b3148199ba 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java @@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.Border; import java.awt.event.*; +import java.util.Collection; /** * Base class for components which allow to set up filter for the VCS Log, by displaying a popup with available choices. @@ -90,13 +91,19 @@ abstract class FilterPopupComponent extends JPanel { protected abstract ActionGroup createActionGroup(); /** - * Return the filter currently selected by this component. + * Return the filters (which would be concatenated with OR) currently selected via this component.
+ * Or return null if no filters are selected via this component. */ @Nullable - protected abstract VcsLogFilter getFilter(); + protected abstract Collection getFilters(); protected void setValue(@NotNull String newValue) { - myFilterValueLabel.setText(newValue); + setValue(newValue, newValue); + } + + protected void setValue(@NotNull String value, @NotNull String tooltip) { + myFilterValueLabel.setText(value); + setToolTipText(tooltip); } @NotNull diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/StructureFilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/StructureFilterPopupComponent.java index 0b4aa3950f47..ed0f8cfd511d 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/StructureFilterPopupComponent.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/StructureFilterPopupComponent.java @@ -52,8 +52,8 @@ class StructureFilterPopupComponent extends FilterPopupComponent { @Nullable @Override - protected VcsLogFilter getFilter() { - return getValue() == ALL || myFiles.isEmpty() ? null : new VcsLogStructureFilter(myFiles, myRoots); + protected Collection getFilters() { + return getValue() == ALL || myFiles.isEmpty() ? null : Collections.singleton(new VcsLogStructureFilter(myFiles, myRoots)); } private void setValue(@NotNull Collection files) { diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java index 6a5ee0a1da34..92ceeb5e4cc5 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java @@ -15,24 +15,36 @@ */ package com.intellij.vcs.log.ui.filter; -import com.intellij.openapi.actionSystem.ActionGroup; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.DumbAwareAction; -import com.intellij.openapi.ui.popup.JBPopup; -import com.intellij.openapi.ui.popup.JBPopupAdapter; -import com.intellij.openapi.ui.popup.LightweightWindowEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.*; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.spellchecker.ui.SpellCheckingEditorCustomization; +import com.intellij.ui.EditorCustomization; +import com.intellij.ui.EditorTextField; +import com.intellij.ui.EditorTextFieldProvider; +import com.intellij.ui.SoftWrapsEditorCustomization; import com.intellij.util.Function; +import com.intellij.util.TextFieldCompletionProviderDumbAware; import com.intellij.util.containers.ContainerUtil; import com.intellij.vcs.log.VcsLogFilter; import com.intellij.vcs.log.VcsUser; import com.intellij.vcs.log.data.VcsLogDataHolder; import com.intellij.vcs.log.data.VcsLogUiProperties; import com.intellij.vcs.log.data.VcsLogUserFilter; -import com.intellij.vcs.log.ui.PopupWithTextFieldWithAutoCompletion; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Collection; +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import java.awt.*; +import java.util.*; import java.util.List; /** @@ -41,9 +53,13 @@ import java.util.List; class UserFilterPopupComponent extends FilterPopupComponent { private static final String ME = "me"; + private static final char[] USERS_SEPARATORS = { ',', '|', '\n' }; + private final VcsLogDataHolder myDataHolder; private final VcsLogUiProperties myUiProperties; + @Nullable private Collection mySelectedUsers; + UserFilterPopupComponent(VcsLogClassicFilterUi filterUi, VcsLogDataHolder dataHolder, VcsLogUiProperties uiProperties) { super(filterUi, "User"); myDataHolder = dataHolder; @@ -52,15 +68,22 @@ class UserFilterPopupComponent extends FilterPopupComponent { @Override protected ActionGroup createActionGroup() { - DefaultActionGroup group = new DefaultActionGroup(); - group.add(createAllAction()); - group.add(new SetValueAction(ME, this)); + AnAction allAction = new DumbAwareAction(ALL) { + @Override + public void actionPerformed(AnActionEvent e) { + apply(null, ALL, ALL); + } + }; - List recentlyFilteredUsers = myUiProperties.getRecentlyFilteredUsers(); + DefaultActionGroup group = new DefaultActionGroup(); + group.add(allAction); + group.add(new UserAction(Collections.singleton(ME))); + + List> recentlyFilteredUsers = myUiProperties.getRecentlyFilteredUserGroups(); if (!recentlyFilteredUsers.isEmpty()) { group.addSeparator("Recently searched"); - for (String recentUser : recentlyFilteredUsers) { - group.add(new SetValueAction(recentUser, this)); + for (List recentGroup : recentlyFilteredUsers) { + group.add(new UserAction(recentGroup)); } } group.addSeparator(); @@ -70,16 +93,50 @@ class UserFilterPopupComponent extends FilterPopupComponent { @Nullable @Override - protected VcsLogFilter getFilter() { - String value = getValue(); - if (value == ALL) { + protected Collection getFilters() { + if (mySelectedUsers == null) { return null; } - if (value == ME) { - return new VcsLogUserFilter.Me(myDataHolder.getCurrentUser()); + myUiProperties.addRecentlyFilteredUserGroup(new ArrayList(mySelectedUsers)); + return ContainerUtil.map(mySelectedUsers, new Function() { + @Override + public VcsLogFilter fun(String name) { + return name == ME ? new VcsLogUserFilter.Me(myDataHolder.getCurrentUser()) : new VcsLogUserFilter.ByName(name); + } + }); + } + + private void apply(Collection users, String text, String tooltip) { + mySelectedUsers = users; + applyFilters(); + setValue(text, tooltip); + } + + @NotNull + private static String displayableText(@NotNull Collection users) { + if (users.size() == 1) { + return users.iterator().next(); + } + return StringUtil.shortenTextWithEllipsis(StringUtil.join(users, "|"), 30, 0, true); + } + + @NotNull + private static String tooltip(@NotNull Collection users) { + return StringUtil.join(users, ", "); + } + + private class UserAction extends DumbAwareAction { + @NotNull private final Collection myUsers; + + UserAction(@NotNull Collection users) { + super(displayableText(users), tooltip(users), null); + myUsers = users; + } + + @Override + public void actionPerformed(AnActionEvent e) { + apply(myUsers, displayableText(myUsers), tooltip(myUsers)); } - myUiProperties.addRecentlyFilteredUser(value); - return new VcsLogUserFilter.ByName(value); } private class SelectUserAction extends DumbAwareAction { @@ -90,6 +147,11 @@ class UserFilterPopupComponent extends FilterPopupComponent { @Override public void actionPerformed(AnActionEvent e) { + Project project = e.getProject(); + if (project == null) { + return; + } + Collection users = ContainerUtil.map(myDataHolder.getAllUsers(), new Function() { @Override public String fun(VcsUser user) { @@ -97,21 +159,105 @@ class UserFilterPopupComponent extends FilterPopupComponent { } }); - final PopupWithTextFieldWithAutoCompletion textField = new PopupWithTextFieldWithAutoCompletion(e.getProject(), users); - JBPopup popup = textField.createPopup(); - + final MultilinePopupBuilder popupBuilder = new MultilinePopupBuilder(project, users); + JBPopup popup = popupBuilder.createPopup(); popup.addListener(new JBPopupAdapter() { @Override public void onClosed(LightweightWindowEvent event) { if (event.isOk()) { - String user = textField.getText(); - setValue(user); - applyFilters(); + final String userText = popupBuilder.getText().trim(); + Collection selectedUsers = ContainerUtil.toCollection(StringUtil.tokenize(userText, new String(USERS_SEPARATORS))); + apply(selectedUsers, displayableText(selectedUsers), tooltip(selectedUsers)); } } }); popup.showUnderneathOf(UserFilterPopupComponent.this); } + } + private static class MultilinePopupBuilder { + private final EditorTextField myTextField; + + MultilinePopupBuilder(@NotNull Project project, @NotNull final Collection users) { + myTextField = createTextField(project); + new UsersCompletionProvider(users).apply(myTextField); + } + + @NotNull + private static EditorTextField createTextField(@NotNull Project project) { + final EditorTextFieldProvider service = ServiceManager.getService(project, EditorTextFieldProvider.class); + List features = Arrays.asList(SoftWrapsEditorCustomization.ENABLED, + SpellCheckingEditorCustomization.DISABLED); + EditorTextField textField = service.getEditorField(FileTypes.PLAIN_TEXT.getLanguage(), project, features); + textField.setBorder(new CompoundBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2), textField.getBorder())); + textField.setOneLineMode(false); + return textField; + } + + @NotNull + JBPopup createPopup() { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(myTextField, BorderLayout.CENTER); + ComponentPopupBuilder builder = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, myTextField) + .setCancelOnClickOutside(true) + .setAdText(KeymapUtil.getShortcutsText(CommonShortcuts.CTRL_ENTER.getShortcuts()) + " to finish") + .setMovable(true) + .setRequestFocus(true) + .setResizable(true) + .setMayBeParent(true); + + final JBPopup popup = builder.createPopup(); + popup.setMinimumSize(new Dimension(200, 90)); + AnAction okAction = new DumbAwareAction() { + @Override + public void actionPerformed(AnActionEvent e) { + unregisterCustomShortcutSet(popup.getContent()); + popup.closeOk(e.getInputEvent()); + } + }; + okAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, popup.getContent()); + return popup; + } + + String getText() { + return myTextField.getText(); + } + + private static class UsersCompletionProvider extends TextFieldCompletionProviderDumbAware { + @NotNull private final Collection myUsers; + + UsersCompletionProvider(@NotNull Collection users) { + super(true); + myUsers = users; + } + + @NotNull + @Override + protected String getPrefix(@NotNull String currentTextPrefix) { + final int separatorPosition = lastSeparatorPosition(currentTextPrefix); + return separatorPosition == -1 ? currentTextPrefix : currentTextPrefix.substring(separatorPosition + 1).trim(); + } + + private static int lastSeparatorPosition(@NotNull String text) { + int lastPosition = -1; + for (char separator : USERS_SEPARATORS) { + int lio = text.lastIndexOf(separator); + if (lio > lastPosition) { + lastPosition = lio; + } + } + return lastPosition; + } + + @Override + protected void addCompletionVariants(@NotNull String text, int offset, @NotNull String prefix, + @NotNull CompletionResultSet result) { + result.addLookupAdvertisement("Select one or more users separated with comma, | or new lines"); + for (String completionVariant : myUsers) { + final LookupElementBuilder element = LookupElementBuilder.create(completionVariant); + result.addElement(element.withLookupString(completionVariant.toLowerCase())); + } + } + } } } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java index ec6a7298f9ce..d8bd69633b06 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java @@ -15,12 +15,14 @@ */ package com.intellij.vcs.log.ui.filter; -import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.ex.CustomComponentAction; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.ui.SearchTextField; import com.intellij.ui.SearchTextFieldWithStoredHistory; -import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import com.intellij.vcs.log.VcsLogFilter; @@ -91,13 +93,14 @@ public class VcsLogClassicFilterUi implements VcsLogFilterUi { @NotNull private List getPopupFilters() { - return new ArrayList(ContainerUtil.mapNotNull(myFilterPopupComponents, - new Function() { - @Override - public VcsLogFilter fun(FilterPopupComponent filterComponent) { - return filterComponent.getFilter(); - } - })); + List filters = new ArrayList(); + for (FilterPopupComponent popupComponent : myFilterPopupComponents) { + Collection popupFilters = popupComponent.getFilters(); + if (popupFilters != null) { + filters.addAll(popupFilters); + } + } + return filters; } void applyFilters() {