From a852eda8cca2d0c87cbb01b8eb284256b44e8382 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 14 Jan 2026 11:49:32 -0500 Subject: [PATCH] ide: add support for declare/fetch/move/close cursor --- crates/squawk_ide/src/binder.rs | 20 +++++++ crates/squawk_ide/src/classify.rs | 12 ++++ crates/squawk_ide/src/document_symbols.rs | 39 +++++++++++++ crates/squawk_ide/src/goto_definition.rs | 70 +++++++++++++++++++++++ crates/squawk_ide/src/hover.rs | 68 ++++++++++++++++++++++ crates/squawk_ide/src/resolve.rs | 6 ++ crates/squawk_ide/src/symbols.rs | 1 + crates/squawk_server/src/lib.rs | 1 + crates/squawk_wasm/src/lib.rs | 1 + 9 files changed, 218 insertions(+) diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 511b6448..c33b79dd 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -192,6 +192,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { ast::Stmt::CreateDatabase(create_database) => bind_create_database(b, create_database), ast::Stmt::CreateServer(create_server) => bind_create_server(b, create_server), ast::Stmt::CreateExtension(create_extension) => bind_create_extension(b, create_extension), + ast::Stmt::Declare(declare) => bind_declare_cursor(b, declare), ast::Stmt::Set(set) => bind_set(b, set), _ => {} } @@ -548,6 +549,25 @@ fn bind_create_extension(b: &mut Binder, create_extension: ast::CreateExtension) b.scopes[root].insert(extension_name, extension_id); } +fn bind_declare_cursor(b: &mut Binder, declare: ast::Declare) { + let Some(name) = declare.name() else { + return; + }; + + let cursor_name = Name::from_node(&name); + let name_ptr = SyntaxNodePtr::new(name.syntax()); + + let cursor_id = b.symbols.alloc(Symbol { + kind: SymbolKind::Cursor, + ptr: name_ptr, + schema: None, + params: None, + }); + + let root = b.root_scope(); + b.scopes[root].insert(cursor_name, cursor_id); +} + fn item_name(path: &ast::Path) -> Option { let segment = path.segment()?; diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index e6b8e297..15aa503f 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -92,6 +92,7 @@ pub(crate) enum NameRefClass { ReindexSystem, AttachPartition, NamedArgParameter, + Cursor, } fn is_special_fn(kind: SyntaxKind) -> bool { @@ -354,6 +355,13 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::SchemaAuthorization::can_cast(ancestor.kind()) { in_schema_authorization = true; } + if ast::Fetch::can_cast(ancestor.kind()) + || ast::Move::can_cast(ancestor.kind()) + || ast::Close::can_cast(ancestor.kind()) + || ast::WhereCurrentOf::can_cast(ancestor.kind()) + { + return Some(NameRefClass::Cursor); + } if ast::DropTable::can_cast(ancestor.kind()) { return Some(NameRefClass::DropTable); } @@ -715,6 +723,7 @@ pub(crate) enum NameClass { name: ast::Name, }, CreateView(ast::CreateView), + DeclareCursor(ast::Declare), } pub(crate) fn classify_name(name: &ast::Name) -> Option { @@ -778,6 +787,9 @@ pub(crate) fn classify_name(name: &ast::Name) -> Option { } return Some(NameClass::CreateView(create_view)); } + if let Some(declare) = ast::Declare::cast(ancestor.clone()) { + return Some(NameClass::DeclareCursor(declare)); + } } if let Some(with_table) = with_table_parent { diff --git a/crates/squawk_ide/src/document_symbols.rs b/crates/squawk_ide/src/document_symbols.rs index b7fbcb21..0791f4e2 100644 --- a/crates/squawk_ide/src/document_symbols.rs +++ b/crates/squawk_ide/src/document_symbols.rs @@ -20,6 +20,7 @@ pub enum DocumentSymbolKind { Enum, Column, Variant, + Cursor, } #[derive(Debug)] @@ -81,6 +82,11 @@ pub fn document_symbols(file: &ast::SourceFile) -> Vec { symbols.push(symbol); } } + ast::Stmt::Declare(declare) => { + if let Some(symbol) = create_declare_cursor_symbol(declare) { + symbols.push(symbol); + } + } ast::Stmt::Select(select) => { symbols.extend(cte_table_symbols(select)); } @@ -421,6 +427,23 @@ fn create_variant_symbol(variant: ast::Variant) -> Option { }) } +fn create_declare_cursor_symbol(declare: ast::Declare) -> Option { + let name_node = declare.name()?; + let name = name_node.syntax().text().to_string(); + + let full_range = declare.syntax().text_range(); + let focus_range = name_node.syntax().text_range(); + + Some(DocumentSymbol { + name, + detail: None, + kind: DocumentSymbolKind::Cursor, + full_range, + focus_range, + children: vec![], + }) +} + #[cfg(test)] mod tests { use super::*; @@ -470,6 +493,7 @@ mod tests { DocumentSymbolKind::Enum => "enum", DocumentSymbolKind::Column => "column", DocumentSymbolKind::Variant => "variant", + DocumentSymbolKind::Cursor => "cursor", }; let title = if let Some(detail) = &symbol.detail { @@ -867,6 +891,21 @@ create function my_schema.hello() returns void as $$ select 1; $$ language sql; ); } + #[test] + fn declare_cursor() { + assert_snapshot!(symbols(" +declare c scroll cursor for select * from t; +"), @r" + info: cursor: c + ╭▸ + 2 │ declare c scroll cursor for select * from t; + │ ┬───────┯────────────────────────────────── + │ │ │ + │ │ 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 66cbe4c9..9a247d7a 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -302,6 +302,76 @@ select c[b]$0 from t; "); } + #[test] + fn goto_fetch_cursor() { + assert_snapshot!(goto(" +declare c scroll cursor for select * from t; +fetch forward 5 from c$0; +"), @r" + ╭▸ + 2 │ declare c scroll cursor for select * from t; + │ ─ 2. destination + 3 │ fetch forward 5 from c; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_close_cursor() { + assert_snapshot!(goto(" +declare c scroll cursor for select * from t; +close c$0; +"), @r" + ╭▸ + 2 │ declare c scroll cursor for select * from t; + │ ─ 2. destination + 3 │ close c; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_move_cursor() { + assert_snapshot!(goto(" +declare c scroll cursor for select * from t; +move forward 10 from c$0; +"), @r" + ╭▸ + 2 │ declare c scroll cursor for select * from t; + │ ─ 2. destination + 3 │ move forward 10 from c; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_delete_where_current_of_cursor() { + assert_snapshot!(goto(" +declare c scroll cursor for select * from t; +delete from t where current of c$0; +"), @r" + ╭▸ + 2 │ declare c scroll cursor for select * from t; + │ ─ 2. destination + 3 │ delete from t where current of c; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_update_where_current_of_cursor() { + assert_snapshot!(goto(" +declare c scroll cursor for select * from t; +update t set a = a + 10 where current of c$0; +"), @r" + ╭▸ + 2 │ declare c scroll cursor for select * from t; + │ ─ 2. destination + 3 │ update t set a = a + 10 where current of c; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_with_table_star() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 548236b6..14bf5f98 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -166,6 +166,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameRefClass::NamedArgParameter => { return hover_named_arg_parameter(root, &name_ref, &binder); } + NameRefClass::Cursor => { + return hover_cursor(root, &name_ref, &binder); + } } } @@ -219,6 +222,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameClass::CreateView(create_view) => { return format_create_view(&create_view, &binder); } + NameClass::DeclareCursor(declare) => { + return format_declare_cursor(&declare); + } } } @@ -775,6 +781,19 @@ fn hover_extension( Some(format!("extension {}", extension_name_node.text())) } +fn hover_cursor( + root: &SyntaxNode, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let cursor_ptr = resolve::resolve_name_ref(binder, root, name_ref)? + .into_iter() + .next()?; + let cursor_name_node = cursor_ptr.to_node(root); + let declare = cursor_name_node.ancestors().find_map(ast::Declare::cast)?; + format_declare_cursor(&declare) +} + fn hover_type( root: &SyntaxNode, name_ref: &ast::NameRef, @@ -791,6 +810,16 @@ fn hover_type( format_create_type(&create_type, binder) } +fn format_declare_cursor(declare: &ast::Declare) -> Option { + let name = declare.name()?; + let query = declare.query()?; + Some(format!( + "cursor {} for {}", + name.syntax().text(), + query.syntax().text() + )) +} + fn format_create_table( create_table: &impl ast::HasCreateTable, binder: &binder::Binder, @@ -3945,4 +3974,43 @@ alter extension my$0ext update to '2.0'; ╰╴ ─ hover "); } + + #[test] + fn hover_on_fetch_cursor() { + assert_snapshot!(check_hover(" +declare c scroll cursor for select * from t; +fetch forward 5 from c$0; +"), @r" + hover: cursor c for select * from t + ╭▸ + 3 │ fetch forward 5 from c; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_close_cursor() { + assert_snapshot!(check_hover(" +declare c scroll cursor for select * from t; +close c$0; +"), @r" + hover: cursor c for select * from t + ╭▸ + 3 │ close c; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_move_cursor() { + assert_snapshot!(check_hover(" +declare c scroll cursor for select * from t; +move forward 10 from c$0; +"), @r" + hover: cursor c for select * from t + ╭▸ + 3 │ move forward 10 from c; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 8648bdc9..da0a5242 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -58,6 +58,12 @@ pub(crate) fn resolve_name_ref( let param_ptr = find_param_in_func_def(root, function_ptr, ¶m_name)?; Some(smallvec![param_ptr]) } + NameRefClass::Cursor => { + let cursor_name = Name::from_node(name_ref); + binder + .lookup(&cursor_name, SymbolKind::Cursor) + .map(|ptr| smallvec![ptr]) + } NameRefClass::SelectFromTable | NameRefClass::UpdateFromTable | NameRefClass::MergeUsingTable diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index 6c9e83fc..79c06f2f 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -53,6 +53,7 @@ pub(crate) enum SymbolKind { Type, View, Sequence, + Cursor, Tablespace, Database, Server, diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index cbf08643..840e7310 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -377,6 +377,7 @@ fn handle_document_symbol( DocumentSymbolKind::Enum => SymbolKind::ENUM, DocumentSymbolKind::Column => SymbolKind::FIELD, DocumentSymbolKind::Variant => SymbolKind::ENUM_MEMBER, + DocumentSymbolKind::Cursor => SymbolKind::VARIABLE, }, tags: None, range, diff --git a/crates/squawk_wasm/src/lib.rs b/crates/squawk_wasm/src/lib.rs index 62570506..93de7b90 100644 --- a/crates/squawk_wasm/src/lib.rs +++ b/crates/squawk_wasm/src/lib.rs @@ -410,6 +410,7 @@ fn convert_document_symbol( squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum", squawk_ide::document_symbols::DocumentSymbolKind::Column => "column", squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant", + squawk_ide::document_symbols::DocumentSymbolKind::Cursor => "cursor", } .to_string(), start_line: full_start_wide.line,