summary refs log blame commit diff stats
path: root/src/suggestion.rs
blob: f8f4741aa0c84dfa576505e266667b6bcf5babd5 (plain) (tree)
1
2
3
4
5
6
7
8
9


                                                            
                                                          
 
                         
 
                          
                      
                    





                                  
                                           



                                                 
                                            






































                                                                               


                                                                              


                

























                                                                              
                                                              



























































































                                                                                
     




                                                                                
 
 



















































































                                                                                
// 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<usize>,
    text: String,
}

impl Suggestion {
    /// Creates a new `Suggestion` for `text` for the given `range`.
    pub fn new(range: Range<usize>, text: String) -> Self {
        Self {
            range: range,
            text: text,
        }
    }

    /// Returns the range associated with this suggestion.
    pub fn get_range(&self) -> Range<usize> {
        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<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 {
    // brigadier also supports "tooltips". FIXME revisit this.
    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,
        }
    }

    /// Creates an empty `Suggestions` future.
    pub fn empty<'a>() -> Pin<Box<dyn Future<Output=Suggestions> + Send + 'a>> {
        Box::pin(async { EMPTY })
    }
}

/// 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<Suggestion>,
}

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<Box<dyn Future<Output=Suggestions> + 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)
    }
}