ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! InlayHints: Type information from rust-analyzer
//!
//! InlayHints provide type annotations that the compiler infers.
//! This module parses LSP InlayHint responses and provides utilities
//! for using them in mutations.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Kind of inlay hint
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Default)]
pub enum InlayHintKind {
    /// Type annotation: `let x: i32 = ...`
    #[default]
    Type,
    /// Parameter name: `foo(/* param: */ value)`
    Parameter,
    /// Method chain result: `.map(...): Vec<_>`
    Chaining,
}

/// Position in a source file
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Position {
    /// Line number (0-indexed)
    pub line: u32,
    /// Character offset (0-indexed, UTF-16 code units)
    pub character: u32,
}

impl Position {
    pub fn new(line: u32, character: u32) -> Self {
        Self { line, character }
    }
}

/// An inlay hint from rust-analyzer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InlayHint {
    /// Position where the hint should be displayed
    pub position: Position,
    /// The hint label (type name, parameter name, etc.)
    pub label: String,
    /// Kind of hint
    pub kind: InlayHintKind,
    /// Whether this hint can be resolved for more details
    pub has_tooltip: bool,
}

impl InlayHint {
    /// Create a new type hint
    pub fn type_hint(position: Position, type_name: impl Into<String>) -> Self {
        Self {
            position,
            label: type_name.into(),
            kind: InlayHintKind::Type,
            has_tooltip: false,
        }
    }

    /// Create a new parameter hint
    pub fn parameter_hint(position: Position, param_name: impl Into<String>) -> Self {
        Self {
            position,
            label: param_name.into(),
            kind: InlayHintKind::Parameter,
            has_tooltip: false,
        }
    }

    /// Check if this is a type hint
    pub fn is_type_hint(&self) -> bool {
        self.kind == InlayHintKind::Type
    }

    /// Check if this is a parameter hint
    pub fn is_parameter_hint(&self) -> bool {
        self.kind == InlayHintKind::Parameter
    }

    /// Get the type name if this is a type hint
    pub fn type_name(&self) -> Option<&str> {
        if self.is_type_hint() {
            Some(&self.label)
        } else {
            None
        }
    }

    /// Check if the type is a known Copy type
    pub fn is_copy_type(&self) -> bool {
        if let Some(type_name) = self.type_name() {
            super::copy_types::is_known_copy(type_name)
        } else {
            false
        }
    }
}

/// Parsed type hint with additional analysis
#[derive(Debug, Clone)]
pub struct TypeHint {
    /// The variable/expression this hint applies to
    pub target: String,
    /// The inferred type
    pub type_name: String,
    /// Position in source
    pub position: Position,
    /// Whether this type is Copy
    pub is_copy: bool,
    /// Whether this type is a reference
    pub is_reference: bool,
    /// Whether this type is mutable
    pub is_mut: bool,
}

impl TypeHint {
    /// Parse a type hint from an InlayHint
    pub fn from_inlay_hint(hint: &InlayHint, target: impl Into<String>) -> Option<Self> {
        if hint.kind != InlayHintKind::Type {
            return None;
        }

        let type_name = &hint.label;
        let is_reference = type_name.starts_with('&');
        let is_mut = type_name.starts_with("&mut ");
        let is_copy = super::copy_types::is_known_copy(type_name);

        Some(Self {
            target: target.into(),
            type_name: type_name.clone(),
            position: hint.position,
            is_copy,
            is_reference,
            is_mut,
        })
    }

    /// Get the base type (without references)
    pub fn base_type(&self) -> &str {
        self.type_name
            .trim_start_matches('&')
            .trim_start_matches("mut ")
            .trim()
    }
}

/// Query parameters for requesting inlay hints
#[derive(Debug, Clone)]
pub struct InlayHintQuery {
    /// File to get hints for
    pub file: PathBuf,
    /// Start line (0-indexed), None for file start
    pub start_line: Option<u32>,
    /// End line (0-indexed), None for file end
    pub end_line: Option<u32>,
    /// Filter by hint kind
    pub kinds: Option<Vec<InlayHintKind>>,
}

impl InlayHintQuery {
    /// Create a query for an entire file
    pub fn file(path: impl Into<PathBuf>) -> Self {
        Self {
            file: path.into(),
            start_line: None,
            end_line: None,
            kinds: None,
        }
    }

    /// Create a query for a specific range
    pub fn range(path: impl Into<PathBuf>, start: u32, end: u32) -> Self {
        Self {
            file: path.into(),
            start_line: Some(start),
            end_line: Some(end),
            kinds: None,
        }
    }

    /// Filter to only type hints
    pub fn types_only(mut self) -> Self {
        self.kinds = Some(vec![InlayHintKind::Type]);
        self
    }

    /// Filter to only parameter hints
    pub fn parameters_only(mut self) -> Self {
        self.kinds = Some(vec![InlayHintKind::Parameter]);
        self
    }

    /// Build LSP request parameters
    pub fn to_lsp_params(&self) -> serde_json::Value {
        use serde_json::json;

        let start_line = self.start_line.unwrap_or(0);
        let end_line = self.end_line.unwrap_or(u32::MAX);

        json!({
            "textDocument": {
                "uri": format!("file://{}", self.file.display())
            },
            "range": {
                "start": { "line": start_line, "character": 0 },
                "end": { "line": end_line, "character": 0 }
            }
        })
    }
}

/// Collection of inlay hints for a file
///
/// Note: This struct is designed for future LSP client integration.
/// Currently constructed only in tests but will be used when AnalyzerClient connects to rust-analyzer.
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct InlayHintCollection {
    hints: Vec<InlayHint>,
}

