Unnamed repository; edit this file 'description' to name the repository.
1
; inherits: _gjs,_typescript,ecma
='n45' href='#n45'>45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
use std::sync::Arc;

use helix_lsp::lsp;
use tokio::sync::mpsc::Sender;
use tokio::time::{Duration, Instant};

use helix_event::{send_blocking, AsyncHook, TaskController, TaskHandle};
use helix_view::Editor;

use super::LspCompletionItem;
use crate::handlers::completion::CompletionItem;
use crate::job;

/// A hook for resolving incomplete completion items.
///
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
///
/// > If computing full completion items is expensive, servers can additionally provide a
/// > handler for the completion item resolve request. ...
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
/// > in the `documentation` property for returned completion items since it is expensive
/// > to compute. When the item is selected in the user interface then a
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
/// > The returned completion item should have the documentation property filled in.
pub struct ResolveHandler {
    last_request: Option<Arc<LspCompletionItem>>,
    resolver: Sender<ResolveRequest>,
}

impl ResolveHandler {
    pub fn new() -> ResolveHandler {
        ResolveHandler {
            last_request: None,
            resolver: ResolveTimeout::default().spawn(),
        }
    }

    pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut LspCompletionItem) {
        if item.resolved {
            return;
        }
        // We consider an item to be fully resolved if it has non-empty, none-`None` details,
        // docs and additional text-edits. Ideally we could use `is_some` instead of this
        // check but some language servers send values like `Some([])` for additional text
        // edits although the items need to be resolved. This is probably a consequence of
        // how `null` works in the JavaScript world.
        let is_resolved = item
            .item
            .documentation
            .as_ref()
            .is_some_and(|docs| match docs {
                lsp::Documentation::String(text) => !text.is_empty(),
                lsp::Documentation::MarkupContent(markup) => !markup.value.is_empty(),
            })
            && item
                .item
                .detail
                .as_ref()
                .is_some_and(|detail| !detail.is_empty())
            && item
                .item
                .additional_text_edits
                .as_ref()
                .is_some_and(|edits| !edits.is_empty());
        if is_resolved {
            item.resolved = true;
            return;
        }
        if self.last_request.as_deref().is_some_and(|it| it == item) {
            return;
        }
        let Some(ls) = editor.language_servers.get_by_id(item.provider).cloned() else {
            item.resolved = true;
            return;
        };
        if matches!(
            ls.capabilities().completion_provider,
            Some(lsp::CompletionOptions {
                resolve_provider: Some(true),
                ..
            })
        ) {
            let item = Arc::new(item.clone());
            self.last_request = Some(item.clone());
            send_blocking(&self.resolver, ResolveRequest { item, ls })
        } else {
            item.resolved = true;
        }
    }
}

struct ResolveRequest {
    item: Arc<LspCompletionItem>,
    ls: Arc<helix_lsp::Client>,
}

#[derive(Default)]
struct ResolveTimeout {
    next_request: Option<ResolveRequest>,
    in_flight: Option<Arc<LspCompletionItem>>,
    task_controller: TaskController,
}

impl AsyncHook for ResolveTimeout {
    type Event = ResolveRequest;

    fn handle_event(
        &mut self,
        request: Self::Event,
        timeout: Option<tokio::time::Instant>,
    ) -> Option<tokio::time::Instant> {
        if self
            .next_request
            .as_ref()
            .is_some_and(|old_request| old_request.item == request.item)
        {
            timeout
        } else if self
            .in_flight
            .as_ref()
            .is_some_and(|old_request| old_request.item == request.item.item)
        {
            self.next_request = None;
            None
        } else {
            self.next_request = Some(request);
            Some(Instant::now() + Duration::from_millis(150))
        }
    }

    fn finish_debounce(&mut self) {
        let Some(request) = self.next_request.take() else {
            return;
        };
        let token = self.task_controller.restart();
        self.in_flight = Some(request.item.clone());
        tokio::spawn(request.execute(token));
    }
}

impl ResolveRequest {
    async fn execute(self, cancel: TaskHandle) {
        let future = self.ls.resolve_completion_item(&self.item.item);
        let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else {
            return;
        };
        job::dispatch(move |_, compositor| {
            if let Some(completion) = &mut compositor
                .find::<crate::ui::EditorView>()
                .unwrap()
                .completion
            {
                let resolved_item = CompletionItem::Lsp(match resolved_item {
                    Ok(item) => LspCompletionItem {
                        item,
                        resolved: true,
                        ..*self.item
                    },
                    Err(err) => {
                        log::error!("completion resolve request failed: {err}");
                        // set item to resolved so we don't request it again
                        // we could also remove it but that oculd be odd ui
                        let mut item = (*self.item).clone();
                        item.resolved = true;
                        item
                    }
                });
                completion.replace_item(&*self.item, resolved_item);
            };
        })
        .await
    }
}