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 find references #4934

Merged
merged 25 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
40da783
prep-work for references
Druue Jun 20, 2024
e1b602d
references
Druue Jun 20, 2024
dfdc2cd
rmv duplicate name
Druue Jun 24, 2024
cbf4605
Added test api for references
Druue Jun 25, 2024
80c08de
test enums on name and as type
Druue Jun 25, 2024
2ef3aa4
test composite types on name and as type
Druue Jun 25, 2024
0c2fa93
test models on name and as type
Druue Jun 25, 2024
891eddd
test datasource on name and as attribute
Druue Jun 25, 2024
7d528fe
Actually use AttributePosition
Druue Jun 25, 2024
69556c7
In the process of adding more granularity for references, fixed a bug…
Druue Jun 25, 2024
cf09ea0
find field in model from field in relation fields
Druue Jun 25, 2024
36bbb34
find field in model from field in relation references
Druue Jun 25, 2024
cebc7eb
support finding fields from relations in views
Druue Jun 25, 2024
17a8531
find field from block attribute index & unique
Druue Jun 25, 2024
e2fe053
find datasource from native type
Druue Jun 25, 2024
65bdf1b
Update docs links
Druue Jun 26, 2024
95625aa
clippy
Druue Jun 26, 2024
fffaaee
clean-up
Druue Jun 26, 2024
28a71e4
map should be offered when we aren't in a specific part of an attribute
Druue Jul 2, 2024
43181a6
remove info! logs
Druue Jul 2, 2024
1d337ab
test that mssql default map doesn't show up inside specific sections …
Druue Jul 2, 2024
1cb0fa7
consume split instead of collecting
Druue Jul 2, 2024
dc1be2a
correctly retrieve datasource identifier
Druue Jul 3, 2024
5d25b91
clippy
Druue Jul 3, 2024
3b70d69
don't unwrap split
Druue Jul 3, 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
20 changes: 19 additions & 1 deletion prisma-fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod merge_schemas;
mod native;
mod offsets;
mod preview;
mod references;
mod schema_file_input;
mod text_document_completion;
mod validate;
Expand Down Expand Up @@ -84,13 +85,30 @@ pub fn code_actions(schema_files: String, params: &str) -> String {

let Ok(input) = serde_json::from_str::<SchemaFileInput>(&schema_files) else {
warn!("Failed to parse schema file input");
return serde_json::to_string(&text_document_completion::empty_completion_list()).unwrap();
return serde_json::to_string(&code_actions::empty_code_actions()).unwrap();
};

let actions = code_actions::available_actions(input.into(), params);
serde_json::to_string(&actions).unwrap()
}

pub fn references(schema_files: String, params: &str) -> String {
let params: lsp_types::ReferenceParams = if let Ok(params) = serde_json::from_str(params) {
params
} else {
warn!("Failed to parse params to references() as ReferenceParams.");
return serde_json::to_string(&references::empty_references()).unwrap();
};

let Ok(input) = serde_json::from_str::<SchemaFileInput>(&schema_files) else {
warn!("Failed to parse schema file input");
return serde_json::to_string(&references::empty_references()).unwrap();
};

let references = references::references(input.into(), params);
serde_json::to_string(&references).unwrap()
}

/// The two parameters are:
/// - The [`SchemaFileInput`] to reformat, as a string.
/// - An LSP
Expand Down
242 changes: 242 additions & 0 deletions prisma-fmt/src/references.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
use log::*;
use lsp_types::{Location, ReferenceParams, Url};
use psl::{
error_tolerant_parse_configuration,
parser_database::ParserDatabase,
schema_ast::ast::{
CompositeTypePosition, EnumPosition, Field, FieldId, FieldPosition, FieldType, Identifier, ModelPosition,
SchemaPosition, SourcePosition, Top, WithIdentifier, WithName,
},
Diagnostics, SourceFile,
};

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

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

pub(crate) fn empty_references() -> Vec<Location> {
Vec::new()
}

pub(crate) fn references(schema_files: Vec<(String, SourceFile)>, params: ReferenceParams) -> Vec<Location> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we actually handle finding all the references

info!("Finding references");
Druue marked this conversation as resolved.
Show resolved Hide resolved

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.text_document.uri.as_str()) else {
warn!("Initating file name is not found in the schema");
return empty_references();
};

let initiating_doc = db.source(initiating_file_id);

let position = if let Some(pos) = position_to_offset(&params.text_document_position.position, initiating_doc) {
pos
} else {
warn!("Received a position outside of the document boundaries in ReferenceParams");
return empty_references();
};

