diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 92aa8e41..d6816179 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -375,7 +375,7 @@ fn bind_set(b: &mut Binder, set: ast::Set) { } } -fn extract_string_literal(literal: &ast::Literal) -> Option { +pub(crate) fn extract_string_literal(literal: &ast::Literal) -> Option { let text = literal.syntax().text().to_string(); if text.starts_with('\'') && text.ends_with('\'') && text.len() >= 2 { diff --git a/crates/squawk_ide/src/document_symbols.rs b/crates/squawk_ide/src/document_symbols.rs index 34652e9f..800c87a4 100644 --- a/crates/squawk_ide/src/document_symbols.rs +++ b/crates/squawk_ide/src/document_symbols.rs @@ -1,7 +1,7 @@ use rowan::TextRange; use squawk_syntax::ast::{self, AstNode}; -use crate::binder; +use crate::binder::{self, extract_string_literal}; use crate::resolve::{resolve_function_info, resolve_table_info, resolve_type_info}; #[derive(Debug)] @@ -10,6 +10,7 @@ pub enum DocumentSymbolKind { Function, Type, Column, + Variant, } #[derive(Debug)] @@ -126,13 +127,28 @@ fn create_type_symbol( let full_range = create_type.syntax().text_range(); let focus_range = name_node.syntax().text_range(); + let mut children = vec![]; + if let Some(variant_list) = create_type.variant_list() { + for variant in variant_list.variants() { + if let Some(variant_symbol) = create_variant_symbol(variant) { + children.push(variant_symbol); + } + } + } else if let Some(column_list) = create_type.column_list() { + for column in column_list.columns() { + if let Some(column_symbol) = create_column_symbol(column) { + children.push(column_symbol); + } + } + } + Some(DocumentSymbol { name, detail: None, kind: DocumentSymbolKind::Type, full_range, focus_range, - children: vec![], + children, }) } @@ -155,6 +171,23 @@ fn create_column_symbol(column: ast::Column) -> Option { }) } +fn create_variant_symbol(variant: ast::Variant) -> Option { + let literal = variant.literal()?; + let name = extract_string_literal(&literal)?; + + let full_range = variant.syntax().text_range(); + let focus_range = literal.syntax().text_range(); + + Some(DocumentSymbol { + name, + detail: None, + kind: DocumentSymbolKind::Variant, + full_range, + focus_range, + children: vec![], + }) +} + #[cfg(test)] mod tests { use super::*; @@ -197,6 +230,7 @@ mod tests { DocumentSymbolKind::Function => "function", DocumentSymbolKind::Type => "type", DocumentSymbolKind::Column => "column", + DocumentSymbolKind::Variant => "variant", }; let title = if let Some(detail) = &symbol.detail { @@ -227,10 +261,14 @@ mod tests { .map(|child| { let kind = match child.kind { DocumentSymbolKind::Column => "column", - _ => unreachable!("only columns can be children"), + DocumentSymbolKind::Variant => "variant", + _ => unreachable!("only columns and variants can be children"), }; - let detail = &child.detail.as_ref().unwrap(); - format!("{}: {} {}", kind, child.name, detail) + if let Some(detail) = &child.detail { + format!("{}: {} {}", kind, child.name, detail) + } else { + format!("{}: {}", kind, child.name) + } }) .collect(); @@ -392,7 +430,16 @@ create function my_schema.hello() returns void as $$ select 1; $$ language sql; │ ┬───────────┯━━━━━─────────────────────────────── │ │ │ │ │ focus range - ╰╴full range + │ full range + │ + ⸬ + 1 │ create type status as enum ('active', 'inactive'); + │ ┯━━━━━━━ ┯━━━━━━━━━ + │ │ │ + │ │ full range for `variant: inactive` + │ │ focus range + │ full range for `variant: active` + ╰╴ focus range " ); } @@ -408,7 +455,43 @@ create function my_schema.hello() returns void as $$ select 1; $$ language sql; │ ┬───────────┯━━━━━──────────────────────── │ │ │ │ │ focus range - ╰╴full range + │ full range + │ + ⸬ + 1 │ create type person as (name text, age int); + │ ┯━━━───── ┯━━──── + │ │ │ + │ │ full range for `column: age int` + │ │ focus range + │ full range for `column: name text` + ╰╴ focus range + " + ); + } + + #[test] + fn create_type_composite_multiple_columns() { + assert_snapshot!( + symbols("create type address as (street text, city text, zip varchar(10));"), + @r" + info: type: public.address + ╭▸ + 1 │ create type address as (street text, city text, zip varchar(10)); + │ ┬───────────┯━━━━━━───────────────────────────────────────────── + │ │ │ + │ │ focus range + │ full range + │ + ⸬ + 1 │ create type address as (street text, city text, zip varchar(10)); + │ ┯━━━━━───── ┯━━━───── ┯━━──────────── + │ │ │ │ + │ │ │ full range for `column: zip varchar(10)` + │ │ │ focus range + │ │ full range for `column: city text` + │ │ focus range + │ full range for `column: street text` + ╰╴ focus range " ); } @@ -424,7 +507,45 @@ create function my_schema.hello() returns void as $$ select 1; $$ language sql; │ ┬────────────────────┯━━━━━─────────────────────────────── │ │ │ │ │ focus range - ╰╴full range + │ full range + │ + ⸬ + 1 │ create type myschema.status as enum ('active', 'inactive'); + │ ┯━━━━━━━ ┯━━━━━━━━━ + │ │ │ + │ │ full range for `variant: inactive` + │ │ focus range + │ full range for `variant: active` + ╰╴ focus range + " + ); + } + + #[test] + fn create_type_enum_multiple_variants() { + assert_snapshot!( + symbols("create type priority as enum ('low', 'medium', 'high', 'urgent');"), + @r" + info: type: public.priority + ╭▸ + 1 │ create type priority as enum ('low', 'medium', 'high', 'urgent'); + │ ┬───────────┯━━━━━━━──────────────────────────────────────────── + │ │ │ + │ │ focus range + │ full range + │ + ⸬ + 1 │ create type priority as enum ('low', 'medium', 'high', 'urgent'); + │ ┯━━━━ ┯━━━━━━━ ┯━━━━━ ┯━━━━━━━ + │ │ │ │ │ + │ │ │ │ full range for `variant: urgent` + │ │ │ │ focus range + │ │ │ full range for `variant: high` + │ │ │ focus range + │ │ full range for `variant: medium` + │ │ focus range + │ full range for `variant: low` + ╰╴ focus range " ); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 44c63f6f..02100f9c 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -457,6 +457,48 @@ drop type int4_range$0; "); } + #[test] + fn goto_cast_operator() { + assert_snapshot!(goto(" +create type foo as enum ('a', 'b'); +select x::foo$0; +"), @r" + ╭▸ + 2 │ create type foo as enum ('a', 'b'); + │ ─── 2. destination + 3 │ select x::foo; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_cast_function() { + assert_snapshot!(goto(" +create type bar as enum ('x', 'y'); +select cast(x as bar$0); +"), @r" + ╭▸ + 2 │ create type bar as enum ('x', 'y'); + │ ─── 2. destination + 3 │ select cast(x as bar); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_cast_with_schema() { + assert_snapshot!(goto(" +create type public.baz as enum ('m', 'n'); +select x::public.baz$0; +"), @r" + ╭▸ + 2 │ create type public.baz as enum ('m', 'n'); + │ ─── 2. destination + 3 │ select x::public.baz; + ╰╴ ─ 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 ffa4c7c1..a75811f1 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -16,6 +16,10 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { return hover_column(file, &name_ref, &binder); } + if is_type_ref(&name_ref) { + return hover_type(file, &name_ref, &binder); + } + if is_select_column(&name_ref) { // Try hover as column first if let Some(result) = hover_column(file, &name_ref, &binder) { @@ -45,10 +49,6 @@ 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); } @@ -461,10 +461,17 @@ fn is_index_ref(name_ref: &ast::NameRef) -> bool { } fn is_type_ref(name_ref: &ast::NameRef) -> bool { + let mut in_type = false; for ancestor in name_ref.syntax().ancestors() { + if ast::PathType::can_cast(ancestor.kind()) || ast::ExprType::can_cast(ancestor.kind()) { + in_type = true; + } if ast::DropType::can_cast(ancestor.kind()) { return true; } + if ast::CastExpr::can_cast(ancestor.kind()) && in_type { + return true; + } } false } @@ -1342,6 +1349,45 @@ drop type int4_range$0; "); } + #[test] + fn hover_on_cast_operator() { + assert_snapshot!(check_hover(" +create type foo as enum ('a', 'b'); +select x::foo$0; +"), @r" + hover: type public.foo as enum ('a', 'b') + ╭▸ + 3 │ select x::foo; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_cast_function() { + assert_snapshot!(check_hover(" +create type bar as enum ('x', 'y'); +select cast(x as bar$0); +"), @r" + hover: type public.bar as enum ('x', 'y') + ╭▸ + 3 │ select cast(x as bar); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_cast_with_schema() { + assert_snapshot!(check_hover(" +create type myschema.baz as enum ('m', 'n'); +select x::myschema.baz$0; +"), @r" + hover: type myschema.baz as enum ('m', 'n') + ╭▸ + 3 │ select x::myschema.baz; + ╰╴ ─ 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 006d313f..4482f866 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -37,6 +37,7 @@ enum NameRefContext { UpdateSetColumn, UpdateFromTable, SchemaQualifier, + TypeReference, } pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Option { @@ -83,10 +84,28 @@ 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); + NameRefContext::DropType | NameRefContext::TypeReference => { + let (type_name, schema) = if let Some(parent) = name_ref.syntax().parent() + && let Some(field_expr) = ast::FieldExpr::cast(parent) + && field_expr + .field() + .is_some_and(|field| field.syntax() == name_ref.syntax()) + { + let type_name = Name::from_node(name_ref); + let schema = if let Some(base) = field_expr.base() + && let ast::Expr::NameRef(schema_name_ref) = base + { + Some(Schema(Name::from_node(&schema_name_ref))) + } else { + None + }; + (type_name, schema) + } else { + let path = find_containing_path(name_ref)?; + let type_name = extract_table_name(&path)?; + let schema = extract_schema_name(&path); + (type_name, schema) + }; let position = name_ref.syntax().text_range().start(); resolve_type(binder, &type_name, &schema, position) } @@ -307,11 +326,18 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option .is_some(); let mut in_from_clause = false; + let mut in_cast_expr = false; for ancestor in parent.ancestors() { + if ast::CastExpr::can_cast(ancestor.kind()) { + in_cast_expr = true; + } if ast::FromClause::can_cast(ancestor.kind()) { in_from_clause = true; } if ast::Select::can_cast(ancestor.kind()) && !in_from_clause { + if in_cast_expr { + return Some(NameRefContext::TypeReference); + } if is_base_of_outer_field_expr { return Some(NameRefContext::SelectQualifiedColumnTable); } else if let Some(base) = field_expr.base() @@ -337,7 +363,11 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option return Some(NameRefContext::SchemaQualifier); } + let mut in_type = false; for ancestor in name_ref.syntax().ancestors() { + if ast::PathType::can_cast(ancestor.kind()) || ast::ExprType::can_cast(ancestor.kind()) { + in_type = true; + } if ast::DropTable::can_cast(ancestor.kind()) { return Some(NameRefContext::DropTable); } @@ -350,6 +380,9 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option if ast::DropType::can_cast(ancestor.kind()) { return Some(NameRefContext::DropType); } + if ast::CastExpr::can_cast(ancestor.kind()) && in_type { + return Some(NameRefContext::TypeReference); + } if ast::DropFunction::can_cast(ancestor.kind()) { return Some(NameRefContext::DropFunction); } diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 1c754480..24cc88bb 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -339,6 +339,7 @@ fn handle_document_symbol( DocumentSymbolKind::Function => SymbolKind::FUNCTION, DocumentSymbolKind::Type => SymbolKind::CLASS, DocumentSymbolKind::Column => SymbolKind::FIELD, + DocumentSymbolKind::Variant => SymbolKind::ENUM_MEMBER, }, tags: None, range, diff --git a/crates/squawk_wasm/src/lib.rs b/crates/squawk_wasm/src/lib.rs index 97e02d10..ba85ef98 100644 --- a/crates/squawk_wasm/src/lib.rs +++ b/crates/squawk_wasm/src/lib.rs @@ -396,6 +396,7 @@ fn convert_document_symbol( squawk_ide::document_symbols::DocumentSymbolKind::Function => "function", squawk_ide::document_symbols::DocumentSymbolKind::Type => "type", squawk_ide::document_symbols::DocumentSymbolKind::Column => "column", + squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant", } .to_string(), start_line: full_start_wide.line,