1use ink_analyzer_ir::syntax::{AstToken, TextRange, TextSize};
4use ink_analyzer_ir::{InkArg, InkArgKind, InkArgValueKind, InkAttributeKind, InkEntity, InkFile};
5
6use crate::Version;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct InlayHint {
11 pub label: String,
13 pub position: TextSize,
15 pub range: TextRange,
17 pub detail: Option<String>,
19}
20
21pub fn inlay_hints(file: &InkFile, range: Option<TextRange>, version: Version) -> Vec<InlayHint> {
23 let mut results = Vec::new();
24
25 let mut process_inlay_hint = |arg: &InkArg, is_constructor: bool| {
26 if range.is_none_or(|it| it.contains_range(arg.text_range())) {
29 let arg_value_kind = if version.is_legacy() {
31 InkArgValueKind::from(*arg.kind())
32 } else {
33 InkArgValueKind::from_v5(*arg.kind(), Some(is_constructor))
34 };
35
36 let label = arg_value_kind.to_string();
37 if !label.is_empty() {
38 let doc = arg_value_kind.detail();
39 results.push(InlayHint {
40 label,
41 position: arg
42 .name()
43 .map(|name| name.syntax().text_range().end())
44 .unwrap_or_else(|| arg.text_range().end()),
45 range: arg
46 .name()
47 .map(|name| name.syntax().text_range())
48 .unwrap_or_else(|| arg.text_range()),
49 detail: (!doc.is_empty()).then(|| doc.to_owned()),
50 })
51 }
52 }
53 };
54
55 for attr in file.tree().ink_attrs_in_scope() {
57 let is_constructor = *attr.kind() == InkAttributeKind::Arg(InkArgKind::Constructor);
59 for arg in attr.args() {
60 if version.is_gte_v6()
63 && matches!(
64 arg.kind(),
65 InkArgKind::Extension | InkArgKind::Function | InkArgKind::HandleStatus
66 )
67 {
68 continue;
69 }
70
71 process_inlay_hint(arg, is_constructor);
72
73 let mut nested_arg = None;
74 while let Some(arg) = nested_arg.as_ref().unwrap_or(arg).nested() {
75 process_inlay_hint(&arg, is_constructor);
76 nested_arg = Some(arg);
77 }
78 }
79 }
80 results
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::Version;
87 use ink_analyzer_ir::MinorVersion;
88 use test_utils::parse_offset_at;
89
90 #[test]
91 fn inlay_hints_works() {
92 let fixtures_gte_v5 = vec![
93 (
94 "#[ink(message, default, payable, selector=1)]",
95 None,
96 vec![(
97 "u32 | _ | @",
98 Some("selector"),
99 (Some("<-selector"), Some("selector")),
100 )],
101 ),
102 (
103 "#[ink(constructor, default, payable, selector=1)]",
104 None,
105 vec![(
106 "u32 | _",
107 Some("selector"),
108 (Some("<-selector"), Some("selector")),
109 )],
110 ),
111 (
112 r#"#[ink::event(signature_topic="1111111111111111111111111111111111111111111111111111111111111111")]"#,
113 None,
114 vec![(
115 "&str",
116 Some("signature_topic"),
117 (Some("<-signature_topic"), Some("signature_topic")),
118 )],
119 ),
120 (
121 r#"#[ink(event, signature_topic="1111111111111111111111111111111111111111111111111111111111111111")]"#,
122 None,
123 vec![(
124 "&str",
125 Some("signature_topic"),
126 (Some("<-signature_topic"), Some("signature_topic")),
127 )],
128 ),
129 (
130 r#"#[ink_e2e::test(
131 environment=ink::env::DefaultEnvironment,
132 backend(node(url="ws://127.0.0.1:9000"))
133 )]"#,
134 None,
135 vec![
136 (
137 "impl Environment",
138 Some("environment"),
139 (Some("<-environment"), Some("environment")),
140 ),
141 (
142 "node | runtime_only",
143 Some("backend"),
144 (Some("<-backend"), Some("backend")),
145 ),
146 ("&str", Some("url"), (Some("<-url"), Some("url"))),
147 ],
148 ),
149 (
150 r#"#[ink_e2e::test(
151 environment=ink::env::DefaultEnvironment,
152 backend(runtime_only(sandbox=ink_e2e::MinimalSandbox))
153 )]"#,
154 None,
155 vec![
156 (
157 "impl Environment",
158 Some("environment"),
159 (Some("<-environment"), Some("environment")),
160 ),
161 (
162 "node | runtime_only",
163 Some("backend"),
164 (Some("<-backend"), Some("backend")),
165 ),
166 (
167 "impl drink::Sandbox",
168 Some("sandbox"),
169 (Some("<-sandbox"), Some("sandbox")),
170 ),
171 ],
172 ),
173 ];
174 let fixtures_v5_only = vec![
175 (
176 "#[ink::chain_extension(extension=1)]",
177 None,
178 vec![(
179 "u16",
180 Some("extension->"),
181 (Some("<-extension->"), Some("extension->")),
182 )],
183 ),
184 (
185 "#[ink(function=1, handle_status=true)]",
186 None,
187 vec![
188 (
189 "u16",
190 Some("function"),
191 (Some("<-function"), Some("function")),
192 ),
193 (
194 "bool",
195 Some("handle_status"),
196 (Some("<-handle_status"), Some("handle_status")),
197 ),
198 ],
199 ),
200 ];
201 for (version, fixtures) in [
202 (
203 Version::Legacy,
204 vec![
205 (
206 "#[ink(message, default, payable, selector=1)]",
207 None,
208 vec![(
209 "u32 | _",
210 Some("selector"),
211 (Some("<-selector"), Some("selector")),
212 )],
213 ),
214 (
215 "#[ink(extension=1, handle_status=true)]",
216 None,
217 vec![
218 (
219 "u32",
220 Some("extension"),
221 (Some("<-extension"), Some("extension")),
222 ),
223 (
224 "bool",
225 Some("handle_status"),
226 (Some("<-handle_status"), Some("handle_status")),
227 ),
228 ],
229 ),
230 (
231 r#"#[ink_e2e::test(
232 additional_contracts="adder/Cargo.toml flipper/Cargo.toml",
233 environment=ink::env::DefaultEnvironment,
234 keep_attr="foo,bar"
235 )]"#,
236 None,
237 vec![
238 (
239 "&str",
240 Some("additional_contracts"),
241 (Some("<-additional_contracts"), Some("additional_contracts")),
242 ),
243 (
244 "impl Environment",
245 Some("environment"),
246 (Some("<-environment"), Some("environment")),
247 ),
248 (
249 "&str",
250 Some("keep_attr"),
251 (Some("<-keep_attr"), Some("keep_attr")),
252 ),
253 ],
254 ),
255 ],
256 ),
257 (
258 Version::V5(MinorVersion::Base),
259 fixtures_gte_v5
260 .clone()
261 .into_iter()
262 .chain(fixtures_v5_only.clone())
263 .collect(),
264 ),
265 (
266 Version::V5(MinorVersion::Latest),
267 fixtures_gte_v5
268 .clone()
269 .into_iter()
270 .chain(fixtures_v5_only.clone())
271 .collect(),
272 ),
273 (
274 Version::V6,
275 [
276 (
277 r#"#[ink(message, name="name")]"#,
278 None,
279 vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
280 ),
281 (
282 r#"#[ink(constructor, name="name")]"#,
283 None,
284 vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
285 ),
286 (
287 r#"#[ink::event(name="name")]"#,
288 None,
289 vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
290 ),
291 (
292 r#"#[ink(event, name="name")]"#,
293 None,
294 vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
295 ),
296 (
297 r#"#[ink::contract_ref(name="name", env=my::env::Types)]"#,
298 None,
299 vec![
300 ("&str", Some("name"), (Some("<-name"), Some("name"))),
301 (
302 "impl Environment",
303 Some("env"),
304 (Some("<-env"), Some("env")),
305 ),
306 ],
307 ),
308 ("#[ink::chain_extension(extension=1)]", None, vec![]),
309 ("#[ink(function=1, handle_status=true)]", None, vec![]),
310 ]
311 .into_iter()
312 .chain(fixtures_gte_v5)
313 .collect(),
314 ),
315 ] {
316 for (code, selection_range_pat, expected_results) in [
317 ("// Nothing", None, vec![]),
329 (
330 r#"
331 mod my_mod {
332 fn my_fn(a: bool, b: u8) {
333 }
334 }
335 "#,
336 None,
337 vec![],
338 ),
339 ("#[ink::contract]", None, vec![]),
341 ("#[ink::trait_definition]", None, vec![]),
342 ("#[ink::chain_extension]", None, vec![]),
343 ("#[ink::storage_item]", None, vec![]),
344 ("#[ink::test]", None, vec![]),
345 (
346 r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
347 None,
348 vec![
349 (
350 "impl Environment",
351 Some("env"),
352 (Some("<-env"), Some("env")),
353 ),
354 (
355 "&str",
356 Some("keep_attr"),
357 (Some("<-keep_attr"), Some("keep_attr")),
358 ),
359 ],
360 ),
361 (
362 r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
363 Some((Some("<-"), Some("->"))),
364 vec![
365 (
366 "impl Environment",
367 Some("env"),
368 (Some("<-env"), Some("env")),
369 ),
370 (
371 "&str",
372 Some("keep_attr"),
373 (Some("<-keep_attr"), Some("keep_attr")),
374 ),
375 ],
376 ),
377 (
378 r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
379 Some((Some("<-"), Some("my::env::Types"))),
380 vec![(
381 "impl Environment",
382 Some("env"),
383 (Some("<-env"), Some("env")),
384 )],
385 ),
386 (
387 r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
388 Some((Some("<-keep_attr"), Some("->"))),
389 vec![(
390 "&str",
391 Some("keep_attr"),
392 (Some("<-keep_attr"), Some("keep_attr")),
393 )],
394 ),
395 (
396 r#"#[ink::trait_definition(namespace="my_namespace", keep_attr="foo,bar")]"#,
397 None,
398 vec![
399 (
400 "&str",
401 Some("namespace"),
402 (Some("<-namespace"), Some("namespace")),
403 ),
404 (
405 "&str",
406 Some("keep_attr"),
407 (Some("<-keep_attr"), Some("keep_attr")),
408 ),
409 ],
410 ),
411 (
412 "#[ink::storage_item(derive=true)]",
413 None,
414 vec![("bool", Some("derive"), (Some("<-derive"), Some("derive")))],
415 ),
416 ("#[ink(storage)]", None, vec![]),
418 ("#[ink(event, anonymous)]", None, vec![]),
419 (
420 "#[ink(constructor, default, selector=1)]",
421 None,
422 vec![(
423 "u32 | _",
424 Some("selector"),
425 (Some("<-selector"), Some("selector")),
426 )],
427 ),
428 (
429 r#"#[ink(impl, namespace="my_namespace")]"#,
430 None,
431 vec![(
432 "&str",
433 Some("namespace"),
434 (Some("<-namespace"), Some("namespace")),
435 )],
436 ),
437 ]
438 .into_iter()
439 .chain(fixtures)
440 {
441 let range = selection_range_pat.map(|(pat_start, pat_end)| {
442 TextRange::new(
443 TextSize::from(parse_offset_at(code, pat_start).unwrap() as u32),
444 TextSize::from(parse_offset_at(code, pat_end).unwrap() as u32),
445 )
446 });
447 let results = inlay_hints(&InkFile::parse(code), range, version);
448
449 assert_eq!(
450 results
451 .into_iter()
452 .map(|item| (item.label, item.position, item.range))
453 .collect::<Vec<(String, TextSize, TextRange)>>(),
454 expected_results
455 .into_iter()
456 .map(|(label, pos_pat_start, (range_pat_start, range_pat_end))| (
457 label.to_owned(),
458 TextSize::from(parse_offset_at(code, pos_pat_start).unwrap() as u32),
459 TextRange::new(
460 TextSize::from(
461 parse_offset_at(code, range_pat_start).unwrap() as u32
462 ),
463 TextSize::from(parse_offset_at(code, range_pat_end).unwrap() as u32)
464 )
465 ))
466 .collect::<Vec<(String, TextSize, TextRange)>>(),
467 "code: {code}, version: {:?}",
468 version
469 );
470 }
471 }
472 }
473}