let target_position = db.ast(initiating_file_id).find_at_position(position);

let ctx = ReferencesContext {
db: &db,
config: &config,
initiating_file_id,
params: &params,
};

reference_locations_for_target(ctx, target_position)
}

fn reference_locations_for_target(ctx: ReferencesContext<'_>, target: SchemaPosition) -> Vec<Location> {
info!("{:?}", target);

match target {
// Blocks
SchemaPosition::Model(_, ModelPosition::Name(name))
| SchemaPosition::Enum(_, EnumPosition::Name(name))
| SchemaPosition::CompositeType(_, CompositeTypePosition::Name(name)) => {
find_where_used_as_top_name(&ctx, name)
.into_iter()
.chain(find_where_used_as_type(ctx, name))
.collect()
}

SchemaPosition::DataSource(_, SourcePosition::Name(name)) => find_where_used_for_native_type(&ctx, name),

// Fields
SchemaPosition::Model(_, ModelPosition::Field(_, FieldPosition::Type(r#type)))
| SchemaPosition::CompositeType(_, CompositeTypePosition::Field(_, FieldPosition::Type(r#type))) => {
find_where_used_as_top_name(&ctx, r#type)
.into_iter()
.chain(find_where_used_as_type(ctx, r#type))
.collect()
}

// Attributes
SchemaPosition::Model(_, ModelPosition::Field(_, FieldPosition::Attribute(name, _, _)))
| SchemaPosition::CompositeType(_, CompositeTypePosition::Field(_, FieldPosition::Attribute(name, _, _))) => {
let ds_name = ctx.datasource().map(|ds| ds.name.as_str());

match ds_name {
Some(ds_name) if name.contains(ds_name) => find_where_used_as_top_name(&ctx, ds_name),
_ => empty_references(),
}
}

_ => empty_references(),
}
}

fn find_where_used_for_native_type(ctx: &ReferencesContext<'_>, name: &str) -> Vec<Location> {
info!("Get references for native types");

let references = ctx

Check failure on line 100 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

useless conversion to the same type: `impl std::iter::Iterator<Item = psl::parser_database::walkers::Walker<'_, (psl::diagnostics::FileId, psl::parser_database::ast::TopId)>>`
.db
.walk_tops()
.into_iter()
.flat_map(|top| match top.ast_top() {
Top::CompositeType(composite_type) => find_native_type_locations(&ctx, name, composite_type.iter_fields()),

Check failure on line 105 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

this expression creates a reference which is immediately dereferenced by the compiler
Top::Model(model) => find_native_type_locations(&ctx, name, model.iter_fields()),

Check failure on line 106 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

this expression creates a reference which is immediately dereferenced by the compiler

Top::Enum(_) | Top::Source(_) | Top::Generator(_) => empty_references(),
})
.collect();

references
}

fn find_native_type_locations<'a>(
ctx: &ReferencesContext<'_>,
name: &str,
fields: impl Iterator<Item = (FieldId, &'a Field)>,
) -> Vec<Location> {
let identifiers = fields
.filter_map(|field| {
field
.1
.attributes
.iter()
.find(|attr| extract_ds_from_native_type(attr.name()) == name)
.map(|attr| attr.identifier())
})
.collect();

identifiers_to_locations(identifiers, &ctx)

Check failure on line 131 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

this expression creates a reference which is immediately dereferenced by the compiler
}

fn extract_ds_from_native_type(attr_name: &str) -> &str {
let split = attr_name.split(".").collect::<Vec<&str>>()[0];

Check failure on line 135 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

single-character string constant used as pattern
split
}

fn find_where_used_as_type(ctx: ReferencesContext<'_>, name: &str) -> Vec<Location> {
info!("Get references for top");

let references: Vec<Location> = ctx

Check failure on line 142 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

useless conversion to the same type: `impl std::iter::Iterator<Item = psl::parser_database::walkers::Walker<'_, (psl::diagnostics::FileId, psl::parser_database::ast::TopId)>>`
.db
.walk_tops()
.into_iter()
.flat_map(|top| match top.ast_top() {
Top::Model(model) => {
info!("Get references in model");

let fields = model.iter_fields();

let identifiers = get_relevent_identifiers(fields, name);

identifiers_to_locations(identifiers, &ctx)
}
Top::CompositeType(composite_type) => {
info!("Get references in composite type");

let fields = composite_type.iter_fields();
let ids = get_relevent_identifiers(fields, name);

identifiers_to_locations(ids, &ctx)
}
Top::Enum(_) | Top::Source(_) | Top::Generator(_) => empty_references(),
})
.collect();

references
}

// fn find_where_used_in_relation(ctx: &ReferencesContext<'_>, name: &str) -> Vec<Location> {
// empty_references()
// }

fn find_where_used_as_top_name(ctx: &ReferencesContext<'_>, name: &str) -> Vec<Location> {
fn ident_to_location(id: &Identifier, name: &str, ctx: &ReferencesContext<'_>) -> Vec<Location> {
if id.name == name {
identifiers_to_locations(vec![id], ctx)
} else {
empty_references()
}
}

let references: Vec<Location> = ctx

Check failure on line 184 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

useless conversion to the same type: `impl std::iter::Iterator<Item = psl::parser_database::walkers::Walker<'_, (psl::diagnostics::FileId, psl::parser_database::ast::TopId)>>`
.db
.walk_tops()
.into_iter()
.flat_map(|top| match top.ast_top() {
Top::CompositeType(composite_type) => ident_to_location(composite_type.identifier(), name, ctx),

Top::Enum(enm) => ident_to_location(enm.identifier(), name, ctx),

Top::Model(model) => ident_to_location(model.identifier(), name, ctx),

Top::Source(source) => ident_to_location(source.identifier(), name, ctx),

Top::Generator(_) => empty_references(),
})
.collect();

references
}

fn get_relevent_identifiers<'a, 'b>(
fields: impl Iterator<Item = (FieldId, &'a Field)>,
name: &str,
) -> Vec<&'a Identifier> {
fields
.filter_map(|(_id, field)| match &field.field_type {
FieldType::Supported(id) => {
if id.name == name {
Some(id)
} else {
None
}
}
FieldType::Unsupported(_, _) => None,
})
.collect()
}

fn identifiers_to_locations<'a>(ids: Vec<&'a Identifier>, ctx: &ReferencesContext<'_>) -> Vec<Location> {

Check failure on line 222 in prisma-fmt/src/references.rs

View workflow job for this annotation

GitHub Actions / clippy linting

the following explicit lifetimes could be elided: 'a
ids.iter()
.filter_map(|identifier| {
let file_id = identifier.span.file_id;

let source = ctx.db.source(file_id);
let range = span_to_range(identifier.span, source);

let file_name = ctx.db.file_name(file_id);

let uri = if let Ok(uri) = Url::parse(file_name) {
uri
} else {
warn!("Failed to parse file path: {:?}", file_name);
return None;
};

Some(Location { uri, range })
})
.collect()
}
6 changes: 5 additions & 1 deletion prisma-fmt/src/text_document_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use psl::{
diagnostics::Span,
error_tolerant_parse_configuration,
parser_database::{ast, ParserDatabase, SourceFile},
schema_ast::ast::AttributePosition,
Diagnostics, PreviewFeature,
};

Expand Down Expand Up @@ -88,7 +89,10 @@ fn push_ast_completions(ctx: CompletionContext<'_>, completion_list: &mut Comple
match ctx.db.ast(ctx.initiating_file_id).find_at_position(position) {
ast::SchemaPosition::Model(
_model_id,
ast::ModelPosition::Field(_, ast::FieldPosition::Attribute("relation", _, Some(attr_name))),
ast::ModelPosition::Field(
_,
ast::FieldPosition::Attribute("relation", _, AttributePosition::Argument(attr_name)),
),
) if attr_name == "onDelete" || attr_name == "onUpdate" => {
for referential_action in ctx.connector().referential_actions(&relation_mode).iter() {
completion_list.items.push(CompletionItem {
Expand Down
2 changes: 2 additions & 0 deletions prisma-fmt/tests/references/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,14 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder", "views"]
}

datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

type Address {
city String
postCode String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
model User {
id String @id @map("_id")
authorId String
address Add<|>ress
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[
{
"uri": "file:///path/to/a.prisma",
"range": {
"start": {
"line": 10,
"character": 5
},
"end": {
"line": 10,
"character": 12
}
}
},
{
"uri": "file:///path/to/b.prisma",
"range": {
"start": {
"line": 3,
"character": 11
},
"end": {
"line": 3,
"character": 18
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder", "views"]
}

datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

type Add<|>ress {
city String
postCode String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
model User {
id String @id @map("_id")
authorId String
address Address
}
Loading
Loading