tinymist_query/
inlay_hint.rs1use lsp_types::{InlayHintKind, InlayHintLabel};
2
3use crate::{
4 analysis::{ParamKind, analyze_call},
5 prelude::*,
6};
7
8pub struct InlayHintConfig {
10 pub on_pos_args: bool,
13 pub off_single_pos_arg: bool,
15
16 pub on_variadic_args: bool,
19 pub only_first_variadic_args: bool,
22
23 pub on_content_block_args: bool,
26}
27
28impl InlayHintConfig {
29 pub const fn smart() -> Self {
31 Self {
32 on_pos_args: true,
33 off_single_pos_arg: true,
34
35 on_variadic_args: true,
36 only_first_variadic_args: true,
37
38 on_content_block_args: false,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
53pub struct InlayHintRequest {
54 pub path: PathBuf,
56 pub range: LspRange,
58}
59
60impl SemanticRequest for InlayHintRequest {
61 type Response = Vec<InlayHint>;
62
63 fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
64 let source = ctx.source_by_path(&self.path).ok()?;
65 let range = ctx.to_typst_range(self.range, &source)?;
66
67 let root = LinkedNode::new(source.root());
68 let mut worker = InlayHintWorker {
69 ctx,
70 source: &source,
71 range,
72 hints: vec![],
73 };
74 worker.work(root);
75
76 (!worker.hints.is_empty()).then_some(worker.hints)
77 }
78}
79
80const SMART: InlayHintConfig = InlayHintConfig::smart();
81
82struct InlayHintWorker<'a> {
83 ctx: &'a mut LocalContext,
84 source: &'a Source,
85 range: Range<usize>,
86 hints: Vec<InlayHint>,
87}
88
89impl InlayHintWorker<'_> {
90 fn work(&mut self, node: LinkedNode) {
91 let rng = node.range();
92 if rng.start >= self.range.end || rng.end <= self.range.start {
93 return;
94 }
95
96 self.analyze_node(&node);
97
98 if node.get().children().len() == 0 {
99 return;
100 }
101
102 for child in node.children() {
104 self.work(child);
105 }
106 }
107
108 fn analyze_node(&mut self, node: &LinkedNode) -> Option<()> {
109 match node.kind() {
111 SyntaxKind::LetBinding => {
113 log::trace!("let binding found: {node:?}");
114 }
115 SyntaxKind::Eq => {
117 log::trace!("assignment found: {node:?}");
118 }
119 SyntaxKind::DestructAssignment => {
120 log::trace!("destruct assignment found: {node:?}");
121 }
122 SyntaxKind::FuncCall => {
124 log::trace!("func call found: {node:?}");
125 let call_info = analyze_call(self.ctx, self.source.clone(), node.clone())?;
126 crate::log_debug_ct!("got call_info {call_info:?}");
127
128 let call = node.cast::<ast::FuncCall>().unwrap();
129 let args = call.args();
130 let args_node = node.find(args.span())?;
131
132 let check_single_pos_arg = || {
133 let mut pos = 0;
134 let mut has_rest = false;
135 let mut content_pos = 0;
136
137 for arg in args.items() {
138 let Some(arg_node) = args_node.find(arg.span()) else {
139 continue;
140 };
141
142 let Some(info) = call_info.arg_mapping.get(&arg_node) else {
143 continue;
144 };
145
146 if info.kind != ParamKind::Named {
147 if info.kind == ParamKind::Rest {
148 has_rest = true;
149 continue;
150 }
151 if info.is_content_block {
152 content_pos += 1;
153 } else {
154 pos += 1;
155 };
156
157 if pos > 1 && content_pos > 1 {
158 break;
159 }
160 }
161 }
162
163 (pos <= if has_rest { 0 } else { 1 }, content_pos <= 1)
164 };
165
166 let (disable_by_single_pos_arg, disable_by_single_content_pos_arg) =
167 if SMART.on_pos_args && SMART.off_single_pos_arg {
168 check_single_pos_arg()
169 } else {
170 (false, false)
171 };
172
173 let disable_by_single_line_content_block = !SMART.on_content_block_args
174 || 'one_line: {
175 for arg in args.items() {
176 let Some(arg_node) = args_node.find(arg.span()) else {
177 continue;
178 };
179
180 let Some(info) = call_info.arg_mapping.get(&arg_node) else {
181 continue;
182 };
183
184 if info.kind != ParamKind::Named
185 && info.is_content_block
186 && !is_one_line(self.source, &arg_node)
187 {
188 break 'one_line false;
189 }
190 }
191
192 true
193 };
194
195 let mut is_first_variadic_arg = true;
196
197 for arg in args.items() {
198 let Some(arg_node) = args_node.find(arg.span()) else {
199 continue;
200 };
201
202 let Some(info) = call_info.arg_mapping.get(&arg_node) else {
203 continue;
204 };
205
206 let name = &info.param_name;
207 if name.is_empty() {
208 continue;
209 }
210
211 match info.kind {
212 ParamKind::Named => {
213 continue;
214 }
215 ParamKind::Positional
216 if call_info.signature.primary().has_fill_or_size_or_stroke =>
217 {
218 continue;
219 }
220 ParamKind::Positional
221 if !SMART.on_pos_args
222 || (info.is_content_block
223 && (disable_by_single_content_pos_arg
224 || disable_by_single_line_content_block))
225 || (!info.is_content_block && disable_by_single_pos_arg) =>
226 {
227 continue;
228 }
229 ParamKind::Rest
230 if (!SMART.on_variadic_args
231 || disable_by_single_pos_arg
232 || (!is_first_variadic_arg && SMART.only_first_variadic_args)) =>
233 {
234 is_first_variadic_arg = false;
235 continue;
236 }
237 ParamKind::Rest => {
238 is_first_variadic_arg = false;
239 }
240 ParamKind::Positional => {}
241 }
242
243 let pos = arg_node.range().start;
244 let lsp_pos = self.ctx.to_lsp_pos(pos, self.source);
245
246 let label = InlayHintLabel::String(if info.kind == ParamKind::Rest {
247 format!("..{name}:")
248 } else {
249 format!("{name}:")
250 });
251
252 self.hints.push(InlayHint {
253 position: lsp_pos,
254 label,
255 kind: Some(InlayHintKind::PARAMETER),
256 text_edits: None,
257 tooltip: None,
258 padding_left: None,
259 padding_right: Some(true),
260 data: None,
261 });
262 }
263
264 }
266 SyntaxKind::Set => {
267 log::trace!("set rule found: {node:?}");
268 }
269 _ => {}
270 }
271
272 None
273 }
274}
275
276fn is_one_line(src: &Source, arg_node: &LinkedNode<'_>) -> bool {
277 is_one_line_(src, arg_node).unwrap_or(true)
278}
279
280fn is_one_line_(src: &Source, arg_node: &LinkedNode<'_>) -> Option<bool> {
281 let lb = arg_node.children().next()?;
282 let rb = arg_node.children().next_back()?;
283 let ll = src.byte_to_line(lb.offset())?;
284 let rl = src.byte_to_line(rb.offset())?;
285 Some(ll == rl)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::tests::*;
292
293 #[test]
294 fn smart() {
295 snapshot_testing("inlay_hints", &|ctx, path| {
296 let source = ctx.source_by_path(&path).unwrap();
297
298 let request = InlayHintRequest {
299 path: path.clone(),
300 range: to_lsp_range(0..source.text().len(), &source, PositionEncoding::Utf16),
301 };
302
303 let result = request.request(ctx);
304 assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
305 });
306 }
307}