tinymist_query/
signature_help.rs1use 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#[derive(Debug, Clone)]
15pub struct SignatureHelpRequest {
16 pub path: PathBuf,
18 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 == ¶m.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(¶m.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}