-
Notifications
You must be signed in to change notification settings - Fork 219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(fmt): lsp hover #4923
Merged
Merged
feat(fmt): lsp hover #4923
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
72b2f2a
clean up lsp contexts
Druue fa820c6
Added new wasm endpoint: hover
Druue da42432
Added hover test setup with test for what is currently implemented
Druue cbadb98
clippy
Druue 7bcb9e8
Apparently we had Type as a variant of FieldPosition, but it was neve…
Druue 39b21c3
clean up offsets import
Druue 82909c4
add docs for hover api
Druue f8b92e1
make hover optional
Druue 8fca5fd
relation info
Druue 77e9821
find_top + clippy
Druue e3fd278
return the correct field one self-relations
Druue 88a5fd2
Add expect for model from view
Druue c6d65df
relation_info cleanup
Druue a5c32a9
hover scalar reference types in models
Druue f2cc369
Show doc on hover of composite and enum block names
Druue d09d7e0
actually use EnumValueId instead of bare u32s / usizes
Druue 3853c95
add positioning for enum value names
Druue 512d91c
spruce up relation field doc with `@relation(...)` info
Druue 17845a3
fix import
Druue 0ba6317
Test for hover on relation field when said relation model has another…
Druue 7f18ff4
remove postgresql test
Druue 3b8f52a
clarify broken relations test
Druue e06d9f2
chore(psl): removing parser DB early bailout (#4942)
Druue beb6763
doc cleanup
Druue 9b731ac
clean-up from review
Druue File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
use log::warn; | ||
use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; | ||
use psl::{ | ||
error_tolerant_parse_configuration, | ||
parser_database::{ | ||
walkers::{self, Walker}, | ||
ParserDatabase, RelationFieldId, ScalarFieldType, | ||
}, | ||
schema_ast::ast::{ | ||
self, CompositeTypePosition, EnumPosition, EnumValuePosition, Field, FieldPosition, ModelPosition, | ||
SchemaPosition, WithDocumentation, WithName, | ||
}, | ||
Diagnostics, SourceFile, | ||
}; | ||
|
||
use crate::{offsets::position_to_offset, LSPContext}; | ||
|
||
pub(super) type HoverContext<'a> = LSPContext<'a, HoverParams>; | ||
|
||
impl<'a> HoverContext<'a> { | ||
pub(super) fn position(&self) -> Option<usize> { | ||
let pos = self.params.text_document_position_params.position; | ||
let initiating_doc = self.initiating_file_source(); | ||
|
||
position_to_offset(&pos, initiating_doc) | ||
} | ||
} | ||
|
||
pub fn run(schema_files: Vec<(String, SourceFile)>, params: HoverParams) -> Option<Hover> { | ||
let (_, config, _) = error_tolerant_parse_configuration(&schema_files); | ||
|
||
let db = { | ||
let mut diag = Diagnostics::new(); | ||
ParserDatabase::new(&schema_files, &mut diag) | ||
}; | ||
|
||
let Some(initiating_file_id) = db.file_id(params.text_document_position_params.text_document.uri.as_str()) else { | ||
warn!("Initiating file name is not found in the schema"); | ||
return None; | ||
}; | ||
|
||
let ctx = HoverContext { | ||
db: &db, | ||
config: &config, | ||
initiating_file_id, | ||
params: ¶ms, | ||
}; | ||
|
||
hover(ctx) | ||
} | ||
|
||
fn hover(ctx: HoverContext<'_>) -> Option<Hover> { | ||
let position = match ctx.position() { | ||
Some(pos) => pos, | ||
None => { | ||
warn!("Received a position outside of the document boundaries in HoverParams"); | ||
return None; | ||
} | ||
}; | ||
|
||
let ast = ctx.db.ast(ctx.initiating_file_id); | ||
let contents = match ast.find_at_position(position) { | ||
SchemaPosition::TopLevel => None, | ||
|
||
// --- Block Names --- | ||
SchemaPosition::Model(model_id, ModelPosition::Name(name)) => { | ||
let model = ctx.db.walk((ctx.initiating_file_id, model_id)).ast_model(); | ||
let variant = if model.is_view() { "view" } else { "model" }; | ||
|
||
Some(format_hover_content( | ||
model.documentation().unwrap_or(""), | ||
variant, | ||
name, | ||
None, | ||
)) | ||
} | ||
SchemaPosition::Enum(enum_id, EnumPosition::Name(name)) => { | ||
let enm = ctx.db.walk((ctx.initiating_file_id, enum_id)).ast_enum(); | ||
Some(hover_enum(enm, name)) | ||
} | ||
SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Name(name)) => { | ||
let ct = ctx.db.walk((ctx.initiating_file_id, ct_id)).ast_composite_type(); | ||
Some(hover_composite(ct, name)) | ||
} | ||
|
||
// --- Block Field Names --- | ||
SchemaPosition::Model(model_id, ModelPosition::Field(field_id, FieldPosition::Name(name))) => { | ||
let field = ctx | ||
.db | ||
.walk((ctx.initiating_file_id, model_id)) | ||
.field(field_id) | ||
.ast_field(); | ||
|
||
Some(format_hover_content( | ||
field.documentation().unwrap_or_default(), | ||
"field", | ||
name, | ||
None, | ||
)) | ||
} | ||
SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Field(field_id, FieldPosition::Name(name))) => { | ||
let field = ctx.db.walk((ctx.initiating_file_id, ct_id)).field(field_id).ast_field(); | ||
|
||
Some(format_hover_content( | ||
field.documentation().unwrap_or_default(), | ||
"field", | ||
name, | ||
None, | ||
)) | ||
} | ||
SchemaPosition::Enum(enm_id, EnumPosition::Value(value_id, EnumValuePosition::Name(name))) => { | ||
let value = ctx | ||
.db | ||
.walk((ctx.initiating_file_id, enm_id)) | ||
.value(value_id) | ||
.ast_value(); | ||
|
||
Some(format_hover_content( | ||
value.documentation().unwrap_or_default(), | ||
"value", | ||
name, | ||
None, | ||
)) | ||
} | ||
|
||
// --- Block Field Types --- | ||
SchemaPosition::Model(model_id, ModelPosition::Field(field_id, FieldPosition::Type(name))) => { | ||
let initiating_field = &ctx.db.walk((ctx.initiating_file_id, model_id)).field(field_id); | ||
|
||
initiating_field.refine().and_then(|field| match field { | ||
walkers::RefinedFieldWalker::Scalar(scalar) => match scalar.scalar_field_type() { | ||
ScalarFieldType::CompositeType(_) => { | ||
let ct = scalar.field_type_as_composite_type().unwrap().ast_composite_type(); | ||
Some(hover_composite(ct, ct.name())) | ||
} | ||
ScalarFieldType::Enum(_) => { | ||
let enm = scalar.field_type_as_enum().unwrap().ast_enum(); | ||
Some(hover_enum(enm, enm.name())) | ||
} | ||
_ => None, | ||
}, | ||
walkers::RefinedFieldWalker::Relation(rf) => { | ||
let opposite_model = rf.related_model(); | ||
let relation_info = rf.opposite_relation_field().map(|rf| (rf, rf.ast_field())); | ||
let related_model_type = if opposite_model.ast_model().is_view() { | ||
"view" | ||
} else { | ||
"model" | ||
}; | ||
|
||
Some(format_hover_content( | ||
opposite_model.ast_model().documentation().unwrap_or_default(), | ||
related_model_type, | ||
name, | ||
relation_info, | ||
)) | ||
} | ||
}) | ||
} | ||
|
||
SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Field(field_id, FieldPosition::Type(_))) => { | ||
let field = &ctx.db.walk((ctx.initiating_file_id, ct_id)).field(field_id); | ||
match field.r#type() { | ||
psl::parser_database::ScalarFieldType::CompositeType(_) => { | ||
let ct = field.field_type_as_composite_type().unwrap().ast_composite_type(); | ||
Some(hover_composite(ct, ct.name())) | ||
} | ||
psl::parser_database::ScalarFieldType::Enum(_) => { | ||
let enm = field.field_type_as_enum().unwrap().ast_enum(); | ||
Some(hover_enum(enm, enm.name())) | ||
} | ||
_ => None, | ||
} | ||
} | ||
_ => None, | ||
}; | ||
|
||
contents.map(|contents| Hover { contents, range: None }) | ||
} | ||
|
||
fn hover_enum(enm: &ast::Enum, name: &str) -> HoverContents { | ||
format_hover_content(enm.documentation().unwrap_or_default(), "enum", name, None) | ||
} | ||
|
||
fn hover_composite(ct: &ast::CompositeType, name: &str) -> HoverContents { | ||
format_hover_content(ct.documentation().unwrap_or_default(), "type", name, None) | ||
} | ||
|
||
fn format_hover_content( | ||
documentation: &str, | ||
variant: &str, | ||
name: &str, | ||
relation: Option<(Walker<RelationFieldId>, &Field)>, | ||
) -> HoverContents { | ||
let fancy_line_break = String::from("\n___\n"); | ||
|
||
let (field, relation_kind) = format_relation_info(relation, &fancy_line_break); | ||
|
||
let prisma_display = match variant { | ||
"model" | "enum" | "view" | "type" => { | ||
format!("```prisma\n{variant} {name} {{{field}}}\n```{fancy_line_break}{relation_kind}") | ||
} | ||
"field" | "value" => format!("```prisma\n{name}\n```{fancy_line_break}"), | ||
_ => "".to_owned(), | ||
}; | ||
let full_signature = format!("{prisma_display}{documentation}"); | ||
|
||
HoverContents::Markup(MarkupContent { | ||
kind: MarkupKind::Markdown, | ||
value: full_signature, | ||
}) | ||
} | ||
|
||
fn format_relation_info( | ||
relation: Option<(Walker<RelationFieldId>, &Field)>, | ||
fancy_line_break: &String, | ||
) -> (String, String) { | ||
if let Some((rf, field)) = relation { | ||
let relation = rf.relation(); | ||
|
||
let fields = rf | ||
.referencing_fields() | ||
.map(|fields| fields.map(|f| f.to_string()).collect::<Vec<String>>().join(", ")) | ||
.map_or_else(String::new, |fields| format!(", fields: [{fields}]")); | ||
|
||
let references = rf | ||
.referenced_fields() | ||
.map(|fields| fields.map(|f| f.to_string()).collect::<Vec<String>>().join(", ")) | ||
.map_or_else(String::new, |fields| format!(", references: [{fields}]")); | ||
|
||
let self_relation = if relation.is_self_relation() { " on self" } else { "" }; | ||
let relation_kind = format!("{}{}", relation.relation_kind(), self_relation); | ||
|
||
let relation_name = relation.relation_name(); | ||
let relation_inner = format!("name: \"{relation_name}\"{fields}{references}"); | ||
|
||
( | ||
format!("\n\t...\n\t{field} @relation({relation_inner})\n"), | ||
format!("{relation_kind}{fancy_line_break}"), | ||
) | ||
} else { | ||
("".to_owned(), "".to_owned()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
mod test_api; | ||
mod tests; |
6 changes: 6 additions & 0 deletions
6
prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"contents": { | ||
"kind": "markdown", | ||
"value": "```prisma\ntype TypeA {}\n```\n___\nThis is doc for TypeA" | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
generator js { | ||
provider = "prisma-client-js" | ||
} | ||
|
||
datasource db { | ||
provider = "mongodb" | ||
url = env("DATABASE_URL") | ||
} | ||
|
||
model ModelNameA { | ||
id String @id @map("_id") | ||
bId Int | ||
val TypeA | ||
} | ||
|
||
/// This is doc for TypeA | ||
type Typ<|>eA { | ||
id String | ||
} |
6 changes: 6 additions & 0 deletions
6
prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"contents": { | ||
"kind": "markdown", | ||
"value": "```prisma\ntype Address {}\n```\n___\nAddress Doc" | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
datasource db { | ||
provider = "mongodb" | ||
url = env("DATABASE_URL") | ||
} | ||
|
||
model User { | ||
id String @id @map("_id") | ||
address Add<|>ress | ||
} | ||
|
||
/// Address Doc | ||
type Address { | ||
street String | ||
} |
6 changes: 6 additions & 0 deletions
6
prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"contents": { | ||
"kind": "markdown", | ||
"value": "```prisma\nmodel Animals {\n\t...\n\tfamily Humans[] @relation(name: \"AnimalsToHumans\", fields: [humanIds], references: [id])\n}\n```\n___\nimplicit many-to-many\n___\n" | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain when the
opposite_relation_field
may beNone
? I'm trying to understand when wouldformat_relation_info
return empty stringsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets say we have the following schema
The opposite relation is yet to be defined in the model
Forum
but we still want to be able to hover over where it's used as a field type ininterm
and see it's model documentation