-
Notifications
You must be signed in to change notification settings - Fork 235
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adds support for LSP hover * updated the parser database so that it doesn't immediately bail out on schema validation errors. The QE will still only accept fully valid schemas so the parser database the QE interacts with will still only contain fully valid data, but prisma-fmt needs to be able to interact with incomplete schemas. * Updated enum values to actually use `EnumValueId`s rather than bare `u32`s --------- Co-authored-by: Sergey Tatarintsev <tatarintsev@prisma.io> Co-authored-by: Flavian Desverne <flavian.dsv@gmail.com>
- Loading branch information
1 parent
4b84e51
commit 5954db1
Showing
59 changed files
with
938 additions
and
106 deletions.
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.