Skip to content

Commit 630ae1c

Browse files
authored
ide: show function comment on hover (#1070)
playground: <img width="524" height="136" alt="Screenshot 2026-04-16 at 7 45 05 PM" src="https://github.com/user-attachments/assets/1ad64d67-6206-4604-a8af-0be5db63b8cc" /> vscode: <img width="531" height="123" alt="Screenshot 2026-04-16 at 7 31 53 PM" src="https://github.com/user-attachments/assets/8e188d54-9a57-42ee-9925-9ffcf60b4828" />
1 parent 3b42de1 commit 630ae1c

9 files changed

Lines changed: 499 additions & 217 deletions

File tree

PLAN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ sql for benchmarks maybe?
194194

195195
https://github.com/earendil-works/absurd/blob/56500e5a23beca5e976f329475063f24692d99cc/sql/absurd.sql
196196

197+
- pg_6502
198+
199+
https://github.com/lasect/pg_6502
200+
197201
### CLI
198202

199203
from `deno`

crates/squawk_ide/src/comments.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use rowan::{Direction, NodeOrToken};
2+
use squawk_syntax::{
3+
SyntaxNode,
4+
ast::{self, AstToken},
5+
};
6+
7+
pub(crate) fn preceding_comment(node: &SyntaxNode) -> Option<String> {
8+
let mut comments = vec![];
9+
10+
for element in node.siblings_with_tokens(Direction::Prev).skip(1) {
11+
let NodeOrToken::Token(token) = element else {
12+
break;
13+
};
14+
15+
if let Some(comment) = ast::Comment::cast(token.clone()) {
16+
let comment = normalize_comment(comment.text());
17+
if !comment.is_empty() {
18+
comments.push(comment);
19+
}
20+
continue;
21+
}
22+
23+
// In the following case, we would skip the `-- foo` since it's not
24+
// connected to the function:
25+
//
26+
// -- foo
27+
//
28+
// create function foo() returns void
29+
// as 'select 1' language sql;
30+
if let Some(ws) = ast::Whitespace::cast(token)
31+
&& !ws.text().contains("\n\n")
32+
{
33+
continue;
34+
}
35+
36+
break;
37+
}
38+
39+
if comments.is_empty() {
40+
None
41+
} else {
42+
comments.reverse();
43+
Some(comments.join("\n"))
44+
}
45+
}
46+
47+
fn normalize_comment(comment: &str) -> String {
48+
if let Some(comment) = comment.strip_prefix("--") {
49+
return comment.trim().to_string();
50+
}
51+
52+
if let Some(comment) = comment
53+
.strip_prefix("/*")
54+
.and_then(|comment| comment.strip_suffix("*/"))
55+
{
56+
let normalized = comment
57+
.lines()
58+
.map(|line| line.trim_start().trim_start_matches('*').trim_start())
59+
.collect::<Vec<_>>()
60+
.join("\n");
61+
62+
return normalized.trim().to_string();
63+
}
64+
65+
comment.trim().to_string()
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use crate::db::{Database, File, parse};
71+
72+
use insta::assert_snapshot;
73+
use squawk_syntax::ast::AstNode;
74+
75+
#[must_use]
76+
fn preceding_comment(sql: &str) -> String {
77+
let db = Database::default();
78+
let file = File::new(&db, sql.to_string().into());
79+
let parse = parse(&db, file);
80+
assert_eq!(parse.errors(), vec![]);
81+
82+
let stmt = parse.tree().stmts().next().unwrap();
83+
super::preceding_comment(stmt.syntax()).unwrap()
84+
}
85+
86+
fn no_comment(sql: &str) {
87+
let db = Database::default();
88+
let file = File::new(&db, sql.to_string().into());
89+
let parse = parse(&db, file);
90+
assert_eq!(parse.errors(), vec![]);
91+
92+
let stmt = parse.tree().stmts().next().unwrap();
93+
assert!(
94+
super::preceding_comment(stmt.syntax()).is_none(),
95+
"We shouldn't find a comment, if that's expected, use the preceding_comment instead"
96+
);
97+
}
98+
99+
#[test]
100+
fn not_preceding_func() {
101+
no_comment(
102+
"
103+
-- whitespace between so we don't count this
104+
105+
create function foo() returns int as $$ select 1 $$ language sql;
106+
",
107+
);
108+
}
109+
110+
#[test]
111+
fn preceding_func_line() {
112+
let comment = preceding_comment(
113+
"
114+
-- whitespace between this and the following, so skip it
115+
116+
-- this is a doc comment
117+
-- for foo
118+
create function foo() returns int as $$ select 1 $$ language sql;
119+
",
120+
);
121+
122+
assert_snapshot!(comment, @r"
123+
this is a doc comment
124+
for foo
125+
");
126+
}
127+
128+
#[test]
129+
fn preceding_func_block() {
130+
let comment = preceding_comment(
131+
"
132+
/** line 1 */
133+
/* line 2 */
134+
create function foo() returns int as $$ select 1 $$ language sql;
135+
",
136+
);
137+
138+
assert_snapshot!(comment, @"
139+
line 1
140+
line 2
141+
");
142+
}
143+
}

0 commit comments

Comments
 (0)