Skip to main content

ryo_mutations/analyzer/
inlay_hints.rs

1//! InlayHints: Type information from rust-analyzer
2//!
3//! InlayHints provide type annotations that the compiler infers.
4//! This module parses LSP InlayHint responses and provides utilities
5//! for using them in mutations.
6
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10/// Kind of inlay hint
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13#[derive(Default)]
14pub enum InlayHintKind {
15    /// Type annotation: `let x: i32 = ...`
16    #[default]
17    Type,
18    /// Parameter name: `foo(/* param: */ value)`
19    Parameter,
20    /// Method chain result: `.map(...): Vec<_>`
21    Chaining,
22}
23
24/// Position in a source file
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Position {
27    /// Line number (0-indexed)
28    pub line: u32,
29    /// Character offset (0-indexed, UTF-16 code units)
30    pub character: u32,
31}
32
33impl Position {
34    pub fn new(line: u32, character: u32) -> Self {
35        Self { line, character }
36    }
37}
38
39/// An inlay hint from rust-analyzer
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct InlayHint {
42    /// Position where the hint should be displayed
43    pub position: Position,
44    /// The hint label (type name, parameter name, etc.)
45    pub label: String,
46    /// Kind of hint
47    pub kind: InlayHintKind,
48    /// Whether this hint can be resolved for more details
49    pub has_tooltip: bool,
50}
51
52impl InlayHint {
53    /// Create a new type hint
54    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    /// Create a new parameter hint
64    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    /// Check if this is a type hint
74    pub fn is_type_hint(&self) -> bool {
75        self.kind == InlayHintKind::Type
76    }
77
78    /// Check if this is a parameter hint
79    pub fn is_parameter_hint(&self) -> bool {
80        self.kind == InlayHintKind::Parameter
81    }
82
83    /// Get the type name if this is a type hint
84    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    /// Check if the type is a known Copy type
93    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/// Parsed type hint with additional analysis
103#[derive(Debug, Clone)]
104pub struct TypeHint {
105    /// The variable/expression this hint applies to
106    pub target: String,
107    /// The inferred type
108    pub type_name: String,
109    /// Position in source
110    pub position: Position,
111    /// Whether this type is Copy
112    pub is_copy: bool,
113    /// Whether this type is a reference
114    pub is_reference: bool,
115    /// Whether this type is mutable
116    pub is_mut: bool,
117}
118
119impl TypeHint {
120    /// Parse a type hint from an InlayHint
121    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    /// Get the base type (without references)
142    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/// Query parameters for requesting inlay hints
151#[derive(Debug, Clone)]
152pub struct InlayHintQuery {
153    /// File to get hints for
154    pub file: PathBuf,
155    /// Start line (0-indexed), None for file start
156    pub start_line: Option<u32>,
157    /// End line (0-indexed), None for file end
158    pub end_line: Option<u32>,
159    /// Filter by hint kind
160    pub kinds: Option<Vec<InlayHintKind>>,
161}
162
163impl InlayHintQuery {
164    /// Create a query for an entire file
165    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    /// Create a query for a specific range
175    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    /// Filter to only type hints
185    pub fn types_only(mut self) -> Self {
186        self.kinds = Some(vec![InlayHintKind::Type]);
187        self
188    }
189
190    /// Filter to only parameter hints
191    pub fn parameters_only(mut self) -> Self {
192        self.kinds = Some(vec![InlayHintKind::Parameter]);
193        self
194    }
195
196    /// Build LSP request parameters
197    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/// Collection of inlay hints for a file
216///
217/// Note: This struct is designed for future LSP client integration.
218/// Currently constructed only in tests but will be used when AnalyzerClient connects to rust-analyzer.
219#[allow(dead_code)]
220#[derive(Debug, Clone, Default)]
221pub struct InlayHintCollection {
222    hints: Vec<InlayHint>,
223}
224
225#[allow(dead_code)]
226impl InlayHintCollection {
227    /// Create an empty collection
228    pub fn new() -> Self {
229        Self::default()
230    }
231
232    /// Create from a list of hints
233    pub fn from_hints(hints: Vec<InlayHint>) -> Self {
234        Self { hints }
235    }
236
237    /// Parse from LSP response JSON
238    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    /// Get all hints
245    pub fn all(&self) -> &[InlayHint] {
246        &self.hints
247    }
248
249    /// Get type hints only
250    pub fn type_hints(&self) -> impl Iterator<Item = &InlayHint> {
251        self.hints.iter().filter(|h| h.is_type_hint())
252    }
253
254    /// Get parameter hints only
255    pub fn parameter_hints(&self) -> impl Iterator<Item = &InlayHint> {
256        self.hints.iter().filter(|h| h.is_parameter_hint())
257    }
258
259    /// Find hints at a specific line
260    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    /// Find type hint for a variable at position
265    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    /// Get all Copy types in this collection
272    pub fn copy_types(&self) -> impl Iterator<Item = &InlayHint> {
273        self.type_hints().filter(|h| h.is_copy_type())
274    }
275}
276
277/// LSP InlayHint response format
278#[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        // LSP InlayHintKind: 1 = Type, 2 = Parameter
313        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}