use crate::ast::{Node, NodeKind};
use crate::position::{Position, Range};
use lsp_types::*;
use std::collections::HashMap;
use url::Url;
#[derive(Debug, Clone)]
pub struct InlayHintsProvider {
config: InlayHintsConfig,
hint_cache: HashMap<Url, Vec<InlayHint>>,
}
#[derive(Debug, Clone)]
pub struct InlayHintsConfig {
pub enable_type_hints: bool,
pub enable_parameter_hints: bool,
pub enable_return_type_hints: bool,
pub enable_declaration_hints: bool,
pub show_builtin_hints: bool,
pub max_hints_per_document: usize,
}
impl Default for InlayHintsConfig {
fn default() -> Self {
Self {
enable_type_hints: true,
enable_parameter_hints: true,
enable_return_type_hints: false, enable_declaration_hints: true,
show_builtin_hints: false, max_hints_per_document: 200,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InlayHintCategory {
Type,
Parameter,
ReturnType,
Declaration,
Lifetime,
Import,
}
impl InlayHintsProvider {
pub fn new() -> Self {
Self {
config: InlayHintsConfig::default(),
hint_cache: HashMap::new(),
}
}
pub fn with_config(config: InlayHintsConfig) -> Self {
Self {
config,
hint_cache: HashMap::new(),
}
}
pub fn inlay_hints(&self, params: InlayHintParams) -> Option<Vec<InlayHint>> {
let uri = params.text_document.uri;
let range = params.range;
let mut hints = Vec::new();
if self.config.enable_type_hints {
hints.push(InlayHint {
position: Position::new(0, 5),
label: InlayHintLabel::String(": Scalar".to_string()),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: "Variable type: Scalar".to_string(),
}),
padding_left: Some(true),
padding_right: Some(false),
data: None,
});
}
if self.config.enable_parameter_hints {
hints.push(InlayHint {
position: Position::new(1, 10),
label: InlayHintLabel::String("data: ".to_string()),
kind: Some(InlayHintKind::PARAMETER),
text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: "Parameter: data".to_string(),
}),
padding_left: Some(true),
padding_right: Some(false),
data: None,
});
}
hints.truncate(self.config.max_hints_per_document);
Some(hints)
}
pub fn resolve_inlay_hint(&self, hint: InlayHint) -> Option<InlayHint> {
let mut resolved = hint.clone();
if let Some(tooltip) = &resolved.tooltip {
let enhanced_value = format!(
"{}\n\n**Additional Information:**\nThis hint provides context about the code.",
tooltip.value
);
resolved.tooltip = Some(MarkupContent {
kind: MarkupKind::Markdown,
value: enhanced_value,
});
}
Some(resolved)
}
fn create_type_hint(&self, position: Position, type_name: &str) -> InlayHint {
InlayHint {
position,
label: InlayHintLabel::String(format!(": {}", type_name)),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("Inferred type: {}", type_name),
}),
padding_left: Some(true),
padding_right: Some(false),
data: None,
}
}
fn create_parameter_hint(&self, position: Position, parameter_name: &str) -> InlayHint {
InlayHint {
position,
label: InlayHintLabel::String(format!("{}: ", parameter_name)),
kind: Some(InlayHintKind::PARAMETER),
text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("Parameter: {}", parameter_name),
}),
padding_left: Some(true),
padding_right: Some(false),
data: None,
}
}
fn create_return_type_hint(&self, position: Position, return_type: &str) -> InlayHint {
InlayHint {
position,
label: InlayHintLabel::String(format!("-> {}", return_type)),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("Return type: {}", return_type),
}),
padding_left: Some(true),
padding_right: Some(false),
data: None,
}
}
fn create_declaration_hint(&self, position: Position, declaration_info: &str) -> InlayHint {
InlayHint {
position,
label: InlayHintLabel::String(format!("/* {} */", declaration_info)),
kind: None, text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("Declaration: {}", declaration_info),
}),
padding_left: Some(false),
padding_right: Some(true),
data: None,
}
}
pub fn update_cache(&mut self, uri: Url, hints: Vec<InlayHint>) {
self.hint_cache.insert(uri, hints);
}
pub fn clear_cache(&mut self) {
self.hint_cache.clear();
}
pub fn cache_stats(&self) -> (usize, usize) {
let document_count = self.hint_cache.len();
let total_hints: usize = self.hint_cache.values()
.map(|hints| hints.len())
.sum();
(document_count, total_hints)
}
}
impl Default for InlayHintsProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_tdd_support::{must, must_some};
#[test]
fn test_inlay_hints_provider_creation() {
let provider = InlayHintsProvider::new();
assert!(provider.config.enable_type_hints);
assert!(provider.config.enable_parameter_hints);
assert!(!provider.config.enable_return_type_hints);
assert!(provider.config.enable_declaration_hints);
assert!(!provider.config.show_builtin_hints);
assert_eq!(provider.config.max_hints_per_document, 200);
}
#[test]
fn test_custom_config() {
let config = InlayHintsConfig {
enable_type_hints: false,
enable_parameter_hints: true,
enable_return_type_hints: true,
enable_declaration_hints: false,
show_builtin_hints: true,
max_hints_per_document: 100,
};
let provider = InlayHintsProvider::with_config(config);
assert!(!provider.config.enable_type_hints);
assert!(provider.config.enable_parameter_hints);
assert!(provider.config.enable_return_type_hints);
assert!(!provider.config.enable_declaration_hints);
assert!(provider.config.show_builtin_hints);
assert_eq!(provider.config.max_hints_per_document, 100);
}
#[test]
fn test_inlay_hints() {
let provider = InlayHintsProvider::new();
let params = InlayHintParams {
text_document: lsp_types::TextDocumentIdentifier {
uri: must(Url::parse("file:///test.pl"))
},
range: Range::new(Position::new(0, 0), Position::new(10, 0)),
work_done_progress_params: Default::default(),
};
let hints = provider.inlay_hints(params);
assert!(hints.is_some());
assert!(!must_some(hints).is_empty());
}
#[test]
fn test_resolve_inlay_hint() {
let provider = InlayHintsProvider::new();
let hint = InlayHint {
position: Position::new(0, 0),
label: InlayHintLabel::String(": Scalar".to_string()),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: Some(MarkupContent {
kind: MarkupKind::Markdown,
value: "Variable type: Scalar".to_string(),
}),
padding_left: Some(true),
padding_right: Some(false),
data: None,
};
let resolved = provider.resolve_inlay_hint(hint);
assert!(resolved.is_some());
let resolved_hint = must_some(resolved);
assert!(resolved_hint.tooltip.is_some());
let tooltip_value = &must_some(resolved_hint.tooltip).value;
assert!(tooltip_value.contains("Additional Information"));
}
#[test]
fn test_hint_creation() {
let provider = InlayHintsProvider::new();
let type_hint = provider.create_type_hint(Position::new(0, 5), "Scalar");
assert_eq!(type_hint.kind, Some(InlayHintKind::TYPE));
assert!(must_some(type_hint.padding_left));
assert!(!must_some(type_hint.padding_right));
let param_hint = provider.create_parameter_hint(Position::new(1, 10), "data");
assert_eq!(param_hint.kind, Some(InlayHintKind::PARAMETER));
assert!(must_some(param_hint.padding_left));
assert!(!must_some(param_hint.padding_right));
let return_hint = provider.create_return_type_hint(Position::new(2, 15), "Array");
assert_eq!(return_hint.kind, Some(InlayHintKind::TYPE));
let decl_hint = provider.create_declaration_hint(Position::new(3, 20), "my $x");
assert!(must_some(decl_hint.padding_right));
}
#[test]
fn test_cache_operations() {
let mut provider = InlayHintsProvider::new();
let (documents, hints) = provider.cache_stats();
assert_eq!(documents, 0);
assert_eq!(hints, 0);
let uri = must(Url::parse("file:///test.pl"));
let hints = vec![InlayHint {
position: Position::new(0, 0),
label: InlayHintLabel::String(": Scalar".to_string()),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: Some(false),
data: None,
}];
provider.update_cache(uri.clone(), hints);
let (documents, total_hints) = provider.cache_stats();
assert_eq!(documents, 1);
assert_eq!(total_hints, 1);
provider.clear_cache();
let (documents, hints) = provider.cache_stats();
assert_eq!(documents, 0);
assert_eq!(hints, 0);
}
}