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