diff options
-rw-r--r-- | src/suggestion.rs | 149 | ||||
-rw-r--r-- | tests/suggestion.rs | 34 | ||||
-rw-r--r-- | tests/suggestions.rs | 53 |
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()), + ]); +} |