From 9d0c6e192d2ef3f2c6c1859ae3cf13e2b1f4f1fa Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 1 Jan 2026 19:07:39 -0500 Subject: [PATCH] ide: add initial goto def support for types we're only supporting create/drop initially --- crates/squawk_ide/src/binder.rs | 27 ++++ crates/squawk_ide/src/document_symbols.rs | 81 ++++++++++- crates/squawk_ide/src/goto_definition.rs | 88 +++++++++++ crates/squawk_ide/src/hover.rs | 170 ++++++++++++++++++++++ crates/squawk_ide/src/resolve.rs | 24 +++ crates/squawk_ide/src/symbols.rs | 1 + crates/squawk_server/src/lib.rs | 1 + crates/squawk_wasm/src/lib.rs | 1 + 8 files changed, 392 insertions(+), 1 deletion(-) diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 8bda33d0..92aa8e41 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -83,6 +83,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { ast::Stmt::CreateAggregate(create_aggregate) => bind_create_aggregate(b, create_aggregate), ast::Stmt::CreateProcedure(create_procedure) => bind_create_procedure(b, create_procedure), ast::Stmt::CreateSchema(create_schema) => bind_create_schema(b, create_schema), + ast::Stmt::CreateType(create_type) => bind_create_type(b, create_type), ast::Stmt::Set(set) => bind_set(b, set), _ => {} } @@ -238,6 +239,32 @@ fn bind_create_schema(b: &mut Binder, create_schema: ast::CreateSchema) { b.scopes[root].insert(schema_name, schema_id); } +fn bind_create_type(b: &mut Binder, create_type: ast::CreateType) { + let Some(path) = create_type.path() else { + return; + }; + + let Some(type_name) = item_name(&path) else { + return; + }; + + let name_ptr = path_to_ptr(&path); + + let Some(schema) = schema_name(b, &path, false) else { + return; + }; + + let type_id = b.symbols.alloc(Symbol { + kind: SymbolKind::Type, + ptr: name_ptr, + schema, + params: None, + }); + + let root = b.root_scope(); + b.scopes[root].insert(type_name, type_id); +} + fn item_name(path: &ast::Path) -> Option { let segment = path.segment()?; diff --git a/crates/squawk_ide/src/document_symbols.rs b/crates/squawk_ide/src/document_symbols.rs index a617a1bf..34652e9f 100644 --- a/crates/squawk_ide/src/document_symbols.rs +++ b/crates/squawk_ide/src/document_symbols.rs @@ -2,12 +2,13 @@ use rowan::TextRange; use squawk_syntax::ast::{self, AstNode}; use crate::binder; -use crate::resolve::{resolve_function_info, resolve_table_info}; +use crate::resolve::{resolve_function_info, resolve_table_info, resolve_type_info}; #[derive(Debug)] pub enum DocumentSymbolKind { Table, Function, + Type, Column, } @@ -40,6 +41,11 @@ pub fn document_symbols(file: &ast::SourceFile) -> Vec { symbols.push(symbol); } } + ast::Stmt::CreateType(create_type) => { + if let Some(symbol) = create_type_symbol(&binder, create_type) { + symbols.push(symbol); + } + } _ => {} } } @@ -106,6 +112,30 @@ fn create_function_symbol( }) } +fn create_type_symbol( + binder: &binder::Binder, + create_type: ast::CreateType, +) -> Option { + let path = create_type.path()?; + let segment = path.segment()?; + let name_node = segment.name()?; + + let (schema, type_name) = resolve_type_info(binder, &path)?; + let name = format!("{}.{}", schema.0, type_name); + + let full_range = create_type.syntax().text_range(); + let focus_range = name_node.syntax().text_range(); + + Some(DocumentSymbol { + name, + detail: None, + kind: DocumentSymbolKind::Type, + full_range, + focus_range, + children: vec![], + }) +} + fn create_column_symbol(column: ast::Column) -> Option { let name_node = column.name()?; let name = name_node.syntax().text().to_string(); @@ -165,6 +195,7 @@ mod tests { let kind = match symbol.kind { DocumentSymbolKind::Table => "table", DocumentSymbolKind::Function => "function", + DocumentSymbolKind::Type => "type", DocumentSymbolKind::Column => "column", }; @@ -350,6 +381,54 @@ create function my_schema.hello() returns void as $$ select 1; $$ language sql; "); } + #[test] + fn create_type() { + assert_snapshot!( + symbols("create type status as enum ('active', 'inactive');"), + @r" + info: type: public.status + ╭▸ + 1 │ create type status as enum ('active', 'inactive'); + │ ┬───────────┯━━━━━─────────────────────────────── + │ │ │ + │ │ focus range + ╰╴full range + " + ); + } + + #[test] + fn create_type_composite() { + assert_snapshot!( + symbols("create type person as (name text, age int);"), + @r" + info: type: public.person + ╭▸ + 1 │ create type person as (name text, age int); + │ ┬───────────┯━━━━━──────────────────────── + │ │ │ + │ │ focus range + ╰╴full range + " + ); + } + + #[test] + fn create_type_with_schema() { + assert_snapshot!( + symbols("create type myschema.status as enum ('active', 'inactive');"), + @r" + info: type: myschema.status + ╭▸ + 1 │ create type myschema.status as enum ('active', 'inactive'); + │ ┬────────────────────┯━━━━━─────────────────────────────── + │ │ │ + │ │ focus range + ╰╴full range + " + ); + } + #[test] fn empty_file() { symbols_not_found("") diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 21b2bfa3..44c63f6f 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -369,6 +369,94 @@ create table t(); "); } + #[test] + fn goto_drop_type() { + assert_snapshot!(goto(" +create type t as enum ('a', 'b'); +drop type t$0; +"), @r" + ╭▸ + 2 │ create type t as enum ('a', 'b'); + │ ─ 2. destination + 3 │ drop type t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_drop_type_with_schema() { + assert_snapshot!(goto(" +create type public.t as enum ('a', 'b'); +drop type t$0; +"), @r" + ╭▸ + 2 │ create type public.t as enum ('a', 'b'); + │ ─ 2. destination + 3 │ drop type t; + ╰╴ ─ 1. source + "); + + assert_snapshot!(goto(" +create type foo.t as enum ('a', 'b'); +drop type foo.t$0; +"), @r" + ╭▸ + 2 │ create type foo.t as enum ('a', 'b'); + │ ─ 2. destination + 3 │ drop type foo.t; + ╰╴ ─ 1. source + "); + + goto_not_found( + " +create type t as enum ('a', 'b'); +drop type foo.t$0; +", + ); + } + + #[test] + fn goto_drop_type_defined_after() { + assert_snapshot!(goto(" +drop type t$0; +create type t as enum ('a', 'b'); +"), @r" + ╭▸ + 2 │ drop type t; + │ ─ 1. source + 3 │ create type t as enum ('a', 'b'); + ╰╴ ─ 2. destination + "); + } + + #[test] + fn goto_drop_type_composite() { + assert_snapshot!(goto(" +create type person as (name text, age int); +drop type person$0; +"), @r" + ╭▸ + 2 │ create type person as (name text, age int); + │ ────── 2. destination + 3 │ drop type person; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_drop_type_range() { + assert_snapshot!(goto(" +create type int4_range as range (subtype = int4); +drop type int4_range$0; +"), @r" + ╭▸ + 2 │ create type int4_range as range (subtype = int4); + │ ────────── 2. destination + 3 │ drop type int4_range; + ╰╴ ─ 1. source + "); + } + #[test] fn begin_to_rollback() { assert_snapshot!(goto( diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 16da7001..ffa4c7c1 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -45,6 +45,10 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { return hover_index(file, &name_ref, &binder); } + if is_type_ref(&name_ref) { + return hover_type(file, &name_ref, &binder); + } + if is_function_ref(&name_ref) { return hover_function(file, &name_ref, &binder); } @@ -98,6 +102,10 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { return format_create_index(&create_index, &binder); } + if let Some(create_type) = name.syntax().ancestors().find_map(ast::CreateType::cast) { + return format_create_type(&create_type, &binder); + } + if let Some(create_function) = name .syntax() .ancestors() @@ -249,6 +257,21 @@ fn hover_index( format_create_index(&create_index, binder) } +fn hover_type( + file: &ast::SourceFile, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let type_ptr = resolve::resolve_name_ref(binder, name_ref)?; + + let root = file.syntax(); + let type_name_node = type_ptr.to_node(root); + + let create_type = type_name_node.ancestors().find_map(ast::CreateType::cast)?; + + format_create_type(&create_type, 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()?; @@ -307,6 +330,44 @@ fn index_schema(create_index: &ast::CreateIndex, binder: &binder::Binder) -> Opt search_path.first().map(|s| s.to_string()) } +fn format_create_type(create_type: &ast::CreateType, binder: &binder::Binder) -> Option { + let path = create_type.path()?; + let segment = path.segment()?; + let type_name = segment.name()?.syntax().text().to_string(); + + let schema = if let Some(qualifier) = path.qualifier() { + qualifier.syntax().text().to_string() + } else { + type_schema(create_type, binder)? + }; + + if let Some(variant_list) = create_type.variant_list() { + let variants = variant_list.syntax().text().to_string(); + return Some(format!( + "type {}.{} as enum {}", + schema, type_name, variants + )); + } + + if let Some(column_list) = create_type.column_list() { + let columns = column_list.syntax().text().to_string(); + return Some(format!("type {}.{} as {}", schema, type_name, columns)); + } + + if let Some(attribute_list) = create_type.attribute_list() { + let attributes = attribute_list.syntax().text().to_string(); + return Some(format!("type {}.{} {}", schema, type_name, attributes)); + } + + Some(format!("type {}.{}", schema, type_name)) +} + +fn type_schema(create_type: &ast::CreateType, binder: &binder::Binder) -> Option { + let position = create_type.syntax().text_range().start(); + let search_path = binder.search_path_at(position); + search_path.first().map(|s| s.to_string()) +} + fn is_column_ref(name_ref: &ast::NameRef) -> bool { let mut in_partition_item = false; let mut in_column_list = false; @@ -399,6 +460,15 @@ fn is_index_ref(name_ref: &ast::NameRef) -> bool { false } +fn is_type_ref(name_ref: &ast::NameRef) -> bool { + for ancestor in name_ref.syntax().ancestors() { + if ast::DropType::can_cast(ancestor.kind()) { + return true; + } + } + false +} + fn is_function_ref(name_ref: &ast::NameRef) -> bool { for ancestor in name_ref.syntax().ancestors() { if ast::DropFunction::can_cast(ancestor.kind()) { @@ -1172,6 +1242,106 @@ drop index idx_x$0; "); } + #[test] + fn hover_on_create_type_definition() { + assert_snapshot!(check_hover(" +create type status$0 as enum ('active', 'inactive'); +"), @r" + hover: type public.status as enum ('active', 'inactive') + ╭▸ + 2 │ create type status as enum ('active', 'inactive'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_type_definition_with_schema() { + assert_snapshot!(check_hover(" +create type myschema.status$0 as enum ('active', 'inactive'); +"), @r" + hover: type myschema.status as enum ('active', 'inactive') + ╭▸ + 2 │ create type myschema.status as enum ('active', 'inactive'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_drop_type() { + assert_snapshot!(check_hover(" +create type status as enum ('active', 'inactive'); +drop type status$0; +"), @r" + hover: type public.status as enum ('active', 'inactive') + ╭▸ + 3 │ drop type status; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_drop_type_with_schema() { + assert_snapshot!(check_hover(" +create type myschema.status as enum ('active', 'inactive'); +drop type myschema.status$0; +"), @r" + hover: type myschema.status as enum ('active', 'inactive') + ╭▸ + 3 │ drop type myschema.status; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_type_composite() { + assert_snapshot!(check_hover(" +create type person$0 as (name text, age int); +"), @r" + hover: type public.person as (name text, age int) + ╭▸ + 2 │ create type person as (name text, age int); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_drop_type_composite() { + assert_snapshot!(check_hover(" +create type person as (name text, age int); +drop type person$0; +"), @r" + hover: type public.person as (name text, age int) + ╭▸ + 3 │ drop type person; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_type_range() { + assert_snapshot!(check_hover(" +create type int4_range$0 as range (subtype = int4); +"), @r" + hover: type public.int4_range (subtype = int4) + ╭▸ + 2 │ create type int4_range as range (subtype = int4); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_drop_type_range() { + assert_snapshot!(check_hover(" +create type int4_range as range (subtype = int4); +drop type int4_range$0; +"), @r" + hover: type public.int4_range (subtype = int4) + ╭▸ + 3 │ drop type int4_range; + ╰╴ ─ hover + "); + } + #[test] fn hover_on_drop_function() { assert_snapshot!(check_hover(" diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 82cf1140..006d313f 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -14,6 +14,7 @@ enum NameRefContext { DropTable, Table, DropIndex, + DropType, DropFunction, DropAggregate, DropProcedure, @@ -82,6 +83,13 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti let position = name_ref.syntax().text_range().start(); resolve_index(binder, &index_name, &schema, position) } + NameRefContext::DropType => { + let path = find_containing_path(name_ref)?; + let type_name = extract_table_name(&path)?; + let schema = extract_schema_name(&path); + let position = name_ref.syntax().text_range().start(); + resolve_type(binder, &type_name, &schema, position) + } NameRefContext::DropFunction => { let function_sig = name_ref .syntax() @@ -339,6 +347,9 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option if ast::DropIndex::can_cast(ancestor.kind()) { return Some(NameRefContext::DropIndex); } + if ast::DropType::can_cast(ancestor.kind()) { + return Some(NameRefContext::DropType); + } if ast::DropFunction::can_cast(ancestor.kind()) { return Some(NameRefContext::DropFunction); } @@ -442,6 +453,15 @@ fn resolve_index( resolve_for_kind(binder, index_name, schema, position, SymbolKind::Index) } +fn resolve_type( + binder: &Binder, + type_name: &Name, + schema: &Option, + position: TextSize, +) -> Option { + resolve_for_kind(binder, type_name, schema, position, SymbolKind::Type) +} + fn resolve_for_kind( binder: &Binder, name: &Name, @@ -1346,6 +1366,10 @@ pub(crate) fn resolve_function_info(binder: &Binder, path: &ast::Path) -> Option resolve_symbol_info(binder, path, SymbolKind::Function) } +pub(crate) fn resolve_type_info(binder: &Binder, path: &ast::Path) -> Option<(Schema, String)> { + resolve_symbol_info(binder, path, SymbolKind::Type) +} + fn resolve_symbol_info( binder: &Binder, path: &ast::Path, diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index cef8e1eb..9f15146a 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -50,6 +50,7 @@ pub(crate) enum SymbolKind { Aggregate, Procedure, Schema, + Type, } #[derive(Clone, Debug)] diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 81b5e3e2..1c754480 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -337,6 +337,7 @@ fn handle_document_symbol( kind: match sym.kind { DocumentSymbolKind::Table => SymbolKind::STRUCT, DocumentSymbolKind::Function => SymbolKind::FUNCTION, + DocumentSymbolKind::Type => SymbolKind::CLASS, DocumentSymbolKind::Column => SymbolKind::FIELD, }, tags: None, diff --git a/crates/squawk_wasm/src/lib.rs b/crates/squawk_wasm/src/lib.rs index fd396c23..97e02d10 100644 --- a/crates/squawk_wasm/src/lib.rs +++ b/crates/squawk_wasm/src/lib.rs @@ -394,6 +394,7 @@ fn convert_document_symbol( kind: match symbol.kind { squawk_ide::document_symbols::DocumentSymbolKind::Table => "table", squawk_ide::document_symbols::DocumentSymbolKind::Function => "function", + squawk_ide::document_symbols::DocumentSymbolKind::Type => "type", squawk_ide::document_symbols::DocumentSymbolKind::Column => "column", } .to_string(),