tinymist_query/
inlay_hint.rs

1use lsp_types::{InlayHintKind, InlayHintLabel};
2
3use crate::{
4    analysis::{ParamKind, analyze_call},
5    prelude::*,
6};
7
8/// Configuration for inlay hints.
9pub struct InlayHintConfig {
10    // positional arguments group
11    /// Show inlay hints for positional arguments.
12    pub on_pos_args: bool,
13    /// Disable inlay hints for single positional arguments.
14    pub off_single_pos_arg: bool,
15
16    // variadic arguments group
17    /// Show inlay hints for variadic arguments.
18    pub on_variadic_args: bool,
19    /// Disable inlay hints for all variadic arguments but the first variadic
20    /// argument.
21    pub only_first_variadic_args: bool,
22
23    // The typst sugar grammar
24    /// Show inlay hints for content block arguments.
25    pub on_content_block_args: bool,
26}
27
28impl InlayHintConfig {
29    /// A smart configuration that enables most useful inlay hints.
30    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/// The [`textDocument/inlayHint`] request is sent from the client to the server
44/// to compute inlay hints for a given `(text document, range)` tuple that may
45/// be rendered in the editor in place with other text.
46///
47/// [`textDocument/inlayHint`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_inlayHint
48///
49/// # Compatibility
50///
51/// This request was introduced in specification version 3.17.0
52#[derive(Debug, Clone)]
53pub struct InlayHintRequest {
54    /// The path of the document to get inlay hints for.
55    pub path: PathBuf,
56    /// The range of the document to get inlay hints for.
57    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        // todo: survey bad performance children?
103        for child in node.children() {
104            self.work(child);
105        }
106    }
107
108    fn analyze_node(&mut self, node: &LinkedNode) -> Option<()> {
109        // analyze node self
110        match node.kind() {
111            // Type inlay hints
112            SyntaxKind::LetBinding => {
113                log::trace!("let binding found: {node:?}");
114            }
115            // Assignment inlay hints
116            SyntaxKind::Eq => {
117                log::trace!("assignment found: {node:?}");
118            }
119            SyntaxKind::DestructAssignment => {
120                log::trace!("destruct assignment found: {node:?}");
121            }
122            // Parameter inlay hints
123            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                // todo: union signatures
265            }
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}