diff --git a/prisma-fmt/src/code_actions.rs b/prisma-fmt/src/code_actions.rs index 371e791e49cd..891940b6b0e4 100644 --- a/prisma-fmt/src/code_actions.rs +++ b/prisma-fmt/src/code_actions.rs @@ -3,89 +3,120 @@ mod multi_schema; mod relation_mode; mod relations; -use lsp_types::{CodeActionOrCommand, CodeActionParams, Diagnostic, Range, TextEdit, WorkspaceEdit}; +use log::warn; +use lsp_types::{CodeActionOrCommand, CodeActionParams, Diagnostic, Range, TextEdit, Url, WorkspaceEdit}; use psl::{ - diagnostics::Span, + diagnostics::{FileId, Span}, parser_database::{ ast, walkers::{ModelWalker, RefinedRelationWalker, ScalarFieldWalker}, - SourceFile, + ParserDatabase, SourceFile, }, schema_ast::ast::{Attribute, IndentationType, NewlineType, WithSpan}, - PreviewFeature, + Configuration, Datasource, PreviewFeature, }; -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; + +pub(super) struct CodeActionsContext<'a> { + pub(super) db: &'a ParserDatabase, + pub(super) config: &'a Configuration, + pub(super) initiating_file_id: FileId, + pub(super) lsp_params: CodeActionParams, +} + +impl<'a> CodeActionsContext<'a> { + pub(super) fn initiating_file_source(&self) -> &str { + self.db.source(self.initiating_file_id) + } + + pub(super) fn initiating_file_uri(&self) -> &str { + self.db.file_name(self.initiating_file_id) + } + + pub(super) fn diagnostics(&self) -> &[Diagnostic] { + &self.lsp_params.context.diagnostics + } + + pub(super) fn datasource(&self) -> Option<&Datasource> { + self.config.datasources.first() + } + + /// A function to find diagnostics matching the given span. Used for + /// copying the diagnostics to a code action quick fix. + #[track_caller] + pub(super) fn diagnostics_for_span(&self, span: ast::Span) -> impl Iterator { + self.diagnostics().iter().filter(move |diag| { + span.overlaps(crate::range_to_span( + diag.range, + self.initiating_file_source(), + self.initiating_file_id, + )) + }) + } + + pub(super) fn diagnostics_for_span_with_message(&self, span: Span, message: &str) -> Vec { + self.diagnostics_for_span(span) + .filter(|diag| diag.message.contains(message)) + .cloned() + .collect() + } +} pub(crate) fn empty_code_actions() -> Vec { Vec::new() } -pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec { +pub(crate) fn available_actions( + schema_files: Vec<(String, SourceFile)>, + params: CodeActionParams, +) -> Vec { let mut actions = Vec::new(); - let file = SourceFile::new_allocated(Arc::from(schema.into_boxed_str())); - - let validated_schema = psl::validate(file); + let validated_schema = psl::validate_multi_file(schema_files); let config = &validated_schema.configuration; let datasource = config.datasources.first(); + let file_uri = params.text_document.uri.as_str(); + let Some(initiating_file_id) = validated_schema.db.file_id(file_uri) else { + warn!("Initiating file name is not found in the schema"); + return vec![]; + }; - for source in validated_schema.db.ast_assert_single().sources() { - relation_mode::edit_referential_integrity( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - source, - ) + let context = CodeActionsContext { + db: &validated_schema.db, + config, + initiating_file_id, + lsp_params: params, + }; + + let initiating_ast = validated_schema.db.ast(initiating_file_id); + for source in initiating_ast.sources() { + relation_mode::edit_referential_integrity(&mut actions, &context, source) } // models AND views for model in validated_schema .db - .walk_models() - .chain(validated_schema.db.walk_views()) + .walk_models_in_file(initiating_file_id) + .chain(validated_schema.db.walk_views_in_file(initiating_file_id)) { if config.preview_features().contains(PreviewFeature::MultiSchema) { - multi_schema::add_schema_block_attribute_model( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - config, - model, - ); - - multi_schema::add_schema_to_schemas( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - config, - model, - ); + multi_schema::add_schema_block_attribute_model(&mut actions, &context, model); + + multi_schema::add_schema_to_schemas(&mut actions, &context, model); } if matches!(datasource, Some(ds) if ds.active_provider == "mongodb") { - mongodb::add_at_map_for_id(&mut actions, ¶ms, validated_schema.db.source_assert_single(), model); - - mongodb::add_native_for_auto_id( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - model, - datasource.unwrap(), - ); + mongodb::add_at_map_for_id(&mut actions, &context, model); + + mongodb::add_native_for_auto_id(&mut actions, &context, model, datasource.unwrap()); } } - for enumerator in validated_schema.db.walk_enums() { + for enumerator in validated_schema.db.walk_enums_in_file(initiating_file_id) { if config.preview_features().contains(PreviewFeature::MultiSchema) { - multi_schema::add_schema_block_attribute_enum( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - config, - enumerator, - ) + multi_schema::add_schema_block_attribute_enum(&mut actions, &context, enumerator) } } @@ -96,39 +127,20 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec None => continue, }; - relations::add_referenced_side_unique( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - complete_relation, - ); + relations::add_referenced_side_unique(&mut actions, &context, complete_relation); if relation.is_one_to_one() { - relations::add_referencing_side_unique( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - complete_relation, - ); + relations::add_referencing_side_unique(&mut actions, &context, complete_relation); } - if validated_schema.relation_mode().is_prisma() { - relations::add_index_for_relation_fields( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - complete_relation.referencing_field(), - ); + if validated_schema.relation_mode().is_prisma() + && relation.referencing_model().is_defined_in_file(initiating_file_id) + { + relations::add_index_for_relation_fields(&mut actions, &context, complete_relation.referencing_field()); } if validated_schema.relation_mode().uses_foreign_keys() { - relation_mode::replace_set_default_mysql( - &mut actions, - ¶ms, - validated_schema.db.source_assert_single(), - complete_relation, - config, - ) + relation_mode::replace_set_default_mysql(&mut actions, &context, complete_relation) } } } @@ -136,40 +148,6 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec actions } -/// A function to find diagnostics matching the given span. Used for -/// copying the diagnostics to a code action quick fix. -#[track_caller] -pub(super) fn diagnostics_for_span( - schema: &str, - diagnostics: &[Diagnostic], - span: ast::Span, -) -> Option> { - let res: Vec<_> = diagnostics - .iter() - .filter(|diag| span.overlaps(crate::range_to_span(diag.range, schema))) - .cloned() - .collect(); - - if res.is_empty() { - None - } else { - Some(res) - } -} - -fn filter_diagnostics(span_diagnostics: Vec, diagnostic_message: &str) -> Option> { - let diagnostics = span_diagnostics - .into_iter() - .filter(|diag| diag.message.contains(diagnostic_message)) - .collect::>(); - - if diagnostics.is_empty() { - return None; - } - - Some(diagnostics) -} - fn create_missing_attribute<'a>( schema: &str, model: ModelWalker<'a>, @@ -259,15 +237,15 @@ fn format_block_attribute( } fn create_text_edit( - schema: &str, + target_file_uri: &str, + target_file_content: &str, formatted_attribute: String, append: bool, span: Span, - params: &CodeActionParams, -) -> WorkspaceEdit { +) -> Result> { let range = match append { - true => range_after_span(schema, span), - false => span_to_range(schema, span), + true => range_after_span(target_file_content, span), + false => span_to_range(target_file_content, span), }; let text = TextEdit { @@ -276,10 +254,19 @@ fn create_text_edit( }; let mut changes = HashMap::new(); - changes.insert(params.text_document.uri.clone(), vec![text]); + let url = parse_url(target_file_uri)?; + changes.insert(url, vec![text]); - WorkspaceEdit { + Ok(WorkspaceEdit { changes: Some(changes), ..Default::default() + }) +} + +pub(crate) fn parse_url(url: &str) -> Result> { + let result = Url::parse(url); + if result.is_err() { + warn!("Could not parse url {url}") } + Ok(result?) } diff --git a/prisma-fmt/src/code_actions/mongodb.rs b/prisma-fmt/src/code_actions/mongodb.rs index 3da4ef911a32..bfc77130c3e0 100644 --- a/prisma-fmt/src/code_actions/mongodb.rs +++ b/prisma-fmt/src/code_actions/mongodb.rs @@ -1,10 +1,11 @@ use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand}; use psl::{parser_database::walkers::ModelWalker, schema_ast::ast::WithSpan, Datasource}; +use super::CodeActionsContext; + pub(super) fn add_at_map_for_id( actions: &mut Vec, - params: &lsp_types::CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, model: ModelWalker<'_>, ) { let pk = match model.primary_key() { @@ -21,23 +22,29 @@ pub(super) fn add_at_map_for_id( None => return, }; - let span_diagnostics = - match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, model.ast_model().span()) { - Some(sd) => sd, - None => return, - }; - - let diagnostics = match super::filter_diagnostics( - span_diagnostics, + let file_id = model.ast_model().span().file_id; + let file_uri = model.db.file_name(file_id); + let file_content = model.db.source(file_id); + let diagnostics = context.diagnostics_for_span_with_message( + model.ast_model().span(), r#"MongoDB model IDs must have an @map("_id") annotation."#, - ) { - Some(value) => value, - None => return, - }; + ); + + if diagnostics.is_empty() { + return; + } let formatted_attribute = super::format_field_attribute(r#"@map("_id")"#); - let edit = super::create_text_edit(schema, formatted_attribute, true, field.ast_field().span(), params); + let Ok(edit) = super::create_text_edit( + file_uri, + file_content, + formatted_attribute, + true, + field.ast_field().span(), + ) else { + return; + }; let action = CodeAction { title: r#"Add @map("_id")"#.to_owned(), @@ -52,8 +59,7 @@ pub(super) fn add_at_map_for_id( pub(super) fn add_native_for_auto_id( actions: &mut Vec, - params: &lsp_types::CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, model: ModelWalker<'_>, source: &Datasource, ) { @@ -71,23 +77,30 @@ pub(super) fn add_native_for_auto_id( None => return, }; - let span_diagnostics = - match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, model.ast_model().span()) { - Some(sd) => sd, - None => return, - }; + let file_id = model.ast_model().span().file_id; + let file_uri = model.db.file_name(file_id); + let file_content = model.db.source(file_id); - let diagnostics = match super::filter_diagnostics( - span_diagnostics, + let diagnostics = context.diagnostics_for_span_with_message( + model.ast_model().span(), r#"MongoDB `@default(auto())` fields must have `ObjectId` native type."#, - ) { - Some(value) => value, - None => return, - }; + ); + + if diagnostics.is_empty() { + return; + } let formatted_attribute = super::format_field_attribute(format!("@{}.ObjectId", source.name).as_str()); - let edit = super::create_text_edit(schema, formatted_attribute, true, field.ast_field().span(), params); + let Ok(edit) = super::create_text_edit( + file_uri, + file_content, + formatted_attribute, + true, + field.ast_field().span(), + ) else { + return; + }; let action = CodeAction { title: r#"Add @db.ObjectId"#.to_owned(), diff --git a/prisma-fmt/src/code_actions/multi_schema.rs b/prisma-fmt/src/code_actions/multi_schema.rs index aa5aaad05175..309c93aa0d47 100644 --- a/prisma-fmt/src/code_actions/multi_schema.rs +++ b/prisma-fmt/src/code_actions/multi_schema.rs @@ -1,19 +1,18 @@ -use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams}; +use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand}; use psl::{ diagnostics::Span, parser_database::walkers::{EnumWalker, ModelWalker}, schema_ast::ast::WithSpan, - Configuration, }; +use super::CodeActionsContext; + pub(super) fn add_schema_block_attribute_model( actions: &mut Vec, - params: &CodeActionParams, - schema: &str, - config: &Configuration, + context: &CodeActionsContext<'_>, model: ModelWalker<'_>, ) { - let datasource = match config.datasources.first() { + let datasource = match context.datasource() { Some(ds) => ds, None => return, }; @@ -26,17 +25,18 @@ pub(super) fn add_schema_block_attribute_model( return; } - let span_diagnostics = - match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, model.ast_model().span()) { - Some(sd) => sd, - None => return, - }; + let file_id = model.ast_model().span().file_id; + let file_uri = model.db.file_name(file_id); + let file_content = model.db.source(file_id); - let diagnostics = - match super::filter_diagnostics(span_diagnostics, "This model is missing an `@@schema` attribute.") { - Some(value) => value, - None => return, - }; + let diagnostics = context.diagnostics_for_span_with_message( + model.ast_model().span(), + "This model is missing an `@@schema` attribute.", + ); + + if diagnostics.is_empty() { + return; + } let formatted_attribute = super::format_block_attribute( "schema()", @@ -45,7 +45,15 @@ pub(super) fn add_schema_block_attribute_model( &model.ast_model().attributes, ); - let edit = super::create_text_edit(schema, formatted_attribute, true, model.ast_model().span(), params); + let Ok(edit) = super::create_text_edit( + file_uri, + file_content, + formatted_attribute, + true, + model.ast_model().span(), + ) else { + return; + }; let action = CodeAction { title: String::from("Add `@@schema` attribute"), @@ -60,12 +68,10 @@ pub(super) fn add_schema_block_attribute_model( pub(super) fn add_schema_block_attribute_enum( actions: &mut Vec, - params: &CodeActionParams, - schema: &str, - config: &Configuration, + context: &CodeActionsContext<'_>, enumerator: EnumWalker<'_>, ) { - let datasource = match config.datasources.first() { + let datasource = match context.datasource() { Some(ds) => ds, None => return, }; @@ -78,17 +84,18 @@ pub(super) fn add_schema_block_attribute_enum( return; } - let span_diagnostics = - match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, enumerator.ast_enum().span()) { - Some(sd) => sd, - None => return, - }; + let file_id = enumerator.ast_enum().span().file_id; + let file_uri = enumerator.db.file_name(file_id); + let file_content = enumerator.db.source(file_id); - let diagnostics = match super::filter_diagnostics(span_diagnostics, "This enum is missing an `@@schema` attribute.") - { - Some(value) => value, - None => return, - }; + let diagnostics = context.diagnostics_for_span_with_message( + enumerator.ast_enum().span(), + "This enum is missing an `@@schema` attribute.", + ); + + if diagnostics.is_empty() { + return; + } let formatted_attribute = super::format_block_attribute( "schema()", @@ -97,7 +104,15 @@ pub(super) fn add_schema_block_attribute_enum( &enumerator.ast_enum().attributes, ); - let edit = super::create_text_edit(schema, formatted_attribute, true, enumerator.ast_enum().span(), params); + let Ok(edit) = super::create_text_edit( + file_uri, + file_content, + formatted_attribute, + true, + enumerator.ast_enum().span(), + ) else { + return; + }; let action = CodeAction { title: String::from("Add `@@schema` attribute"), @@ -112,38 +127,37 @@ pub(super) fn add_schema_block_attribute_enum( pub(super) fn add_schema_to_schemas( actions: &mut Vec, - params: &CodeActionParams, - schema: &str, - config: &Configuration, + context: &CodeActionsContext<'_>, model: ModelWalker<'_>, ) { - let datasource = match config.datasources.first() { + let datasource = match context.datasource() { Some(ds) => ds, None => return, }; - let span_diagnostics = - match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, model.ast_model().span()) { - Some(sd) => sd, - None => return, - }; + let diagnostics = context.diagnostics_for_span_with_message( + model.ast_model().span(), + "This schema is not defined in the datasource.", + ); - let diagnostics = match super::filter_diagnostics(span_diagnostics, "This schema is not defined in the datasource.") - { - Some(value) => value, - None => return, - }; + if diagnostics.is_empty() { + return; + } + + let datasource_file_id = datasource.span.file_id; + let datasource_file_uri = context.db.file_name(datasource_file_id); + let datasource_content = context.db.source(datasource_file_id); let edit = match datasource.schemas_span { Some(span) => { let formatted_attribute = format!(r#"", "{}""#, model.schema_name().unwrap()); super::create_text_edit( - schema, + datasource_file_uri, + datasource_content, formatted_attribute, true, // todo: update spans so that we can just append to the end of the _inside_ of the array. Instead of needing to re-append the `]` or taking the span end -1 Span::new(span.start, span.end - 1, psl::parser_database::FileId::ZERO), - params, ) } None => { @@ -161,10 +175,20 @@ pub(super) fn add_schema_to_schemas( has_properties, ); - super::create_text_edit(schema, formatted_attribute, true, datasource.url_span, params) + super::create_text_edit( + datasource_file_uri, + datasource_content, + formatted_attribute, + true, + datasource.url_span, + ) } }; + let Ok(edit) = edit else { + return; + }; + let action = CodeAction { title: String::from("Add schema to schemas"), kind: Some(CodeActionKind::QUICKFIX), diff --git a/prisma-fmt/src/code_actions/relation_mode.rs b/prisma-fmt/src/code_actions/relation_mode.rs index 751fb956073b..0367e65d7169 100644 --- a/prisma-fmt/src/code_actions/relation_mode.rs +++ b/prisma-fmt/src/code_actions/relation_mode.rs @@ -1,10 +1,11 @@ use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand}; -use psl::{parser_database::walkers::CompleteInlineRelationWalker, schema_ast::ast::SourceConfig, Configuration}; +use psl::{parser_database::walkers::CompleteInlineRelationWalker, schema_ast::ast::SourceConfig}; + +use super::CodeActionsContext; pub(crate) fn edit_referential_integrity( actions: &mut Vec, - params: &lsp_types::CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, source: &SourceConfig, ) { let prop = match source.properties.iter().find(|p| p.name.name == "referentialIntegrity") { @@ -12,18 +13,18 @@ pub(crate) fn edit_referential_integrity( None => return, }; - let span_diagnostics = match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, source.span) { - Some(sd) => sd, - None => return, - }; - let diagnostics = - match super::filter_diagnostics(span_diagnostics, "The `referentialIntegrity` attribute is deprecated.") { - Some(value) => value, - None => return, - }; + context.diagnostics_for_span_with_message(source.span, "The `referentialIntegrity` attribute is deprecated."); - let edit = super::create_text_edit(schema, "relationMode".to_owned(), false, prop.name.span, params); + let Ok(edit) = super::create_text_edit( + context.initiating_file_uri(), + context.initiating_file_source(), + "relationMode".to_owned(), + false, + prop.name.span, + ) else { + return; + }; let action = CodeAction { title: String::from("Rename property to relationMode"), @@ -38,12 +39,10 @@ pub(crate) fn edit_referential_integrity( pub(crate) fn replace_set_default_mysql( actions: &mut Vec, - params: &lsp_types::CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, relation: CompleteInlineRelationWalker<'_>, - config: &Configuration, ) { - let datasource = match config.datasources.first() { + let datasource = match context.datasource() { Some(ds) => ds, None => return, }; @@ -57,20 +56,26 @@ pub(crate) fn replace_set_default_mysql( None => return, }; - let span_diagnostics = match super::diagnostics_for_span(schema, ¶ms.context.diagnostics, span) { - Some(sd) => sd, - None => return, - }; + if span.file_id != context.initiating_file_id { + return; + } + + let file_name = context.initiating_file_uri(); + let file_content = context.initiating_file_source(); + + let diagnostics = + context.diagnostics_for_span_with_message( + span, + "MySQL does not actually support the `SetDefault` referential action, so using it may result in unexpected errors." + ); - let diagnostics = match - super::filter_diagnostics( - span_diagnostics, - "MySQL does not actually support the `SetDefault` referential action, so using it may result in unexpected errors.") { - Some(value) => value, - None => return, - }; + if diagnostics.is_empty() { + return; + } - let edit = super::create_text_edit(schema, "NoAction".to_owned(), false, span, params); + let Ok(edit) = super::create_text_edit(file_name, file_content, "NoAction".to_owned(), false, span) else { + return; + }; let action = CodeAction { title: r#"Replace SetDefault with NoAction"#.to_owned(), diff --git a/prisma-fmt/src/code_actions/relations.rs b/prisma-fmt/src/code_actions/relations.rs index 3f84e9ddb7ce..850c1182dfe2 100644 --- a/prisma-fmt/src/code_actions/relations.rs +++ b/prisma-fmt/src/code_actions/relations.rs @@ -1,11 +1,11 @@ -use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, TextEdit, WorkspaceEdit}; +use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, TextEdit, WorkspaceEdit}; use psl::parser_database::{ ast::WithSpan, walkers::{CompleteInlineRelationWalker, RelationFieldWalker}, }; use std::collections::HashMap; -use super::format_block_attribute; +use super::{format_block_attribute, parse_url, CodeActionsContext}; /// If the referencing side of the one-to-one relation does not point /// to a unique constraint, the action adds the attribute. @@ -49,8 +49,7 @@ use super::format_block_attribute; /// ``` pub(super) fn add_referencing_side_unique( actions: &mut Vec, - params: &CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, relation: CompleteInlineRelationWalker<'_>, ) { if relation @@ -69,14 +68,14 @@ pub(super) fn add_referencing_side_unique( let attribute_name = "unique"; let text = super::create_missing_attribute( - schema, + context.initiating_file_source(), relation.referencing_model(), relation.referencing_fields(), attribute_name, ); let mut changes = HashMap::new(); - changes.insert(params.text_document.uri.clone(), vec![text]); + changes.insert(context.lsp_params.text_document.uri.clone(), vec![text]); let edit = WorkspaceEdit { changes: Some(changes), @@ -85,17 +84,16 @@ pub(super) fn add_referencing_side_unique( // The returned diagnostics are the ones we promise to fix with // the code action. - let diagnostics = super::diagnostics_for_span( - schema, - ¶ms.context.diagnostics, - relation.referencing_field().ast_field().span(), - ); + let diagnostics = context + .diagnostics_for_span(relation.referencing_field().ast_field().span()) + .cloned() + .collect(); let action = CodeAction { title: String::from("Make referencing fields unique"), kind: Some(CodeActionKind::QUICKFIX), edit: Some(edit), - diagnostics, + diagnostics: Some(diagnostics), ..Default::default() }; @@ -141,8 +139,7 @@ pub(super) fn add_referencing_side_unique( /// ``` pub(super) fn add_referenced_side_unique( actions: &mut Vec, - params: &CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, relation: CompleteInlineRelationWalker<'_>, ) { if relation @@ -153,6 +150,10 @@ pub(super) fn add_referenced_side_unique( return; } + let file_id = relation.referenced_model().ast_model().span().file_id; + let file_uri = relation.db.file_name(file_id); + let file_content = relation.db.source(file_id); + match (relation.referencing_fields().len(), relation.referenced_fields().len()) { (0, 0) => return, (a, b) if a != b => return, @@ -161,14 +162,17 @@ pub(super) fn add_referenced_side_unique( let attribute_name = "unique"; let text = super::create_missing_attribute( - schema, + file_content, relation.referenced_model(), relation.referenced_fields(), attribute_name, ); let mut changes = HashMap::new(); - changes.insert(params.text_document.uri.clone(), vec![text]); + let Ok(url) = parse_url(file_uri) else { + return; + }; + changes.insert(url, vec![text]); let edit = WorkspaceEdit { changes: Some(changes), @@ -177,17 +181,16 @@ pub(super) fn add_referenced_side_unique( // The returned diagnostics are the ones we promise to fix with // the code action. - let diagnostics = super::diagnostics_for_span( - schema, - ¶ms.context.diagnostics, - relation.referencing_field().ast_field().span(), - ); + let diagnostics = context + .diagnostics_for_span(relation.referencing_field().ast_field().span()) + .cloned() + .collect(); let action = CodeAction { title: String::from("Make referenced field(s) unique"), kind: Some(CodeActionKind::QUICKFIX), edit: Some(edit), - diagnostics, + diagnostics: Some(diagnostics), ..Default::default() }; @@ -241,8 +244,7 @@ pub(super) fn add_referenced_side_unique( /// ``` pub(super) fn add_index_for_relation_fields( actions: &mut Vec, - params: &CodeActionParams, - schema: &str, + context: &CodeActionsContext<'_>, relation: RelationFieldWalker<'_>, ) { let fields = match relation.fields() { @@ -269,33 +271,26 @@ pub(super) fn add_index_for_relation_fields( &relation.model().ast_model().attributes, ); - let range = super::range_after_span(schema, relation.model().ast_model().span()); + let range = super::range_after_span(context.initiating_file_source(), relation.model().ast_model().span()); let text = TextEdit { range, new_text: formatted_attribute, }; let mut changes = HashMap::new(); - changes.insert(params.text_document.uri.clone(), vec![text]); + changes.insert(context.lsp_params.text_document.uri.clone(), vec![text]); let edit = WorkspaceEdit { changes: Some(changes), ..Default::default() }; - let span_diagnostics = match super::diagnostics_for_span( - schema, - ¶ms.context.diagnostics, - relation.relation_attribute().unwrap().span(), - ) { - Some(sd) => sd, - None => return, - }; + let diagnostics = context + .diagnostics_for_span_with_message(relation.relation_attribute().unwrap().span, "relationMode = \"prisma\""); - let diagnostics = match super::filter_diagnostics(span_diagnostics, "relationMode = \"prisma\"") { - Some(value) => value, - None => return, - }; + if diagnostics.is_empty() { + return; + } let action = CodeAction { title: String::from("Add an index for the relation's field(s)"), diff --git a/prisma-fmt/src/get_config.rs b/prisma-fmt/src/get_config.rs index ad7895358476..3da4a2a022ca 100644 --- a/prisma-fmt/src/get_config.rs +++ b/prisma-fmt/src/get_config.rs @@ -43,8 +43,8 @@ pub(crate) fn get_config(params: &str) -> Result { } fn get_config_impl(params: GetConfigParams) -> Result { - let (files, mut config) = - psl::parse_configuration_multi_file(params.prisma_schema.into()).map_err(create_get_config_error)?; + let prisma_schema: Vec<_> = params.prisma_schema.into(); + let (files, mut config) = psl::parse_configuration_multi_file(&prisma_schema).map_err(create_get_config_error)?; if !params.ignore_env_var_errors { let overrides: Vec<(_, _)> = params.datasource_overrides.into_iter().collect(); diff --git a/prisma-fmt/src/lib.rs b/prisma-fmt/src/lib.rs index 8cbca952234d..d44c9b76938e 100644 --- a/prisma-fmt/src/lib.rs +++ b/prisma-fmt/src/lib.rs @@ -12,14 +12,14 @@ mod validate; use log::*; use lsp_types::{Position, Range}; -use psl::parser_database::ast; +use psl::{diagnostics::FileId, parser_database::ast}; use schema_file_input::SchemaFileInput; /// The API is modelled on an LSP [completion /// request](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#textDocument_completion). /// Input and output are both JSON, the request being a `CompletionParams` object and the response /// being a `CompletionList` object. -pub fn text_document_completion(schema: String, params: &str) -> String { +pub fn text_document_completion(schema_files: String, params: &str) -> String { let params = if let Ok(params) = serde_json::from_str::(params) { params } else { @@ -27,13 +27,18 @@ pub fn text_document_completion(schema: String, params: &str) -> String { return serde_json::to_string(&text_document_completion::empty_completion_list()).unwrap(); }; - let completion_list = text_document_completion::completion(schema, params); + let Ok(input) = serde_json::from_str::(&schema_files) else { + warn!("Failed to parse schema file input"); + return serde_json::to_string(&text_document_completion::empty_completion_list()).unwrap(); + }; + + let completion_list = text_document_completion::completion(input.into(), params); serde_json::to_string(&completion_list).unwrap() } /// This API is modelled on an LSP [code action request](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#textDocument_codeAction=). Input and output are both JSON, the request being a `CodeActionParams` object and the response being a list of `CodeActionOrCommand` objects. -pub fn code_actions(schema: String, params: &str) -> String { +pub fn code_actions(schema_files: String, params: &str) -> String { let params = if let Ok(params) = serde_json::from_str::(params) { params } else { @@ -41,7 +46,12 @@ pub fn code_actions(schema: String, params: &str) -> String { return serde_json::to_string(&code_actions::empty_code_actions()).unwrap(); }; - let actions = code_actions::available_actions(schema, params); + let Ok(input) = serde_json::from_str::(&schema_files) else { + warn!("Failed to parse schema file input"); + return serde_json::to_string(&text_document_completion::empty_completion_list()).unwrap(); + }; + + let actions = code_actions::available_actions(input.into(), params); serde_json::to_string(&actions).unwrap() } @@ -254,11 +264,11 @@ pub(crate) fn position_to_offset(position: &Position, document: &str) -> Option< #[track_caller] /// Converts an LSP range to a span. -pub(crate) fn range_to_span(range: Range, document: &str) -> ast::Span { +pub(crate) fn range_to_span(range: Range, document: &str, file_id: FileId) -> ast::Span { let start = position_to_offset(&range.start, document).unwrap(); let end = position_to_offset(&range.end, document).unwrap(); - ast::Span::new(start, end, psl::parser_database::FileId::ZERO) + ast::Span::new(start, end, file_id) } /// Gives the LSP position right after the given span. diff --git a/prisma-fmt/src/text_document_completion.rs b/prisma-fmt/src/text_document_completion.rs index ea7329eb3da5..49151c27b497 100644 --- a/prisma-fmt/src/text_document_completion.rs +++ b/prisma-fmt/src/text_document_completion.rs @@ -3,12 +3,11 @@ use log::*; use lsp_types::*; use psl::{ datamodel_connector::Connector, - diagnostics::Span, - parse_configuration, + diagnostics::{FileId, Span}, + parse_configuration_multi_file, parser_database::{ast, ParserDatabase, SourceFile}, Configuration, Datasource, Diagnostics, Generator, PreviewFeature, }; -use std::sync::Arc; use crate::position_to_offset; @@ -21,18 +20,10 @@ pub(crate) fn empty_completion_list() -> CompletionList { } } -pub(crate) fn completion(schema: String, params: CompletionParams) -> CompletionList { - let source_file = SourceFile::new_allocated(Arc::from(schema.into_boxed_str())); - - let position = - if let Some(pos) = super::position_to_offset(¶ms.text_document_position.position, source_file.as_str()) { - pos - } else { - warn!("Received a position outside of the document boundaries in CompletionParams"); - return empty_completion_list(); - }; - - let config = parse_configuration(source_file.as_str()).ok(); +pub(crate) fn completion(schema_files: Vec<(String, SourceFile)>, params: CompletionParams) -> CompletionList { + let config = parse_configuration_multi_file(&schema_files) + .ok() + .map(|(_, config)| config); let mut list = CompletionList { is_incomplete: false, @@ -41,7 +32,21 @@ pub(crate) fn completion(schema: String, params: CompletionParams) -> Completion let db = { let mut diag = Diagnostics::new(); - ParserDatabase::new_single_file(source_file, &mut diag) + 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!("Initiating file name is not found in the schema"); + return empty_completion_list(); + }; + + let initiating_doc = db.source(initiating_file_id); + let position = if let Some(pos) = super::position_to_offset(¶ms.text_document_position.position, initiating_doc) + { + pos + } else { + warn!("Received a position outside of the document boundaries in CompletionParams"); + return empty_completion_list(); }; let ctx = CompletionContext { @@ -49,6 +54,7 @@ pub(crate) fn completion(schema: String, params: CompletionParams) -> Completion params: ¶ms, db: &db, position, + initiating_file_id, }; push_ast_completions(ctx, &mut list); @@ -62,6 +68,7 @@ struct CompletionContext<'a> { params: &'a CompletionParams, db: &'a ParserDatabase, position: usize, + initiating_file_id: FileId, } impl<'a> CompletionContext<'a> { @@ -96,7 +103,7 @@ fn push_ast_completions(ctx: CompletionContext<'_>, completion_list: &mut Comple _ => ctx.connector().default_relation_mode(), }; - match ctx.db.ast_assert_single().find_at_position(ctx.position) { + match ctx.db.ast(ctx.initiating_file_id).find_at_position(ctx.position) { ast::SchemaPosition::Model( _model_id, ast::ModelPosition::Field(_, ast::FieldPosition::Attribute("relation", _, Some(attr_name))), @@ -195,7 +202,7 @@ fn ds_has_prop(ctx: CompletionContext<'_>, prop: &str) -> bool { fn push_namespaces(ctx: CompletionContext<'_>, completion_list: &mut CompletionList) { for (namespace, _) in ctx.namespaces() { - let insert_text = if add_quotes(ctx.params, ctx.db.source_assert_single()) { + let insert_text = if add_quotes(ctx.params, ctx.db.source(ctx.initiating_file_id)) { format!(r#""{namespace}""#) } else { namespace.to_string() diff --git a/prisma-fmt/src/text_document_completion/datasource.rs b/prisma-fmt/src/text_document_completion/datasource.rs index 22da182868ae..6532d12be533 100644 --- a/prisma-fmt/src/text_document_completion/datasource.rs +++ b/prisma-fmt/src/text_document_completion/datasource.rs @@ -144,7 +144,7 @@ pub(super) fn url_env_db_completion(completion_list: &mut CompletionList, kind: _ => unreachable!(), }; - let insert_text = if add_quotes(ctx.params, ctx.db.source_assert_single()) { + let insert_text = if add_quotes(ctx.params, ctx.db.source(ctx.initiating_file_id)) { format!(r#""{text}""#) } else { text.to_owned() diff --git a/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/_target.prisma new file mode 100644 index 000000000000..6ca9071e74e3 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/_target.prisma @@ -0,0 +1,3 @@ +model Kattbjorn { + id String @id +} diff --git a/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/config.prisma b/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/config.prisma new file mode 100644 index 000000000000..0362423c75a4 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/config.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/result.json new file mode 100644 index 000000000000..fcfe7e97d7e5 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/mongodb_at_map_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Add @map(\"_id\")", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "severity": 1, + "message": "Error validating field `id` in model `Kattbjorn`: MongoDB model IDs must have an @map(\"_id\") annotation." + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 1, + "character": 17 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "newText": " @map(\"_id\")\n" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/_target.prisma new file mode 100644 index 000000000000..39ea6e2c70a9 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/_target.prisma @@ -0,0 +1,5 @@ +model A { + id Int @id + + @@schema("base") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/datasource.prisma new file mode 100644 index 000000000000..ad9034e8e487 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/datasource.prisma @@ -0,0 +1,6 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + schemas = ["a", "b"] + relationMode = "prisma" +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/generator.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/generator.prisma new file mode 100644 index 000000000000..dd349532f781 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/generator.prisma @@ -0,0 +1,4 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema", "prismaSchemaFolder"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/result.json new file mode 100644 index 000000000000..181c63dd4785 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_add_to_existing_schemas_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Add schema to schemas", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 13 + }, + "end": { + "line": 3, + "character": 19 + } + }, + "severity": 1, + "message": "This schema is not defined in the datasource. Read more on `@@schema` at https://pris.ly/d/multi-schema" + } + ], + "edit": { + "changes": { + "file:///path/to/datasource.prisma": [ + { + "range": { + "start": { + "line": 3, + "character": 27 + }, + "end": { + "line": 3, + "character": 28 + } + }, + "newText": "\", \"base\"" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/_target.prisma new file mode 100644 index 000000000000..edc69dac2950 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/_target.prisma @@ -0,0 +1,3 @@ +model User { + id Int @id +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/datasource.prisma new file mode 100644 index 000000000000..9ae54b6ef5ab --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/datasource.prisma @@ -0,0 +1,5 @@ +datasource db { + provider = "postgresql" + url = env("TEST_DATABASE_URL") + schemas = ["one", "two"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/generator.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/generator.prisma new file mode 100644 index 000000000000..dd349532f781 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/generator.prisma @@ -0,0 +1,4 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema", "prismaSchemaFolder"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/result.json new file mode 100644 index 000000000000..a719b5b15b31 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Add `@@schema` attribute", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + }, + "severity": 1, + "message": "Error validating model \"User\": This model is missing an `@@schema` attribute." + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + }, + "newText": "\n @@schema()\n}" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/_target.prisma new file mode 100644 index 000000000000..9f874a6f3cb8 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/_target.prisma @@ -0,0 +1,7 @@ +model User { + id Int @id +} + +enum Colour { + Red +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/config.prisma b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/config.prisma new file mode 100644 index 000000000000..931f92e8c713 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/config.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema", "prismaSchemaFolder"] +} + +datasource db { + provider = "postgresql" + url = env("TEST_DATABASE_URL") + schemas = ["one", "two"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/result.json new file mode 100644 index 000000000000..af6834985b98 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/multi_schema_one_model_one_enum_multifile/result.json @@ -0,0 +1,80 @@ +[ + { + "title": "Add `@@schema` attribute", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + }, + "severity": 1, + "message": "Error validating model \"User\": This model is missing an `@@schema` attribute." + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + }, + "newText": "\n @@schema()\n}" + } + ] + } + } + }, + { + "title": "Add `@@schema` attribute", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 6, + "character": 1 + } + }, + "severity": 1, + "message": "This enum is missing an `@@schema` attribute." + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 6, + "character": 1 + } + }, + "newText": "\n @@schema()\n}" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/A.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/A.prisma new file mode 100644 index 000000000000..3b8fd5027039 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/A.prisma @@ -0,0 +1,6 @@ +model A { + id Int @id + field1 Int + field2 Int + B B[] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/_target.prisma new file mode 100644 index 000000000000..637a2fafd05a --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/_target.prisma @@ -0,0 +1,8 @@ +model B { + id Int @id + bId1 Int + bId2 Int + A A @relation(fields: [bId1, bId2], references: [field1, field2]) + + @@unique([bId1, bId2]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/datasource.prisma new file mode 100644 index 000000000000..8f0b81f14329 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/result.json new file mode 100644 index 000000000000..82863e96a923 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_compound_field_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Make referenced field(s) unique", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 2 + }, + "end": { + "line": 5, + "character": 0 + } + }, + "severity": 1, + "message": "Error parsing attribute \"@relation\": The argument `references` must refer to a unique criterion in the related model. Consider adding an `@@unique([field1, field2])` attribute to the model `A`." + } + ], + "edit": { + "changes": { + "file:///path/to/A.prisma": [ + { + "range": { + "start": { + "line": 5, + "character": 0 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "newText": "\n @@unique([field1, field2])\n}" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/A.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/A.prisma new file mode 100644 index 000000000000..62b017574614 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/A.prisma @@ -0,0 +1,5 @@ +model A { + id Int @id + field Int + B B[] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/_target.prisma new file mode 100644 index 000000000000..d4b0adb57089 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/_target.prisma @@ -0,0 +1,5 @@ +model B { + id Int @id + bId Int + A A @relation(fields: [bId], references: [field]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/datasource.prisma new file mode 100644 index 000000000000..c26ac8e3a79e --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/result.json new file mode 100644 index 000000000000..956353f0c9b8 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_many_referenced_side_misses_unique_single_field_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Make referenced field(s) unique", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 4, + "character": 0 + } + }, + "severity": 1, + "message": "Error parsing attribute \"@relation\": The argument `references` must refer to a unique criterion in the related model. Consider adding an `@unique` attribute to the field `field` in the model `A`." + } + ], + "edit": { + "changes": { + "file:///path/to/A.prisma": [ + { + "range": { + "start": { + "line": 2, + "character": 13 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "newText": " @unique" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/A.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/A.prisma new file mode 100644 index 000000000000..ec824442c022 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/A.prisma @@ -0,0 +1,6 @@ +model A { + id Int @id + field1 Int + field2 Int + B B? +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/_target.prisma new file mode 100644 index 000000000000..637a2fafd05a --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/_target.prisma @@ -0,0 +1,8 @@ +model B { + id Int @id + bId1 Int + bId2 Int + A A @relation(fields: [bId1, bId2], references: [field1, field2]) + + @@unique([bId1, bId2]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/datasource.prisma new file mode 100644 index 000000000000..c26ac8e3a79e --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/generator.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/generator.prisma new file mode 100644 index 000000000000..2325fac338e3 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/generator.prisma @@ -0,0 +1,4 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/result.json new file mode 100644 index 000000000000..4bb25efd7bc6 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_compound_field_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Make referenced field(s) unique", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 2 + }, + "end": { + "line": 5, + "character": 0 + } + }, + "severity": 1, + "message": "Error parsing attribute \"@relation\": The argument `references` must refer to a unique criterion in the related model. Consider adding an `@@unique([field1, field2])` attribute to the model `A`." + } + ], + "edit": { + "changes": { + "file:///path/to/A.prisma": [ + { + "range": { + "start": { + "line": 5, + "character": 0 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "newText": "\n @@unique([field1, field2])\n}" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/A.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/A.prisma new file mode 100644 index 000000000000..22a6b353d34e --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/A.prisma @@ -0,0 +1,5 @@ +model A { + id Int @id + field Int + B B? +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/_target.prisma new file mode 100644 index 000000000000..2877fa449b3d --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/_target.prisma @@ -0,0 +1,5 @@ +model B { + id Int @id + bId Int @unique + A A @relation(fields: [bId], references: [field]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/datasource.prisma new file mode 100644 index 000000000000..c26ac8e3a79e --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/generator.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/generator.prisma new file mode 100644 index 000000000000..2325fac338e3 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/generator.prisma @@ -0,0 +1,4 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/result.json new file mode 100644 index 000000000000..b488d8e75fff --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referenced_side_misses_unique_single_field_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Make referenced field(s) unique", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 4, + "character": 0 + } + }, + "severity": 1, + "message": "Error parsing attribute \"@relation\": The argument `references` must refer to a unique criterion in the related model. Consider adding an `@unique` attribute to the field `field` in the model `A`." + } + ], + "edit": { + "changes": { + "file:///path/to/A.prisma": [ + { + "range": { + "start": { + "line": 2, + "character": 11 + }, + "end": { + "line": 2, + "character": 11 + } + }, + "newText": " @unique" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/A.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/A.prisma new file mode 100644 index 000000000000..a73233194368 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/A.prisma @@ -0,0 +1,8 @@ +model A { + id Int @id + field1 Int + field2 Int + B B? + + @@unique([field1, field2]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/_target.prisma new file mode 100644 index 000000000000..1ce1e30dbe53 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/_target.prisma @@ -0,0 +1,6 @@ +model B { + id Int @id + bId1 Int + bId2 Int + A A @relation(fields: [bId1, bId2], references: [field1, field2]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/datasource.prisma new file mode 100644 index 000000000000..c26ac8e3a79e --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/generator.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/generator.prisma new file mode 100644 index 000000000000..2325fac338e3 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/generator.prisma @@ -0,0 +1,4 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/result.json new file mode 100644 index 000000000000..8f2d42bd3ce1 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_compound_field_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Make referencing fields unique", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 2 + }, + "end": { + "line": 5, + "character": 0 + } + }, + "severity": 1, + "message": "Error parsing attribute \"@relation\": A one-to-one relation must use unique fields on the defining side. Either add an `@@unique([bId1, bId2])` attribute to the model, or change the relation to one-to-many." + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 5, + "character": 0 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "newText": "\n @@unique([bId1, bId2])\n}" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/A.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/A.prisma new file mode 100644 index 000000000000..4c8ef27b3e8c --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/A.prisma @@ -0,0 +1,5 @@ +model A { + id Int @id + field Int @unique + B B? +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/_target.prisma new file mode 100644 index 000000000000..d4b0adb57089 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/_target.prisma @@ -0,0 +1,5 @@ +model B { + id Int @id + bId Int + A A @relation(fields: [bId], references: [field]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/datasource.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/datasource.prisma new file mode 100644 index 000000000000..c26ac8e3a79e --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/generator.prisma b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/generator.prisma new file mode 100644 index 000000000000..2325fac338e3 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/generator.prisma @@ -0,0 +1,4 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/result.json new file mode 100644 index 000000000000..c0ebbc5ec6ab --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/one_to_one_referencing_side_misses_unique_single_field_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Make referencing fields unique", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 4, + "character": 0 + } + }, + "severity": 1, + "message": "Error parsing attribute \"@relation\": A one-to-one relation must use unique fields on the defining side. Either add an `@unique` attribute to the field `bId`, or change the relation to one-to-many." + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 2, + "character": 9 + }, + "end": { + "line": 2, + "character": 9 + } + }, + "newText": " @unique" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/Bar.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/Bar.prisma new file mode 100644 index 000000000000..950d86686923 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/Bar.prisma @@ -0,0 +1,4 @@ +model Bar { + id Int @id + foo Foo? +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/Test.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/Test.prisma new file mode 100644 index 000000000000..48e6b86bc1bc --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/Test.prisma @@ -0,0 +1,5 @@ +// This is a test enum. +enum Test { + TestUno + TestDue +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/_target.prisma new file mode 100644 index 000000000000..b57ffc80b959 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/_target.prisma @@ -0,0 +1,8 @@ +/// multi line +/// commennttt +model Foo { + id Int @id + bar Bar @relation(fields: [bar_id], references: [id], onUpdate: SetDefault) + bar_id Int @unique + t Test +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/config.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/config.prisma new file mode 100644 index 000000000000..24d8fc9a0dff --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/config.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + relationMode = "foreignKeys" +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/result.json new file mode 100644 index 000000000000..bf5b524d5243 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_mysql_foreign_keys_set_default_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Replace SetDefault with NoAction", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 62 + }, + "end": { + "line": 4, + "character": 82 + } + }, + "severity": 2, + "message": "MySQL does not actually support the `SetDefault` referential action, so using it may result in unexpected errors. Read more at https://pris.ly/d/mysql-set-default " + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 4, + "character": 72 + }, + "end": { + "line": 4, + "character": 82 + } + }, + "newText": "NoAction" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/SomeUser.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/SomeUser.prisma new file mode 100644 index 000000000000..b410777b6990 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/SomeUser.prisma @@ -0,0 +1,4 @@ +model SomeUser { + id Int @id + posts Post[] +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/_target.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/_target.prisma new file mode 100644 index 000000000000..475a7884814a --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/_target.prisma @@ -0,0 +1,5 @@ +model Post { + id Int @id + userId Int + user SomeUser @relation(fields: [userId], references: [id]) +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/config.prisma b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/config.prisma new file mode 100644 index 000000000000..1abe1590c7a6 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/config.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} + +datasource db { + provider = "mysql" + url = env("TEST_DATABASE_URL") + relationMode = "prisma" +} diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/result.json b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/result.json new file mode 100644 index 000000000000..ec9be9472974 --- /dev/null +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_prisma_missing_index_multifile/result.json @@ -0,0 +1,41 @@ +[ + { + "title": "Add an index for the relation's field(s)", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 20 + }, + "end": { + "line": 3, + "character": 65 + } + }, + "severity": 2, + "message": "With `relationMode = \"prisma\"`, no foreign keys are used, so relation fields will not benefit from the index usually created by the relational database under the hood. This can lead to poor performance when querying these fields. We recommend adding an index manually. Learn more at https://pris.ly/d/relation-mode-prisma-indexes\" " + } + ], + "edit": { + "changes": { + "file:///path/to/_target.prisma": [ + { + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "newText": "\n @@index([userId])\n}" + } + ] + } + } + } +] \ No newline at end of file diff --git a/prisma-fmt/tests/code_actions/test_api.rs b/prisma-fmt/tests/code_actions/test_api.rs index ff874cf86997..b92f98ec856b 100644 --- a/prisma-fmt/tests/code_actions/test_api.rs +++ b/prisma-fmt/tests/code_actions/test_api.rs @@ -1,41 +1,53 @@ use lsp_types::{Diagnostic, DiagnosticSeverity}; use once_cell::sync::Lazy; use prisma_fmt::offset_to_position; -use psl::SourceFile; -use std::{fmt::Write as _, io::Write as _, sync::Arc}; +use psl::{diagnostics::Span, SourceFile}; +use std::{fmt::Write as _, io::Write as _, path::PathBuf}; + +use crate::helpers::load_schema_files; const SCENARIOS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/code_actions/scenarios"); +/** + * Code actions are requested only for single file. So, when emulating lsp request + * we need a way to designate that file somehow. + */ +const TARGET_SCHEMA_FILE: &str = "_target.prisma"; static UPDATE_EXPECT: Lazy = Lazy::new(|| std::env::var("UPDATE_EXPECT").is_ok()); -fn parse_schema_diagnostics(file: impl Into) -> Option> { - let schema = psl::validate(file.into()); +fn parse_schema_diagnostics(files: &[(String, String)], initiating_file_name: &str) -> Option> { + let schema = psl::validate_multi_file( + files + .iter() + .map(|(name, content)| (name.to_owned(), SourceFile::from(content))) + .collect(), + ); + let file_id = schema.db.file_id(initiating_file_name).unwrap(); + let source = schema.db.source(file_id); match (schema.diagnostics.warnings(), schema.diagnostics.errors()) { ([], []) => None, (warnings, errors) => { let mut diagnostics = Vec::new(); for warn in warnings.iter() { - diagnostics.push(Diagnostic { - severity: Some(DiagnosticSeverity::WARNING), - message: warn.message().to_owned(), - range: lsp_types::Range { - start: offset_to_position(warn.span().start, schema.db.source_assert_single()), - end: offset_to_position(warn.span().end, schema.db.source_assert_single()), - }, - ..Default::default() - }); + if warn.span().file_id == file_id { + diagnostics.push(create_diagnostic( + DiagnosticSeverity::WARNING, + warn.message(), + warn.span(), + source, + )); + } } for error in errors.iter() { - diagnostics.push(Diagnostic { - severity: Some(DiagnosticSeverity::ERROR), - message: error.message().to_owned(), - range: lsp_types::Range { - start: offset_to_position(error.span().start, schema.db.source_assert_single()), - end: offset_to_position(error.span().end, schema.db.source_assert_single()), - }, - ..Default::default() - }); + if error.span().file_id == file_id { + diagnostics.push(create_diagnostic( + DiagnosticSeverity::ERROR, + error.message(), + error.span(), + source, + )); + } } Some(diagnostics) @@ -43,17 +55,45 @@ fn parse_schema_diagnostics(file: impl Into) -> Option Diagnostic { + Diagnostic { + severity: Some(severity), + message: message.to_owned(), + range: lsp_types::Range { + start: offset_to_position(span.start, source), + end: offset_to_position(span.end, source), + }, + ..Default::default() + } +} + pub(crate) fn test_scenario(scenario_name: &str) { let mut path = String::with_capacity(SCENARIOS_PATH.len() + 12); - let schema = { - write!(path, "{SCENARIOS_PATH}/{scenario_name}/schema.prisma").unwrap(); - std::fs::read_to_string(&path).unwrap() + let schema_files = { + write!(path, "{SCENARIOS_PATH}/{scenario_name}").unwrap(); + load_schema_files(&path) }; - let source_file = psl::parser_database::SourceFile::new_allocated(Arc::from(schema.clone().into_boxed_str())); + let initiating_file_name = if schema_files.len() == 1 { + schema_files[0].0.as_str() + } else { + schema_files + .iter() + .find_map(|(file_path, _)| { + let path = PathBuf::from(file_path); + let file_name = path.file_name()?; + if file_name == TARGET_SCHEMA_FILE { + Some(file_path) + } else { + None + } + }) + .unwrap_or_else(|| panic!("Expected to have {TARGET_SCHEMA_FILE} in when multi-file schema are used")) + .as_str() + }; - let diagnostics = match parse_schema_diagnostics(source_file) { + let diagnostics = match parse_schema_diagnostics(&schema_files, initiating_file_name) { Some(diagnostics) => diagnostics, None => Vec::new(), }; @@ -64,7 +104,7 @@ pub(crate) fn test_scenario(scenario_name: &str) { let params = lsp_types::CodeActionParams { text_document: lsp_types::TextDocumentIdentifier { - uri: "file:/path/to/schema.prisma".parse().unwrap(), + uri: initiating_file_name.parse().unwrap(), }, range: lsp_types::Range::default(), context: lsp_types::CodeActionContext { @@ -77,7 +117,10 @@ pub(crate) fn test_scenario(scenario_name: &str) { }, }; - let result = prisma_fmt::code_actions(schema, &serde_json::to_string_pretty(¶ms).unwrap()); + let result = prisma_fmt::code_actions( + serde_json::to_string_pretty(&schema_files).unwrap(), + &serde_json::to_string_pretty(¶ms).unwrap(), + ); // Prettify the JSON let result = serde_json::to_string_pretty(&serde_json::from_str::>(&result).unwrap()) diff --git a/prisma-fmt/tests/code_actions/tests.rs b/prisma-fmt/tests/code_actions/tests.rs index 7bb3d1024ec4..00c65fd9003f 100644 --- a/prisma-fmt/tests/code_actions/tests.rs +++ b/prisma-fmt/tests/code_actions/tests.rs @@ -13,25 +13,37 @@ macro_rules! scenarios { scenarios! { one_to_many_referenced_side_misses_unique_single_field + one_to_many_referenced_side_misses_unique_single_field_multifile one_to_many_referenced_side_misses_unique_single_field_broken_relation one_to_many_referenced_side_misses_unique_compound_field + one_to_many_referenced_side_misses_unique_compound_field_multifile one_to_many_referenced_side_misses_unique_compound_field_existing_arguments one_to_many_referenced_side_misses_unique_compound_field_indentation_four_spaces one_to_many_referenced_side_misses_unique_compound_field_broken_relation one_to_one_referenced_side_misses_unique_single_field + one_to_one_referenced_side_misses_unique_single_field_multifile one_to_one_referenced_side_misses_unique_compound_field + one_to_one_referenced_side_misses_unique_compound_field_multifile one_to_one_referencing_side_misses_unique_single_field + one_to_one_referencing_side_misses_unique_single_field_multifile one_to_one_referencing_side_misses_unique_compound_field + one_to_one_referencing_side_misses_unique_compound_field_multifile one_to_one_referencing_side_misses_unique_compound_field_indentation_four_spaces relation_mode_prisma_missing_index + relation_mode_prisma_missing_index_multifile relation_mode_referential_integrity relation_mode_mysql_foreign_keys_set_default + relation_mode_mysql_foreign_keys_set_default_multifile multi_schema_one_model + multi_schema_one_model_multifile multi_schema_one_model_one_enum + multi_schema_one_model_one_enum_multifile multi_schema_two_models multi_schema_add_to_existing_schemas + multi_schema_add_to_existing_schemas_multifile multi_schema_add_to_nonexisting_schemas mongodb_at_map + mongodb_at_map_multifile mongodb_at_map_with_validation_errors mongodb_auto_native } diff --git a/prisma-fmt/tests/code_actions_tests.rs b/prisma-fmt/tests/code_actions_tests.rs index 1c460f038970..ce58517a78a0 100644 --- a/prisma-fmt/tests/code_actions_tests.rs +++ b/prisma-fmt/tests/code_actions_tests.rs @@ -1 +1,2 @@ mod code_actions; +mod helpers; diff --git a/prisma-fmt/tests/helpers.rs b/prisma-fmt/tests/helpers.rs new file mode 100644 index 000000000000..169c8202cb6e --- /dev/null +++ b/prisma-fmt/tests/helpers.rs @@ -0,0 +1,30 @@ +use std::path::Path; + +pub fn load_schema_files(dir: impl AsRef) -> Vec<(String, String)> { + let schema_files = { + std::fs::read_dir(dir.as_ref()) + .unwrap() + .map(Result::unwrap) + .filter_map(|entry| { + let ft = entry.file_type().ok()?; + if ft.is_dir() { + return None; + } + let path = entry.path(); + let name = path.file_name()?.to_str()?; + let ext = path.extension()?; + if ext != "prisma" { + return None; + } + + Some(( + format!("file:///path/to/{name}"), + std::fs::read_to_string(&path).unwrap(), + )) + }) + .collect::>() + }; + assert!(!schema_files.is_empty()); + + schema_files +} diff --git a/prisma-fmt/tests/regressions/language_tools_1466.rs b/prisma-fmt/tests/regressions/language_tools_1466.rs index adae2328c312..bdb86e7fa218 100644 --- a/prisma-fmt/tests/regressions/language_tools_1466.rs +++ b/prisma-fmt/tests/regressions/language_tools_1466.rs @@ -27,5 +27,8 @@ fn code_actions_should_not_crash_on_validation_errors_with_mongodb() { }, }; - prisma_fmt::code_actions(schema.to_owned(), &serde_json::to_string_pretty(¶ms).unwrap()); + prisma_fmt::code_actions( + serde_json::to_string_pretty(&[("schema.prisma", schema.to_owned())]).unwrap(), + &serde_json::to_string_pretty(¶ms).unwrap(), + ); } diff --git a/prisma-fmt/tests/regressions/language_tools_1473.rs b/prisma-fmt/tests/regressions/language_tools_1473.rs index 26ea341f482e..274e71873cf7 100644 --- a/prisma-fmt/tests/regressions/language_tools_1473.rs +++ b/prisma-fmt/tests/regressions/language_tools_1473.rs @@ -30,5 +30,8 @@ fn code_actions_should_not_crash_on_validation_errors_with_multi_schema() { }, }; - prisma_fmt::code_actions(schema.to_owned(), &serde_json::to_string_pretty(¶ms).unwrap()); + prisma_fmt::code_actions( + serde_json::to_string_pretty(&[("schema.prisma", schema.to_owned())]).unwrap(), + &serde_json::to_string_pretty(¶ms).unwrap(), + ); } diff --git a/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/Test.prisma b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/Test.prisma new file mode 100644 index 000000000000..084d8c807de0 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/Test.prisma @@ -0,0 +1,4 @@ +model Test { + id Int @id + name String @default(<|>) +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/TestB.prisma b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/TestB.prisma new file mode 100644 index 000000000000..624c63f2810d --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/TestB.prisma @@ -0,0 +1,4 @@ +model TestB { + id String @id + name Int +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/config.prisma b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/config.prisma new file mode 100644 index 000000000000..6292144fc10a --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/config.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "sqlserver" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/result.json b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/result.json new file mode 100644 index 000000000000..768f17216bad --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/default_map_mssql_multifile/result.json @@ -0,0 +1,9 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "map: ", + "kind": 10 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/A.prisma b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/A.prisma new file mode 100644 index 000000000000..e81f4b7c163f --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/A.prisma @@ -0,0 +1,6 @@ +model A { + id Int @id + val String + + @@index([val], type: <|>) +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/datasource.prisma b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/datasource.prisma new file mode 100644 index 000000000000..59db4986f239 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/datasource.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/generator.prisma b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/generator.prisma new file mode 100644 index 000000000000..d94508c43631 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/generator.prisma @@ -0,0 +1,3 @@ +generator js { + provider = "prisma-client-js" +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/result.json b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/result.json new file mode 100644 index 000000000000..4a09cced8e3a --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/extended_indexes_types_postgres_multifile/result.json @@ -0,0 +1,35 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "BTree", + "kind": 13, + "detail": "Can handle equality and range queries on data that can be sorted into some ordering (default)." + }, + { + "label": "Hash", + "kind": 13, + "detail": "Can handle simple equality queries, but no ordering. Faster than BTree, if ordering is not needed." + }, + { + "label": "Gist", + "kind": 13, + "detail": "Generalized Search Tree. A framework for building specialized indices for custom data types." + }, + { + "label": "Gin", + "kind": 13, + "detail": "Generalized Inverted Index. Useful for indexing composite items, such as arrays or text." + }, + { + "label": "SpGist", + "kind": 13, + "detail": "Space-partitioned Generalized Search Tree. For implenting a wide range of different non-balanced data structures." + }, + { + "label": "Brin", + "kind": 13, + "detail": "Block Range Index. If the data has some natural correlation with their physical location within the table, can compress very large amount of data into a small space." + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/Test.prisma b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/Test.prisma new file mode 100644 index 000000000000..084d8c807de0 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/Test.prisma @@ -0,0 +1,4 @@ +model Test { + id Int @id + name String @default(<|>) +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/TestB.prisma b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/TestB.prisma new file mode 100644 index 000000000000..624c63f2810d --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/TestB.prisma @@ -0,0 +1,4 @@ +model TestB { + id String @id + name Int +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/config.prisma b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/config.prisma new file mode 100644 index 000000000000..8f0b81f14329 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/config.prisma @@ -0,0 +1,4 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/result.json b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/result.json new file mode 100644 index 000000000000..cd41656e5303 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/no_default_map_on_postgres_multifile/result.json @@ -0,0 +1,4 @@ +{ + "isIncomplete": false, + "items": [] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/config.prisma b/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/config.prisma new file mode 100644 index 000000000000..0934a8bc3ee3 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/config.prisma @@ -0,0 +1,5 @@ +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + relationMode = "prisma" +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/models.prisma b/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/models.prisma new file mode 100644 index 000000000000..7f44e6c8f6b1 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/models.prisma @@ -0,0 +1,11 @@ +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id], onDelete: <|>) + authorId Int +} + +model User { + id Int @id @default(autoincrement()) + posts Post[] +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/result.json b/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/result.json new file mode 100644 index 000000000000..f55d95e46625 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/referential_actions_multifile/result.json @@ -0,0 +1,25 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "Cascade", + "kind": 13, + "detail": "Delete the child records when the parent record is deleted." + }, + { + "label": "Restrict", + "kind": 13, + "detail": "Prevent deleting a parent record as long as it is referenced." + }, + { + "label": "NoAction", + "kind": 13, + "detail": "Prevent deleting a parent record as long as it is referenced." + }, + { + "label": "SetNull", + "kind": 13, + "detail": "Set the referencing fields to NULL when the referenced record is deleted." + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/test_api.rs b/prisma-fmt/tests/text_document_completion/test_api.rs index 2284b179269f..122196157e6f 100644 --- a/prisma-fmt/tests/text_document_completion/test_api.rs +++ b/prisma-fmt/tests/text_document_completion/test_api.rs @@ -1,3 +1,4 @@ +use crate::helpers::load_schema_files; use once_cell::sync::Lazy; use std::{fmt::Write as _, io::Write as _}; @@ -8,20 +9,20 @@ static UPDATE_EXPECT: Lazy = Lazy::new(|| std::env::var("UPDATE_EXPECT").i pub(crate) fn test_scenario(scenario_name: &str) { let mut path = String::with_capacity(SCENARIOS_PATH.len() + 12); - let schema = { - write!(path, "{SCENARIOS_PATH}/{scenario_name}/schema.prisma").unwrap(); - std::fs::read_to_string(&path).unwrap() + let schema_files = { + write!(path, "{SCENARIOS_PATH}/{scenario_name}").unwrap(); + load_schema_files(&path) }; path.clear(); write!(path, "{SCENARIOS_PATH}/{scenario_name}/result.json").unwrap(); let expected_result = std::fs::read_to_string(&path).unwrap_or_else(|_| String::new()); - let (cursor_position, schema) = take_cursor(&schema); + let (initiating_file_uri, cursor_position, schema_files) = take_cursor(schema_files); let params = lsp_types::CompletionParams { text_document_position: lsp_types::TextDocumentPositionParams { text_document: lsp_types::TextDocumentIdentifier { - uri: "https://example.com/meow".parse().unwrap(), + uri: initiating_file_uri.parse().unwrap(), }, // ignored position: cursor_position, }, @@ -32,7 +33,10 @@ pub(crate) fn test_scenario(scenario_name: &str) { context: None, }; - let result = prisma_fmt::text_document_completion(schema, &serde_json::to_string_pretty(¶ms).unwrap()); + let result = prisma_fmt::text_document_completion( + serde_json::to_string_pretty(&schema_files).unwrap(), + &serde_json::to_string_pretty(¶ms).unwrap(), + ); // Prettify the JSON let result = serde_json::to_string_pretty(&serde_json::from_str::(&result).unwrap()).unwrap(); @@ -73,7 +77,24 @@ fn format_chunks(chunks: Vec) -> String { buf } -fn take_cursor(schema: &str) -> (lsp_types::Position, String) { +fn take_cursor(schema_files: Vec<(String, String)>) -> (String, lsp_types::Position, Vec<(String, String)>) { + let mut result = Vec::with_capacity(schema_files.len()); + let mut file_and_pos = None; + for (file_name, content) in schema_files { + if let Some((pos, without_cursor)) = take_cursor_one(&content) { + file_and_pos = Some((file_name.clone(), pos)); + result.push((file_name, without_cursor)); + } else { + result.push((file_name, content)); + } + } + + let (file_name, position) = file_and_pos.expect("Could not find a cursor in any of the schema files"); + + (file_name, position, result) +} + +fn take_cursor_one(schema: &str) -> Option<(lsp_types::Position, String)> { let mut schema_without_cursor = String::with_capacity(schema.len() - 3); let mut cursor_position = lsp_types::Position { character: 0, line: 0 }; let mut cursor_found = false; @@ -96,11 +117,13 @@ fn take_cursor(schema: &str) -> (lsp_types::Position, String) { } } - assert!(cursor_found); + if !cursor_found { + return None; + } // remove extra newline schema_without_cursor.truncate(schema_without_cursor.len() - 1); - (cursor_position, schema_without_cursor) + Some((cursor_position, schema_without_cursor)) } #[test] @@ -116,7 +139,7 @@ fn take_cursor_works() { } "#; - let (pos, schema) = take_cursor(schema); + let (pos, schema) = take_cursor_one(schema).unwrap(); assert_eq!(pos.line, 2); assert_eq!(pos.character, 28); assert_eq!(schema, expected_schema); diff --git a/prisma-fmt/tests/text_document_completion/tests.rs b/prisma-fmt/tests/text_document_completion/tests.rs index d7757b13e63e..c1332e6f9d41 100644 --- a/prisma-fmt/tests/text_document_completion/tests.rs +++ b/prisma-fmt/tests/text_document_completion/tests.rs @@ -15,9 +15,11 @@ scenarios! { argument_after_trailing_comma default_map_end_of_args_list default_map_mssql + default_map_mssql_multifile empty_schema extended_indexes_basic extended_indexes_types_postgres + extended_indexes_types_postgres_multifile extended_indexes_types_mysql extended_indexes_types_sqlserver extended_indexes_types_sqlite @@ -30,6 +32,8 @@ scenarios! { extended_indexes_operators_cockroach_gin language_tools_relation_directive no_default_map_on_postgres + no_default_map_on_postgres_multifile + referential_actions_multifile referential_actions_end_of_args_list referential_actions_in_progress referential_actions_in_progress_2 diff --git a/prisma-fmt/tests/text_document_completion_tests.rs b/prisma-fmt/tests/text_document_completion_tests.rs index 644e44d4e912..0a077044e6b2 100644 --- a/prisma-fmt/tests/text_document_completion_tests.rs +++ b/prisma-fmt/tests/text_document_completion_tests.rs @@ -1 +1,2 @@ +mod helpers; mod text_document_completion; diff --git a/prisma-schema-wasm/src/lib.rs b/prisma-schema-wasm/src/lib.rs index 073f13377243..4e2797571d4d 100644 --- a/prisma-schema-wasm/src/lib.rs +++ b/prisma-schema-wasm/src/lib.rs @@ -87,9 +87,9 @@ pub fn preview_features() -> String { /// Input and output are both JSON, the request being a `CompletionParams` object and the response /// being a `CompletionList` object. #[wasm_bindgen] -pub fn text_document_completion(schema: String, params: String) -> String { +pub fn text_document_completion(schema_files: String, params: String) -> String { register_panic_hook(); - prisma_fmt::text_document_completion(schema, ¶ms) + prisma_fmt::text_document_completion(schema_files, ¶ms) } /// This API is modelled on an LSP [code action diff --git a/psl/diagnostics/src/span.rs b/psl/diagnostics/src/span.rs index 42110aa29792..8b476e118303 100644 --- a/psl/diagnostics/src/span.rs +++ b/psl/diagnostics/src/span.rs @@ -38,7 +38,7 @@ impl Span { /// Is the given span overlapping with the current span. pub fn overlaps(self, other: Span) -> bool { - self.contains(other.start) || self.contains(other.end) + self.file_id == other.file_id && (self.contains(other.start) || self.contains(other.end)) } } diff --git a/psl/parser-database/src/files.rs b/psl/parser-database/src/files.rs index 685ae5f322c8..b43154927c3c 100644 --- a/psl/parser-database/src/files.rs +++ b/psl/parser-database/src/files.rs @@ -11,14 +11,14 @@ pub struct Files(pub Vec<(String, schema_ast::SourceFile, ast::SchemaAst)>); impl Files { /// Create a new Files instance from multiple files. - pub fn new(files: Vec<(String, schema_ast::SourceFile)>, diagnostics: &mut Diagnostics) -> Self { + pub fn new(files: &[(String, schema_ast::SourceFile)], diagnostics: &mut Diagnostics) -> Self { let asts = files - .into_iter() + .iter() .enumerate() .map(|(file_idx, (path, source))| { let id = FileId(file_idx as u32); let ast = schema_ast::parse_schema(source.as_str(), diagnostics, id); - (path, source, ast) + (path.to_owned(), source.clone(), ast) }) .collect(); Self(asts) diff --git a/psl/parser-database/src/lib.rs b/psl/parser-database/src/lib.rs index 56629ae6b2be..4f6c81257740 100644 --- a/psl/parser-database/src/lib.rs +++ b/psl/parser-database/src/lib.rs @@ -47,6 +47,7 @@ pub use ids::*; pub use names::is_reserved_type_name; use names::Names; pub use relations::{ManyToManyRelationId, ReferentialAction, RelationId}; +use schema_ast::ast::SourceConfig; pub use schema_ast::{ast, SourceFile}; pub use types::{ IndexAlgorithm, IndexFieldPath, IndexType, OperatorClass, RelationFieldId, ScalarFieldId, ScalarFieldType, @@ -83,11 +84,11 @@ pub struct ParserDatabase { impl ParserDatabase { /// See the docs on [ParserDatabase](/struct.ParserDatabase.html). pub fn new_single_file(file: SourceFile, diagnostics: &mut Diagnostics) -> Self { - Self::new(vec![("schema.prisma".to_owned(), file)], diagnostics) + Self::new(&[("schema.prisma".to_owned(), file)], diagnostics) } /// See the docs on [ParserDatabase](/struct.ParserDatabase.html). - pub fn new(schemas: Vec<(String, schema_ast::SourceFile)>, diagnostics: &mut Diagnostics) -> Self { + pub fn new(schemas: &[(String, schema_ast::SourceFile)], diagnostics: &mut Diagnostics) -> Self { let asts = Files::new(schemas, diagnostics); let mut interner = Default::default(); @@ -172,6 +173,13 @@ impl ParserDatabase { self.asts.iter().map(|(_, _, _, ast)| ast) } + /// Returns file id by name + pub fn file_id(&self, file_name: &str) -> Option { + self.asts + .iter() + .find_map(|(file_id, name, _, _)| if name == file_name { Some(file_id) } else { None }) + } + /// Iterate all parsed ASTs, consuming parser database pub fn into_iter_asts(self) -> impl Iterator { self.asts.into_iter().map(|(_, _, _, ast)| ast) @@ -219,6 +227,11 @@ impl ParserDatabase { pub fn file_name(&self, file_id: FileId) -> &str { self.asts[file_id].0.as_str() } + + /// Iterate all datasources defined in the schema + pub fn datasources(&self) -> impl Iterator { + self.iter_asts().flat_map(|ast| ast.sources()) + } } impl std::ops::Index for ParserDatabase { diff --git a/psl/parser-database/src/walkers.rs b/psl/parser-database/src/walkers.rs index abfe290b5bd6..0fc402a15341 100644 --- a/psl/parser-database/src/walkers.rs +++ b/psl/parser-database/src/walkers.rs @@ -24,6 +24,7 @@ pub use r#enum::*; pub use relation::*; pub use relation_field::*; pub use scalar_field::*; +use schema_ast::ast::WithSpan; use crate::{ast, FileId}; @@ -90,6 +91,12 @@ impl crate::ParserDatabase { .map(move |enum_id| self.walk(enum_id)) } + /// walk all enums in specified file + pub fn walk_enums_in_file(&self, file_id: FileId) -> impl Iterator> { + self.walk_enums() + .filter(move |walker| walker.ast_enum().span().file_id == file_id) + } + /// Walk all the models in the schema. pub fn walk_models(&self) -> impl Iterator> + '_ { self.iter_tops() @@ -98,6 +105,12 @@ impl crate::ParserDatabase { .filter(|m| !m.ast_model().is_view()) } + /// walk all models in specified file + pub fn walk_models_in_file(&self, file_id: FileId) -> impl Iterator> { + self.walk_models() + .filter(move |walker| walker.is_defined_in_file(file_id)) + } + /// Walk all the views in the schema. pub fn walk_views(&self) -> impl Iterator> + '_ { self.iter_tops() @@ -106,6 +119,12 @@ impl crate::ParserDatabase { .filter(|m| m.ast_model().is_view()) } + /// walk all views in specified file + pub fn walk_views_in_file(&self, file_id: FileId) -> impl Iterator> { + self.walk_views() + .filter(move |walker| walker.is_defined_in_file(file_id)) + } + /// Walk all the composite types in the schema. pub fn walk_composite_types(&self) -> impl Iterator> + '_ { self.iter_tops() diff --git a/psl/parser-database/src/walkers/model.rs b/psl/parser-database/src/walkers/model.rs index e4290a1a00f7..262e25b0b187 100644 --- a/psl/parser-database/src/walkers/model.rs +++ b/psl/parser-database/src/walkers/model.rs @@ -60,6 +60,11 @@ impl<'db> ModelWalker<'db> { .is_some() } + /// Is the model defined in a specific file? + pub fn is_defined_in_file(self, file_id: FileId) -> bool { + return self.ast_model().span().file_id == file_id; + } + /// The AST node. pub fn ast_model(self) -> &'db ast::Model { &self.db.asts[self.id] diff --git a/psl/parser-database/src/walkers/relation/inline/complete.rs b/psl/parser-database/src/walkers/relation/inline/complete.rs index 3f7b1b67dc60..2fab404fe7a5 100644 --- a/psl/parser-database/src/walkers/relation/inline/complete.rs +++ b/psl/parser-database/src/walkers/relation/inline/complete.rs @@ -11,7 +11,8 @@ use schema_ast::ast; pub struct CompleteInlineRelationWalker<'db> { pub(crate) side_a: RelationFieldId, pub(crate) side_b: RelationFieldId, - pub(crate) db: &'db ParserDatabase, + /// The parser database being traversed. + pub db: &'db ParserDatabase, } #[allow(missing_docs)] diff --git a/psl/psl-core/src/configuration/datasource.rs b/psl/psl-core/src/configuration/datasource.rs index 2f81674ce745..8fdc4e5be885 100644 --- a/psl/psl-core/src/configuration/datasource.rs +++ b/psl/psl-core/src/configuration/datasource.rs @@ -9,6 +9,8 @@ use std::{any::Any, borrow::Cow, path::Path}; /// a `datasource` from the prisma schema. pub struct Datasource { pub name: String, + /// Span of the whole datasource block (including `datasource` keyword and braces) + pub span: Span, /// The provider string pub provider: String, /// The provider that was selected as active from all specified providers diff --git a/psl/psl-core/src/lib.rs b/psl/psl-core/src/lib.rs index 21abf8481686..ccf66ba32ffe 100644 --- a/psl/psl-core/src/lib.rs +++ b/psl/psl-core/src/lib.rs @@ -84,7 +84,7 @@ pub fn validate_multi_file(files: Vec<(String, SourceFile)>, connectors: Connect "psl::validate_multi_file() must be called with at least one file" ); let mut diagnostics = Diagnostics::new(); - let db = ParserDatabase::new(files, &mut diagnostics); + let db = ParserDatabase::new(&files, &mut diagnostics); // TODO: the bulk of configuration block analysis should be part of ParserDatabase::new(). let mut configuration = Configuration::default(); @@ -139,7 +139,7 @@ pub fn parse_configuration( } pub fn parse_configuration_multi_file( - files: Vec<(String, SourceFile)>, + files: &[(String, SourceFile)], connectors: ConnectorRegistry<'_>, ) -> Result<(Files, Configuration), (Files, diagnostics::Diagnostics)> { let mut diagnostics = Diagnostics::default(); diff --git a/psl/psl-core/src/reformat.rs b/psl/psl-core/src/reformat.rs index a18b32e301b2..c9e2bfafc49e 100644 --- a/psl/psl-core/src/reformat.rs +++ b/psl/psl-core/src/reformat.rs @@ -26,7 +26,7 @@ pub fn reformat_validated_schema_into_single(schema: ValidatedSchema, indent_wid pub fn reformat_multiple(sources: Vec<(String, SourceFile)>, indent_width: usize) -> Vec<(String, String)> { let mut diagnostics = diagnostics::Diagnostics::new(); - let db = parser_database::ParserDatabase::new(sources, &mut diagnostics); + let db = parser_database::ParserDatabase::new(&sources, &mut diagnostics); if diagnostics.has_errors() { db.iter_file_ids() diff --git a/psl/psl-core/src/validate/datasource_loader.rs b/psl/psl-core/src/validate/datasource_loader.rs index dcdf49cb9d05..9f95c04230ae 100644 --- a/psl/psl-core/src/validate/datasource_loader.rs +++ b/psl/psl-core/src/validate/datasource_loader.rs @@ -10,6 +10,7 @@ use parser_database::{ ast::{Expression, WithDocumentation}, coerce, coerce_array, coerce_opt, }; +use schema_ast::ast::WithSpan; use std::{borrow::Cow, collections::HashMap}; const PREVIEW_FEATURES_KEY: &str = "previewFeatures"; @@ -219,6 +220,7 @@ fn lift_datasource( Some(Datasource { namespaces: schemas.into_iter().map(|(s, span)| (s.to_owned(), span)).collect(), + span: ast_source.span(), schemas_span, name: source_name.to_owned(), provider: provider.to_owned(), diff --git a/psl/psl/src/lib.rs b/psl/psl/src/lib.rs index 7bb0d521ccb6..9cbbc1bcc05a 100644 --- a/psl/psl/src/lib.rs +++ b/psl/psl/src/lib.rs @@ -44,7 +44,7 @@ pub fn parse_configuration(schema: &str) -> Result { /// Parses and validates Prisma schemas, but skip analyzing everything except datasource and generator /// blocks. pub fn parse_configuration_multi_file( - files: Vec<(String, SourceFile)>, + files: &[(String, SourceFile)], ) -> Result<(Files, Configuration), (Files, Diagnostics)> { psl_core::parse_configuration_multi_file(files, builtin_connectors::BUILTIN_CONNECTORS) }