Skip to content
Draft
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
111 changes: 105 additions & 6 deletions crates/diffr-plugin-sdk/src/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! side keeps its leaf's ids. The second piece takes a fresh `id` on each
//! side and a fresh `alignment_id` shared by the two, and its
//! `fold_state_id` is the `id` of the lhs piece, or of its only piece.
//! Pieces keep the leaf's tags and visibility and the `changed` spans on
//! Pieces keep the leaf's tags and visibility and both change/search spans on
//! their lines. A fold, or the file, cannot be cut.
//! - `JoinFolds { regions }` needs two or more region ids. Each side wraps the
//! ones it holds, which must be two or more consecutive siblings under one
Expand All @@ -24,10 +24,12 @@
//! so is an id no side holds.
//! - `LinkFoldState { regions }` needs two or more region ids. Every region
//! in any of their fold states, on either side, takes the first region's
//! `fold_state_id` and whether it starts collapsed.
//! `fold_state_id` and whether it starts collapsed. A linked state containing
//! search highlights stays open.
//! - `SetCollapsed { region, collapsed }` sets whether every region sharing
//! the region's `fold_state_id` starts collapsed, on both sides: they open
//! and close together. On [`ROOT`] it sets whether the file starts hidden.
//! Collapse requests that would conceal search highlights leave it open.
//! - `SetLabel { region, label }` sets the label of that region alone, or of
//! the file on [`ROOT`]; `None` clears it.
//! - `SetTags { region, tags }` replaces that region's tags. The file's tags
Expand All @@ -38,7 +40,9 @@
//! [`ROOT`]) when a plugin's moves begin, and fresh `alignment_id`s above the
//! largest leaf `alignment_id`; each is handed out in the order the moves
//! need them, lhs before rhs.
use crate::tree::{walk, walk_mut, Node, Pairing, Region, Source};
use crate::tree::{
has_search_highlights, highlights_in_states, walk, walk_mut, Node, Pairing, Region, Source,
};
use crate::types::{Cut, Move, Position, Range, Visibility, ROOT};
use anyhow::{bail, ensure};
use std::collections::BTreeSet;
Expand Down Expand Up @@ -68,11 +72,16 @@ impl Applier {
Move::JoinFolds(regions) => join(sides, &regions, &mut self.fresh),
Move::LinkFoldState(regions) => link(sides, &regions),
Move::SetCollapsed((ROOT, collapsed)) => {
file.collapsed = collapsed;
file.collapsed = collapsed
&& !sides
.sides()
.iter()
.any(|source| source.regions.iter().any(has_search_highlights));
Ok(())
}
Move::SetCollapsed((region, collapsed)) => {
let state = region_of(sides, region)?.fold_state_id;
let collapsed = collapsed && !highlights_in_states(sides, &[region]);
for tree in trees(sides) {
walk_mut(tree, &mut |region| {
if region.fold_state_id == state {
Expand Down Expand Up @@ -116,6 +125,7 @@ pub fn apply(

fn trees(sides: &mut Pairing<Source>) -> Vec<&mut Vec<Region>> {
match sides {
Pairing::Same { source } => vec![&mut source.regions],
Pairing::Both { lhs, rhs } => vec![&mut lhs.regions, &mut rhs.regions],
Pairing::LeftOnly { lhs } => vec![&mut lhs.regions],
Pairing::RightOnly { rhs } => vec![&mut rhs.regions],
Expand All @@ -124,6 +134,7 @@ fn trees(sides: &mut Pairing<Source>) -> Vec<&mut Vec<Region>> {

pub fn trees_ref(sides: &Pairing<Source>) -> Vec<&[Region]> {
match sides {
Pairing::Same { source } => vec![&source.regions],
Pairing::Both { lhs, rhs } => vec![&lhs.regions, &rhs.regions],
Pairing::LeftOnly { lhs } => vec![&lhs.regions],
Pairing::RightOnly { rhs } => vec![&rhs.regions],
Expand Down Expand Up @@ -306,7 +317,12 @@ fn cut(sides: &mut Pairing<Source>, id: u32, offset: u32, fresh: &mut Fresh) ->
/// A leaf split at relative line `offset`. The second piece takes `id`,
/// `alignment_id` and `fold_state_id`.
fn split(leaf: Region, offset: u32, id: u32, alignment_id: u32, fold_state_id: u32) -> [Region; 2] {
let Node::Leaf { changed, .. } = &leaf.node else {
let Node::Leaf {
changed,
search_highlights,
..
} = &leaf.node
else {
unreachable!("only leaves are cut");
};
let boundary = Position {
Expand All @@ -328,6 +344,11 @@ fn split(leaf: Region, offset: u32, id: u32, alignment_id: u32, fold_state_id: u
.copied()
.filter(|span| lines.contains(&span.line))
.collect(),
search_highlights: search_highlights
.iter()
.copied()
.filter(|span| lines.contains(&span.line))
.collect(),
},
}
};
Expand Down Expand Up @@ -366,7 +387,10 @@ fn check_regions(ids: &[u32], what: &str) -> anyhow::Result<()> {
fn link(sides: &mut Pairing<Source>, ids: &[u32]) -> anyhow::Result<()> {
check_regions(ids, "a link")?;
let first = region_of(sides, ids[0])?;
let (state, collapsed) = (first.fold_state_id, first.visibility.collapsed);
let (state, collapsed) = (
first.fold_state_id,
first.visibility.collapsed && !highlights_in_states(sides, ids),
);
let states = ids
.iter()
.map(|id| Ok(region_of(sides, *id)?.fold_state_id))
Expand Down Expand Up @@ -462,6 +486,7 @@ mod tests {
visibility: Visibility::default(),
node: Node::Leaf {
alignment_id: alignment,
search_highlights: Vec::new(),
changed: changed
.iter()
.map(|&line| Span {
Expand Down Expand Up @@ -807,4 +832,78 @@ mod tests {
assert_eq!(lhs.regions[0].visibility, Visibility::default());
assert_eq!(rhs.regions[0].tags, ["mine:tag"]);
}
#[test]
fn cuts_preserve_each_sides_search_spans_through_record_roundtrips() {
let mut left = leaf(1, 7, 10, 16, &[11]);
let right = in_state(leaf(2, 7, 20, 26, &[]), 1);
if let Node::Leaf {
search_highlights, ..
} = &mut left.node
{
*search_highlights = vec![
Span {
line: 11,
start_column: 2,
end_column: 7,
},
Span {
line: 13,
start_column: 0,
end_column: 5,
},
];
}
let mut sides = both(vec![left], vec![right]);
run(vec![Move::Cut(Cut { region: 1, at: 3 })], &mut sides).unwrap();
let (lhs, rhs) = sides_of(&sides);
let spans = |region: &Region| match &region.node {
Node::Leaf {
search_highlights, ..
} => search_highlights.clone(),
_ => panic!("expected leaf"),
};
assert_eq!(spans(&lhs.regions[0])[0].line, 11);
assert_eq!(spans(&lhs.regions[1])[0].line, 13);
assert!(rhs.regions.iter().all(|r| spans(r).is_empty()));
assert_eq!(Source::from_record(&lhs.to_record()).unwrap(), *lhs);
assert_eq!(Source::from_record(&rhs.to_record()).unwrap(), *rhs);
}

#[test]
fn later_moves_cannot_hide_highlights_through_files_links_or_ancestors() {
let mut matched = leaf(1, 0, 0, 2, &[]);
if let Node::Leaf {
search_highlights, ..
} = &mut matched.node
{
search_highlights.push(Span {
line: 1,
start_column: 0,
end_column: 3,
});
}
let mut sides = both(
vec![fold(3, false, vec![matched]), leaf(4, 2, 2, 4, &[2])],
vec![],
);
let visibility = run(
vec![
Move::SetCollapsed((4, true)),
Move::LinkFoldState(vec![4, 3]),
Move::SetCollapsed((4, true)),
Move::SetCollapsed((ROOT, true)),
Move::JoinFolds(vec![3, 4]),
Move::SetCollapsed((5, true)),
Move::SetLabel((5, Some("summary".into()))),
Move::SetTags((5, vec!["test:tag".into()])),
],
&mut sides,
)
.unwrap();
assert!(!visibility.collapsed);
walk(&sides.lhs().unwrap().regions, &mut |region| {
assert!(!region.visibility.collapsed)
});
assert!(has_search_highlights(&sides.lhs().unwrap().regions[0]));
}
}
5 changes: 3 additions & 2 deletions crates/diffr-plugin-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ pub use anyhow;
pub use draft::Draft;
use serde::de::DeserializeOwned;
pub use tree::{
before_and_after_ids, docstring_of, has_tag, is_fold, line_count, one_sided, path_to,
siblings_of, sides_with_other_ids, walk, walk_mut, Node, OtherSide, Pairing, Region, Source,
before_and_after_ids, docstring_of, has_search_highlights, has_tag, highlights_in_states,
is_fold, line_count, one_sided, path_to, 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,
Expand Down
47 changes: 46 additions & 1 deletion crates/diffr-plugin-sdk/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::collections::BTreeSet;
/// Which sides a thing exists on.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pairing<T> {
/// One unchanged source, with highlights on its regions. Never an addition or deletion.
Same { source: T },
Both { lhs: T, rhs: T },
LeftOnly { lhs: T },
RightOnly { rhs: T },
Expand All @@ -19,20 +21,23 @@ impl<T> Pairing<T> {
lhs: f(lhs),
rhs: f(rhs),
},
Self::Same { source } => Pairing::Same { source: f(source) },
Self::LeftOnly { lhs } => Pairing::LeftOnly { lhs: f(lhs) },
Self::RightOnly { rhs } => Pairing::RightOnly { rhs: f(rhs) },
}
}

pub fn lhs(&self) -> Option<&T> {
match self {
Self::Same { source } => Some(source),
Self::Both { lhs, .. } | Self::LeftOnly { lhs } => Some(lhs),
Self::RightOnly { .. } => None,
}
}

pub fn rhs(&self) -> Option<&T> {
match self {
Self::Same { source } => Some(source),
Self::Both { rhs, .. } | Self::RightOnly { rhs } => Some(rhs),
Self::LeftOnly { .. } => None,
}
Expand All @@ -41,6 +46,7 @@ impl<T> Pairing<T> {
/// Every side that exists, lhs first.
pub fn sides(&self) -> Vec<&T> {
match self {
Self::Same { source } => vec![source],
Self::Both { lhs, rhs } => vec![lhs, rhs],
Self::LeftOnly { lhs } => vec![lhs],
Self::RightOnly { rhs } => vec![rhs],
Expand All @@ -50,6 +56,7 @@ impl<T> Pairing<T> {
/// Every side that exists, lhs first.
pub fn sides_mut(&mut self) -> Vec<&mut T> {
match self {
Self::Same { source } => vec![source],
Self::Both { lhs, rhs } => vec![lhs, rhs],
Self::LeftOnly { lhs } => vec![lhs],
Self::RightOnly { rhs } => vec![rhs],
Expand All @@ -73,6 +80,7 @@ pub enum Node {
Leaf {
alignment_id: u32,
changed: Vec<Span>,
search_highlights: Vec<Span>,
},
Fold {
children: Vec<Region>,
Expand All @@ -89,6 +97,36 @@ impl Region {
}
}

/// Whether collapsing this region would conceal a search match.
pub fn has_search_highlights(region: &Region) -> bool {
match &region.node {
Node::Leaf {
search_highlights, ..
} => !search_highlights.is_empty(),
Node::Fold { children } => children.iter().any(has_search_highlights),
}
}

/// Whether any of these regions' linked collapse states contains a match.
/// Include a body's docstring when deciding whether it can be summarized.
pub fn highlights_in_states(sides: &Pairing<Source>, ids: &[u32]) -> bool {
let mut states = BTreeSet::new();
for source in sides.sides() {
walk(&source.regions, &mut |region| {
if ids.contains(&region.id) {
states.insert(region.fold_state_id);
}
});
}
sides.sides().iter().any(|source| {
let mut found = false;
walk(&source.regions, &mut |region| {
found |= states.contains(&region.fold_state_id) && has_search_highlights(region);
});
found
})
}

/// One side of the diffed file as a tree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Source {
Expand All @@ -110,6 +148,7 @@ impl Source {
types::Kind::Leaf(leaf) => Node::Leaf {
alignment_id: leaf.alignment_id,
changed: leaf.changed.clone(),
search_highlights: leaf.search_highlights.clone(),
},
types::Kind::Fold => Node::Fold {
children: children(regions, region.id),
Expand Down Expand Up @@ -149,9 +188,11 @@ impl Source {
Node::Leaf {
alignment_id,
changed,
search_highlights,
} => types::Kind::Leaf(types::Leaf {
alignment_id: *alignment_id,
changed: changed.clone(),
search_highlights: search_highlights.clone(),
}),
Node::Fold { .. } => types::Kind::Fold,
};
Expand Down Expand Up @@ -182,6 +223,7 @@ impl Source {
/// on the way in, so a plugin is handed the pairing rather than the record.
pub fn sides(sides: &types::SourceSides) -> anyhow::Result<Pairing<Source>> {
Ok(match sides {
types::SourceSides::Same(source) => Pairing::Same { source: Source::from_record(source)? },
types::SourceSides::Both((lhs, rhs)) => Pairing::Both {
lhs: Source::from_record(lhs)?,
rhs: Source::from_record(rhs)?,
Expand Down Expand Up @@ -376,7 +418,7 @@ pub fn before_and_after_ids(sides: &Pairing<Source>) -> Option<(&Source, OtherSi
match sides {
Pairing::Both { lhs, rhs } => Some((lhs, OtherSide::of(&rhs.regions))),
Pairing::LeftOnly { lhs } => Some((lhs, OtherSide::default())),
Pairing::RightOnly { .. } => None,
Pairing::Same { .. } | Pairing::RightOnly { .. } => None,
}
}

Expand All @@ -387,6 +429,7 @@ pub fn sides_with_other_ids(sides: &Pairing<Source>) -> Vec<(&Source, OtherSide)
(lhs, OtherSide::of(&rhs.regions)),
(rhs, OtherSide::of(&lhs.regions)),
],
Pairing::Same { source } => vec![(source, OtherSide::of(&source.regions))],
Pairing::LeftOnly { lhs } => vec![(lhs, OtherSide::default())],
Pairing::RightOnly { rhs } => vec![(rhs, OtherSide::default())],
}
Expand Down Expand Up @@ -415,6 +458,7 @@ mod tests {
visibility: Visibility::default(),
node: Node::Leaf {
alignment_id: alignment,
search_highlights: Vec::new(),
changed: vec![],
},
};
Expand All @@ -428,6 +472,7 @@ mod tests {
fold_state_id: id + 100,
node: Node::Leaf {
alignment_id: alignment + 100,
search_highlights: Vec::new(),
changed: vec![],
},
..leaf.clone()
Expand Down
1 change: 1 addition & 0 deletions examples/plugins/fixtures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl Plugin for Fixtures {
// piece on this leaf's side takes the first fresh id either way.
let side = match &sides {
Pairing::Both { lhs, .. } | Pairing::LeftOnly { lhs } => lhs,
Pairing::Same { source } => source,
Pairing::RightOnly { rhs } => rhs,
};
let leaf = side
Expand Down
8 changes: 8 additions & 0 deletions plugins/context/queries/javascript.scm
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@
(#set! tag "context:scope"))
((return_statement) @fold
(#set! tag "context:scope"))

; Preserve both the try body boundary (`} catch`) and the end of the whole
; try/catch construct when its opening line is visible in context.
((try_statement) @fold
(#set! tag "context:scope"))
(try_statement
body: (statement_block "{" @fold.open "}" @fold.close) @fold
(#set! tag "context:body"))
Loading
Loading