Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/handlers/spelling.rs')
| -rw-r--r-- | helix-view/src/handlers/spelling.rs | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/helix-view/src/handlers/spelling.rs b/helix-view/src/handlers/spelling.rs new file mode 100644 index 00000000..49fd7812 --- /dev/null +++ b/helix-view/src/handlers/spelling.rs @@ -0,0 +1,128 @@ +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<SpellingEvent>, + pub requests: HashMap<DocumentId, TaskController>, + pub loading_dictionaries: HashSet<SpellingLanguage>, +} + +impl SpellingHandler { + pub fn new(event_tx: Sender<SpellingEvent>) -> 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<BoxFuture<'static, anyhow::Result<Vec<Action>>>> { + 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()) + } +} |