Skip to content

Commit 8e6da9d

Browse files
feat(hover): hover on schemas (#514)
<img width="555" height="380" alt="Screenshot 2025-09-13 at 08 37 11" src="https://github.com/user-attachments/assets/ad449ec2-6bac-401d-86c5-eeb22360aeaf" />
1 parent 8740f3a commit 8e6da9d

File tree

13 files changed

+305
-33
lines changed

13 files changed

+305
-33
lines changed

.sqlx/query-36862f7f9d2d1c50ba253b28a7648e76ff7e255960a4ce5466674ff35a97b151.json

Lines changed: 0 additions & 32 deletions
This file was deleted.

.sqlx/query-c775d3eaa6e95504de411dd5c2433a1121d69c7dca3c0e5e202835d9fefaa7cc.json

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pgt_hover/src/hoverables/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdow
33
mod column;
44
mod function;
55
mod role;
6+
mod schema;
67
mod table;
78

89
mod test_helper;
@@ -14,6 +15,13 @@ pub enum Hoverable<'a> {
1415
Column(&'a pgt_schema_cache::Column),
1516
Function(&'a pgt_schema_cache::Function),
1617
Role(&'a pgt_schema_cache::Role),
18+
Schema(&'a pgt_schema_cache::Schema),
19+
}
20+
21+
impl<'a> From<&'a pgt_schema_cache::Schema> for Hoverable<'a> {
22+
fn from(value: &'a pgt_schema_cache::Schema) -> Self {
23+
Hoverable::Schema(value)
24+
}
1725
}
1826

1927
impl<'a> From<&'a pgt_schema_cache::Table> for Hoverable<'a> {
@@ -47,6 +55,7 @@ impl ContextualPriority for Hoverable<'_> {
4755
Hoverable::Column(column) => column.relevance_score(ctx),
4856
Hoverable::Function(function) => function.relevance_score(ctx),
4957
Hoverable::Role(role) => role.relevance_score(ctx),
58+
Hoverable::Schema(schema) => schema.relevance_score(ctx),
5059
}
5160
}
5261
}
@@ -58,6 +67,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
5867
Hoverable::Column(column) => ToHoverMarkdown::hover_headline(*column, writer),
5968
Hoverable::Function(function) => ToHoverMarkdown::hover_headline(*function, writer),
6069
Hoverable::Role(role) => ToHoverMarkdown::hover_headline(*role, writer),
70+
Hoverable::Schema(schema) => ToHoverMarkdown::hover_headline(*schema, writer),
6171
}
6272
}
6373

@@ -67,6 +77,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
6777
Hoverable::Column(column) => ToHoverMarkdown::hover_body(*column, writer),
6878
Hoverable::Function(function) => ToHoverMarkdown::hover_body(*function, writer),
6979
Hoverable::Role(role) => ToHoverMarkdown::hover_body(*role, writer),
80+
Hoverable::Schema(schema) => ToHoverMarkdown::hover_body(*schema, writer),
7081
}
7182
}
7283

@@ -76,6 +87,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
7687
Hoverable::Column(column) => ToHoverMarkdown::hover_footer(*column, writer),
7788
Hoverable::Function(function) => ToHoverMarkdown::hover_footer(*function, writer),
7889
Hoverable::Role(role) => ToHoverMarkdown::hover_footer(*role, writer),
90+
Hoverable::Schema(schema) => ToHoverMarkdown::hover_footer(*schema, writer),
7991
}
8092
}
8193

@@ -85,6 +97,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
8597
Hoverable::Column(column) => column.body_markdown_type(),
8698
Hoverable::Function(function) => function.body_markdown_type(),
8799
Hoverable::Role(role) => role.body_markdown_type(),
100+
Hoverable::Schema(schema) => schema.body_markdown_type(),
88101
}
89102
}
90103

@@ -94,6 +107,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
94107
Hoverable::Column(column) => column.footer_markdown_type(),
95108
Hoverable::Function(function) => function.footer_markdown_type(),
96109
Hoverable::Role(role) => role.footer_markdown_type(),
110+
Hoverable::Schema(schema) => schema.footer_markdown_type(),
97111
}
98112
}
99113
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use std::fmt::Write;
2+
3+
use pgt_schema_cache::Schema;
4+
use pgt_treesitter::TreesitterContext;
5+
6+
use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown};
7+
8+
impl ToHoverMarkdown for Schema {
9+
fn hover_headline<W: Write>(&self, writer: &mut W) -> Result<(), std::fmt::Error> {
10+
write!(writer, "`{}` - owned by {}", self.name, self.owner)?;
11+
12+
Ok(())
13+
}
14+
15+
fn hover_body<W: Write>(&self, writer: &mut W) -> Result<bool, std::fmt::Error> {
16+
if let Some(comment) = &self.comment {
17+
write!(writer, "Comment: '{}'", comment)?;
18+
writeln!(writer)?;
19+
writeln!(writer)?;
20+
}
21+
22+
if !self.allowed_creators.is_empty() {
23+
write!(writer, "CREATE privileges:")?;
24+
writeln!(writer)?;
25+
26+
for creator in &self.allowed_creators {
27+
write!(writer, "- {}", creator)?;
28+
writeln!(writer)?;
29+
}
30+
31+
writeln!(writer)?;
32+
}
33+
34+
if !self.allowed_users.is_empty() {
35+
write!(writer, "USAGE privileges:")?;
36+
writeln!(writer)?;
37+
38+
for user in &self.allowed_users {
39+
write!(writer, "- {}", user)?;
40+
writeln!(writer)?;
41+
}
42+
43+
writeln!(writer)?;
44+
}
45+
46+
Ok(true)
47+
}
48+
49+
fn hover_footer<W: Write>(&self, writer: &mut W) -> Result<bool, std::fmt::Error> {
50+
writeln!(writer)?;
51+
write!(
52+
writer,
53+
"~{}, {} tables, {} views, {} functions",
54+
self.total_size, self.table_count, self.view_count, self.function_count,
55+
)?;
56+
Ok(true)
57+
}
58+
}
59+
60+
impl ContextualPriority for Schema {
61+
// there are no schemas with duplicate names.
62+
fn relevance_score(&self, _ctx: &TreesitterContext) -> f32 {
63+
0.0
64+
}
65+
}

