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

  1. Generate HTML Template: The Language Server generates an HTML template using the Go html/template library. The template includes placeholders for dynamic content.
  2. 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.
  3. 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:

  1. 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()
}
  1. Update the HTML template in infrastructure/code/template/details.html to 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>
  1. If necessary, add IDE-specific CSS:

VSCode

IntelliJ

  1. 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 nonce placeholders 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 via infrastructure/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

  1. Generate HTML Template with Nonce: The Language Server generates an HTML template, including placeholders for nonce attributes.
  2. Send HTML Template to IDE: The IDE receives the template and prepares to render it.
  3. 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

ComponentPackagePurpose
TreeNode, TreeViewDatadomain/ide/treeviewData types for the tree hierarchy
TreeBuilderdomain/ide/treeviewBuilds tree from workspace folder data
TreeHtmlRendererdomain/ide/treeviewRenders TreeViewData → HTML via Go templates
TreeScanStateEmitterdomain/ide/treeviewAdapts ScanStateChangeEmitter interface; sends $/snyk.treeView notifications (mutex-guarded)
CompositeEmitterdomain/scanstatesFans out to summary + tree view emitters
getTreeViewCommanddomain/ide/commandOn-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 synchronously
  • snyk.toggleTreeFilter — toggle severity/issueView filters, triggers $/snyk.treeView re-render
  • snyk.setNodeExpanded — persist expand/collapse state server-side
  • snyk.updateFolderConfig — update delta reference (branch or folder), triggers rescan via context.Background()
  • snyk.navigateToRange — navigate to file location; optionally opens detail panel via snyk:// 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> — uses div + 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)

  • getTreeView command returns valid HTML
  • toggleTreeFilter: severity + issueView toggles, error cases
  • setNodeExpanded: expand/collapse persistence
  • updateFolderConfig: mutual exclusivity (branch/folder), error handling
  • navigateToRange: snyk:// URI uses uri.PathToUri for 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