use std::{
collections::BTreeMap,
ops::Range,
sync::{Arc, Mutex},
};
use duat_core::{
Ns,
buffer::Change,
context::Handle,
data::Pass,
hook::{self, BufferOpened, BufferUpdated},
text::{Point, RegexHaystack, Spacer, Strs, Text, txt},
};
use crate::widgets::completions::{CompletionsProvider, string_cmp};
static BUFFER_WORDS: Mutex<BTreeMap<Arc<str>, WordInfo>> = Mutex::new(BTreeMap::new());
pub struct WordCompletions;
impl CompletionsProvider for WordCompletions {
type Info = WordInfo;
fn default_fmt(entry: &str, info: &Self::Info) -> Text {
txt!(
"[word.Completions]{entry}[]{Spacer}[buffer.source.Completions]{}",
&info.source
)
}
fn matches(&mut self, text: &Text, caret: Point, prefix: &str) -> Vec<(Arc<str>, Self::Info)> {
let suffix = &text[text.search(r"\A\w*").range(caret..).next().unwrap()];
let mut matches: Vec<_> = BUFFER_WORDS
.lock()
.unwrap_or_else(|err| err.into_inner())
.iter()
.filter(|&(word, info)| {
if let Some(difference) = string_cmp(prefix, word) {
!(difference == 0 && info.count == 1 && &word[prefix.len()..] == suffix)
} else {
false
}
})
.map(|(entry, info)| (entry.clone(), info.clone()))
.collect();
matches.sort_by_key(|(entry, _)| (string_cmp(prefix, entry), entry.len()));
matches
}
fn get_start(&self, text: &Text, caret: Point) -> Option<usize> {
text.search(r"\w*\z")
.range(..caret)
.next_back()
.map(|r| r.start)
}
}
#[derive(Clone, Debug)]
pub struct WordInfo {
pub source: Arc<str>,
pub count: usize,
}
#[doc(hidden)]
pub(super) fn track_words() {
let ns = Ns::new();
hook::add::<BufferOpened>(move |pa, buffer| {
let mut words = BUFFER_WORDS.lock().unwrap();
let buf = buffer.read(pa);
let source: Arc<str> = buf.name().into();
for range in buf.text().search(r"\w{3,}") {
let word = buf.text()[range].to_string().into();
let info = words
.entry(word)
.or_insert_with(|| WordInfo { source: source.clone(), count: 0 });
info.count += 1;
}
});
hook::add::<BufferUpdated>(move |pa, buffer| update_counts(pa, buffer, ns));
}
fn update_counts(pa: &mut Pass, buffer: &Handle, ns: Ns) {
fn to_str<'a>(str: &'a str) -> impl Fn(Range<usize>) -> (Range<usize>, &'a str) {
|range| (range.clone(), &str[range])
}
let source: Arc<str> = buffer.read(pa).name().into();
let (text, moment) = {
let buf = buffer.read(pa);
(buf.text(), buf.moment_for(ns))
};
if moment.is_empty() {
return;
}
let surrounded = |match_r: Range<usize>, word: &str, change_str: &str, change: &Change| {
let prefix = if match_r.start == 0
&& let Some(text_range) = text.search(r"\w+\z").range(..change.start()).next_back()
{
&text[text_range]
} else {
Strs::empty()
};
if match_r.end == change_str.len()
&& let Some(text_range) = text.search(r"\A\w+").range(change.added_end()..).next()
{
format!("{prefix}{word}{}", &text[text_range])
} else {
format!("{prefix}{word}")
}
};
let mut buffer_words = BUFFER_WORDS.lock().unwrap();
let mut process_word = |word: &str, is_taken: bool| {
if word.chars().count() < 3 {
return;
}
match (buffer_words.get_mut(word), is_taken) {
(Some(info), false) => info.count += 1,
(None, false) => {
buffer_words.insert(word.into(), WordInfo { source: source.clone(), count: 1 });
}
(Some(info), true) if info.count > 1 => info.count -= 1,
(Some(_), true) => {
buffer_words.remove(word);
}
(None, true) => {}
}
};
for change in moment.iter() {
let added_str = change.added_str();
let added_words: Vec<_> = added_str.search(r"\w+").map(to_str(added_str)).collect();
let taken_str = change.taken_str();
let taken_words: Vec<_> = taken_str.search(r"\w+").map(to_str(taken_str)).collect();
for (is_taken, mut words, change_str) in [
(false, added_words, added_str),
(true, taken_words, taken_str),
] {
let suffix_range = change_str.len()..change_str.len();
let first = (words.len() > 1).then(|| words.remove(0));
match (words.pop(), first) {
(None, None) => {
let prefix = surrounded(0..0, "", change_str, &change);
let suffix = (!change_str.is_empty())
.then(|| surrounded(suffix_range, "", change_str, &change));
for word in [Some(prefix), suffix].into_iter().flatten() {
process_word(&word, is_taken);
}
}
(Some((last_range, last_word)), None) => {
let prefix =
(last_range.start != 0).then(|| surrounded(0..0, "", change_str, &change));
let suffix = (last_range.end != change_str.len())
.then(|| surrounded(suffix_range, "", change_str, &change));
let last_word = surrounded(last_range, last_word, change_str, &change);
for word in [prefix, Some(last_word), suffix].into_iter().flatten() {
process_word(&word, is_taken);
}
}
(Some((last_range, last_word)), Some((first_range, first_word))) => {
let prefix =
(first_range.start != 0).then(|| surrounded(0..0, "", change_str, &change));
let first_word = surrounded(first_range, first_word, change_str, &change);
let suffix = (last_range.end != change_str.len())
.then(|| surrounded(suffix_range, "", change_str, &change));
let last_word = surrounded(last_range, last_word, change_str, &change);
let on_sides = [
prefix.as_ref(),
Some(&first_word),
Some(&last_word),
suffix.as_ref(),
];
for word in on_sides
.into_iter()
.flatten()
.map(|word| word.as_str())
.chain(words.into_iter().map(|(_, word)| word))
{
process_word(word, is_taken);
}
}
(None, Some(_)) => unreachable!(),
}
}
}
}