[profiler] IDEA-205452: migrate to xdebugger.attach API

This commit is contained in:
Kirill Timofeev
2019-01-21 16:22:27 +03:00
parent ca414ba8e9
commit d76b1a79b0
2 changed files with 720 additions and 698 deletions
@@ -1,707 +1,17 @@
// Copyright 2000-2018 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.
// Copyright 2000-2019 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.
package com.intellij.xdebugger.impl.actions;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.process.ProcessInfo;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.icons.AllIcons;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.*;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.ui.popup.async.AsyncPopupStep;
import com.intellij.ui.popup.list.ListPopupImpl;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.StatusText;
import com.intellij.xdebugger.XDebuggerBundle;
import com.intellij.xdebugger.attach.*;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.event.InputEvent;
import java.util.*;
public class AttachToProcessAction extends AnAction {
private static final Key<Map<XAttachHost, LinkedHashSet<RecentItem>>> RECENT_ITEMS_KEY = Key.create("AttachToProcessAction.RECENT_ITEMS_KEY");
private static final Logger LOG = Logger.getInstance(AttachToProcessAction.class);
import com.intellij.xdebugger.attach.XAttachDebuggerProvider;
public class AttachToProcessAction extends AttachToProcessActionBase {
public AttachToProcessAction() {
super(XDebuggerBundle.message("xdebugger.attach.action"),
XDebuggerBundle.message("xdebugger.attach.action.description"), AllIcons.Debugger.AttachToProcess);
}
@Override
public void update(@NotNull AnActionEvent e) {
super.update(e);
Project project = getEventProject(e);
int attachDebuggerProvidersNumber = XAttachDebuggerProvider.getAttachDebuggerProviders().size();
boolean enabled = project != null && attachDebuggerProvidersNumber > 0;
e.getPresentation().setEnabledAndVisible(enabled);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
final Project project = getEventProject(e);
if (project == null) return;
new Task.Backgroundable(project,
XDebuggerBundle.message("xdebugger.attach.action.collectingItems"), true,
PerformInBackgroundOption.DEAF) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
List<AttachItem> allItems = ContainerUtil.immutableList(getTopLevelItems(indicator, project));
ApplicationManager.getApplication().invokeLater(() -> {
AttachListStep step = new AttachListStep(allItems, XDebuggerBundle.message("xdebugger.attach.popup.title.default"), project);
final ListPopup popup = JBPopupFactory.getInstance().createListPopup(step);
final JList mainList = ((ListPopupImpl)popup).getList();
ListSelectionListener listener = event -> {
if (event.getValueIsAdjusting()) return;
Object item = ((JList)event.getSource()).getSelectedValue();
// if a sub-list is closed, fallback to the selected value from the main list
if (item == null) {
item = mainList.getSelectedValue();
}
if (item instanceof AttachToProcessItem) {
String debuggerName = ((AttachToProcessItem)item).getSelectedDebugger().getDebuggerDisplayName();
debuggerName = StringUtil.shortenTextWithEllipsis(debuggerName, 50, 0);
popup.setCaption(XDebuggerBundle.message("xdebugger.attach.popup.title", debuggerName));
}
if (item instanceof AttachHostItem) {
AttachHostItem hostItem = (AttachHostItem)item;
String attachHostName = hostItem.getText(project);
attachHostName = StringUtil.shortenTextWithEllipsis(attachHostName, 50, 0);
popup.setCaption(XDebuggerBundle.message("xdebugger.attach.host.popup.title", attachHostName));
}
};
popup.addListSelectionListener(listener);
// force first valueChanged event
listener.valueChanged(new ListSelectionEvent(mainList, mainList.getMinSelectionIndex(), mainList.getMaxSelectionIndex(), false));
popup.showCenteredInCurrentWindow(project);
}, project.getDisposed());
}
}.queue();
}
@NotNull
protected List<? extends AttachItem> getTopLevelItems(@NotNull ProgressIndicator indicator, @NotNull Project project) {
List<AttachItem> attachHostItems = collectAttachHostsItems(project, indicator);
// If any of hosts available, fold local PIDs into "Local Host" subgroup
if (!attachHostItems.isEmpty()) {
AttachItem localHostGroupItem = new AttachHostItem(
LocalAttachHostPresentationGroup.INSTANCE, false, LocalAttachHost.INSTANCE, project, new UserDataHolderBase());
attachHostItems.add(localHostGroupItem);
doUpdateFirstInGroup(attachHostItems);
return attachHostItems;
}
return collectAttachProcessItems(project, LocalAttachHost.INSTANCE, indicator);
}
private static void doUpdateFirstInGroup(@NotNull List<? extends AttachItem> items) {
if(items.isEmpty()) {
return;
}
items.get(0).makeFirstInGroup();
for(int i = 1; i < items.size(); i++) {
if(items.get(i).getGroup() != items.get(i - 1).getGroup()) {
items.get(i).makeFirstInGroup();
}
}
}
@NotNull
public List<AttachItem> collectAttachHostsItems(@NotNull final Project project,
@NotNull ProgressIndicator indicator) {
List<AttachItem> currentItems = ContainerUtil.newArrayList();
UserDataHolderBase dataHolder = new UserDataHolderBase();
for (XAttachHostProvider hostProvider : XAttachHostProvider.EP.getExtensionList()) {
indicator.checkCanceled();
//noinspection unchecked
Set<XAttachHost> hosts = ContainerUtil.newHashSet(hostProvider.getAvailableHosts(project));
for (XAttachHost host : hosts) {
//noinspection unchecked
currentItems.add(new AttachHostItem(hostProvider.getPresentationGroup(), false, host, project, dataHolder));
}
}
//noinspection unchecked
Collections.sort(currentItems);
doUpdateFirstInGroup(currentItems);
return currentItems;
}
@NotNull
private static List<AttachToProcessItem> getRecentItems(@NotNull List<? extends AttachToProcessItem> currentItems,
@NotNull XAttachHost host,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
final List<AttachToProcessItem> result = ContainerUtil.newArrayList();
final List<RecentItem> recentItems = getRecentItems(host, project);
for (int i = recentItems.size() - 1; i >= 0; i--) {
RecentItem recentItem = recentItems.get(i);
for (AttachToProcessItem currentItem : currentItems) {
boolean isSuitableItem = recentItem.getGroup().equals(currentItem.getGroup()) &&
recentItem.getProcessInfo().getCommandLine()
.equals(currentItem.getProcessInfo().getCommandLine());
if (!isSuitableItem) continue;
List<XAttachDebugger> debuggers = currentItem.getDebuggers();
int selectedDebugger = -1;
for (int j = 0; j < debuggers.size(); j++) {
XAttachDebugger debugger = debuggers.get(j);
if (debugger.getDebuggerDisplayName().equals(recentItem.getDebuggerName())) {
selectedDebugger = j;
break;
}
}
if (selectedDebugger == -1) continue;
result.add(AttachToProcessItem.createRecentAttachItem(currentItem, result.isEmpty(), debuggers, selectedDebugger, project,
dataHolder));
}
}
return result;
}
@NotNull
private static List<ProcessInfo> getProcessInfos(@NotNull XAttachHost host) {
try {
return host.getProcessList();
}
catch (ExecutionException e) {
Notifications.Bus.notify(new Notification("Attach to Process action",
XDebuggerBundle.message("xdebugger.attach.action.items.error.title"),
XDebuggerBundle.message("xdebugger.attach.action.items.error.message"),
NotificationType.WARNING));
LOG.warn("Error while getting attach items", e);
return Collections.emptyList();
}
}
@NotNull
private static List<XAttachDebuggerProvider> getProvidersApplicableForHost(@NotNull XAttachHost host) {
return ContainerUtil.filter(XAttachDebuggerProvider.getAttachDebuggerProviders(), provider -> provider.isAttachHostApplicable(host));
}
@NotNull
public static List<AttachToProcessItem> collectAttachProcessItems(@NotNull final Project project,
@NotNull XAttachHost host,
@NotNull ProgressIndicator indicator) {
return doCollectAttachProcessItems(project, host, getProcessInfos(host), indicator, getProvidersApplicableForHost(host));
}
@NotNull
static List<AttachToProcessItem> doCollectAttachProcessItems(@NotNull final Project project,
@NotNull XAttachHost host,
@NotNull List<? extends ProcessInfo> processInfos,
@NotNull ProgressIndicator indicator,
@NotNull List<? extends XAttachDebuggerProvider> providers) {
UserDataHolderBase dataHolder = new UserDataHolderBase();
List<AttachToProcessItem> currentItems = ContainerUtil.newArrayList();
for (ProcessInfo process : processInfos) {
MultiMap<XAttachPresentationGroup<ProcessInfo>, XAttachDebugger> groupsWithDebuggers = new MultiMap<>();
for (XAttachDebuggerProvider provider : providers) {
indicator.checkCanceled();
groupsWithDebuggers.putValues(provider.getPresentationGroup(),
provider.getAvailableDebuggers(project, host, process, dataHolder));
}
for (XAttachPresentationGroup<ProcessInfo> group : groupsWithDebuggers.keySet()) {
Collection<XAttachDebugger> debuggers = groupsWithDebuggers.get(group);
if (!debuggers.isEmpty()) {
currentItems.add(new AttachToProcessItem(group, false, host, process, new ArrayList<>(debuggers), project, dataHolder));
}
}
}
Collections.sort(currentItems);
doUpdateFirstInGroup(currentItems);
List<AttachToProcessItem> result = getRecentItems(currentItems, host, project, dataHolder);
result.addAll(currentItems);
return result;
}
public static void addToRecent(@NotNull Project project, @NotNull AttachToProcessItem item) {
Map<XAttachHost, LinkedHashSet<RecentItem>> recentItems = project.getUserData(RECENT_ITEMS_KEY);
if (recentItems == null) {
project.putUserData(RECENT_ITEMS_KEY, recentItems = ContainerUtil.newHashMap());
}
XAttachHost host = item.getHost();
LinkedHashSet<RecentItem> hostRecentItems = recentItems.get(host);
if(hostRecentItems == null) {
recentItems.put(host, ContainerUtil.newLinkedHashSet());
hostRecentItems = recentItems.get(host);
}
final RecentItem newRecentItem = new RecentItem(host, item);
hostRecentItems.remove(newRecentItem);
hostRecentItems.add(newRecentItem);
while (hostRecentItems.size() > 4) {
hostRecentItems.remove(hostRecentItems.iterator().next());
}
}
@NotNull
public static List<RecentItem> getRecentItems(@NotNull XAttachHost host,
@NotNull Project project) {
Map<XAttachHost, LinkedHashSet<RecentItem>> recentItems = project.getUserData(RECENT_ITEMS_KEY);
return recentItems == null || !recentItems.containsKey(host)
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(recentItems.get(host)));
}
public static class RecentItem {
@NotNull private final XAttachHost myHost;
@NotNull private final ProcessInfo myProcessInfo;
@NotNull private final XAttachPresentationGroup myGroup;
@NotNull private final String myDebuggerName;
public RecentItem(@NotNull XAttachHost host,
@NotNull AttachToProcessItem item) {
this(host, item.getProcessInfo(), item.getGroup(), item.getSelectedDebugger().getDebuggerDisplayName());
}
private RecentItem(@NotNull XAttachHost host,
@NotNull ProcessInfo info,
@NotNull XAttachPresentationGroup group,
@NotNull String debuggerName) {
myHost = host;
myProcessInfo = info;
myGroup = group;
myDebuggerName = debuggerName;
}
@TestOnly
public static RecentItem createRecentItem(@NotNull XAttachHost host, @NotNull ProcessInfo info, @NotNull XAttachPresentationGroup group, @NotNull String debuggerName) {
return new RecentItem(host, info, group, debuggerName);
}
@NotNull
public XAttachHost getHost() {
return myHost;
}
@NotNull
public ProcessInfo getProcessInfo() {
return myProcessInfo;
}
@NotNull
public XAttachPresentationGroup getGroup() {
return myGroup;
}
@NotNull
public String getDebuggerName() {
return myDebuggerName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecentItem item = (RecentItem)o;
return Objects.equals(myProcessInfo.getCommandLine(), item.myProcessInfo.getCommandLine());
}
@Override
public int hashCode() {
return Objects.hash(myProcessInfo.getCommandLine());
}
}
public static abstract class AttachItem<T> implements Comparable<AttachItem<T>> {
@NotNull
XAttachPresentationGroup<T> myGroup;
boolean myIsFirstInGroup;
@NotNull
String myGroupName;
@NotNull
Project myProject;
@NotNull
UserDataHolder myDataHolder;
@NotNull
T myInfo;
public AttachItem(@NotNull XAttachPresentationGroup<T> group,
boolean isFirstInGroup,
@NotNull String groupName,
@NotNull T info,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
myGroup = group;
myIsFirstInGroup = isFirstInGroup;
myInfo = info;
myProject = project;
myDataHolder = dataHolder;
myGroupName = groupName;
}
void makeFirstInGroup() {
myIsFirstInGroup = true;
}
@NotNull
XAttachPresentationGroup<T> getGroup() {
return myGroup;
}
@Nullable
String getSeparatorTitle() {
return myIsFirstInGroup ? myGroupName : null;
}
@Nullable
protected Icon getIcon(@NotNull Project project) {
return myGroup.getItemIcon(project, myInfo, myDataHolder);
}
protected abstract boolean hasSubStep();
protected abstract String getText(@NotNull Project project);
@Nullable
protected abstract String getTooltipText(@NotNull Project project);
protected abstract List<AttachToProcessItem> getSubItems();
@Override
public int compareTo(AttachItem<T> compareItem) {
int groupDifference = myGroup.getOrder() - compareItem.getGroup().getOrder();
if(groupDifference != 0) {
return groupDifference;
}
return myGroup.compare(myInfo, compareItem.myInfo);
}
}
private static class AttachHostItem extends AttachItem<XAttachHost> {
AttachHostItem(@NotNull XAttachPresentationGroup<XAttachHost> group,
boolean isFirstInGroup,
@NotNull XAttachHost host,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
super(group, isFirstInGroup, group.getGroupName(), host, project, dataHolder);
}
@Override
public boolean hasSubStep() {
return true;
}
@NotNull
@Override
public String getText(@NotNull Project project) {
return myGroup.getItemDisplayText(project, myInfo, myDataHolder);
}
@Override
@Nullable
public String getTooltipText(@NotNull Project project) {
return myGroup.getItemDescription(project, myInfo, myDataHolder);
}
@Override
public List<AttachToProcessItem> getSubItems() {
return collectAttachProcessItems(myProject, myInfo, new EmptyProgressIndicator());
}
}
public static class AttachToProcessItem extends AttachItem<ProcessInfo> {
@NotNull private final List<XAttachDebugger> myDebuggers;
private final int mySelectedDebugger;
@NotNull private final List<AttachToProcessItem> mySubItems;
@NotNull private final XAttachHost myHost;
public AttachToProcessItem(@NotNull XAttachPresentationGroup<ProcessInfo> group,
boolean isFirstInGroup,
@NotNull XAttachHost host,
@NotNull ProcessInfo info,
@NotNull List<XAttachDebugger> debuggers,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
this(group, isFirstInGroup, group.getGroupName(), host, info, debuggers, 0, project, dataHolder);
}
public AttachToProcessItem(@NotNull XAttachPresentationGroup<ProcessInfo> group,
boolean isFirstInGroup,
@NotNull String groupName,
@NotNull XAttachHost host,
@NotNull ProcessInfo info,
@NotNull List<XAttachDebugger> debuggers,
int selectedDebugger,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
super(group, isFirstInGroup, groupName, info, project, dataHolder);
assert !debuggers.isEmpty() : "debugger list should not be empty";
assert selectedDebugger >= 0 && selectedDebugger < debuggers.size() : "wrong selected debugger index";
myDebuggers = debuggers;
mySelectedDebugger = selectedDebugger;
myHost = host;
if (debuggers.size() > 1) {
mySubItems = ContainerUtil
.map(debuggers,
debugger -> new AttachToProcessItem(myGroup, false, myHost, myInfo, Collections.singletonList(debugger), myProject, dataHolder));
}
else {
mySubItems = Collections.emptyList();
}
}
static AttachToProcessItem createRecentAttachItem(AttachToProcessItem item,
boolean isFirstInGroup,
List<XAttachDebugger> debuggers,
int selectedDebugger,
Project project, UserDataHolder dataHolder) {
return new AttachToProcessItem(item.getGroup(), isFirstInGroup, XDebuggerBundle.message("xdebugger.attach.toLocal.popup.recent"),
item.getHost(), item.getProcessInfo(), debuggers, selectedDebugger, project, dataHolder);
}
@NotNull
public ProcessInfo getProcessInfo() {
return myInfo;
}
@NotNull
public XAttachHost getHost() {
return myHost;
}
@Override
public boolean hasSubStep() {
return !mySubItems.isEmpty();
}
@Override
@Nullable
public String getTooltipText(@NotNull Project project) {
return myGroup.getItemDescription(project, myInfo, myDataHolder);
}
@Override
@NotNull
public String getText(@NotNull Project project) {
String shortenedText = StringUtil.shortenTextWithEllipsis(myGroup.getItemDisplayText(project, myInfo, myDataHolder), 200, 0);
int pid = myInfo.getPid();
return (pid == -1 ? "" : pid + " ") + shortenedText;
}
@NotNull
public List<XAttachDebugger> getDebuggers() {
return myDebuggers;
}
@Override
@NotNull
public List<AttachToProcessItem> getSubItems() {
return mySubItems;
}
@NotNull
public XAttachDebugger getSelectedDebugger() {
return myDebuggers.get(mySelectedDebugger);
}
public void startDebugSession(@NotNull Project project) {
XAttachDebugger debugger = getSelectedDebugger();
try {
debugger.attachDebugSession(project, myHost, myInfo);
}
catch (ExecutionException e) {
ExecutionUtil.handleExecutionError(project, ToolWindowId.DEBUG, "pid " + myInfo.getPid(), e);
}
}
}
private static class MyBasePopupStep<T extends AttachItem> extends BaseListPopupStep<T> {
@NotNull final Project myProject;
MyBasePopupStep(@NotNull Project project,
@Nullable String title,
List<T> values) {
super(title, values);
myProject = project;
}
@Override
public boolean isSpeedSearchEnabled() {
return true;
}
@Override
public boolean isAutoSelectionEnabled() {
return false;
}
@Override
public boolean hasSubstep(AttachItem selectedValue) {
return !selectedValue.getSubItems().isEmpty();
}
@Override
public PopupStep onChosen(T selectedValue, boolean finalChoice) {
return null;
}
}
public static class AttachListStep extends MyBasePopupStep<AttachItem> implements ListPopupStepEx<AttachItem> {
public AttachListStep(@NotNull List<AttachItem> items, @Nullable String title, @NotNull Project project) {
super(project, title, items);
}
@Nullable
@Override
public ListSeparator getSeparatorAbove(AttachItem value) {
String separatorTitle = value.getSeparatorTitle();
return separatorTitle == null ? null : new ListSeparator(separatorTitle);
}
@Override
public Icon getIconFor(AttachItem value) {
return value.getIcon(myProject);
}
@NotNull
@Override
public String getTextFor(AttachItem value) {
return value.getText(myProject);
}
@Override
public boolean hasSubstep(AttachItem selectedValue) {
return selectedValue.hasSubStep();
}
@Nullable
@Override
public String getTooltipTextFor(AttachItem value) {
return value.getTooltipText(myProject);
}
@Override
public void setEmptyText(@NotNull StatusText emptyText) {
emptyText.setText(XDebuggerBundle.message("xdebugger.attach.popup.emptyText"));
}
@Override
public PopupStep onChosen(AttachItem selectedValue, boolean finalChoice) {
if (selectedValue instanceof AttachToProcessItem) {
AttachToProcessItem attachToProcessItem = (AttachToProcessItem)selectedValue;
if (finalChoice) {
addToRecent(myProject, attachToProcessItem);
return doFinalStep(() -> attachToProcessItem.startDebugSession(myProject));
}
else {
return new DebuggerListStep(attachToProcessItem.getSubItems(), attachToProcessItem.mySelectedDebugger);
}
}
if (selectedValue instanceof AttachHostItem) {
AttachHostItem attachHostItem = (AttachHostItem)selectedValue;
return new AsyncPopupStep() {
@Override
public PopupStep call() {
List<AttachItem> attachItems = new ArrayList<>(attachHostItem.getSubItems());
return new AttachListStep(attachItems, null, myProject);
}
};
}
return null;
}
@Override
public PopupStep onChosen(AttachItem selectedValue,
boolean finalChoice,
@MagicConstant(flagsFromClass = InputEvent.class) int eventModifiers) {
return onChosen(selectedValue, finalChoice);
}
private class DebuggerListStep extends MyBasePopupStep<AttachToProcessItem> {
DebuggerListStep(List<AttachToProcessItem> items, int selectedItem) {
super(AttachListStep.this.myProject, XDebuggerBundle.message("xdebugger.attach.popup.selectDebugger.title"), items);
setDefaultOptionIndex(selectedItem);
}
@NotNull
@Override
public String getTextFor(AttachToProcessItem value) {
return value.getSelectedDebugger().getDebuggerDisplayName();
}
@Override
public PopupStep onChosen(AttachToProcessItem selectedValue, boolean finalChoice) {
addToRecent(myProject, selectedValue);
return doFinalStep(() -> selectedValue.startDebugSession(myProject));
}
}
XDebuggerBundle.message("xdebugger.attach.action.description"),
AllIcons.Debugger.AttachToProcess,
() -> XAttachDebuggerProvider.getAttachDebuggerProviders(),
XDebuggerBundle.message("xdebugger.attach.popup.selectDebugger.title")
);
}
}
@@ -0,0 +1,712 @@
// Copyright 2000-2019 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.
package com.intellij.xdebugger.impl.actions;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.process.ProcessInfo;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.*;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.ui.popup.async.AsyncPopupStep;
import com.intellij.ui.popup.list.ListPopupImpl;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.StatusText;
import com.intellij.xdebugger.XDebuggerBundle;
import com.intellij.xdebugger.attach.*;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.event.InputEvent;
import java.util.*;
import java.util.function.Supplier;
public abstract class AttachToProcessActionBase extends AnAction {
private static final Key<Map<XAttachHost, LinkedHashSet<RecentItem>>> RECENT_ITEMS_KEY =
Key.create("AttachToProcessAction.RECENT_ITEMS_KEY");
private static final Logger LOG = Logger.getInstance(AttachToProcessActionBase.class);
private final Supplier<? extends List<XAttachDebuggerProvider>> attachProvidersSupplier;
private final String attachActionsListTitle;
public AttachToProcessActionBase(@Nullable String text,
@Nullable String description,
@Nullable Icon icon, @NotNull Supplier<? extends List<XAttachDebuggerProvider>> attachProvidersSupplier,
@NotNull String attachActionsListTitle) {
super(text, description, icon);
this.attachProvidersSupplier = attachProvidersSupplier;
this.attachActionsListTitle = attachActionsListTitle;
}
@Override
public void update(@NotNull AnActionEvent e) {
super.update(e);
Project project = getEventProject(e);
int attachDebuggerProvidersNumber = attachProvidersSupplier.get().size();
boolean enabled = project != null && attachDebuggerProvidersNumber > 0;
e.getPresentation().setEnabledAndVisible(enabled);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
final Project project = getEventProject(e);
if (project == null) return;
new Task.Backgroundable(project,
XDebuggerBundle.message("xdebugger.attach.action.collectingItems"), true,
PerformInBackgroundOption.DEAF) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
List<AttachItem> allItems = getTopLevelItems(indicator, project);
ApplicationManager.getApplication().invokeLater(() -> {
AttachListStep step = new AttachListStep(allItems, XDebuggerBundle.message("xdebugger.attach.popup.title.default"), project);
final ListPopup popup = JBPopupFactory.getInstance().createListPopup(step);
final JList mainList = ((ListPopupImpl)popup).getList();
ListSelectionListener listener = event -> {
if (event.getValueIsAdjusting()) return;
Object item = ((JList)event.getSource()).getSelectedValue();
// if a sub-list is closed, fallback to the selected value from the main list
if (item == null) {
item = mainList.getSelectedValue();
}
if (item instanceof AttachToProcessItem) {
String debuggerName = ((AttachToProcessItem)item).getSelectedDebugger().getDebuggerDisplayName();
debuggerName = StringUtil.shortenTextWithEllipsis(debuggerName, 50, 0);
popup.setCaption(XDebuggerBundle.message("xdebugger.attach.popup.title", debuggerName));
}
if (item instanceof AttachHostItem) {
AttachHostItem hostItem = (AttachHostItem)item;
String attachHostName = hostItem.getText(project);
attachHostName = StringUtil.shortenTextWithEllipsis(attachHostName, 50, 0);
popup.setCaption(XDebuggerBundle.message("xdebugger.attach.host.popup.title", attachHostName));
}
};
popup.addListSelectionListener(listener);
// force first valueChanged event
listener.valueChanged(new ListSelectionEvent(mainList, mainList.getMinSelectionIndex(), mainList.getMaxSelectionIndex(), false));
popup.showCenteredInCurrentWindow(project);
}, project.getDisposed());
}
}.queue();
}
@NotNull
protected List<AttachItem> getTopLevelItems(@NotNull ProgressIndicator indicator, @NotNull Project project) {
List<AttachToProcessItem> localAttachToProcessItems = collectAttachProcessItems(
project, LocalAttachHost.INSTANCE, indicator
);
List<AttachItem> remoteAttachToProcessItems = collectAttachHostsItems(
project, indicator
);
return ContainerUtil.concat(remoteAttachToProcessItems, localAttachToProcessItems);
}
private static void doUpdateFirstInGroup(@NotNull List<? extends AttachItem> items) {
if(items.isEmpty()) {
return;
}
items.get(0).makeFirstInGroup();
for(int i = 1; i < items.size(); i++) {
if(items.get(i).getGroup() != items.get(i - 1).getGroup()) {
items.get(i).makeFirstInGroup();
}
}
}
@NotNull
public List<AttachItem> collectAttachHostsItems(@NotNull final Project project,
@NotNull ProgressIndicator indicator) {
List<AttachItem> currentItems = ContainerUtil.newArrayList();
UserDataHolderBase dataHolder = new UserDataHolderBase();
for (XAttachHostProvider hostProvider : XAttachHostProvider.EP.getExtensionList()) {
indicator.checkCanceled();
//noinspection unchecked
Set<XAttachHost> hosts = ContainerUtil.newHashSet(hostProvider.getAvailableHosts(project));
for (XAttachHost host : hosts) {
//noinspection unchecked
currentItems.add(new AttachHostItem(hostProvider.getPresentationGroup(), false, host, project, dataHolder));
}
}
//noinspection unchecked
Collections.sort(currentItems);
doUpdateFirstInGroup(currentItems);
return currentItems;
}
@NotNull
private static List<AttachToProcessItem> getRecentItems(@NotNull List<? extends AttachToProcessItem> currentItems,
@NotNull XAttachHost host,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
final List<AttachToProcessItem> result = ContainerUtil.newArrayList();
final List<RecentItem> recentItems = getRecentItems(host, project);
for (int i = recentItems.size() - 1; i >= 0; i--) {
RecentItem recentItem = recentItems.get(i);
for (AttachToProcessItem currentItem : currentItems) {
boolean isSuitableItem = recentItem.getGroup().equals(currentItem.getGroup()) &&
recentItem.getProcessInfo().getCommandLine()
.equals(currentItem.getProcessInfo().getCommandLine());
if (!isSuitableItem) continue;
List<XAttachDebugger> debuggers = currentItem.getDebuggers();
int selectedDebugger = -1;
for (int j = 0; j < debuggers.size(); j++) {
XAttachDebugger debugger = debuggers.get(j);
if (debugger.getDebuggerDisplayName().equals(recentItem.getDebuggerName())) {
selectedDebugger = j;
break;
}
}
if (selectedDebugger == -1) continue;
result.add(AttachToProcessItem.createRecentAttachItem(currentItem, result.isEmpty(), debuggers, selectedDebugger, project,
dataHolder));
}
}
return result;
}
@NotNull
private static List<ProcessInfo> getProcessInfos(@NotNull XAttachHost host) {
try {
return host.getProcessList();
}
catch (ExecutionException e) {
Notifications.Bus.notify(new Notification("Attach to Process action",
XDebuggerBundle.message("xdebugger.attach.action.items.error.title"),
XDebuggerBundle.message("xdebugger.attach.action.items.error.message"),
NotificationType.WARNING));
LOG.warn("Error while getting attach items", e);
return Collections.emptyList();
}
}
@NotNull
private List<XAttachDebuggerProvider> getProvidersApplicableForHost(@NotNull XAttachHost host) {
return ContainerUtil.filter(attachProvidersSupplier.get(), provider -> provider.isAttachHostApplicable(host));
}
@NotNull
public List<AttachToProcessItem> collectAttachProcessItems(@NotNull final Project project,
@NotNull XAttachHost host,
@NotNull ProgressIndicator indicator) {
return doCollectAttachProcessItems(project, host, getProcessInfos(host), indicator, getProvidersApplicableForHost(host));
}
@NotNull
static List<AttachToProcessItem> doCollectAttachProcessItems(@NotNull final Project project,
@NotNull XAttachHost host,
@NotNull List<? extends ProcessInfo> processInfos,
@NotNull ProgressIndicator indicator,
@NotNull List<? extends XAttachDebuggerProvider> providers) {
UserDataHolderBase dataHolder = new UserDataHolderBase();
List<AttachToProcessItem> currentItems = ContainerUtil.newArrayList();
for (ProcessInfo process : processInfos) {
MultiMap<XAttachPresentationGroup<ProcessInfo>, XAttachDebugger> groupsWithDebuggers = new MultiMap<>();
for (XAttachDebuggerProvider provider : providers) {
indicator.checkCanceled();
groupsWithDebuggers.putValues(provider.getPresentationGroup(),
provider.getAvailableDebuggers(project, host, process, dataHolder));
}
for (XAttachPresentationGroup<ProcessInfo> group : groupsWithDebuggers.keySet()) {
Collection<XAttachDebugger> debuggers = groupsWithDebuggers.get(group);
if (!debuggers.isEmpty()) {
currentItems.add(new AttachToProcessItem(group, false, host, process, new ArrayList<>(debuggers), project, dataHolder));
}
}
}
Collections.sort(currentItems);
doUpdateFirstInGroup(currentItems);
List<AttachToProcessItem> result = getRecentItems(currentItems, host, project, dataHolder);
result.addAll(currentItems);
return result;
}
public static void addToRecent(@NotNull Project project, @NotNull AttachToProcessItem item) {
Map<XAttachHost, LinkedHashSet<RecentItem>> recentItems = project.getUserData(RECENT_ITEMS_KEY);
if (recentItems == null) {
project.putUserData(RECENT_ITEMS_KEY, recentItems = ContainerUtil.newHashMap());
}
XAttachHost host = item.getHost();
LinkedHashSet<RecentItem> hostRecentItems = recentItems.get(host);
if(hostRecentItems == null) {
recentItems.put(host, ContainerUtil.newLinkedHashSet());
hostRecentItems = recentItems.get(host);
}
final RecentItem newRecentItem = new RecentItem(host, item);
hostRecentItems.remove(newRecentItem);
hostRecentItems.add(newRecentItem);
while (hostRecentItems.size() > 4) {
hostRecentItems.remove(hostRecentItems.iterator().next());
}
}
@NotNull
public static List<RecentItem> getRecentItems(@NotNull XAttachHost host,
@NotNull Project project) {
Map<XAttachHost, LinkedHashSet<RecentItem>> recentItems = project.getUserData(RECENT_ITEMS_KEY);
return recentItems == null || !recentItems.containsKey(host)
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(recentItems.get(host)));
}
public static class RecentItem {
@NotNull private final XAttachHost myHost;
@NotNull private final ProcessInfo myProcessInfo;
@NotNull private final XAttachPresentationGroup myGroup;
@NotNull private final String myDebuggerName;
public RecentItem(@NotNull XAttachHost host,
@NotNull AttachToProcessItem item) {
this(host, item.getProcessInfo(), item.getGroup(), item.getSelectedDebugger().getDebuggerDisplayName());
}
private RecentItem(@NotNull XAttachHost host,
@NotNull ProcessInfo info,
@NotNull XAttachPresentationGroup group,
@NotNull String debuggerName) {
myHost = host;
myProcessInfo = info;
myGroup = group;
myDebuggerName = debuggerName;
}
@TestOnly
public static RecentItem createRecentItem(@NotNull XAttachHost host, @NotNull ProcessInfo info, @NotNull XAttachPresentationGroup group, @NotNull String debuggerName) {
return new RecentItem(host, info, group, debuggerName);
}
@NotNull
public XAttachHost getHost() {
return myHost;
}
@NotNull
public ProcessInfo getProcessInfo() {
return myProcessInfo;
}
@NotNull
public XAttachPresentationGroup getGroup() {
return myGroup;
}
@NotNull
public String getDebuggerName() {
return myDebuggerName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecentItem item = (RecentItem)o;
return Objects.equals(myProcessInfo.getCommandLine(), item.myProcessInfo.getCommandLine());
}
@Override
public int hashCode() {
return Objects.hash(myProcessInfo.getCommandLine());
}
}
public static abstract class AttachItem<T> implements Comparable<AttachItem<T>> {
@NotNull
XAttachPresentationGroup<T> myGroup;
boolean myIsFirstInGroup;
@NotNull
String myGroupName;
@NotNull
Project myProject;
@NotNull
UserDataHolder myDataHolder;
@NotNull
T myInfo;
public AttachItem(@NotNull XAttachPresentationGroup<T> group,
boolean isFirstInGroup,
@NotNull String groupName,
@NotNull T info,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
myGroup = group;
myIsFirstInGroup = isFirstInGroup;
myInfo = info;
myProject = project;
myDataHolder = dataHolder;
myGroupName = groupName;
}
void makeFirstInGroup() {
myIsFirstInGroup = true;
}
@NotNull
XAttachPresentationGroup<T> getGroup() {
return myGroup;
}
@Nullable
String getSeparatorTitle() {
return myIsFirstInGroup ? myGroupName : null;
}
@Nullable
protected Icon getIcon(@NotNull Project project) {
return myGroup.getItemIcon(project, myInfo, myDataHolder);
}
protected abstract boolean hasSubStep();
protected abstract String getText(@NotNull Project project);
@Nullable
protected abstract String getTooltipText(@NotNull Project project);
protected abstract List<AttachToProcessItem> getSubItems();
@Override
public int compareTo(AttachItem<T> compareItem) {
int groupDifference = myGroup.getOrder() - compareItem.getGroup().getOrder();
if(groupDifference != 0) {
return groupDifference;
}
return myGroup.compare(myInfo, compareItem.myInfo);
}
}
private class AttachHostItem extends AttachItem<XAttachHost> {
AttachHostItem(@NotNull XAttachPresentationGroup<XAttachHost> group,
boolean isFirstInGroup,
@NotNull XAttachHost host,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
super(group, isFirstInGroup, group.getGroupName(), host, project, dataHolder);
}
@Override
public boolean hasSubStep() {
return true;
}
@NotNull
@Override
public String getText(@NotNull Project project) {
return myGroup.getItemDisplayText(project, myInfo, myDataHolder);
}
@Override
@Nullable
public String getTooltipText(@NotNull Project project) {
return myGroup.getItemDescription(project, myInfo, myDataHolder);
}
@Override
public List<AttachToProcessItem> getSubItems() {
return collectAttachProcessItems(myProject, myInfo, new EmptyProgressIndicator());
}
}
public static class AttachToProcessItem extends AttachItem<ProcessInfo> {
@NotNull private final List<XAttachDebugger> myDebuggers;
private final int mySelectedDebugger;
@NotNull private final List<AttachToProcessItem> mySubItems;
@NotNull private final XAttachHost myHost;
public AttachToProcessItem(@NotNull XAttachPresentationGroup<ProcessInfo> group,
boolean isFirstInGroup,
@NotNull XAttachHost host,
@NotNull ProcessInfo info,
@NotNull List<XAttachDebugger> debuggers,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
this(group, isFirstInGroup, group.getGroupName(), host, info, debuggers, 0, project, dataHolder);
}
public AttachToProcessItem(@NotNull XAttachPresentationGroup<ProcessInfo> group,
boolean isFirstInGroup,
@NotNull String groupName,
@NotNull XAttachHost host,
@NotNull ProcessInfo info,
@NotNull List<XAttachDebugger> debuggers,
int selectedDebugger,
@NotNull Project project,
@NotNull UserDataHolder dataHolder) {
super(group, isFirstInGroup, groupName, info, project, dataHolder);
assert !debuggers.isEmpty() : "debugger list should not be empty";
assert selectedDebugger >= 0 && selectedDebugger < debuggers.size() : "wrong selected debugger index";
myDebuggers = debuggers;
mySelectedDebugger = selectedDebugger;
myHost = host;
if (debuggers.size() > 1) {
mySubItems = ContainerUtil
.map(debuggers,
debugger -> new AttachToProcessItem(myGroup, false, myHost, myInfo, Collections.singletonList(debugger), myProject, dataHolder));
}
else {
mySubItems = Collections.emptyList();
}
}
static AttachToProcessItem createRecentAttachItem(AttachToProcessItem item,
boolean isFirstInGroup,
List<XAttachDebugger> debuggers,
int selectedDebugger,
Project project, UserDataHolder dataHolder) {
return new AttachToProcessItem(item.getGroup(), isFirstInGroup, XDebuggerBundle.message("xdebugger.attach.toLocal.popup.recent"),
item.getHost(), item.getProcessInfo(), debuggers, selectedDebugger, project, dataHolder);
}
@NotNull
public ProcessInfo getProcessInfo() {
return myInfo;
}
@NotNull
public XAttachHost getHost() {
return myHost;
}
@Override
public boolean hasSubStep() {
return !mySubItems.isEmpty();
}
@Override
@Nullable
public String getTooltipText(@NotNull Project project) {
return myGroup.getItemDescription(project, myInfo, myDataHolder);
}
@Override
@NotNull
public String getText(@NotNull Project project) {
String shortenedText = StringUtil.shortenTextWithEllipsis(myGroup.getItemDisplayText(project, myInfo, myDataHolder), 200, 0);
int pid = myInfo.getPid();
return (pid == -1 ? "" : pid + " ") + shortenedText;
}
@NotNull
public List<XAttachDebugger> getDebuggers() {
return myDebuggers;
}
@Override
@NotNull
public List<AttachToProcessItem> getSubItems() {
return mySubItems;
}
@NotNull
public XAttachDebugger getSelectedDebugger() {
return myDebuggers.get(mySelectedDebugger);
}
public void startDebugSession(@NotNull Project project) {
XAttachDebugger debugger = getSelectedDebugger();
try {
debugger.attachDebugSession(project, myHost, myInfo);
}
catch (ExecutionException e) {
ExecutionUtil.handleExecutionError(project, ToolWindowId.DEBUG, "pid " + myInfo.getPid(), e);
}
}
}
private static class MyBasePopupStep<T extends AttachItem> extends BaseListPopupStep<T> {
@NotNull final Project myProject;
MyBasePopupStep(@NotNull Project project,
@Nullable String title,
List<T> values) {
super(title, values);
myProject = project;
}
@Override
public boolean isSpeedSearchEnabled() {
return true;
}
@Override
public boolean isAutoSelectionEnabled() {
return false;
}
@Override
public boolean hasSubstep(AttachItem selectedValue) {
return !selectedValue.getSubItems().isEmpty();
}
@Override
public PopupStep onChosen(T selectedValue, boolean finalChoice) {
return null;
}
}
public class AttachListStep extends MyBasePopupStep<AttachItem> implements ListPopupStepEx<AttachItem> {
public AttachListStep(@NotNull List<AttachItem> items, @Nullable String title, @NotNull Project project) {
super(project, title, items);
}
@Nullable
@Override
public ListSeparator getSeparatorAbove(AttachItem value) {
String separatorTitle = value.getSeparatorTitle();
return separatorTitle == null ? null : new ListSeparator(separatorTitle);
}
@Override
public Icon getIconFor(AttachItem value) {
return value.getIcon(myProject);
}
@NotNull
@Override
public String getTextFor(AttachItem value) {
return value.getText(myProject);
}
@Override
public boolean hasSubstep(AttachItem selectedValue) {
return selectedValue.hasSubStep();
}
@Nullable
@Override
public String getTooltipTextFor(AttachItem value) {
return value.getTooltipText(myProject);
}
@Override
public void setEmptyText(@NotNull StatusText emptyText) {
emptyText.setText(XDebuggerBundle.message("xdebugger.attach.popup.emptyText"));
}
@Override
public PopupStep onChosen(AttachItem selectedValue, boolean finalChoice) {
if (selectedValue instanceof AttachToProcessItem) {
AttachToProcessItem attachToProcessItem = (AttachToProcessItem)selectedValue;
if (finalChoice) {
addToRecent(myProject, attachToProcessItem);
return doFinalStep(() -> attachToProcessItem.startDebugSession(myProject));
}
else {
return new ActionListStep(attachToProcessItem.getSubItems(), attachToProcessItem.mySelectedDebugger);
}
}
if (selectedValue instanceof AttachHostItem) {
AttachHostItem attachHostItem = (AttachHostItem)selectedValue;
return new AsyncPopupStep() {
@Override
public PopupStep call() {
List<AttachItem> attachItems = new ArrayList<>(attachHostItem.getSubItems());
return new AttachListStep(attachItems, null, myProject);
}
};
}
return null;
}
@Override
public PopupStep onChosen(AttachItem selectedValue,
boolean finalChoice,
@MagicConstant(flagsFromClass = InputEvent.class) int eventModifiers) {
return onChosen(selectedValue, finalChoice);
}
private class ActionListStep extends MyBasePopupStep<AttachToProcessItem> {
ActionListStep(List<AttachToProcessItem> items, int selectedItem) {
super(AttachListStep.this.myProject, AttachToProcessActionBase.this.attachActionsListTitle, items);
setDefaultOptionIndex(selectedItem);
}
@NotNull
@Override
public String getTextFor(AttachToProcessItem value) {
return value.getSelectedDebugger().getDebuggerDisplayName();
}
@Override
public PopupStep onChosen(AttachToProcessItem selectedValue, boolean finalChoice) {
addToRecent(myProject, selectedValue);
return doFinalStep(() -> selectedValue.startDebugSession(myProject));
}
}
}
}