tinymist_query/
signature_help.rs

1use typst_shim::syntax::LinkedNodeExt;
2
3use crate::{
4    SemanticRequest,
5    adt::interner::Interned,
6    prelude::*,
7    syntax::{ArgClass, SyntaxContext, classify_context, classify_syntax},
8};
9
10/// The [`textDocument/signatureHelp`] request is sent from the client to the
11/// server to request signature information at a given cursor position.
12///
13/// [`textDocument/signatureHelp`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp
14#[derive(Debug, Clone)]
15pub struct SignatureHelpRequest {
16    /// The path of the document to get signature help for.
17    pub path: PathBuf,
18    /// The position of the cursor to get signature help for.
19    pub position: LspPosition,
20}
21
22impl SemanticRequest for SignatureHelpRequest {
23    type Response = SignatureHelp;
24
25    fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
26        let source = ctx.source_by_path(&self.path).ok()?;
27        let cursor = ctx.to_typst_pos(self.position, &source)? + 1;
28
29        let ast_node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
30        let SyntaxContext::Arg {
31            callee,
32            target,
33            is_set,
34            ..
35        } = classify_context(ast_node, Some(cursor))?
36        else {
37            return None;
38        };
39
40        let syntax = classify_syntax(callee, cursor)?;
41        let def = ctx.def_of_syntax_or_dyn(&source, None, syntax)?;
42        let sig = ctx.sig_of_def(def.clone())?;
43        crate::log_debug_ct!("got signature {sig:?}");
44
45        let param_shift = sig.param_shift();
46        let mut active_parameter = None;
47
48        let mut label = def.name().as_ref().to_owned();
49        let mut params = Vec::new();
50
51        label.push('(');
52
53        let mut real_offset = 0;
54        let focus_name = OnceLock::new();
55        for (idx, (param, ty)) in sig.params().enumerate() {
56            if is_set && !param.attrs.settable {
57                continue;
58            }
59
60            match &target {
61                ArgClass::Positional { .. } if is_set => {}
62                ArgClass::Positional { positional, .. } => {
63                    if (*positional) + param_shift == idx {
64                        active_parameter = Some(real_offset);
65                    }
66                }
67                ArgClass::Named(name) => {
68                    let focus_name = focus_name
69                        .get_or_init(|| Interned::new_str(&name.get().clone().into_text()));
70                    if focus_name == &param.name {
71                        active_parameter = Some(real_offset);
72                    }
73                }
74            }
75
76            real_offset += 1;
77
78            if !params.is_empty() {
79                label.push_str(", ");
80            }
81
82            label.push_str(&format!(
83                "{}: {}",
84                param.name,
85                ty.unwrap_or(&param.ty)
86                    .describe()
87                    .as_deref()
88                    .unwrap_or("any")
89            ));
90
91            params.push(ParameterInformation {
92                label: lsp_types::ParameterLabel::Simple(format!("{}:", param.name)),
93                documentation: param.docs.as_ref().map(|docs| {
94                    Documentation::MarkupContent(MarkupContent {
95                        value: docs.as_ref().into(),
96                        kind: MarkupKind::Markdown,
97                    })
98                }),
99            });
100        }
101        label.push(')');
102        let ret = sig.type_sig().body.clone();
103        if let Some(ret_ty) = ret {
104            label.push_str(" -> ");
105            label.push_str(ret_ty.describe().as_deref().unwrap_or("any"));
106        }
107
108        if matches!(target, ArgClass::Positional { .. }) {
109            active_parameter =
110                active_parameter.map(|x| x.min(sig.primary().pos_size().saturating_sub(1)));
111        }
112
113        crate::log_debug_ct!("got signature info {label} {params:?}");
114
115        Some(SignatureHelp {
116            signatures: vec![SignatureInformation {
117                label: label.to_string(),
118                documentation: sig.primary().docs.as_deref().map(markdown_docs),
119                parameters: Some(params),
120                active_parameter: active_parameter.map(|x| x as u32),
121            }],
122            active_signature: Some(0),
123            active_parameter: None,
124        })
125    }
126}
127
128fn markdown_docs(docs: &str) -> Documentation {
129    Documentation::MarkupContent(MarkupContent {
130        kind: MarkupKind::Markdown,
131        value: docs.to_owned(),
132    })
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::tests::*;
139
140    #[test]
141    fn test() {
142        snapshot_testing("signature_help", &|ctx, path| {
143            let source = ctx.source_by_path(&path).unwrap();
144            let (position, anno) = make_pos_annotation(&source);
145
146            let request = SignatureHelpRequest { path, position };
147
148            let result = request.request(ctx);
149            with_settings!({
150                description => format!("signature help on {anno}"),
151            }, {
152                assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
153            })
154        });
155    }
156}