diff options
Diffstat (limited to 'src/suggestion.rs')
-rw-r--r-- | src/suggestion.rs | 149 |
1 files changed, 128 insertions, 21 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); |