| name | add-renderer |
| description | Step-by-step guide for adding a new diff output renderer to diffsitter. Use when adding a new output format. |
| allowed-tools | Read, Grep, Glob, Bash, Edit, Write |
| user-invocable | true |
| argument-hint | [name] Name of the new renderer (e.g., 'delta', 'html') |
Adding a New Renderer to diffsitter
Follow this checklist to add a new diff output renderer named $ARGUMENTS. If the user did not provide a name, ask for one before proceeding.
Prerequisites
Read these files first to understand the existing patterns:
src/render/mod.rs -- trait definition, enum, config
src/render/json.rs -- minimal renderer example
src/render/unified.rs -- full-featured renderer example
src/config.rs -- top-level config struct
assets/sample_config.json5 -- sample config (CI parses this as a test)
Step-by-Step Checklist
Step 1: Create the renderer module
Create src/render/$ARGUMENTS.rs with a struct that derives the required traits:
use super::DisplayData;
use crate::render::Renderer;
use console::Term;
use serde::{Deserialize, Serialize};
use std::io::Write;
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug, Default)]
pub struct $ARGUMENTS_PASCAL_CASE {
}
impl Renderer for $ARGUMENTS_PASCAL_CASE {
fn render(
&self,
writer: &mut dyn Write,
data: &DisplayData,
term_info: Option<&Term>,
) -> anyhow::Result<()> {
todo!()
}
}
Key types available in DisplayData:
data.hunks.0 -- Vec<RichHunk<'a>> where RichHunk is DocumentType<Hunk>
- Each
Hunk contains Vec<Line>, each Line has line_index: usize and entries: Vec<&Entry>
- Each
Entry has text: Cow<str>, start_position: Point, end_position: Point, kind_id: u16
DocumentType::Old(hunk) / DocumentType::New(hunk) distinguishes old vs new document hunks
Use src/render/json.rs as a minimal reference (just serializes DisplayData to JSON). Use src/render/unified.rs for a full-featured example with terminal colors, hunk titles, and line-by-line rendering.
Step 2: Register the module in src/render/mod.rs
Add the module declaration and use statement near the top:
mod $ARGUMENTS;
use self::$ARGUMENTS::$ARGUMENTS_PASCAL_CASE;
These go alongside the existing:
mod json;
mod unified;
use self::json::Json;
use unified::Unified;
Step 3: Add a variant to the Renderers enum
Add your variant to the Renderers enum in src/render/mod.rs:
#[enum_dispatch]
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, Display, EnumIter, EnumString)]
#[strum(serialize_all = "snake_case")]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Renderers {
Unified,
Json,
$ARGUMENTS_PASCAL_CASE,
}
The enum_dispatch attribute automatically generates the Renderer trait dispatch for the new variant. The strum and serde derives handle string conversion and serialization using the snake_case name.
Step 4: Add a field to RenderConfig
In src/render/mod.rs, add a field to RenderConfig:
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case", default)]
pub struct RenderConfig {
default: String,
unified: unified::Unified,
json: json::Json,
$ARGUMENTS: $ARGUMENTS::$ARGUMENTS_PASCAL_CASE,
}
Update the Default impl for RenderConfig:
impl Default for RenderConfig {
fn default() -> Self {
let default_renderer = Renderers::default();
RenderConfig {
default: default_renderer.to_string(),
unified: Unified::default(),
json: Json::default(),
$ARGUMENTS: $ARGUMENTS_PASCAL_CASE::default(),
}
}
}
Step 5: Update assets/sample_config.json5
Add a section for the new renderer's configuration under the "formatting" key. CI parses this file as a test (test_sample_config in src/config.rs), so it must be valid.
Step 6: Add tests
At minimum:
- Add a
#[test_case("$ARGUMENTS")] line to the test_get_renderer_custom_tag test in src/render/mod.rs:
#[test_case("unified")]
#[test_case("json")]
#[test_case("$ARGUMENTS")]
fn test_get_renderer_custom_tag(tag: &str) {
-
Add unit tests in your renderer module for any non-trivial logic.
-
Consider adding snapshot tests with insta if the output format is complex.
Step 7: Build and test
cargo build
cargo test --all
If you updated sample_config.json5, the test_sample_config test will verify it parses correctly.
Common Pitfalls
- Forgetting
Default derive/impl: The RenderConfig uses #[serde(default)], so your struct must implement Default.
- Case sensitivity: The
Renderers enum uses snake_case serialization via strum/serde. Your variant MyRenderer becomes "my_renderer" as a string tag.
- The
writer is generic: Don't assume stdout. The renderer receives &mut dyn Write which could be a buffered terminal, a pager, or a file.
term_info may be None: If the output is piped or redirected, there is no terminal. Handle gracefully (see how unified.rs handles missing terminal width).