2021-04-13 03:24:42 +00:00
|
|
|
use std::cmp::{max, min};
|
|
|
|
|
|
|
|
use glutin::event::ModifiersState;
|
|
|
|
|
|
|
|
use alacritty_terminal::grid::BidirectionalIterator;
|
2021-04-30 00:23:09 +00:00
|
|
|
use alacritty_terminal::index::{Boundary, Direction, Point};
|
|
|
|
use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
|
2021-04-14 01:05:20 +00:00
|
|
|
use alacritty_terminal::term::{Term, TermMode};
|
2021-03-01 19:50:39 +00:00
|
|
|
|
2021-04-03 23:52:44 +00:00
|
|
|
use crate::config::ui_config::{Hint, HintAction};
|
2021-04-13 03:24:42 +00:00
|
|
|
use crate::config::Config;
|
2021-03-01 19:50:39 +00:00
|
|
|
use crate::display::content::RegexMatches;
|
2021-04-13 03:24:42 +00:00
|
|
|
use crate::display::MAX_SEARCH_LINES;
|
2021-03-01 19:50:39 +00:00
|
|
|
|
|
|
|
/// Percentage of characters in the hints alphabet used for the last character.
|
|
|
|
const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
|
|
|
|
|
|
|
|
/// Keyboard regex hint state.
|
|
|
|
pub struct HintState {
|
|
|
|
/// Hint currently in use.
|
|
|
|
hint: Option<Hint>,
|
|
|
|
|
|
|
|
/// Alphabet for hint labels.
|
|
|
|
alphabet: String,
|
|
|
|
|
|
|
|
/// Visible matches.
|
|
|
|
matches: RegexMatches,
|
|
|
|
|
|
|
|
/// Key label for each visible match.
|
|
|
|
labels: Vec<Vec<char>>,
|
|
|
|
|
|
|
|
/// Keys pressed for hint selection.
|
|
|
|
keys: Vec<char>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl HintState {
|
|
|
|
/// Initialize an inactive hint state.
|
|
|
|
pub fn new<S: Into<String>>(alphabet: S) -> Self {
|
|
|
|
Self {
|
|
|
|
alphabet: alphabet.into(),
|
|
|
|
hint: Default::default(),
|
|
|
|
matches: Default::default(),
|
|
|
|
labels: Default::default(),
|
|
|
|
keys: Default::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check if a hint selection is in progress.
|
|
|
|
pub fn active(&self) -> bool {
|
|
|
|
self.hint.is_some()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Start the hint selection process.
|
|
|
|
pub fn start(&mut self, hint: Hint) {
|
|
|
|
self.hint = Some(hint);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Cancel the hint highlighting process.
|
|
|
|
fn stop(&mut self) {
|
|
|
|
self.matches.clear();
|
|
|
|
self.labels.clear();
|
|
|
|
self.keys.clear();
|
|
|
|
self.hint = None;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Update the visible hint matches and key labels.
|
|
|
|
pub fn update_matches<T>(&mut self, term: &Term<T>) {
|
|
|
|
let hint = match self.hint.as_mut() {
|
|
|
|
Some(hint) => hint,
|
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Find visible matches.
|
2021-04-13 03:24:42 +00:00
|
|
|
self.matches.0 = hint.regex.with_compiled(|regex| {
|
|
|
|
let mut matches = RegexMatches::new(term, regex);
|
|
|
|
|
|
|
|
// Apply post-processing and search for sub-matches if necessary.
|
|
|
|
if hint.post_processing {
|
|
|
|
matches
|
|
|
|
.drain(..)
|
|
|
|
.map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
|
|
|
|
.flatten()
|
|
|
|
.collect()
|
|
|
|
} else {
|
|
|
|
matches.0
|
|
|
|
}
|
|
|
|
});
|
2021-03-01 19:50:39 +00:00
|
|
|
|
|
|
|
// Cancel highlight with no visible matches.
|
|
|
|
if self.matches.is_empty() {
|
|
|
|
self.stop();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut generator = HintLabels::new(&self.alphabet, HINT_SPLIT_PERCENTAGE);
|
|
|
|
let match_count = self.matches.len();
|
|
|
|
let keys_len = self.keys.len();
|
|
|
|
|
|
|
|
// Get the label for each match.
|
|
|
|
self.labels.resize(match_count, Vec::new());
|
|
|
|
for i in (0..match_count).rev() {
|
|
|
|
let mut label = generator.next();
|
|
|
|
if label.len() >= keys_len && label[..keys_len] == self.keys[..] {
|
|
|
|
self.labels[i] = label.split_off(keys_len);
|
|
|
|
} else {
|
|
|
|
self.labels[i] = Vec::new();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handle keyboard input during hint selection.
|
2021-04-03 23:52:44 +00:00
|
|
|
pub fn keyboard_input<T>(&mut self, term: &Term<T>, c: char) -> Option<HintMatch> {
|
2021-03-01 19:50:39 +00:00
|
|
|
match c {
|
|
|
|
// Use backspace to remove the last character pressed.
|
|
|
|
'\x08' | '\x1f' => {
|
|
|
|
self.keys.pop();
|
|
|
|
},
|
2021-05-08 03:12:34 +00:00
|
|
|
// Cancel hint highlighting on ESC/Ctrl+c.
|
|
|
|
'\x1b' | '\x03' => self.stop(),
|
2021-03-01 19:50:39 +00:00
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the visible matches.
|
|
|
|
self.update_matches(term);
|
|
|
|
|
2021-04-03 23:52:44 +00:00
|
|
|
let hint = self.hint.as_ref()?;
|
2021-03-01 19:50:39 +00:00
|
|
|
|
|
|
|
// Find the last label starting with the input character.
|
|
|
|
let mut labels = self.labels.iter().enumerate().rev();
|
2021-04-03 23:52:44 +00:00
|
|
|
let (index, label) = labels.find(|(_, label)| !label.is_empty() && label[0] == c)?;
|
2021-03-01 19:50:39 +00:00
|
|
|
|
|
|
|
// Check if the selected label is fully matched.
|
|
|
|
if label.len() == 1 {
|
2021-04-03 23:52:44 +00:00
|
|
|
let bounds = self.matches[index].clone();
|
|
|
|
let action = hint.action.clone();
|
2021-03-01 19:50:39 +00:00
|
|
|
|
|
|
|
self.stop();
|
2021-04-03 23:52:44 +00:00
|
|
|
|
|
|
|
Some(HintMatch { action, bounds })
|
2021-03-01 19:50:39 +00:00
|
|
|
} else {
|
|
|
|
// Store character to preserve the selection.
|
|
|
|
self.keys.push(c);
|
2021-04-03 23:52:44 +00:00
|
|
|
|
|
|
|
None
|
2021-03-01 19:50:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Hint key labels.
|
|
|
|
pub fn labels(&self) -> &Vec<Vec<char>> {
|
|
|
|
&self.labels
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Visible hint regex matches.
|
|
|
|
pub fn matches(&self) -> &RegexMatches {
|
|
|
|
&self.matches
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Update the alphabet used for hint labels.
|
|
|
|
pub fn update_alphabet(&mut self, alphabet: &str) {
|
|
|
|
if self.alphabet != alphabet {
|
|
|
|
self.alphabet = alphabet.to_owned();
|
|
|
|
self.keys.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-03 23:52:44 +00:00
|
|
|
/// Hint match which was selected by the user.
|
2021-05-09 20:44:14 +00:00
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
2021-04-03 23:52:44 +00:00
|
|
|
pub struct HintMatch {
|
|
|
|
/// Action for handling the text.
|
|
|
|
pub action: HintAction,
|
|
|
|
|
|
|
|
/// Terminal range matching the hint.
|
|
|
|
pub bounds: Match,
|
|
|
|
}
|
|
|
|
|
2021-03-01 19:50:39 +00:00
|
|
|
/// Generator for creating new hint labels.
|
|
|
|
struct HintLabels {
|
|
|
|
/// Full character set available.
|
|
|
|
alphabet: Vec<char>,
|
|
|
|
|
|
|
|
/// Alphabet indices for the next label.
|
|
|
|
indices: Vec<usize>,
|
|
|
|
|
|
|
|
/// Point separating the alphabet's head and tail characters.
|
|
|
|
///
|
|
|
|
/// To make identification of the tail character easy, part of the alphabet cannot be used for
|
|
|
|
/// any other position.
|
|
|
|
///
|
|
|
|
/// All characters in the alphabet before this index will be used for the last character, while
|
|
|
|
/// the rest will be used for everything else.
|
|
|
|
split_point: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl HintLabels {
|
|
|
|
/// Create a new label generator.
|
|
|
|
///
|
|
|
|
/// The `split_ratio` should be a number between 0.0 and 1.0 representing the percentage of
|
|
|
|
/// elements in the alphabet which are reserved for the tail of the hint label.
|
|
|
|
fn new(alphabet: impl Into<String>, split_ratio: f32) -> Self {
|
|
|
|
let alphabet: Vec<char> = alphabet.into().chars().collect();
|
|
|
|
let split_point = ((alphabet.len() - 1) as f32 * split_ratio.min(1.)) as usize;
|
|
|
|
|
|
|
|
Self { indices: vec![0], split_point, alphabet }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the characters for the next label.
|
|
|
|
fn next(&mut self) -> Vec<char> {
|
|
|
|
let characters = self.indices.iter().rev().map(|index| self.alphabet[*index]).collect();
|
|
|
|
self.increment();
|
|
|
|
characters
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Increment the character sequence.
|
|
|
|
fn increment(&mut self) {
|
|
|
|
// Increment the last character; if it's not at the split point we're done.
|
|
|
|
let tail = &mut self.indices[0];
|
|
|
|
if *tail < self.split_point {
|
|
|
|
*tail += 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
*tail = 0;
|
|
|
|
|
|
|
|
// Increment all other characters in reverse order.
|
|
|
|
let alphabet_len = self.alphabet.len();
|
|
|
|
for index in self.indices.iter_mut().skip(1) {
|
|
|
|
if *index + 1 == alphabet_len {
|
|
|
|
// Reset character and move to the next if it's already at the limit.
|
|
|
|
*index = self.split_point + 1;
|
|
|
|
} else {
|
|
|
|
// If the character can be incremented, we're done.
|
|
|
|
*index += 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extend the sequence with another character when nothing could be incremented.
|
|
|
|
self.indices.push(self.split_point + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-13 03:24:42 +00:00
|
|
|
/// Check if there is a hint highlighted at the specified point.
|
|
|
|
pub fn highlighted_at<T>(
|
|
|
|
term: &Term<T>,
|
|
|
|
config: &Config,
|
|
|
|
point: Point,
|
|
|
|
mouse_mods: ModifiersState,
|
|
|
|
) -> Option<HintMatch> {
|
2021-04-14 01:05:20 +00:00
|
|
|
let mouse_mode = term.mode().intersects(TermMode::MOUSE_MODE);
|
|
|
|
|
2021-04-13 03:24:42 +00:00
|
|
|
config.ui_config.hints.enabled.iter().find_map(|hint| {
|
|
|
|
// Check if all required modifiers are pressed.
|
2021-04-14 01:05:20 +00:00
|
|
|
let highlight = hint.mouse.map_or(false, |mouse| {
|
|
|
|
mouse.enabled
|
|
|
|
&& mouse_mods.contains(mouse.mods.0)
|
|
|
|
&& (!mouse_mode || mouse_mods.contains(ModifiersState::SHIFT))
|
|
|
|
});
|
|
|
|
if !highlight {
|
2021-04-13 03:24:42 +00:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
hint.regex.with_compiled(|regex| {
|
|
|
|
// Setup search boundaries.
|
|
|
|
let mut start = term.line_search_left(point);
|
|
|
|
start.line = max(start.line, point.line - MAX_SEARCH_LINES);
|
|
|
|
let mut end = term.line_search_right(point);
|
|
|
|
end.line = min(end.line, point.line + MAX_SEARCH_LINES);
|
|
|
|
|
2021-04-30 00:23:09 +00:00
|
|
|
// Function to verify that the specified point is inside the match.
|
|
|
|
let at_point = |rm: &Match| *rm.end() >= point && *rm.start() <= point;
|
2021-04-13 03:24:42 +00:00
|
|
|
|
|
|
|
// Check if there's any match at the specified point.
|
2021-04-30 00:23:09 +00:00
|
|
|
let mut iter = RegexIter::new(start, end, Direction::Right, term, regex);
|
|
|
|
let regex_match = iter.find(at_point)?;
|
2021-04-13 03:24:42 +00:00
|
|
|
|
|
|
|
// Apply post-processing and search for sub-matches if necessary.
|
|
|
|
let regex_match = if hint.post_processing {
|
|
|
|
HintPostProcessor::new(term, regex, regex_match).find(at_point)
|
|
|
|
} else {
|
|
|
|
Some(regex_match)
|
|
|
|
};
|
|
|
|
|
|
|
|
regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Iterator over all post-processed matches inside an existing hint match.
|
|
|
|
struct HintPostProcessor<'a, T> {
|
|
|
|
/// Regex search DFAs.
|
|
|
|
regex: &'a RegexSearch,
|
|
|
|
|
|
|
|
/// Terminal reference.
|
|
|
|
term: &'a Term<T>,
|
|
|
|
|
|
|
|
/// Next hint match in the iterator.
|
|
|
|
next_match: Option<Match>,
|
|
|
|
|
|
|
|
/// Start point for the next search.
|
|
|
|
start: Point,
|
|
|
|
|
|
|
|
/// End point for the hint match iterator.
|
|
|
|
end: Point,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, T> HintPostProcessor<'a, T> {
|
|
|
|
/// Create a new iterator for an unprocessed match.
|
|
|
|
fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self {
|
2021-11-11 19:04:25 +00:00
|
|
|
let mut post_processor = Self {
|
|
|
|
next_match: None,
|
|
|
|
start: *regex_match.start(),
|
|
|
|
end: *regex_match.end(),
|
|
|
|
term,
|
|
|
|
regex,
|
|
|
|
};
|
2021-04-13 03:24:42 +00:00
|
|
|
|
|
|
|
// Post-process the first hint match.
|
2021-11-11 19:04:25 +00:00
|
|
|
post_processor.next_processed_match(regex_match);
|
2021-04-13 03:24:42 +00:00
|
|
|
|
|
|
|
post_processor
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Apply some hint post processing heuristics.
|
|
|
|
///
|
|
|
|
/// This will check the end of the hint and make it shorter if certain characters are determined
|
|
|
|
/// to be unlikely to be intentionally part of the hint.
|
|
|
|
///
|
|
|
|
/// This is most useful for identifying URLs appropriately.
|
2021-11-11 19:04:25 +00:00
|
|
|
fn hint_post_processing(&self, regex_match: &Match) -> Option<Match> {
|
2021-04-13 03:24:42 +00:00
|
|
|
let mut iter = self.term.grid().iter_from(*regex_match.start());
|
|
|
|
|
|
|
|
let mut c = iter.cell().c;
|
|
|
|
|
|
|
|
// Truncate uneven number of brackets.
|
|
|
|
let end = *regex_match.end();
|
|
|
|
let mut open_parents = 0;
|
|
|
|
let mut open_brackets = 0;
|
|
|
|
loop {
|
|
|
|
match c {
|
|
|
|
'(' => open_parents += 1,
|
|
|
|
'[' => open_brackets += 1,
|
|
|
|
')' => {
|
|
|
|
if open_parents == 0 {
|
|
|
|
iter.prev();
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
open_parents -= 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
']' => {
|
|
|
|
if open_brackets == 0 {
|
|
|
|
iter.prev();
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
open_brackets -= 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
if iter.point() == end {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
match iter.next() {
|
|
|
|
Some(indexed) => c = indexed.cell.c,
|
|
|
|
None => break,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Truncate trailing characters which are likely to be delimiters.
|
|
|
|
let start = *regex_match.start();
|
|
|
|
while iter.point() != start {
|
|
|
|
if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
match iter.prev() {
|
|
|
|
Some(indexed) => c = indexed.cell.c,
|
|
|
|
None => break,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 19:04:25 +00:00
|
|
|
if start > iter.point() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(start..=iter.point())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Loop over submatches until a non-empty post-processed match is found.
|
|
|
|
fn next_processed_match(&mut self, mut regex_match: Match) {
|
|
|
|
self.next_match = loop {
|
|
|
|
if let Some(next_match) = self.hint_post_processing(®ex_match) {
|
|
|
|
self.start = next_match.end().add(self.term, Boundary::Grid, 1);
|
|
|
|
break Some(next_match);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.start = regex_match.start().add(self.term, Boundary::Grid, 1);
|
|
|
|
if self.start > self.end {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
match self.term.regex_search_right(self.regex, self.start, self.end) {
|
|
|
|
Some(rm) => regex_match = rm,
|
|
|
|
None => return,
|
|
|
|
}
|
|
|
|
};
|
2021-04-13 03:24:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, T> Iterator for HintPostProcessor<'a, T> {
|
|
|
|
type Item = Match;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
let next_match = self.next_match.take()?;
|
|
|
|
|
|
|
|
if self.start <= self.end {
|
|
|
|
if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) {
|
2021-11-11 19:04:25 +00:00
|
|
|
self.next_processed_match(rm);
|
2021-04-13 03:24:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(next_match)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-01 19:50:39 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-11-11 19:04:25 +00:00
|
|
|
use alacritty_terminal::index::{Column, Line};
|
|
|
|
use alacritty_terminal::term::test::mock_term;
|
|
|
|
|
2021-03-01 19:50:39 +00:00
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn hint_label_generation() {
|
|
|
|
let mut generator = HintLabels::new("0123", 0.5);
|
|
|
|
|
|
|
|
assert_eq!(generator.next(), vec!['0']);
|
|
|
|
assert_eq!(generator.next(), vec!['1']);
|
|
|
|
|
|
|
|
assert_eq!(generator.next(), vec!['2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '1']);
|
|
|
|
|
|
|
|
assert_eq!(generator.next(), vec!['2', '2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '3', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '3', '1']);
|
|
|
|
|
|
|
|
assert_eq!(generator.next(), vec!['2', '2', '2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '2', '2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '2', '3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '2', '3', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '3', '2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '3', '2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '3', '3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['2', '3', '3', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '2', '2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '2', '2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '2', '3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '2', '3', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '3', '2', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '3', '2', '1']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '3', '3', '0']);
|
|
|
|
assert_eq!(generator.next(), vec!['3', '3', '3', '1']);
|
|
|
|
}
|
2021-11-11 19:04:25 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn closed_bracket_does_not_result_in_infinite_iterator() {
|
|
|
|
let term = mock_term(" ) ");
|
|
|
|
|
|
|
|
let search = RegexSearch::new("[^/ ]").unwrap();
|
|
|
|
|
|
|
|
let count = HintPostProcessor::new(
|
|
|
|
&term,
|
|
|
|
&search,
|
|
|
|
Point::new(Line(0), Column(1))..=Point::new(Line(0), Column(1)),
|
|
|
|
)
|
|
|
|
.take(1)
|
|
|
|
.count();
|
|
|
|
|
|
|
|
assert_eq!(count, 0);
|
|
|
|
}
|
2021-03-01 19:50:39 +00:00
|
|
|
}
|