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, 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().full_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            let documentation = param.docs.as_ref().map(|docs| markdown_docs(ctx, docs));
92
93            params.push(ParameterInformation {
94                label: lsp_types::ParameterLabel::Simple(format!("{}:", param.name)),
95                documentation,
96            });
97        }
98        label.push(')');
99        let ret = sig.type_sig().body.clone();
100        if let Some(ret_ty) = ret {
101            label.push_str(" -> ");
102            label.push_str(ret_ty.describe().as_deref().unwrap_or("any"));
103        }
104
105        if matches!(target, ArgClass::Positional { .. }) {
106            active_parameter =
107                active_parameter.map(|x| x.min(sig.primary().pos_size().saturating_sub(1)));
108        }
109
110        crate::log_debug_ct!("got signature info {label} {params:?}");
111
112        Some(SignatureHelp {
113            signatures: vec![SignatureInformation {
114                label: label.to_string(),
115                documentation: sig
116                    .primary()
117                    .docs
118                    .as_ref()
119                    .map(|docs| markdown_docs(ctx, docs)),
120                parameters: Some(params),
121                active_parameter: active_parameter.map(|x| x as u32),
122            }],
123            active_signature: Some(0),
124            active_parameter: None,
125        })
126    }
127}
128
129fn markdown_docs(ctx: &mut LocalContext, docs: &crate::docs::DocText) -> Documentation {
130    let docs = crate::docs::resolve_doc_text(ctx.shared(), docs);
131    Documentation::MarkupContent(MarkupContent {
132        kind: MarkupKind::Markdown,
133        value: docs.into(),
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::tests::*;
141
142    #[test]
143    fn test() {
144        snapshot_testing("signature_help", &|ctx, path| {
145            let source = ctx.source_by_path(&path).unwrap();
146            let (position, anno) = make_pos_annotation(&source);
147
148            let request = SignatureHelpRequest { path, position };
149
150            let result = request.request(ctx);
151            with_settings!({
152                description => format!("signature help on {anno}"),
153            }, {
154                assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
155            })
156        });
157    }
158}