Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/diffr-plugin-sdk/src/guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ impl<P: Plugin + 'static> guest::GuestPlugin for Instance<P> {
Ok(guest::Plugin::new(Instance(plugin)))
}

fn enrich(
&self,
file: FileEntry,
sides: SourceSides,
) -> Result<Vec<crate::Annotation>, String> {
let sides = tree::sides(&sides).map_err(|error| format!("{error:#}"))?;
self.0
.enrich(&file, &sides)
.map_err(|error| format!("{error:#}"))
}
fn queries(&self) -> Result<Vec<crate::QuerySource>, String> {
self.0.queries().map_err(|error| format!("{error:#}"))
}
Expand Down
13 changes: 11 additions & 2 deletions crates/diffr-plugin-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ pub use tree::{
siblings_of, sides_with_other_ids, walk, walk_mut, Node, OtherSide, Pairing, Region, Source,
};
pub use types::{
FileEntry, FileRef, FileSides, FileStatus, Move, Position, QuerySource, Range, Side, Span,
Visibility, ROOT,
Annotation, FileEntry, FileRef, FileSides, FileStatus, Move, Position, QuerySource, Range,
Side, Span, Visibility, ROOT,
};

/// A diffr plugin: the `plugin` resource of `wit/plugin.wit`. diffr makes one
Expand Down Expand Up @@ -71,6 +71,15 @@ pub trait Plugin: Sized {
/// The moves that shape how the diffed file starts out. `sides` are the
/// sides the file has, already rebuilt as trees.
fn mutate(&self, file: &FileEntry, sides: &Pairing<Source>) -> anyhow::Result<Vec<Move>>;

/// Deferred labels for stable region IDs, after all initial mutations.
fn enrich(
&self,
_file: &FileEntry,
_sides: &Pairing<Source>,
) -> anyhow::Result<Vec<Annotation>> {
Ok(Vec::new())
}
}

/// The contract generated from `wit/plugin.wit`. Its records are plain Rust
Expand Down
12 changes: 12 additions & 0 deletions crates/diffr-plugin-sdk/src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use crate::{tree, FileEntry, Move, Plugin, QuerySource};
/// The object-safe form of a plugin, after its options have been deserialized.
pub trait Instance: Send + Sync {
fn queries(&self) -> anyhow::Result<Vec<QuerySource>>;
fn enrich(
&self,
file: &FileEntry,
sides: &SourceSides,
) -> anyhow::Result<Vec<crate::Annotation>>;
fn classify(&self, file: &FileEntry) -> anyhow::Result<Vec<String>>;
fn mutate(&self, file: &FileEntry, sides: &SourceSides) -> anyhow::Result<Vec<Move>>;
}
Expand All @@ -27,6 +32,13 @@ pub fn create<P: Plugin + Send + Sync + 'static>(
struct Adapter<P>(P);

