use apollo_compiler::Schema;
use apollo_parser::{
cst::{CstNode, Document},
SyntaxKind,
};
use ropey::Rope;
use crate::utils::{
get_ancestor_node_of_kind::get_ancestor_node_of_kind,
lsp_range_from_cst_textrange::lsp_range_from_cst_textrange,
};
pub fn get_hover_at_position(
document: &Document,
schema: &Schema,
source_text: &Rope,
position: &lsp::Position,
) -> Option<lsp::Hover> {
let char = source_text.try_line_to_byte(position.line as usize).ok()?;
let offset = char + position.character as usize;
if document.syntax().text_range().end() < offset.try_into().unwrap() {
return None;
}
hover_named_type(document, schema, source_text, offset).or(hover_directive_application(
document,
schema,
source_text,
offset,
))
}
fn hover_named_type(
document: &Document,
schema: &Schema,
source_text: &Rope,
offset: usize,
) -> Option<lsp::Hover> {
let token = document
.syntax()
.token_at_offset((offset).try_into().unwrap())
.right_biased()?;
let parent = token.parent()?;
let grandparent = parent.parent()?;
let type_in_schema = schema.types.get(token.text())?;
(token.kind() == SyntaxKind::IDENT
&& parent.kind() == SyntaxKind::NAME
&& grandparent.kind() == SyntaxKind::NAMED_TYPE)
.then(|| lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: format_hover_contents(&type_in_schema.to_string()),
}),
range: Some(lsp_range_from_cst_textrange(
token.text_range(),
source_text,
None,
)),
})
}
fn hover_directive_application(
document: &Document,
schema: &Schema,
source_text: &Rope,
offset: usize,
) -> Option<lsp::Hover> {
let token = document
.syntax()
.token_at_offset((offset as u32).into())
.right_biased()?
.parent()?;
let directive_node = get_ancestor_node_of_kind(
&token,
SyntaxKind::DIRECTIVE,
Some(vec![SyntaxKind::ARGUMENTS]),
)?;
let directive_name = directive_node.children().next()?;
let directive_in_schema = schema
.directive_definitions
.get(directive_name.text().to_string().as_str())?;
let range = lsp_range_from_cst_textrange(directive_name.text_range(), source_text, None);
Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: format_hover_contents(&directive_in_schema.to_string()),
}),
range: Some(lsp::Range {
start: lsp::Position {
line: range.start.line,
character: range.start.character - 1,
},
end: range.end,
}),
})
}
fn format_hover_contents(serialized_definition: &str) -> String {
if serialized_definition.starts_with("\"\"\"") {
let vec = serialized_definition
.splitn(3, "\"\"\"")
.collect::<Vec<_>>();
format!(
"{}\n***\n```graphql\n{}\n```",
vec[1].trim(),
vec[2].trim()
)
} else {
format!("```graphql\n{}\n```", serialized_definition)
}
}
#[cfg(test)]
mod tests {
use crate::graph::{supergraph::KnownSubgraphs, Graph, GraphConfig};
use insta::assert_snapshot;
use tower_lsp::lsp_types::{self as lsp, HoverContents};
const URI: &str = "file:///test.graphql";
fn get_testing_graph() -> Graph {
Graph::new(
lsp::Url::parse(URI).unwrap(),
r#"
type Query {
user: User
me: ID!
}
"""
User description
spanning multiple lines
"""
type User {
id: ID! @deprecated(reason: "Use `me` instead")
me: ID!
name: String
}
"#
.to_string(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
)
}
fn get_hover_start_end_cases(
graph: &Graph,
line: u32,
character: u32,
length: u32,
) -> (lsp::Hover, lsp::Hover) {
let before_start = graph.on_hover(
&lsp::Url::parse(URI).unwrap(),
&lsp::Position {
line,
character: character - 1,
},
);
let start = graph.on_hover(
&lsp::Url::parse(URI).unwrap(),
&lsp::Position { line, character },
);
let end = graph.on_hover(
&lsp::Url::parse(URI).unwrap(),
&lsp::Position {
line,
character: character + length - 1,
},
);
let beyond_end = graph.on_hover(
&lsp::Url::parse(URI).unwrap(),
&lsp::Position {
line,
character: character + length,
},
);
assert!(before_start.is_none());
assert!(beyond_end.is_none());
(
start.expect("Expected `start` to be Some"),
end.expect("Expected `end` to be Some"),
)
}
#[test]
fn test_hover_builtin_type() {
let graph = get_testing_graph();
let (start, end) = get_hover_start_end_cases(&graph, 3, 6, 2);
let HoverContents::Markup(markup) = &start.contents else {
panic!("Expected HoverContents::Markup");
};
assert_snapshot!(markup.value);
assert_eq!(start.range.unwrap().start.character, 6);
assert_eq!(start.range.unwrap().end.character, 8);
assert_eq!(end, start);
}
#[test]
fn test_hover_user_type() {
let graph = get_testing_graph();
let (start, end) = get_hover_start_end_cases(&graph, 2, 8, 4);
let HoverContents::Markup(markup) = &start.contents else {
panic!("Expected HoverContents::Markup");
};
assert_snapshot!(markup.value);
assert_eq!(start.range.unwrap().start.character, 8);
assert_eq!(start.range.unwrap().end.character, 12);
assert_eq!(end, start);
}
#[test]
fn test_hover_directive() {
let graph = get_testing_graph();
let (start, end) = get_hover_start_end_cases(&graph, 11, 10, 11);
let HoverContents::Markup(markup) = &start.contents else {
panic!("Expected HoverContents::Markup");
};
assert_snapshot!(markup.value);
assert_eq!(start.range.unwrap().start.character, 10);
assert_eq!(start.range.unwrap().end.character, 21);
assert_eq!(end, start);
}
}