1use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13#[derive(Default)]
14pub enum InlayHintKind {
15 #[default]
17 Type,
18 Parameter,
20 Chaining,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Position {
27 pub line: u32,
29 pub character: u32,
31}
32
33impl Position {
34 pub fn new(line: u32, character: u32) -> Self {
35 Self { line, character }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct InlayHint {
42 pub position: Position,
44 pub label: String,
46 pub kind: InlayHintKind,
48 pub has_tooltip: bool,
50}
51
52impl InlayHint {
53 pub fn type_hint(position: Position, type_name: impl Into<String>) -> Self {
55 Self {
56 position,
57 label: type_name.into(),
58 kind: InlayHintKind::Type,
59 has_tooltip: false,
60 }
61 }
62
63 pub fn parameter_hint(position: Position, param_name: impl Into<String>) -> Self {
65 Self {
66 position,
67 label: param_name.into(),
68 kind: InlayHintKind::Parameter,
69 has_tooltip: false,
70 }
71 }
72
73 pub fn is_type_hint(&self) -> bool {
75 self.kind == InlayHintKind::Type
76 }
77
78 pub fn is_parameter_hint(&self) -> bool {
80 self.kind == InlayHintKind::Parameter
81 }
82
83 pub fn type_name(&self) -> Option<&str> {
85 if self.is_type_hint() {
86 Some(&self.label)
87 } else {
88 None
89 }
90 }
91
92 pub fn is_copy_type(&self) -> bool {
94 if let Some(type_name) = self.type_name() {
95 super::copy_types::is_known_copy(type_name)
96 } else {
97 false
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct TypeHint {
105 pub target: String,
107 pub type_name: String,
109 pub position: Position,
111 pub is_copy: bool,
113 pub is_reference: bool,
115 pub is_mut: bool,
117}
118
119impl TypeHint {
120 pub fn from_inlay_hint(hint: &InlayHint, target: impl Into<String>) -> Option<Self> {
122 if hint.kind != InlayHintKind::Type {
123 return None;
124 }
125
126 let type_name = &hint.label;
127 let is_reference = type_name.starts_with('&');
128 let is_mut = type_name.starts_with("&mut ");
129 let is_copy = super::copy_types::is_known_copy(type_name);
130
131 Some(Self {
132 target: target.into(),
133 type_name: type_name.clone(),
134 position: hint.position,
135 is_copy,
136 is_reference,
137 is_mut,
138 })
139 }
140
141 pub fn base_type(&self) -> &str {
143 self.type_name
144 .trim_start_matches('&')
145 .trim_start_matches("mut ")
146 .trim()
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct InlayHintQuery {
153 pub file: PathBuf,
155 pub start_line: Option<u32>,
157 pub end_line: Option<u32>,
159 pub kinds: Option<Vec<InlayHintKind>>,
161}
162
163impl InlayHintQuery {
164 pub fn file(path: impl Into<PathBuf>) -> Self {
166 Self {
167 file: path.into(),
168 start_line: None,
169 end_line: None,
170 kinds: None,
171 }
172 }
173
174 pub fn range(path: impl Into<PathBuf>, start: u32, end: u32) -> Self {
176 Self {
177 file: path.into(),
178 start_line: Some(start),
179 end_line: Some(end),
180 kinds: None,
181 }
182 }
183
184 pub fn types_only(mut self) -> Self {
186 self.kinds = Some(vec![InlayHintKind::Type]);
187 self
188 }
189
190 pub fn parameters_only(mut self) -> Self {
192 self.kinds = Some(vec![InlayHintKind::Parameter]);
193 self
194 }
195
196 pub fn to_lsp_params(&self) -> serde_json::Value {
198 use serde_json::json;
199
200 let start_line = self.start_line.unwrap_or(0);
201 let end_line = self.end_line.unwrap_or(u32::MAX);
202
203 json!({
204 "textDocument": {
205 "uri": format!("file://{}", self.file.display())
206 },
207 "range": {
208 "start": { "line": start_line, "character": 0 },
209 "end": { "line": end_line, "character": 0 }
210 }
211 })
212 }
213}
214
215#[allow(dead_code)]
220#[derive(Debug, Clone, Default)]
221pub struct InlayHintCollection {
222 hints: Vec<InlayHint>,
223}
224
225#[allow(dead_code)]
226impl InlayHintCollection {
227 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn from_hints(hints: Vec<InlayHint>) -> Self {
234 Self { hints }
235 }
236
237 pub fn from_lsp_response(response: &serde_json::Value) -> Result<Self, serde_json::Error> {
239 let hints: Vec<LspInlayHint> = serde_json::from_value(response.clone())?;
240 let hints = hints.into_iter().map(|h| h.into()).collect();
241 Ok(Self { hints })
242 }
243
244 pub fn all(&self) -> &[InlayHint] {
246 &self.hints
247 }
248
249 pub fn type_hints(&self) -> impl Iterator<Item = &InlayHint> {
251 self.hints.iter().filter(|h| h.is_type_hint())
252 }
253
254 pub fn parameter_hints(&self) -> impl Iterator<Item = &InlayHint> {
256 self.hints.iter().filter(|h| h.is_parameter_hint())
257 }
258
259 pub fn at_line(&self, line: u32) -> impl Iterator<Item = &InlayHint> {
261 self.hints.iter().filter(move |h| h.position.line == line)
262 }
263
264 pub fn type_at(&self, line: u32, character: u32) -> Option<&InlayHint> {
266 self.hints.iter().find(|h| {
267 h.is_type_hint() && h.position.line == line && h.position.character == character
268 })
269 }
270
271 pub fn copy_types(&self) -> impl Iterator<Item = &InlayHint> {
273 self.type_hints().filter(|h| h.is_copy_type())
274 }
275}
276
277#[derive(Debug, Deserialize)]
279struct LspInlayHint {
280 position: LspPosition,
281 label: LspLabel,
282 kind: Option<u32>,
283 #[serde(default)]
284 tooltip: Option<serde_json::Value>,
285}
286
287#[derive(Debug, Deserialize)]
288struct LspPosition {
289 line: u32,
290 character: u32,
291}
292
293#[derive(Debug, Deserialize)]
294#[serde(untagged)]
295enum LspLabel {
296 String(String),
297 Parts(Vec<LspLabelPart>),
298}
299
300#[derive(Debug, Deserialize)]
301struct LspLabelPart {
302 value: String,
303}
304
305impl From<LspInlayHint> for InlayHint {
306 fn from(lsp: LspInlayHint) -> Self {
307 let label = match lsp.label {
308 LspLabel::String(s) => s,
309 LspLabel::Parts(parts) => parts.into_iter().map(|p| p.value).collect(),
310 };
311
312 let kind = match lsp.kind {
314 Some(1) => InlayHintKind::Type,
315 Some(2) => InlayHintKind::Parameter,
316 _ => InlayHintKind::Type,
317 };
318
319 InlayHint {
320 position: Position {
321 line: lsp.position.line,
322 character: lsp.position.character,
323 },
324 label,
325 kind,
326 has_tooltip: lsp.tooltip.is_some(),
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_inlay_hint_type() {
337 let hint = InlayHint::type_hint(Position::new(0, 10), "i32");
338 assert!(hint.is_type_hint());
339 assert!(!hint.is_parameter_hint());
340 assert_eq!(hint.type_name(), Some("i32"));
341 assert!(hint.is_copy_type());
342 }
343
344 #[test]
345 fn test_inlay_hint_parameter() {
346 let hint = InlayHint::parameter_hint(Position::new(5, 20), "name");
347 assert!(hint.is_parameter_hint());
348 assert!(!hint.is_type_hint());
349 assert_eq!(hint.type_name(), None);
350 }
351
352 #[test]
353 fn test_type_hint_copy_detection() {
354 let copy_hint = InlayHint::type_hint(Position::new(0, 0), "u64");
355 assert!(copy_hint.is_copy_type());
356
357 let non_copy_hint = InlayHint::type_hint(Position::new(0, 0), "String");
358 assert!(!non_copy_hint.is_copy_type());
359 }
360
361 #[test]
362 fn test_type_hint_from_inlay_hint() {
363 let hint = InlayHint::type_hint(Position::new(1, 5), "&mut Vec<i32>");
364 let type_hint = TypeHint::from_inlay_hint(&hint, "items").unwrap();
365
366 assert_eq!(type_hint.target, "items");
367 assert!(type_hint.is_reference);
368 assert!(type_hint.is_mut);
369 assert!(!type_hint.is_copy);
370 assert_eq!(type_hint.base_type(), "Vec<i32>");
371 }
372
373 #[test]
374 fn test_hint_collection() {
375 let hints = vec![
376 InlayHint::type_hint(Position::new(0, 10), "i32"),
377 InlayHint::parameter_hint(Position::new(0, 20), "x"),
378 InlayHint::type_hint(Position::new(1, 10), "String"),
379 ];
380 let collection = InlayHintCollection::from_hints(hints);
381
382 assert_eq!(collection.all().len(), 3);
383 assert_eq!(collection.type_hints().count(), 2);
384 assert_eq!(collection.parameter_hints().count(), 1);
385 assert_eq!(collection.copy_types().count(), 1);
386 }
387
388 #[test]
389 fn test_query_to_lsp_params() {
390 let query = InlayHintQuery::range("/src/lib.rs", 10, 20);
391 let params = query.to_lsp_params();
392
393 assert!(params["textDocument"]["uri"]
394 .as_str()
395 .unwrap()
396 .contains("lib.rs"));
397 assert_eq!(params["range"]["start"]["line"], 10);
398 assert_eq!(params["range"]["end"]["line"], 20);
399 }
400}