impl<P: Plugin + Send + Sync> Instance for Adapter<P> {
fn enrich(
&self,
file: &FileEntry,
sides: &SourceSides,
) -> anyhow::Result<Vec<crate::Annotation>> {
self.0.enrich(file, &tree::sides(sides)?)
}
fn queries(&self) -> anyhow::Result<Vec<QuerySource>> {
self.0.queries()
}
Expand Down
4 changes: 2 additions & 2 deletions crates/diffr-plugin-sdk/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! This module adds only what the WIT cannot say: [`ROOT`], the id that names
//! the file rather than a region, and the impls below.
pub use crate::bindings::diffr::plugin::types::{
Cut, FileEntry, FileRef, FileSides, FileStatus, Kind, Leaf, Move, Position, QuerySource, Range,
Region, Side, Source, SourceSides, Span, Visibility,
Annotation, Cut, FileEntry, FileRef, FileSides, FileStatus, Kind, Leaf, Move, Position,
QuerySource, Range, Region, Side, Source, SourceSides, Span, Visibility,
};

use crate::tree::Pairing;
Expand Down
17 changes: 16 additions & 1 deletion docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ external plugin never requires rebuilding diffr.
## The contract

[`wit/plugin.wit`](../wit/plugin.wit) is the contract: the package
`diffr:plugin@0.1.0`, world `plugin`. A plugin exports the interface `guest`,
`diffr:plugin@0.2.0`, world `plugin`. A plugin exports the interface `guest`,
which holds one resource, the plugin itself (the component model has no
optional exports; a plugin that does not classify returns an empty list):

Expand Down Expand Up @@ -358,3 +358,18 @@ Opening a module or aggregate fold reveals each test's pseudocode; opening an
individual body reveals its source. JS/TS `describe` suites remain containers,
with summaries on their individual `it`/`test` callbacks. An explicit custom
`plugins.order` is respected; update it to this order to match the defaults.

## Deferred annotations

The 0.2 plugin ABI adds `enrich(file, sides) -> list<annotation>`. Rebuild
external components against the updated SDK (`cargo xtask build-plugins` rebuilds
bundled components). SDK plugins default to returning no annotations.

`mutate` performs initial presentation only. After all mutations finish, `enrich`
may do slow work and return `{region-id, label}` records. IDs refer to the final
region trees and are validated by the host. Annotations cannot change ranges,
alignment, fold-state IDs, or collapsed state. The summarizer reserves its folds
and links docstrings during mutation; enrichment only attaches pseudocode.
Rejected or absent summaries leave the initial fold available to expand.

The ordinary stream still applies both phases before emitting a file.
Binary file modified plugins/summarize/plugin.wasm
Binary file not shown.
76 changes: 48 additions & 28 deletions plugins/summarize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! Requests use WASI HTTP. The host calls one file at a time per instance.
use diffr_plugin_sdk::anyhow::{self, anyhow, Context as _};
use diffr_plugin_sdk::{
docstring_of, export, has_tag, is_fold, line_count, one_sided, walk, Draft, FileEntry, Move,
Node, OtherSide, Pairing, Plugin, Region, Source,
docstring_of, export, has_tag, is_fold, line_count, one_sided, walk, Annotation, Draft,
FileEntry, Move, Node, OtherSide, Pairing, Plugin, Region, Source,
};
use serde::Deserialize;
use serde_json::json;
Expand Down Expand Up @@ -61,7 +61,6 @@ struct Request {
id: u32,
first_line: u32,
last_line: u32,
docstring: Option<u32>,
doc: Option<String>,
}

Expand Down Expand Up @@ -320,7 +319,7 @@ fn quoted(doc: Option<&str>, summary: &str) -> Option<String> {

/// Pseudocode earns its place only when it is clearly shorter than the
/// code: a summary with more than half the body's non-blank lines is
/// dropped and the body stays open.
/// dropped; the initially folded body remains expandable.
fn compresses(summary: &str, body: &[&str]) -> bool {
let summary_lines = summary
.lines()
Expand Down Expand Up @@ -411,12 +410,46 @@ impl Plugin for Summarize {
Ok(Vec::new())
}

fn mutate(&self, file: &FileEntry, sides: &Pairing<Source>) -> anyhow::Result<Vec<Move>> {
fn mutate(&self, _file: &FileEntry, sides: &Pairing<Source>) -> anyhow::Result<Vec<Move>> {
let selected = select(
sides,
self.options.min_lines,
self.options.tests.then_some(self.options.test_min_lines),
);
let mut draft = Draft::new(sides);
let mut tags = BTreeMap::new();
if let Pairing::Both { rhs, .. } | Pairing::RightOnly { rhs } = sides {
walk(&rhs.regions, &mut |region| {
tags.insert(region.id, region.tags.clone());
});
}
for (id, _, _, docstring) in selected {
let mut region_tags = tags.remove(&id).expect("selected region exists");
region_tags.push("summarize:pending".into());
draft.push(Move::SetTags((id, region_tags)))?;
draft.collapse(id, String::new())?;
if let Some(docstring) = docstring {
draft.link(&[id, docstring])?;
}
}
Ok(draft.into_moves())
}

fn enrich(&self, file: &FileEntry, sides: &Pairing<Source>) -> anyhow::Result<Vec<Annotation>> {
let mut selected = Vec::new();
if let Pairing::Both { rhs, .. } | Pairing::RightOnly { rhs } = sides {
walk(&rhs.regions, &mut |region| {
if has_tag(region, "summarize:pending") {
let lines = region.range.lines();
selected.push((
region.id,
lines.start + 1,
lines.end,
docstring_of(rhs, region, PLUGIN),
));
}
});
}
let (Pairing::Both { rhs, .. } | Pairing::RightOnly { rhs }) = &sides else {
return Ok(Vec::new());
};
Expand All @@ -426,7 +459,6 @@ impl Plugin for Summarize {
id,
first_line,
last_line,
docstring,
doc: docstring.and_then(|docstring| documentation(rhs, docstring)),
})
.collect();
Expand All @@ -445,28 +477,16 @@ impl Plugin for Summarize {
let body = &lines[fold.first_line as usize - 1..fold.last_line as usize];
compresses(&summary.pseudocode, body)
});
// Collapse every summarized body first, then link each to its
// docstring: a summary and its docstring are one thing.
let mut draft = Draft::new(sides);
let mut links = Vec::new();
for (id, summary) in texts {
let text = match &summary.quote {
Some(quote) => format!("{quote}\n{}", summary.pseudocode),
None => summary.pseudocode.clone(),
};
draft.collapse(id, text)?;
let fold = folds
.iter()
.find(|fold| fold.id == id)
.expect("answered fold");
if let Some(docstring) = fold.docstring {
links.push([id, docstring]);
}
}
for link in links {
draft.link(&link)?;
}
Ok(draft.into_moves())
Ok(texts
.into_iter()
.map(|(region_id, summary)| Annotation {
region_id,
label: match summary.quote {
Some(quote) => format!("{quote}\n{}", summary.pseudocode),
None => summary.pseudocode,
},
})
.collect())
}
}

Expand Down
90 changes: 89 additions & 1 deletion src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ use std::time::Instant;
/// its options, behind the contract's two calls on a file. Each call gets
/// the host for that call.
pub(crate) trait Runner: Send + Sync {
fn enrich(
&self,
_host: Host,
_file: &types::FileEntry,
_sides: &types::SourceSides,
) -> anyhow::Result<Vec<types::Annotation>> {
Ok(Vec::new())
}

fn queries(&self, host: Host) -> anyhow::Result<Vec<types::QuerySource>>;

fn classify(&self, host: Host, file: &types::FileEntry) -> anyhow::Result<Vec<String>>;
Expand Down Expand Up @@ -201,9 +210,88 @@ impl Pipeline {
Ok(entry.tags)
}

/// Compatibility path: return the fully enriched file in one operation.
pub(crate) fn run(
&self,
file: &FileChange,
sides: &mut Pairing<protocol::Source>,
) -> anyhow::Result<protocol::Visibility> {
let visibility = self.prepare(file, sides)?;
let annotations = self.enrich(file, sides)?;
Self::apply_annotations(sides, &annotations)?;
Ok(visibility)
}

/// Deferred plugins may only attach labels to existing regions.
pub(crate) fn enrich(
&self,
file: &FileChange,
sides: &Pairing<protocol::Source>,
) -> anyhow::Result<Vec<protocol::Annotation>> {
let trees = match sides {
Pairing::Both { lhs, rhs } => tree::Pairing::Both {
lhs: to_tree(lhs),
rhs: to_tree(rhs),
},
Pairing::LeftOnly { lhs } => tree::Pairing::LeftOnly { lhs: to_tree(lhs) },
Pairing::RightOnly { rhs } => tree::Pairing::RightOnly { rhs: to_tree(rhs) },
};
let records = source_sides(&trees);
let entry = file_entry(file);
let mut annotations = Vec::new();
for plugin in &self.plugins {
let labels = plugin
.runner
.enrich(self.host(&plugin.name), &entry, &records)
.with_context(|| MutationFailed(plugin.name.to_string()))?;
annotations.extend(labels.into_iter().map(|label| protocol::Annotation {
region_id: label.region_id,
label: label.label,
}));
}
// Validate the complete batch before it can leave the host.
Self::apply_annotations(&mut sides.clone(), &annotations)?;
Ok(annotations)
}

pub(crate) fn apply_annotations(
sides: &mut Pairing<protocol::Source>,
annotations: &[protocol::Annotation],
) -> anyhow::Result<()> {
fn find(regions: &mut [protocol::Region], id: u32) -> Option<&mut protocol::Region> {
for region in regions {
if region.id == id {
return Some(region);
}
if let protocol::Node::Fold { children } = &mut region.node {
if let Some(found) = find(children, id) {
return Some(found);
}
}
}
None
}
for annotation in annotations {
let region = match sides {
Pairing::Both { lhs, rhs } => find(&mut lhs.regions, annotation.region_id)
.or_else(|| find(&mut rhs.regions, annotation.region_id)),
Pairing::LeftOnly { lhs } => find(&mut lhs.regions, annotation.region_id),
Pairing::RightOnly { rhs } => find(&mut rhs.regions, annotation.region_id),
}
.ok_or_else(|| {
anyhow!(
"annotation refers to missing region {}",
annotation.region_id
)
})?;
region.visibility.label = annotation.label.clone();
}
Ok(())
}

/// Run every plugin on one file's sides, returning the file's own
/// visibility.
pub(crate) fn run(
pub(crate) fn prepare(
&self,
file: &FileChange,
sides: &mut Pairing<protocol::Source>,
Expand Down
8 changes: 8 additions & 0 deletions src/plugin/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ fn call<R>(host: Host, call: impl FnOnce() -> anyhow::Result<R>) -> anyhow::Resu
struct Native(Box<dyn sdk::Instance>);

impl Runner for Native {
fn enrich(
&self,
host: Host,
file: &FileEntry,
sides: &SourceSides,
) -> anyhow::Result<Vec<diffr_plugin_sdk::Annotation>> {
call(host, || self.0.enrich(file, sides))
}
fn queries(&self, host: Host) -> anyhow::Result<Vec<diffr_plugin_sdk::QuerySource>> {
call(host, || self.0.queries())
}
Expand Down
Loading
Loading