How Zed Implements Diagnostic Markers and Error Display

Zed implements diagnostic markers by treating Language Server Protocol (LSP) diagnostics as first-class data that flows from the Language crate through the Editor to specialized renderers, displaying inline gutter markers, hover popovers with markdown, and a project-wide diagnostics panel.

The zed-industries/zed repository handles compiler and language-server diagnostics through a multi-layered architecture that transforms raw LSP payloads into visible UI elements. Understanding how Zed implements diagnostic markers and error display reveals a flexible system where buffer-level storage meets pluggable rendering.

The Diagnostic Data Model

The Diagnostic Struct

In crates/language/src/buffer.rs, the Diagnostic struct captures the complete LSP diagnostic payload including source, code, severity, message, markdown, group_id, and is_primary flags (lines 53-80). This struct serves as the canonical representation for all diagnostic data entering the editor, supporting both error and informational severities.

Buffer-Level Diagnostic Sets

Each Buffer maintains a DiagnosticSet implemented as a BTreeMap<Point, DiagnosticEntry>, enabling efficient range-based queries and severity filtering. This storage mechanism allows the editor to quickly retrieve relevant diagnostics when rendering specific line ranges or responding to hover events.

The Diagnostic Rendering Pipeline

Global Renderer Registration

The editor delegates all visual representation to a global DiagnosticRenderer trait defined in crates/editor/src/editor.rs (lines 393-425). The default renderer implementation resides in crates/diagnostics/src/diagnostic_renderer.rs and registers at startup via editor::set_diagnostic_renderer, which is invoked in crates/diagnostics/src/diagnostics.rs (lines 70-71).

Building Diagnostic Blocks

The default renderer's diagnostic_blocks_for_group method constructs DiagnosticBlock instances for each diagnostic group, generating markdown bodies, copy-button text, and severity-based color mappings (lines 22-86). These blocks encapsulate both the visual styling and interactive elements displayed to users.

Inline Gutter Markers

In crates/editor/src/element.rs, the EditorElement::layout_inline_diagnostics method (lines 55-115) traverses visible rows and filters diagnostics by the user-configured max_severity setting. The method creates colored gutter elements using a local severity_to_color mapping that matches the theme's status colors, skipping diagnostics from the currently active group to avoid visual clutter.

Hover Popovers and Project-Wide Panels

Hover State and Diagnostic Popovers

When hovering over a line containing diagnostics, Editor::hover_state instantiates a DiagnosticPopover defined in crates/editor/src/hover_popover.rs (lines 314-334). This popover renders markdown generated by the renderer's DiagnosticBlock::render_block method and includes a copy button for quick error message extraction.

Status Bar and Diagnostics Panel

The DiagnosticIndicator in crates/diagnostics/src/items.rs (lines 17-65) observes project events such as DiagnosticsUpdated and DiskBasedDiagnosticsFinished, displaying error and warning counts in the status bar. Clicking this indicator opens the ProjectDiagnosticsEditor, which aggregates all diagnostics grouped by file and severity across the entire project.

Extending the Diagnostic System

Developers can override the default visualization by implementing the DiagnosticRenderer trait and registering it via set_diagnostic_renderer.

Registering a Custom Renderer

use editor::{set_diagnostic_renderer, DiagnosticRenderer};
use gpui::AppContext;
use std::sync::Arc;

struct SimpleRenderer;

impl DiagnosticRenderer for SimpleRenderer {
    fn render_group(
        &self,
        diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
        buffer_id: BufferId,
        snapshot: EditorSnapshot,
        editor: gpui::WeakEntity<Editor>,
        language_registry: Option<Arc<LanguageRegistry>>,
        cx: &mut AppContext,
    ) -> Vec<BlockProperties<Anchor>> {
        let mut blocks = editor::default_diagnostic_renderer().render_group(
            diagnostic_group,
            buffer_id,
            snapshot,
            editor,
            language_registry,
            cx,
        );
        for block in &mut blocks {
            block.style = BlockStyle::Overlay(Color::from_rgba8(0x80, 0x00, 0x80, 0xFF));
        }
        blocks
    }

    fn render_hover(&self, ..) -> Option<gpui::Entity<Markdown>> { None }
    fn open_link(&self, ..) {}
}

// In your app start-up code
set_diagnostic_renderer(SimpleRenderer, cx);

Creating Diagnostics Programmatically

For static analysis extensions, manually inject diagnostics into the editor's inline_diagnostics map:

use language::{Diagnostic, DiagnosticSeverity, Point, Range};

let diag = Diagnostic {
    source: Some("my-linter".into()),
    registration_id: None,
    code: Some(NumberOrString::String("M001".into())),
    code_description: None,
    severity: DiagnosticSeverity::WARNING,
    message: "Avoid using `foo` here".into(),
    markdown: None,
    group_id: 0,
    is_primary: true,
};

let range = Range {
    start: Point::new(9, 5),
    end: Point::new(9, 15),
};

editor.update(cx, |ed, cx| {
    ed.add_inline_diagnostic(range, diag);
});

Opening the Diagnostics Panel

Extensions can trigger the project-wide view programmatically:

use workspace::Workspace;
use diagnostics::ProjectDiagnosticsEditor;

workspace.update(cx, |ws, cx| {
    ProjectDiagnosticsEditor::deploy(ws, &Default::default(), window, cx);
});

Summary

  • Diagnostic storage: The Diagnostic struct and DiagnosticSet in the Language crate provide the foundation for buffer-level diagnostic tracking.
  • Renderer abstraction: The global DiagnosticRenderer trait decouples data from presentation, enabling custom visual implementations.
  • Inline display: EditorElement::layout_inline_diagnostics renders severity-colored gutter markers based on visible ranges.
  • Interactive UI: Hover popovers and the status-bar DiagnosticIndicator provide detailed error information and project-wide navigation.
  • Extensibility: The set_diagnostic_renderer API allows complete customization of how Zed displays errors, warnings, and hints.

Frequently Asked Questions

What is the DiagnosticRenderer trait in Zed?

The DiagnosticRenderer trait defined in crates/editor/src/editor.rs (lines 393-425) is the core interface for converting diagnostic data into UI elements. It provides methods like render_group for creating visual blocks and render_hover for popover content, allowing developers to customize every aspect of error display.

How does Zed store diagnostics internally?

Zed stores diagnostics per-buffer using a DiagnosticSet, which is a BTreeMap<Point, DiagnosticEntry> defined in the Language crate. This structure enables efficient querying by line range and severity, supporting fast lookups when the editor renders inline markers or hover states.

Can I customize the colors of diagnostic markers in Zed?

Yes, by implementing a custom DiagnosticRenderer and registering it via set_diagnostic_renderer, you can override the default severity_to_color mappings. The custom renderer receives DiagnosticEntryRef instances and returns BlockProperties with your chosen colors, styles, and interactive elements.

How do I access the project-wide diagnostics panel programmatically?

Use ProjectDiagnosticsEditor::deploy from the diagnostics crate, passing the current Workspace instance. This opens the panel that aggregates all diagnostics across files, as triggered by the DiagnosticIndicator in the status bar when users click the error count display.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →