Merge remote-tracking branch 'origin/master' into prendota/plugin-manager-new-protocol
[idea/community.git] / platform / platform-impl / src / com / intellij / ide / plugins / PluginManagerConfigurable.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ide.plugins;
3
4 import com.intellij.featureStatistics.FeatureUsageTracker;
5 import com.intellij.icons.AllIcons;
6 import com.intellij.ide.CopyProvider;
7 import com.intellij.ide.DataManager;
8 import com.intellij.ide.IdeBundle;
9 import com.intellij.ide.plugins.marketplace.MarketplaceRequests;
10 import com.intellij.ide.plugins.newui.*;
11 import com.intellij.ide.util.PropertiesComponent;
12 import com.intellij.openapi.actionSystem.*;
13 import com.intellij.openapi.application.ApplicationInfo;
14 import com.intellij.openapi.application.ApplicationManager;
15 import com.intellij.openapi.application.ApplicationNamesInfo;
16 import com.intellij.openapi.application.ModalityState;
17 import com.intellij.openapi.application.ex.ApplicationInfoEx;
18 import com.intellij.openapi.application.ex.ApplicationManagerEx;
19 import com.intellij.openapi.application.impl.ApplicationInfoImpl;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.extensions.PluginId;
22 import com.intellij.openapi.ide.CopyPasteManager;
23 import com.intellij.openapi.options.Configurable;
24 import com.intellij.openapi.options.ConfigurationException;
25 import com.intellij.openapi.options.SearchableConfigurable;
26 import com.intellij.openapi.options.ShowSettingsUtil;
27 import com.intellij.openapi.project.DumbAware;
28 import com.intellij.openapi.project.DumbAwareAction;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.ui.Messages;
31 import com.intellij.openapi.ui.popup.JBPopup;
32 import com.intellij.openapi.ui.popup.JBPopupListener;
33 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
34 import com.intellij.openapi.updateSettings.impl.PluginDownloader;
35 import com.intellij.openapi.updateSettings.impl.UpdateChecker;
36 import com.intellij.openapi.updateSettings.impl.UpdateSettings;
37 import com.intellij.openapi.util.SystemInfo;
38 import com.intellij.openapi.util.ThrowableNotNullFunction;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.ui.*;
41 import com.intellij.ui.components.JBScrollPane;
42 import com.intellij.ui.components.JBTextField;
43 import com.intellij.ui.components.fields.ExtendableTextComponent;
44 import com.intellij.ui.components.labels.LinkLabel;
45 import com.intellij.ui.components.labels.LinkListener;
46 import com.intellij.ui.popup.PopupFactoryImpl;
47 import com.intellij.ui.popup.list.PopupListElementRenderer;
48 import com.intellij.ui.scale.JBUIScale;
49 import com.intellij.util.containers.ContainerUtil;
50 import com.intellij.util.net.HttpConfigurable;
51 import com.intellij.util.ui.*;
52 import org.jetbrains.annotations.Nls;
53 import org.jetbrains.annotations.NonNls;
54 import org.jetbrains.annotations.NotNull;
55 import org.jetbrains.annotations.Nullable;
56
57 import javax.swing.*;
58 import javax.swing.border.Border;
59 import javax.swing.border.EmptyBorder;
60 import java.awt.*;
61 import java.io.IOException;
62 import java.text.DecimalFormat;
63 import java.text.SimpleDateFormat;
64 import java.util.List;
65 import java.util.*;
66 import java.util.Map.Entry;
67 import java.util.function.Consumer;
68 import java.util.function.Function;
69
70 /**
71  * @author Alexander Lobas
72  */
73 public class PluginManagerConfigurable
74   implements SearchableConfigurable, Configurable.NoScroll, Configurable.NoMargin, Configurable.TopComponentProvider {
75
76   private static final Logger LOG = Logger.getInstance(PluginManagerConfigurable.class);
77
78   public static final String ID = "preferences.pluginManager";
79   public static final String SELECTION_TAB_KEY = "PluginConfigurable.selectionTab";
80
81   @SuppressWarnings("UseJBColor") public static final Color MAIN_BG_COLOR =
82     JBColor.namedColor("Plugins.background", new JBColor(() -> JBColor.isBright() ? UIUtil.getListBackground() : new Color(0x313335)));
83   public static final Color SEARCH_BG_COLOR = JBColor.namedColor("Plugins.SearchField.background", MAIN_BG_COLOR);
84   public static final Color SEARCH_FIELD_BORDER_COLOR =
85     JBColor.namedColor("Plugins.SearchField.borderColor", new JBColor(0xC5C5C5, 0x515151));
86
87   private static final int MARKETPLACE_TAB = 0;
88   private static final int INSTALLED_TAB = 1;
89
90   public static final int ITEMS_PER_GROUP = 9;
91
92   public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM dd, yyyy");
93   private static final DecimalFormat K_FORMAT = new DecimalFormat("###.#K");
94   private static final DecimalFormat M_FORMAT = new DecimalFormat("###.#M");
95
96   private TabbedPaneHeaderComponent myTabHeaderComponent;
97   private MultiPanel myCardPanel;
98
99   private PluginsTab myMarketplaceTab;
100   private PluginsTab myInstalledTab;
101
102   private PluginsGroupComponentWithProgress myMarketplacePanel;
103   private PluginsGroupComponent myInstalledPanel;
104
105   private Runnable myMarketplaceRunnable;
106
107   private SearchResultPanel myMarketplaceSearchPanel;
108   private SearchResultPanel myInstalledSearchPanel;
109
110   private final LinkLabel<Object> myUpdateAll = new LinkLabel<>(IdeBundle.message("plugin.manager.update.all"), null);
111   private final JLabel myUpdateCounter = new CountComponent();
112   private final CountIcon myCountIcon = new CountIcon();
113
114   private final MyPluginModel myPluginModel = new MyPluginModel() {
115     @Override
116     @NotNull
117     public Collection<IdeaPluginDescriptor> getCustomRepoPlugins() {
118       synchronized (myRepositoriesLock) {
119         if (myCustomRepositoryPluginsList != null) {
120           return myCustomRepositoryPluginsList;
121         }
122       }
123       LOG.error("PluginManagerConfigurable.myPluginModel.getCustomRepoPlugins() has been called before PluginManagerConfigurable#createMarketplaceTab()");
124       return ContainerUtil.emptyList();
125     }
126   };
127
128   private PluginUpdatesService myPluginUpdatesService;
129
130   private Collection<IdeaPluginDescriptor> myCustomRepositoryPluginsList;
131   private Map<String, List<IdeaPluginDescriptor>> myCustomRepositoryPluginsMap;
132   private final Object myRepositoriesLock = new Object();
133   private List<String> myTagsSorted;
134   private List<String> myVendorsSorted;
135
136   private DefaultActionGroup myMarketplaceSortByGroup;
137   private Consumer<MarketplaceSortByAction> myMarketplaceSortByCallback;
138   private LinkComponent myMarketplaceSortByAction;
139
140   private DefaultActionGroup myInstalledSearchGroup;
141   private Consumer<InstalledSearchOptionAction> myInstalledSearchCallback;
142   private boolean myInstalledSearchSetState = true;
143
144   private Collection<PluginDownloader> myInitUpdates;
145
146   public PluginManagerConfigurable() {
147   }
148
149   /**
150    * @deprecated use {@link PluginManagerConfigurable}
151    */
152   @Deprecated
153   public PluginManagerConfigurable(PluginManagerUISettings uiSettings) {
154   }
155
156   @NotNull
157   @Override
158   public String getId() {
159     return ID;
160   }
161
162   @Override
163   public String getDisplayName() {
164     return IdeBundle.message("title.plugins");
165   }
166
167   @NotNull
168   @Override
169   public Component getCenterComponent(@NotNull TopComponentController controller) {
170     myPluginModel.setTopController(controller);
171     return myTabHeaderComponent;
172   }
173
174   @Nullable
175   @Override
176   public JComponent createComponent() {
177     myTabHeaderComponent = new TabbedPaneHeaderComponent(createGearActions(), index -> {
178       myCardPanel.select(index, true);
179       storeSelectionTab(index);
180
181       String query = (index == MARKETPLACE_TAB ? myInstalledTab : myMarketplaceTab).getSearchQuery();
182       (index == MARKETPLACE_TAB ? myMarketplaceTab : myInstalledTab).setSearchQuery(query);
183     });
184
185     myUpdateAll.setVisible(false);
186     myUpdateCounter.setVisible(false);
187
188     myTabHeaderComponent.addTab(IdeBundle.message("plugin.manager.tab.marketplace"), null);
189     myTabHeaderComponent.addTab(IdeBundle.message("plugin.manager.tab.installed"), myCountIcon);
190
191     Consumer<Integer> callback = countValue -> {
192       int count = countValue == null ? 0 : countValue;
193       String text = String.valueOf(count);
194       boolean visible = count > 0;
195
196       myUpdateAll.setEnabled(true);
197       myUpdateAll.setVisible(visible);
198
199       myUpdateCounter.setText(text);
200       myUpdateCounter.setVisible(visible);
201
202       myCountIcon.setText(text);
203       myTabHeaderComponent.update();
204     };
205     if (myInitUpdates != null) {
206       callback.accept(myInitUpdates.size());
207     }
208     myPluginUpdatesService = PluginUpdatesService.connectConfigurable(callback);
209     myPluginModel.setPluginUpdatesService(myPluginUpdatesService);
210
211     boolean selectInstalledTab = !ContainerUtil.isEmpty(myInitUpdates);
212
213     createMarketplaceTab();
214     createInstalledTab();
215
216     myCardPanel = new MultiPanel() {
217       @Override
218       protected JComponent create(Integer key) {
219         if (key == MARKETPLACE_TAB) {
220           return myMarketplaceTab.createPanel();
221         }
222         if (key == INSTALLED_TAB) {
223           return myInstalledTab.createPanel();
224         }
225         return super.create(key);
226       }
227     };
228     myCardPanel.setMinimumSize(new JBDimension(580, 380));
229     myCardPanel.setPreferredSize(new JBDimension(800, 600));
230
231     myTabHeaderComponent.setListener();
232
233     int selectionTab = selectInstalledTab ? INSTALLED_TAB : getStoredSelectionTab();
234     myTabHeaderComponent.setSelection(selectionTab);
235     myCardPanel.select(selectionTab, true);
236
237     if (selectInstalledTab) {
238       myInstalledTab.setSearchQuery("/outdated");
239     }
240
241     return myCardPanel;
242   }
243
244   @NotNull
245   private DefaultActionGroup createGearActions() {
246     DefaultActionGroup actions = new DefaultActionGroup();
247     actions.add(new DumbAwareAction(IdeBundle.message("plugin.manager.repositories")) {
248       @Override
249       public void actionPerformed(@NotNull AnActionEvent e) {
250         if (ShowSettingsUtil.getInstance().editConfigurable(myCardPanel, new PluginHostsConfigurable())) {
251           resetPanels();
252         }
253       }
254     });
255     actions.add(new DumbAwareAction(IdeBundle.message("button.http.proxy.settings")) {
256       @Override
257       public void actionPerformed(@NotNull AnActionEvent e) {
258         if (HttpConfigurable.editConfigurable(myCardPanel)) {
259           resetPanels();
260         }
261       }
262     });
263     actions.addSeparator();
264     actions.add(new InstallFromDiskAction());
265     actions.addSeparator();
266     actions.add(new ChangePluginStateAction(false));
267     actions.add(new ChangePluginStateAction(true));
268
269     return actions;
270   }
271
272   private static void showRightBottomPopup(@NotNull Component component, @NotNull @Nls String title, @NotNull ActionGroup group) {
273     DefaultActionGroup actions = new GroupByActionGroup();
274     actions.addSeparator("  " + title);
275     actions.addAll(group);
276
277     DataContext context = DataManager.getInstance().getDataContext(component);
278
279     JBPopup popup = new PopupFactoryImpl.ActionGroupPopup(null, actions, context, false, false, false, true, null, -1, null, null) {
280       @Override
281       protected ListCellRenderer getListElementRenderer() {
282         return new PopupListElementRenderer(this) {
283           @Override
284           protected SeparatorWithText createSeparator() {
285             return new SeparatorWithText() {
286               {
287                 setTextForeground(JBColor.BLACK);
288                 setCaptionCentered(false);
289               }
290
291               @Override
292               protected void paintLine(Graphics g, int x, int y, int width) {
293               }
294             };
295           }
296
297           @Override
298           protected void setSeparatorFont(Font font) {
299             mySeparatorComponent.setFont(font);
300           }
301
302           @Override
303           protected Border getDefaultItemComponentBorder() {
304             return new EmptyBorder(JBInsets.create(UIUtil.getListCellVPadding(), 15));
305           }
306         };
307       }
308     };
309     popup.addListener(new JBPopupListener() {
310       @Override
311       public void beforeShown(@NotNull LightweightWindowEvent event) {
312         Point location = component.getLocationOnScreen();
313         Dimension size = popup.getSize();
314         popup.setLocation(new Point(location.x + component.getWidth() - size.width, location.y + component.getHeight()));
315       }
316     });
317     popup.show(component);
318   }
319
320   private void resetPanels() {
321     synchronized (myRepositoriesLock) {
322       myCustomRepositoryPluginsList = null;
323       myCustomRepositoryPluginsMap = null;
324     }
325
326     myTagsSorted = null;
327     myVendorsSorted = null;
328
329     myPluginUpdatesService.recalculateUpdates();
330
331     if (myMarketplacePanel == null) {
332       return;
333     }
334
335     int selectionTab = myTabHeaderComponent.getSelectionTab();
336     if (selectionTab == MARKETPLACE_TAB) {
337       myMarketplaceRunnable.run();
338     }
339     else {
340       myMarketplacePanel.setVisibleRunnable(myMarketplaceRunnable);
341     }
342   }
343
344   private static int getStoredSelectionTab() {
345     int value = PropertiesComponent.getInstance().getInt(SELECTION_TAB_KEY, MARKETPLACE_TAB);
346     return value < MARKETPLACE_TAB || value > INSTALLED_TAB ? MARKETPLACE_TAB : value;
347   }
348
349   private static void storeSelectionTab(int value) {
350     PropertiesComponent.getInstance().setValue(SELECTION_TAB_KEY, value, MARKETPLACE_TAB);
351   }
352
353   private void createMarketplaceTab() {
354     myMarketplaceTab = new PluginsTab() {
355       @Override
356       protected void createSearchTextField(int flyDelay) {
357         super.createSearchTextField(250);
358         mySearchTextField.setHistoryPropertyName("MarketplacePluginsSearchHistory");
359       }
360
361       @NotNull
362       @Override
363       protected PluginDetailsPageComponent createDetailsPanel(@NotNull LinkListener<Object> searchListener) {
364         PluginDetailsPageComponent detailPanel = new PluginDetailsPageComponent(myPluginModel, searchListener, true);
365         myPluginModel.addDetailPanel(detailPanel);
366         return detailPanel;
367       }
368
369       @NotNull
370       @Override
371       protected JComponent createPluginsPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
372         MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
373         myMarketplacePanel =
374           new PluginsGroupComponentWithProgress(new PluginListLayout(), eventHandler,
375                                                 d -> new ListPluginComponent(myPluginModel, d, mySearchListener, true));
376
377         myMarketplacePanel.setSelectionListener(selectionListener);
378         registerCopyProvider(myMarketplacePanel);
379
380         //noinspection ConstantConditions
381         ((SearchUpDownPopupController)myMarketplaceSearchPanel.controller).setEventHandler(eventHandler);
382
383         Runnable runnable = () -> {
384           List<PluginsGroup> groups = new ArrayList<>();
385
386           try {
387             Map<String, List<IdeaPluginDescriptor>> customRepositoriesMap = loadCustomRepositoryPlugins();
388             try {
389               addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.featured"), "is_featured_search=true",
390                                          "/sortBy:featured");
391               addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.new.and.updated"), "orderBy=update+date",
392                                          "/sortBy:updated");
393               addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.top.downloads"), "orderBy=downloads",
394                                          "/sortBy:downloads");
395               addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.top.rated"), "orderBy=rating", "/sortBy:rating");
396             }
397             catch (IOException e) {
398               LOG.info("Main plugin repository is not available ('" + e.getMessage() + "'). Please check your network settings.");
399             }
400
401             for (String host : UpdateSettings.getInstance().getPluginHosts()) {
402               List<IdeaPluginDescriptor> allDescriptors = customRepositoriesMap.get(host);
403               if (allDescriptors != null) {
404                 addGroup(groups, IdeBundle.message("plugins.configurable.repository.0", host), "/repository:\"" + host + "\"",
405                          descriptors -> {
406                            int allSize = allDescriptors.size();
407                            descriptors.addAll(ContainerUtil.getFirstItems(allDescriptors, ITEMS_PER_GROUP));
408                            PluginsGroup.sortByName(descriptors);
409                            return allSize > ITEMS_PER_GROUP;
410                          });
411               }
412             }
413           }
414           catch (IOException e) {
415             LOG.info(e);
416           }
417           finally {
418             ApplicationManager.getApplication().invokeLater(() -> {
419               myMarketplacePanel.stopLoading();
420               PluginLogo.startBatchMode();
421
422               for (PluginsGroup group : groups) {
423                 myMarketplacePanel.addGroup(group);
424               }
425
426               PluginLogo.endBatchMode();
427               myMarketplacePanel.doLayout();
428               myMarketplacePanel.initialSelection();
429             }, ModalityState.any());
430           }
431         };
432
433         myMarketplaceRunnable = () -> {
434           myMarketplacePanel.clear();
435           myMarketplacePanel.startLoading();
436           ApplicationManager.getApplication().executeOnPooledThread(runnable);
437         };
438
439         myMarketplacePanel.getEmptyText().setText(IdeBundle.message("plugins.configurable.marketplace.plugins.not.loaded"))
440           .appendSecondaryText(IdeBundle.message("message.check.the.internet.connection.and") + " ", StatusText.DEFAULT_ATTRIBUTES, null)
441           .appendSecondaryText(IdeBundle.message("message.link.refresh"), SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES,
442                                e -> myMarketplaceRunnable.run());
443
444         ApplicationManager.getApplication().executeOnPooledThread(runnable);
445         return createScrollPane(myMarketplacePanel, false);
446       }
447
448       @Override
449       protected void updateMainSelection(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
450         selectionListener.accept(myMarketplacePanel);
451       }
452
453       @NotNull
454       @Override
455       protected SearchResultPanel createSearchPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
456         SearchUpDownPopupController marketplaceController = new SearchUpDownPopupController(mySearchTextField) {
457           @NotNull
458           @Override
459           protected List<String> getAttributes() {
460             List<String> attributes = new ArrayList<>();
461             attributes.add("/tag:");
462             attributes.add("/sortBy:");
463             attributes.add("/vendor:");
464             if (!UpdateSettings.getInstance().getPluginHosts().isEmpty()) {
465               attributes.add("/repository:");
466             }
467             return attributes;
468           }
469
470           @Nullable
471           @Override
472           protected List<String> getValues(@NotNull String attribute) {
473             switch (attribute) {
474               case "/tag:":
475                 if (ContainerUtil.isEmpty(myTagsSorted)) {
476                   Set<String> allTags = new HashSet<>();
477                   for (IdeaPluginDescriptor descriptor : myCustomRepositoryPluginsList) {
478                     if (descriptor instanceof PluginNode) {
479                       List<String> tags = ((PluginNode)descriptor).getTags();
480                       if (!ContainerUtil.isEmpty(tags)) {
481                         allTags.addAll(tags);
482                       }
483                     }
484                   }
485                   allTags.addAll(MarketplaceRequests.getAllPluginsTags());
486                   myTagsSorted = ContainerUtil.sorted(allTags, String::compareToIgnoreCase);
487                 }
488                 return myTagsSorted;
489               case "/sortBy:":
490                 return Arrays.asList("downloads", "name", "rating", "updated");
491               case "/vendor:":
492                 if (ContainerUtil.isEmpty(myVendorsSorted)) {
493                   List<String> customRepositoriesVendors = MyPluginModel.getVendors(myCustomRepositoryPluginsList);
494                   LinkedHashSet<String> vendors = new LinkedHashSet<>(customRepositoriesVendors);
495                   vendors.addAll(MarketplaceRequests.getAllPluginsVendors());
496                   myVendorsSorted = new ArrayList<>(vendors);
497                 }
498                 return myVendorsSorted;
499               case "/repository:":
500                 return UpdateSettings.getInstance().getPluginHosts();
501             }
502             return null;
503           }
504
505           @Override
506           protected void showPopupForQuery() {
507             showSearchPanel(mySearchTextField.getText());
508           }
509
510           @Override
511           protected void handleEnter() {
512             if (!mySearchTextField.getText().isEmpty()) {
513               handleTrigger("marketplace.suggest.popup.enter");
514             }
515           }
516
517           @Override
518           protected void handlePopupListFirstSelection() {
519             handleTrigger("marketplace.suggest.popup.select");
520           }
521
522           private void handleTrigger(@NonNls String key) {
523             if (myPopup != null && myPopup.type == SearchPopup.Type.SearchQuery) {
524               FeatureUsageTracker.getInstance().triggerFeatureUsed(key);
525             }
526           }
527         };
528
529         myMarketplaceSortByGroup = new DefaultActionGroup();
530
531         for (SortBySearchOption option : SortBySearchOption.values()) {
532           myMarketplaceSortByGroup.addAction(new MarketplaceSortByAction(option));
533         }
534
535         myMarketplaceSortByAction = new LinkComponent() {
536           @Override
537           protected boolean isInClickableArea(Point pt) {
538             return true;
539           }
540         };
541         myMarketplaceSortByAction.setIcon(new Icon() {
542           @Override
543           public void paintIcon(Component c, Graphics g, int x, int y) {
544             getIcon().paintIcon(c, g, x, y + 1);
545           }
546
547           @Override
548           public int getIconWidth() {
549             return getIcon().getIconWidth();
550           }
551
552           @Override
553           public int getIconHeight() {
554             return getIcon().getIconHeight();
555           }
556
557           @NotNull
558           private Icon getIcon() {
559             return AllIcons.General.ButtonDropTriangle;
560           }
561         }); // TODO: icon
562         myMarketplaceSortByAction.setPaintUnderline(false);
563         myMarketplaceSortByAction.setIconTextGap(JBUIScale.scale(4));
564         myMarketplaceSortByAction.setHorizontalTextPosition(SwingConstants.LEFT);
565         myMarketplaceSortByAction.setForeground(PluginsGroupComponent.SECTION_HEADER_FOREGROUND);
566
567         //noinspection unchecked
568         myMarketplaceSortByAction.setListener(
569           (component, __) -> showRightBottomPopup(component.getParent().getParent(), IdeBundle.message("plugins.configurable.sort.by"),
570                                                   myMarketplaceSortByGroup), null);
571
572         myMarketplaceSortByCallback = updateAction -> {
573           MarketplaceSortByAction removeAction = null;
574           MarketplaceSortByAction addAction = null;
575
576           if (updateAction.myState) {
577             for (AnAction action : myMarketplaceSortByGroup.getChildren(null)) {
578               MarketplaceSortByAction sortByAction = (MarketplaceSortByAction)action;
579               if (sortByAction != updateAction && sortByAction.myState) {
580                 sortByAction.myState = false;
581                 removeAction = sortByAction;
582                 break;
583               }
584             }
585             addAction = updateAction;
586           }
587           else {
588             if (updateAction.myOption == SortBySearchOption.Relevance) {
589               updateAction.myState = true;
590               return;
591             }
592
593             for (AnAction action : myMarketplaceSortByGroup.getChildren(null)) {
594               MarketplaceSortByAction sortByAction = (MarketplaceSortByAction)action;
595               if (sortByAction.myOption == SortBySearchOption.Relevance) {
596                 sortByAction.myState = true;
597                 break;
598               }
599             }
600
601             removeAction = updateAction;
602           }
603
604           List<String> queries = new ArrayList<>();
605           new SearchQueryParser.Marketplace(mySearchTextField.getText()) {
606             @Override
607             protected void addToSearchQuery(@NotNull String query) {
608               queries.add(query);
609             }
610
611             @Override
612             protected void handleAttribute(@NotNull String name, @NotNull String value) {
613               queries.add(name + SearchQueryParser.wrapAttribute(value));
614             }
615           };
616           if (removeAction != null) {
617             String query = removeAction.getQuery();
618             if (query != null) {
619               queries.remove(query);
620             }
621           }
622           if (addAction != null) {
623             String query = addAction.getQuery();
624             if (query != null) {
625               queries.add(query);
626             }
627           }
628
629           String query = StringUtil.join(queries, " ");
630           mySearchTextField.setTextIgnoreEvents(query);
631           if (query.isEmpty()) {
632             myMarketplaceTab.hideSearchPanel();
633           }
634           else {
635             myMarketplaceTab.showSearchPanel(query);
636           }
637         };
638
639         MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
640         marketplaceController.setSearchResultEventHandler(eventHandler);
641
642         PluginsGroupComponentWithProgress panel =
643           new PluginsGroupComponentWithProgress(new PluginListLayout(), eventHandler,
644                                                 descriptor -> new ListPluginComponent(myPluginModel, descriptor, mySearchListener, true));
645
646         panel.setSelectionListener(selectionListener);
647         registerCopyProvider(panel);
648
649         myMarketplaceSearchPanel =
650           new SearchResultPanel(marketplaceController, panel, 0, 0) {
651             @Override
652             protected void handleQuery(@NotNull String query, @NotNull PluginsGroup result) {
653               try {
654                 Map<String, List<IdeaPluginDescriptor>> customRepositoriesMap = loadCustomRepositoryPlugins();
655
656                 SearchQueryParser.Marketplace parser = new SearchQueryParser.Marketplace(query);
657
658                 if (!parser.repositories.isEmpty()) {
659                   for (String repository : parser.repositories) {
660                     List<IdeaPluginDescriptor> descriptors = customRepositoriesMap.get(repository);
661                     if (descriptors == null) {
662                       continue;
663                     }
664                     if (parser.searchQuery == null) {
665                       result.descriptors.addAll(descriptors);
666                     }
667                     else {
668                       for (IdeaPluginDescriptor descriptor : descriptors) {
669                         if (StringUtil.containsIgnoreCase(descriptor.getName(), parser.searchQuery)) {
670                           result.descriptors.add(descriptor);
671                         }
672                       }
673                     }
674                   }
675                   ContainerUtil.removeDuplicates(result.descriptors);
676                   result.sortByName();
677                   return;
678                 }
679
680                 List<PluginNode> plugins = MarketplaceRequests.searchPlugins(parser.getUrlQuery(), 10000);
681                 result.descriptors.addAll(plugins);
682
683                 if (parser.searchQuery != null) {
684                   String builtinUrl = ApplicationInfoEx.getInstanceEx().getBuiltinPluginsUrl();
685                   List<IdeaPluginDescriptor> builtinList = new ArrayList<>();
686
687                   for (Entry<String, List<IdeaPluginDescriptor>> entry : customRepositoriesMap.entrySet()) {
688                     List<IdeaPluginDescriptor> descriptors = entry.getKey().equals(builtinUrl) ? builtinList : result.descriptors;
689                     for (IdeaPluginDescriptor descriptor : entry.getValue()) {
690                       if (StringUtil.containsIgnoreCase(descriptor.getName(), parser.searchQuery)) {
691                         descriptors.add(descriptor);
692                       }
693                     }
694                   }
695
696                   result.descriptors.addAll(0, builtinList);
697                 }
698
699                 ContainerUtil.removeDuplicates(result.descriptors);
700
701                 if (!result.descriptors.isEmpty()) {
702                   String title = "Sort By";
703
704                   for (AnAction action : myMarketplaceSortByGroup.getChildren(null)) {
705                     MarketplaceSortByAction sortByAction = (MarketplaceSortByAction)action;
706                     sortByAction.setState(parser);
707                     if (sortByAction.myState) {
708                       title = "Sort By: " + sortByAction.myOption.name();
709                     }
710                   }
711
712                   myMarketplaceSortByAction.setText(title);
713                   result.addRightAction(myMarketplaceSortByAction);
714                 }
715               }
716               catch (IOException e) {
717                 LOG.info(e);
718                 ApplicationManager.getApplication().invokeLater(
719                   () -> myPanel.getEmptyText()
720                     .setText(IdeBundle.message("plugins.configurable.search.result.not.loaded"))
721                     .appendSecondaryText(
722                       IdeBundle.message("plugins.configurable.check.internet"), StatusText.DEFAULT_ATTRIBUTES, null), ModalityState.any()
723                 );
724               }
725             }
726           };
727
728         return myMarketplaceSearchPanel;
729       }
730     };
731   }
732
733   private void createInstalledTab() {
734     myInstalledSearchGroup = new DefaultActionGroup();
735
736     for (InstalledSearchOption option : InstalledSearchOption.values()) {
737       myInstalledSearchGroup.add(new InstalledSearchOptionAction(option));
738     }
739
740     myInstalledTab = new PluginsTab() {
741       @Override
742       protected void createSearchTextField(int flyDelay) {
743         super.createSearchTextField(flyDelay);
744
745         JBTextField textField = mySearchTextField.getTextEditor();
746         textField.putClientProperty("search.extension", ExtendableTextComponent.Extension
747           .create(AllIcons.Actions.More, AllIcons.Actions.More, IdeBundle.message("plugins.configurable.search.options"), // TODO: icon
748                   () -> showRightBottomPopup(textField, IdeBundle.message("plugins.configurable.show"), myInstalledSearchGroup)));
749         textField.putClientProperty("JTextField.variant", null);
750         textField.putClientProperty("JTextField.variant", "search");
751
752         mySearchTextField.setHistoryPropertyName("InstalledPluginsSearchHistory");
753       }
754
755       @NotNull
756       @Override
757       protected PluginDetailsPageComponent createDetailsPanel(@NotNull LinkListener<Object> searchListener) {
758         PluginDetailsPageComponent detailPanel = new PluginDetailsPageComponent(myPluginModel, searchListener, false);
759         myPluginModel.addDetailPanel(detailPanel);
760         return detailPanel;
761       }
762
763       @NotNull
764       @Override
765       protected JComponent createPluginsPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
766         MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
767         myInstalledPanel =
768           new PluginsGroupComponent(new PluginListLayout(), eventHandler,
769                                     descriptor -> new ListPluginComponent(myPluginModel, descriptor, mySearchListener, false));
770
771         myInstalledPanel.setSelectionListener(selectionListener);
772         registerCopyProvider(myInstalledPanel);
773
774         //noinspection ConstantConditions
775         ((SearchUpDownPopupController)myInstalledSearchPanel.controller).setEventHandler(eventHandler);
776
777         PluginLogo.startBatchMode();
778
779         PluginsGroup installing = new PluginsGroup(IdeBundle.message("plugins.configurable.installing"));
780         installing.descriptors.addAll(MyPluginModel.getInstallingPlugins());
781         if (!installing.descriptors.isEmpty()) {
782           installing.sortByName();
783           installing.titleWithCount();
784           myInstalledPanel.addGroup(installing);
785         }
786
787         PluginsGroup downloaded = new PluginsGroup(IdeBundle.message("plugins.configurable.downloaded"));
788         downloaded.descriptors.addAll(InstalledPluginsState.getInstance().getInstalledPlugins());
789
790         Map<String, List<IdeaPluginDescriptor>> bundledGroups = new HashMap<>();
791         ApplicationInfoEx appInfo = ApplicationInfoEx.getInstanceEx();
792         int downloadedEnabled = 0;
793         boolean hideImplDetails = PluginManager.getInstance().hideImplementationDetails();
794         String otherCategoryTitle = IdeBundle.message("plugins.configurable.other.bundled");
795
796         for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
797           if (!appInfo.isEssentialPlugin(descriptor.getPluginId())) {
798             if (descriptor.isBundled()) {
799               if (hideImplDetails && descriptor.isImplementationDetail()) {
800                 continue;
801               }
802               String category = StringUtil.defaultIfEmpty(descriptor.getCategory(), otherCategoryTitle);
803               List<IdeaPluginDescriptor> groupDescriptors = bundledGroups.get(category);
804               if (groupDescriptors == null) {
805                 bundledGroups.put(category, groupDescriptors = new ArrayList<>());
806               }
807               groupDescriptors.add(descriptor);
808             }
809             else {
810               downloaded.descriptors.add(descriptor);
811               if (descriptor.isEnabled()) {
812                 downloadedEnabled++;
813               }
814             }
815           }
816         }
817
818         if (!downloaded.descriptors.isEmpty()) {
819           myUpdateAll.setListener(new LinkListener<Object>() {
820             @Override
821             public void linkSelected(LinkLabel<Object> aSource, Object aLinkData) {
822               myUpdateAll.setEnabled(false);
823
824               for (UIPluginGroup group : myInstalledPanel.getGroups()) {
825                 for (ListPluginComponent plugin : group.plugins) {
826                   plugin.updatePlugin();
827                 }
828               }
829             }
830           }, null);
831           downloaded.addRightAction(myUpdateAll);
832
833           downloaded.addRightAction(myUpdateCounter);
834
835           downloaded.sortByName();
836           downloaded.titleWithCount(downloadedEnabled);
837           myInstalledPanel.addGroup(downloaded);
838           myPluginModel.addEnabledGroup(downloaded);
839         }
840
841         myPluginModel.setDownloadedGroup(myInstalledPanel, downloaded, installing);
842
843         List<PluginsGroup> groups = new ArrayList<>();
844
845         for (Entry<String, List<IdeaPluginDescriptor>> entry : bundledGroups.entrySet()) {
846           PluginsGroup group = new PluginsGroup(entry.getKey()) {
847             @Override
848             public void titleWithCount(int enabled) {
849               rightAction.setText(enabled == 0 ? IdeBundle.message("plugins.configurable.enable.all")
850                                                : IdeBundle.message("plugins.configurable.disable.all"));
851             }
852           };
853           group.descriptors.addAll(entry.getValue());
854           group.sortByName();
855           group.rightAction = new LinkLabel<>("", null, (__, ___) -> myPluginModel
856             .changeEnableDisable(ContainerUtil.toArray(group.descriptors, IdeaPluginDescriptor[]::new),
857                                  group.rightAction.getText().startsWith("Enable")));
858           group.titleWithEnabled(myPluginModel);
859           groups.add(group);
860         }
861
862         ContainerUtil.sort(groups, (o1, o2) -> StringUtil.compare(o1.title, o2.title, true));
863         PluginsGroup otherGroup = ContainerUtil.find(groups, group -> group.title.equals(otherCategoryTitle));
864         if (otherGroup != null) {
865           groups.remove(otherGroup);
866           groups.add(otherGroup);
867         }
868
869         for (PluginsGroup group : groups) {
870           myInstalledPanel.addGroup(group);
871           myPluginModel.addEnabledGroup(group);
872         }
873
874         myPluginUpdatesService.connectInstalled(updates -> {
875           if (ContainerUtil.isEmpty(updates)) {
876             clearUpdates(myInstalledPanel);
877             clearUpdates(myInstalledSearchPanel.getPanel());
878           }
879           else {
880             applyUpdates(myInstalledPanel, updates);
881             applyUpdates(myInstalledSearchPanel.getPanel(), updates);
882           }
883           selectionListener.accept(myInstalledPanel);
884         });
885
886         PluginLogo.endBatchMode();
887
888         if (myInitUpdates != null) {
889           applyUpdates(myInstalledPanel, myInitUpdates);
890         }
891
892         return createScrollPane(myInstalledPanel, true);
893       }
894
895       @Override
896       protected void updateMainSelection(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
897         selectionListener.accept(myInstalledPanel);
898       }
899
900       @Override
901       public void hideSearchPanel() {
902         super.hideSearchPanel();
903         if (myInstalledSearchSetState) {
904           for (AnAction action : myInstalledSearchGroup.getChildren(null)) {
905             ((InstalledSearchOptionAction)action).setState(null);
906           }
907         }
908         myPluginModel.setInvalidFixCallback(null);
909       }
910
911       @NotNull
912       @Override
913       protected SearchResultPanel createSearchPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
914         SearchUpDownPopupController installedController = new SearchUpDownPopupController(mySearchTextField) {
915           @NotNull
916           @Override
917           protected List<String> getAttributes() {
918             return Arrays.asList("/downloaded", "/outdated", "/enabled", "/disabled", "/invalid", "/bundled", "/vendor:", "/tag:");
919           }
920
921           @Nullable
922           @Override
923           protected List<String> getValues(@NotNull String attribute) {
924             if ("/vendor:".equals(attribute)) {
925               return myPluginModel.getVendors();
926             }
927             if ("/tag:".equals(attribute)) {
928               return myPluginModel.getTags();
929             }
930             return null;
931           }
932
933           @Override
934           protected void showPopupForQuery() {
935             showSearchPanel(mySearchTextField.getText());
936           }
937         };
938
939         MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
940         installedController.setSearchResultEventHandler(eventHandler);
941
942         PluginsGroupComponent panel =
943           new PluginsGroupComponent(new PluginListLayout(), eventHandler,
944                                     descriptor -> new ListPluginComponent(myPluginModel, descriptor, mySearchListener, false));
945
946         panel.setSelectionListener(selectionListener);
947         registerCopyProvider(panel);
948
949         myInstalledSearchCallback = updateAction -> {
950           List<String> queries = new ArrayList<>();
951           new SearchQueryParser.Installed(mySearchTextField.getText()) {
952             @Override
953             protected void addToSearchQuery(@NotNull String query) {
954               queries.add(query);
955             }
956
957             @Override
958             protected void handleAttribute(@NotNull String name, @NotNull String value) {
959               if (!updateAction.myState) {
960                 queries.add(name + (value.isEmpty() ? "" : SearchQueryParser.wrapAttribute(value)));
961               }
962             }
963           };
964
965           if (updateAction.myState) {
966             for (AnAction action : myInstalledSearchGroup.getChildren(null)) {
967               if (action != updateAction) {
968                 ((InstalledSearchOptionAction)action).myState = false;
969               }
970             }
971
972             queries.add(updateAction.getQuery());
973           }
974           else {
975             queries.remove(updateAction.getQuery());
976           }
977
978           try {
979             myInstalledSearchSetState = false;
980
981             String query = StringUtil.join(queries, " ");
982             mySearchTextField.setTextIgnoreEvents(query);
983             if (query.isEmpty()) {
984               myInstalledTab.hideSearchPanel();
985             }
986             else {
987               myInstalledTab.showSearchPanel(query);
988             }
989           }
990           finally {
991             myInstalledSearchSetState = true;
992           }
993         };
994
995         myInstalledSearchPanel = new SearchResultPanel(installedController, panel, 0, 0) {
996           @Override
997           protected void setEmptyText(@NotNull String query) {
998             myPanel.getEmptyText().setText(IdeBundle.message("plugins.configurable.nothing.found"));
999             if (query.contains("/downloaded") || query.contains("/outdated") ||
1000                 query.contains("/enabled") || query.contains("/disabled") ||
1001                 query.contains("/invalid") || query.contains("/bundled")) {
1002               return;
1003             }
1004             myPanel.getEmptyText().appendSecondaryText(IdeBundle.message("plugins.configurable.search.in.marketplace"),
1005                                                        SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES,
1006                                                        e -> myTabHeaderComponent.setSelectionWithEvents(MARKETPLACE_TAB));
1007           }
1008
1009           @Override
1010           protected void handleQuery(@NotNull String query, @NotNull PluginsGroup result) {
1011             myPluginModel.setInvalidFixCallback(null);
1012
1013             SearchQueryParser.Installed parser = new SearchQueryParser.Installed(query);
1014
1015             if (myInstalledSearchSetState) {
1016               for (AnAction action : myInstalledSearchGroup.getChildren(null)) {
1017                 ((InstalledSearchOptionAction)action).setState(parser);
1018               }
1019             }
1020
1021             List<IdeaPluginDescriptor> descriptors = myPluginModel.getInstalledDescriptors();
1022
1023             if (!parser.vendors.isEmpty()) {
1024               for (Iterator<IdeaPluginDescriptor> I = descriptors.iterator(); I.hasNext(); ) {
1025                 if (!MyPluginModel.isVendor(I.next(), parser.vendors)) {
1026                   I.remove();
1027                 }
1028               }
1029             }
1030             if (!parser.tags.isEmpty()) {
1031               for (Iterator<IdeaPluginDescriptor> I = descriptors.iterator(); I.hasNext(); ) {
1032                 if (!ContainerUtil.intersects(getTags(I.next()), parser.tags)) {
1033                   I.remove();
1034                 }
1035               }
1036             }
1037             for (Iterator<IdeaPluginDescriptor> I = descriptors.iterator(); I.hasNext(); ) {
1038               IdeaPluginDescriptor descriptor = I.next();
1039               if (parser.attributes) {
1040                 if (parser.enabled && (!myPluginModel.isEnabled(descriptor) || myPluginModel.hasErrors(descriptor))) {
1041                   I.remove();
1042                   continue;
1043                 }
1044                 if (parser.disabled && (myPluginModel.isEnabled(descriptor) || myPluginModel.hasErrors(descriptor))) {
1045                   I.remove();
1046                   continue;
1047                 }
1048                 if (parser.bundled && !descriptor.isBundled()) {
1049                   I.remove();
1050                   continue;
1051                 }
1052                 if (parser.downloaded && descriptor.isBundled()) {
1053                   I.remove();
1054                   continue;
1055                 }
1056                 if (parser.invalid && !myPluginModel.hasErrors(descriptor)) {
1057                   I.remove();
1058                   continue;
1059                 }
1060                 if (parser.needUpdate && !PluginUpdatesService.isNeedUpdate(descriptor)) {
1061                   I.remove();
1062                   continue;
1063                 }
1064               }
1065               if (parser.searchQuery != null && !StringUtil.containsIgnoreCase(descriptor.getName(), parser.searchQuery)) {
1066                 I.remove();
1067               }
1068             }
1069
1070             result.descriptors.addAll(descriptors);
1071
1072             if (!result.descriptors.isEmpty()) {
1073               if (parser.invalid) {
1074                 myPluginModel.setInvalidFixCallback(() -> {
1075                   PluginsGroup group = myInstalledSearchPanel.getGroup();
1076                   if (group.ui == null) {
1077                     myPluginModel.setInvalidFixCallback(null);
1078                     return;
1079                   }
1080
1081                   PluginsGroupComponent resultPanel = myInstalledSearchPanel.getPanel();
1082
1083                   for (IdeaPluginDescriptor descriptor : new ArrayList<>(group.descriptors)) {
1084                     if (!myPluginModel.hasErrors(descriptor)) {
1085                       resultPanel.removeFromGroup(group, descriptor);
1086                     }
1087                   }
1088
1089                   group.titleWithCount();
1090                   myInstalledSearchPanel.fullRepaint();
1091
1092                   if (group.descriptors.isEmpty()) {
1093                     myPluginModel.setInvalidFixCallback(null);
1094                   }
1095                 });
1096               }
1097               else if (parser.needUpdate) {
1098                 result.rightAction = new LinkLabel<>(IdeBundle.message("plugin.manager.update.all"), null, (__, ___) -> {
1099                   result.rightAction.setEnabled(false);
1100
1101                   for (ListPluginComponent plugin : result.ui.plugins) {
1102                     plugin.updatePlugin();
1103                   }
1104                 });
1105               }
1106
1107               Collection<PluginDownloader> updates = myInitUpdates == null ? PluginUpdatesService.getUpdates() : myInitUpdates;
1108               myInitUpdates = null;
1109               if (!ContainerUtil.isEmpty(updates)) {
1110                 myPostFillGroupCallback = () -> {
1111                   applyUpdates(myPanel, updates);
1112                   selectionListener.accept(myInstalledPanel);
1113                 };
1114               }
1115             }
1116           }
1117         };
1118
1119         return myInstalledSearchPanel;
1120       }
1121     };
1122   }
1123
1124   private static void clearUpdates(@NotNull PluginsGroupComponent panel) {
1125     for (UIPluginGroup group : panel.getGroups()) {
1126       for (ListPluginComponent plugin : group.plugins) {
1127         plugin.setUpdateDescriptor(null);
1128       }
1129     }
1130   }
1131
1132   private static void applyUpdates(@NotNull PluginsGroupComponent panel, @NotNull Collection<PluginDownloader> updates) {
1133     for (PluginDownloader downloader : updates) {
1134       IdeaPluginDescriptor descriptor = downloader.getDescriptor();
1135       for (UIPluginGroup group : panel.getGroups()) {
1136         ListPluginComponent component = group.findComponent(descriptor);
1137         if (component != null) {
1138           component.setUpdateDescriptor(descriptor);
1139           break;
1140         }
1141       }
1142     }
1143   }
1144
1145   public static void registerCopyProvider(@NotNull PluginsGroupComponent component) {
1146     CopyProvider copyProvider = new CopyProvider() {
1147       @Override
1148       public void performCopy(@NotNull DataContext dataContext) {
1149         StringBuilder result = new StringBuilder();
1150         for (ListPluginComponent pluginComponent : component.getSelection()) {
1151           result.append(pluginComponent.myPlugin.getName()).append(" (").append(pluginComponent.myPlugin.getVersion()).append(")\n");
1152         }
1153         CopyPasteManager.getInstance().setContents(new TextTransferable(result.substring(0, result.length() - 1)));
1154       }
1155
1156       @Override
1157       public boolean isCopyEnabled(@NotNull DataContext dataContext) {
1158         return !component.getSelection().isEmpty();
1159       }
1160
1161       @Override
1162       public boolean isCopyVisible(@NotNull DataContext dataContext) {
1163         return true;
1164       }
1165     };
1166
1167     DataManager.registerDataProvider(component, dataId -> PlatformDataKeys.COPY_PROVIDER.is(dataId) ? copyProvider : null);
1168   }
1169
1170   @NotNull
1171   public static List<String> getTags(@NotNull IdeaPluginDescriptor plugin) {
1172     List<String> tags = null;
1173     String productCode = plugin.getProductCode();
1174
1175     if (plugin instanceof PluginNode) {
1176       tags = ((PluginNode)plugin).getTags();
1177
1178       if (productCode != null && tags != null && !tags.contains("Paid")) {
1179         tags = new ArrayList<>(tags);
1180         tags.add(0, "Paid");
1181       }
1182     }
1183     else if (productCode != null && !plugin.isBundled()) {
1184       LicensingFacade instance = LicensingFacade.getInstance();
1185       if (instance != null) {
1186         String stamp = instance.getConfirmationStamp(productCode);
1187         if (stamp != null) {
1188           return Collections.singletonList(stamp.startsWith("eval:") ? "Trial" : "Purchased");
1189         }
1190       }
1191       return Collections.singletonList("Paid");
1192     }
1193     if (ContainerUtil.isEmpty(tags)) {
1194       return Collections.emptyList();
1195     }
1196
1197     if (tags.size() > 1) {
1198       tags = new ArrayList<>(tags);
1199       if (tags.remove("EAP")) {
1200         tags.add(0, "EAP");
1201       }
1202       if (tags.remove("Paid")) {
1203         tags.add(0, "Paid");
1204       }
1205     }
1206
1207     return tags;
1208   }
1209
1210   @NotNull
1211   public static <T extends Component> T setTinyFont(@NotNull T component) {
1212     return SystemInfo.isMac ? RelativeFont.TINY.install(component) : component;
1213   }
1214
1215   public static int offset5() {
1216     return JBUIScale.scale(5);
1217   }
1218
1219   @Nullable
1220   public static synchronized String getDownloads(@NotNull IdeaPluginDescriptor plugin) {
1221     String downloads = null;
1222     if (plugin instanceof PluginNode) {
1223       downloads = ((PluginNode)plugin).getDownloads();
1224     }
1225     return getFormatLength(downloads);
1226   }
1227
1228   @Nullable
1229   static synchronized String getFormatLength(@Nullable String len) {
1230     if (!StringUtil.isEmptyOrSpaces(len)) {
1231       try {
1232         long value = Long.parseLong(len);
1233         if (value > 1000) {
1234           return value < 1000000 ? K_FORMAT.format(value / 1000D) : M_FORMAT.format(value / 1000000D);
1235         }
1236         return Long.toString(value);
1237       }
1238       catch (NumberFormatException ignore) {
1239       }
1240     }
1241
1242     return null;
1243   }
1244
1245   @Nullable
1246   public static synchronized String getLastUpdatedDate(@NotNull IdeaPluginDescriptor plugin) {
1247     long date = 0;
1248     if (plugin instanceof PluginNode) {
1249       date = ((PluginNode)plugin).getDate();
1250     }
1251     return date > 0 && date != Long.MAX_VALUE ? DATE_FORMAT.format(new Date(date)) : null;
1252   }
1253
1254   @Nullable
1255   public static String getRating(@NotNull IdeaPluginDescriptor plugin) {
1256     String rating = null;
1257     if (plugin instanceof PluginNode) {
1258       rating = ((PluginNode)plugin).getRating();
1259     }
1260     if (rating != null) {
1261       try {
1262         if (Double.valueOf(rating) > 0) {
1263           return StringUtil.trimEnd(rating, ".0");
1264         }
1265       }
1266       catch (NumberFormatException ignore) {
1267       }
1268     }
1269     return null;
1270   }
1271
1272   @Nullable
1273   public static synchronized String getSize(@NotNull IdeaPluginDescriptor plugin) {
1274     String size = null;
1275     if (plugin instanceof PluginNode) {
1276       size = ((PluginNode)plugin).getSize();
1277     }
1278     return getFormatLength(size);
1279   }
1280
1281   @NotNull
1282   public static String getVersion(@NotNull IdeaPluginDescriptor oldPlugin, @NotNull IdeaPluginDescriptor newPlugin) {
1283     return StringUtil.defaultIfEmpty(oldPlugin.getVersion(), "unknown") +
1284            " " + UIUtil.rightArrow() + " " +
1285            StringUtil.defaultIfEmpty(newPlugin.getVersion(), "unknown");
1286   }
1287
1288   @Messages.YesNoResult
1289   public static int showRestartDialog() {
1290     return showRestartDialog(IdeBundle.message("update.notifications.title"));
1291   }
1292
1293   @Messages.YesNoResult
1294   public static int showRestartDialog(@NotNull String title) {
1295     return showRestartDialog(title, action -> IdeBundle
1296       .message("ide.restart.required.message", action, ApplicationNamesInfo.getInstance().getFullProductName()));
1297   }
1298
1299   @Messages.YesNoResult
1300   public static int showRestartDialog(@NotNull String title, @NotNull Function<String, String> message) {
1301     String action =
1302       IdeBundle.message(ApplicationManager.getApplication().isRestartCapable() ? "ide.restart.action" : "ide.shutdown.action");
1303     return Messages
1304       .showYesNoDialog(message.apply(action), title, action, IdeBundle.message("ide.notnow.action"), Messages.getQuestionIcon());
1305   }
1306
1307   public static void shutdownOrRestartApp() {
1308     shutdownOrRestartApp(IdeBundle.message("update.notifications.title"));
1309   }
1310
1311   public static void shutdownOrRestartApp(@NotNull String title) {
1312     if (showRestartDialog(title) == Messages.YES) {
1313       ApplicationManagerEx.getApplicationEx().restart(true);
1314     }
1315   }
1316
1317   public static void shutdownOrRestartAppAfterInstall(@NotNull String plugin) {
1318     String title = IdeBundle.message("update.notifications.title");
1319     Function<String, String> message = action -> IdeBundle
1320       .message("plugin.installed.ide.restart.required.message", plugin, action, ApplicationNamesInfo.getInstance().getFullProductName());
1321
1322     if (showRestartDialog(title, message) == Messages.YES) {
1323       ApplicationManagerEx.getApplicationEx().restart(true);
1324     }
1325   }
1326
1327   public static void showPluginConfigurableAndEnable(@Nullable Project project, IdeaPluginDescriptor @NotNull ... descriptors) {
1328     PluginManagerConfigurable configurable = new PluginManagerConfigurable();
1329     ShowSettingsUtil.getInstance().editConfigurable(project, configurable, () -> {
1330       configurable.getPluginModel().changeEnableDisable(descriptors, true);
1331       configurable.select(descriptors);
1332     });
1333   }
1334
1335   public static void showPluginConfigurable(@Nullable Project project, IdeaPluginDescriptor @NotNull ... descriptors) {
1336     PluginManagerConfigurable configurable = new PluginManagerConfigurable();
1337     ShowSettingsUtil.getInstance().editConfigurable(project, configurable, () -> configurable.select(descriptors));
1338   }
1339
1340   public static void showPluginConfigurable(@Nullable Project project, @NotNull Collection<PluginDownloader> updates) {
1341     PluginManagerConfigurable configurable = new PluginManagerConfigurable();
1342     configurable.setInitUpdates(updates);
1343     ShowSettingsUtil.getInstance().editConfigurable(project, configurable);
1344   }
1345
1346   private enum SortBySearchOption {
1347     Downloads, Name, Rating, Relevance, Updated
1348   }
1349
1350   private class MarketplaceSortByAction extends ToggleAction implements DumbAware {
1351     private final SortBySearchOption myOption;
1352     private boolean myState;
1353
1354     private MarketplaceSortByAction(@NotNull SortBySearchOption option) {
1355       super(option.name());
1356       myOption = option;
1357     }
1358
1359     @Override
1360     public boolean isSelected(@NotNull AnActionEvent e) {
1361       return myState;
1362     }
1363
1364     @Override
1365     public void setSelected(@NotNull AnActionEvent e, boolean state) {
1366       myState = state;
1367       myMarketplaceSortByCallback.accept(this);
1368     }
1369
1370     public void setState(@NotNull SearchQueryParser.Marketplace parser) {
1371       if (myOption == SortBySearchOption.Relevance) {
1372         myState = parser.sortBy == null;
1373         getTemplatePresentation().setVisible(
1374           parser.sortBy == null || !parser.tags.isEmpty() || !parser.vendors.isEmpty() || parser.searchQuery != null
1375         );
1376       }
1377       else {
1378         myState = parser.sortBy != null && myOption.name().equalsIgnoreCase(parser.sortBy);
1379       }
1380     }
1381
1382     @Nullable
1383     public String getQuery() {
1384       switch (myOption) {
1385         case Downloads:
1386           return "/sortBy:downloads";
1387         case Name:
1388           return "/sortBy:name";
1389         case Rating:
1390           return "/sortBy:rating";
1391         case Updated:
1392           return "/sortBy:updated";
1393         case Relevance:
1394         default:
1395           return null;
1396       }
1397     }
1398   }
1399
1400   private enum InstalledSearchOption {
1401     Downloaded, NeedUpdate, Enabled, Disabled, Invalid, Bundled
1402   }
1403
1404   private class InstalledSearchOptionAction extends ToggleAction implements DumbAware {
1405     private final InstalledSearchOption myOption;
1406     private boolean myState;
1407
1408     private InstalledSearchOptionAction(@NotNull InstalledSearchOption option) {
1409       super(option == InstalledSearchOption.NeedUpdate ? IdeBundle.message("plugins.configurable.update.available") : option.name());
1410       myOption = option;
1411     }
1412
1413     @Override
1414     public boolean isSelected(@NotNull AnActionEvent e) {
1415       return myState;
1416     }
1417
1418     @Override
1419     public void setSelected(@NotNull AnActionEvent e, boolean state) {
1420       myState = state;
1421       myInstalledSearchCallback.accept(this);
1422     }
1423
1424     public void setState(@Nullable SearchQueryParser.Installed parser) {
1425       if (parser == null) {
1426         myState = false;
1427         return;
1428       }
1429
1430       switch (myOption) {
1431         case Enabled:
1432           myState = parser.enabled;
1433           break;
1434         case Disabled:
1435           myState = parser.disabled;
1436           break;
1437         case Downloaded:
1438           myState = parser.downloaded;
1439           break;
1440         case Bundled:
1441           myState = parser.bundled;
1442           break;
1443         case Invalid:
1444           myState = parser.invalid;
1445           break;
1446         case NeedUpdate:
1447           myState = parser.needUpdate;
1448           break;
1449       }
1450     }
1451
1452     @NotNull
1453     public String getQuery() {
1454       return myOption == InstalledSearchOption.NeedUpdate ? "/outdated" : "/" + StringUtil.decapitalize(myOption.name());
1455     }
1456   }
1457
1458   private static class GroupByActionGroup extends DefaultActionGroup implements CheckedActionGroup {
1459   }
1460
1461   private class ChangePluginStateAction extends DumbAwareAction {
1462     private final boolean myEnable;
1463
1464     private ChangePluginStateAction(boolean enable) {
1465       super(enable ? IdeBundle.message("plugins.configurable.enable.all.downloaded")
1466                    : IdeBundle.message("plugins.configurable.disable.all.downloaded"));
1467       myEnable = enable;
1468     }
1469
1470     @Override
1471     public void actionPerformed(@NotNull AnActionEvent e) {
1472       IdeaPluginDescriptor[] descriptors;
1473       PluginsGroup group = myPluginModel.getDownloadedGroup();
1474
1475       if (group == null || group.ui == null) {
1476         ApplicationInfoImpl appInfo = (ApplicationInfoImpl)ApplicationInfo.getInstance();
1477         List<IdeaPluginDescriptor> descriptorList = new ArrayList<>();
1478
1479         for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
1480           if (!appInfo.isEssentialPlugin(descriptor.getPluginId()) &&
1481               !descriptor.isBundled() && descriptor.isEnabled() != myEnable) {
1482             descriptorList.add(descriptor);
1483           }
1484         }
1485
1486         descriptors = descriptorList.toArray(new IdeaPluginDescriptor[0]);
1487       }
1488       else {
1489         descriptors = group.ui.plugins.stream().filter(component -> myPluginModel.isEnabled(component.myPlugin) != myEnable)
1490           .map(component -> component.myPlugin).toArray(IdeaPluginDescriptor[]::new);
1491       }
1492
1493       if (descriptors.length > 0) {
1494         myPluginModel.changeEnableDisable(descriptors, myEnable);
1495       }
1496     }
1497   }
1498
1499   @NotNull
1500   public static JComponent createScrollPane(@NotNull PluginsGroupComponent panel, boolean initSelection) {
1501     JBScrollPane pane =
1502       new JBScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
1503     pane.setBorder(JBUI.Borders.empty());
1504     if (initSelection) {
1505       panel.initialSelection();
1506     }
1507     return pane;
1508   }
1509
1510   @NotNull
1511   private Map<String, List<IdeaPluginDescriptor>> loadCustomRepositoryPlugins() {
1512     synchronized (myRepositoriesLock) {
1513       if (myCustomRepositoryPluginsMap != null) {
1514         return myCustomRepositoryPluginsMap;
1515       }
1516     }
1517     Map<PluginId, IdeaPluginDescriptor> latestCustomPluginsAsMap = new HashMap<>();
1518     Map<String, List<IdeaPluginDescriptor>> customRepositoryPluginsMap = new HashMap<>();
1519     for (String host : RepositoryHelper.getPluginHosts()) {
1520       try {
1521         if (host != null) {
1522           List<IdeaPluginDescriptor> descriptors = RepositoryHelper.loadPlugins(host, null);
1523           for (IdeaPluginDescriptor descriptor : descriptors) {
1524             PluginId pluginId = descriptor.getPluginId();
1525             IdeaPluginDescriptor savedDescriptor = latestCustomPluginsAsMap.get(pluginId);
1526             if (savedDescriptor == null) {
1527               latestCustomPluginsAsMap.put(pluginId, descriptor);
1528             } else {
1529               if (StringUtil.compareVersionNumbers(descriptor.getVersion(), savedDescriptor.getVersion()) > 0) {
1530                 latestCustomPluginsAsMap.put(pluginId, descriptor);
1531               }
1532             }
1533           }
1534           customRepositoryPluginsMap.put(host, descriptors);
1535         }
1536       }
1537       catch (IOException e) {
1538         LOG.info(host, e);
1539       }
1540     }
1541
1542     ApplicationManager.getApplication().executeOnPooledThread(() -> {
1543       UpdateChecker.updateDescriptorsForInstalledPlugins(InstalledPluginsState.getInstance());
1544     });
1545
1546     synchronized (myRepositoriesLock) {
1547       if (myCustomRepositoryPluginsMap == null) {
1548         myCustomRepositoryPluginsMap = customRepositoryPluginsMap;
1549         myCustomRepositoryPluginsList = latestCustomPluginsAsMap.values();
1550       }
1551       return myCustomRepositoryPluginsMap;
1552     }
1553   }
1554
1555   private void addGroup(
1556     @NotNull List<? super PluginsGroup> groups,
1557     @NotNull @Nls String name,
1558     @NotNull String showAllQuery,
1559     @NotNull ThrowableNotNullFunction<? super List<IdeaPluginDescriptor>, Boolean, ? extends IOException> function
1560   )
1561     throws IOException {
1562     PluginsGroup group = new PluginsGroup(name);
1563
1564     if (Boolean.TRUE.equals(function.fun(group.descriptors))) {
1565       group.rightAction =
1566         new LinkLabel<>(IdeBundle.message("plugins.configurable.show.all"), null, myMarketplaceTab.mySearchListener, showAllQuery);
1567       group.rightAction.setBorder(JBUI.Borders.emptyRight(5));
1568     }
1569
1570     if (!group.descriptors.isEmpty()) {
1571       groups.add(group);
1572     }
1573   }
1574
1575   private void addGroupViaLightDescriptor(
1576     @NotNull List<? super PluginsGroup> groups,
1577     @NotNull @Nls String name,
1578     @NotNull @NonNls String query,
1579     @NotNull @NonNls String showAllQuery
1580   ) throws IOException {
1581     addGroup(groups, name, showAllQuery, descriptors -> {
1582       List<PluginNode> pluginNodes = MarketplaceRequests.searchPlugins(query, ITEMS_PER_GROUP);
1583       descriptors.addAll(pluginNodes);
1584       return pluginNodes.size() == ITEMS_PER_GROUP;
1585     });
1586   }
1587
1588   @Override
1589   @NotNull
1590   public String getHelpTopic() {
1591     return ID;
1592   }
1593
1594   @Override
1595   public void disposeUIResources() {
1596     if (myPluginModel.toBackground()) {
1597       InstallPluginInfo.showRestart();
1598       InstalledPluginsState.getInstance().clearShutdownCallback();
1599     }
1600
1601     myMarketplaceTab.dispose();
1602     myInstalledTab.dispose();
1603
1604     if (myMarketplacePanel != null) {
1605       myMarketplacePanel.dispose();
1606     }
1607     if (myMarketplaceSearchPanel != null) {
1608       myMarketplaceSearchPanel.dispose();
1609     }
1610
1611     myPluginUpdatesService.dispose();
1612     PluginPriceService.cancel();
1613
1614     InstalledPluginsState.getInstance().runShutdownCallback();
1615
1616     InstalledPluginsState.getInstance().resetChangesAppliedWithoutRestart();
1617   }
1618
1619   @Override
1620   public void cancel() {
1621     myPluginModel.removePluginsOnCancel(myCardPanel);
1622   }
1623
1624   @Override
1625   public boolean isModified() {
1626     return myPluginModel.isModified();
1627   }
1628
1629   @Override
1630   public void apply() throws ConfigurationException {
1631     if (myPluginModel.apply(myCardPanel)) return;
1632
1633     if (myPluginModel.createShutdownCallback) {
1634       InstalledPluginsState.getInstance()
1635         .setShutdownCallback(() -> ApplicationManager.getApplication().invokeLater(() -> shutdownOrRestartApp()));
1636     }
1637   }
1638
1639   @Override
1640   public void reset() {
1641     myPluginModel.removePluginsOnCancel(myCardPanel);
1642   }
1643
1644   @NotNull
1645   public MyPluginModel getPluginModel() {
1646     return myPluginModel;
1647   }
1648
1649   public void setInitUpdates(@NotNull Collection<PluginDownloader> initUpdates) {
1650     myInitUpdates = initUpdates;
1651   }
1652
1653   public void select(IdeaPluginDescriptor @NotNull ... descriptors) {
1654     if (myTabHeaderComponent.getSelectionTab() != INSTALLED_TAB) {
1655       myTabHeaderComponent.setSelectionWithEvents(INSTALLED_TAB);
1656     }
1657
1658     if (descriptors.length == 0) {
1659       return;
1660     }
1661
1662     List<ListPluginComponent> components = new ArrayList<>();
1663
1664     for (IdeaPluginDescriptor descriptor : descriptors) {
1665       for (UIPluginGroup group : myInstalledPanel.getGroups()) {
1666         ListPluginComponent component = group.findComponent(descriptor);
1667         if (component != null) {
1668           components.add(component);
1669           break;
1670         }
1671       }
1672     }
1673
1674     if (!components.isEmpty()) {
1675       myInstalledPanel.setSelection(components);
1676     }
1677   }
1678
1679   @Nullable
1680   @Override
1681   public Runnable enableSearch(String option) {
1682     if (StringUtil.isEmpty(option) && (myTabHeaderComponent.getSelectionTab() == MARKETPLACE_TAB || myInstalledSearchPanel.isEmpty())) {
1683       return null;
1684     }
1685
1686     return () -> {
1687       boolean marketplace = option != null && option.startsWith("/tag:");
1688       int tabIndex = marketplace ? MARKETPLACE_TAB : INSTALLED_TAB;
1689
1690       if (myTabHeaderComponent.getSelectionTab() != tabIndex) {
1691         myTabHeaderComponent.setSelectionWithEvents(tabIndex);
1692       }
1693
1694       PluginsTab tab = marketplace ? myMarketplaceTab : myInstalledTab;
1695       tab.clearSearchPanel(option);
1696
1697       if (!StringUtil.isEmpty(option)) {
1698         tab.showSearchPanel(option);
1699       }
1700     };
1701   }
1702
1703   private class InstallFromDiskAction extends DumbAwareAction {
1704     private InstallFromDiskAction() {super(IdeBundle.messagePointer("action.InstallFromDiskAction.text"));}
1705
1706     @Override
1707     public void actionPerformed(@NotNull AnActionEvent e) {
1708       PluginInstaller.chooseAndInstall(myPluginModel, myCardPanel, callbackData -> {
1709         myPluginModel.pluginInstalledFromDisk(callbackData);
1710
1711         boolean select = myInstalledPanel == null;
1712
1713         if (myTabHeaderComponent.getSelectionTab() != INSTALLED_TAB) {
1714           myTabHeaderComponent.setSelectionWithEvents(INSTALLED_TAB);
1715         }
1716
1717         myInstalledTab.clearSearchPanel("");
1718
1719         if (select) {
1720           for (UIPluginGroup group : myInstalledPanel.getGroups()) {
1721             ListPluginComponent component = group.findComponent(callbackData.getPluginDescriptor());
1722             if (component != null) {
1723               myInstalledPanel.setSelection(component);
1724               break;
1725             }
1726           }
1727         }
1728       });
1729     }
1730   }
1731 }