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, 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 == ¶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 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}