summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/suggestion.rs149
-rw-r--r--tests/suggestion.rs34
-rw-r--r--tests/suggestions.rs53
3 files changed, 196 insertions, 40 deletions
diff --git a/src/suggestion.rs b/src/suggestion.rs
index bad9793..128d025 100644
--- a/src/suggestion.rs
+++ b/src/suggestion.rs
@@ -2,8 +2,9 @@
 // Copyright (c) Microsoft Corporation. All rights reserved.
 // Licensed under the MIT license.
 
-//! A Suggestion.
+//! Suggestion machinery.
 
+//use ::std::collections::HashSet;
 use ::std::ops::Range;
 
 /// A suggested editing operation.
@@ -11,12 +12,12 @@ use ::std::ops::Range;
 /// # Examples
 ///
 /// ```rust
-/// use iosonism::suggestion::Suggestion;
+/// use ::iosonism::suggestion::Suggestion;
 ///
 /// let s = Suggestion::new(3..3, "home".into());
 /// assert_eq!(s.apply("Go ".into()), "Go home");
 /// ```
-#[derive(Debug, PartialEq, Eq, Hash)]
+#[derive(Debug, PartialEq, Eq, Hash, Clone)]
 pub struct Suggestion {
     range: Range<usize>,
     text: String,
@@ -56,26 +57,132 @@ impl Suggestion {
         input
     }
 
-    /// Creates a new `Suggestion` by applying this `Suggestion` on the
-    /// `range` part of the given `input`.
+    /// Expands this suggestion to cover the whole given range.
+    ///
+    /// The text needed to cover the range will be taken from the given input.
     ///
     /// # Panics
     ///
-    /// May panic if this suggestion's range is outside the bounds given by
-    /// `range`. Panics if the given `range` is outside `input`'s bounds, or any
-    /// range is not on an UTF-8 boundary.
-    // FIXME: This function could really use a better description.
-    pub fn expand(&self, mut input: String, range: Range<usize>) -> Self {
-        // It is our understanding that both ranges must be within input.
-        // It's also our understanding that self.range must be within the passed
-        // range.
-        // So we just do our best to enforce those.
-        input.truncate(range.end);
-        input.drain(..range.start);
-        input.replace_range(
-            (self.range.start-range.start)..(self.range.end-range.start),
-            &self.text,
-        );
-        Self::new(range, input)
+    /// Panics if any range is not on an UTF-8 boundary. Panics if the given
+    /// range doesn't cover this suggestion's range, or is outside the input's
+    /// bounds.
+    ///
+    /// # Examples
+    ///
+    /// Basic usage:
+    ///
+    /// ```rust
+    /// use ::iosonism::suggestion::Suggestion;
+    ///
+    /// let mut s = Suggestion::new(7..7, "orld!".into());
+    /// s.expand("hello w", 6..7);
+    /// assert_eq!(s, Suggestion::new(6..7, "world!".into()));
+    /// ```
+    pub fn expand(&mut self, input: &str, range: Range<usize>) {
+        let input = &input[range.clone()];
+        self.text.insert_str(0, &input[..(self.range.start-range.start)]);
+        self.text.push_str(&input[(self.range.end-range.start)..]);
+        self.range = range;
+    }
+}
+
+/// Editing suggestions.
+#[derive(Debug, PartialEq, Eq, Hash, Clone)]
+pub struct Suggestions {
+    range: Range<usize>,
+    suggestions: Vec<Suggestion>,
+}
+
+/// [private] An empty `Suggestions`.
+const EMPTY: Suggestions = Suggestions { range: 0..0, suggestions: Vec::new() };
+
+impl Suggestions {
+    /// Returns the range these suggestions apply to.
+    pub fn get_range(&self) -> Range<usize> {
+        self.range.clone()
+    }
+
+    /// Returns the suggestions.
+    pub fn get_list(&self) -> &[Suggestion] {
+        &self.suggestions
+    }
+
+    /// Returns the suggestions.
+    pub fn take_list(self) -> Vec<Suggestion> {
+        self.suggestions
+    }
+
+    /// Returns `true` if there are no suggestions.
+    pub fn is_empty(&self) -> bool {
+        self.suggestions.is_empty()
+    }
+
+    /// Merges together multiple `Suggestions` for the command.
+    pub fn merge<I: IntoIterator<Item=Suggestions>>(
+        command: &str,
+        input: I,
+    ) -> Self {
+        // check if empty or single-element
+        let mut it = input.into_iter();
+        let first = it.next();
+        if first.is_none() {
+            return EMPTY
+        }
+        let first = first.unwrap();
+        let second = it.next();
+        if second.is_none() {
+            return first
+        }
+        let second = second.unwrap();
+
+        // combine everything back together and hope for the best
+        Self::create(command, first.take_list().into_iter().chain({
+            second.take_list()
+        }).chain(it.flat_map(|e| e.take_list())))
+    }
+
+    /// Creates a `Suggestions` from the individual suggestions for the command.
+    pub fn create<I: IntoIterator<Item=Suggestion>>(
+        command: &str,
+        suggestions: I,
+    ) -> Self {
+        let mut suggestions: Vec<_> = suggestions.into_iter().collect();
+        if suggestions.is_empty() {
+            return EMPTY
+        }
+
+        // find the largest range that spans all the suggestions.
+        let mut start = usize::MAX;
+        let mut end = 0;
+        for suggestion in suggestions.iter() {
+            start = ::std::cmp::min(suggestion.get_range().start, start);
+            end = ::std::cmp::max(suggestion.get_range().end, end);
+        }
+        let range = start..end;
+
+        // expand all suggestions.
+        for suggestion in suggestions.iter_mut() {
+            // cloning a range is basically free.
+            suggestion.expand(command, range.clone());
+        }
+
+        // brigadier uses a HashSet first for deduplication. we currently use
+        // a case-sensitive sort and then do the deduplication. we can probably
+        // do something better here, and maybe even be language aware?
+        // FIXME investigate
+        // note that by the time we're here, all suggestions have been expanded,
+        // so the ranges are all the same (and no longer relevant).
+        suggestions.sort_unstable_by(|a, b| {
+            ::std::cmp::Ord::cmp(a.get_text(), b.get_text())
+        });
+        suggestions.dedup();
+
+        Self {
+            range: range,
+            suggestions: suggestions,
+        }
     }
 }
+
+///// Helper for building suggestions.
+//pub struct SuggestionsBuilder<'a>(&'a str);
diff --git a/tests/suggestion.rs b/tests/suggestion.rs
index 0e1b9fe..5886f16 100644
--- a/tests/suggestion.rs
+++ b/tests/suggestion.rs
@@ -51,39 +51,35 @@ fn test_apply__replacement_everything() {
 
 #[test]
 fn test_expand__unchanged() {
-    let s = Suggestion::new(1..1, "oo".into());
-    assert_eq!(s.expand("f".into(), 1..1), s);
+    let mut s = Suggestion::new(1..1, "oo".into());
+    s.expand("f", 1..1);
+    assert_eq!(s, Suggestion::new(1..1, "oo".into()));
 }
 
 #[test]
 fn test_expand__left() {
-    let s = Suggestion::new(1..1, "oo".into());
-    assert_eq!(s.expand("f".into(), 0..1), Suggestion::new(0..1, "foo".into()));
+    let mut s = Suggestion::new(1..1, "oo".into());
+    s.expand("f", 0..1);
+    assert_eq!(s, Suggestion::new(0..1, "foo".into()));
 }
 
 #[test]
 fn test_expand__right() {
-    let s = Suggestion::new(0..0, "minecraft:".into());
-    assert_eq!(
-        s.expand("fish".into(), 0..4),
-        Suggestion::new(0..4, "minecraft:fish".into()),
-    );
+    let mut s = Suggestion::new(0..0, "minecraft:".into());
+    s.expand("fish", 0..4);
+    assert_eq!(s, Suggestion::new(0..4, "minecraft:fish".into()));
 }
 
 #[test]
 fn test_expand__both() {
-    let s = Suggestion::new(11..11, "minecraft:".into());
-    assert_eq!(
-        s.expand("give Steve fish_block".into(), 5..21),
-        Suggestion::new(5..21, "Steve minecraft:fish_block".into()),
-    );
+    let mut s = Suggestion::new(11..11, "minecraft:".into());
+    s.expand("give Steve fish_block", 5..21);
+    assert_eq!(s, Suggestion::new(5..21, "Steve minecraft:fish_block".into()));
 }
 
 #[test]
 fn test_expand__replacement() {
-    let s = Suggestion::new(6..11, "strangers".into());
-    assert_eq!(
-        s.expand("Hello world!".into(), 0..12),
-        Suggestion::new(0..12, "Hello strangers!".into()),
-    );
+    let mut s = Suggestion::new(6..11, "strangers".into());
+    s.expand("Hello world!", 0..12);
+    assert_eq!(s, Suggestion::new(0..12, "Hello strangers!".into()));
 }
diff --git a/tests/suggestions.rs b/tests/suggestions.rs
new file mode 100644
index 0000000..d7f3631
--- /dev/null
+++ b/tests/suggestions.rs
@@ -0,0 +1,53 @@
+// Copyright (c) 2021 Soni L.
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+// because we wanna use double underscore (__) for test names
+#![allow(non_snake_case)]
+
+use ::iosonism::suggestion::Suggestion;
+use ::iosonism::suggestion::Suggestions;
+
+#[test]
+fn test_merge__empty() {
+    let merged = Suggestions::merge("foo b", Vec::new());
+    assert!(merged.is_empty());
+}
+
+#[test]
+fn test_merge__single() {
+    let suggestions = Suggestions::create("foo b", vec![
+        Suggestion::new(5..5, "ar".into()),
+    ]);
+    let merged = Suggestions::merge("foo b", vec![suggestions.clone()]);
+    assert_eq!(merged, suggestions);
+}
+
+#[test]
+fn test_merge__multiple() {
+    // it is possible the equivalent of this test fails sometimes in brigdier,
+    // but it should never fail here.
+    // also we use ASCII/UTF-8 ordering rather than locale-sensitive ordering.
+    let a = Suggestions::create("foo b", vec![
+        Suggestion::new(5..5, "ar".into()),
+        Suggestion::new(5..5, "az".into()),
+        Suggestion::new(5..5, "Ar".into()),
+    ]);
+    let b = Suggestions::create("foo b", vec![
+        Suggestion::new(4..5, "foo".into()),
+        Suggestion::new(4..5, "qux".into()),
+        Suggestion::new(4..5, "apple".into()),
+        Suggestion::new(4..5, "Bar".into()),
+    ]);
+    let merged = Suggestions::merge("foo b", vec![a, b]);
+    assert_eq!(merged.get_range(), 4..5);
+    assert_eq!(merged.take_list(), vec![
+        Suggestion::new(4..5, "Bar".into()),
+        Suggestion::new(4..5, "apple".into()),
+        Suggestion::new(4..5, "bAr".into()),
+        Suggestion::new(4..5, "bar".into()),
+        Suggestion::new(4..5, "baz".into()),
+        Suggestion::new(4..5, "foo".into()),
+        Suggestion::new(4..5, "qux".into()),
+    ]);
+}