Skip to content

Commit

Permalink
feat(fmt): lsp hover (#4923)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Flavian Desverne <[email protected]>
  • Loading branch information
3 people authored Jul 10, 2024
1 parent 4b84e51 commit 5954db1
Show file tree
Hide file tree
Showing 59 changed files with 938 additions and 106 deletions.
244 changes: 244 additions & 0 deletions prisma-fmt/src/hover.rs
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: &params,
};

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())
}
}
28 changes: 26 additions & 2 deletions prisma-fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ mod code_actions;
mod get_config;
mod get_datamodel;
mod get_dmmf;
mod hover;
mod lint;
mod merge_schemas;
mod native;
mod offsets;
mod preview;
mod references;
mod schema_file_input;
mod text_document_completion;
mod validate;

pub mod offsets;

use log::*;
pub use offsets::span_to_range;
use psl::{
datamodel_connector::Connector, diagnostics::FileId, parser_database::ParserDatabase, Configuration, Datasource,
Generator,
};
use schema_file_input::SchemaFileInput;
use serde_json::json;

#[derive(Debug, Clone, Copy)]
pub(crate) struct LSPContext<'a, T> {
Expand Down Expand Up @@ -110,6 +112,28 @@ pub fn references(schema_files: String, params: &str) -> String {
serde_json::to_string(&references).unwrap()
}

pub fn hover(schema_files: String, params: &str) -> String {
let schema: SchemaFileInput = match serde_json::from_str(&schema_files) {
Ok(schema) => schema,
Err(serde_err) => {
warn!("Failed to deserialize SchemaFileInput: {serde_err}");
return json!(null).to_string();
}
};

let params: lsp_types::HoverParams = match serde_json::from_str(params) {
Ok(params) => params,
Err(_) => {
warn!("Failed to deserialize Hover");
return json!(null).to_string();
}
};

let hover = hover::run(schema.into(), params);

serde_json::to_string(&hover).unwrap()
}

/// The two parameters are:
/// - The [`SchemaFileInput`] to reformat, as a string.
/// - An LSP
Expand Down
5 changes: 4 additions & 1 deletion prisma-fmt/src/references.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use psl::{
Diagnostics, SourceFile,
};

use crate::{offsets::position_to_offset, span_to_range, LSPContext};
use crate::{
offsets::{position_to_offset, span_to_range},
LSPContext,
};

pub(super) type ReferencesContext<'a> = LSPContext<'a, ReferenceParams>;

Expand Down
3 changes: 2 additions & 1 deletion prisma-fmt/tests/code_actions/test_api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use lsp_types::{Diagnostic, DiagnosticSeverity};
use once_cell::sync::Lazy;
use prisma_fmt::span_to_range;

use prisma_fmt::offsets::span_to_range;
use psl::{diagnostics::Span, SourceFile};
use std::{fmt::Write as _, io::Write as _, path::PathBuf};

Expand Down
2 changes: 2 additions & 0 deletions prisma-fmt/tests/hover/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod test_api;
mod tests;
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"
}
}
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
}
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"
}
}
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
}
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"
}
}
Loading

0 comments on commit 5954db1

Please sign in to comment.