Skip to content

Commit 3037c1b

Browse files
feat: add context menu actions [IDE-784] (#240)
* feat: add context menu actions * fix: errors in commands, display output * fix: errors in commands, display output * fix: allow iac ignores, add tests * fix: allow iac ignores, add tests
1 parent 232d12a commit 3037c1b

9 files changed

Lines changed: 410 additions & 54 deletions

File tree

plugin/src/main/java/io/snyk/eclipse/plugin/domain/ProductConstants.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import java.util.Map;
44

55
public interface ProductConstants {
6-
String SCAN_STATE_IN_PROGRESS="inProgress";
7-
String SCAN_STATE_SUCCESS="success";
8-
String SCAN_STATE_ERROR="error";
9-
6+
String SCAN_STATE_IN_PROGRESS = "inProgress";
7+
String SCAN_STATE_SUCCESS = "success";
8+
String SCAN_STATE_ERROR = "error";
9+
1010
String SCAN_PARAMS_OSS = "oss";
1111
String SCAN_PARAMS_CODE = "code";
1212
String SCAN_PARAMS_IAC = "iac";
@@ -24,10 +24,20 @@ public interface ProductConstants {
2424
String SEVERITY_HIGH = "high";
2525
String SEVERITY_MEDIUM = "medium";
2626
String SEVERITY_LOW = "low";
27-
27+
28+
String FILTERABLE_ISSUE_OPEN_SOURCE = "Open Source";
29+
String FILTERABLE_ISSUE_CODE_SECURITY = "Code Security";
30+
String FILTERABLE_ISSUE_CODE_QUALITY = "Code Quality";
31+
String FILTERABLE_ISSUE_INFRASTRUCTURE_AS_CODE = "Infrastructure As Code";
32+
33+
Map<String, String> FILTERABLE_ISSUE_TYPE_TO_DISPLAY = Map.of(FILTERABLE_ISSUE_CODE_QUALITY, DISPLAYED_CODE_QUALITY,
34+
FILTERABLE_ISSUE_CODE_SECURITY, DISPLAYED_CODE_SECURITY, FILTERABLE_ISSUE_INFRASTRUCTURE_AS_CODE,
35+
DISPLAYED_IAC, FILTERABLE_ISSUE_OPEN_SOURCE, DISPLAYED_OSS);
36+
2837
Map<String, String> LSP_SOURCE_TO_SCAN_PARAMS = Map.of(DIAGNOSTIC_SOURCE_SNYK_CODE, SCAN_PARAMS_CODE,
2938
DIAGNOSTIC_SOURCE_SNYK_IAC, SCAN_PARAMS_IAC, DIAGNOSTIC_SOURCE_SNYK_OSS, SCAN_PARAMS_OSS);
3039

3140
// code cannot be mapped easily
32-
Map<String, String> SCAN_PARAMS_TO_DISPLAYED = Map.of(SCAN_PARAMS_OSS, DISPLAYED_OSS, SCAN_PARAMS_IAC, DISPLAYED_IAC);
41+
Map<String, String> SCAN_PARAMS_TO_DISPLAYED = Map.of(SCAN_PARAMS_OSS, DISPLAYED_OSS, SCAN_PARAMS_IAC,
42+
DISPLAYED_IAC);
3343
}

plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
package io.snyk.eclipse.plugin.views.snyktoolview;
22

3+
import java.nio.file.Path;
34
import java.nio.file.Paths;
5+
import java.util.Map;
6+
import java.util.Set;
47
import java.util.concurrent.CompletableFuture;
5-
8+
import java.util.concurrent.ExecutionException;
9+
import java.util.concurrent.TimeUnit;
10+
import java.util.concurrent.TimeoutException;
11+
12+
import org.eclipse.core.resources.IProject;
13+
import org.eclipse.core.runtime.IProgressMonitor;
14+
import org.eclipse.core.runtime.IStatus;
15+
import org.eclipse.core.runtime.Status;
16+
import org.eclipse.core.runtime.jobs.Job;
17+
import org.eclipse.jface.action.Action;
618
import org.eclipse.jface.action.IMenuManager;
719
import org.eclipse.jface.action.MenuManager;
820
import org.eclipse.jface.viewers.ISelectionChangedListener;
@@ -11,13 +23,16 @@
1123
import org.eclipse.jface.viewers.TreeNode;
1224
import org.eclipse.jface.viewers.TreeViewer;
1325
import org.eclipse.lsp4e.LSPEclipseUtils;
26+
import org.eclipse.lsp4j.MessageParams;
27+
import org.eclipse.lsp4j.MessageType;
1428
import org.eclipse.swt.SWT;
1529
import org.eclipse.swt.browser.Browser;
1630
import org.eclipse.swt.custom.SashForm;
1731
import org.eclipse.swt.layout.FillLayout;
1832
import org.eclipse.swt.layout.GridData;
1933
import org.eclipse.swt.layout.GridLayout;
2034
import org.eclipse.swt.widgets.Composite;
35+
import org.eclipse.swt.widgets.Control;
2136
import org.eclipse.swt.widgets.Display;
2237
import org.eclipse.swt.widgets.Menu;
2338
import org.eclipse.swt.widgets.Tree;
@@ -26,12 +41,16 @@
2641
import org.eclipse.ui.menus.CommandContributionItemParameter;
2742
import org.eclipse.ui.part.ViewPart;
2843

44+
import io.snyk.eclipse.plugin.domain.ProductConstants;
2945
import io.snyk.eclipse.plugin.properties.preferences.FolderConfigs;
3046
import io.snyk.eclipse.plugin.properties.preferences.Preferences;
3147
import io.snyk.eclipse.plugin.utils.ResourceUtils;
48+
import io.snyk.eclipse.plugin.utils.SnykLogger;
3249
import io.snyk.eclipse.plugin.views.snyktoolview.providers.TreeContentProvider;
3350
import io.snyk.eclipse.plugin.views.snyktoolview.providers.TreeLabelProvider;
51+
import io.snyk.languageserver.CommandHandler;
3452
import io.snyk.languageserver.protocolextension.SnykExtendedLanguageClient;
53+
import io.snyk.languageserver.protocolextension.messageObjects.scanResults.Issue;
3554

3655
/**
3756
* TODO This view will replace the old SnykView. Move the snyktoolview classes
@@ -49,6 +68,7 @@ public class SnykToolView extends ViewPart implements ISnykToolView {
4968
private Browser browser;
5069
private BrowserHandler browserHandler;
5170
private FolderConfigs folderConfigs = FolderConfigs.getInstance();
71+
private TreeNode selectedNode;
5272

5373
@Override
5474
public void createPartControl(Composite parent) {
@@ -71,7 +91,7 @@ public void createPartControl(Composite parent) {
7191
treeViewer.setInput(new RootNode());
7292
treeViewer.expandAll();
7393

74-
registerTreeContextMeny(parent);
94+
registerTreeContextMenu(treeViewer.getControl());
7595

7696
// Create Browser
7797
// SWT.EDGE will be ignored if OS not windows and will be set to SWT.NONE.
@@ -83,29 +103,29 @@ public void createPartControl(Composite parent) {
83103

84104
// Add selection listener to the tree
85105
treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
106+
86107
@SuppressWarnings("restriction")
87108
@Override
88109
public void selectionChanged(SelectionChangedEvent event) {
89110
Display.getDefault().asyncExec(() -> {
90111
IStructuredSelection selection = (IStructuredSelection) event.getSelection();
91112
if (selection.isEmpty())
92113
return;
93-
TreeNode node = (TreeNode) selection.getFirstElement();
94-
browserHandler.updateBrowserContent(node);
95-
if (node instanceof IssueTreeNode) {
96-
IssueTreeNode issueTreeNode = (IssueTreeNode) node;
114+
selectedNode = (TreeNode) selection.getFirstElement();
115+
browserHandler.updateBrowserContent(selectedNode);
116+
if (selectedNode instanceof IssueTreeNode) {
117+
IssueTreeNode issueTreeNode = (IssueTreeNode) selectedNode;
97118
FileTreeNode fileNode = (FileTreeNode) issueTreeNode.getParent();
98119
LSPEclipseUtils.open(fileNode.getPath().toUri().toASCIIString(),
99120
issueTreeNode.getIssue().getLSP4JRange());
100121
}
101122
boolean deltaEnabled = Preferences.isDeltaEnabled();
102-
if (node instanceof ContentRootNode && deltaEnabled) {
103-
ContentRootNode contentNode = (ContentRootNode) node;
123+
if (selectedNode instanceof ContentRootNode && deltaEnabled) {
124+
ContentRootNode contentNode = (ContentRootNode) selectedNode;
104125
String[] localBranches = folderConfigs.getLocalBranches(contentNode.getPath())
105126
.toArray(new String[0]);
106127

107-
new BaseBranchDialog().open(Display.getDefault(), contentNode.getPath(),
108-
localBranches);
128+
new BaseBranchDialog().open(Display.getDefault(), contentNode.getPath(), localBranches);
109129
}
110130
});
111131
}
@@ -115,11 +135,97 @@ public void selectionChanged(SelectionChangedEvent event) {
115135
this.enableDelta();
116136
}
117137

118-
private void registerTreeContextMeny(Composite parent) {
138+
private void registerTreeContextMenu(Control control) {
119139
MenuManager menuMgr = new MenuManager("treemenu");
120-
Menu menu = menuMgr.createContextMenu(parent);
140+
Menu menu = menuMgr.createContextMenu(control);
121141
getSite().registerContextMenu(menuMgr, null);
122-
parent.setMenu(menu);
142+
control.setMenu(menu);
143+
144+
Action ignoreAction = getIgnoreAction();
145+
Action monitorAction = getMonitorAction();
146+
147+
menuMgr.add(ignoreAction);
148+
menuMgr.add(monitorAction);
149+
}
150+
151+
private Action getIgnoreAction() {
152+
return new Action("Ignore issue in .snyk") {
153+
@Override
154+
public void run() {
155+
CompletableFuture.runAsync(() -> {
156+
IssueTreeNode itn = (IssueTreeNode) selectedNode;
157+
Issue issue = itn.getIssue();
158+
new Job("Ignoring issue " + issue.getDisplayTitle()) {
159+
160+
@Override
161+
protected IStatus run(IProgressMonitor monitor) {
162+
try {
163+
var result = CommandHandler.getInstance().ignoreIssue(issue).get(10, TimeUnit.SECONDS);
164+
outputCommandResult(result);
165+
itn.setText("[ IGNORED ] " + itn.getText());
166+
Display.getDefault().asyncExec(() -> {
167+
treeViewer.refresh(itn, true);
168+
});
169+
} catch (InterruptedException e) {
170+
Thread.currentThread().interrupt();
171+
SnykLogger.logError(e);
172+
return Status.CANCEL_STATUS;
173+
} catch (ExecutionException | TimeoutException e) {
174+
SnykLogger.logError(e);
175+
return Status.CANCEL_STATUS;
176+
}
177+
return Status.OK_STATUS;
178+
}
179+
}.schedule();
180+
});
181+
}
182+
183+
public boolean isEnabled() {
184+
return selectedNode instanceof IssueTreeNode && CommandHandler.getInstance().canBeIgnored(getProduct());
185+
}
186+
187+
protected String getProduct() {
188+
var pn = (ProductTreeNode) selectedNode.getParent().getParent();
189+
String product = pn.getProduct();
190+
return product;
191+
}
192+
};
193+
}
194+
195+
private Action getMonitorAction() {
196+
return new Action("Monitor project") {
197+
@Override
198+
public void run() {
199+
CompletableFuture.runAsync(() -> {
200+
IssueTreeNode itn = (IssueTreeNode) selectedNode;
201+
Issue issue = itn.getIssue();
202+
IProject project = ResourceUtils.getProjectByPath(Paths.get(issue.filePath()));
203+
new Job("Monitoring project " + project.getName()) {
204+
205+
@Override
206+
protected IStatus run(IProgressMonitor monitor) {
207+
try {
208+
Path workingDir = ResourceUtils.getFullPath(project);
209+
var result = CommandHandler.getInstance().monitorProject(workingDir).get();
210+
outputCommandResult(result);
211+
} catch (InterruptedException e) {
212+
Thread.currentThread().interrupt();
213+
SnykLogger.logError(e);
214+
return Status.CANCEL_STATUS;
215+
} catch (ExecutionException e) {
216+
SnykLogger.logError(e);
217+
return Status.CANCEL_STATUS;
218+
}
219+
return Status.OK_STATUS;
220+
}
221+
}.schedule();
222+
});
223+
}
224+
225+
public boolean isEnabled() {
226+
return true;
227+
}
228+
};
123229
}
124230

125231
@Override
@@ -318,4 +424,20 @@ private void addCommandIfNotPresent(IMenuManager menu, String commandId) {
318424
}
319425
}
320426

427+
@SuppressWarnings("restriction")
428+
protected void outputCommandResult(Object result) {
429+
if (result != null && result instanceof Map) {
430+
@SuppressWarnings("unchecked")
431+
Map<String, Object> resultMap = (Map<String, Object>) result;
432+
String stdOut = resultMap.get("stdOut").toString();
433+
boolean exitCode = (Double) resultMap.get("exitCode") == 0;
434+
if (exitCode) {
435+
MessageParams messageParams = new MessageParams(MessageType.Info, stdOut);
436+
SnykExtendedLanguageClient.getInstance().showMessage(messageParams);
437+
} else {
438+
SnykLogger.logError(new RuntimeException(stdOut));
439+
}
440+
}
441+
}
442+
321443
}

plugin/src/main/java/io/snyk/eclipse/plugin/wizards/SnykWizard.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.eclipse.ui.IWorkbench;
1414

1515
import io.snyk.eclipse.plugin.SnykStartup;
16+
import io.snyk.eclipse.plugin.utils.ResourceUtils;
1617
import io.snyk.languageserver.LsConfigurationUpdater;
1718
import io.snyk.languageserver.protocolextension.SnykExtendedLanguageClient;
1819

@@ -83,8 +84,8 @@ protected IStatus run(IProgressMonitor monitor) {
8384
SnykExtendedLanguageClient.getInstance().triggerAuthentication();
8485
monitor.worked(20);
8586
monitor.subTask("trusting workspace folders...");
86-
var projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
87-
if (projects != null && projects.length > 0 && Arrays.stream(projects).filter(p -> p.isAccessible()).count() > 0) {
87+
var projects = ResourceUtils.getAccessibleTopLevelProjects();
88+
if (projects != null && projects.size()>0) {
8889
SnykExtendedLanguageClient.getInstance().trustWorkspaceFolders();
8990
}
9091
monitor.worked(20);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.snyk.languageserver;
2+
3+
import java.nio.file.Path;
4+
import java.nio.file.Paths;
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
import java.util.Set;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.stream.Collectors;
10+
11+
import org.eclipse.core.resources.IProject;
12+
import org.eclipse.lsp4j.ExecuteCommandParams;
13+
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
14+
import org.eclipse.lsp4j.services.LanguageServer;
15+
16+
import io.snyk.eclipse.plugin.domain.ProductConstants;
17+
import io.snyk.eclipse.plugin.utils.ResourceUtils;
18+
import io.snyk.eclipse.plugin.utils.SnykLogger;
19+
import io.snyk.languageserver.protocolextension.SnykExtendedLanguageClient;
20+
import io.snyk.languageserver.protocolextension.messageObjects.scanResults.Issue;
21+
22+
public class CommandHandler {
23+
private LanguageServer ls;
24+
private static CommandHandler instance;
25+
private static final Set<String> allowedProducts = Set.of(ProductConstants.DISPLAYED_IAC,
26+
ProductConstants.DISPLAYED_OSS);
27+
28+
public CommandHandler(LanguageServer ls) {
29+
this.ls = ls;
30+
}
31+
32+
public static synchronized CommandHandler getInstance() {
33+
if (instance == null) {
34+
SnykExtendedLanguageClient lc = SnykExtendedLanguageClient.getInstance();
35+
lc.ensureLanguageServerRunning();
36+
instance = new CommandHandler(lc.getConnectedLanguageServer());
37+
}
38+
return instance;
39+
}
40+
41+
public CompletableFuture<Object> executeCommand(@NonNull String command, List<Object> args) {
42+
ExecuteCommandParams params = new ExecuteCommandParams(command, args);
43+
try {
44+
return ls.getWorkspaceService().executeCommand(params);
45+
} catch (Exception e) {
46+
SnykLogger.logError(e);
47+
}
48+
return CompletableFuture.completedFuture(null);
49+
}
50+
51+
public CompletableFuture<Object> ignoreIssue(Issue issue) {
52+
String displayProduct = ProductConstants.FILTERABLE_ISSUE_TYPE_TO_DISPLAY.get(issue.filterableIssueType());
53+
if (!canBeIgnored(displayProduct))
54+
return CompletableFuture.completedFuture(null);
55+
56+
Path workingDir = getWorkingDirectory(issue);
57+
String pathArg = null;
58+
String idArg = "--id=" + issue.additionalData().ruleId();
59+
60+
List<Object> args = new ArrayList<>();
61+
args.add(workingDir.toString());
62+
args.add("ignore");
63+
64+
if (issue.filterableIssueType().equals(ProductConstants.FILTERABLE_ISSUE_INFRASTRUCTURE_AS_CODE)) {
65+
Path relativePath = workingDir.relativize(Paths.get(issue.filePath()));
66+
var separator = " > ";
67+
pathArg = "--path=" + relativePath.toString() + separator
68+
+ issue.additionalData().path().stream().collect(Collectors.joining(separator));
69+
args.add(pathArg);
70+
idArg = "--id=" + issue.additionalData().publicId();
71+
}
72+
73+
args.add(idArg);
74+
75+
return this.executeCommand(LsConstants.COMMAND_SNYK_CLI, args);
76+
}
77+
78+
protected Path getWorkingDirectory(Issue issue) {
79+
IProject project = ResourceUtils.getProjectByPath(Paths.get(issue.filePath()));
80+
if (project == null)
81+
return Path.of(".");
82+
var workingDir = ResourceUtils.getFullPath(project);
83+
return workingDir;
84+
}
85+
86+
public CompletableFuture<Object> monitorProject(Path path) {
87+
List<Object> args = List.of(path.toString(), "monitor", "--all-projects");
88+
return this.executeCommand(LsConstants.COMMAND_SNYK_CLI, args);
89+
}
90+
91+
/**
92+
* checks if a product can be ignored
93+
*
94+
* @param product the DISPLAY product from ProductConstants
95+
* @return
96+
*/
97+
public boolean canBeIgnored(String product) {
98+
if (product == null)
99+
return false;
100+
return allowedProducts.contains(product);
101+
}
102+
103+
}

0 commit comments

Comments
 (0)