use std::{ borrow::Cow, collections::{HashMap, HashSet}, }; use futures_util::{future::BoxFuture, FutureExt as _}; use helix_core::{SpellingLanguage, Tendril, Transaction}; use helix_event::{TaskController, TaskHandle}; use tokio::sync::mpsc::Sender; use crate::{diagnostic::DiagnosticProvider, Action, DocumentId, Editor}; const ACTION_PRIORITY: u8 = 0; #[derive(Debug)] pub struct SpellingHandler { pub event_tx: Sender, pub requests: HashMap, pub loading_dictionaries: HashSet, } impl SpellingHandler { pub fn new(event_tx: Sender) -> Self { Self { event_tx, requests: Default::default(), loading_dictionaries: Default::default(), } } pub fn open_request(&mut self, document: DocumentId) -> TaskHandle { let mut controller = TaskController::new(); let handle = controller.restart(); self.requests.insert(document, controller); handle } } #[derive(Debug)] pub enum SpellingEvent { /* DictionaryUpdated { word: String, language: SpellingLanguage, }, */ DictionaryLoaded { language: SpellingLanguage }, DocumentOpened { doc: DocumentId }, DocumentChanged { doc: DocumentId }, } impl Editor { pub(crate) fn spelling_actions( &self, ) -> Option>>> { let (view, doc) = current_ref!(self); let doc_id = doc.id(); let view_id = view.id; let language = doc.spelling_language()?; // TODO: consider fixes for all selections? let range = doc.selection(view_id).primary(); let text = doc.text().clone(); let dictionary = self.dictionaries.get(&language)?.clone(); // TODO: can do this faster with partition_point + take_while let selected_diagnostics: Vec<_> = doc .diagnostics() .iter() .filter(|d| { range.overlaps(&helix_core::Range::new(d.range.start, d.range.end)) && d.inner.provider == DiagnosticProvider::Spelling }) .map(|d| d.range) .collect(); let future = tokio::task::spawn_blocking(move || { let text = text.slice(..); let dictionary = dictionary.read(); let mut suggest_buffer = Vec::new(); selected_diagnostics .into_iter() .flat_map(|range| { suggest_buffer.clear(); let word = Cow::from(text.slice(range.start..range.end)); dictionary.suggest(&word, &mut suggest_buffer); let mut actions = Vec::with_capacity(suggest_buffer.len() + 1); actions.extend( suggest_buffer.drain(..).map(|suggestion| { Action::new( format!("Replace '{word}' with '{suggestion}'"), ACTION_PRIORITY, move |editor| { let doc = doc_mut!(editor, &doc_id); let view = view_mut!(editor, view_id); let transaction = Transaction::change( doc.text(), [(range.start, range.end, Some(Tendril::from(suggestion.as_str())))].into_iter(), ); doc.apply(&transaction, view_id); doc.append_changes_to_history(view); // TODO: get rid of the diagnostic for this word. }, ) }) ); let word = word.to_string(); actions.push(Action::new( format!("Add '{word}' to dictionary '{language}'"), ACTION_PRIORITY, move |editor| { let Some(dictionary) = editor.dictionaries.get(&language) else { log::error!("Failed to add '{word}' to dictionary '{language}' because the dictionary does not exist"); return; }; // TODO: fire an event? let mut dictionary = dictionary.write(); if let Err(err) = dictionary.add(&word) { log::error!("Failed to add '{word}' to dictionary '{language}': {err}"); } } )); actions }) .collect() }); Some(async move { Ok(future.await?) }.boxed()) } }