Skip to content

Commit a06ec07

Browse files
authored
ide: composite field goto def & hover (#810)
1 parent c889b6f commit a06ec07

File tree

4 files changed

+313
-0
lines changed

4 files changed

+313
-0
lines changed

crates/squawk_ide/src/classify.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub(crate) enum NameRefClass {
2020
SelectColumn,
2121
SelectQualifiedColumnTable,
2222
SelectQualifiedColumn,
23+
CompositeTypeField,
2324
InsertTable,
2425
InsertColumn,
2526
DeleteTable,
@@ -116,6 +117,8 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option<NameRefClass>
116117
&& matches!(base, ast::Expr::NameRef(_) | ast::Expr::FieldExpr(_))
117118
{
118119
return Some(NameRefClass::SelectQualifiedColumn);
120+
} else if let Some(ast::Expr::ParenExpr(_)) = field_expr.base() {
121+
return Some(NameRefClass::CompositeTypeField);
119122
} else {
120123
return Some(NameRefClass::SelectQualifiedColumnTable);
121124
}

crates/squawk_ide/src/goto_definition.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,22 @@ drop type person$0;
443443
");
444444
}
445445

446+
#[test]
447+
fn goto_composite_type_field() {
448+
assert_snapshot!(goto("
449+
create type person_info as (name text, email text);
450+
create table user(id int, member person_info);
451+
select (member).name$0 from user;
452+
"), @r"
453+
╭▸
454+
2 │ create type person_info as (name text, email text);
455+
│ ──── 2. destination
456+
3 │ create table user(id int, member person_info);
457+
4 │ select (member).name from user;
458+
╰╴ ─ 1. source
459+
");
460+
}
461+
446462
#[test]
447463
fn goto_drop_type_range() {
448464
assert_snapshot!(goto("
@@ -702,6 +718,114 @@ select x::public.baz$0;
702718
");
703719
}
704720

721+
#[test]
722+
fn goto_cast_composite_type() {
723+
assert_snapshot!(goto("
724+
create type person_info as (name varchar(50), age int);
725+
select ('Alice', 30)::person_info$0;
726+
"), @r"
727+
╭▸
728+
2 │ create type person_info as (name varchar(50), age int);
729+
│ ─────────── 2. destination
730+
3 │ select ('Alice', 30)::person_info;
731+
╰╴ ─ 1. source
732+
");
733+
}
734+
735+
#[test]
736+
fn goto_cast_composite_type_in_cte() {
737+
assert_snapshot!(goto("
738+
create type person_info as (name varchar(50), age int);
739+
with team as (
740+
select 1 as id, ('Alice', 30)::person_info$0 as member
741+
)
742+
select * from team;
743+
"), @r"
744+
╭▸
745+
2 │ create type person_info as (name varchar(50), age int);
746+
│ ─────────── 2. destination
747+
3 │ with team as (
748+
4 │ select 1 as id, ('Alice', 30)::person_info as member
749+
╰╴ ─ 1. source
750+
");
751+
}
752+
753+
#[test]
754+
fn goto_composite_type_field_name() {
755+
assert_snapshot!(goto("
756+
create type person_info as (name varchar(50), age int);
757+
with team as (
758+
select 1 as id, ('Alice', 30)::person_info as member
759+
)
760+
select (member).name$0, (member).age from team;
761+
"), @r"
762+
╭▸
763+
2 │ create type person_info as (name varchar(50), age int);
764+
│ ──── 2. destination
765+
766+
6 │ select (member).name, (member).age from team;
767+
╰╴ ─ 1. source
768+
");
769+
}
770+
771+
#[test]
772+
fn goto_composite_type_field_in_where() {
773+
assert_snapshot!(goto("
774+
create type person_info as (name varchar(50), age int);
775+
with team as (
776+
select 1 as id, ('Alice', 30)::person_info as member
777+
union all
778+
select 2, ('Bob', 25)::person_info
779+
)
780+
select (member).name, (member).age
781+
from team
782+
where (member).age$0 >= 18;
783+
"), @r"
784+
╭▸
785+
2 │ create type person_info as (name varchar(50), age int);
786+
│ ─── 2. destination
787+
788+
10 │ where (member).age >= 18;
789+
╰╴ ─ 1. source
790+
");
791+
}
792+
793+
#[test]
794+
fn goto_composite_type_field_base() {
795+
assert_snapshot!(goto("
796+
create type person_info as (name varchar(50), age int);
797+
with team as (
798+
select 1 as id, ('Alice', 30)::person_info as member
799+
)
800+
select (member$0).age from team;
801+
"), @r"
802+
╭▸
803+
4 │ select 1 as id, ('Alice', 30)::person_info as member
804+
│ ────── 2. destination
805+
5 │ )
806+
6 │ select (member).age from team;
807+
╰╴ ─ 1. source
808+
");
809+
}
810+
811+
#[test]
812+
fn goto_composite_type_field_nested_parens() {
813+
assert_snapshot!(goto("
814+
create type person_info as (name varchar(50), age int);
815+
with team as (
816+
select 1 as id, ('Alice', 30)::person_info as member
817+
)
818+
select ((((member))).name$0) from team;
819+
"), @r"
820+
╭▸
821+
2 │ create type person_info as (name varchar(50), age int);
822+
│ ──── 2. destination
823+
824+
6 │ select ((((member))).name) from team;
825+
╰╴ ─ 1. source
826+
");
827+
}
828+
705829
#[test]
706830
fn begin_to_rollback() {
707831
assert_snapshot!(goto(

crates/squawk_ide/src/hover.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option<String> {
2222
NameRefClass::TypeReference | NameRefClass::DropType => {
2323
return hover_type(file, &name_ref, &binder);
2424
}
25+
NameRefClass::CompositeTypeField => {
26+
return hover_composite_type_field(file, &name_ref, &binder);
27+
}
2528
NameRefClass::SelectColumn | NameRefClass::SelectQualifiedColumn => {
2629
// Try hover as column first
2730
if let Some(result) = hover_column(file, &name_ref, &binder) {
@@ -175,6 +178,41 @@ fn hover_column(
175178
))
176179
}
177180

181+
fn hover_composite_type_field(
182+
file: &ast::SourceFile,
183+
name_ref: &ast::NameRef,
184+
binder: &binder::Binder,
185+
) -> Option<String> {
186+
let field_ptr = resolve::resolve_name_ref(binder, name_ref)?;
187+
let root = file.syntax();
188+
let field_name_node = field_ptr.to_node(root);
189+
190+
let column = field_name_node.ancestors().find_map(ast::Column::cast)?;
191+
let field_name = column.name()?.syntax().text().to_string();
192+
let ty = column.ty()?;
193+
194+
let create_type = column
195+
.syntax()
196+
.ancestors()
197+
.find_map(ast::CreateType::cast)?;
198+
let type_path = create_type.path()?;
199+
let type_name = type_path.segment()?.name()?.syntax().text().to_string();
200+
201+
let schema = if let Some(qualifier) = type_path.qualifier() {
202+
qualifier.syntax().text().to_string()
203+
} else {
204+
type_schema(&create_type, binder)?
205+
};
206+
207+
Some(format!(
208+
"field {}.{}.{} {}",
209+
schema,
210+
type_name,
211+
field_name,
212+
ty.syntax().text()
213+
))
214+
}
215+
178216
fn hover_column_definition(
179217
create_table: &ast::CreateTable,
180218
column: &ast::Column,
@@ -2571,4 +2609,52 @@ drop view v$0;
25712609
╰╴ ─ hover
25722610
");
25732611
}
2612+
2613+
#[test]
2614+
fn hover_composite_type_field() {
2615+
assert_snapshot!(check_hover("
2616+
create type person_info as (name varchar(50), age int);
2617+
with team as (
2618+
select 1 as id, ('Alice', 30)::person_info as member
2619+
)
2620+
select (member).name$0, (member).age from team;
2621+
"), @r"
2622+
hover: field public.person_info.name varchar(50)
2623+
╭▸
2624+
6 │ select (member).name, (member).age from team;
2625+
╰╴ ─ hover
2626+
");
2627+
}
2628+
2629+
#[test]
2630+
fn hover_composite_type_field_age() {
2631+
assert_snapshot!(check_hover("
2632+
create type person_info as (name varchar(50), age int);
2633+
with team as (
2634+
select 1 as id, ('Alice', 30)::person_info as member
2635+
)
2636+
select (member).name, (member).age$0 from team;
2637+
"), @r"
2638+
hover: field public.person_info.age int
2639+
╭▸
2640+
6 │ select (member).name, (member).age from team;
2641+
╰╴ ─ hover
2642+
");
2643+
}
2644+
2645+
#[test]
2646+
fn hover_composite_type_field_nested_parens() {
2647+
assert_snapshot!(check_hover("
2648+
create type person_info as (name varchar(50), age int);
2649+
with team as (
2650+
select 1 as id, ('Alice', 30)::person_info as member
2651+
)
2652+
select ((((member))).name$0) from team;
2653+
"), @r"
2654+
hover: field public.person_info.name varchar(50)
2655+
╭▸
2656+
6 │ select ((((member))).name) from team;
2657+
╰╴ ─ hover
2658+
");
2659+
}
25742660
}

crates/squawk_ide/src/resolve.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti
213213
resolve_select_qualified_column_table(binder, name_ref)
214214
}
215215
NameRefClass::SelectQualifiedColumn => resolve_select_qualified_column(binder, name_ref),
216+
NameRefClass::CompositeTypeField => resolve_composite_type_field(binder, name_ref),
216217
NameRefClass::InsertColumn => resolve_insert_column(binder, name_ref),
217218
NameRefClass::DeleteWhereColumn => resolve_delete_where_column(binder, name_ref),
218219
NameRefClass::UpdateWhereColumn | NameRefClass::UpdateSetColumn => {
@@ -1341,3 +1342,102 @@ fn extract_param_signature(node: &impl ast::HasParamList) -> Option<Vec<Name>> {
13411342
}
13421343
(!params.is_empty()).then_some(params)
13431344
}
1345+
1346+
fn unwrap_paren_expr(expr: ast::Expr) -> Option<ast::NameRef> {
1347+
let mut current = expr;
1348+
for _ in 0..10_000 {
1349+
match current {
1350+
ast::Expr::ParenExpr(paren_expr) => {
1351+
current = paren_expr.expr()?;
1352+
}
1353+
ast::Expr::NameRef(nr) => return Some(nr),
1354+
_ => return None,
1355+
}
1356+
}
1357+
None
1358+
}
1359+
1360+
fn resolve_composite_type_field(binder: &Binder, name_ref: &ast::NameRef) -> Option<SyntaxNodePtr> {
1361+
let field_name = Name::from_node(name_ref);
1362+
let field_expr = name_ref.syntax().parent().and_then(ast::FieldExpr::cast)?;
1363+
let base = field_expr.base()?;
1364+
1365+
let base_name_ref = unwrap_paren_expr(base)?;
1366+
let root = &name_ref.syntax().ancestors().last()?;
1367+
1368+
let (type_name, schema) =
1369+
if let Some(type_info) = resolve_composite_type_from_column(binder, &base_name_ref, root) {
1370+
type_info
1371+
} else {
1372+
resolve_composite_type_from_cast(binder, &base_name_ref, root)?
1373+
};
1374+
1375+
let position = name_ref.syntax().text_range().start();
1376+
let type_ptr = resolve_type(binder, &type_name, &schema, position)?;
1377+
let type_node = type_ptr.to_node(root);
1378+
1379+
let create_type = type_node.ancestors().find_map(ast::CreateType::cast)?;
1380+
let column_list = create_type.column_list()?;
1381+
1382+
for column in column_list.columns() {
1383+
if let Some(col_name) = column.name()
1384+
&& Name::from_node(&col_name) == field_name
1385+
{
1386+
return Some(SyntaxNodePtr::new(col_name.syntax()));
1387+
}
1388+
}
1389+
1390+
None
1391+
}
1392+
1393+
fn resolve_composite_type_from_column(
1394+
binder: &Binder,
1395+
base_name_ref: &ast::NameRef,
1396+
root: &SyntaxNode,
1397+
) -> Option<(Name, Option<Schema>)> {
1398+
let column_ptr = resolve_select_column(binder, base_name_ref)?;
1399+
let column_node = column_ptr.to_node(root);
1400+
let column = column_node.ancestors().find_map(ast::Column::cast)?;
1401+
let ty = column.ty()?;
1402+
extract_type_name_and_schema(&ty)
1403+
}
1404+
1405+
fn resolve_composite_type_from_cast(
1406+
binder: &Binder,
1407+
base_name_ref: &ast::NameRef,
1408+
root: &SyntaxNode,
1409+
) -> Option<(Name, Option<Schema>)> {
1410+
let column_ptr = resolve_select_column(binder, base_name_ref)?;
1411+
let column_node = column_ptr.to_node(root);
1412+
let target = column_node.ancestors().find_map(ast::Target::cast)?;
1413+
let ast::Expr::CastExpr(cast_expr) = target.expr()? else {
1414+
return None;
1415+
};
1416+
let ty = cast_expr.ty()?;
1417+
extract_type_name_and_schema(&ty)
1418+
}
1419+
1420+
fn extract_type_name_and_schema(ty: &ast::Type) -> Option<(Name, Option<Schema>)> {
1421+
match ty {
1422+
ast::Type::PathType(path_type) => {
1423+
let path = path_type.path()?;
1424+
let type_name = extract_table_name(&path)?;
1425+
let schema = extract_schema_name(&path);
1426+
Some((type_name, schema))
1427+
}
1428+
ast::Type::ExprType(expr_type) => {
1429+
let expr = expr_type.expr()?;
1430+
if let ast::Expr::FieldExpr(field_expr) = expr
1431+
&& let Some(field) = field_expr.field()
1432+
&& let Some(ast::Expr::NameRef(schema_name_ref)) = field_expr.base()
1433+
{
1434+
let type_name = Name::from_node(&field);
1435+
let schema = Some(Schema(Name::from_node(&schema_name_ref)));
1436+
Some((type_name, schema))
1437+
} else {
1438+
None
1439+
}
1440+
}
1441+
_ => None,
1442+
}
1443+
}

0 commit comments

Comments
 (0)