Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/handlers/completion/word.rs')
| -rw-r--r-- | helix-term/src/handlers/completion/word.rs | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/helix-term/src/handlers/completion/word.rs b/helix-term/src/handlers/completion/word.rs new file mode 100644 index 00000000..aa204bf8 --- /dev/null +++ b/helix-term/src/handlers/completion/word.rs @@ -0,0 +1,134 @@ +use std::{borrow::Cow, sync::Arc}; + +use helix_core::{ + self as core, chars::char_is_word, completion::CompletionProvider, movement, Transaction, +}; +use helix_event::TaskHandle; +use helix_stdx::rope::RopeSliceExt as _; +use helix_view::{ + document::SavePoint, handlers::completion::ResponseContext, Document, Editor, ViewId, +}; + +use super::{request::TriggerKind, CompletionItem, CompletionItems, CompletionResponse, Trigger}; + +const COMPLETION_KIND: &str = "word"; + +pub(super) fn completion( + editor: &Editor, + trigger: Trigger, + handle: TaskHandle, + savepoint: Arc<SavePoint>, +) -> Option<impl FnOnce() -> CompletionResponse> { + if !doc!(editor).word_completion_enabled() { + return None; + } + let config = editor.config().word_completion; + let doc_config = doc!(editor) + .language_config() + .and_then(|config| config.word_completion); + let trigger_length = doc_config + .and_then(|c| c.trigger_length) + .unwrap_or(config.trigger_length) + .get() as usize; + + let (view, doc) = current_ref!(editor); + let rope = doc.text().clone(); + let word_index = editor.handlers.word_index().clone(); + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone(); + let pos = selection.primary().cursor(text); + + let cursor = movement::move_prev_word_start(text, core::Range::point(pos), 1); + if cursor.head == pos { + return None; + } + if trigger.kind != TriggerKind::Manual + && text + .slice(cursor.head..) + .graphemes() + .take(trigger_length) + .take_while(|g| g.chars().all(char_is_word)) + .count() + != trigger_length + { + return None; + } + + let typed_word_range = cursor.head..pos; + let typed_word = text.slice(typed_word_range.clone()); + let edit_diff = if typed_word + .char(typed_word.len_chars().saturating_sub(1)) + .is_whitespace() + { + 0 + } else { + typed_word.len_chars() + }; + + if handle.is_canceled() { + return None; + } + + let future = move || { + let text = rope.slice(..); + let typed_word: Cow<_> = text.slice(typed_word_range).into(); + let items = word_index + .matches(&typed_word) + .into_iter() + .filter(|word| word.as_str() != typed_word.as_ref()) + .map(|word| { + let transaction = Transaction::change_by_selection(&rope, &selection, |range| { + let cursor = range.cursor(text); + (cursor - edit_diff, cursor, Some((&word).into())) + }); + CompletionItem::Other(core::CompletionItem { + transaction, + label: word.into(), + kind: Cow::Borrowed(COMPLETION_KIND), + documentation: None, + provider: CompletionProvider::Word, + }) + }) + .collect(); + + CompletionResponse { + items: CompletionItems::Other(items), + provider: CompletionProvider::Word, + context: ResponseContext { + is_incomplete: false, + priority: 0, + savepoint, + }, + } + }; + + Some(future) +} + +pub(super) fn retain_valid_completions( + trigger: Trigger, + doc: &Document, + view_id: ViewId, + items: &mut Vec<CompletionItem>, +) { + if trigger.kind == TriggerKind::Manual { + return; + } + + let text = doc.text().slice(..); + let cursor = doc.selection(view_id).primary().cursor(text); + if text + .get_char(cursor.saturating_sub(1)) + .is_some_and(|ch| ch.is_whitespace()) + { + items.retain(|item| { + !matches!( + item, + CompletionItem::Other(core::CompletionItem { + provider: CompletionProvider::Word, + .. + }) + ) + }); + } +} |