use termesh_core::ProposalId;
use crate::change::{Assoc, ChangeSet, RangeEffect};
use crate::{ConflictReason, HunkState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyntaxKind {
Keyword,
StringLit,
Comment,
Number,
Type,
Function,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HunkSide {
Removed,
Added,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecorationClass {
Syntax(SyntaxKind),
Diagnostic(Severity),
Hunk {
proposal: ProposalId,
side: HunkSide,
state: HunkState,
},
Match {
current: bool,
},
}
impl DecorationClass {
fn is_derived(&self) -> bool {
matches!(
self,
DecorationClass::Syntax(_)
| DecorationClass::Diagnostic(_)
| DecorationClass::Match { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Decoration {
pub start: usize,
pub end: usize,
pub class: DecorationClass,
}
impl Decoration {
pub fn new(start: usize, end: usize, class: DecorationClass) -> Self {
Self { start, end, class }
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineDecoration {
pub start: usize,
pub end: usize,
pub class: DecorationClass,
}
#[derive(Debug, Default, Clone)]
pub struct DecorationSet {
items: Vec<Decoration>,
}
impl DecorationSet {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, decoration: Decoration) {
self.items.push(decoration);
}
pub fn iter(&self) -> impl Iterator<Item = &Decoration> {
self.items.iter()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn clear_syntax(&mut self) {
self.items.retain(|d| !matches!(d.class, DecorationClass::Syntax(_)));
}
pub fn clear_matches(&mut self) {
self.items.retain(|d| !matches!(d.class, DecorationClass::Match { .. }));
}
pub fn clear_diagnostics(&mut self) {
self.items.retain(|d| !matches!(d.class, DecorationClass::Diagnostic(_)));
}
pub fn clear_derived(&mut self) {
self.items.retain(|d| !d.class.is_derived());
}
pub fn remove_proposal(&mut self, proposal: ProposalId) {
self.items.retain(
|d| !matches!(d.class, DecorationClass::Hunk { proposal: p, .. } if p == proposal),
);
}
pub fn map(&mut self, changes: &ChangeSet) {
self.items.retain_mut(|d| {
let effect = changes.touches(d.start, d.end);
if let DecorationClass::Hunk { state, .. } = &mut d.class {
if let Some(reason) = ConflictReason::from_effect(effect) {
*state = HunkState::Conflicted(reason);
}
} else if effect != RangeEffect::Untouched {
return false;
}
d.start = changes.map_pos(d.start, Assoc::After);
d.end = changes.map_pos(d.end, Assoc::After);
true
});
}
pub fn for_line(&self, line_start: usize, line_end: usize) -> Vec<LineDecoration> {
let mut out: Vec<LineDecoration> = self
.items
.iter()
.filter(|d| overlaps(d, line_start, line_end))
.map(|d| LineDecoration {
start: d.start.clamp(line_start, line_end) - line_start,
end: d.end.clamp(line_start, line_end) - line_start,
class: d.class,
})
.collect();
out.sort_by_key(|d| (d.start, d.end));
out
}
}
fn overlaps(d: &Decoration, line_start: usize, line_end: usize) -> bool {
if d.is_empty() {
return d.start >= line_start && d.start <= line_end;
}
d.start < line_end && d.end > line_start
}
#[cfg(test)]
mod tests {
use super::*;
fn hunk(start: usize, end: usize, side: HunkSide) -> Decoration {
Decoration::new(
start,
end,
DecorationClass::Hunk { proposal: ProposalId::new(1), side, state: HunkState::Clean },
)
}
fn syntax(start: usize, end: usize) -> Decoration {
Decoration::new(start, end, DecorationClass::Syntax(SyntaxKind::Keyword))
}
fn state_of(set: &DecorationSet) -> Option<HunkState> {
set.iter().find_map(|d| match d.class {
DecorationClass::Hunk { state, .. } => Some(state),
_ => None,
})
}
#[test]
fn an_edit_before_a_decoration_shifts_it() {
let mut set = DecorationSet::new();
set.push(syntax(10, 20));
set.map(&ChangeSet::replace(40, 0, 0, "abc"));
let d = set.iter().next().unwrap();
assert_eq!((d.start, d.end), (13, 23));
}
#[test]
fn an_edit_after_a_decoration_leaves_it_alone() {
let mut set = DecorationSet::new();
set.push(syntax(10, 20));
set.map(&ChangeSet::replace(40, 30, 30, "abc"));
let d = set.iter().next().unwrap();
assert_eq!((d.start, d.end), (10, 20));
}
#[test]
fn a_disturbed_syntax_span_is_dropped() {
let mut set = DecorationSet::new();
set.push(syntax(10, 20));
set.map(&ChangeSet::replace(40, 12, 18, "x"));
assert!(set.is_empty(), "stale highlighting is worse than none");
}
#[test]
fn a_disturbed_hunk_is_kept_and_marked_conflicted() {
let mut set = DecorationSet::new();
set.push(hunk(10, 20, HunkSide::Removed));
set.map(&ChangeSet::replace(40, 12, 18, "mine"));
assert_eq!(set.len(), 1, "the human is mid-review; it must not vanish");
assert_eq!(state_of(&set), Some(HunkState::Conflicted(ConflictReason::AnchorDeleted)));
}
#[test]
fn typing_inside_a_hunk_conflicts_it_by_the_right_reason() {
let mut set = DecorationSet::new();
set.push(hunk(10, 20, HunkSide::Removed));
set.map(&ChangeSet::replace(40, 15, 15, "mine"));
assert_eq!(state_of(&set), Some(HunkState::Conflicted(ConflictReason::EditedInsideRange)));
}
#[test]
fn an_untouched_hunk_stays_clean_and_rides_forward() {
let mut set = DecorationSet::new();
set.push(hunk(10, 20, HunkSide::Removed));
set.map(&ChangeSet::replace(40, 0, 0, "xx"));
assert_eq!(state_of(&set), Some(HunkState::Clean));
let d = set.iter().next().unwrap();
assert_eq!((d.start, d.end), (12, 22));
}
#[test]
fn a_zero_width_insertion_anchor_rides_forward_too() {
let mut set = DecorationSet::new();
set.push(hunk(10, 10, HunkSide::Added));
set.map(&ChangeSet::replace(40, 0, 0, "abc"));
let d = set.iter().next().unwrap();
assert_eq!((d.start, d.end), (13, 13), "still zero-width, just moved");
}
#[test]
fn a_conflicted_hunk_does_not_silently_go_clean_again() {
let mut set = DecorationSet::new();
set.push(hunk(10, 20, HunkSide::Removed));
set.map(&ChangeSet::replace(40, 15, 15, "mine")); set.map(&ChangeSet::replace(44, 0, 0, "x"));
assert!(matches!(state_of(&set), Some(HunkState::Conflicted(_))));
}
#[test]
fn each_producer_clears_only_its_own_class() {
let mut set = DecorationSet::new();
set.push(syntax(0, 5));
set.push(Decoration::new(6, 8, DecorationClass::Match { current: true }));
set.push(hunk(10, 20, HunkSide::Removed));
set.clear_syntax();
assert_eq!(set.len(), 2, "the match and the hunk survive a re-parse");
set.clear_matches();
assert_eq!(set.len(), 1, "the hunk survives a new search");
assert!(matches!(set.iter().next().unwrap().class, DecorationClass::Hunk { .. }));
}
#[test]
fn clearing_derived_decorations_leaves_pending_hunks_alone() {
let mut set = DecorationSet::new();
set.push(syntax(0, 5));
set.push(Decoration::new(6, 8, DecorationClass::Diagnostic(Severity::Error)));
set.push(hunk(10, 20, HunkSide::Removed));
set.clear_derived();
assert_eq!(set.len(), 1);
assert!(matches!(set.iter().next().unwrap().class, DecorationClass::Hunk { .. }));
}
#[test]
fn a_resolved_proposal_takes_only_its_own_hunks() {
let mut set = DecorationSet::new();
set.push(hunk(0, 5, HunkSide::Removed));
set.push(Decoration::new(
10,
15,
DecorationClass::Hunk {
proposal: ProposalId::new(2),
side: HunkSide::Removed,
state: HunkState::Clean,
},
));
set.remove_proposal(ProposalId::new(1));
assert_eq!(set.len(), 1, "the other proposal's review is untouched");
}
#[test]
fn decorations_are_clipped_and_rebased_onto_their_line() {
let mut set = DecorationSet::new();
set.push(syntax(5, 14)); set.push(syntax(16, 30));
let spans = set.for_line(10, 20);
assert_eq!(spans.len(), 2);
assert_eq!((spans[0].start, spans[0].end), (0, 4));
assert_eq!((spans[1].start, spans[1].end), (6, 10));
}
#[test]
fn decorations_on_other_lines_are_excluded() {
let mut set = DecorationSet::new();
set.push(syntax(0, 5));
set.push(syntax(30, 35));
assert!(set.for_line(10, 20).is_empty());
}
#[test]
fn spans_come_back_in_order_so_the_renderer_can_walk_them() {
let mut set = DecorationSet::new();
set.push(syntax(18, 20));
set.push(syntax(10, 12));
set.push(syntax(14, 16));
let starts: Vec<usize> = set.for_line(10, 20).iter().map(|d| d.start).collect();
assert_eq!(starts, [0, 4, 8]);
}
#[test]
fn an_insertion_anchor_at_the_end_of_a_line_is_drawn_on_that_line() {
let mut set = DecorationSet::new();
set.push(hunk(20, 20, HunkSide::Added));
assert_eq!(set.for_line(10, 20).len(), 1, "belongs to the line it ends");
}
#[test]
fn a_decoration_touching_only_a_boundary_does_not_bleed_onto_the_next_line() {
let mut set = DecorationSet::new();
set.push(syntax(5, 10)); assert!(set.for_line(10, 20).is_empty());
}
}