use std::collections::HashMap;
use crate::{
federation::link::ParsedLink,
graph::Graph,
graphql::{
DEPRECATED_DIRECTIVE, DIRECTIVE_LOCATIONS, EXTEND, GRAPHQL_KEYWORDS, ON, REPEATABLE, SCHEMA,
},
server::MaxSpecVersions,
specs::{SpecDirective, KNOWN_SPECS},
utils::{
get_ancestor_node_of_kind::get_ancestor_node_of_kind,
lsp_range_from_cst_textrange::lsp_range_from_cst_textrange,
},
};
use apollo_compiler::{
ast::{DirectiveDefinition, DirectiveLocation, Type},
schema::ExtendedType,
Name, Schema,
};
use apollo_parser::{cst::CstNode, SyntaxKind, SyntaxNode};
use memoize::memoize;
mod custom_snippets;
use custom_snippets::apply_custom_completions_for_spec_directive;
use ropey::Rope;
const EXTEND_KEYWORDS: [&str; 7] = [
"extend enum",
"extend type",
"extend scalar",
"extend schema",
"extend interface",
"extend union",
"extend input",
];
pub fn get_completions_at_position(
graph: &Graph,
subgraph_uri: &lsp::Url, position: lsp::Position,
max_spec_versions: &MaxSpecVersions,
) -> Option<lsp::CompletionResponse> {
let (cst, source_text, schema, builtins, specs_with_aliases, links) = match graph {
Graph::Monolith(monolith) => (
&monolith.cst,
&monolith.source_text,
monolith.schema(),
None,
None,
None,
),
Graph::Supergraph(supergraph) => {
let subgraph = supergraph
.subgraph_by_uri(subgraph_uri)
.unwrap_or_else(|| panic!("No subgraph found at uri: {}", subgraph_uri));
(
&subgraph.cst,
&subgraph.source_text,
subgraph.schema(),
Some(&subgraph.builtins),
Some(subgraph.specs_with_aliases()),
Some(subgraph.links()),
)
}
};
let alias_to_spec_map = specs_with_aliases
.map(|specs| {
specs
.iter()
.map(|(alias, spec)| (spec, alias))
.collect::<HashMap<&String, &String>>()
})
.unwrap_or_default();
let has_schema_definition = schema.schema_definition.location().is_some();
let char = source_text.try_line_to_byte(position.line as usize).ok()?;
let offset = char + position.character as usize;
let doc = cst.document();
if doc.syntax().text_range().end() < offset.try_into().unwrap() {
return None;
}
let token_at_offset = doc.syntax().token_at_offset(offset.try_into().unwrap());
let token = token_at_offset.clone().left_biased()?;
let matching_link_directive = links.and_then(|links| {
links.values().find(|parsed_link| {
parsed_link.node.location().is_some_and(|location| {
location.offset() <= offset && offset <= location.end_offset()
})
})
});
if let Some(right_token) = token_at_offset.right_biased() {
if right_token.kind() == SyntaxKind::STRING
&& token.kind() == SyntaxKind::STRING
&& matching_link_directive.is_none()
{
return None;
}
}
let nearest_whitespace_token_or_non_error = if token.kind() == SyntaxKind::WHITESPACE {
token.clone()
} else if token.kind() == SyntaxKind::ERROR {
if let Some(prev) = token.prev_sibling_or_token() {
if let Some(prev_node) = prev.as_node() {
let child_tokens = prev_node
.children_with_tokens()
.filter(|c| c.as_token().is_some());
let last_token = child_tokens.last().unwrap_or(prev.clone());
last_token.into_token().unwrap_or(token.clone())
} else if let Some(prev_token) = prev.as_token() {
prev_token.clone()
} else {
token.clone()
}
} else {
token.clone()
}
} else if token.kind() == SyntaxKind::PIPE || token.kind() == SyntaxKind::AMP {
token.next_token().unwrap_or(token.clone()).clone()
} else {
token.prev_token().unwrap_or(token.clone()).clone()
};
let parent = token.parent()?;
let prev_token = if nearest_whitespace_token_or_non_error.kind() == SyntaxKind::WHITESPACE {
match nearest_whitespace_token_or_non_error.prev_token() {
Some(token) => token.clone(),
None => {
if parent.kind() == SyntaxKind::DOCUMENT
&& token.text() != "@"
&& token.text() != "|"
&& token.text() != "&"
&& token.text() != "\""
{
return Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: get_root_completions(has_schema_definition),
}));
} else {
return None;
}
}
}
} else {
nearest_whitespace_token_or_non_error.clone()
};
let prev_token_parent = prev_token.parent()?;
if get_ancestor_node_of_kind(&parent, SyntaxKind::DESCRIPTION, None).is_some() {
return None;
}
if prev_token.kind() == SyntaxKind::type_KW
&& prev_token_parent.clone().kind() == SyntaxKind::OBJECT_TYPE_EXTENSION
&& (parent.kind() == SyntaxKind::NAME || !has_name_node_as_child(&prev_token_parent))
{
return Some(get_completions_for_extension(schema, builtins, |t| {
t.is_object() && !t.is_built_in()
}));
}
if prev_token.kind() == SyntaxKind::scalar_KW
&& prev_token_parent.clone().kind() == SyntaxKind::SCALAR_TYPE_EXTENSION
&& (parent.kind() == SyntaxKind::NAME || !has_name_node_as_child(&prev_token_parent))
{
return Some(get_completions_for_extension(
schema,
builtins,
ExtendedType::is_scalar,
));
}
if prev_token.kind() == SyntaxKind::interface_KW
&& prev_token_parent.clone().kind() == SyntaxKind::INTERFACE_TYPE_EXTENSION
&& (parent.kind() == SyntaxKind::NAME || !has_name_node_as_child(&prev_token_parent))
{
return Some(get_completions_for_extension(
schema,
builtins,
ExtendedType::is_interface,
));
}
if prev_token.kind() == SyntaxKind::union_KW
&& prev_token_parent.clone().kind() == SyntaxKind::UNION_TYPE_EXTENSION
&& (parent.kind() == SyntaxKind::NAME || !has_name_node_as_child(&prev_token_parent))
{
return Some(get_completions_for_extension(
schema,
builtins,
ExtendedType::is_union,
));
}
if prev_token.kind() == SyntaxKind::input_KW
&& prev_token_parent.clone().kind() == SyntaxKind::INPUT_OBJECT_TYPE_EXTENSION
&& (parent.kind() == SyntaxKind::NAME || !has_name_node_as_child(&prev_token_parent))
{
return Some(get_completions_for_extension(
schema,
builtins,
ExtendedType::is_input_object,
));
}
if prev_token.kind() == SyntaxKind::enum_KW
&& prev_token_parent.clone().kind() == SyntaxKind::ENUM_TYPE_EXTENSION
&& (parent.kind() == SyntaxKind::NAME || !has_name_node_as_child(&prev_token_parent))
{
return Some(get_completions_for_extension(schema, builtins, |t| {
t.is_enum() && !t.is_built_in()
}));
}
let unwrapped_node = unwrap_node(&prev_token_parent);
let mut completion_items = vec![];
if let Some(directive_location) = get_directive_location(&unwrapped_node) {
let current_token_text: String =
match get_ancestor_node_of_kind(&parent, SyntaxKind::DIRECTIVE, None) {
Some(directive) => directive.text().to_string(),
None => token.text().to_string(),
};
let is_after_directive =
get_ancestor_node_of_kind(&prev_token_parent, SyntaxKind::DIRECTIVE, None).is_some()
&& parent.kind() != SyntaxKind::ARGUMENTS;
let is_writing_directive_name = parent.kind() == SyntaxKind::NAME
&& get_ancestor_node_of_kind(&parent, SyntaxKind::DIRECTIVES, None).is_some();
let should_suggest_directives = matching_link_directive.is_none()
&& (is_after_directive
|| is_writing_directive_name
|| match directive_location {
DirectiveLocation::Schema => prev_token.kind() == SyntaxKind::schema_KW,
DirectiveLocation::Object | DirectiveLocation::Interface => {
let is_before_implements_keyword =
token.next_token().is_some_and(|next_token| {
next_token.kind() == SyntaxKind::implements_KW
});
let maybe_prev_implementations = get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::IMPLEMENTS_INTERFACES,
None,
);
!is_before_implements_keyword
&& prev_token.kind() != SyntaxKind::implements_KW
&& prev_token.kind() != SyntaxKind::AMP
&& (prev_token_parent.kind() == SyntaxKind::NAME
|| maybe_prev_implementations.is_some())
}
DirectiveLocation::Scalar
| DirectiveLocation::Enum
| DirectiveLocation::Union
| DirectiveLocation::InputObject => {
let is_within_definition = get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::ENUM_VALUES_DEFINITION,
None,
)
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::UNION_MEMBER_TYPES,
None,
))
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::INPUT_FIELDS_DEFINITION,
None,
))
.is_some();
prev_token_parent.kind() == SyntaxKind::NAME && !is_within_definition
}
DirectiveLocation::FieldDefinition => {
let maybe_named_type = get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::NAMED_TYPE,
None,
)
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::LIST_TYPE,
None,
))
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::NON_NULL_TYPE,
None,
));
maybe_named_type.is_some_and(|t| {
t.kind() != SyntaxKind::LIST_TYPE
|| prev_token.kind() != SyntaxKind::L_BRACK
})
}
DirectiveLocation::ArgumentDefinition
| DirectiveLocation::InputFieldDefinition => {
let maybe_named_type_or_default_value = get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::NAMED_TYPE,
None,
)
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::LIST_TYPE,
None,
))
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::NON_NULL_TYPE,
None,
))
.or(get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::DEFAULT_VALUE,
None,
));
maybe_named_type_or_default_value.is_some_and(|t| {
t.kind() != SyntaxKind::LIST_TYPE
|| prev_token.kind() != SyntaxKind::L_BRACK
})
}
DirectiveLocation::EnumValue => {
let maybe_enum_value = get_ancestor_node_of_kind(
&prev_token_parent,
SyntaxKind::ENUM_VALUE,
None,
);
maybe_enum_value.is_some()
}
_ => false,
});
if should_suggest_directives {
let should_include_at_prefix = !current_token_text.starts_with('@');
let next_token_is_args_list = token
.next_token()
.map(|t| t.kind() == SyntaxKind::L_PAREN)
.unwrap_or_default();
completion_items.extend(get_completions_for_directive(
schema,
source_text,
&directive_location,
should_include_at_prefix,
!next_token_is_args_list,
alias_to_spec_map,
links,
max_spec_versions,
))
}
}
if token.text() != "@"
&& (token.text() != "|" || unwrapped_node.kind() == SyntaxKind::DIRECTIVE_DEFINITION)
&& (!token.text().starts_with('"') || matching_link_directive.is_some())
{
match unwrapped_node.kind() {
SyntaxKind::OBJECT_TYPE_DEFINITION
| SyntaxKind::OBJECT_TYPE_EXTENSION
| SyntaxKind::INTERFACE_TYPE_DEFINITION
| SyntaxKind::INTERFACE_TYPE_EXTENSION => {
if prev_token_parent.kind() == SyntaxKind::NAME
&& !unwrapped_node
.children()
.any(|child| child.kind() == SyntaxKind::IMPLEMENTS_INTERFACES)
{
completion_items.push(lsp::CompletionItem {
label: "implements".to_string(),
kind: Some(lsp::CompletionItemKind::KEYWORD),
commit_characters: Some(vec![" ".to_string()]),
..Default::default()
})
}
if prev_token_parent.kind() == SyntaxKind::IMPLEMENTS_INTERFACES
&& (prev_token.kind() == SyntaxKind::implements_KW
|| prev_token.kind() == SyntaxKind::AMP)
{
let current_object_name = get_node_name(&unwrapped_node)
.expect("Expected a name for the current graphql definition");
let existing_interfaces =
schema.types.iter().find_map(|(name, t)| match t {
ExtendedType::Object(t) => {
if name.to_string() == current_object_name {
Some(
t.implements_interfaces
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
)
} else {
None
}
}
ExtendedType::Interface(t) => {
if name.to_string() == current_object_name {
Some(
t.implements_interfaces
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
)
} else {
None
}
}
_ => None,
})?;
let items = schema
.types
.iter()
.filter_map(|(name, t)| {
if t.is_interface()
&& !existing_interfaces.contains(&name.to_string())
&& name.to_string() != current_object_name
{
Some(get_type_completion_item(
t,
lsp::CompletionItemKind::INTERFACE,
None,
))
} else {
None
}
})
.collect::<Vec<_>>();
completion_items.extend(items)
}
}
SyntaxKind::UNION_TYPE_DEFINITION | SyntaxKind::UNION_TYPE_EXTENSION => {
if prev_token_parent.kind() == SyntaxKind::UNION_MEMBER_TYPES
&& (prev_token.kind() == SyntaxKind::EQ
|| prev_token.kind() == SyntaxKind::PIPE)
{
let current_object_name = get_node_name(&unwrapped_node)
.expect("Expected a name for the current graphql definition");
let existing_members = schema.types.iter().find_map(|(name, t)| match t {
ExtendedType::Union(t) => {
if name.to_string() == current_object_name {
Some(
t.members
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
)
} else {
None
}
}
_ => None,
})?;
let items = schema
.types
.iter()
.filter_map(|(name, t)| {
if t.is_object()
&& !t.is_built_in()
&& !existing_members.contains(&name.to_string())
&& !builtins.is_some_and(|b| b.contains(&name.to_string()))
{
Some(get_type_completion_item(
t,
lsp::CompletionItemKind::FUNCTION,
None,
))
} else {
None
}
})
.collect::<Vec<_>>();
completion_items.extend(items)
}
}
SyntaxKind::DIRECTIVE_DEFINITION => {
if prev_token_parent.kind() == SyntaxKind::NAME
|| prev_token_parent.kind() == SyntaxKind::ARGUMENTS_DEFINITION
{
return Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: vec![
lsp::CompletionItem {
label: format!("{} {}", REPEATABLE, ON),
kind: Some(lsp::CompletionItemKind::KEYWORD),
insert_text: Some(get_snippet_for_directive_location(format!(
"{} {}",
REPEATABLE, ON
))),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
commit_characters: Some(vec![" ".to_string()]),
..Default::default()
},
lsp::CompletionItem {
label: ON.to_string(),
kind: Some(lsp::CompletionItemKind::KEYWORD),
insert_text: Some(get_snippet_for_directive_location(
ON.to_string(),
)),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
commit_characters: Some(vec![" ".to_string()]),
..Default::default()
},
],
}));
}
if (prev_token.kind() == SyntaxKind::on_KW
&& prev_token_parent.clone().kind() == SyntaxKind::DIRECTIVE_DEFINITION)
|| (prev_token.kind() == SyntaxKind::PIPE
&& prev_token_parent.clone().kind() == SyntaxKind::DIRECTIVE_LOCATIONS)
{
let current_directive_name = get_node_name(&unwrapped_node)?;
let existing_locations =
schema.directive_definitions.iter().find_map(|(name, d)| {
if name.to_string() == current_directive_name {
Some(
d.locations
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
)
} else {
None
}
})?;
return Some(get_completions_for_directive_location(
existing_locations,
token.prev_token().map_or(true, |t| {
t.kind() == SyntaxKind::WHITESPACE && token.kind() == SyntaxKind::PIPE
}),
));
}
}
SyntaxKind::FIELD_DEFINITION | SyntaxKind::INPUT_VALUE_DEFINITION => {
let unwrapped_type = unwrap_type(&parent);
if prev_token.kind() == SyntaxKind::COLON
|| unwrapped_type.kind() == SyntaxKind::NAMED_TYPE
{
let is_field_definition = unwrapped_node.kind() == SyntaxKind::FIELD_DEFINITION;
let current_object_name = if let Some(parent) = unwrapped_node.parent() {
if parent.kind() != SyntaxKind::FIELD_DEFINITION
&& parent.kind() != SyntaxKind::INPUT_VALUE_DEFINITION
{
get_node_name(&parent).unwrap_or_default()
} else {
"".to_string()
}
} else {
"".to_string()
};
let items = schema
.types
.iter()
.filter_map(|(name, t)| {
let is_correct_response_type = if is_field_definition {
t.is_output_type()
} else {
t.is_input_type()
};
if is_correct_response_type
&& !name.starts_with("__")
&& (!t.is_input_type() || name.to_string() != current_object_name)
&& !builtins.is_some_and(|b| b.contains(&name.to_string()))
{
Some(get_type_completion_item(
t,
match t {
ExtendedType::Scalar(_) => lsp::CompletionItemKind::STRUCT,
ExtendedType::Enum(_) => lsp::CompletionItemKind::ENUM,
ExtendedType::Object(_)
| ExtendedType::Interface(_)
| ExtendedType::Union(_)
| ExtendedType::InputObject(_) => {
lsp::CompletionItemKind::FUNCTION
}
},
Some(lsp::CompletionItem {
commit_characters: Some(vec![
" ".to_string(),
"!".to_string(),
",".to_string(), ]),
..Default::default()
}),
))
} else {
None
}
})
.collect::<Vec<_>>();
completion_items.extend(items);
}
}
_ => {}
}
if let Some(argument) = get_ancestor_node_of_kind(
&parent,
SyntaxKind::ARGUMENT,
Some(vec![SyntaxKind::OBJECT_VALUE]),
) {
if let Some(matching_parsed_link) = matching_link_directive {
let argument_name = argument.first_child()?.text().to_string();
let is_within_argument_value = argument
.last_child()?
.text_range()
.contains(offset.try_into().unwrap())
&& argument.children().count() > 1;
if argument_name == "import" && is_within_argument_value {
let items = matching_parsed_link
.unimported_directives()
.values()
.map(|d| {
get_link_import_directive_completion_item(
d,
lsp_range_from_cst_textrange(token.text_range(), source_text, None),
schema
.directive_definitions
.contains_key(d.node.name.as_str()),
!token.text().starts_with('"'),
!token.text().replace('"', "").starts_with('@'),
)
})
.collect::<Vec<_>>();
completion_items = items;
}
}
}
if parent.kind() == SyntaxKind::DOCUMENT {
completion_items.extend(get_root_completions(has_schema_definition))
}
}
if completion_items.is_empty() {
None
} else {
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: completion_items,
}))
}
}
#[memoize]
fn get_root_completions(has_schema_definition: bool) -> Vec<lsp::CompletionItem> {
GRAPHQL_KEYWORDS
.into_iter()
.chain(EXTEND_KEYWORDS)
.filter_map(|completion: &str| {
if completion != SCHEMA || !has_schema_definition {
Some(lsp::CompletionItem {
label: completion.to_string(),
kind: Some(lsp::CompletionItemKind::KEYWORD),
commit_characters: if completion.starts_with(EXTEND) {
None
} else {
Some(vec![" ".to_string()])
},
sort_text: Some(format!(
"{}{}",
if completion.starts_with(EXTEND) {
"1-"
} else {
"0-"
},
completion,
)),
..Default::default()
})
} else {
None
}
})
.collect()
}
fn get_completions_for_directive_location(
existing_locations: Vec<String>,
needs_leading_whitespace: bool,
) -> lsp::CompletionResponse {
lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: DIRECTIVE_LOCATIONS
.into_iter()
.filter_map(|location| {
if existing_locations.contains(&location.to_string()) {
None
} else {
Some(lsp::CompletionItem {
label: location.to_string(),
insert_text: needs_leading_whitespace.then(|| format!(" {}", location)),
kind: Some(lsp::CompletionItemKind::CONSTANT),
commit_characters: (!needs_leading_whitespace)
.then(|| vec![" ".to_string()]),
..Default::default()
})
}
})
.collect(),
})
}
fn get_completions_for_extension(
schema: &Schema,
builtins_to_ignore: Option<&Vec<String>>,
type_predicate: fn(&ExtendedType) -> bool,
) -> lsp::CompletionResponse {
let items = schema
.types
.iter()
.filter(|&(_, t)| {
type_predicate(t)
&& t.location().is_some()
&& !builtins_to_ignore.is_some_and(|b| b.contains(&t.name().to_string()))
})
.map(|(_, t)| get_type_completion_item(t, lsp::CompletionItemKind::FUNCTION, None))
.collect();
lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items,
})
}
#[allow(clippy::too_many_arguments)]
fn get_completions_for_directive(
schema: &Schema,
source_text: &Rope,
location: &DirectiveLocation,
should_include_at_prefix: bool,
should_suggest_snippet: bool,
alias_to_spec_map: HashMap<&String, &String>,
links_in_schema: Option<&HashMap<String, ParsedLink>>,
max_spec_versions: &MaxSpecVersions,
) -> Vec<lsp::CompletionItem> {
let input_object_types = schema
.types
.iter()
.filter(|(_, ty)| ty.is_input_type())
.collect::<HashMap<_, _>>();
let directive_completion_items = schema
.directive_definitions
.iter()
.filter_map(|(_, d)| {
d.locations
.contains(location)
.then_some(get_directive_completion_items(
d,
source_text,
should_include_at_prefix,
should_suggest_snippet,
&input_object_types,
&alias_to_spec_map,
None,
links_in_schema,
max_spec_versions,
))
})
.flatten();
let Some(links_in_schema) = links_in_schema else {
return directive_completion_items.collect();
};
let auto_import_spec_directive_completion_items = {
links_in_schema
.values()
.flat_map(|parsed_link| {
parsed_link
.unimported_directives()
.iter()
.filter_map(|(_, d)| {
(!schema
.directive_definitions
.contains_key(d.node.name.as_str()))
.then(|| {
d.node.locations.contains(location).then_some(
get_directive_completion_items(
d.node.as_ref(),
source_text,
should_include_at_prefix,
should_suggest_snippet,
&input_object_types,
&alias_to_spec_map,
Some(parsed_link),
Some(links_in_schema),
max_spec_versions,
),
)
})
.flatten()
})
.collect::<Vec<_>>()
})
.flatten()
};
directive_completion_items
.chain(auto_import_spec_directive_completion_items)
.collect()
}
fn has_deprecated_directive(t: &ExtendedType) -> bool {
t.directives()
.iter()
.any(|d| d.name == DEPRECATED_DIRECTIVE)
}
fn has_name_node_as_child(node: &SyntaxNode) -> bool {
node.children().any(|child| {
child.kind() == SyntaxKind::NAME
&& !GRAPHQL_KEYWORDS.contains(&child.text().to_string().as_str())
})
}
fn get_type_completion_item(
t: &ExtendedType,
kind: lsp::CompletionItemKind,
extra_completion_item_options: Option<lsp::CompletionItem>,
) -> lsp::CompletionItem {
lsp::CompletionItem {
label: t.name().to_string(),
kind: Some(kind),
documentation: t.description().map(|desc| {
lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: desc.to_string(),
})
}),
tags: if has_deprecated_directive(t) {
Some(vec![lsp::CompletionItemTag::DEPRECATED])
} else {
None
},
..extra_completion_item_options.unwrap_or_default()
}
}
fn get_link_import_directive_completion_item(
directive: &SpecDirective,
token_range: lsp::Range,
has_matching_directive_definition: bool,
should_include_quotes: bool,
should_include_at_prefix: bool,
) -> lsp::CompletionItem {
let maybe_text_edit = (has_matching_directive_definition
&& (!should_include_at_prefix || !should_include_quotes))
.then_some(vec![lsp::TextEdit {
range: token_range,
new_text: "".to_string(),
}]);
lsp::CompletionItem {
label: format!("\"@{}\"", directive.node.name),
filter_text: Some(directive.node.name.as_str().to_string()),
insert_text: Some(if has_matching_directive_definition {
format!("{{ name: \"@{}\", as: \"@$1\" }}", directive.node.name)
} else {
format!(
"{maybe_quote}{}{}{maybe_quote}",
should_include_at_prefix.then_some("@").unwrap_or_default(),
directive.node.name,
maybe_quote = should_include_quotes.then_some("\"").unwrap_or_default()
)
}),
label_details: has_matching_directive_definition.then_some(
lsp::CompletionItemLabelDetails {
detail: None,
description: Some("import with alias".to_string()),
},
),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
documentation: directive.node.description.as_ref().map(|desc| {
lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: desc.to_string(),
})
}),
kind: Some(lsp::CompletionItemKind::METHOD),
commit_characters: Some(vec![",".to_string()]),
additional_text_edits: maybe_text_edit,
..Default::default()
}
}
#[allow(clippy::too_many_arguments)]
fn get_directive_completion_items(
directive: &DirectiveDefinition,
source_text: &Rope,
should_include_at_prefix: bool,
should_suggest_snippet: bool,
input_object_types: &HashMap<&Name, &ExtendedType>,
alias_to_spec_map: &HashMap<&String, &String>,
parsed_link: Option<&ParsedLink>,
links_in_schema: Option<&HashMap<String, ParsedLink>>,
max_spec_versions: &MaxSpecVersions,
) -> Vec<lsp::CompletionItem> {
let mut completion_item = get_base_directive_completion_item(
directive,
should_include_at_prefix,
should_suggest_snippet,
input_object_types,
);
if let Some(parsed_link) = parsed_link {
completion_item.additional_text_edits = Some(vec![
parsed_link.text_edit_for_import(source_text, directive)
]);
completion_item.label_details = Some(lsp::CompletionItemLabelDetails {
detail: None,
description: Some(format!("import from {}", parsed_link.spec_name)),
});
}
apply_custom_completions_for_spec_directive(
completion_item,
directive,
alias_to_spec_map,
links_in_schema,
should_include_at_prefix,
max_spec_versions,
should_suggest_snippet,
)
}
fn snippet_with_default(name: &Name, default: &str, index: &usize) -> String {
format!("{}: ${{{}:{}}}", name, index, default)
}
fn get_snippet_for_arg(
arg_name: &apollo_compiler::Name,
arg_type: &apollo_compiler::ast::Type,
input_types: &HashMap<&Name, &ExtendedType>,
arg_index: &mut usize,
) -> String {
*arg_index += 1;
match arg_type {
Type::NonNullNamed(named) => match named.as_str() {
"String" | "ID" => format!("{}: \"${}\"", arg_name, arg_index),
"Int" => snippet_with_default(arg_name, "0", arg_index),
"Float" => snippet_with_default(arg_name, "0.0", arg_index),
"Boolean" => snippet_with_default(arg_name, "true", arg_index),
_ => {
let Some(input_type) = input_types.get(named) else {
return "".into();
};
match input_type {
ExtendedType::InputObject(input_object_type) => {
let fields = input_object_type
.fields
.values()
.filter(|field| field.is_required())
.map(|field| {
get_snippet_for_arg(
&field.name,
field.ty.as_ref(),
input_types,
arg_index,
)
})
.collect::<Vec<_>>()
.join(", ");
format!("{}: {{ {} }}", arg_name, fields)
}
ExtendedType::Scalar(_) => snippet_with_default(arg_name, "", arg_index),
_ => {
tracing::error!("Unexpected type: {}", named);
snippet_with_default(arg_name, named.as_str(), arg_index)
}
}
}
},
Type::NonNullList(_) => format!("{}: [${}]", arg_name, arg_index),
Type::List(_) | Type::Named(_) => {
let msg = format!(
"Unexpected nullable argument type: `{}`, argument should've been filtered",
arg_type
);
tracing::error!(msg);
panic!("{}", msg);
}
}
}
#[memoize]
fn get_snippet_for_directive_location(insert_text: String) -> String {
format!("{} ${{1|{}|}}", insert_text, DIRECTIVE_LOCATIONS.join(","))
}
fn unwrap_node(node: &SyntaxNode) -> SyntaxNode {
if let Some(parent) = node.parent() {
match node.kind() {
SyntaxKind::DIRECTIVE_DEFINITION
| SyntaxKind::OBJECT_TYPE_DEFINITION
| SyntaxKind::TYPE_DEFINITION
| SyntaxKind::FIELD_DEFINITION
| SyntaxKind::SCHEMA_DEFINITION
| SyntaxKind::ENUM_TYPE_DEFINITION
| SyntaxKind::UNION_TYPE_DEFINITION
| SyntaxKind::INPUT_VALUE_DEFINITION
| SyntaxKind::INTERFACE_TYPE_DEFINITION
| SyntaxKind::INPUT_OBJECT_TYPE_DEFINITION
| SyntaxKind::ENUM_VALUE_DEFINITION
| SyntaxKind::SCALAR_TYPE_DEFINITION
| SyntaxKind::OBJECT_TYPE_EXTENSION
| SyntaxKind::TYPE_EXTENSION
| SyntaxKind::SCHEMA_EXTENSION
| SyntaxKind::ENUM_TYPE_EXTENSION
| SyntaxKind::UNION_TYPE_EXTENSION
| SyntaxKind::INPUT_OBJECT_TYPE_EXTENSION
| SyntaxKind::SCALAR_TYPE_EXTENSION
| SyntaxKind::INTERFACE_TYPE_EXTENSION => return node.clone(),
_ => {
return unwrap_node(&parent);
}
}
}
node.clone()
}
fn get_directive_location(node: &SyntaxNode) -> Option<DirectiveLocation> {
match node.kind() {
SyntaxKind::SCHEMA_DEFINITION | SyntaxKind::SCHEMA_EXTENSION => {
Some(DirectiveLocation::Schema)
}
SyntaxKind::SCALAR_TYPE_DEFINITION | SyntaxKind::SCALAR_TYPE_EXTENSION => {
Some(DirectiveLocation::Scalar)
}
SyntaxKind::OBJECT_TYPE_DEFINITION | SyntaxKind::OBJECT_TYPE_EXTENSION => {
Some(DirectiveLocation::Object)
}
SyntaxKind::FIELD_DEFINITION => Some(DirectiveLocation::FieldDefinition),
SyntaxKind::INPUT_VALUE_DEFINITION => {
let parent = node
.parent()
.expect("Input Value Definition should have a parent");
if parent.kind() == SyntaxKind::ARGUMENTS_DEFINITION {
Some(DirectiveLocation::ArgumentDefinition)
} else {
Some(DirectiveLocation::InputFieldDefinition)
}
}
SyntaxKind::INTERFACE_TYPE_DEFINITION | SyntaxKind::INTERFACE_TYPE_EXTENSION => {
Some(DirectiveLocation::Interface)
}
SyntaxKind::UNION_TYPE_DEFINITION | SyntaxKind::UNION_TYPE_EXTENSION => {
Some(DirectiveLocation::Union)
}
SyntaxKind::ENUM_TYPE_DEFINITION | SyntaxKind::ENUM_TYPE_EXTENSION => {
Some(DirectiveLocation::Enum)
}
SyntaxKind::ENUM_VALUE_DEFINITION => Some(DirectiveLocation::EnumValue),
SyntaxKind::INPUT_OBJECT_TYPE_DEFINITION | SyntaxKind::INPUT_OBJECT_TYPE_EXTENSION => {
Some(DirectiveLocation::InputObject)
}
_ => None,
}
}
fn unwrap_type(node: &SyntaxNode) -> SyntaxNode {
if let Some(parent) = node.parent() {
match node.kind() {
SyntaxKind::NAME | SyntaxKind::NON_NULL_TYPE | SyntaxKind::LIST_TYPE => {
unwrap_type(&parent)
}
_ => node.clone(),
}
} else {
node.clone()
}
}
fn get_node_name(node: &SyntaxNode) -> Option<String> {
unwrap_node(node)
.children()
.find(|child| child.kind() == SyntaxKind::NAME)
.map(|n| n.text().to_string())
}
fn get_base_directive_completion_item(
directive: &DirectiveDefinition,
should_include_at_prefix: bool,
should_suggest_snippet: bool,
input_object_types: &HashMap<&Name, &ExtendedType>,
) -> lsp::CompletionItem {
let required_args = directive.arguments.iter().filter(|arg| arg.is_required());
let has_required_args = required_args.clone().count() > 0;
let is_default_spec_import = KNOWN_SPECS
.keys()
.any(|spec_name| directive.name.starts_with(&format!("{}__", spec_name)));
lsp::CompletionItem {
label: format!(
"@{}{}",
directive.name,
has_required_args.then_some("(…)").unwrap_or_default()
),
label_details: directive
.repeatable
.then_some(lsp::CompletionItemLabelDetails {
detail: None,
description: Some(REPEATABLE.to_string()),
}),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}{}{}",
should_include_at_prefix.then_some("@").unwrap_or_default(),
directive.name,
(should_suggest_snippet && has_required_args)
.then(|| {
let mut arg_index = 0;
format!(
"({})",
required_args
.map(|arg| get_snippet_for_arg(
&arg.name,
&arg.ty,
input_object_types,
&mut arg_index
))
.collect::<Vec<_>>()
.join(", ")
)
})
.unwrap_or_default()
)),
sort_text: Some(if is_default_spec_import {
format!("zz-@{}", directive.name)
} else {
format!("@{}", directive.name)
}),
filter_text: Some(directive.name.to_string()),
documentation: directive.description.as_ref().map(|desc| {
lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: desc.to_string(),
})
}),
commit_characters: (!has_required_args).then(|| vec![" ".to_string()]),
kind: Some(lsp::CompletionItemKind::METHOD),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{supergraph::KnownSubgraphs, GraphConfig};
use insta::assert_snapshot;
use itertools::Itertools;
use std::fmt::Write;
use tower_lsp::lsp_types::{self as lsp};
macro_rules! position {
($line:expr, $character:expr) => {
lsp::Position::new($line, $character)
};
}
fn get_document(extra_text: &str) -> String {
let base_text = r#"type Query {
hello: String
}
directive @typeDirective(arg: String) on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
directive @fieldDirective on FIELD_DEFINITION
directive @argDirective on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
directive @enumValueDirective on ENUM_VALUE
type A {
b: String
int: Int
float: Float
}
scalar Date
input User {
name: String
}
interface Node {
id: ID!
}
enum SearchResultBody {
A
}
union SearchResult = A | Query
"#;
format!("{}\n{}", base_text, extra_text)
}
const CURSOR: &str = "|cursor|";
fn assert_completions_at_position_given_string(
extra_text: &str,
expected: Option<Vec<&str>>,
custom_document: Option<&str>,
) {
let document_with_cursor = match custom_document {
Some(doc) => format!("{}\n{}", doc, extra_text),
None => get_document(extra_text),
};
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document_with_cursor.replace(CURSOR, "").clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let document_position = if let Some((cursor_line_number, cursor_line_content)) =
document_with_cursor
.split('\n')
.find_position(|line| line.contains(CURSOR))
{
let cursor_character_number = cursor_line_content.split(CURSOR).next().unwrap().len();
position!(cursor_line_number as u32, cursor_character_number as u32)
} else {
position!(
(graph.source_text_for_uri(&uri).len_lines() - 1) as u32,
extra_text.split('\n').next_back().unwrap().len() as u32
)
};
let completions = get_completions_at_position(
&graph,
&uri,
document_position,
&MaxSpecVersions::default(),
);
if completions.is_none()
|| completions
== Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: vec![],
}))
{
assert!(expected.is_none());
return;
}
let completions = completions.expect("Expected completions to not be None");
let lsp::CompletionResponse::List(list) = completions else {
panic!("Expected a list of completions");
};
let expected_items = expected
.unwrap_or_else(|| {
panic!(
"Completion expected to be None but got: {:?}",
list.items
.iter()
.map(|i| i.label.clone())
.collect::<Vec<_>>()
)
})
.into_iter()
.sorted()
.collect::<Vec<_>>();
assert_eq!(
expected_items,
list.items
.iter()
.map(|i| i.label.clone())
.sorted()
.collect::<Vec<_>>()
);
}
fn suggestions_with_root_keywords(suggestions: Vec<&str>) -> Vec<&str> {
vec![
suggestions,
GRAPHQL_KEYWORDS
.into_iter()
.chain(EXTEND_KEYWORDS)
.collect::<Vec<_>>(),
]
.into_iter()
.flatten()
.collect()
}
fn suggestions_with_builtin_scalars(suggestions: Vec<&str>) -> Vec<&str> {
vec![vec!["Int", "Float", "String", "Boolean", "ID"], suggestions]
.into_iter()
.flatten()
.collect()
}
fn suggestions_with_type_directives(suggestions: Vec<&str>) -> Vec<&str> {
vec![vec!["@typeDirective"], suggestions]
.into_iter()
.flatten()
.collect()
}
#[test]
fn get_completions_at_position_edge_cases() {
assert_completions_at_position_given_string(
"",
Some(suggestions_with_root_keywords(vec![])),
Some(&format!("{}\n", CURSOR)),
);
assert_completions_at_position_given_string("@", None, Some(""));
assert_completions_at_position_given_string("|", None, Some(""));
assert_completions_at_position_given_string("type Test |", None, Some(""));
assert_completions_at_position_given_string("&", None, Some(""));
assert_completions_at_position_given_string("extend interface", None, None);
assert_completions_at_position_given_string(
"extend interface ",
Some(vec!["Node"]),
None,
);
assert_completions_at_position_given_string(
"extend interface \n",
Some(vec!["Node"]),
None,
);
assert_completions_at_position_given_string(
"extend type aaaaa",
Some(vec!["Query", "A"]),
None,
);
assert_completions_at_position_given_string(
"type A @",
Some(suggestions_with_type_directives(vec![])),
None,
);
assert_completions_at_position_given_string(
"type {
hello: @",
None,
None,
)
}
#[test]
fn get_completions_at_position_type_extension() {
assert_completions_at_position_given_string("extend type ", Some(vec!["Query", "A"]), None);
assert_completions_at_position_given_string(
"extend type Q",
Some(vec!["Query", "A"]), None,
);
}
#[test]
fn get_completions_at_position_scalar_extension() {
assert_completions_at_position_given_string(
"extend scalar ",
Some(suggestions_with_builtin_scalars(vec!["Date"])),
None,
);
assert_completions_at_position_given_string(
"extend scalar D",
Some(suggestions_with_builtin_scalars(vec!["Date"])), None,
);
}
#[test]
fn get_completions_at_position_interface_extension() {
assert_completions_at_position_given_string("extend interface ", Some(vec!["Node"]), None);
assert_completions_at_position_given_string("extend interface N", Some(vec!["Node"]), None)
}
#[test]
fn get_completions_at_position_union_extension() {
assert_completions_at_position_given_string(
"extend union ",
Some(vec!["SearchResult"]),
None,
);
assert_completions_at_position_given_string(
"extend union S",
Some(vec!["SearchResult"]),
None,
);
}
#[test]
fn get_completions_at_position_input_extension() {
assert_completions_at_position_given_string("extend input ", Some(vec!["User"]), None);
assert_completions_at_position_given_string("extend input U", Some(vec!["User"]), None);
}
#[test]
fn get_completions_at_position_enum_extension() {
assert_completions_at_position_given_string(
"extend enum ",
Some(vec!["SearchResultBody"]),
None,
);
assert_completions_at_position_given_string(
"extend enum S",
Some(vec!["SearchResultBody"]),
None,
);
}
#[test]
fn get_completions_at_position_root() {
assert_completions_at_position_given_string(
"",
Some(suggestions_with_root_keywords(vec![])),
None,
);
assert_completions_at_position_given_string(
"",
Some(suggestions_with_root_keywords(vec![])),
None,
);
assert_completions_at_position_given_string(
"extend ",
Some(suggestions_with_root_keywords(vec![])),
None,
);
assert_completions_at_position_given_string(
"extend t",
Some(suggestions_with_root_keywords(vec![])), None,
);
assert_completions_at_position_given_string(
"d",
Some(suggestions_with_root_keywords(vec![])), None,
);
assert_completions_at_position_given_string(
"schema {
query: Query
}
",
Some(
suggestions_with_root_keywords(vec![])
.iter()
.filter(|k| **k != "schema")
.copied()
.collect(),
),
None,
);
}
#[test]
fn get_completions_at_position_directive_definition() {
assert_completions_at_position_given_string("directive @", None, None);
assert_completions_at_position_given_string("directive @deprecated", None, None);
assert_completions_at_position_given_string(
"directive @deprecated ",
Some(vec!["repeatable on", "on"]),
None,
);
assert_completions_at_position_given_string(
"directive @deprecated on ",
Some(DIRECTIVE_LOCATIONS.into()),
None,
);
assert_completions_at_position_given_string(
"directive @deprecated on QUERY ",
Some(suggestions_with_root_keywords(vec![])),
None,
);
assert_completions_at_position_given_string(
"directive @deprecated on QUERY |",
Some(
DIRECTIVE_LOCATIONS
.into_iter()
.filter(|s| *s != "QUERY")
.collect(),
),
None,
);
assert_completions_at_position_given_string(
"directive @deprecated on QUERY | ",
Some(
DIRECTIVE_LOCATIONS
.into_iter()
.filter(|s| *s != "QUERY")
.collect(),
),
None,
);
}
#[test]
fn get_completions_at_position_implements() {
assert_completions_at_position_given_string("interface Node2", None, None);
assert_completions_at_position_given_string(
"interface Node2 ",
Some(suggestions_with_type_directives(
suggestions_with_root_keywords(vec!["implements"]),
)),
None,
);
assert_completions_at_position_given_string("type NodeResult", None, None);
assert_completions_at_position_given_string(
"type NodeResult ",
Some(suggestions_with_type_directives(
suggestions_with_root_keywords(vec!["implements"]),
)),
None,
);
assert_completions_at_position_given_string(
"interface Node2 implements ",
Some(suggestions_with_root_keywords(vec!["Node"])),
None,
);
assert_completions_at_position_given_string(
"interface Node2 implements Node & ",
Some(suggestions_with_root_keywords(vec![])),
None,
);
assert_completions_at_position_given_string(
"interface Node2 {
a: String!
}
type NodeResult implements ",
Some(suggestions_with_root_keywords(vec!["Node", "Node2"])),
None,
);
assert_completions_at_position_given_string(
"interface Node2 {
a: String!
}
type NodeResult implements Node & ",
Some(suggestions_with_root_keywords(vec!["Node2"])),
None,
);
assert_completions_at_position_given_string(
"interface Node2 {
a: String!
}
type NodeResult implements Node &",
Some(vec!["Node2"]),
None,
);
}
#[test]
fn get_completions_at_position_union_members() {
assert_completions_at_position_given_string("union SearchResult2", None, None);
assert_completions_at_position_given_string(
"
type B {
c: String
}
extend union SearchResult = ",
Some(suggestions_with_root_keywords(vec!["B"])),
None,
);
assert_completions_at_position_given_string(
"union SearchResult2 = ",
Some(suggestions_with_root_keywords(vec!["Query", "A"])),
None,
);
assert_completions_at_position_given_string(
"union SearchResult2 = A | ",
Some(suggestions_with_root_keywords(vec!["Query"])),
None,
);
assert_completions_at_position_given_string(
"union SearchResult2 = A | Query | ",
Some(suggestions_with_root_keywords(vec![])),
None,
);
}
#[test]
fn get_completions_at_position_field_definition() {
let suggestion_types = [
"Query",
"A",
"Date",
"Node",
"SearchResultBody",
"SearchResult",
"Person",
];
assert_completions_at_position_given_string(
"type Person {
name: ",
Some(suggestions_with_root_keywords(
suggestions_with_builtin_scalars(suggestion_types.to_vec()),
)),
None,
);
assert_completions_at_position_given_string(
"type Person {
name: [",
Some(suggestions_with_builtin_scalars(suggestion_types.to_vec())),
None,
);
assert_completions_at_position_given_string(
"type Person {
name: S",
Some(suggestions_with_builtin_scalars(suggestion_types.to_vec())), None,
);
assert_completions_at_position_given_string(
"type Person {
name: [S",
Some(suggestions_with_builtin_scalars(suggestion_types.to_vec())), None,
);
assert_completions_at_position_given_string(
"type Person {
name: String!",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
}
#[test]
fn get_completions_at_position_input_value_definition() {
assert_completions_at_position_given_string(
"type Person {
name(in: ",
Some(suggestions_with_root_keywords(
suggestions_with_builtin_scalars(vec!["Date", "User", "SearchResultBody"]),
)),
None,
);
assert_completions_at_position_given_string(
"type Person {
name(in: [",
Some(suggestions_with_builtin_scalars(vec![
"Date",
"User",
"SearchResultBody",
])),
None,
);
assert_completions_at_position_given_string(
"type Person {
name(in: S",
Some(suggestions_with_builtin_scalars(vec![
"Date",
"User",
"SearchResultBody",
])), None,
);
assert_completions_at_position_given_string(
"input UserInput {
name: ",
Some(suggestions_with_root_keywords(
suggestions_with_builtin_scalars(vec![
"Date",
"User",
"SearchResultBody",
]),
)),
None,
);
assert_completions_at_position_given_string(
"input UserInput {
name: S",
Some(suggestions_with_builtin_scalars(vec![
"Date",
"User",
"SearchResultBody",
])), None,
);
assert_completions_at_position_given_string(
"input UserInput {
name: String!",
Some(vec!["@deprecated", "@argDirective"]),
None,
);
}
#[test]
fn get_completions_at_position_directive_use() {
let type_prefixes = [
("type QueryResult", "object"),
("scalar Date2", "scalar"),
("interface Node3", "object"),
("union SearchResult2", "other"),
("input UserInput2", "other"),
("enum SearchResultBody2", "other"),
];
let type_suffixes = [
("", true),
("@", false),
("@ty", false),
("@typeDirective", false),
("@typeDirective ", true),
];
type_prefixes.iter().for_each(|(p, group)| {
type_suffixes.iter().for_each(|(s, include_implements)| {
let directives = if *group == "scalar" {
vec!["@specifiedBy(…)", "@typeDirective"]
} else {
suggestions_with_type_directives(vec![])
};
assert_completions_at_position_given_string(
&format!("{} {}", p, s),
if *include_implements {
if *group == "object" {
Some(suggestions_with_root_keywords(
suggestions_with_type_directives(vec!["implements"]),
))
} else {
Some(suggestions_with_root_keywords(directives))
}
} else {
Some(directives)
},
None,
);
});
});
assert_completions_at_position_given_string(
"type QueryResult {
hello: String @",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
assert_completions_at_position_given_string(
"type QueryResult {
hello: [String] @",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
assert_completions_at_position_given_string(
"type QueryResult {
hello: String! @",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
assert_completions_at_position_given_string(
"interface SearchResult2 {
hello: String @",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
assert_completions_at_position_given_string(
"interface SearchResult2 {
hello: [String] @",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
assert_completions_at_position_given_string(
"interface SearchResult2 {
hello: String! @",
Some(vec!["@deprecated", "@fieldDirective"]),
None,
);
assert_completions_at_position_given_string(
"input UserInput2 {
hello: String @",
Some(vec!["@deprecated", "@argDirective"]),
None,
);
assert_completions_at_position_given_string(
"input UserInput2 {
hello: [String] @",
Some(vec!["@deprecated", "@argDirective"]),
None,
);
assert_completions_at_position_given_string(
"input UserInput2 {
hello: String! @",
Some(vec!["@deprecated", "@argDirective"]),
None,
);
assert_completions_at_position_given_string(
"type QueryResult {
hello(input: String @",
Some(vec!["@deprecated", "@argDirective"]),
None,
);
assert_completions_at_position_given_string(
"directive @hello(input: String @",
Some(vec!["@deprecated", "@argDirective"]),
None,
);
assert_completions_at_position_given_string(
"enum SearchResultBody2 {
YES @",
Some(vec!["@deprecated", "@enumValueDirective"]),
None,
);
}
#[test]
fn get_completions_with_no_builtins() {
assert_completions_at_position_given_string(
"type QueryResult {
myField: ",
Some(suggestions_with_root_keywords(
suggestions_with_builtin_scalars(vec![
"Query",
"A",
"Date",
"Node",
"SearchResultBody",
"SearchResult",
"QueryResult",
]),
)),
None,
);
assert_completions_at_position_given_string(
"extend scalar ",
Some(suggestions_with_builtin_scalars(vec!["Date"])),
None,
);
}
fn snapshot_for_completion_list(completion_response: lsp::CompletionResponse) -> String {
let lsp::CompletionResponse::List(list) = completion_response else {
panic!("Expected a list of completions");
};
list.items
.iter()
.filter(|completion_item| completion_item.label.starts_with('@') || completion_item.label.starts_with('"'))
.sorted_by_key(|completion_item| completion_item.sort_text.as_ref().unwrap_or(&completion_item.label))
.fold(String::new(), |mut snapshot, completion_item| {
write!(snapshot,
"label: {}\ndetails: {:?}\ndocumentation: {:?}\ncommit characters: {:?}\ninsert text: {:?}\nadditional edits: {:#?}\n---\n",
completion_item.label, completion_item.label_details.as_ref().map(|label_details| &label_details.description),
completion_item.documentation, completion_item.commit_characters, completion_item.insert_text, completion_item.additional_text_edits
).unwrap();
snapshot
})
}
#[test]
fn get_completions_at_directives_with_snippets() {
let document = r#"
type Query {
field: String
}
scalar CustomScalar
directive @string(str: String!) on FIELD_DEFINITION
directive @int(int: Int!) on FIELD_DEFINITION
directive @float(float: Float!) on FIELD_DEFINITION
directive @boolean(bool: Boolean!) on FIELD_DEFINITION
directive @id(id: ID!) on FIELD_DEFINITION
directive @list(list: [String!]!) on FIELD_DEFINITION
directive @customScalar(scalar: CustomScalar!) on FIELD_DEFINITION
input Input {
nullableString: String
nonNullableString: String!
nonNullableList: [String!]!
}
directive @input(input: Input!) on FIELD_DEFINITION
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let document_position = position!(2, 18);
let completions = get_completions_at_position(
&graph,
&uri,
document_position,
&MaxSpecVersions::default(),
)
.unwrap();
assert_snapshot!(snapshot_for_completion_list(completions));
}
#[test]
fn no_snippets_when_args_list_is_present() {
let document = r#"
type Query {
field: String @s(str: "hello")
}
directive @string(str: String!) on FIELD_DEFINITION
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let document_position = position!(2, 20);
let lsp::CompletionResponse::List(completions) = get_completions_at_position(
&graph,
&uri,
document_position,
&MaxSpecVersions::default(),
)
.unwrap() else {
panic!("Expected a list of completions");
};
assert_eq!(completions.items[1].insert_text, Some("string".to_string()));
}
#[test]
fn get_completions_at_directives_with_custom_snippets() {
let document = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", { name: "@provides", as: "@fed_provides"}])
@link(url: "https://specs.apollo.dev/connect/v0.1", import: ["@connect", "@source"])
type Entity {
id: ID!
}
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let document_positions = [
position!(4, 1),
position!(5, 13),
position!(6, 12),
];
let mut snapshot = String::new();
for document_position in document_positions {
let completions = get_completions_at_position(
&graph,
&uri,
document_position,
&MaxSpecVersions::default(),
)
.unwrap();
let snapshot_for_position = snapshot_for_completion_list(completions);
snapshot.push_str(&snapshot_for_position);
}
assert_snapshot!(snapshot);
}
#[test]
fn get_completions_at_directives_with_auto_imports() {
let document = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.12", import: ["@key"])
@link(url: "https://specs.apollo.dev/connect/v0.1")
type Entity {
id: ID!
}
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let document_positions = [
position!(4, 1),
position!(6, 12),
];
let mut snapshot = String::new();
for document_position in document_positions {
let completions = get_completions_at_position(
&graph,
&uri,
document_position,
&MaxSpecVersions::default(),
)
.unwrap();
let snapshot_for_position = snapshot_for_completion_list(completions);
snapshot.push_str(&snapshot_for_position);
}
assert_snapshot!(snapshot);
}
#[test]
fn get_completions_on_link_directive_import_list() {
let document = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.12", import: ["@key", "@"])
@link(url: "https://specs.apollo.dev/connect/v0.1", import: ["@connect", ])
directive @external on FIELD_DEFINITION
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let document_positions = vec![
position!(2, 77),
position!(3, 74),
];
let mut snapshot = String::new();
for document_position in document_positions {
let completions = get_completions_at_position(
&graph,
&uri,
document_position,
&MaxSpecVersions::default(),
)
.unwrap();
let snapshot_for_position = snapshot_for_completion_list(completions);
snapshot.push_str(&snapshot_for_position);
}
assert_snapshot!(snapshot);
}
#[test]
fn get_completions_at_directives_with_auto_imports_omitting_user_defined_directives() {
let document = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key"])
@link(url: "https://specs.apollo.dev/connect/v0.1", import: ["@connect", "@source"])
type Entity {
id: ID!
}
directive @external on FIELD_DEFINITION
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let lsp::CompletionResponse::List(completions) = get_completions_at_position(
&graph,
&uri,
position!(6, 12),
&MaxSpecVersions::default(),
)
.unwrap() else {
panic!("Expected a list of completions");
};
let external_completion = completions
.items
.iter()
.find(|item| item.label.contains("@external"))
.unwrap();
assert!(external_completion.additional_text_edits.is_none());
}
#[test]
fn get_link_completions() {
let document = r#"
extend schema @
type Query {
hello: String
}
type Entity @key(fields: "id") {
id: ID!
}
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let completions = get_completions_at_position(
&graph,
&uri,
position!(1, 15),
&MaxSpecVersions::default(),
)
.unwrap();
assert_snapshot!(snapshot_for_completion_list(completions));
}
#[test]
fn get_link_completions_with_max_spec_version_set() {
let document = r#"
extend schema @
type Query {
hello: String
}
type Entity @key(fields: "id") {
id: ID!
}
"#
.to_string();
let uri = lsp::Url::parse("file:///test.graphql").unwrap();
let graph = Graph::new(
uri.clone(),
document.clone(),
0,
KnownSubgraphs::default(),
GraphConfig::default(),
);
let completions = get_completions_at_position(
&graph,
&uri,
position!(1, 15),
&MaxSpecVersions {
federation: Some(semver::Version::new(2, 2, 0)),
connect: Some(semver::Version::new(0, 1, 0)),
},
)
.unwrap();
assert_snapshot!(snapshot_for_completion_list(completions));
}
#[test]
fn no_suggestions_before_implements_keyword() {
let type_prefixes = [
"extend schema @link(url: \"https://specs.apollo.dev/federation/v2.8\") interface InterfaceC {a: String} type QueryResult ",
"type QueryResult ",
];
let implements_suffixes = [
"implements",
"implements InterfaceA",
"implements InterfaceA & InterfaceB",
];
type_prefixes.iter().for_each(|t| {
implements_suffixes.iter().for_each(|s| {
assert_completions_at_position_given_string(
&format!("{}{} {}", t, CURSOR, s),
None,
None,
)
})
});
}
#[test]
fn no_suggestions_in_string_literal() {
assert_completions_at_position_given_string(
&format!("type Test @typeDirective(arg: \"{}\")", CURSOR),
None,
None,
);
assert_completions_at_position_given_string(
&format!("type Test @typeDirective(arg: \"\"\"{}\"\"\")", CURSOR),
None,
None,
);
assert_completions_at_position_given_string(
&format!(
"
\"{}\"
type Test @typeDirective",
CURSOR
),
None,
None,
);
assert_completions_at_position_given_string(
&format!(
"
\"\"\"{}\"\"\"
type Test @typeDirective",
CURSOR
),
None,
None,
);
}
}