// Copyright (c) 2021 Soni L. // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. // Documentation and comments licensed under CC BY-SA 4.0. //! Suggestion machinery. use ::std::future::Future; use ::std::ops::Range; use ::std::pin::Pin; /// 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 { // brigadier also supports "tooltips". FIXME revisit this. 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> { // brigadier throws an inputLowerCase/remainingLowerCase in here. // given we're using 'a, it feels *weird* to use String with it. // so for now we don't bother. FIXME revisit this? // also note that brigadier exists for minecraft, whereas we're porting it // for use with a debugger. we can assume our users are familiar with and/or // prefer case-sensitivity. input: &'a str, start: usize, remaining: &'a str, result: Vec, } impl<'a> SuggestionsBuilder<'a> { /// Creates a new `SuggestionsBuilder`. pub fn new(input: &'a str, start: usize) -> Self { Self { input: input, start: start, remaining: &input[start..], result: Vec::new(), } } /// Returns the full input. pub fn get_input(&self) -> &'a str { self.input } /// Returns the start position. pub fn get_start(&self) -> usize { self.start } /// Returns the remaining input. pub fn get_remaining(&self) -> &'a str { self.remaining } /// Builds the final `Suggestions`, draining this builder. pub fn drain_build(&mut self) -> Suggestions { Suggestions::create(self.input, ::std::mem::take(&mut self.result)) } /// Builds the final `Suggestions`, as a `Future`, draining this builder. pub fn drain_build_future( &mut self, ) -> Pin + Send + 'a>> { let input = self.input; let result = ::std::mem::take(&mut self.result); Box::pin(async move { Suggestions::create(input, result) }) } /// Adds a new suggestion. pub fn suggest(&mut self, text: String) -> &mut Self { if text != self.remaining { self.result.push({ Suggestion::new(self.start..self.input.len(), text) }); } self } /// Adds all the suggestions from the given SuggestionBuilder, draining it. pub fn add_drain(&mut self, other: &mut SuggestionsBuilder) -> &mut Self { self.result.extend(std::mem::take(&mut other.result)); self } /// Creates a *new* `SuggestionsBuilder`, with no suggestions, starting at /// the given position. pub fn starting_from(&self, start: usize) -> Self { Self::new(self.input, start) } /// Creates a *new* `SuggestionsBuilder`, with no suggestions, starting from /// the same position as this one. pub fn restart(&self) -> Self { self.starting_from(self.start) } }