Skip to content

Commit baaaa56

Browse files
feat: show columns on table hover (#700)
Fixes [this issue](#665) which it turns out wasn't a bug but rather a missing feature :)
1 parent 9efd454 commit baaaa56

11 files changed

Lines changed: 246 additions & 23 deletions

crates/pgls_hover/src/hoverables/table.rs

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use pgls_treesitter::TreesitterContext;
66

77
use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown};
88

9+
const MAX_COLUMNS_IN_HOVER: usize = 20;
10+
911
impl ToHoverMarkdown for Table {
1012
fn hover_headline<W: Write>(
1113
&self,
@@ -37,15 +39,61 @@ impl ToHoverMarkdown for Table {
3739
fn hover_body<W: Write>(
3840
&self,
3941
writer: &mut W,
40-
_schema_cache: &SchemaCache,
42+
schema_cache: &SchemaCache,
4143
) -> Result<bool, std::fmt::Error> {
4244
if let Some(comment) = &self.comment {
4345
write!(writer, "Comment: '{comment}'")?;
4446
writeln!(writer)?;
45-
Ok(true)
46-
} else {
47-
Ok(false)
4847
}
48+
49+
let mut columns: Vec<_> = schema_cache
50+
.columns
51+
.iter()
52+
.filter(|column| column.schema_name == self.schema && column.table_name == self.name)
53+
.collect();
54+
columns.sort_by_key(|column| column.number);
55+
56+
writeln!(writer, "Columns:")?;
57+
58+
for column in columns.iter().take(MAX_COLUMNS_IN_HOVER) {
59+
write!(writer, "- {}: ", column.name)?;
60+
61+
if let Some(type_name) = &column.type_name {
62+
write!(writer, "{type_name}")?;
63+
64+
if let Some(varchar_length) = column.varchar_length {
65+
write!(writer, "({varchar_length})")?;
66+
}
67+
} else {
68+
write!(writer, "typeid:{}", column.type_id)?;
69+
}
70+
71+
if column.is_nullable {
72+
write!(writer, " - nullable")?;
73+
} else {
74+
write!(writer, " - not null")?;
75+
}
76+
77+
if let Some(default_expr) = column
78+
.default_expr
79+
.as_deref()
80+
.and_then(extract_basic_default_literal)
81+
{
82+
write!(writer, " - default: {default_expr}")?;
83+
}
84+
85+
writeln!(writer)?;
86+
}
87+
88+
if columns.len() > MAX_COLUMNS_IN_HOVER {
89+
writeln!(
90+
writer,
91+
"... +{} more columns",
92+
columns.len() - MAX_COLUMNS_IN_HOVER
93+
)?;
94+
}
95+
96+
Ok(true)
4997
}
5098

5199
fn hover_footer<W: Write>(
@@ -65,6 +113,45 @@ impl ToHoverMarkdown for Table {
65113
}
66114
}
67115

116+
// `extract_basic_default_literal` will extract simple default literals for table hover.
117+
// Example: `'anonymous'::text` -> `anonymous`, `(42)::int8` -> `42`, `now()` -> ignored.
118+
fn extract_basic_default_literal(default_expr: &str) -> Option<String> {
119+
let mut cast_parts = default_expr.split("::");
120+
let mut value = cast_parts.next().unwrap_or(default_expr).trim();
121+
122+
if cast_parts.any(|cast_part| !is_type_cast_fragment(cast_part)) {
123+
return None;
124+
}
125+
126+
while value.starts_with('(') && value.ends_with(')') && value.len() > 1 {
127+
value = value[1..value.len() - 1].trim();
128+
}
129+
130+
if value.starts_with('\'') && value.ends_with('\'') && value.len() > 1 {
131+
value = &value[1..value.len() - 1];
132+
}
133+
134+
let value = value.trim();
135+
if value.is_empty() || !contains_only_basic_chars(value) {
136+
return None;
137+
}
138+
139+
Some(value.to_string())
140+
}
141+
142+
fn contains_only_basic_chars(value: &str) -> bool {
143+
value
144+
.chars()
145+
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '_' | '-' | '.'))
146+
}
147+
148+
fn is_type_cast_fragment(value: &str) -> bool {
149+
value.trim().chars().all(|c| {
150+
c.is_ascii_alphanumeric()
151+
|| matches!(c, ' ' | '_' | '.' | '"' | '[' | ']' | '(' | ')' | ',')
152+
})
153+
}
154+
68155
impl ContextualPriority for Table {
69156
fn relevance_score(&self, ctx: &TreesitterContext) -> f32 {
70157
let mut score = 0.0;
@@ -93,3 +180,42 @@ impl ContextualPriority for Table {
93180
score
94181
}
95182
}
183+
184+
#[cfg(test)]
185+
mod tests {
186+
use super::extract_basic_default_literal;
187+
188+
#[test]
189+
fn extracts_basic_defaults_with_optional_casts() {
190+
assert_eq!(
191+
extract_basic_default_literal("'anonymous'::text"),
192+
Some("anonymous".to_string())
193+
);
194+
assert_eq!(
195+
extract_basic_default_literal("(42)::int8"),
196+
Some("42".to_string())
197+
);
198+
assert_eq!(
199+
extract_basic_default_literal("NULL::character varying"),
200+
Some("NULL".to_string())
201+
);
202+
assert_eq!(
203+
extract_basic_default_literal("false::boolean"),
204+
Some("false".to_string())
205+
);
206+
}
207+
208+
#[test]
209+
fn ignores_non_basic_defaults() {
210+
assert_eq!(
211+
extract_basic_default_literal("nextval('users_id_seq'::regclass)"),
212+
None
213+
);
214+
assert_eq!(extract_basic_default_literal("now()"), None);
215+
assert_eq!(
216+
extract_basic_default_literal("'a'::text || 'b'::text"),
217+
None
218+
);
219+
assert_eq!(extract_basic_default_literal("'with@symbol'::text"), None);
220+
}
221+
}

crates/pgls_hover/tests/hover_integration_tests.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,49 @@ async fn test_table_hover_works(test_db: PgPool) {
200200
test_hover_at_cursor("table_hover", query, Some(setup), &test_db).await;
201201
}
202202

203+
#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
204+
async fn test_table_hover_select_star(test_db: PgPool) {
205+
let setup = r#"
206+
create table users (
207+
id serial primary key,
208+
email varchar(255) not null
209+
);
210+
"#;
211+
212+
let query = format!(
213+
"select * from use{}rs",
214+
QueryWithCursorPosition::cursor_marker()
215+
);
216+
217+
test_hover_at_cursor("table_hover_select_star", query, Some(setup), &test_db).await;
218+
}
219+
220+
#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
221+
async fn test_table_hover_shows_nullable_and_basic_defaults(test_db: PgPool) {
222+
let setup = r#"
223+
create table users (
224+
id serial primary key,
225+
name text default 'anonymous',
226+
score int default 0,
227+
enabled bool default false,
228+
created_at timestamptz default now()
229+
);
230+
"#;
231+
232+
let query = format!(
233+
"select * from use{}rs",
234+
QueryWithCursorPosition::cursor_marker()
235+
);
236+
237+
test_hover_at_cursor(
238+
"table_hover_nullable_and_basic_defaults",
239+
query,
240+
Some(setup),
241+
&test_db,
242+
)
243+
.await;
244+
}
245+
203246
#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
204247
async fn test_no_hover_on_keyword(test_db: PgPool) {
205248
let setup = r#"

crates/pgls_hover/tests/snapshots/create_policy.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/pgt_hover/tests/hover_integration_tests.rs
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
33
expression: snapshot
44
---
55
# Input
@@ -11,6 +11,9 @@ create policy "my cool pol" on users for all to public with check (true);
1111
# Hover Results
1212
### `public.users` - 🔓 RLS disabled
1313
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- name: text - nullable
1417
1518
```
1619
---

crates/pgls_hover/tests/snapshots/grant_select.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/pgt_hover/tests/hover_integration_tests.rs
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
33
expression: snapshot
44
---
55
# Input
@@ -11,6 +11,9 @@ grant select on users to public;
1111
# Hover Results
1212
### `public.users` - 🔓 RLS disabled
1313
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- name: text - nullable
1417
1518
```
1619
---

crates/pgls_hover/tests/snapshots/revoke_select.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/pgt_hover/tests/hover_integration_tests.rs
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
33
expression: snapshot
44
---
55
# Input
@@ -11,6 +11,9 @@ revoke select on users from public;
1111
# Hover Results
1212
### `public.users` - 🔓 RLS disabled
1313
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- name: text - nullable
1417
1518
```
1619
---

crates/pgls_hover/tests/snapshots/table_hover.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/pgt_hover/tests/hover_integration_tests.rs
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
33
expression: snapshot
44
---
55
# Input
@@ -11,6 +11,9 @@ select id from users
1111
# Hover Results
1212
### `public.users` - 🔓 RLS disabled
1313
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- email: varchar(255) - not null
1417
1518
```
1619
---
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
3+
expression: snapshot
4+
---
5+
# Input
6+
```sql
7+
select * from users
8+
↑ hovered here
9+
```
10+
11+
# Hover Results
12+
### `public.users` - 🔓 RLS disabled
13+
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- name: text - nullable - default: anonymous
17+
- score: int4 - nullable - default: 0
18+
- enabled: bool - nullable - default: false
19+
- created_at: timestamptz - nullable
20+
21+
```
22+
---
23+
```plain
24+
25+
~0 rows, ~0 dead rows, 16.38 kB
26+
```

crates/pgls_hover/tests/snapshots/table_hover_quoted_schema.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/pgt_hover/tests/hover_integration_tests.rs
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
33
expression: snapshot
44
---
55
# Input
@@ -11,6 +11,9 @@ select * from "auth".users
1111
# Hover Results
1212
### `auth.users` - 🔓 RLS disabled
1313
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- email: varchar(255) - not null
1417
1518
```
1619
---

crates/pgls_hover/tests/snapshots/table_hover_quoted_table_name.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/pgt_hover/tests/hover_integration_tests.rs
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
33
expression: snapshot
44
---
55
# Input
@@ -11,6 +11,9 @@ select * from "auth"."users"
1111
# Hover Results
1212
### `auth.users` - 🔓 RLS disabled
1313
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- email: varchar(255) - not null
1417
1518
```
1619
---
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/pgls_hover/tests/hover_integration_tests.rs
3+
expression: snapshot
4+
---
5+
# Input
6+
```sql
7+
select * from users
8+
↑ hovered here
9+
```
10+
11+
# Hover Results
12+
### `public.users` - 🔓 RLS disabled
13+
```plain
14+
Columns:
15+
- id: int4 - not null
16+
- email: varchar(255) - not null
17+
18+
```
19+
---
20+
```plain
21+
22+
~0 rows, ~0 dead rows, 8.19 kB
23+
```

0 commit comments

Comments
 (0)