Panics · P22 Collaboration: marks outside their text stay out under the owner's index; accept answers None; zero reactions and stale masked offsets hold
This commit is contained in:
1 parent
19b0b2997a
commit
9eab2019a8
9 files changed
+215
-35
No files matched your search
@@ -261,7 +261,10 @@ const BASE: &str = "A lift blends color toward white. Half a lift sits midway be
|
|||||||
fn applied(base: &str, suggestions: Vec<Suggestion>) -> String {
|
fn applied(base: &str, suggestions: Vec<Suggestion>) -> String {
|
||||||
let (mut text, mut left) = (base.to_string(), suggestions);
|
let (mut text, mut left) = (base.to_string(), suggestions);
|
||||||
while !left.is_empty() {
|
while !left.is_empty() {
|
||||||
(text, left) = accept(&text, &left, 0);
|
let Some(taken) = accept(&text, &left, 0) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
(text, left) = taken;
|
||||||
}
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
@@ -306,11 +309,16 @@ pub fn changes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
let pending = suggested(&base_text, field.read(cx).text(), &you());
|
let pending = suggested(&base_text, field.read(cx).text(), &you());
|
||||||
match decision {
|
match decision {
|
||||||
Decision::Accept(ix) => {
|
Decision::Accept(ix) => {
|
||||||
set(&decided, accept(&base_text, &pending, ix).0, cx)
|
if let Some((text, _)) = accept(&base_text, &pending, ix) {
|
||||||
|
set(&decided, text, cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Decision::AcceptAll => set(&decided, applied(&base_text, pending), cx),
|
Decision::AcceptAll => set(&decided, applied(&base_text, pending), cx),
|
||||||
Decision::Reject(ix) => {
|
Decision::Reject(ix) => {
|
||||||
let mut rest = pending;
|
let mut rest = pending;
|
||||||
|
if ix >= rest.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
rest.remove(ix);
|
rest.remove(ix);
|
||||||
let text = applied(&base_text, rest);
|
let text = applied(&base_text, rest);
|
||||||
field.update(cx, |input, cx| input.set_text(text, cx));
|
field.update(cx, |input, cx| input.set_text(text, cx));
|
||||||
|
|||||||
+43
-8
@@ -26,6 +26,7 @@ pub struct AnnotatedText {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
text: SharedString,
|
text: SharedString,
|
||||||
annotations: Rc<Vec<Annotation>>,
|
annotations: Rc<Vec<Annotation>>,
|
||||||
|
kept: Rc<Vec<usize>>,
|
||||||
picked: Option<usize>,
|
picked: Option<usize>,
|
||||||
on_pick: Option<Pick>,
|
on_pick: Option<Pick>,
|
||||||
}
|
}
|
||||||
@@ -38,15 +39,25 @@ impl AnnotatedText {
|
|||||||
annotations: impl IntoIterator<Item = Annotation>,
|
annotations: impl IntoIterator<Item = Annotation>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let text = text.into();
|
let text = text.into();
|
||||||
let annotations: Vec<Annotation> = annotations.into_iter().collect();
|
let (kept, annotations): (Vec<usize>, Vec<Annotation>) = annotations
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, annotation)| {
|
||||||
|
let inside = text.get(annotation.range.clone()).is_some();
|
||||||
|
if !inside {
|
||||||
|
log::error!(
|
||||||
|
"annotated text: {:?} lies outside the text; left out",
|
||||||
|
annotation.range
|
||||||
|
);
|
||||||
|
}
|
||||||
|
inside
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
for annotation in &annotations {
|
for annotation in &annotations {
|
||||||
let range = &annotation.range;
|
|
||||||
assert!(
|
assert!(
|
||||||
range.start < range.end
|
!annotation.range.is_empty(),
|
||||||
&& range.end <= text.len()
|
"annotation {:?} is empty",
|
||||||
&& text.is_char_boundary(range.start)
|
annotation.range
|
||||||
&& text.is_char_boundary(range.end),
|
|
||||||
"annotation {range:?} lies outside the text"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
@@ -59,6 +70,7 @@ impl AnnotatedText {
|
|||||||
id: id.into(),
|
id: id.into(),
|
||||||
text,
|
text,
|
||||||
annotations: Rc::new(annotations),
|
annotations: Rc::new(annotations),
|
||||||
|
kept: Rc::new(kept),
|
||||||
picked: None,
|
picked: None,
|
||||||
on_pick: None,
|
on_pick: None,
|
||||||
}
|
}
|
||||||
@@ -78,6 +90,9 @@ impl AnnotatedText {
|
|||||||
|
|
||||||
impl RenderOnce for AnnotatedText {
|
impl RenderOnce for AnnotatedText {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
if let Some(picked) = self.picked.filter(|picked| !self.kept.contains(picked)) {
|
||||||
|
log::error!("annotated text: annotation {picked} is not shown; none picked");
|
||||||
|
}
|
||||||
let thickness = cx.theme().underline_thickness();
|
let thickness = cx.theme().underline_thickness();
|
||||||
let highlights: Vec<(Range<usize>, HighlightStyle)> = self
|
let highlights: Vec<(Range<usize>, HighlightStyle)> = self
|
||||||
.annotations
|
.annotations
|
||||||
@@ -85,7 +100,7 @@ impl RenderOnce for AnnotatedText {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, annotation)| {
|
.map(|(ix, annotation)| {
|
||||||
let color = annotation.author.color(cx);
|
let color = annotation.author.color(cx);
|
||||||
let wash = if self.picked == Some(ix) {
|
let wash = if self.picked == Some(self.kept[ix]) {
|
||||||
PICKED
|
PICKED
|
||||||
} else {
|
} else {
|
||||||
WASH
|
WASH
|
||||||
@@ -123,6 +138,7 @@ impl RenderOnce for AnnotatedText {
|
|||||||
});
|
});
|
||||||
match self.on_pick {
|
match self.on_pick {
|
||||||
Some(pick) => text.on_click(ranges, move |ix, window, cx| {
|
Some(pick) => text.on_click(ranges, move |ix, window, cx| {
|
||||||
|
let ix = self.kept[ix];
|
||||||
log::info!("annotated text: picked {ix}");
|
log::info!("annotated text: picked {ix}");
|
||||||
pick(ix, window, cx)
|
pick(ix, window, cx)
|
||||||
}),
|
}),
|
||||||
@@ -130,3 +146,22 @@ impl RenderOnce for AnnotatedText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{AnnotatedText, Annotation};
|
||||||
|
use crate::collab::Peer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_annotation_outside_the_text_is_left_out() {
|
||||||
|
let note = |range| Annotation {
|
||||||
|
range,
|
||||||
|
author: Peer::new("ada", "Ada", 0),
|
||||||
|
note: None,
|
||||||
|
};
|
||||||
|
let text = AnnotatedText::new("text", "éabc", [note(1..2), note(2..3)]);
|
||||||
|
assert_eq!(text.annotations.len(), 1);
|
||||||
|
assert_eq!(text.annotations[0].range, 2..3);
|
||||||
|
assert_eq!(*text.kept, [1], "a pick names the owner's index");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,11 @@ impl PresenceAvatars {
|
|||||||
|
|
||||||
impl RenderOnce for PresenceAvatars {
|
impl RenderOnce for PresenceAvatars {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
if let Some(following) = &self.following
|
||||||
|
&& !self.peers.iter().any(|peer| &peer.key == following)
|
||||||
|
{
|
||||||
|
log::error!("presence: {following} left; following none");
|
||||||
|
}
|
||||||
let size = AvatarSize::Sm;
|
let size = AvatarSize::Sm;
|
||||||
let rest = self.peers.len().saturating_sub(self.max);
|
let rest = self.peers.len().saturating_sub(self.max);
|
||||||
let shown: Vec<AnyElement> = self
|
let shown: Vec<AnyElement> = self
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ pub fn toggled(reactions: &[Reaction], emoji: &str) -> Vec<Reaction> {
|
|||||||
.position(|reaction| reaction.emoji.as_ref() == emoji)
|
.position(|reaction| reaction.emoji.as_ref() == emoji)
|
||||||
{
|
{
|
||||||
Some(ix) if next[ix].mine => {
|
Some(ix) if next[ix].mine => {
|
||||||
next[ix].count -= 1;
|
if next[ix].count == 0 {
|
||||||
|
log::error!("reactions: {emoji} counts none yet is yours; it comes off");
|
||||||
|
}
|
||||||
|
next[ix].count = next[ix].count.saturating_sub(1);
|
||||||
next[ix].mine = false;
|
next[ix].mine = false;
|
||||||
if next[ix].count == 0 {
|
if next[ix].count == 0 {
|
||||||
next.remove(ix);
|
next.remove(ix);
|
||||||
@@ -191,6 +194,11 @@ impl RenderOnce for Reactions {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yours_at_no_count_comes_off() {
|
||||||
|
assert_eq!(toggled(&[reaction("👍", 0, true)], "👍"), []);
|
||||||
|
}
|
||||||
|
|
||||||
fn reaction(emoji: &str, count: usize, mine: bool) -> Reaction {
|
fn reaction(emoji: &str, count: usize, mine: bool) -> Reaction {
|
||||||
Reaction {
|
Reaction {
|
||||||
emoji: emoji.to_string().into(),
|
emoji: emoji.to_string().into(),
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ impl RenderOnce for CommentSidebar {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
if let Some(active) = &self.active
|
||||||
|
&& !self.threads.iter().any(|thread| &thread.key == active)
|
||||||
|
{
|
||||||
|
log::error!("comment sidebar: thread {active} is gone; none active");
|
||||||
|
}
|
||||||
let shown: Vec<Thread> = self
|
let shown: Vec<Thread> = self
|
||||||
.threads
|
.threads
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
+69
-2
@@ -3,11 +3,11 @@ use gpui::{
|
|||||||
TestAppContext, VisualTestContext, Window, div,
|
TestAppContext, VisualTestContext, Window, div,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{FollowMode, Peer, Reply};
|
use super::{Decision, FollowMode, Peer, Reply, Suggestion, TrackChanges};
|
||||||
use crate::{
|
use crate::{
|
||||||
forms,
|
forms,
|
||||||
forms::{Input, TextInput},
|
forms::{Input, TextInput},
|
||||||
theme::Theme,
|
theme::{ActiveTheme, ControlSize, Theme},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A field outside a followed view and one inside it, with what they asked: replies sent, follows stopped.
|
/// A field outside a followed view and one inside it, with what they asked: replies sent, follows stopped.
|
||||||
@@ -145,3 +145,70 @@ fn without_resolving_one_list_takes_keyboard_picks(cx: &mut TestAppContext) {
|
|||||||
let picked = board.read_with(cx, |board, _| board.picked.clone());
|
let picked = board.read_with(cx, |board, _| board.picked.clone());
|
||||||
assert_eq!(picked, vec![gpui::SharedString::from("a")]);
|
assert_eq!(picked, vec![gpui::SharedString::from("a")]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Two suggestions whose first falls inside a character once the text changes, and the decisions asked.
|
||||||
|
struct Changing {
|
||||||
|
changed: bool,
|
||||||
|
decided: Vec<Decision>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Changing {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let owner = cx.entity();
|
||||||
|
let suggestion = |range, text: &str| Suggestion {
|
||||||
|
author: Peer::new("ada", "Ada", 0),
|
||||||
|
range,
|
||||||
|
text: text.to_string().into(),
|
||||||
|
};
|
||||||
|
let (base, second) = match self.changed {
|
||||||
|
false => ("abc", 1..2),
|
||||||
|
true => ("éabc", 2..3),
|
||||||
|
};
|
||||||
|
let pending = [suggestion(0..1, "X"), suggestion(second, "B")];
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
TrackChanges::new("track", base, pending).on_decide(move |decision, _, cx| {
|
||||||
|
owner.update(cx, |owner, _| owner.decided.push(decision))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_press_on_a_suggestion_left_out_decides_nothing(cx: &mut TestAppContext) {
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let (view, cx) = cx.add_window_view(|_, _| Changing {
|
||||||
|
changed: false,
|
||||||
|
decided: Vec::new(),
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
let row = cx
|
||||||
|
.debug_bounds("suggestion 0")
|
||||||
|
.expect("the first suggestion");
|
||||||
|
let at = cx.update(|window, cx| {
|
||||||
|
let button = cx
|
||||||
|
.theme()
|
||||||
|
.control_height(ControlSize::Sm)
|
||||||
|
.to_pixels(window.rem_size());
|
||||||
|
let gap = gpui::rems(0.5).to_pixels(window.rem_size());
|
||||||
|
gpui::point(row.right() - button - gap - button / 2.0, row.center().y)
|
||||||
|
});
|
||||||
|
cx.simulate_click(at, gpui::Modifiers::none());
|
||||||
|
assert_eq!(
|
||||||
|
view.read_with(cx, |view, _| view.decided.clone()),
|
||||||
|
[Decision::Accept(0)]
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_down(at, gpui::MouseButton::Left, gpui::Modifiers::none());
|
||||||
|
view.update(cx, |view, cx| {
|
||||||
|
view.changed = true;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.simulate_mouse_up(at, gpui::MouseButton::Left, gpui::Modifiers::none());
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
view.read_with(cx, |view, _| view.decided.clone()),
|
||||||
|
[Decision::Accept(0)]
|
||||||
|
);
|
||||||
|
}
|
||||||
+57
-19
@@ -158,9 +158,23 @@ pub fn suggested(base: &str, edited: &str, author: &Peer) -> Vec<Suggestion> {
|
|||||||
found
|
found
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `base` with suggestion `ix` taken, and the others moved to match.
|
/// `base` with suggestion `ix` taken, and the others moved to match; none when `ix` or its range left `base`.
|
||||||
pub fn accept(base: &str, suggestions: &[Suggestion], ix: usize) -> (String, Vec<Suggestion>) {
|
pub fn accept(
|
||||||
let taken = &suggestions[ix];
|
base: &str,
|
||||||
|
suggestions: &[Suggestion],
|
||||||
|
ix: usize,
|
||||||
|
) -> Option<(String, Vec<Suggestion>)> {
|
||||||
|
let Some(taken) = suggestions.get(ix) else {
|
||||||
|
log::error!("track changes: no suggestion {ix} of {}", suggestions.len());
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if base.get(taken.range.clone()).is_none() {
|
||||||
|
log::error!(
|
||||||
|
"track changes: suggestion {:?} lies outside the text",
|
||||||
|
taken.range
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let text = format!(
|
let text = format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
&base[..taken.range.start],
|
&base[..taken.range.start],
|
||||||
@@ -181,7 +195,7 @@ pub fn accept(base: &str, suggestions: &[Suggestion], ix: usize) -> (String, Vec
|
|||||||
suggestion
|
suggestion
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(text, rest)
|
Some((text, rest))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a suggestion does, in a line: replace, add or delete, with the words.
|
/// What a suggestion does, in a line: replace, add or delete, with the words.
|
||||||
@@ -206,6 +220,7 @@ pub struct TrackChanges {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
base: SharedString,
|
base: SharedString,
|
||||||
suggestions: Vec<Suggestion>,
|
suggestions: Vec<Suggestion>,
|
||||||
|
kept: Vec<usize>,
|
||||||
on_decide: Option<OnDecide>,
|
on_decide: Option<OnDecide>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +232,22 @@ impl TrackChanges {
|
|||||||
suggestions: impl IntoIterator<Item = Suggestion>,
|
suggestions: impl IntoIterator<Item = Suggestion>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let base = base.into();
|
let base = base.into();
|
||||||
let suggestions: Vec<Suggestion> = suggestions.into_iter().collect();
|
let (kept, suggestions): (Vec<usize>, Vec<Suggestion>) = suggestions
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, suggestion)| {
|
||||||
|
let inside = base.get(suggestion.range.clone()).is_some();
|
||||||
|
if !inside {
|
||||||
|
log::error!(
|
||||||
|
"track changes: {:?} lies outside the text; left out",
|
||||||
|
suggestion.range
|
||||||
|
);
|
||||||
|
}
|
||||||
|
inside
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
for suggestion in &suggestions {
|
for suggestion in &suggestions {
|
||||||
let range = &suggestion.range;
|
let range = &suggestion.range;
|
||||||
assert!(
|
|
||||||
range.end <= base.len()
|
|
||||||
&& base.is_char_boundary(range.start)
|
|
||||||
&& base.is_char_boundary(range.end),
|
|
||||||
"suggestion {range:?} lies outside the text"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
!(range.is_empty() && suggestion.text.is_empty()),
|
!(range.is_empty() && suggestion.text.is_empty()),
|
||||||
"suggestion at {range:?} changes nothing"
|
"suggestion at {range:?} changes nothing"
|
||||||
@@ -241,6 +263,7 @@ impl TrackChanges {
|
|||||||
id: id.into(),
|
id: id.into(),
|
||||||
base,
|
base,
|
||||||
suggestions,
|
suggestions,
|
||||||
|
kept,
|
||||||
on_decide: None,
|
on_decide: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,8 +320,10 @@ impl RenderOnce for TrackChanges {
|
|||||||
};
|
};
|
||||||
let pending = !self.suggestions.is_empty() && self.on_decide.is_some();
|
let pending = !self.suggestions.is_empty() && self.on_decide.is_some();
|
||||||
let rows = self.suggestions.iter().enumerate().map(|(ix, suggestion)| {
|
let rows = self.suggestions.iter().enumerate().map(|(ix, suggestion)| {
|
||||||
let id = |what: &str| (self.id.clone(), format!("{what}-{ix}"));
|
let at = self.kept[ix];
|
||||||
|
let id = |what: &str| (self.id.clone(), format!("{what}-{at}"));
|
||||||
div()
|
div()
|
||||||
|
.debug_selector(move || format!("suggestion {at}"))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -327,14 +352,14 @@ impl RenderOnce for TrackChanges {
|
|||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.size(ControlSize::Sm)
|
.size(ControlSize::Sm)
|
||||||
.tooltip("Accept")
|
.tooltip("Accept")
|
||||||
.on_click(decide(Decision::Accept(ix))),
|
.on_click(decide(Decision::Accept(at))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new(id("reject"), IconName::X)
|
IconButton::new(id("reject"), IconName::X)
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.size(ControlSize::Sm)
|
.size(ControlSize::Sm)
|
||||||
.tooltip("Reject")
|
.tooltip("Reject")
|
||||||
.on_click(decide(Decision::Reject(ix))),
|
.on_click(decide(Decision::Reject(at))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -430,9 +455,9 @@ mod tests {
|
|||||||
found,
|
found,
|
||||||
[suggestion(2..6, "tint"), suggestion(14..14, "every ")]
|
[suggestion(2..6, "tint"), suggestion(14..14, "every ")]
|
||||||
);
|
);
|
||||||
let (text, rest) = accept("A lift blends color toward white.", &found, 0);
|
let (text, rest) = accept("A lift blends color toward white.", &found, 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
accept(&text, &rest, 0).0,
|
accept(&text, &rest, 0).unwrap().0,
|
||||||
"A tint blends every color toward white."
|
"A tint blends every color toward white."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -441,16 +466,29 @@ mod tests {
|
|||||||
fn accepting_one_moves_the_rest() {
|
fn accepting_one_moves_the_rest() {
|
||||||
let base = "A lift blends color.";
|
let base = "A lift blends color.";
|
||||||
let all = [suggestion(2..6, "tint"), suggestion(14..14, "every ")];
|
let all = [suggestion(2..6, "tint"), suggestion(14..14, "every ")];
|
||||||
let (text, rest) = accept(base, &all, 0);
|
let (text, rest) = accept(base, &all, 0).unwrap();
|
||||||
assert_eq!(text, "A tint blends color.");
|
assert_eq!(text, "A tint blends color.");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rest,
|
rest,
|
||||||
[suggestion(14..14, "every ")],
|
[suggestion(14..14, "every ")],
|
||||||
"same length, same place"
|
"same length, same place"
|
||||||
);
|
);
|
||||||
let (text, rest) = accept(base, &[suggestion(2..6, "shade of"), all[1].clone()], 0);
|
let (text, rest) =
|
||||||
|
accept(base, &[suggestion(2..6, "shade of"), all[1].clone()], 0).unwrap();
|
||||||
assert_eq!(text, "A shade of blends color.");
|
assert_eq!(text, "A shade of blends color.");
|
||||||
let (text, _) = accept(&text, &rest, 0);
|
let (text, _) = accept(&text, &rest, 0).unwrap();
|
||||||
assert_eq!(text, "A shade of blends every color.");
|
assert_eq!(text, "A shade of blends every color.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_suggestion_gone_or_outside_the_text_is_not_taken() {
|
||||||
|
let base = "A lift blends.";
|
||||||
|
let inside = suggestion(2..6, "tint");
|
||||||
|
assert_eq!(accept(base, std::slice::from_ref(&inside), 1), None);
|
||||||
|
assert_eq!(accept("A", std::slice::from_ref(&inside), 0), None);
|
||||||
|
let stale = suggestion(0..40, "");
|
||||||
|
let track = TrackChanges::new("track", base, [stale, inside.clone()]);
|
||||||
|
assert_eq!(track.suggestions, [inside]);
|
||||||
|
assert_eq!(track.kept, [1], "decisions name the owner's index");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+15
-2
@@ -69,9 +69,12 @@ pub(crate) fn from_utf16(text: &str, offset: usize) -> usize {
|
|||||||
text.len()
|
text.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display offset of a content offset when each grapheme shows as `bullet`.
|
/// Display offset of a content offset when each grapheme shows as `bullet`; an offset the text lost clamps into it.
|
||||||
pub(crate) fn masked_offset(text: &str, offset: usize, bullet: char) -> usize {
|
pub(crate) fn masked_offset(text: &str, offset: usize, bullet: char) -> usize {
|
||||||
text[..offset].graphemes(true).count() * bullet.len_utf8()
|
text[..text.floor_char_boundary(offset)]
|
||||||
|
.graphemes(true)
|
||||||
|
.count()
|
||||||
|
* bullet.len_utf8()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Content offset of a display offset in masked text.
|
/// Content offset of a display offset in masked text.
|
||||||
@@ -182,6 +185,16 @@ mod tests {
|
|||||||
assert_eq!(masked_offset(text, 6, '•'), 9);
|
assert_eq!(masked_offset(text, 6, '•'), 9);
|
||||||
assert_eq!(unmasked_offset(text, 9, '•'), 6);
|
assert_eq!(unmasked_offset(text, 9, '•'), 6);
|
||||||
assert_eq!(unmasked_offset(text, 12, '•'), text.len());
|
assert_eq!(unmasked_offset(text, 12, '•'), text.len());
|
||||||
|
assert_eq!(
|
||||||
|
masked_offset("a", 2, '•'),
|
||||||
|
3,
|
||||||
|
"an offset past the text clamps"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
masked_offset(text, 4, '•'),
|
||||||
|
6,
|
||||||
|
"inside a character, before it"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+2
-1
@@ -36,7 +36,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
|
|||||||
- [x] P19 Git — `src/git`
|
- [x] P19 Git — `src/git`
|
||||||
- [x] P20 Debug — `src/debug`
|
- [x] P20 Debug — `src/debug`
|
||||||
- [x] P21 Documents — `src/documents`
|
- [x] P21 Documents — `src/documents`
|
||||||
- [ ] P22 Collaboration — `src/collab`
|
- [x] P22 Collaboration — `src/collab`
|
||||||
- [ ] P23 AI Chat — `src/chat`
|
- [ ] P23 AI Chat — `src/chat`
|
||||||
- [ ] P24 Agent — `src/agent`
|
- [ ] P24 Agent — `src/agent`
|
||||||
- [ ] P25 Generative — `src/generative`
|
- [ ] P25 Generative — `src/generative`
|
||||||
@@ -87,3 +87,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
|||||||
- P19: a blame that lags its code draws every line, bare where no owner fits; `git::resolve` returns `Option` for a conflict gone from the text. Kept: diff groups, merge regions, similar's pinned resolutions. Review: 2 rounds.
|
- P19: a blame that lags its code draws every line, bare where no owner fits; `git::resolve` returns `Option` for a conflict gone from the text. Kept: diff groups, merge regions, similar's pinned resolutions. Review: 2 rounds.
|
||||||
- P20: a hex selection off the bytes and a stack frame past the stack select none; a flame focus keeps what the profile holds; the timeline's wheel reports only finite, positive ranges from finite factors; a dump's last byte must have an address; flame paints log. Kept: disassembly patterns, `zoomed`'s contract. Review: 3 rounds.
|
- P20: a hex selection off the bytes and a stack frame past the stack select none; a flame focus keeps what the profile holds; the timeline's wheel reports only finite, positive ranges from finite factors; a dump's last byte must have an address; flame paints log. Kept: disassembly patterns, `zoomed`'s contract. Review: 3 rounds.
|
||||||
- P21: block editor listeners painted for a block, field, cell or kind since gone log and change nothing, a dropped picture drag leaves undo alone; a visual block press or edit over text the owner replaced is dropped; nested images parse; a version or find hit past its list selects none. Review: 4 rounds.
|
- P21: block editor listeners painted for a block, field, cell or kind since gone log and change nothing, a dropped picture drag leaves undo alone; a visual block press or edit over text the owner replaced is dropped; nested images parse; a version or find hit past its list selects none. Review: 4 rounds.
|
||||||
|
- P22: annotations and suggestions outside their text log and stay out, picks and decisions keeping the owner's index; `accept` returns `Option`; a gone pick, active thread or followed peer logs; yours at no count comes off; a masked field clamps a stale offset, so remote cursors and selections draw. Kept: a thread's comments, presence max, one list's order, empty marks, constant role and mode tables. Review: 3 rounds.
|
||||||
Reference in new issue
Block a user