crates/pgt_hover/src/hovered_node.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ impl HoveredNode {
2626

2727
match under_cursor.kind() {
2828
"identifier" if ctx.matches_ancestor_history(&["relation", "object_reference"]) => {
29+
let num_sibs = ctx.num_siblings();
30+
if ctx.node_under_cursor_is_nth_child(1) && num_sibs > 0 {
31+
return Some(HoveredNode::Schema(NodeIdentification::Name(node_content)));
32+
}
33+
2934
if let Some(schema) = ctx.schema_or_alias_name.as_ref() {
3035
Some(HoveredNode::Table(NodeIdentification::SchemaAndName((
3136
schema.clone(),
@@ -60,6 +65,7 @@ impl HoveredNode {
6065
"identifier" if ctx.matches_ancestor_history(&["alter_role"]) => {
6166
Some(HoveredNode::Role(NodeIdentification::Name(node_content)))
6267
}
68+
6369
"revoke_role" | "grant_role" | "policy_role" => {
6470
Some(HoveredNode::Role(NodeIdentification::Name(node_content)))
6571
}

crates/pgt_hover/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ pub fn on_hover(params: OnHoverParams) -> Vec<String> {
107107
hovered_node::NodeIdentification::SchemaAndTableAndName(_) => vec![],
108108
},
109109

110+
HoveredNode::Schema(node_identification) => match node_identification {
111+
hovered_node::NodeIdentification::Name(schema_name) => params
112+
.schema_cache
113+
.find_schema(&schema_name)
114+
.map(Hoverable::from)
115+
.map(|s| vec![s])
116+
.unwrap_or_default(),
117+
118+
_ => vec![],
119+
},
120+
110121
_ => todo!(),
111122
};
112123

crates/pgt_hover/tests/hover_integration_tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,25 @@ async fn test_column_hover_with_quoted_column_name_with_table(test_db: PgPool) {
443443
.await;
444444
}
445445

446+
#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")]
447+
async fn hover_on_schemas(test_db: PgPool) {
448+
let setup = r#"
449+
create schema auth;
450+
451+
create table auth.users (
452+
id serial primary key,
453+
email varchar(255) not null
454+
);
455+
"#;
456+
457+
let query = format!(
458+
r#"select * from au{}th.users;"#,
459+
QueryWithCursorPosition::cursor_marker()
460+
);
461+
462+
test_hover_at_cursor("hover_on_schemas", query, Some(setup), &test_db).await;
463+
}
464+
446465
#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")]
447466
async fn test_policy_table_hover(test_db: PgPool) {
448467
let setup = r#"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
source: crates/pgt_hover/tests/hover_integration_tests.rs
3+
expression: snapshot
4+
---
5+
# Input
6+
```sql
7+
select * from auth.users;
8+
↑ hovered here
9+
```
10+
11+
# Hover Results
12+
### `auth` - owned by postgres
13+
```plain
14+
15+
```
16+
---
17+
```plain
18+
19+
~16 kB, 1 tables, 0 views, 0 functions
20+
```

crates/pgt_schema_cache/src/queries/schemas.sql

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
11
select
22
n.oid :: int8 as "id!",
33
n.nspname as name,
4-
u.rolname as "owner!"
4+
u.rolname as "owner!",
5+
obj_description(n.oid, 'pg_namespace') as "comment",
6+
7+
coalesce((
8+
select array_agg(grantee::regrole::text)
9+
from aclexplode(n.nspacl)
10+
where privilege_type = 'USAGE'
11+
and grantee::regrole::text <> ''
12+
and grantee::regrole::text <> '-'
13+
), ARRAY[]::text[]) as "allowed_users!",
14+
15+
coalesce((
16+
select array_agg(grantee::regrole::text)
17+
from aclexplode(n.nspacl)
18+
where privilege_type = 'CREATE'
19+
and grantee::regrole::text <> ''
20+
and grantee::regrole::text <> '-'
21+
), ARRAY[]::text[]) as "allowed_creators!",
22+
23+
(select count(*) from pg_class c where c.relnamespace = n.oid and c.relkind = 'r') as "table_count!",
24+
(select count(*) from pg_class c where c.relnamespace = n.oid and c.relkind = 'v') as "view_count!",
25+
(select count(*) from pg_proc p where p.pronamespace = n.oid) as "function_count!",
26+
27+
coalesce(
28+
(select pg_size_pretty(sum(pg_total_relation_size(c.oid)))
29+
from pg_class c
30+
where c.relnamespace = n.oid and c.relkind in ('r', 'i', 'm')),
31+
'0 bytes'
32+
) as "total_size!"
533
from
634
pg_namespace n,
735
pg_roles u

crates/pgt_schema_cache/src/schema_cache.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ impl SchemaCache {
6363
})
6464
}
6565

66+
pub fn find_schema(&self, name: &str) -> Option<&Schema> {
67+
let sanitized_name = Self::sanitize_identifier(name);
68+
self.schemas.iter().find(|s| s.name == sanitized_name)
69+
}
70+
6671
pub fn find_tables(&self, name: &str, schema: Option<&str>) -> Vec<&Table> {
6772
let sanitized_name = Self::sanitize_identifier(name);
6873
self.tables

0 commit comments

Comments
 (0)