ui-rendering.md
May 29, 2026 · View on GitHub
UI Rendering with Language Server
This document explains the architecture and interaction between the Language Server and IDEs for rendering the UI components. It aims to guide contributors in adding or modifying UI components.
Architecture Overview
Our approach unifies the user interface for Snyk Code Ignores across multiple IDEs by leveraging the Language Server Protocol. This ensures consistency, reduces redundancy, and simplifies future enhancements.
sequenceDiagram
participant LSP
participant Template
participant IDE
LSP->>Template: Generate HTML Template
Template->>IDE: Send HTML Template
IDE->>Template: Inject IDE-specific CSS
IDE->>User: Rendered Content
Workflow Description
- Generate HTML Template: The Language Server generates an HTML template using the Go
html/templatelibrary. The template includes placeholders for dynamic content. - Send HTML Template: The generated HTML template is sent to the IDEs. This template includes the structure of the UI components but lacks specific styling.
- Injected IDE-specific CSS: Each IDE injects its own CSS to style the HTML template according to its theming and design guidelines. This allows the same HTML structure to be visually consistent with the rest of the IDE.
Using //go:embed for CSS and HTML Files
This directive loads static assets like CSS and HTML files directly into our application. This is particularly useful for embedding resources such as templates and stylesheets that are needed for rendering the UI components.
//go:embed template/index.html
var detailsHtmlTemplate string
//go:embed template/styles.css
var stylesCSS string
The Go build system will recognize the directives and arrange for the declared variable to be populated with the matching files from the file system.
Adding or Modifying UI Components
Snyk Code Suggestion Panel
To add or modify UI components in the IDEs that support the Language Server, follow these steps:
- Process the dynamic data to be rendered in the HTML template in
infrastructure/code/code_html.go. For example, to display the issue ecosystem in the UI:
func getCodeDetailsHtml(engine workflow.Engine, issue snyk.Issue) string {
logger := engine.GetLogger()
data := map[string]interface{}{
"Ecosystem": issue.Ecosystem,
"IssueTitle": additionalData.Title,
// more data
}
var html bytes.Buffer
if err := globalTemplate.Execute(&html, data); err != nil {
logger.Error().Msgf("Failed to execute main details template: %v", err)
return ""
}
return html.String()
}
- Update the HTML template in
infrastructure/code/template/details.htmlto include the placeholders for the dynamic data. Include CSS in the template itself if the styles are the same across all IDEs, otherwise use IDE-specific files.
<head>
<style>
.ecosystem-badge {
padding: 0.35em 0.35em;
border-radius: 0.25em;
margin-left: 1em;
background: #FFF4ED;
color: #B6540B;
border: 1px solid #E27122;
}
</style>
</head>
<body>
<h2 class="severity-title">{{.IssueTitle}}</h2>
<span class="delimiter"></span>
<div>Priority score: {{.PriorityScore}}</div>
<span class="delimiter"></span>
<div class="ecosystem-badge">{{.Ecosystem}}</div>
<!-- more HTML -->
</body>
- If necessary, add IDE-specific CSS:
VSCode
- CSS: suggestionLS.scss
- HTML Rendering: codeSuggestionWebviewProvider.ts
- Script Injection: codeSuggestionWebviewScriptLS.ts
IntelliJ
-
HTML Rendering: JCEFDescriptionPanel.kt
- Handle Nonce and IDE-Specific Styles
When dealing with Content Security Policies (CSP) in the HTML generated by the Language Server, it’s important to correctly handle nonce attributes for both styles and scripts to ensure they are applied securely.
- Language Server: Ensure the HTML template includes
nonceplaceholders that will be replaced by dynamically generated nonces. For example:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'self' 'nonce-{{.Nonce}}' 'nonce-ideNonce'; script-src 'nonce-{{.Nonce}}';">
<!--noformat-->
<style nonce="{{.Nonce}}">
{{.Styles}}
</style>
<!--noformat-->
<style nonce="ideNonce" data-ide-style></style>
</head>
-
{{.Nonce}}: This is a dynamically generated nonce passed from, for example,infrastructure/iac/iac_html.go. -
{{.Styles}}: This is where the styles defined in the Language Server are injected, also passed viainfrastructure/iac/iac_html.go. -
ideNonce: This placeholder will be replaced by the IDE with its own dynamically generated nonce for IDE-specific styles. -
IDE (e.g., VSCode): Replace the nonce placeholder in the HTML with the actual nonce generated by the IDE, and inject IDE-specific styles:
private getHtmlFromLanguageServer(html: string): string {
const nonce = getNonce();
const ideStylePath = vscode.Uri.joinPath(
vscode.Uri.file(this.context.extensionPath),
'media',
'views',
'snykCode',
'suggestion',
'suggestionLS.css',
);
const ideStyle = readFileSync(ideStylePath.fsPath, 'utf8');
html = html.replace(/nonce-ideNonce/g, `nonce-${nonce}`); // Replace the placeholder with IDE nonce
html = html.replace(
'<style nonce="ideNonce" data-ide-style></style>',
`<style nonce="${nonce}">${ideStyle}</style>`,
);
return html;
}
Final Workflow
- Generate HTML Template with Nonce: The Language Server generates an HTML template, including placeholders for
nonceattributes. - Send HTML Template to IDE: The IDE receives the template and prepares to render it.
- Replace Nonce and Inject Styles: The IDE replaces the nonce placeholders with actual nonces and injects any IDE-specific styles.
Settings dialog HTML (command snyk.workspace.configuration) uses the same embed pattern; wire format from the form to the IDE is documented in configuration-dialog.md and the overall config model in configuration.md.
Server-Driven HTML Tree View
The tree view panel is rendered server-side and sent to IDEs as a web view. This replaces per-IDE native tree implementations (IntelliJ JTree, VS Code TreeDataProvider) to reduce maintenance.
Architecture
sequenceDiagram
participant Scanner
participant ScanStateAggregator
participant CompositeEmitter
participant TreeScanStateEmitter
participant SummaryEmitter
participant IDE
Scanner->>ScanStateAggregator: SetScanDone / SetScanInProgress
ScanStateAggregator->>CompositeEmitter: Emit(StateSnapshot)
CompositeEmitter->>SummaryEmitter: Emit(StateSnapshot)
CompositeEmitter->>TreeScanStateEmitter: Emit(StateSnapshot)
TreeScanStateEmitter->>IDE: $/snyk.treeView notification (HTML)
SummaryEmitter->>IDE: $/snyk.scanSummary notification (HTML)
Key Components
| Component | Package | Purpose |
|---|---|---|
TreeNode, TreeViewData | domain/ide/treeview | Data types for the tree hierarchy |
TreeBuilder | domain/ide/treeview | Builds tree from workspace folder data |
TreeHtmlRenderer | domain/ide/treeview | Renders TreeViewData → HTML via Go templates |
TreeScanStateEmitter | domain/ide/treeview | Adapts ScanStateChangeEmitter interface; sends $/snyk.treeView notifications (mutex-guarded) |
CompositeEmitter | domain/scanstates | Fans out to summary + tree view emitters |
getTreeViewCommand | domain/ide/command | On-demand snyk.getTreeView LSP command |
Tree Hierarchy
[Folder] ← only in multi-root workspaces
└─ [Product] (Open Source, Code Security, IaC, Secrets)
└─ [File]
└─ [Issue] ← leaf, clickable → navigate to file
LSP Notifications & Commands
$/snyk.treeView— automatic push notification containing{ "treeViewHtml": "<html>...", "totalIssues": N }snyk.getTreeView— on-demand command returning the tree HTML synchronouslysnyk.toggleTreeFilter— toggle severity/issueView filters, triggers$/snyk.treeViewre-rendersnyk.setNodeExpanded— persist expand/collapse state server-sidesnyk.updateFolderConfig— update delta reference (branch or folder), triggers rescan viacontext.Background()snyk.navigateToRange— navigate to file location; optionally opens detail panel viasnyk://URI
IDE Integration
IDEs render the tree HTML in a WebView. The HTML includes:
${ideStyle}— placeholder for IDE-injected CSS${ideScript}— placeholder for IDE-injected JS bridge${nonce}— placeholder for CSP nonce
The tree uses a single unified bridge window.__ideExecuteCommand__(command, args, callback) for all
JS→IDE communication. IDEs implement this one function to forward calls as workspace/executeCommand.
For example, clicking an issue calls __ideExecuteCommand__('snyk.navigateToRange', [filePath, range, issueId, product])
where range is { start: { line, character }, end: { line, character } }. When issueId and product are provided,
the command also opens the issue detail panel via a snyk:// URI constructed from uri.PathToUri.
Browser Compatibility
Visual Studio's webview has migrated to Chromium; IE11 is no longer a supported target.
- ES5 JavaScript only (no
const,let, arrow functions, template literals) - No
<details>/<summary>— usesdiv+ class toggling - CSS compatible with Chromium-based webviews (no CSS variables, no grid)
Filtering
Filters (severity, open/ignored) are applied server-side via existing
SeverityFilter and IssueViewOptions in the workspace folder configuration.
The tree builder reads FilteredIssues which already reflect these filters.
Expand/Collapse
Expand/collapse state is persisted server-side via ExpandState. The client sends
snyk.setNodeExpanded on toggle; the server stores the state and applies it on re-renders.
Product nodes default to expanded, file nodes default to collapsed. Trees with <= 50 issues auto-expand progressively.
Tested Scenarios
Unit Tests (domain/ide/treeview)
- Tree node creation with all option variants
- Tree building: empty workspace, single/multi folder, single/multi product
- Issue sorting by severity, file sorting alphabetically
- Ignored/new/fixable badge flags, nil AdditionalData handling
- Product and file description containing issue counts
- HTML rendering: valid HTML structure, CSS, IE11 meta tag
- Data attributes on issue nodes for click navigation
- Scan-in-progress indicator, per-product scan status
- Multi-root folder rendering, delta reference selection (branch/folder)
- Emitter notification delivery (both direct and scan-state-driven)
- Concurrent Emit() calls (race detector)
- ExpandState: set/get, defaults, overrides, concurrent access
Unit Tests (domain/scanstates)
- Composite emitter calls all child emitters
Unit Tests (domain/ide/command)
getTreeViewcommand returns valid HTMLtoggleTreeFilter: severity + issueView toggles, error casessetNodeExpanded: expand/collapse persistenceupdateFolderConfig: mutual exclusivity (branch/folder), error handlingnavigateToRange: snyk:// URI usesuri.PathToUrifor cross-platform normalization
JS Runtime Tests (domain/ide/treeview/template/js-tests/)
- Expand/collapse, filter toggle, issue click navigation, auto-expand
Smoke Tests (application/server/server_smoke_treeview_test.go)
- Tree view notification after scan, getTreeView command, toggleTreeFilter