From 69f7536b55515d081b6e1542dec572404ab81c1d Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 26 Dec 2025 22:54:41 -0500 Subject: [PATCH] ide: add hover for create table also support hover on create table and column defs instead of just refs --- crates/squawk_ide/src/hover.rs | 344 +++++++++++++++++++++++++++++++-- 1 file changed, 329 insertions(+), 15 deletions(-) diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 5d7c2815..5b1e747c 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -8,12 +8,38 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { let token = token_from_offset(file, offset)?; let parent = token.parent()?; - let name_ref = ast::NameRef::cast(parent)?; + let binder = binder::bind(file); + + if let Some(name_ref) = ast::NameRef::cast(parent.clone()) { + if is_column_ref(&name_ref) { + return hover_column(file, &name_ref, &binder); + } - if !is_column_ref(&name_ref) { - return None; + if is_table_ref(&name_ref) { + return hover_table(file, &name_ref, &binder); + } } + if let Some(name) = ast::Name::cast(parent) { + if let Some(column) = name.syntax().parent().and_then(ast::Column::cast) + && let Some(create_table) = column.syntax().ancestors().find_map(ast::CreateTable::cast) + { + return hover_column_definition(&create_table, &column, &binder); + } + + if let Some(create_table) = name.syntax().ancestors().find_map(ast::CreateTable::cast) { + return format_create_table(&create_table, &binder); + } + } + + None +} + +fn hover_column( + file: &ast::SourceFile, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { let column_name = name_ref.syntax().text().to_string(); let create_index = name_ref @@ -24,11 +50,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { let relation_name = create_index.relation_name()?; let path = relation_name.path()?; - let binder = binder::bind(file); - - let (schema, table_name) = resolve::resolve_table_info(&binder, &path)?; + let (schema, table_name) = resolve::resolve_table_info(binder, &path)?; - let column_ptr = resolve::resolve_name_ref(&binder, &name_ref)?; + let column_ptr = resolve::resolve_name_ref(binder, name_ref)?; let root = file.syntax(); let column_name_node = column_ptr.to_node(root); @@ -43,14 +67,128 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { )) } +fn hover_column_definition( + create_table: &ast::CreateTable, + column: &ast::Column, + binder: &binder::Binder, +) -> Option { + let column_name = column.name()?.syntax().text().to_string(); + let ty = column.ty()?; + let path = create_table.path()?; + let table_name = path.segment()?.name()?.syntax().text().to_string(); + + let schema = if let Some(qualifier) = path.qualifier() { + qualifier.syntax().text().to_string() + } else if let Some(schema) = table_schema(create_table, binder) { + schema + } else { + return Some(format!( + "{}.{} {}", + table_name, + column_name, + ty.syntax().text() + )); + }; + + Some(format!( + "{}.{}.{} {}", + schema, + table_name, + column_name, + ty.syntax().text() + )) +} + +fn hover_table( + file: &ast::SourceFile, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let table_ptr = resolve::resolve_name_ref(binder, name_ref)?; + + let root = file.syntax(); + let table_name_node = table_ptr.to_node(root); + + let create_table = table_name_node + .ancestors() + .find_map(ast::CreateTable::cast)?; + + format_create_table(&create_table, binder) +} + +// Insert inferred schema into the create table hover info +fn format_create_table(create_table: &ast::CreateTable, binder: &binder::Binder) -> Option { + let path = create_table.path()?; + let mut text = create_table.syntax().text().to_string(); + + if path.qualifier().is_some() { + return Some(text); + } + + let Some(schema) = table_schema(create_table, binder) else { + return Some(text); + }; + + let Some(offset) = table_name_offset(create_table, &path) else { + return Some(text); + }; + + text.insert_str(offset, &format!("{}.", schema)); + Some(text) +} + +fn table_schema(create_table: &ast::CreateTable, binder: &binder::Binder) -> Option { + let is_temp = create_table.temp_token().is_some() || create_table.temporary_token().is_some(); + if is_temp { + return Some("pg_temp".to_string()); + } + + let position = create_table.syntax().text_range().start(); + let search_path = binder.search_path_at(position); + search_path.first().map(|s| s.to_string()) +} + +fn table_name_offset(create_table: &ast::CreateTable, path: &ast::Path) -> Option { + let segment = path.segment()?; + let name = segment.name()?; + let name_start = name.syntax().text_range().start(); + let create_table_start = create_table.syntax().text_range().start(); + Some((name_start - create_table_start).into()) +} + fn is_column_ref(name_ref: &ast::NameRef) -> bool { + let mut in_partition_item = false; + for ancestor in name_ref.syntax().ancestors() { if ast::PartitionItem::can_cast(ancestor.kind()) { - return true; + in_partition_item = true; } if ast::CreateIndex::can_cast(ancestor.kind()) { + return in_partition_item; + } + } + false +} + +fn is_table_ref(name_ref: &ast::NameRef) -> bool { + let mut in_partition_item = false; + + for ancestor in name_ref.syntax().ancestors() { + if ast::DropTable::can_cast(ancestor.kind()) { + return true; + } + if ast::Table::can_cast(ancestor.kind()) { return true; } + if ast::DropIndex::can_cast(ancestor.kind()) { + return false; + } + if ast::PartitionItem::can_cast(ancestor.kind()) { + in_partition_item = true; + } + if ast::CreateIndex::can_cast(ancestor.kind()) { + return !in_partition_item; + } } false } @@ -239,13 +377,16 @@ create index idx_email on public.users(email$0); } #[test] - fn hover_not_on_table_name() { - hover_not_found( - " -create table users(id int); -create index idx on users$0(id); -", - ); + fn hover_on_table_name() { + assert_snapshot!(check_hover(" +create table t(id int); +create index idx on t$0(id); +"), @r" + hover: create table public.t(id int) + ╭▸ + 3 │ create index idx on t(id); + ╰╴ ─ hover + "); } #[test] @@ -257,4 +398,177 @@ create index idx$0 on users(id); ", ); } + + #[test] + fn hover_table_in_create_index() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +create index idx_email on users$0(email); +"), @r" + hover: create table public.users(id int, email text) + ╭▸ + 3 │ create index idx_email on users(email); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_table_with_schema() { + assert_snapshot!(check_hover(" +create table public.users(id int, email text); +create index idx on public.users$0(id); +"), @r" + hover: create table public.users(id int, email text) + ╭▸ + 3 │ create index idx on public.users(id); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_table_temp() { + assert_snapshot!(check_hover(" +create temp table users(id int, email text); +create index idx on users$0(id); +"), @r" + hover: create temp table pg_temp.users(id int, email text) + ╭▸ + 3 │ create index idx on users(id); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_table_multiline() { + assert_snapshot!(check_hover(" +create table users( + id int, + email text, + name varchar(100) +); +create index idx on users$0(id); +"), @r" + hover: create table public.users( + id int, + email text, + name varchar(100) + ) + ╭▸ + 7 │ create index idx on users(id); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_table_with_search_path() { + assert_snapshot!(check_hover(r#" +set search_path to myschema; +create table users(id int, email text); +create index idx on users$0(id); +"#), @r" + hover: create table myschema.users(id int, email text) + ╭▸ + 4 │ create index idx on users(id); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_table_search_path_at_definition() { + assert_snapshot!(check_hover(r#" +set search_path to myschema; +create table users(id int, email text); +set search_path to myschema, otherschema; +create index idx on users$0(id); +"#), @r" + hover: create table myschema.users(id int, email text) + ╭▸ + 5 │ create index idx on users(id); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_table_definition() { + assert_snapshot!(check_hover(" +create table t$0(x bigint); +"), @r" + hover: create table public.t(x bigint) + ╭▸ + 2 │ create table t(x bigint); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_table_definition_with_schema() { + assert_snapshot!(check_hover(" +create table myschema.users$0(id int); +"), @r" + hover: create table myschema.users(id int) + ╭▸ + 2 │ create table myschema.users(id int); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_temp_table_definition() { + assert_snapshot!(check_hover(" +create temp table t$0(x bigint); +"), @r" + hover: create temp table pg_temp.t(x bigint) + ╭▸ + 2 │ create temp table t(x bigint); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_column_in_create_table() { + assert_snapshot!(check_hover(" +create table t(id$0 int); +"), @r" + hover: public.t.id int + ╭▸ + 2 │ create table t(id int); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_column_in_create_table_with_schema() { + assert_snapshot!(check_hover(" +create table myschema.users(id$0 int, name text); +"), @r" + hover: myschema.users.id int + ╭▸ + 2 │ create table myschema.users(id int, name text); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_column_in_temp_table() { + assert_snapshot!(check_hover(" +create temp table t(x$0 bigint); +"), @r" + hover: pg_temp.t.x bigint + ╭▸ + 2 │ create temp table t(x bigint); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_multiple_columns() { + assert_snapshot!(check_hover(" +create table t(id int, email$0 text, name varchar(100)); +"), @r" + hover: public.t.email text + ╭▸ + 2 │ create table t(id int, email text, name varchar(100)); + ╰╴ ─ hover + "); + } }