use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Default)]
pub enum InlayHintKind {
#[default]
Type,
Parameter,
Chaining,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Position {
pub line: u32,
pub character: u32,
}
impl Position {
pub fn new(line: u32, character: u32) -> Self {
Self { line, character }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InlayHint {
pub position: Position,
pub label: String,
pub kind: InlayHintKind,
pub has_tooltip: bool,
}
impl InlayHint {
pub fn type_hint(position: Position, type_name: impl Into<String>) -> Self {
Self {
position,
label: type_name.into(),
kind: InlayHintKind::Type,
has_tooltip: false,
}
}
pub fn parameter_hint(position: Position, param_name: impl Into<String>) -> Self {
Self {
position,
label: param_name.into(),
kind: InlayHintKind::Parameter,
has_tooltip: false,
}
}
pub fn is_type_hint(&self) -> bool {
self.kind == InlayHintKind::Type
}
pub fn is_parameter_hint(&self) -> bool {
self.kind == InlayHintKind::Parameter
}
pub fn type_name(&self) -> Option<&str> {
if self.is_type_hint() {
Some(&self.label)
} else {
None
}
}
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
}
}
}
#[derive(Debug, Clone)]
pub struct TypeHint {
pub target: String,
pub type_name: String,
pub position: Position,
pub is_copy: bool,
pub is_reference: bool,
pub is_mut: bool,
}
impl TypeHint {
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,
})
}
pub fn base_type(&self) -> &str {
self.type_name
.trim_start_matches('&')
.trim_start_matches("mut ")
.trim()
}
}
#[derive(Debug, Clone)]
pub struct InlayHintQuery {
pub file: PathBuf,
pub start_line: Option<u32>,
pub end_line: Option<u32>,
pub kinds: Option<Vec<InlayHintKind>>,
}
impl InlayHintQuery {
pub fn file(path: impl Into<PathBuf>) -> Self {
Self {
file: path.into(),
start_line: None,
end_line: None,
kinds: None,
}
}
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,
}
}
pub fn types_only(mut self) -> Self {
self.kinds = Some(vec![InlayHintKind::Type]);
self
}
pub fn parameters_only(mut self) -> Self {
self.kinds = Some(vec![InlayHintKind::Parameter]);
self
}
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 }
}
})
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct InlayHintCollection {
hints: Vec<InlayHint>,
}
#[allow(dead_code)]
impl InlayHintCollection {
pub fn new() -> Self {
Self::default()
}
pub fn from_hints(hints: Vec<InlayHint>) -> Self {
Self { hints }
}
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 })
}
pub fn all(&self) -> &[InlayHint] {
&self.hints
}
pub fn type_hints(&self) -> impl Iterator<Item = &InlayHint> {
self.hints.iter().filter(|h| h.is_type_hint())
}
pub fn parameter_hints(&self) -> impl Iterator<Item = &InlayHint> {
self.hints.iter().filter(|h| h.is_parameter_hint())
}
pub fn at_line(&self, line: u32) -> impl Iterator<Item = &InlayHint> {
self.hints.iter().filter(move |h| h.position.line == line)
}
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
})
}
pub fn copy_types(&self) -> impl Iterator<Item = &InlayHint> {
self.type_hints().filter(|h| h.is_copy_type())
}
}
#[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(),
};
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);
}
}