view helix-term/src/handlers/completion/word.rs @ 6833:a1c38f138388 draft

(13206) word completion (grafted from 1efa919fb2b9841da497a9f29b492bf4389858f3)
author Michael Davis <mcarsondavis@gmail.com>
date Wed, 14 May 2025 10:23:43 -0400
parents
children
line wrap: on
line source

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,
                    ..
                })
            )
        });
    }
}