use std::sync::{Arc, RwLock};
use itertools::Itertools;
use tower_lsp_server::ls_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, Range};
use url::Url;
pub fn response(
hir: Arc<RwLock<mq_hir::Hir>>,
url: Url,
type_env: Option<mq_check::TypeEnv>,
position: Position,
) -> Option<Hover> {
let source = hir.read().unwrap().source_by_url(&url);
if let Some(source) = source {
if let Some((symbol_id, symbol)) = hir.read().unwrap().find_symbol_in_position(
source,
mq_lang::Position::new(position.line + 1, (position.character + 1) as usize),
) {
match &symbol.kind {
mq_hir::SymbolKind::Function(_)
| mq_hir::SymbolKind::Macro(_)
| mq_hir::SymbolKind::Variable
| mq_hir::SymbolKind::DestructuringBinding
| mq_hir::SymbolKind::PatternVariable { .. } => {
let deprecated = symbol.is_deprecated();
let deprecated_notice = if deprecated { "⚠️ **DEPRECATED**\n\n" } else { "" };
let type_scheme = type_env.as_ref().and_then(|env| env.get(&symbol_id));
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: match &symbol.kind {
mq_hir::SymbolKind::Function(args) | mq_hir::SymbolKind::Macro(args) => {
let type_annotation =
type_scheme.map(|s| format!(": {}", s.ty)).unwrap_or_default();
format!(
"```mq\n{}({}){}\n```\n\n{deprecated_notice}{doc}",
&symbol.value.unwrap_or_default(),
args.iter().map(|p| p.to_string()).join(", "),
type_annotation,
deprecated_notice = deprecated_notice,
doc = symbol.doc.iter().map(|(_, doc)| doc).join("\n")
)
}
mq_hir::SymbolKind::Variable
| mq_hir::SymbolKind::DestructuringBinding
| mq_hir::SymbolKind::PatternVariable { .. } => {
let type_annotation =
type_scheme.map(|s| format!(": {}", s.ty)).unwrap_or_default();
format!(
"```mq\n{}{}\n```\n\n{deprecated_notice}{doc}",
&symbol.value.unwrap_or_default(),
type_annotation,
deprecated_notice = deprecated_notice,
doc = symbol.doc.iter().map(|(_, doc)| doc).join("\n")
)
}
_ => String::new(),
},
}),
range: symbol.source.text_range.map(|text_range| {
Range::new(
Position::new(text_range.start.line - 1, (text_range.start.column - 1) as u32),
Position::new(text_range.end.line - 1, (text_range.end.column - 1) as u32),
)
}),
})
}
_ => None,
}
} else {
None
}
} else {
None
}
}
#[cfg(test)]
mod tests {
use mq_hir::Hir;
use tower_lsp_server::ls_types;
use super::*;
#[test]
fn test_function_hover() {
let mut hir = Hir::default();
let url = Url::parse("file:///test.mq").unwrap();
let position = Position::new(0, 5);
hir.add_code(Some(url.clone()), "def func1(): 1;");
let hover = response(Arc::new(RwLock::new(hir)), url, None, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let HoverContents::Markup(content) = hover.contents {
assert_eq!(content.kind, MarkupKind::Markdown);
assert!(content.value.contains("func1"));
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_val_hover() {
let mut hir = Hir::default();
let url = Url::parse("file:///test.mq").unwrap();
let position = Position::new(0, 5);
hir.add_code(Some(url.clone()), "let val = 1 | val");
let hover = response(Arc::new(RwLock::new(hir)), url, None, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let HoverContents::Markup(content) = hover.contents {
assert_eq!(content.kind, MarkupKind::Markdown);
assert!(content.value.contains("val"));
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_no_symbol_at_position() {
let hir = Hir::default();
let url = Url::parse("file:///test.mq").unwrap();
let position = Position::new(10, 10);
let hover = response(Arc::new(RwLock::new(hir)), url, None, position);
assert!(hover.is_none());
}
#[test]
fn test_invalid_url() {
let hir = Hir::default();
let url = Url::parse("file:///nonexistent.mq").unwrap();
let position = Position::new(0, 0);
let hover = response(Arc::new(RwLock::new(hir)), url, None, position);
assert!(hover.is_none());
}
#[test]
fn test_builtin_function_hover() {
let mut hir = Hir::default();
let url = Url::parse("file:///test.mq").unwrap();
hir.add_code(Some(url.clone()), "\"hello\" | len");
let position = Position::new(0, 11);
let hover = response(Arc::new(RwLock::new(hir)), url, None, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let ls_types::HoverContents::Markup(content) = hover.contents {
assert_eq!(content.kind, ls_types::MarkupKind::Markdown);
assert!(content.value.contains("len(value)"));
assert!(
content
.value
.contains("Returns the length of the given string or array")
);
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_deprecated_function_hover() {
let mut hir = Hir::default();
let url = Url::parse("file:///test.mq").unwrap();
let code = r#"# deprecated: This function is no longer supported
def old_func(x): x + 1;"#;
hir.add_code(Some(url.clone()), code);
let position = Position::new(1, 5);
let hover = response(Arc::new(RwLock::new(hir)), url, None, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let ls_types::HoverContents::Markup(content) = hover.contents {
assert_eq!(content.kind, ls_types::MarkupKind::Markdown);
assert!(content.value.contains("DEPRECATED"), "Should contain DEPRECATED marker");
assert!(
content
.value
.contains("deprecated: This function is no longer supported"),
"Should contain deprecation message"
);
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_hover_with_type_info() {
use mq_check::TypeChecker;
let mut hir = Hir::default();
let url = Url::parse("file:///test.mq").unwrap();
hir.add_code(Some(url.clone()), "let val = 1 | val");
let hir = Arc::new(RwLock::new(hir));
let mut checker = TypeChecker::new();
checker.check(&hir.read().unwrap());
let type_env = Some(checker.symbol_types().clone());
let position = Position::new(0, 5);
let hover = response(Arc::clone(&hir), url, type_env, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let HoverContents::Markup(content) = hover.contents {
assert_eq!(content.kind, MarkupKind::Markdown);
assert!(content.value.contains("val"));
assert!(content.value.contains(":"), "Should contain type annotation");
} else {
panic!("Expected markup content");
}
}
}