Skip to content

Commit ec41f63

Browse files
committed
feat: hover crate
1 parent 1fb094f commit ec41f63

File tree

6 files changed

+178
-35
lines changed

6 files changed

+178
-35
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/hover/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ text-size = "1.1.1"
88
sql_parser.workspace = true
99
schema_cache.workspace = true
1010
tree-sitter.workspace = true
11+
tree_sitter_sql.workspace = true
1112

1213
[dev-dependencies]
1314

crates/hover/src/lib.rs

+27-24
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,47 @@
1+
mod resolve;
2+
3+
use resolve::Hoverable;
14
use schema_cache::SchemaCache;
25
use text_size::{TextRange, TextSize};
36

47
pub struct HoverParams {
58
pub position: text_size::TextSize,
9+
pub source: String,
610
pub ast: Option<sql_parser::EnrichedAst>,
711
pub tree: tree_sitter::Tree,
812
pub schema_cache: SchemaCache,
913
}
1014

15+
#[derive(Debug)]
1116
pub struct HoverResult {
1217
range: Option<TextRange>,
1318
content: String,
1419
}
1520

1621
pub fn hover(params: HoverParams) -> Option<HoverResult> {
17-
// if ast, find deepest node at position
18-
if params.ast.is_some() {
19-
let ast = params.ast.unwrap();
20-
let node = ast.covering_node(TextRange::empty(params.position));
21-
if node.is_some() {
22-
let node = node.unwrap();
23-
return Some(HoverResult {
24-
range: Some(node.range()),
25-
content: "Hover".to_string(),
26-
});
27-
}
28-
}
29-
// else, try to find ts node at position in tree
30-
// using: https://docs.rs/tree-sitter/0.22.6/tree_sitter/struct.TreeCursor.html#method.goto_first_child_for_byte
31-
// get byte offset from position
32-
let r = params.tree.root_node().named_descendant_for_byte_range(
33-
usize::from(params.position),
34-
usize::from(params.position),
35-
);
36-
if r.is_some() {
37-
let r = r.unwrap();
38-
match r.kind() {}
22+
let elem = if params.ast.is_some() {
23+
resolve::resolve_from_enriched_ast(params.position, params.ast.unwrap())
24+
} else {
25+
resolve::resolve_from_tree_sitter(params.position, params.tree, &params.source)
26+
};
27+
28+
if elem.is_none() {
29+
return None;
3930
}
4031

41-
// TODO: get table / column whatever COMMENT from schema cache
32+
match elem.unwrap() {
33+
Hoverable::Relation(r) => {
34+
let table = params.schema_cache.find_table(&r.name, r.schema.as_deref());
4235

43-
None
36+
table.map(|t| HoverResult {
37+
range: Some(r.range),
38+
content: if t.comment.is_some() {
39+
format!("{}\n{}", t.name, t.comment.as_ref().unwrap())
40+
} else {
41+
t.name.clone()
42+
},
43+
})
44+
}
45+
_ => None,
46+
}
4447
}

crates/hover/src/resolve.rs

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use sql_parser::{AstNode, EnrichedAst};
2+
use text_size::{TextRange, TextSize};
3+
use tree_sitter::Tree;
4+
5+
#[derive(Debug, Eq, PartialEq)]
6+
pub struct HoverableRelation {
7+
pub name: String,
8+
pub schema: Option<String>,
9+
pub range: TextRange,
10+
}
11+
12+
#[derive(Debug, Eq, PartialEq)]
13+
pub struct HoverableColumn {
14+
pub name: String,
15+
pub table: Option<String>,
16+
pub schema: Option<String>,
17+
pub range: TextRange,
18+
}
19+
20+
#[derive(Debug, Eq, PartialEq)]
21+
pub enum Hoverable {
22+
Relation(HoverableRelation),
23+
Column(HoverableColumn),
24+
}
25+
26+
pub fn resolve_from_enriched_ast(pos: TextSize, ast: EnrichedAst) -> Option<Hoverable> {
27+
let node = ast.covering_node(TextRange::empty(pos));
28+
29+
if node.is_none() {
30+
return None;
31+
}
32+
33+
let node = node.unwrap();
34+
35+
match node.node {
36+
AstNode::RangeVar(ref range_var) => Some(Hoverable::Relation(HoverableRelation {
37+
range: node.range(),
38+
name: range_var.relname.clone(),
39+
schema: if range_var.schemaname.is_empty() {
40+
None
41+
} else {
42+
Some(range_var.schemaname.clone())
43+
},
44+
})),
45+
_ => None,
46+
}
47+
}
48+
49+
pub fn resolve_from_tree_sitter(pos: TextSize, tree: Tree, source: &str) -> Option<Hoverable> {
50+
let r = tree
51+
.root_node()
52+
.named_descendant_for_byte_range(usize::from(pos), usize::from(pos));
53+
54+
if r.is_none() {
55+
return None;
56+
}
57+
58+
let mut node = r.unwrap();
59+
let node_range = node.range();
60+
61+
while node.parent().is_some() && node.parent().unwrap().range() == node_range {
62+
node = node.parent().unwrap();
63+
}
64+
65+
match node.kind() {
66+
"relation" => Some(Hoverable::Relation(HoverableRelation {
67+
range: TextRange::new(
68+
TextSize::try_from(node.range().start_byte).unwrap(),
69+
TextSize::try_from(node.range().end_byte).unwrap(),
70+
),
71+
name: node.utf8_text(source.as_bytes()).unwrap().to_string(),
72+
schema: None,
73+
})),
74+
_ => None,
75+
}
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use sql_parser::parse_ast;
81+
use text_size::{TextRange, TextSize};
82+
83+
use super::{Hoverable, HoverableRelation};
84+
85+
#[test]
86+
fn test_resolve_from_enriched_ast() {
87+
let input = "select id from contact;";
88+
let position = TextSize::new(15);
89+
90+
let root = sql_parser::parse_sql_statement(input).unwrap();
91+
let ast = parse_ast(input, &root).ast;
92+
93+
let hover = super::resolve_from_enriched_ast(position, ast);
94+
95+
assert!(hover.is_some());
96+
97+
assert_eq!(
98+
hover.unwrap(),
99+
Hoverable::Relation(HoverableRelation {
100+
range: TextRange::new(TextSize::new(15), TextSize::new(22)),
101+
name: "contact".to_string(),
102+
schema: None,
103+
})
104+
);
105+
}
106+
107+
#[test]
108+
fn test_resolve_from_tree_sitter() {
109+
let input = "select id from contact;";
110+
let position = TextSize::new(15);
111+
112+
let mut parser = tree_sitter::Parser::new();
113+
parser
114+
.set_language(tree_sitter_sql::language())
115+
.expect("Error loading sql language");
116+
117+
let tree = parser.parse(input, None).unwrap();
118+
119+
let hover = super::resolve_from_tree_sitter(position, tree, input);
120+
121+
assert!(hover.is_some());
122+
123+
assert_eq!(
124+
hover.unwrap(),
125+
Hoverable::Relation(HoverableRelation {
126+
range: TextRange::new(TextSize::new(15), TextSize::new(22)),
127+
name: "contact".to_string(),
128+
schema: None,
129+
})
130+
);
131+
}
132+
}

crates/schema_cache/src/schema_cache.rs

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ impl SchemaCache {
2929
pub fn mutate(&mut self) {
3030
unimplemented!();
3131
}
32+
33+
pub fn find_table(&self, name: &str, schema: Option<&str>) -> Option<&Table> {
34+
self.tables
35+
.iter()
36+
.find(|t| t.name == name && t.schema == schema.unwrap_or("public"))
37+
}
3238
}
3339

3440
pub trait SchemaCacheItem {

crates/schema_cache/src/tables.rs

+11-11
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,17 @@ impl From<String> for ReplicaIdentity {
3030

3131
#[derive(Debug, Clone, Default)]
3232
pub struct Table {
33-
id: i64,
34-
schema: String,
35-
name: String,
36-
rls_enabled: bool,
37-
rls_forced: bool,
38-
replica_identity: ReplicaIdentity,
39-
bytes: i64,
40-
size: String,
41-
live_rows_estimate: i64,
42-
dead_rows_estimate: i64,
43-
comment: Option<String>,
33+
pub id: i64,
34+
pub schema: String,
35+
pub name: String,
36+
pub rls_enabled: bool,
37+
pub rls_forced: bool,
38+
pub replica_identity: ReplicaIdentity,
39+
pub bytes: i64,
40+
pub size: String,
41+
pub live_rows_estimate: i64,
42+
pub dead_rows_estimate: i64,
43+
pub comment: Option<String>,
4444
}
4545

4646
impl SchemaCacheItem for Table {

0 commit comments

Comments
 (0)