Skip to content

Commit 67b9150

Browse files
authored
ide: add hover for column in create index (#763)
1 parent 111e02c commit 67b9150

File tree

5 files changed

+364
-3
lines changed

5 files changed

+364
-3
lines changed

crates/squawk_ide/src/hover.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
use crate::binder;
2+
use crate::offsets::token_from_offset;
3+
use crate::resolve;
4+
use rowan::TextSize;
5+
use squawk_syntax::ast::{self, AstNode};
6+
7+
pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option<String> {
8+
let token = token_from_offset(file, offset)?;
9+
let parent = token.parent()?;
10+
11+
let name_ref = ast::NameRef::cast(parent)?;
12+
13+
if !is_column_ref(&name_ref) {
14+
return None;
15+
}
16+
17+
let column_name = name_ref.syntax().text().to_string();
18+
19+
let create_index = name_ref
20+
.syntax()
21+
.ancestors()
22+
.find_map(ast::CreateIndex::cast)?;
23+
24+
let relation_name = create_index.relation_name()?;
25+
let path = relation_name.path()?;
26+
27+
let binder = binder::bind(file);
28+
29+
let (schema, table_name) = resolve::resolve_table_info(&binder, &path)?;
30+
31+
let column_ptr = resolve::resolve_name_ref(&binder, &name_ref)?;
32+
33+
let root = file.syntax();
34+
let column_name_node = column_ptr.to_node(root);
35+
36+
let column = column_name_node.ancestors().find_map(ast::Column::cast)?;
37+
38+
let ty = column.ty()?;
39+
40+
Some(format!(
41+
"{schema}.{table_name}.{column_name} {}",
42+
ty.syntax().text()
43+
))
44+
}
45+
46+
fn is_column_ref(name_ref: &ast::NameRef) -> bool {
47+
for ancestor in name_ref.syntax().ancestors() {
48+
if ast::PartitionItem::can_cast(ancestor.kind()) {
49+
return true;
50+
}
51+
if ast::CreateIndex::can_cast(ancestor.kind()) {
52+
return true;
53+
}
54+
}
55+
false
56+
}
57+
58+
#[cfg(test)]
59+
mod test {
60+
use crate::hover::hover;
61+
use crate::test_utils::fixture;
62+
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
63+
use insta::assert_snapshot;
64+
use squawk_syntax::ast;
65+
66+
#[track_caller]
67+
fn check_hover(sql: &str) -> String {
68+
check_hover_(sql).expect("should find hover information")
69+
}
70+
71+
#[track_caller]
72+
fn check_hover_(sql: &str) -> Option<String> {
73+
let (mut offset, sql) = fixture(sql);
74+
offset = offset.checked_sub(1.into()).unwrap_or_default();
75+
let parse = ast::SourceFile::parse(&sql);
76+
assert_eq!(parse.errors(), vec![]);
77+
let file: ast::SourceFile = parse.tree();
78+
79+
if let Some(type_info) = hover(&file, offset) {
80+
let offset_usize: usize = offset.into();
81+
let title = format!("hover: {}", type_info);
82+
let group = Level::INFO.primary_title(&title).element(
83+
Snippet::source(&sql).fold(true).annotation(
84+
AnnotationKind::Context
85+
.span(offset_usize..offset_usize + 1)
86+
.label("hover"),
87+
),
88+
);
89+
let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
90+
return Some(
91+
renderer
92+
.render(&[group])
93+
.to_string()
94+
// neater
95+
.replace("info: hover:", "hover:"),
96+
);
97+
}
98+
None
99+
}
100+
101+
fn hover_not_found(sql: &str) {
102+
assert!(
103+
check_hover_(sql).is_none(),
104+
"Should not find hover information"
105+
);
106+
}
107+
108+
#[test]
109+
fn hover_column_in_create_index() {
110+
assert_snapshot!(check_hover("
111+
create table users(id int, email text);
112+
create index idx_email on users(email$0);
113+
"), @r"
114+
hover: public.users.email text
115+
╭▸
116+
3 │ create index idx_email on users(email);
117+
╰╴ ─ hover
118+
");
119+
}
120+
121+
#[test]
122+
fn hover_column_int_type() {
123+
assert_snapshot!(check_hover("
124+
create table users(id int, email text);
125+
create index idx_id on users(id$0);
126+
"), @r"
127+
hover: public.users.id int
128+
╭▸
129+
3 │ create index idx_id on users(id);
130+
╰╴ ─ hover
131+
");
132+
}
133+
134+
#[test]
135+
fn hover_column_with_schema() {
136+
assert_snapshot!(check_hover("
137+
create table public.users(id int, email text);
138+
create index idx_email on public.users(email$0);
139+
"), @r"
140+
hover: public.users.email text
141+
╭▸
142+
3 │ create index idx_email on public.users(email);
143+
╰╴ ─ hover
144+
");
145+
}
146+
147+
#[test]
148+
fn hover_column_temp_table() {
149+
assert_snapshot!(check_hover("
150+
create temp table users(id int, email text);
151+
create index idx_email on users(email$0);
152+
"), @r"
153+
hover: pg_temp.users.email text
154+
╭▸
155+
3 │ create index idx_email on users(email);
156+
╰╴ ─ hover
157+
");
158+
}
159+
160+
#[test]
161+
fn hover_column_multiple_columns() {
162+
assert_snapshot!(check_hover("
163+
create table users(id int, email text, name varchar(100));
164+
create index idx_users on users(id, email$0, name);
165+
"), @r"
166+
hover: public.users.email text
167+
╭▸
168+
3 │ create index idx_users on users(id, email, name);
169+
╰╴ ─ hover
170+
");
171+
}
172+
173+
#[test]
174+
fn hover_column_varchar() {
175+
assert_snapshot!(check_hover("
176+
create table users(id int, name varchar(100));
177+
create index idx_name on users(name$0);
178+
"), @r"
179+
hover: public.users.name varchar(100)
180+
╭▸
181+
3 │ create index idx_name on users(name);
182+
╰╴ ─ hover
183+
");
184+
}
185+
186+
#[test]
187+
fn hover_column_bigint() {
188+
assert_snapshot!(check_hover("
189+
create table metrics(value bigint);
190+
create index idx_value on metrics(value$0);
191+
"), @r"
192+
hover: public.metrics.value bigint
193+
╭▸
194+
3 │ create index idx_value on metrics(value);
195+
╰╴ ─ hover
196+
");
197+
}
198+
199+
#[test]
200+
fn hover_column_timestamp() {
201+
assert_snapshot!(check_hover("
202+
create table events(created_at timestamp with time zone);
203+
create index idx_created on events(created_at$0);
204+
"), @r"
205+
hover: public.events.created_at timestamp with time zone
206+
╭▸
207+
3 │ create index idx_created on events(created_at);
208+
╰╴ ─ hover
209+
");
210+
}
211+
212+
#[test]
213+
fn hover_column_with_search_path() {
214+
assert_snapshot!(check_hover(r#"
215+
set search_path to myschema;
216+
create table myschema.users(id int, email text);
217+
create index idx_email on users(email$0);
218+
"#), @r"
219+
hover: myschema.users.email text
220+
╭▸
221+
4 │ create index idx_email on users(email);
222+
╰╴ ─ hover
223+
");
224+
}
225+
226+
#[test]
227+
fn hover_column_explicit_schema_overrides_search_path() {
228+
assert_snapshot!(check_hover(r#"
229+
set search_path to myschema;
230+
create table public.users(id int, email text);
231+
create table myschema.users(value bigint);
232+
create index idx_email on public.users(email$0);
233+
"#), @r"
234+
hover: public.users.email text
235+
╭▸
236+
5 │ create index idx_email on public.users(email);
237+
╰╴ ─ hover
238+
");
239+
}
240+
241+
#[test]
242+
fn hover_not_on_table_name() {
243+
hover_not_found(
244+
"
245+
create table users(id int);
246+
create index idx on users$0(id);
247+
",
248+
);
249+
}
250+
251+
#[test]
252+
fn hover_not_on_index_name() {
253+
hover_not_found(
254+
"
255+
create table users(id int);
256+
create index idx$0 on users(id);
257+
",
258+
);
259+
}
260+
}

crates/squawk_ide/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod expand_selection;
55
pub mod find_references;
66
mod generated;
77
pub mod goto_definition;
8+
pub mod hover;
89
mod offsets;
910
mod resolve;
1011
mod scope;

crates/squawk_ide/src/resolve.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use squawk_syntax::{
55
};
66

77
use crate::binder::Binder;
8-
use crate::symbols::{Name, Schema, SymbolKind};
8+
pub(crate) use crate::symbols::Schema;
9+
use crate::symbols::{Name, SymbolKind};
910
use squawk_syntax::SyntaxNode;
1011

1112
#[derive(Debug)]
@@ -180,3 +181,52 @@ fn extract_schema_name(path: &ast::Path) -> Option<Schema> {
180181
.and_then(|s| s.name_ref())
181182
.map(|name_ref| Schema(Name::new(name_ref.syntax().text().to_string())))
182183
}
184+
185+
pub(crate) fn resolve_table_info(binder: &Binder, path: &ast::Path) -> Option<(Schema, String)> {
186+
let table_name_str = extract_table_name_from_path(path)?;
187+
let schema = extract_schema_from_path(path);
188+
189+
let table_name_normalized = Name::new(table_name_str.clone());
190+
let symbols = binder.scopes[binder.root_scope()].get(&table_name_normalized)?;
191+
192+
if let Some(schema_name) = schema {
193+
let schema_normalized = Schema::new(schema_name);
194+
let symbol_id = symbols.iter().copied().find(|id| {
195+
let symbol = &binder.symbols[*id];
196+
symbol.kind == SymbolKind::Table && symbol.schema == schema_normalized
197+
})?;
198+
let symbol = &binder.symbols[symbol_id];
199+
return Some((symbol.schema.clone(), table_name_str));
200+
} else {
201+
let position = path.syntax().text_range().start();
202+
let search_path = binder.search_path_at(position);
203+
for search_schema in search_path {
204+
if let Some(symbol_id) = symbols.iter().copied().find(|id| {
205+
let symbol = &binder.symbols[*id];
206+
symbol.kind == SymbolKind::Table && &symbol.schema == search_schema
207+
}) {
208+
let symbol = &binder.symbols[symbol_id];
209+
return Some((symbol.schema.clone(), table_name_str));
210+
}
211+
}
212+
}
213+
None
214+
}
215+
216+
fn extract_table_name_from_path(path: &ast::Path) -> Option<String> {
217+
let segment = path.segment()?;
218+
if let Some(name_ref) = segment.name_ref() {
219+
return Some(name_ref.syntax().text().to_string());
220+
}
221+
if let Some(name) = segment.name() {
222+
return Some(name.syntax().text().to_string());
223+
}
224+
None
225+
}
226+
227+
fn extract_schema_from_path(path: &ast::Path) -> Option<String> {
228+
path.qualifier()
229+
.and_then(|q| q.segment())
230+
.and_then(|s| s.name_ref())
231+
.map(|name_ref| name_ref.syntax().text().to_string())
232+
}

crates/squawk_ide/src/symbols.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use la_arena::Idx;
22
use smol_str::SmolStr;
33
use squawk_syntax::SyntaxNodePtr;
4+
use std::fmt;
45

56
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
67
pub(crate) struct Name(pub(crate) SmolStr);
@@ -14,6 +15,12 @@ impl Schema {
1415
}
1516
}
1617

18+
impl fmt::Display for Schema {
19+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20+
write!(f, "{}", self.0.0)
21+
}
22+
}
23+
1724
impl Name {
1825
pub(crate) fn new(text: impl Into<SmolStr>) -> Self {
1926
let text = text.into();

0 commit comments

Comments
 (0)