Skip to content
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 25 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
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 Jun 14, 2024
fa820c6
Added new wasm endpoint: hover
Druue Jun 14, 2024
da42432
Added hover test setup with test for what is currently implemented
Druue Jun 14, 2024
cbadb98
clippy
Druue Jun 14, 2024
7bcb9e8
Apparently we had Type as a variant of FieldPosition, but it was neve…
Druue Jun 15, 2024
39b21c3
clean up offsets import
Druue Jun 20, 2024
82909c4
add docs for hover api
Druue Jun 27, 2024
f8b92e1
make hover optional
Druue Jul 3, 2024
8fca5fd
relation info
Druue Jul 3, 2024
77e9821
find_top + clippy
Druue Jul 3, 2024
e3fd278
return the correct field one self-relations
Druue Jul 3, 2024
88a5fd2
Add expect for model from view
Druue Jul 3, 2024
c6d65df
relation_info cleanup
Druue Jul 3, 2024
a5c32a9
hover scalar reference types in models
Druue Jul 3, 2024
f2cc369
Show doc on hover of composite and enum block names
Druue Jul 3, 2024
d09d7e0
actually use EnumValueId instead of bare u32s / usizes
Druue Jul 3, 2024
3853c95
add positioning for enum value names
Druue Jul 3, 2024
512d91c
spruce up relation field doc with `@relation(...)` info
Druue Jul 3, 2024
17845a3
fix import
Druue Jul 3, 2024
0ba6317
Test for hover on relation field when said relation model has another…
Druue Jul 4, 2024
7f18ff4
remove postgresql test
Druue Jul 4, 2024
3b8f52a
clarify broken relations test
Druue Jul 5, 2024
e06d9f2
chore(psl): removing parser DB early bailout (#4942)
Druue Jul 9, 2024
beb6763
doc cleanup
Druue Jul 10, 2024
9b731ac
clean-up from review
Druue Jul 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()));
Copy link
Member

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 be None? I'm trying to understand when would format_relation_info return empty strings

Copy link
Contributor Author

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

model interm {
    id Int @id

    forumId Int
    forum   Forum @relation(fields: [forumId], references: [id])
}


/// Forum Doc
model Forum {
    id Int @id
}

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 in interm and see it's model documentation

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 @@ -2,23 +2,25 @@ mod actions;
mod code_actions;
mod get_config;
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 @@ -109,6 +111,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
Loading