#[allow(dead_code)]
impl InlayHintCollection {
    /// Create an empty collection
    pub fn new() -> Self {
        Self::default()
    }

    /// Create from a list of hints
    pub fn from_hints(hints: Vec<InlayHint>) -> Self {
        Self { hints }
    }

    /// Parse from LSP response JSON
    pub fn from_lsp_response(response: &serde_json::Value) -> Result<Self, serde_json::Error> {
        let hints: Vec<LspInlayHint> = serde_json::from_value(response.clone())?;
        let hints = hints.into_iter().map(|h| h.into()).collect();
        Ok(Self { hints })
    }

    /// Get all hints
    pub fn all(&self) -> &[InlayHint] {
        &self.hints
    }

    /// Get type hints only
    pub fn type_hints(&self) -> impl Iterator<Item = &InlayHint> {
        self.hints.iter().filter(|h| h.is_type_hint())
    }

    /// Get parameter hints only
    pub fn parameter_hints(&self) -> impl Iterator<Item = &InlayHint> {
        self.hints.iter().filter(|h| h.is_parameter_hint())
    }

    /// Find hints at a specific line
    pub fn at_line(&self, line: u32) -> impl Iterator<Item = &InlayHint> {
        self.hints.iter().filter(move |h| h.position.line == line)
    }

    /// Find type hint for a variable at position
    pub fn type_at(&self, line: u32, character: u32) -> Option<&InlayHint> {
        self.hints.iter().find(|h| {
            h.is_type_hint() && h.position.line == line && h.position.character == character
        })
    }

    /// Get all Copy types in this collection
    pub fn copy_types(&self) -> impl Iterator<Item = &InlayHint> {
        self.type_hints().filter(|h| h.is_copy_type())
    }
}

/// LSP InlayHint response format
#[derive(Debug, Deserialize)]
struct LspInlayHint {
    position: LspPosition,
    label: LspLabel,
    kind: Option<u32>,
    #[serde(default)]
    tooltip: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize)]
struct LspPosition {
    line: u32,
    character: u32,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum LspLabel {
    String(String),
    Parts(Vec<LspLabelPart>),
}

#[derive(Debug, Deserialize)]
struct LspLabelPart {
    value: String,
}

impl From<LspInlayHint> for InlayHint {
    fn from(lsp: LspInlayHint) -> Self {
        let label = match lsp.label {
            LspLabel::String(s) => s,
            LspLabel::Parts(parts) => parts.into_iter().map(|p| p.value).collect(),
        };

        // LSP InlayHintKind: 1 = Type, 2 = Parameter
        let kind = match lsp.kind {
            Some(1) => InlayHintKind::Type,
            Some(2) => InlayHintKind::Parameter,
            _ => InlayHintKind::Type,
        };

        InlayHint {
            position: Position {
                line: lsp.position.line,
                character: lsp.position.character,
            },
            label,
            kind,
            has_tooltip: lsp.tooltip.is_some(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_inlay_hint_type() {
        let hint = InlayHint::type_hint(Position::new(0, 10), "i32");
        assert!(hint.is_type_hint());
        assert!(!hint.is_parameter_hint());
        assert_eq!(hint.type_name(), Some("i32"));
        assert!(hint.is_copy_type());
    }

    #[test]
    fn test_inlay_hint_parameter() {
        let hint = InlayHint::parameter_hint(Position::new(5, 20), "name");
        assert!(hint.is_parameter_hint());
        assert!(!hint.is_type_hint());
        assert_eq!(hint.type_name(), None);
    }

    #[test]
    fn test_type_hint_copy_detection() {
        let copy_hint = InlayHint::type_hint(Position::new(0, 0), "u64");
        assert!(copy_hint.is_copy_type());

        let non_copy_hint = InlayHint::type_hint(Position::new(0, 0), "String");
        assert!(!non_copy_hint.is_copy_type());
    }

    #[test]
    fn test_type_hint_from_inlay_hint() {
        let hint = InlayHint::type_hint(Position::new(1, 5), "&mut Vec<i32>");
        let type_hint = TypeHint::from_inlay_hint(&hint, "items").unwrap();

        assert_eq!(type_hint.target, "items");
        assert!(type_hint.is_reference);
        assert!(type_hint.is_mut);
        assert!(!type_hint.is_copy);
        assert_eq!(type_hint.base_type(), "Vec<i32>");
    }

    #[test]
    fn test_hint_collection() {
        let hints = vec![
            InlayHint::type_hint(Position::new(0, 10), "i32"),
            InlayHint::parameter_hint(Position::new(0, 20), "x"),
            InlayHint::type_hint(Position::new(1, 10), "String"),
        ];
        let collection = InlayHintCollection::from_hints(hints);

        assert_eq!(collection.all().len(), 3);
        assert_eq!(collection.type_hints().count(), 2);
        assert_eq!(collection.parameter_hints().count(), 1);
        assert_eq!(collection.copy_types().count(), 1);
    }

    #[test]
    fn test_query_to_lsp_params() {
        let query = InlayHintQuery::range("/src/lib.rs", 10, 20);
        let params = query.to_lsp_params();

        assert!(params["textDocument"]["uri"]
            .as_str()
            .unwrap()
            .contains("lib.rs"));
        assert_eq!(params["range"]["start"]["line"], 10);
        assert_eq!(params["range"]["end"]["line"], 20);
    }
}