Skip to content

Commit

Permalink
Feature: LSP Fragment completions (#4134)
Browse files Browse the repository at this point in the history
Summary:
# LSP Fragment Completions

Adds support for `__typename`, fragment, and inline fragment completions for `interface`/`union`/`type`.

Completions are ordered using [`sortText`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion), with the following `precedence`:

1. Fields on the type, e.g., fields from `type`/`interface`
2. `__typename`
3. `...on T` inline fragments
4. `...F` fragments

The modified tests show the behaviour, but a summary here is:

## `interface`

- `__typename: String!` completion
- `...on T` completions for the `interface` type itself and all types which implement it
- `...F` completions for all fragments `F` on the `interface` or any `implements`' of it

### Example

<img width="468" alt="Screenshot 2022-11-21 at 13 59 39" src="https://user-images.githubusercontent.com/17055343/203078448-465eb2ab-6165-49ee-9f9d-403bcbe6f739.png">

## `union`

- `__typename: String!` completion
- `...on T` completions for the `union` type itself and for each variant
- `...F` completions for all fragments `F` on the `union` or any variants of it

### Example

<img width="471" alt="Screenshot 2022-11-21 at 13 59 56" src="https://user-images.githubusercontent.com/17055343/203078444-9f98c439-facb-4c5d-ab0b-ccf552fca0e2.png">

## `type`

- `__typename: String!` completion
- `...F` completions for all fragments `F` on the `type` or on any `interface`s or `union`s it implements

### Example

<img width="469" alt="Screenshot 2022-11-21 at 14 00 10" src="https://user-images.githubusercontent.com/17055343/203078437-10b43e4d-152c-4592-a7e9-2bfad020cde4.png">

## Questions:

- [x] Does Relay have a preferred style for spacing of fragments? Which should be used on these completions? Should this be a user configurable option?
  - `...on T` vs `... on T`
  - `...F` vs `... F`
- [x] Should inline fragment completions exist for the type itself? E.g., when fragmenting on a `type User`, should `...on User` be suggested?
  - At the moment this MR adds this for `interface` & `union` but not `type` - should this be consistent?
- [x] Will this feature be annoying for users of the LSP in orgs with a large codebase & lots of fragments? Should either of these be user configurable?
- [x] Is the `sortText` robust enough? This has been implemented by prefixing a number to the start of the `label`; it seems to work for me locally ([code](https://github.com/facebook/relay/pull/4134/files#diff-04b7155ac238238b6434c2f32c30439229c0127075572f2d2f7519b574ff90c6R1018))

Pull Request resolved: #4134

Reviewed By: tyao1

Differential Revision: D41470372

Pulled By: alunyov

fbshipit-source-id: ef04a34fb7e145cb9884c8b79345fd8e0a26cf83
  • Loading branch information
beaumontjonathan authored and facebook-github-bot committed Nov 29, 2022
1 parent 8eea4aa commit 96c7193
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 77 deletions.
192 changes: 125 additions & 67 deletions compiler/crates/relay-lsp/src/completion/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ impl CompletionRequestBuilder {
}
};
if is_cursor_in_next_white_space {
// Handles the following speicial case
// Handles the following special case
// (args1: | args2:$var)
// ^ cursor here
// The cursor is on the white space between args1 and args2.
Expand Down Expand Up @@ -549,35 +549,62 @@ fn completion_items_for_request(
match kind {
CompletionKind::FragmentSpread => {
let leaf_type = request.type_path.resolve_leaf_type(schema)?;

debug!("has source program");
let items = resolve_completion_items_for_fragment_spread(leaf_type, program, schema);
Some(items)
Some(resolve_completion_items_for_fragment_spread(
leaf_type, program, schema, true,
))
}
CompletionKind::FieldName {
existing_linked_field,
} => match request.type_path.resolve_leaf_type(schema)? {
Type::Interface(interface_id) => {
let interface = schema.interface(interface_id);
let items = resolve_completion_items_from_fields(
interface,
schema,
schema_documentation,
existing_linked_field,
);
Some(items)
Some(merge_completion_items_ordered([
resolve_completion_items_for_fields(
interface,
schema,
schema_documentation,
existing_linked_field,
),
resolve_completion_items_typename(Type::Interface(interface_id), schema),
resolve_completion_items_for_inline_fragment(
Type::Interface(interface_id),
schema,
false,
),
resolve_completion_items_for_fragment_spread(
Type::Interface(interface_id),
program,
schema,
false,
),
]))
}
Type::Object(object_id) => {
let object = schema.object(object_id);
let items = resolve_completion_items_from_fields(
object,
Type::Object(object_id) => Some(merge_completion_items_ordered([
resolve_completion_items_for_fields(
schema.object(object_id),
schema,
schema_documentation,
existing_linked_field,
);
Some(items)
}
Type::Enum(_) | Type::InputObject(_) | Type::Scalar(_) | Type::Union(_) => None,
),
resolve_completion_items_typename(Type::Object(object_id), schema),
resolve_completion_items_for_fragment_spread(
Type::Object(object_id),
program,
schema,
false,
),
])),
Type::Union(union_id) => Some(merge_completion_items_ordered([
resolve_completion_items_typename(Type::Union(union_id), schema),
resolve_completion_items_for_inline_fragment(Type::Union(union_id), schema, false),
resolve_completion_items_for_fragment_spread(
Type::Union(union_id),
program,
schema,
false,
),
])),
Type::Enum(_) | Type::InputObject(_) | Type::Scalar(_) => None,
},
CompletionKind::DirectiveName { location } => {
let directives = schema.directives_for_location(location);
Expand Down Expand Up @@ -656,7 +683,7 @@ fn completion_items_for_request(
existing_inline_fragment,
} => {
let type_ = request.type_path.resolve_leaf_type(schema)?;
Some(resolve_completion_items_for_inline_fragment_type(
Some(resolve_completion_items_for_inline_fragment(
type_,
schema,
existing_inline_fragment,
Expand All @@ -665,6 +692,16 @@ fn completion_items_for_request(
}
}

fn resolve_completion_items_typename(type_: Type, schema: &SDLSchema) -> Vec<CompletionItem> {
if type_.is_root_type(schema) {
vec![]
} else {
let mut item = CompletionItem::new_simple("__typename".to_owned(), "String!".to_owned());
item.kind = Some(CompletionItemKind::FIELD);
vec![item]
}
}

fn resolve_completion_items_for_argument_name<T: ArgumentLike>(
arguments: impl Iterator<Item = T>,
schema: &SDLSchema,
Expand Down Expand Up @@ -706,7 +743,7 @@ fn resolve_completion_items_for_argument_name<T: ArgumentLike>(
.collect()
}

fn resolve_completion_items_for_inline_fragment_type(
fn resolve_completion_items_for_inline_fragment(
type_: Type,
schema: &SDLSchema,
existing_inline_fragment: bool,
Expand Down Expand Up @@ -734,24 +771,24 @@ fn resolve_completion_items_for_inline_fragment_type(
)
.collect()
}
Type::Enum(_) | Type::Object(_) | Type::InputObject(_) | Type::Scalar(_) => vec![type_],
Type::Enum(_) | Type::Object(_) | Type::InputObject(_) | Type::Scalar(_) => vec![],
}
.into_iter()
.map(|type_| {
let label = schema.get_type_name(type_).lookup().into();
let type_name = schema.get_type_name(type_).lookup();
if existing_inline_fragment {
CompletionItem::new_simple(label, "".into())
CompletionItem::new_simple(type_name.to_owned(), "".into())
} else {
CompletionItem {
label: label.clone(),
label: format!("... on {type_name}"),
kind: None,
detail: None,
documentation: None,
deprecated: None,
preselect: None,
sort_text: None,
filter_text: None,
insert_text: Some(format!("{} {{\n\t$1\n}}", label)),
insert_text: Some(format!("... on {type_name} {{\n\t$1\n}}")),
insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
text_edit: None,
additional_text_edits: None,
Expand Down Expand Up @@ -822,7 +859,7 @@ fn resolve_completion_items_for_argument_value(
completion_items
}

fn resolve_completion_items_from_fields<T: TypeWithFields + Named>(
fn resolve_completion_items_for_fields<T: TypeWithFields + Named>(
type_: &T,
schema: &SDLSchema,
schema_documentation: impl SchemaDocumentation,
Expand Down Expand Up @@ -916,52 +953,73 @@ fn resolve_completion_items_for_fragment_spread(
type_: Type,
source_program: &Program,
schema: &SDLSchema,
existing_fragment_spread: bool,
) -> Vec<CompletionItem> {
let mut valid_fragments = vec![];
for fragment in source_program.fragments() {
if schema.are_overlapping_types(fragment.type_condition, type_) {
let label = fragment.name.item.to_string();
source_program
.fragments()
.filter(|fragment| schema.are_overlapping_types(fragment.type_condition, type_))
.map(|fragment| {
let label = if existing_fragment_spread {
fragment.name.item.to_string()
} else {
format!("...{}", fragment.name.item)
};
let detail = schema
.get_type_name(fragment.type_condition)
.lookup()
.to_string();
if fragment.variable_definitions.is_empty() {
valid_fragments.push(CompletionItem::new_simple(label, detail))
} else {
// Create a snippet if the fragment has required argumentDefinition with no default values
let args = create_arguments_snippets(fragment.variable_definitions.iter(), schema);
valid_fragments.push(if args.is_empty() {
CompletionItem::new_simple(label, detail)
} else {
let insert_text = format!("{} @arguments({})", label, args.join(", "));
CompletionItem {
label,
kind: None,
detail: Some(detail),
documentation: None,
deprecated: None,
preselect: None,
sort_text: None,
filter_text: None,
insert_text: Some(insert_text),
insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
text_edit: None,
additional_text_edits: None,
command: Some(lsp_types::Command::new(
"Suggest".into(),
"editor.action.triggerSuggest".into(),
None,
)),
data: None,
tags: None,
..Default::default()
}
});
return CompletionItem::new_simple(label, detail);
}
}
}
debug!("get_valid_fragments_for_type {:#?}", valid_fragments);
valid_fragments
// Create a snippet if the fragment has required argumentDefinition with no default values
let args = create_arguments_snippets(fragment.variable_definitions.iter(), schema);
if args.is_empty() {
return CompletionItem::new_simple(label, detail);
}
let insert_text = format!("{} @arguments({})", label, args.join(", "));
CompletionItem {
label,
kind: None,
detail: Some(detail),
documentation: None,
deprecated: None,
preselect: None,
sort_text: None,
filter_text: None,
insert_text: Some(insert_text),
insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
text_edit: None,
additional_text_edits: None,
command: Some(lsp_types::Command::new(
"Suggest".into(),
"editor.action.triggerSuggest".into(),
None,
)),
data: None,
tags: None,
..Default::default()
}
})
.collect()
}

fn merge_completion_items_ordered<I: IntoIterator<Item = Vec<CompletionItem>>>(
completion_item_groups: I,
) -> Vec<CompletionItem> {
completion_item_groups
.into_iter()
.enumerate()
.flat_map(|(index, mut items)| {
items.iter_mut().for_each(|item| {
item.sort_text = Some(format!(
"{}{}",
index,
item.sort_text.clone().unwrap_or_else(|| item.label.clone())
));
});
items
})
.collect()
}

fn completion_item_from_directive(
Expand Down
Loading

0 comments on commit 96c7193

Please sign in to comment.