diff --git a/crates/pgt_completions/src/relevance/filtering.rs b/crates/pgt_completions/src/relevance/filtering.rs index 6b263e889..c11390bd1 100644 --- a/crates/pgt_completions/src/relevance/filtering.rs +++ b/crates/pgt_completions/src/relevance/filtering.rs @@ -163,7 +163,7 @@ impl CompletionFilter<'_> { // only autocomplete left side of binary expression WrappingClause::Where => { ctx.before_cursor_matches_kind(&["keyword_and", "keyword_where"]) - || (ctx.before_cursor_matches_kind(&["."]) + || (ctx.before_cursor_matches_kind(&["field_qualifier"]) && ctx.matches_ancestor_history(&["field"])) } diff --git a/crates/pgt_hover/src/hoverables/column.rs b/crates/pgt_hover/src/hoverables/column.rs index 913aae86e..43e407163 100644 --- a/crates/pgt_hover/src/hoverables/column.rs +++ b/crates/pgt_hover/src/hoverables/column.rs @@ -1,12 +1,16 @@ use std::fmt::Write; -use pgt_schema_cache::Column; +use pgt_schema_cache::{Column, SchemaCache}; use pgt_treesitter::TreesitterContext; use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; impl ToHoverMarkdown for pgt_schema_cache::Column { - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + fn hover_headline( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { write!( writer, "`{}.{}.{}`", @@ -14,7 +18,11 @@ impl ToHoverMarkdown for pgt_schema_cache::Column { ) } - fn hover_body(&self, writer: &mut W) -> Result { + fn hover_body( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { if let Some(comment) = &self.comment { write!(writer, "Comment: '{}'", comment)?; writeln!(writer)?; @@ -46,7 +54,11 @@ impl ToHoverMarkdown for pgt_schema_cache::Column { Ok(true) } - fn hover_footer(&self, writer: &mut W) -> Result { + fn hover_footer( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { if let Some(default) = &self.default_expr { writeln!(writer)?; write!(writer, "Default: {}", default)?; diff --git a/crates/pgt_hover/src/hoverables/function.rs b/crates/pgt_hover/src/hoverables/function.rs index 54bfcc75d..171c78f6e 100644 --- a/crates/pgt_hover/src/hoverables/function.rs +++ b/crates/pgt_hover/src/hoverables/function.rs @@ -1,6 +1,6 @@ use std::fmt::Write; -use pgt_schema_cache::Function; +use pgt_schema_cache::{Function, SchemaCache}; use pgt_treesitter::TreesitterContext; use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; @@ -10,7 +10,11 @@ impl ToHoverMarkdown for Function { "sql" } - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + fn hover_headline( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { write!(writer, "`{}.{}", self.schema, self.name)?; if let Some(args) = &self.argument_types { @@ -28,7 +32,11 @@ impl ToHoverMarkdown for Function { Ok(()) } - fn hover_body(&self, writer: &mut W) -> Result { + fn hover_body( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { let kind_text = match self.kind { pgt_schema_cache::ProcKind::Function => "Function", pgt_schema_cache::ProcKind::Procedure => "Procedure", @@ -55,7 +63,11 @@ impl ToHoverMarkdown for Function { Ok(true) } - fn hover_footer(&self, writer: &mut W) -> Result { + fn hover_footer( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { if let Some(def) = self.definition.as_ref() { /* * We don't want to show 250 lines of functions to the user. diff --git a/crates/pgt_hover/src/hoverables/mod.rs b/crates/pgt_hover/src/hoverables/mod.rs index 675c1366a..ad8bc19aa 100644 --- a/crates/pgt_hover/src/hoverables/mod.rs +++ b/crates/pgt_hover/src/hoverables/mod.rs @@ -1,7 +1,10 @@ +use pgt_schema_cache::SchemaCache; + use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; mod column; mod function; +mod postgres_type; mod role; mod schema; mod table; @@ -16,6 +19,7 @@ pub enum Hoverable<'a> { Function(&'a pgt_schema_cache::Function), Role(&'a pgt_schema_cache::Role), Schema(&'a pgt_schema_cache::Schema), + PostgresType(&'a pgt_schema_cache::PostgresType), } impl<'a> From<&'a pgt_schema_cache::Schema> for Hoverable<'a> { @@ -48,6 +52,12 @@ impl<'a> From<&'a pgt_schema_cache::Role> for Hoverable<'a> { } } +impl<'a> From<&'a pgt_schema_cache::PostgresType> for Hoverable<'a> { + fn from(value: &'a pgt_schema_cache::PostgresType) -> Self { + Hoverable::PostgresType(value) + } +} + impl ContextualPriority for Hoverable<'_> { fn relevance_score(&self, ctx: &pgt_treesitter::TreesitterContext) -> f32 { match self { @@ -56,38 +66,76 @@ impl ContextualPriority for Hoverable<'_> { Hoverable::Function(function) => function.relevance_score(ctx), Hoverable::Role(role) => role.relevance_score(ctx), Hoverable::Schema(schema) => schema.relevance_score(ctx), + Hoverable::PostgresType(type_) => type_.relevance_score(ctx), } } } impl ToHoverMarkdown for Hoverable<'_> { - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + fn hover_headline( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { match self { - Hoverable::Table(table) => ToHoverMarkdown::hover_headline(*table, writer), - Hoverable::Column(column) => ToHoverMarkdown::hover_headline(*column, writer), - Hoverable::Function(function) => ToHoverMarkdown::hover_headline(*function, writer), - Hoverable::Role(role) => ToHoverMarkdown::hover_headline(*role, writer), - Hoverable::Schema(schema) => ToHoverMarkdown::hover_headline(*schema, writer), + Hoverable::Table(table) => { + ToHoverMarkdown::hover_headline(*table, writer, schema_cache) + } + Hoverable::Column(column) => { + ToHoverMarkdown::hover_headline(*column, writer, schema_cache) + } + Hoverable::Function(function) => { + ToHoverMarkdown::hover_headline(*function, writer, schema_cache) + } + Hoverable::Role(role) => ToHoverMarkdown::hover_headline(*role, writer, schema_cache), + Hoverable::Schema(schema) => { + ToHoverMarkdown::hover_headline(*schema, writer, schema_cache) + } + Hoverable::PostgresType(type_) => { + ToHoverMarkdown::hover_headline(*type_, writer, schema_cache) + } } } - fn hover_body(&self, writer: &mut W) -> Result { + fn hover_body( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result { match self { - Hoverable::Table(table) => ToHoverMarkdown::hover_body(*table, writer), - Hoverable::Column(column) => ToHoverMarkdown::hover_body(*column, writer), - Hoverable::Function(function) => ToHoverMarkdown::hover_body(*function, writer), - Hoverable::Role(role) => ToHoverMarkdown::hover_body(*role, writer), - Hoverable::Schema(schema) => ToHoverMarkdown::hover_body(*schema, writer), + Hoverable::Table(table) => ToHoverMarkdown::hover_body(*table, writer, schema_cache), + Hoverable::Column(column) => ToHoverMarkdown::hover_body(*column, writer, schema_cache), + Hoverable::Function(function) => { + ToHoverMarkdown::hover_body(*function, writer, schema_cache) + } + Hoverable::Role(role) => ToHoverMarkdown::hover_body(*role, writer, schema_cache), + Hoverable::Schema(schema) => ToHoverMarkdown::hover_body(*schema, writer, schema_cache), + Hoverable::PostgresType(type_) => { + ToHoverMarkdown::hover_body(*type_, writer, schema_cache) + } } } - fn hover_footer(&self, writer: &mut W) -> Result { + fn hover_footer( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result { match self { - Hoverable::Table(table) => ToHoverMarkdown::hover_footer(*table, writer), - Hoverable::Column(column) => ToHoverMarkdown::hover_footer(*column, writer), - Hoverable::Function(function) => ToHoverMarkdown::hover_footer(*function, writer), - Hoverable::Role(role) => ToHoverMarkdown::hover_footer(*role, writer), - Hoverable::Schema(schema) => ToHoverMarkdown::hover_footer(*schema, writer), + Hoverable::Table(table) => ToHoverMarkdown::hover_footer(*table, writer, schema_cache), + Hoverable::Column(column) => { + ToHoverMarkdown::hover_footer(*column, writer, schema_cache) + } + Hoverable::Function(function) => { + ToHoverMarkdown::hover_footer(*function, writer, schema_cache) + } + Hoverable::Role(role) => ToHoverMarkdown::hover_footer(*role, writer, schema_cache), + Hoverable::Schema(schema) => { + ToHoverMarkdown::hover_footer(*schema, writer, schema_cache) + } + Hoverable::PostgresType(type_) => { + ToHoverMarkdown::hover_footer(*type_, writer, schema_cache) + } } } @@ -98,6 +146,7 @@ impl ToHoverMarkdown for Hoverable<'_> { Hoverable::Function(function) => function.body_markdown_type(), Hoverable::Role(role) => role.body_markdown_type(), Hoverable::Schema(schema) => schema.body_markdown_type(), + Hoverable::PostgresType(type_) => type_.body_markdown_type(), } } @@ -108,6 +157,7 @@ impl ToHoverMarkdown for Hoverable<'_> { Hoverable::Function(function) => function.footer_markdown_type(), Hoverable::Role(role) => role.footer_markdown_type(), Hoverable::Schema(schema) => schema.footer_markdown_type(), + Hoverable::PostgresType(type_) => type_.footer_markdown_type(), } } } diff --git a/crates/pgt_hover/src/hoverables/postgres_type.rs b/crates/pgt_hover/src/hoverables/postgres_type.rs new file mode 100644 index 000000000..e9dced294 --- /dev/null +++ b/crates/pgt_hover/src/hoverables/postgres_type.rs @@ -0,0 +1,101 @@ +use std::fmt::Write; + +use pgt_schema_cache::{PostgresType, SchemaCache}; +use pgt_treesitter::TreesitterContext; + +use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; + +impl ToHoverMarkdown for PostgresType { + fn hover_headline( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { + write!(writer, "`{}.{}` (Custom Type)", self.schema, self.name)?; + Ok(()) + } + + fn hover_body( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result { + if let Some(comment) = &self.comment { + write!(writer, "Comment: '{}'", comment)?; + writeln!(writer)?; + writeln!(writer)?; + } + + if !self.attributes.attrs.is_empty() { + write!(writer, "Attributes:")?; + writeln!(writer)?; + + for attribute in &self.attributes.attrs { + write!(writer, "- {}", attribute.name)?; + + if let Some(type_info) = schema_cache.find_type_by_id(attribute.type_id) { + write!(writer, ": ")?; + + if type_info.schema != "pg_catalog" { + write!(writer, "{}.", type_info.schema)?; + } + + write!(writer, "{}", type_info.name)?; + } else { + write!(writer, " (type_id: {})", attribute.type_id)?; + } + + writeln!(writer)?; + } + + writeln!(writer)?; + } + + if !self.enums.values.is_empty() { + write!(writer, "Enum Permutations:")?; + writeln!(writer)?; + + for kind in &self.enums.values { + write!(writer, "- {}", kind)?; + writeln!(writer)?; + } + + writeln!(writer)?; + } + + Ok(true) + } + + fn hover_footer( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { + writeln!(writer)?; + Ok(true) + } +} + +impl ContextualPriority for PostgresType { + // there are no schemas with duplicate names. + fn relevance_score(&self, ctx: &TreesitterContext) -> f32 { + let mut score = 0.0; + + if ctx + .get_mentioned_relations(&Some(self.schema.clone())) + .is_some() + { + score += 100.0; + } + + if ctx.get_mentioned_relations(&None).is_some() && self.schema == "public" { + score += 100.0; + } + + if self.schema == "public" && score == 0.0 { + score += 10.0; + } + + score + } +} diff --git a/crates/pgt_hover/src/hoverables/role.rs b/crates/pgt_hover/src/hoverables/role.rs index 9908f08c3..d6b440382 100644 --- a/crates/pgt_hover/src/hoverables/role.rs +++ b/crates/pgt_hover/src/hoverables/role.rs @@ -1,18 +1,26 @@ use std::fmt::Write; -use pgt_schema_cache::Role; +use pgt_schema_cache::{Role, SchemaCache}; use pgt_treesitter::TreesitterContext; use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; impl ToHoverMarkdown for pgt_schema_cache::Role { - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + fn hover_headline( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { write!(writer, "`{}`", self.name)?; Ok(()) } - fn hover_body(&self, writer: &mut W) -> Result { + fn hover_body( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { if let Some(comm) = self.comment.as_ref() { write!(writer, "Comment: '{}'", comm)?; writeln!(writer)?; @@ -81,7 +89,11 @@ impl ToHoverMarkdown for pgt_schema_cache::Role { Ok(true) } - fn hover_footer(&self, _writer: &mut W) -> Result { + fn hover_footer( + &self, + _writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { Ok(false) } } diff --git a/crates/pgt_hover/src/hoverables/schema.rs b/crates/pgt_hover/src/hoverables/schema.rs index cb45a3c9b..90281371a 100644 --- a/crates/pgt_hover/src/hoverables/schema.rs +++ b/crates/pgt_hover/src/hoverables/schema.rs @@ -1,18 +1,26 @@ use std::fmt::Write; -use pgt_schema_cache::Schema; +use pgt_schema_cache::{Schema, SchemaCache}; use pgt_treesitter::TreesitterContext; use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; impl ToHoverMarkdown for Schema { - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + fn hover_headline( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { write!(writer, "`{}` - owned by {}", self.name, self.owner)?; Ok(()) } - fn hover_body(&self, writer: &mut W) -> Result { + fn hover_body( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { if let Some(comment) = &self.comment { write!(writer, "Comment: '{}'", comment)?; writeln!(writer)?; @@ -46,7 +54,11 @@ impl ToHoverMarkdown for Schema { Ok(true) } - fn hover_footer(&self, writer: &mut W) -> Result { + fn hover_footer( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { writeln!(writer)?; write!( writer, diff --git a/crates/pgt_hover/src/hoverables/table.rs b/crates/pgt_hover/src/hoverables/table.rs index 4c457c28d..a12132102 100644 --- a/crates/pgt_hover/src/hoverables/table.rs +++ b/crates/pgt_hover/src/hoverables/table.rs @@ -1,13 +1,17 @@ use std::fmt::Write; use humansize::DECIMAL; -use pgt_schema_cache::Table; +use pgt_schema_cache::{SchemaCache, Table}; use pgt_treesitter::TreesitterContext; use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown}; impl ToHoverMarkdown for Table { - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + fn hover_headline( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error> { write!(writer, "`{}.{}`", self.schema, self.name)?; let table_kind = match self.table_kind { @@ -30,7 +34,11 @@ impl ToHoverMarkdown for Table { Ok(()) } - fn hover_body(&self, writer: &mut W) -> Result { + fn hover_body( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { if let Some(comment) = &self.comment { write!(writer, "Comment: '{}'", comment)?; writeln!(writer)?; @@ -40,7 +48,11 @@ impl ToHoverMarkdown for Table { } } - fn hover_footer(&self, writer: &mut W) -> Result { + fn hover_footer( + &self, + writer: &mut W, + _schema_cache: &SchemaCache, + ) -> Result { writeln!(writer)?; write!( writer, diff --git a/crates/pgt_hover/src/hovered_node.rs b/crates/pgt_hover/src/hovered_node.rs index 7f1b54c88..b0d1cf0f3 100644 --- a/crates/pgt_hover/src/hovered_node.rs +++ b/crates/pgt_hover/src/hovered_node.rs @@ -18,6 +18,7 @@ pub(crate) enum HoveredNode { Policy(NodeIdentification), Trigger(NodeIdentification), Role(NodeIdentification), + PostgresType(NodeIdentification), } impl HoveredNode { @@ -78,6 +79,7 @@ impl HoveredNode { Some(HoveredNode::Column(NodeIdentification::Name(node_content))) } } + "identifier" if ctx.matches_ancestor_history(&["invocation", "object_reference"]) => { if let Some(schema) = ctx.schema_or_alias_name.as_ref() { Some(HoveredNode::Function(NodeIdentification::SchemaAndName(( @@ -90,10 +92,41 @@ impl HoveredNode { ))) } } + "identifier" if ctx.matches_one_of_ancestors(&["alter_role", "policy_to_role"]) => { Some(HoveredNode::Role(NodeIdentification::Name(node_content))) } + "identifier" + if ( + // hover over custom type in `create table` or `returns` + (ctx.matches_ancestor_history(&["type", "object_reference"]) + && ctx.node_under_cursor_is_within_field_name("custom_type")) + + // hover over type in `select` clause etc… + || (ctx + .matches_ancestor_history(&["field_qualifier", "object_reference"]) + && ctx.before_cursor_matches_kind(&["("]))) + + // make sure we're not checking against an alias + && ctx + .get_mentioned_table_for_alias( + node_content.replace(['(', ')'], "").as_str(), + ) + .is_none() => + { + let sanitized = node_content.replace(['(', ')'], ""); + if let Some(schema) = ctx.schema_or_alias_name.as_ref() { + Some(HoveredNode::PostgresType( + NodeIdentification::SchemaAndName((schema.clone(), sanitized)), + )) + } else { + Some(HoveredNode::PostgresType(NodeIdentification::Name( + sanitized, + ))) + } + } + "revoke_role" | "grant_role" | "policy_role" => { Some(HoveredNode::Role(NodeIdentification::Name(node_content))) } diff --git a/crates/pgt_hover/src/lib.rs b/crates/pgt_hover/src/lib.rs index b690d7f05..ff9aab023 100644 --- a/crates/pgt_hover/src/lib.rs +++ b/crates/pgt_hover/src/lib.rs @@ -117,12 +117,30 @@ pub fn on_hover(params: OnHoverParams) -> Vec { _ => vec![], }, + HoveredNode::PostgresType(node_identification) => match node_identification { + hovered_node::NodeIdentification::Name(type_name) => params + .schema_cache + .find_type(&type_name, None) + .map(Hoverable::from) + .map(|s| vec![s]) + .unwrap_or_default(), + + hovered_node::NodeIdentification::SchemaAndName((schema, type_name)) => params + .schema_cache + .find_type(&type_name, Some(schema.as_str())) + .map(Hoverable::from) + .map(|s| vec![s]) + .unwrap_or_default(), + + _ => vec![], + }, + _ => todo!(), }; prioritize_by_context(items, &ctx) .into_iter() - .map(|item| format_hover_markdown(&item)) + .map(|item| format_hover_markdown(&item, params.schema_cache)) .filter_map(Result::ok) .collect() } else { diff --git a/crates/pgt_hover/src/to_markdown.rs b/crates/pgt_hover/src/to_markdown.rs index 1cd45c6b5..ecc0f60f8 100644 --- a/crates/pgt_hover/src/to_markdown.rs +++ b/crates/pgt_hover/src/to_markdown.rs @@ -1,5 +1,7 @@ use std::fmt::Write; +use pgt_schema_cache::SchemaCache; + pub(crate) trait ToHoverMarkdown { fn body_markdown_type(&self) -> &'static str { "plain" @@ -9,25 +11,38 @@ pub(crate) trait ToHoverMarkdown { "plain" } - fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error>; + fn hover_headline( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result<(), std::fmt::Error>; - fn hover_body(&self, writer: &mut W) -> Result; // returns true if something was written + fn hover_body( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result; // returns true if something was written - fn hover_footer(&self, writer: &mut W) -> Result; // returns true if something was written + fn hover_footer( + &self, + writer: &mut W, + schema_cache: &SchemaCache, + ) -> Result; // returns true if something was written } pub(crate) fn format_hover_markdown( item: &T, + schema_cache: &SchemaCache, ) -> Result { let mut markdown = String::new(); write!(markdown, "### ")?; - item.hover_headline(&mut markdown)?; + item.hover_headline(&mut markdown, schema_cache)?; markdown_newline(&mut markdown)?; write!(markdown, "```{}", item.body_markdown_type())?; markdown_newline(&mut markdown)?; - item.hover_body(&mut markdown)?; + item.hover_body(&mut markdown, schema_cache)?; markdown_newline(&mut markdown)?; write!(markdown, "```")?; @@ -37,7 +52,7 @@ pub(crate) fn format_hover_markdown( write!(markdown, "```{}", item.footer_markdown_type())?; markdown_newline(&mut markdown)?; - item.hover_footer(&mut markdown)?; + item.hover_footer(&mut markdown, schema_cache)?; markdown_newline(&mut markdown)?; write!(markdown, "```")?; diff --git a/crates/pgt_hover/tests/hover_integration_tests.rs b/crates/pgt_hover/tests/hover_integration_tests.rs index e9c8f0c11..262e83c21 100644 --- a/crates/pgt_hover/tests/hover_integration_tests.rs +++ b/crates/pgt_hover/tests/hover_integration_tests.rs @@ -519,6 +519,48 @@ async fn test_grant_table_hover(test_db: PgPool) { test_hover_at_cursor("grant_select", query, None, &test_db).await; } +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn hover_on_composite_type(test_db: PgPool) { + let setup = r#"create type compfoo as (f1 int, f2 text);"#; + + let query = format!( + "create function getfoo() returns setof comp{}foo as $$ select fooid, fooname from foo $$ language sql;", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor( + "hover_custom_type_with_properties", + query, + Some(setup), + &test_db, + ) + .await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn hover_on_enum_type(test_db: PgPool) { + let setup = r#"create type compfoo as ENUM ('yes', 'no');"#; + + let query = format!( + "create function getfoo() returns setof comp{}foo as $$ select fooid, fooname from foo $$ language sql;", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("hover_custom_type_enum", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn hover_type_in_select_clause(test_db: PgPool) { + let setup = r#"create type compfoo as (f1 int, f2 text);"#; + + let query = format!( + "select (co{}mpfoo).f1 from some_table s;", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("hover_type_in_select_clause", query, Some(setup), &test_db).await; +} + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] async fn no_hover_results_over_params(test_db: PgPool) { let setup = r#" diff --git a/crates/pgt_hover/tests/snapshots/hover_custom_type_enum.snap b/crates/pgt_hover/tests/snapshots/hover_custom_type_enum.snap new file mode 100644 index 000000000..6f9e9ffae --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/hover_custom_type_enum.snap @@ -0,0 +1,24 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +create function getfoo() returns setof compfoo as $$ select fooid, fooname from foo $$ language sql; + ↑ hovered here +``` + +# Hover Results +### `public.compfoo` (Custom Type) +```plain +Enum Permutations: +- yes +- no + + +``` +--- +```plain + + +``` diff --git a/crates/pgt_hover/tests/snapshots/hover_custom_type_with_properties.snap b/crates/pgt_hover/tests/snapshots/hover_custom_type_with_properties.snap new file mode 100644 index 000000000..70c9b91f6 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/hover_custom_type_with_properties.snap @@ -0,0 +1,24 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +create function getfoo() returns setof compfoo as $$ select fooid, fooname from foo $$ language sql; + ↑ hovered here +``` + +# Hover Results +### `public.compfoo` (Custom Type) +```plain +Attributes: +- f1: int4 +- f2: text + + +``` +--- +```plain + + +``` diff --git a/crates/pgt_hover/tests/snapshots/hover_type_in_select_clause.snap b/crates/pgt_hover/tests/snapshots/hover_type_in_select_clause.snap new file mode 100644 index 000000000..9586dad4b --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/hover_type_in_select_clause.snap @@ -0,0 +1,24 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select (compfoo).f1 from some_table s; + ↑ hovered here +``` + +# Hover Results +### `public.compfoo` (Custom Type) +```plain +Attributes: +- f1: int4 +- f2: text + + +``` +--- +```plain + + +``` diff --git a/crates/pgt_schema_cache/src/schema_cache.rs b/crates/pgt_schema_cache/src/schema_cache.rs index fa87b2af2..c4d57b81d 100644 --- a/crates/pgt_schema_cache/src/schema_cache.rs +++ b/crates/pgt_schema_cache/src/schema_cache.rs @@ -98,6 +98,10 @@ impl SchemaCache { }) } + pub fn find_type_by_id(&self, id: i64) -> Option<&PostgresType> { + self.types.iter().find(|t| t.id == id) + } + pub fn find_cols(&self, name: &str, table: Option<&str>, schema: Option<&str>) -> Vec<&Column> { let sanitized_name = Self::sanitize_identifier(name); self.columns diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index e695351b4..9c404dadb 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -438,10 +438,13 @@ impl<'a> TreesitterContext<'a> { match current_node_kind { "object_reference" | "field" => { + let start = current_node.start_byte(); let content = self.get_ts_node_content(¤t_node); if let Some(txt) = content { let parts: Vec<&str> = txt.split('.').collect(); - if parts.len() == 2 { + // we do not want to set it if we're on the schema or alias node itself + let is_on_schema_node = start + parts[0].len() >= self.position; + if parts.len() == 2 && !is_on_schema_node { self.schema_or_alias_name = Some(parts[0].to_string()); } } @@ -828,6 +831,42 @@ impl<'a> TreesitterContext<'a> { .unwrap_or(0) } + /// Returns true if the node under the cursor matches the field_name OR has a parent that matches the field_name. + pub fn node_under_cursor_is_within_field_name(&self, name: &str) -> bool { + self.node_under_cursor + .as_ref() + .map(|n| match n { + NodeUnderCursor::TsNode(node) => { + // It might seem weird that we have to check for the field_name from the parent, + // but TreeSitter wants it this way, since nodes often can only be named in + // the context of their parents. + let root_node = self.tree.root_node(); + let mut cursor = node.walk(); + let mut parent = node.parent(); + + while let Some(p) = parent { + if p == root_node { + break; + } + + if p.children_by_field_name(name, &mut cursor).any(|c| { + let r = c.range(); + // if the parent range contains the node range, the node is of the field_name. + r.start_byte <= node.start_byte() && r.end_byte >= node.end_byte() + }) { + return true; + } else { + parent = p.parent(); + } + } + + false + } + NodeUnderCursor::CustomNode { .. } => false, + }) + .unwrap_or(false) + } + pub fn get_mentioned_relations(&self, key: &Option) -> Option<&HashSet> { if let Some(key) = key.as_ref() { let sanitized_key = key.replace('"', ""); @@ -1217,6 +1256,27 @@ mod tests { } } + #[test] + fn verifies_node_has_field_name() { + let query = format!( + r#"create table foo (id int not null, compfoo som{}e_type);"#, + QueryWithCursorPosition::cursor_marker() + ); + let (position, text) = QueryWithCursorPosition::from(query).get_text_and_position(); + + let tree = get_tree(text.as_str()); + + let params = TreeSitterContextParams { + position: (position as u32).into(), + text: &text, + tree: &tree, + }; + + let ctx = TreesitterContext::new(params); + + assert!(ctx.node_under_cursor_is_within_field_name("custom_type")); + } + #[test] fn does_not_overflow_callstack_on_smaller_treesitter_child() { let query = format!( diff --git a/crates/pgt_treesitter/src/queries/parameters.rs b/crates/pgt_treesitter/src/queries/parameters.rs index b64c73ae7..a137cc8de 100644 --- a/crates/pgt_treesitter/src/queries/parameters.rs +++ b/crates/pgt_treesitter/src/queries/parameters.rs @@ -10,10 +10,10 @@ static TS_QUERY: LazyLock = LazyLock::new(|| { static QUERY_STR: &str = r#" [ (field - (identifier)) @reference - (field - (object_reference) - "." (identifier)) @reference + (field_qualifier)? + (identifier) + ) @reference + (parameter) @parameter ] "#; diff --git a/crates/pgt_treesitter/src/queries/select_columns.rs b/crates/pgt_treesitter/src/queries/select_columns.rs index ea3eb9bd1..d8fa1d16a 100644 --- a/crates/pgt_treesitter/src/queries/select_columns.rs +++ b/crates/pgt_treesitter/src/queries/select_columns.rs @@ -10,8 +10,10 @@ static TS_QUERY: LazyLock = LazyLock::new(|| { (select_expression (term (field - (object_reference)? @alias - "."? + (field_qualifier + (object_reference) @alias + "." + )? (identifier) @column ) ) diff --git a/crates/pgt_treesitter/src/queries/where_columns.rs b/crates/pgt_treesitter/src/queries/where_columns.rs index 27b466eac..b3371518c 100644 --- a/crates/pgt_treesitter/src/queries/where_columns.rs +++ b/crates/pgt_treesitter/src/queries/where_columns.rs @@ -12,8 +12,10 @@ static TS_QUERY: LazyLock = LazyLock::new(|| { (binary_expression (binary_expression (field - (object_reference)? @alias - "."? + (field_qualifier + (object_reference) @alias + "." + )? (identifier) @column ) ) diff --git a/crates/pgt_treesitter_grammar/grammar.js b/crates/pgt_treesitter_grammar/grammar.js index 6fc29ac95..cf33f72bb 100644 --- a/crates/pgt_treesitter_grammar/grammar.js +++ b/crates/pgt_treesitter_grammar/grammar.js @@ -21,6 +21,7 @@ module.exports = grammar({ ], conflicts: ($) => [ + [$.all_fields, $.field_qualifier], [$.object_reference, $._qualified_field], [$.object_reference], [$.between_expression, $.binary_expression], @@ -484,7 +485,7 @@ module.exports = grammar({ keyword_array: (_) => make_keyword("array"), // not included in _type since it's a constructor literal - _type: ($) => + type: ($) => prec.left( seq( choice( @@ -857,7 +858,7 @@ module.exports = grammar({ seq( optional($._argmode), optional($.identifier), - $._type, + $.type, optional(seq(choice($.keyword_default, "="), $.literal)) ), @@ -1234,8 +1235,8 @@ module.exports = grammar({ $.function_arguments, $.keyword_returns, choice( - $._type, - seq($.keyword_setof, $._type), + $.type, + seq($.keyword_setof, $.type), seq($.keyword_table, $.column_definitions), $.keyword_trigger ), @@ -1275,7 +1276,7 @@ module.exports = grammar({ function_declaration: ($) => seq( $.identifier, - $._type, + $.type, optional( seq( ":=", @@ -1523,7 +1524,7 @@ module.exports = grammar({ $.object_reference, repeat( choice( - seq($.keyword_as, $._type), + seq($.keyword_as, $.type), seq( $.keyword_increment, optional($.keyword_by), @@ -1781,7 +1782,7 @@ module.exports = grammar({ seq( optional(seq($.keyword_set, $.keyword_data)), $.keyword_type, - field("type", $._type) + field("type", $.type) ), seq( $.keyword_set, @@ -1975,7 +1976,7 @@ module.exports = grammar({ choice( repeat1( choice( - seq($.keyword_as, $._type), + seq($.keyword_as, $.type), seq($.keyword_increment, optional($.keyword_by), $.literal), seq( $.keyword_minvalue, @@ -2057,7 +2058,7 @@ module.exports = grammar({ ), seq( choice( - seq($.keyword_add, $.keyword_attribute, $.identifier, $._type), + seq($.keyword_add, $.keyword_attribute, $.identifier, $.type), seq( $.keyword_drop, $.keyword_attribute, @@ -2070,7 +2071,7 @@ module.exports = grammar({ $.identifier, optional(seq($.keyword_set, $.keyword_data)), $.keyword_type, - $._type + $.type ) ), optional(seq($.keyword_collate, $.identifier)), @@ -2624,7 +2625,7 @@ module.exports = grammar({ column_definition: ($) => seq( field("name", $._column), - field("type", $._type), + field("type", $.type), repeat($._column_constraint) ), @@ -2805,12 +2806,12 @@ module.exports = grammar({ field: ($) => field("name", $.identifier), _qualified_field: ($) => - seq( - optional(seq(optional_parenthesis($.object_reference), ".")), - field("name", $.identifier) - ), + seq(optional($.field_qualifier), field("name", $.identifier)), + + field_qualifier: ($) => + seq(prec.left(optional_parenthesis($.object_reference)), "."), - implicit_cast: ($) => seq($._expression, "::", $._type), + implicit_cast: ($) => seq($._expression, "::", $.type), // Postgres syntax for intervals interval: ($) => seq($.keyword_interval, $._literal_string), @@ -2819,7 +2820,7 @@ module.exports = grammar({ seq( field("name", $.keyword_cast), wrapped_in_parenthesis( - seq(field("parameter", $._expression), $.keyword_as, $._type) + seq(field("parameter", $._expression), $.keyword_as, $.type) ) ), diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index 24ea5d503..eb681cc41 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -122,7 +122,7 @@ export type DiagnosticTags = DiagnosticTag[]; /** * Serializable representation of a [Diagnostic](super::Diagnostic) advice -See the [Visitor] trait for additional documentation on all the supported advice types. +See the [Visitor] trait for additional documentation on all the supported advice types. */ export type Advice = | { log: [LogCategory, MarkupBuf] } @@ -227,7 +227,7 @@ export interface CompletionItem { /** * The text that the editor should fill in. If `None`, the `label` should be used. Tables, for example, might have different completion_texts: -label: "users", description: "Schema: auth", completion_text: "auth.users". +label: "users", description: "Schema: auth", completion_text: "auth.users". */ export interface CompletionText { is_snippet: boolean; @@ -411,7 +411,7 @@ export interface PartialVcsConfiguration { /** * The folder where we should check for VCS files. By default, we will use the same folder where `postgres-language-server.jsonc` was found. -If we can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, we won't use the VCS integration, and a diagnostic will be emitted +If we can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, we won't use the VCS integration, and a diagnostic will be emitted */ root?: string; /**