// Copyright (c) 2021 Soni L. // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. //! Suggestion machinery. //use ::std::collections::HashSet; use ::std::ops::Range; /// A suggested editing operation. /// /// # Examples /// /// ```rust /// 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, Clone)] pub struct Suggestion { range: Range, text: String, } impl Suggestion { /// Creates a new `Suggestion` for `text` for the given `range`. pub fn new(range: Range, text: String) -> Self { Self { range: range, text: text, } } /// Returns the range associated with this suggestion. pub fn get_range(&self) -> Range { self.range.clone() } /// Returns the replacement text associated with this suggestion. pub fn get_text(&self) -> &str { &self.text } /// Applies this suggestion on the `input`. /// /// # Panics /// /// Panics if the range is outside the `input`'s bounds, or not on an UTF-8 /// boundary. pub fn apply(&self, mut input: String) -> String { // the use of String here actually has a performance reason: // either you already have a String, in which case this is fast, // or you need a String anyway, in which case it's your responsibility // to make your &str into one. input.replace_range(self.range.clone(), &self.text); 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 /// /// 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) { 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, suggestions: Vec, } /// [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 { self.range.clone() } /// Returns the suggestions. pub fn get_list(&self) -> &[Suggestion] { &self.suggestions } /// Returns the suggestions. pub fn take_list(self) -> Vec { 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>( 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>( 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);