From c948e304412673fefcfc285262844b8d9d6c4d63 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 20 Nov 2024 18:45:56 +0100 Subject: [PATCH 1/2] wip: html provider --- .../eclipse/plugin/html/BaseHtmlProvider.java | 121 ++++++++++++++ .../eclipse/plugin/html/CodeHtmlProvider.java | 157 ++++++++++++++++++ .../plugin/html/HtmlProviderFactory.java | 20 +++ .../eclipse/plugin/html/IacHtmlProvider.java | 15 ++ .../eclipse/plugin/html/OssHtmlProvider.java | 15 ++ .../views/snyktoolview/SnykToolView.java | 75 ++++++++- 6 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java create mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java create mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java create mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java create mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java new file mode 100644 index 00000000..45765d2b --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java @@ -0,0 +1,121 @@ +package io.snyk.eclipse.plugin.html; + +import java.util.Random; + +import org.eclipse.jface.resource.ColorRegistry; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.themes.ITheme; +import org.eclipse.ui.themes.IThemeManager; + +public class BaseHtmlProvider { + public String getCss() { + return """ + html, body { + height: 100%; + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + } + + body { + background-color: var(--background-color); + color: var(--text-color); + font-weight: 400; + } + + section { + padding: 20px; + } + + .font-light { + font-weight: bold; + } + + a, + .link { + color: var(--link-color); + } + + .delimiter-top { + border-top: 1px solid var(--horizontal-border-color); + } + code { + background-color: var(--code-background-color); + padding: 1px 3px; + border-radius: 4px; + } + """; + } + + public String getJs() { + return ""; + } + + public String getInitScript() { + return ""; + } + + public String getNonce() { + String allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder nonceBuilder = new StringBuilder(32); + for (int i = 0; i < 32; i++) { + nonceBuilder.append(allowedChars.charAt(random.nextInt(allowedChars.length()))); + } + return nonceBuilder.toString(); + } + + public String replaceCssVariables(String html) { + // Build the CSS with the nonce + String nonce = getNonce(); + String css = ""; + html = html.replace("${ideStyle}", css); + html = html.replace("", css); + html = html.replace("var(--default-font)", " ui-sans-serif, \"SF Pro Text\", \"Segoe UI\", \"Ubuntu\", Tahoma, Geneva, Verdana, sans-serif;"); + + + // Replace CSS variables with actual color values + html = html.replace("var(--text-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_TEXT_COLOR", "#000000")); + html = html.replace("var(--background-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_BG_START", "#FFFFFF")); + html = html.replace("var(--border-color)", getColorAsHex( "org.eclipse.ui.workbench.ACTIVE_TAB_BORDER_COLOR", "#CCCCCC")); + html = html.replace("var(--link-color)", getColorAsHex("org.eclipse.ui.workbench.HYPERLINK_COLOR", "#0000FF")); + html = html.replace("var(--horizontal-border-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_HIGHLIGHT_BORDER_COLOR", "#CCCCCC")); + html = html.replace("var(--code-background-color)", getColorAsHex("org.eclipse.ui.workbench.CODE_BACKGROUND_COLOR", "#F0F0F0")); + + // Update the HTML head + String ideHeaders = """ + + + + """; + html = html.replace("", ideHeaders); + html = html.replace("${headerEnd}", ""); + + // Replace nonce placeholders + html = html.replace("${nonce}", nonce); + html = html.replace("ideNonce", nonce); + html = html.replace("${ideScript}", ""); + + return html; + } + + public String getColorAsHex(String colorKey, String defaultColor) { + ColorRegistry colorRegistry = getColorRegistry(); + Color color = colorRegistry.get(colorKey); + if (color == null) { + return defaultColor; + } else { + RGB rgb = color.getRGB(); + return String.format("#%02x%02x%02x", rgb.red, rgb.green, rgb.blue); + } + } + + private ColorRegistry getColorRegistry() { + IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager(); + ITheme currentTheme = themeManager.getCurrentTheme(); + return currentTheme.getColorRegistry(); + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java new file mode 100644 index 00000000..a5351d8a --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java @@ -0,0 +1,157 @@ +package io.snyk.eclipse.plugin.html; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.themes.ITheme; +import org.eclipse.ui.themes.IThemeManager; + +public class CodeHtmlProvider extends BaseHtmlProvider { + private static CodeHtmlProvider instance = new CodeHtmlProvider(); + + public static CodeHtmlProvider getInstance() { + if (instance == null) { + synchronized (CodeHtmlProvider.class) { + if (instance == null) { + instance = new CodeHtmlProvider(); + } + } + } + return instance; + } + + @Override + public String getCss() { + return super.getCss() + "\n" + """ + .identifiers { + padding-bottom: 20px; + } + .data-flow-table { + background-color: var(--code-background-color); + border: 1px solid transparent; + } + .tabs-nav { + margin: 21px 0 -21px; + } + .light .dark-only, + .high-contrast.high-contrast-light .dark-only { + display: none; + } + + .dark .light-only, + .high-contrast:not(.high-contrast-light) .light-only { + display: none; + } + .tab-item { + cursor: pointer; + display: inline-block; + padding: 5px 10px; + border-bottom: 1px solid transparent; + color: var(--text-color); + text-transform: uppercase; + } + + .tab-item:hover { + /* Add hover styles if needed */ + } + + .tab-item.is-selected { + border-bottom: 3px solid var(--link-color); + } + + .tab-content { + display: none; + } + + .tab-content.is-selected { + display: block; + } + .removed { + background-color: var(--line-removed); + color: #fff; + } + .lesson-link { + margin-left: 3px; + } + .added { + background-color: var(--line-added); + color: #fff; + } + .arrow { + cursor: pointer; + width: 20px; + height: 20px; + padding: 4px; + border-radius: 4px; + text-align: center; + line-height: 1; + } + .example { + background-color: var(--container-background-color); + } + """; + } + + @Override + public String getInitScript() { + String themeScript = getThemeScript(); + String initScript = super.getInitScript(); + return initScript + "\n" + """ + function navigateToIssue(e, target) { + e.preventDefault(); + var filePath = target.getAttribute('file-path'); + var startLine = target.getAttribute('start-line'); + var endLine = target.getAttribute('end-line'); + var startCharacter = target.getAttribute('start-character'); + var endCharacter = target.getAttribute('end-character'); + window.openInEditor(filePath, startLine, endLine, startCharacter, endCharacter); + } + var navigatableLines = document.getElementsByClassName('data-flow-clickable-row'); + for(var i = 0; i < navigatableLines.length; i++) { + navigatableLines[i].onclick = function(e) { + navigateToIssue(e, this); + return false; + }; + } + if(document.getElementById('position-line')) { + document.getElementById('position-line').onclick = function(e) { + var target = navigatableLines[0]; + if(target) { + navigateToIssue(e, target); + } + } + } + // Disable Autofix and ignores + if(document.getElementById('ai-fix-wrapper') && document.getElementById('no-ai-fix-wrapper')){ + document.getElementById('ai-fix-wrapper').className = 'hidden'; + document.getElementById('no-ai-fix-wrapper').className = ''; + } + if(document.getElementsByClassName('ignore-action-container') && document.getElementsByClassName('ignore-action-container')[0]){ + document.getElementsByClassName('ignore-action-container')[0].className = 'hidden'; + } + """ + themeScript; + } + + private String getThemeScript() { + IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager(); + ITheme currentTheme = themeManager.getCurrentTheme(); + String themeId = currentTheme.getId().toLowerCase(); + + boolean isDarkTheme = themeId.contains("dark"); + boolean isHighContrast = themeId.contains("highcontrast") || themeId.contains("high-contrast"); + + String themeScript = "var isDarkTheme = " + isDarkTheme + ";\n" + + "var isHighContrast = " + isHighContrast + ";\n" + + "document.body.classList.add(isHighContrast ? 'high-contrast' : (isDarkTheme ? 'dark' : 'light'));"; + return themeScript; + } + + @Override + public String replaceCssVariables(String html) { + html = super.replaceCssVariables(html); + + // Replace CSS variables with actual color values + html = html.replace("var(--line-removed)", super.getColorAsHex("org.eclipse.ui.workbench.lineRemovedColor", "#ff0000")); + html = html.replace("var(--line-added)", super.getColorAsHex("org.eclipse.ui.workbench.lineAddedColor", "#00ff00")); + + return html; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java new file mode 100644 index 00000000..3db5d826 --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/HtmlProviderFactory.java @@ -0,0 +1,20 @@ +package io.snyk.eclipse.plugin.html; + +import io.snyk.eclipse.plugin.domain.ProductConstants; + +public class HtmlProviderFactory { + + public static BaseHtmlProvider GetHtmlProvider(String product) + { + switch (product) { + case ProductConstants.DISPLAYED_CODE_SECURITY: + case ProductConstants.DISPLAYED_CODE_QUALITY: + return CodeHtmlProvider.getInstance(); + case ProductConstants.DISPLAYED_OSS: + return OssHtmlProvider.getInstance(); + case ProductConstants.DISPLAYED_IAC: + return IacHtmlProvider.getInstance(); + } + return null; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java new file mode 100644 index 00000000..f54f32c0 --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/IacHtmlProvider.java @@ -0,0 +1,15 @@ +package io.snyk.eclipse.plugin.html; + +public class IacHtmlProvider extends BaseHtmlProvider { + private static IacHtmlProvider instance = new IacHtmlProvider(); + public static IacHtmlProvider getInstance() { + if (instance == null) { + synchronized (IacHtmlProvider.class) { + if (instance == null) { + instance = new IacHtmlProvider(); + } + } + } + return instance; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java new file mode 100644 index 00000000..b6d4e9be --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/OssHtmlProvider.java @@ -0,0 +1,15 @@ +package io.snyk.eclipse.plugin.html; + +public class OssHtmlProvider extends BaseHtmlProvider { + private static OssHtmlProvider instance = new OssHtmlProvider(); + public static OssHtmlProvider getInstance() { + if (instance == null) { + synchronized (OssHtmlProvider.class) { + if (instance == null) { + instance = new OssHtmlProvider(); + } + } + } + return instance; + } +} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java index 171b647f..aeeb4668 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java @@ -9,31 +9,51 @@ import java.util.List; import java.util.stream.Collectors; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.text.IDocument; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TreeNode; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.program.Program; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.ViewPart; +import org.eclipse.ui.texteditor.ITextEditor; import org.osgi.framework.Bundle; +import io.snyk.eclipse.plugin.html.CodeHtmlProvider; +import io.snyk.eclipse.plugin.html.HtmlProviderFactory; import io.snyk.eclipse.plugin.utils.ResourceUtils; import io.snyk.eclipse.plugin.views.snyktoolview.providers.TreeContentProvider; import io.snyk.eclipse.plugin.views.snyktoolview.providers.TreeLabelProvider; import io.snyk.languageserver.protocolextension.FileTreeNode; +import io.snyk.languageserver.protocolextension.messageObjects.scanResults.LineRange; + +import java.io.File; +import java.nio.file.Path; /** * TODO This view will replace the old SnykView. Move the snyktoolview classes @@ -79,6 +99,53 @@ public void createPartControl(Composite parent) { // Create Browser // SWT.EDGE will be ignored if OS not windows and will be set to SWT.NONE. browser = new Browser(sashForm, SWT.EDGE); + // Register the Java function as an anonymous class + + new BrowserFunction(browser, "openInEditor") { + @SuppressWarnings("restriction") + @Override + public Object function(Object[] arguments) { + if (arguments.length != 5) { + return null; + } + String filePath = (String) arguments[0]; + var fileUri = Paths.get(filePath).toUri().toASCIIString(); + int startLine = Integer.parseInt(arguments[1].toString()); + int endLine = Integer.parseInt(arguments[2].toString()); + int startCharacter = Integer.parseInt(arguments[3].toString()); + int endCharacter = Integer.parseInt(arguments[4].toString()); + + Display.getDefault().asyncExec(() -> { + try { + Position startPosition = new Position(startLine, startCharacter); + Position endPosition = new Position(endLine, endCharacter); + Range range = new Range(startPosition, endPosition); + + var location = new Location(fileUri, range); + LSPEclipseUtils.openInEditor(location); + + } catch (Exception e) { + e.printStackTrace(); + } + }); + return null; + } + }; + + browser.addLocationListener(new LocationListener() { + @Override + public void changing(LocationEvent event) { + String url = event.location; + if(url.startsWith("http")) { + event.doit = false; + Program.launch(url); + } + } + + @Override + public void changed(LocationEvent event) { + } + }); initBrowserText(); // Set sash weights @@ -114,7 +181,13 @@ private void registerTreeContextMeny(Composite parent) { private void updateBrowserContent(TreeNode node) { // Generate HTML content based on the selected node String htmlContent = generateHtmlContent(node); - browser.setText(htmlContent); + if (node instanceof IssueTreeNode) { + var product = ((ProductTreeNode) node.getParent().getParent()).getProduct(); + var htmlProvider = HtmlProviderFactory.GetHtmlProvider(product); + htmlContent = htmlProvider.replaceCssVariables(htmlContent); + browser.setText(htmlContent); + browser.execute(htmlProvider.getInitScript()); + } } private void updateBrowserContent(String text) { From eca2756bb5fb12a349e589b638d3742bfcff2963 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Thu, 21 Nov 2024 15:05:16 +0100 Subject: [PATCH 2/2] fix: populate tree after snyk.Scan success --- .../views/snyktoolview/SnykToolView.java | 24 ++--------------- .../snyk/languageserver/SnykIssueCache.java | 2 +- .../SnykExtendedLanguageClient.java | 27 ++++++++++--------- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java index aeeb4668..3f33e547 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java @@ -254,34 +254,14 @@ public void addFileNode(ProductTreeNode parent, FileTreeNode toBeAdded) { @Override public void addInfoNode(ProductTreeNode parent, InfoTreeNode toBeAdded) { - List list = new ArrayList<>(); - var children = parent.getChildren(); - if (children != null) { - list = Arrays.stream(children).map(it -> (BaseTreeNode) it).collect(Collectors.toList()); - } - toBeAdded.setParent(parent); - int insertIndex = GetLastInfoNodeIndex(list); - list.add(insertIndex, toBeAdded); - parent.setChildren(list.toArray(new BaseTreeNode[0])); - + parent.addChild(toBeAdded); + Display.getDefault().asyncExec(() -> { this.treeViewer.refresh(parent, true); }); } - private int GetLastInfoNodeIndex(List list) { - int insertIndex = 0; - for (int i = 0; i < list.size(); i++) { - if (list.get(i) instanceof InfoTreeNode) { - insertIndex += 1; - } else { - break; - } - } - return insertIndex; - } - @Override public ProductTreeNode getProductNode(String product, String folderPath) { if (product == null || folderPath == null) { diff --git a/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java b/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java index 6851fd5a..8ab12c2f 100644 --- a/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java +++ b/plugin/src/main/java/io/snyk/languageserver/SnykIssueCache.java @@ -208,7 +208,7 @@ public long getTotalCount(String product) { return getCacheByDisplayProduct(product).values().stream().flatMap(Collection::stream).count(); } - private Map> getCacheByDisplayProduct(String displayProduct) { + public Map> getCacheByDisplayProduct(String displayProduct) { switch (displayProduct) { case ProductConstants.DISPLAYED_OSS: return ossIssues; diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java index eb422478..b416537f 100644 --- a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java @@ -23,7 +23,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -326,7 +328,9 @@ public void snykScan(SnykScanParam param) { case SCAN_STATE_SUCCESS: scanState.setScanInProgress(inProgressKey, false); for (ProductTreeNode productTreeNode : affectedProductTreeNodes) { + productTreeNode.reset(); addInfoNodes(productTreeNode, param.getFolderPath(), issueCache); + populateFileAndIssueNodes(productTreeNode, param.getFolderPath(), issueCache); } break; case SCAN_STATE_ERROR: @@ -451,19 +455,19 @@ public CompletableFuture publishDiagnostics316(PublishDiagnostics316Param return; } - var productTreeNodes = populateIssueCache(param, filePath); - populateFileAndIssueNodes(filePath, productTreeNodes); + populateIssueCache(param, filePath); }); } - private void populateFileAndIssueNodes(String filePath, Set nodes) { - for (ProductTreeNode productTreeNode : nodes) { - var issueCache = IssueCacheHolder.getInstance().getCacheInstance(filePath); - var issues = issueCache.getIssues(filePath, productTreeNode.getProduct()); - if (issues.isEmpty()) + private void populateFileAndIssueNodes(ProductTreeNode productTreeNode, String folderPath, SnykIssueCache issueCache) { + var cacheHashMap = Collections.unmodifiableMap(issueCache.getCacheByDisplayProduct(productTreeNode.getProduct())); + for (var kv : cacheHashMap.entrySet()) { + var fileName = kv.getKey(); + var issues = kv.getValue(); + if(issues.isEmpty()) continue; issues = IssueSorter.sortIssuesBySeverity(issues); - FileTreeNode fileNode = new FileTreeNode(filePath); + FileTreeNode fileNode = new FileTreeNode(fileName); toolView.addFileNode(productTreeNode, fileNode); for (Issue issue : issues) { toolView.addIssueNode(fileNode, new IssueTreeNode(issue)); @@ -471,16 +475,16 @@ private void populateFileAndIssueNodes(String filePath, Set nod } } - private Set populateIssueCache(PublishDiagnostics316Param param, String filePath) { + private void populateIssueCache(PublishDiagnostics316Param param, String filePath) { var issueCache = getIssueCache(filePath); Diagnostic316[] diagnostics = param.getDiagnostics(); if (diagnostics.length == 0) { issueCache.removeAllIssuesForPath(filePath); - return Set.of(); + return; } var source = diagnostics[0].getSource(); if (StringUtils.isEmpty(source)) { - return Set.of(); + return; } var snykProduct = LSP_SOURCE_TO_SCAN_PARAMS.get(source); List issueList = new ArrayList<>(); @@ -503,7 +507,6 @@ private Set populateIssueCache(PublishDiagnostics316Param param issueCache.addIacIssues(filePath, issueList); break; } - return getAffectedProductNodes(snykProduct, filePath); } public void reportAnalytics(AbstractAnalyticsEvent event) {