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



                                                            
                         
 
                                  






                                  
                                           



                                                 
                                            






































                                                                               


                                                                              


                





















































































































                                                                                

     


                                             
// 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<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 {
    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);