1use lsp_types::{InlayHintKind, InlayHintLabel};
2use typst::syntax::{LinkedNode, SyntaxKind, ast};
3
4use crate::{
5 analysis::{ParamKind, analyze_call, call_parts},
6 package::{find_package_and_latest, parse_package_import},
7 prelude::*,
8};
9
10pub struct InlayHintConfig {
12 pub on_pos_args: bool,
15 pub off_single_pos_arg: bool,
17
18 pub on_variadic_args: bool,
21 pub only_first_variadic_args: bool,
24
25 pub on_content_block_args: bool,
28
29 pub on_package_version_status: bool,
32}
33
34impl InlayHintConfig {
35 pub const fn smart() -> Self {
37 Self {
38 on_pos_args: true,
39 off_single_pos_arg: true,
40
41 on_variadic_args: true,
42 only_first_variadic_args: true,
43
44 on_content_block_args: false,
45
46 on_package_version_status: true,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
61pub struct InlayHintRequest {
62 pub path: PathBuf,
64 pub range: LspRange,
66}
67
68impl SemanticRequest for InlayHintRequest {
69 type Response = Vec<InlayHint>;
70
71 fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
72 let source = ctx.source_by_path(&self.path).ok()?;
73 let range = ctx.to_typst_range(self.range, &source)?;
74
75 let root = LinkedNode::new(source.root());
76 let mut worker = InlayHintWorker {
77 ctx,
78 source: &source,
79 range,
80 hints: vec![],
81 };
82 worker.work(root);
83
84 (!worker.hints.is_empty()).then_some(worker.hints)
85 }
86}
87
88const SMART: InlayHintConfig = InlayHintConfig::smart();
89
90struct InlayHintWorker<'a> {
91 ctx: &'a mut LocalContext,
92 source: &'a Source,
93 range: Range<usize>,
94 hints: Vec<InlayHint>,
95}
96
97impl InlayHintWorker<'_> {
98 fn work(&mut self, node: LinkedNode) {
99 let rng = node.range();
100 if rng.start >= self.range.end || rng.end <= self.range.start {
101 return;
102 }
103
104 self.analyze_node(&node);
105
106 if node.get().children().len() == 0 {
107 return;
108 }
109
110 for child in node.children() {
112 self.work(child);
113 }
114 }
115
116 fn analyze_node(&mut self, node: &LinkedNode) -> Option<()> {
117 match node.kind() {
119 SyntaxKind::LetBinding => {
121 log::trace!("let binding found: {node:?}");
122 }
123 SyntaxKind::Eq => {
125 log::trace!("assignment found: {node:?}");
126 }
127 SyntaxKind::DestructAssignment => {
128 log::trace!("destruct assignment found: {node:?}");
129 }
130 SyntaxKind::Str if SMART.on_package_version_status => {
132 self.check_package_import(node);
133 }
134 SyntaxKind::FuncCall | SyntaxKind::MathCall => {
136 log::trace!("func call found: {node:?}");
137 let call_info = analyze_call(self.ctx, self.source.clone(), node.clone())?;
138 crate::log_debug_ct!("got call_info {call_info:?}");
139
140 let (_callee_node, args_node) = call_parts(node)?;
141
142 let check_single_pos_arg = || {
143 let mut pos = 0;
144 let mut has_rest = false;
145 let mut content_pos = 0;
146
147 for arg_node in args_node.children() {
148 if arg_node.cast::<ast::Arg>().is_none() {
149 continue;
150 }
151
152 let Some(info) = call_info.arg_mapping.get(&arg_node) else {
153 continue;
154 };
155
156 if info.kind != ParamKind::Named {
157 if info.kind == ParamKind::Rest {
158 has_rest = true;
159 continue;
160 }
161 if info.is_content_block {
162 content_pos += 1;
163 } else {
164 pos += 1;
165 };
166
167 if pos > 1 && content_pos > 1 {
168 break;
169 }
170 }
171 }
172
173 (pos <= if has_rest { 0 } else { 1 }, content_pos <= 1)
174 };
175
176 let (disable_by_single_pos_arg, disable_by_single_content_pos_arg) =
177 if SMART.on_pos_args && SMART.off_single_pos_arg {
178 check_single_pos_arg()
179 } else {
180 (false, false)
181 };
182
183 let disable_by_single_line_content_block = !SMART.on_content_block_args
184 || 'one_line: {
185 for arg_node in args_node.children() {
186 if arg_node.cast::<ast::Arg>().is_none() {
187 continue;
188 }
189
190 let Some(info) = call_info.arg_mapping.get(&arg_node) else {
191 continue;
192 };
193
194 if info.kind != ParamKind::Named
195 && info.is_content_block
196 && !is_one_line(self.source, &arg_node)
197 {
198 break 'one_line false;
199 }
200 }
201
202 true
203 };
204
205 let mut is_first_variadic_arg = true;
206
207 for arg_node in args_node.children() {
208 if arg_node.cast::<ast::Arg>().is_none() {
209 continue;
210 }
211
212 let Some(info) = call_info.arg_mapping.get(&arg_node) else {
213 continue;
214 };
215
216 let name = &info.param_name;
217 if name.is_empty() {
218 continue;
219 }
220
221 match info.kind {
222 ParamKind::Named => {
223 continue;
224 }
225 ParamKind::Positional
226 if call_info.signature.primary().has_fill_or_size_or_stroke =>
227 {
228 continue;
229 }
230 ParamKind::Positional
231 if !SMART.on_pos_args
232 || (info.is_content_block
233 && (disable_by_single_content_pos_arg
234 || disable_by_single_line_content_block))
235 || (!info.is_content_block && disable_by_single_pos_arg) =>
236 {
237 continue;
238 }
239 ParamKind::Rest
240 if (!SMART.on_variadic_args
241 || disable_by_single_pos_arg
242 || (!is_first_variadic_arg && SMART.only_first_variadic_args)) =>
243 {
244 is_first_variadic_arg = false;
245 continue;
246 }
247 ParamKind::Rest => {
248 is_first_variadic_arg = false;
249 }
250 ParamKind::Positional => {}
251 }
252
253 let pos = arg_node.range().start;
254 let lsp_pos = self.ctx.to_lsp_pos(pos, self.source);
255
256 let label = InlayHintLabel::String(if info.kind == ParamKind::Rest {
257 format!("..{name}:")
258 } else {
259 format!("{name}:")
260 });
261
262 self.hints.push(InlayHint {
263 position: lsp_pos,
264 label,
265 kind: Some(InlayHintKind::PARAMETER),
266 text_edits: None,
267 tooltip: None,
268 padding_left: None,
269 padding_right: Some(true),
270 data: None,
271 });
272 }
273
274 }
276 SyntaxKind::Set => {
277 log::trace!("set rule found: {node:?}");
278 }
279 _ => {}
280 }
281
282 None
283 }
284
285 fn check_package_import(&mut self, node: &LinkedNode) -> Option<()> {
286 let package_spec = parse_package_import(node)?;
287
288 let (current_entry, latest) = find_package_and_latest(self.ctx.shared(), &package_spec);
289
290 let (label, tooltip) = if current_entry.is_none() {
291 let version_str = package_spec.version.to_string();
293 (
294 tinymist_l10n::t!("inlay-hint.package.version-not-found", "version not found"),
295 Some(tinymist_l10n::t!(
296 "inlay-hint.package.version-not-found-tooltip",
297 "Version {version} not found",
298 version = version_str.as_str().into()
299 )),
300 )
301 } else if let Some(latest) = latest
302 && let latest_version = latest.package.version
303 && latest_version != package_spec.version
304 {
305 let latest_str = latest_version.to_string();
307 (
308 tinymist_l10n::t!(
309 "inlay-hint.package.upgradable",
310 "→ {version} available",
311 version = latest_str.as_str().into()
312 ),
313 Some(tinymist_l10n::t!(
314 "inlay-hint.package.upgradable-tooltip",
315 "Newer version available: {version}",
316 version = latest_str.as_str().into()
317 )),
318 )
319 } else {
320 (
322 tinymist_l10n::t!("inlay-hint.package.up-to-date", "√ latest"),
323 Some(tinymist_l10n::t!(
324 "inlay-hint.package.up-to-date-tooltip",
325 "Up to date (latest version)"
326 )),
327 )
328 };
329
330 let pos = node.range().end;
332 let lsp_pos = self.ctx.to_lsp_pos(pos, self.source);
333
334 self.hints.push(InlayHint {
335 position: lsp_pos,
336 label: InlayHintLabel::String(label.to_string()),
337 kind: Some(InlayHintKind::TYPE),
338 text_edits: None,
339 tooltip: tooltip.map(|t| lsp_types::InlayHintTooltip::String(t.to_string())),
340 padding_left: Some(true),
341 padding_right: None,
342 data: None,
343 });
344
345 Some(())
346 }
347}
348
349fn is_one_line(src: &Source, arg_node: &LinkedNode<'_>) -> bool {
350 is_one_line_(src, arg_node).unwrap_or(true)
351}
352
353fn is_one_line_(src: &Source, arg_node: &LinkedNode<'_>) -> Option<bool> {
354 let lb = arg_node.children().next()?;
355 let rb = arg_node.children().next_back()?;
356 let ll = src.lines().byte_to_line(lb.offset())?;
357 let rl = src.lines().byte_to_line(rb.offset())?;
358 Some(ll == rl)
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::tests::*;
365
366 #[test]
367 fn smart() {
368 snapshot_testing("inlay_hints", &|ctx, path| {
369 let source = ctx.source_by_path(&path).unwrap();
370
371 let request = InlayHintRequest {
372 path: path.clone(),
373 range: to_lsp_range(0..source.text().len(), &source, PositionEncoding::Utf16),
374 };
375
376 let result = request.request(ctx);
377 assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
378 });
379 }
380}