diff --git a/Cargo.lock b/Cargo.lock index be7eacb75..672bef798 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2928,6 +2928,7 @@ name = "pgt_hover" version = "0.0.0" dependencies = [ "humansize", + "insta", "pgt_query", "pgt_schema_cache", "pgt_test_utils", diff --git a/crates/pgt_completions/src/relevance/filtering.rs b/crates/pgt_completions/src/relevance/filtering.rs index 18e3d7ce5..0514a485e 100644 --- a/crates/pgt_completions/src/relevance/filtering.rs +++ b/crates/pgt_completions/src/relevance/filtering.rs @@ -90,7 +90,7 @@ impl CompletionFilter<'_> { .is_none_or(|n| n != &WrappingNode::List) && (ctx.before_cursor_matches_kind(&["keyword_into"]) || (ctx.before_cursor_matches_kind(&["."]) - && ctx.parent_matches_one_of_kind(&["object_reference"]))) + && ctx.matches_ancestor_history(&["object_reference"]))) } WrappingClause::DropTable | WrappingClause::AlterTable => ctx @@ -136,7 +136,7 @@ impl CompletionFilter<'_> { WrappingClause::Where => { ctx.before_cursor_matches_kind(&["keyword_and", "keyword_where"]) || (ctx.before_cursor_matches_kind(&["."]) - && ctx.parent_matches_one_of_kind(&["field"])) + && ctx.matches_ancestor_history(&["field"])) } WrappingClause::PolicyCheck => { diff --git a/crates/pgt_hover/Cargo.toml b/crates/pgt_hover/Cargo.toml index 5b3a97140..8b791e31b 100644 --- a/crates/pgt_hover/Cargo.toml +++ b/crates/pgt_hover/Cargo.toml @@ -27,6 +27,7 @@ tree-sitter.workspace = true tree_sitter_sql.workspace = true [dev-dependencies] +insta = { version = "1.42.1" } pgt_test_utils.workspace = true [lib] diff --git a/crates/pgt_hover/src/contextual_priority.rs b/crates/pgt_hover/src/contextual_priority.rs index a23a1e114..994de58e1 100644 --- a/crates/pgt_hover/src/contextual_priority.rs +++ b/crates/pgt_hover/src/contextual_priority.rs @@ -1,4 +1,4 @@ -use pgt_schema_cache::{Column, Table}; +use pgt_schema_cache::{Column, Function, Table}; use pgt_treesitter::context::TreesitterContext; pub(crate) trait ContextualPriority { @@ -74,6 +74,34 @@ impl ContextualPriority for Table { } } +impl ContextualPriority for Function { + fn relevance_score(&self, _ctx: &TreesitterContext) -> f32 { + let mut score = 0.0; + + // built-in functions get higher priority + if self.language == "internal" { + score += 100.0; + } + + // public schema functions get base priority + if self.schema == "public" { + score += 50.0; + } else { + score += 25.0; + } + + // aggregate and window functions are commonly used + match self.kind { + pgt_schema_cache::ProcKind::Aggregate => score += 20.0, + pgt_schema_cache::ProcKind::Window => score += 15.0, + pgt_schema_cache::ProcKind::Function => score += 10.0, + pgt_schema_cache::ProcKind::Procedure => score += 5.0, + } + + score + } +} + /// Will first sort the items by a score and then filter out items with a score gap algorithm. /// /// `[200, 180, 150, 140]` => all items are returned diff --git a/crates/pgt_hover/src/hovered_item.rs b/crates/pgt_hover/src/hovered_item.rs index 3c7d4a3ff..3367ef9e1 100644 --- a/crates/pgt_hover/src/hovered_item.rs +++ b/crates/pgt_hover/src/hovered_item.rs @@ -5,6 +5,7 @@ use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdow pub(crate) enum HoverItem<'a> { Table(&'a pgt_schema_cache::Table), Column(&'a pgt_schema_cache::Column), + Function(&'a pgt_schema_cache::Function), } impl<'a> From<&'a pgt_schema_cache::Table> for HoverItem<'a> { @@ -19,11 +20,18 @@ impl<'a> From<&'a pgt_schema_cache::Column> for HoverItem<'a> { } } +impl<'a> From<&'a pgt_schema_cache::Function> for HoverItem<'a> { + fn from(value: &'a pgt_schema_cache::Function) -> Self { + HoverItem::Function(value) + } +} + impl ContextualPriority for HoverItem<'_> { fn relevance_score(&self, ctx: &pgt_treesitter::TreesitterContext) -> f32 { match self { HoverItem::Table(table) => table.relevance_score(ctx), HoverItem::Column(column) => column.relevance_score(ctx), + HoverItem::Function(function) => function.relevance_score(ctx), } } } @@ -33,6 +41,7 @@ impl ToHoverMarkdown for HoverItem<'_> { match self { HoverItem::Table(table) => ToHoverMarkdown::hover_headline(*table, writer), HoverItem::Column(column) => ToHoverMarkdown::hover_headline(*column, writer), + HoverItem::Function(function) => ToHoverMarkdown::hover_headline(*function, writer), } } @@ -40,6 +49,7 @@ impl ToHoverMarkdown for HoverItem<'_> { match self { HoverItem::Table(table) => ToHoverMarkdown::hover_body(*table, writer), HoverItem::Column(column) => ToHoverMarkdown::hover_body(*column, writer), + HoverItem::Function(function) => ToHoverMarkdown::hover_body(*function, writer), } } @@ -47,6 +57,7 @@ impl ToHoverMarkdown for HoverItem<'_> { match self { HoverItem::Table(table) => ToHoverMarkdown::hover_footer(*table, writer), HoverItem::Column(column) => ToHoverMarkdown::hover_footer(*column, writer), + HoverItem::Function(function) => ToHoverMarkdown::hover_footer(*function, writer), } } } diff --git a/crates/pgt_hover/src/hovered_node.rs b/crates/pgt_hover/src/hovered_node.rs index de5f7533f..1e586b43f 100644 --- a/crates/pgt_hover/src/hovered_node.rs +++ b/crates/pgt_hover/src/hovered_node.rs @@ -25,7 +25,7 @@ impl HoveredNode { let under_node = ctx.node_under_cursor.as_ref()?; match under_node.kind() { - "identifier" if ctx.parent_matches_one_of_kind(&["object_reference", "relation"]) => { + "identifier" if ctx.matches_ancestor_history(&["relation", "object_reference"]) => { if let Some(schema) = ctx.schema_or_alias_name.as_ref() { Some(HoveredNode::Table(NodeIdentification::SchemaAndName(( schema.clone(), @@ -35,7 +35,7 @@ impl HoveredNode { Some(HoveredNode::Table(NodeIdentification::Name(node_content))) } } - "identifier" if ctx.parent_matches_one_of_kind(&["field"]) => { + "identifier" if ctx.matches_ancestor_history(&["field"]) => { if let Some(table_or_alias) = ctx.schema_or_alias_name.as_ref() { Some(HoveredNode::Column(NodeIdentification::SchemaAndName(( table_or_alias.clone(), @@ -45,6 +45,18 @@ 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(( + schema.clone(), + node_content, + )))) + } else { + Some(HoveredNode::Function(NodeIdentification::Name( + node_content, + ))) + } + } _ => None, } } diff --git a/crates/pgt_hover/src/lib.rs b/crates/pgt_hover/src/lib.rs index 6daf77779..1c5ff4f70 100644 --- a/crates/pgt_hover/src/lib.rs +++ b/crates/pgt_hover/src/lib.rs @@ -56,9 +56,16 @@ pub fn on_hover(params: OnHoverParams) -> Vec { .collect(), hovered_node::NodeIdentification::SchemaAndName((table_or_alias, column_name)) => { + // resolve alias to actual table name if needed + let actual_table = ctx + .mentioned_table_aliases + .get(table_or_alias.as_str()) + .map(|s| s.as_str()) + .unwrap_or(table_or_alias.as_str()); + params .schema_cache - .find_cols(&column_name, Some(&table_or_alias), None) + .find_cols(&column_name, Some(actual_table), None) .into_iter() .map(HoverItem::from) .collect() @@ -67,6 +74,24 @@ pub fn on_hover(params: OnHoverParams) -> Vec { hovered_node::NodeIdentification::SchemaAndTableAndName(_) => vec![], }, + HoveredNode::Function(node_identification) => match node_identification { + hovered_node::NodeIdentification::Name(function_name) => params + .schema_cache + .find_functions(&function_name, None) + .into_iter() + .map(HoverItem::from) + .collect(), + + hovered_node::NodeIdentification::SchemaAndName((schema, function_name)) => params + .schema_cache + .find_functions(&function_name, Some(&schema)) + .into_iter() + .map(HoverItem::from) + .collect(), + + hovered_node::NodeIdentification::SchemaAndTableAndName(_) => vec![], + }, + _ => todo!(), }; diff --git a/crates/pgt_hover/src/to_markdown.rs b/crates/pgt_hover/src/to_markdown.rs index ea8ebea88..0c8c311ae 100644 --- a/crates/pgt_hover/src/to_markdown.rs +++ b/crates/pgt_hover/src/to_markdown.rs @@ -17,10 +17,12 @@ pub(crate) fn format_hover_markdown( item.hover_headline(&mut markdown)?; markdown_newline(&mut markdown)?; - if item.hover_body(&mut markdown)? { - markdown_newline(&mut markdown)?; - } + write!(markdown, "#### ")?; + item.hover_body(&mut markdown)?; + markdown_newline(&mut markdown)?; + write!(markdown, "--- ")?; + markdown_newline(&mut markdown)?; item.hover_footer(&mut markdown)?; Ok(markdown) @@ -28,6 +30,8 @@ pub(crate) fn format_hover_markdown( impl ToHoverMarkdown for pgt_schema_cache::Table { fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + write!(writer, "`{}.{}`", self.schema, self.name)?; + let table_kind = match self.table_kind { pgt_schema_cache::TableKind::View => " (View)", pgt_schema_cache::TableKind::MaterializedView => " (M.View)", @@ -35,17 +39,17 @@ impl ToHoverMarkdown for pgt_schema_cache::Table { pgt_schema_cache::TableKind::Ordinary => "", }; + write!(writer, "{}", table_kind)?; + let locked_txt = if self.rls_enabled { " - πŸ”’ RLS enabled" } else { " - πŸ”“ RLS disabled" }; - write!( - writer, - "{}.{}{}{}", - self.schema, self.name, table_kind, locked_txt - ) + write!(writer, "{}", locked_txt)?; + + Ok(()) } fn hover_body(&self, writer: &mut W) -> Result { @@ -73,7 +77,7 @@ impl ToHoverMarkdown for pgt_schema_cache::Column { fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { write!( writer, - "{}.{}.{}", + "`{}.{}.{}`", self.schema_name, self.table_name, self.name ) } @@ -118,6 +122,100 @@ impl ToHoverMarkdown for pgt_schema_cache::Column { } } +impl ToHoverMarkdown for pgt_schema_cache::Function { + fn hover_headline(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + write!(writer, "`{}.{}", self.schema, self.name)?; + + if let Some(args) = &self.argument_types { + write!(writer, "({})", args)?; + } else { + write!(writer, "()")?; + } + + write!( + writer, + " β†’ {}`", + self.return_type.as_ref().unwrap_or(&"void".to_string()) + )?; + + Ok(()) + } + + fn hover_body(&self, writer: &mut W) -> Result { + let kind_text = match self.kind { + pgt_schema_cache::ProcKind::Function => "Function", + pgt_schema_cache::ProcKind::Procedure => "Procedure", + pgt_schema_cache::ProcKind::Aggregate => "Aggregate", + pgt_schema_cache::ProcKind::Window => "Window", + }; + + write!(writer, "{}", kind_text)?; + + let behavior_text = match self.behavior { + pgt_schema_cache::Behavior::Immutable => " - Immutable", + pgt_schema_cache::Behavior::Stable => " - Stable", + pgt_schema_cache::Behavior::Volatile => "", + }; + + write!(writer, "{}", behavior_text)?; + + if self.security_definer { + write!(writer, " - Security DEFINER")?; + } else { + write!(writer, " - Security INVOKER")?; + } + + Ok(true) + } + + fn hover_footer(&self, writer: &mut W) -> Result { + if let Some(def) = self.definition.as_ref() { + /* + * We don't want to show 250 lines of functions to the user. + * If we have more than 30 lines, we'll only show the signature. + */ + if def.lines().count()> 30 { + let without_boilerplate: String = def + .split_ascii_whitespace() + .skip_while(|elem| { + ["create", "or", "replace", "function"] + .contains(&elem.to_ascii_lowercase().as_str()) + }) + .collect::>() + .join(" "); + + for char in without_boilerplate.chars() { + match char { + '(' => { + write!(writer, "(\n ")?; + } + + ')' => { + write!(writer, "\n)\n")?; + break; + } + + ',' => { + // one space already present + write!(writer, ",\n ")?; + } + + _ => { + write!(writer, "{}", char)?; + } + } + } + } else { + write!(writer, "```\n{}\n```", def)?; + } + + Ok(true) + } else { + Ok(false) + } + } +} + fn markdown_newline(writer: &mut W) -> Result<(), std::fmt::Error> { write!(writer, " ")?; writeln!(writer)?; diff --git a/crates/pgt_hover/tests/hover_integration_tests.rs b/crates/pgt_hover/tests/hover_integration_tests.rs new file mode 100644 index 000000000..1ec9fd747 --- /dev/null +++ b/crates/pgt_hover/tests/hover_integration_tests.rs @@ -0,0 +1,280 @@ +use pgt_hover::{OnHoverParams, on_hover}; +use pgt_schema_cache::SchemaCache; +use pgt_test_utils::QueryWithCursorPosition; +use pgt_text_size::TextSize; +use sqlx::{Executor, PgPool}; + +async fn test_hover_at_cursor(name: &str, query: String, setup: Option<&str>, test_db: &PgPool) { + if let Some(setup) = setup { + test_db + .execute(setup) + .await + .expect("Failed to setup test database"); + } + + let schema_cache = SchemaCache::load(test_db) + .await + .expect("Failed to load Schema Cache"); + + let (position, sql) = QueryWithCursorPosition::from(query).get_text_and_position(); + + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(tree_sitter_sql::language()) + .expect("Error loading sql language"); + + let tree = parser.parse(&sql, None).unwrap(); + let ast = pgt_query::parse(&sql) + .ok() + .map(|parsed| parsed.into_root().unwrap()); + + let hover_results = on_hover(OnHoverParams { + position: TextSize::new(position as u32), + schema_cache: &schema_cache, + stmt_sql: &sql, + ast: ast.as_ref(), + ts_tree: &tree, + }); + + let mut snapshot = String::new(); + snapshot.push_str("# Input\n"); + snapshot.push_str("```sql\n"); + snapshot.push_str(&sql); + snapshot.push('\n'); + + for _ in 0..position { + snapshot.push(' '); + } + snapshot.push_str("↑ hovered here\n"); + + snapshot.push_str("```\n\n"); + + if hover_results.is_empty() { + snapshot.push_str("# Hover Results\n"); + snapshot.push_str("No hover information found.\n"); + } else { + snapshot.push_str("# Hover Results\n"); + for (i, result) in hover_results.iter().enumerate() { + if i> 0 { + snapshot.push_str("\n---\n\n"); + } + snapshot.push_str(result); + snapshot.push('\n'); + } + } + + insta::with_settings!({ + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(name, snapshot); + }); +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_function_hover_builtin_count(test_db: PgPool) { + let query = format!( + "select cou{}nt(*) from users", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("builtin_count", query, None, &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_function_hover_builtin_now(test_db: PgPool) { + let query = format!( + "select n{}ow() from users", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("builtin_now", query, None, &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_function_hover_builtin_max(test_db: PgPool) { + let query = format!( + "select m{}ax(id) from users", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("builtin_max", query, None, &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_function_hover_custom_function(test_db: PgPool) { + let setup = r#" + create or replace function custom_add(a integer, b integer) + returns integer + language plpgsql + immutable + as $$ + begin + return a + b; + end; + $$; + "#; + + let query = format!( + "select custom_a{}dd(1, 2)", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("custom_function", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_function_hover_with_schema(test_db: PgPool) { + let setup = r#" + create schema test_schema; + create or replace function test_schema.schema_func(text) + returns text + language sql + stable + as $$ + select 1ドル || ' processed'; + $$; + "#; + + let query = format!( + "select test_schema.schema_f{}unc('test')", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("function_with_schema", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_column_hover_with_table_ref(test_db: PgPool) { + let setup = r#" + create table users ( + id serial primary key, + email varchar(255) not null + ); + "#; + + let query = format!( + "select users.i{}d from users", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("column_hover", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_column_hover_in_join(test_db: PgPool) { + let setup = r#" + create table users ( + id serial primary key, + email varchar(255) not null + ); + + create table posts ( + id serial primary key, + user_id serial references users(id), + content text + ); + "#; + + let query = format!( + "select * from users u join posts p on u.id = p.use{}r_id", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("column_hover_join", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_table_hover_works(test_db: PgPool) { + let setup = r#" + create table users ( + id serial primary key, + email varchar(255) not null + ); + "#; + + let query = format!( + "select id from use{}rs", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("table_hover", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_no_hover_on_keyword(test_db: PgPool) { + let setup = r#" + create table users ( + id serial primary key, + email varchar(255) not null + ); + "#; + + let query = format!( + "sel{}ect id from users", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("no_hover_keyword", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn shortens_lengthy_functions(test_db: PgPool) { + let setup = r#" + create or replace function public.func(cool_stuff text, something_else int,a_third_thing text) + returns void + language sql + stable + as $$ + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + select 1; + $$; + "#; + + let query = format!( + "select public.fu{}nc()", + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("lenghty_function", query, Some(setup), &test_db).await; +} diff --git a/crates/pgt_hover/tests/snapshots/builtin_count.snap b/crates/pgt_hover/tests/snapshots/builtin_count.snap new file mode 100644 index 000000000..2b1035d71 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/builtin_count.snap @@ -0,0 +1,21 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select count(*) from users + ↑ hovered here +``` + +# Hover Results +### `pg_catalog.count("any") β†’ bigint` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.count() β†’ bigint` +#### Aggregate - Immutable - Security INVOKER +--- diff --git a/crates/pgt_hover/tests/snapshots/builtin_max.snap b/crates/pgt_hover/tests/snapshots/builtin_max.snap new file mode 100644 index 000000000..d9ea35c83 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/builtin_max.snap @@ -0,0 +1,161 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select max(id) from users + ↑ hovered here +``` + +# Hover Results +### `pg_catalog.max(bigint) β†’ bigint` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(integer) β†’ integer` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(smallint) β†’ smallint` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(oid) β†’ oid` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(real) β†’ real` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(double precision) β†’ double precision` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(date) β†’ date` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(time without time zone) β†’ time without time zone` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(time with time zone) β†’ time with time zone` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(money) β†’ money` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(timestamp without time zone) β†’ timestamp without time zone` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(timestamp with time zone) β†’ timestamp with time zone` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(interval) β†’ interval` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(text) β†’ text` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(numeric) β†’ numeric` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(anyarray) β†’ anyarray` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(character) β†’ character` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(tid) β†’ tid` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(inet) β†’ inet` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(pg_lsn) β†’ pg_lsn` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(xid8) β†’ xid8` +#### Aggregate - Immutable - Security INVOKER +--- + + +--- + +### `pg_catalog.max(anyenum) β†’ anyenum` +#### Aggregate - Immutable - Security INVOKER +--- diff --git a/crates/pgt_hover/tests/snapshots/builtin_now.snap b/crates/pgt_hover/tests/snapshots/builtin_now.snap new file mode 100644 index 000000000..ead055838 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/builtin_now.snap @@ -0,0 +1,14 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select now() from users + ↑ hovered here +``` + +# Hover Results +### `pg_catalog.now() β†’ timestamp with time zone` +#### Function - Stable - Security INVOKER +--- diff --git a/crates/pgt_hover/tests/snapshots/column_hover.snap b/crates/pgt_hover/tests/snapshots/column_hover.snap new file mode 100644 index 000000000..34fc4552d --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/column_hover.snap @@ -0,0 +1,15 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select users.id from users + ↑ hovered here +``` + +# Hover Results +### `public.users.id` +#### `int4` - πŸ”‘ primary key - not null +--- +Default: `nextval('users_id_seq'::regclass)` diff --git a/crates/pgt_hover/tests/snapshots/column_hover_join.snap b/crates/pgt_hover/tests/snapshots/column_hover_join.snap new file mode 100644 index 000000000..432a72ea8 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/column_hover_join.snap @@ -0,0 +1,15 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select * from users u join posts p on u.id = p.user_id + ↑ hovered here +``` + +# Hover Results +### `public.posts.user_id` +#### `int4` - not null +--- +Default: `nextval('posts_user_id_seq'::regclass)` diff --git a/crates/pgt_hover/tests/snapshots/custom_function.snap b/crates/pgt_hover/tests/snapshots/custom_function.snap new file mode 100644 index 000000000..a58777e69 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/custom_function.snap @@ -0,0 +1,26 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select custom_add(1, 2) + ↑ hovered here +``` + +# Hover Results +### `public.custom_add(a integer, b integer) β†’ integer` +#### Function - Immutable - Security INVOKER +--- +``` +CREATE OR REPLACE FUNCTION public.custom_add(a integer, b integer) + RETURNS integer + LANGUAGE plpgsql + IMMUTABLE +AS $function$ + begin + return a + b; + end; + $function$ + +``` diff --git a/crates/pgt_hover/tests/snapshots/function_with_schema.snap b/crates/pgt_hover/tests/snapshots/function_with_schema.snap new file mode 100644 index 000000000..95188ebde --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/function_with_schema.snap @@ -0,0 +1,24 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select test_schema.schema_func('test') + ↑ hovered here +``` + +# Hover Results +### `test_schema.schema_func(text) β†’ text` +#### Function - Stable - Security INVOKER +--- +``` +CREATE OR REPLACE FUNCTION test_schema.schema_func(text) + RETURNS text + LANGUAGE sql + STABLE +AS $function$ + select 1ドル || ' processed'; + $function$ + +``` diff --git a/crates/pgt_hover/tests/snapshots/lenghty_function.snap b/crates/pgt_hover/tests/snapshots/lenghty_function.snap new file mode 100644 index 000000000..e22fc87a2 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/lenghty_function.snap @@ -0,0 +1,19 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select public.func() + ↑ hovered here +``` + +# Hover Results +### `public.func(cool_stuff text, something_else integer, a_third_thing text) β†’ void` +#### Function - Stable - Security INVOKER +--- +public.func( + cool_stuff text, + something_else integer, + a_third_thing text +) diff --git a/crates/pgt_hover/tests/snapshots/no_hover_keyword.snap b/crates/pgt_hover/tests/snapshots/no_hover_keyword.snap new file mode 100644 index 000000000..31cfdfc69 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/no_hover_keyword.snap @@ -0,0 +1,12 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select id from users + ↑ hovered here +``` + +# Hover Results +No hover information found. diff --git a/crates/pgt_hover/tests/snapshots/table_hover.snap b/crates/pgt_hover/tests/snapshots/table_hover.snap new file mode 100644 index 000000000..6e3b8d9ea --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/table_hover.snap @@ -0,0 +1,15 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select id from users + ↑ hovered here +``` + +# Hover Results +### `public.users` - πŸ”“ RLS disabled +#### +--- +~0 rows, ~0 dead rows, 8.19 kB diff --git a/crates/pgt_schema_cache/src/schema_cache.rs b/crates/pgt_schema_cache/src/schema_cache.rs index e09ce58e0..24b20ccd8 100644 --- a/crates/pgt_schema_cache/src/schema_cache.rs +++ b/crates/pgt_schema_cache/src/schema_cache.rs @@ -93,6 +93,13 @@ impl SchemaCache { .filter(|t| t.name == name && schema.is_none_or(|s| s == t.schema.as_str())) .collect() } + + pub fn find_functions(&self, name: &str, schema: Option<&str>) -> Vec<&function> { + self.functions + .iter() + .filter(|f| f.name == name && schema.is_none_or(|s| s == f.schema.as_str())) + .collect() + } } pub trait SchemaCacheItem { diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index 9cfaadea1..383e4c993 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -749,17 +749,6 @@ impl<'a> TreesitterContext<'a> { } } - pub fn parent_matches_one_of_kind(&self, kinds: &[&'static str]) -> bool { - self.node_under_cursor - .as_ref() - .is_some_and(|under_cursor| match under_cursor { - NodeUnderCursor::TsNode(node) => node - .parent() - .is_some_and(|parent| kinds.contains(&parent.kind())), - - NodeUnderCursor::CustomNode { .. } => false, - }) - } pub fn before_cursor_matches_kind(&self, kinds: &[&'static str]) -> bool { self.node_under_cursor.as_ref().is_some_and(|under_cursor| { match under_cursor { @@ -784,6 +773,33 @@ impl<'a> TreesitterContext<'a> { } }) } + + /// Verifies whether the node_under_cursor has the passed in ancestors in the right order. + /// Note that you need to pass in the ancestors in the order as they would appear in the tree: + /// + /// If the tree shows `relation> object_reference> identifier` and the "identifier" is a leaf node, + /// you need to pass `&["relation", "object_reference"]`. + pub fn matches_ancestor_history(&self, expected_ancestors: &[&'static str]) -> bool { + self.node_under_cursor + .as_ref() + .is_some_and(|under_cursor| match under_cursor { + NodeUnderCursor::TsNode(node) => { + let mut current = Some(*node); + + for &expected_kind in expected_ancestors.iter().rev() { + current = current.and_then(|n| n.parent()); + + match current { + Some(ancestor) if ancestor.kind() == expected_kind => continue, + _ => return false, + } + } + + true + } + NodeUnderCursor::CustomNode { .. } => false, + }) + } } #[cfg(test)] diff --git a/postgrestools.jsonc b/postgrestools.jsonc index fd1149e81..47d08c729 100644 --- a/postgrestools.jsonc +++ b/postgrestools.jsonc @@ -17,7 +17,7 @@ // YOU CAN COMMENT ME OUT :) "db": { "host": "127.0.0.1", - "port": 54322, + "port": 5432, "username": "postgres", "password": "postgres", "database": "postgres",

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /