Skip to content

Commit 634b6fc

Browse files
authored
lsp: go to def for case exprs (#708)
This doesn't require semantic analysis so it's easy to add now instead of having to wait until sema is ready. I think we can also do some syntax only based code actions as well.
1 parent 7cd5021 commit 634b6fc

File tree

12 files changed

+329
-101
lines changed

12 files changed

+329
-101
lines changed

PLAN.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,8 +1056,6 @@ select x from "t";
10561056

10571057
gives:
10581058

1059-
Note: we have to be mindful of casing here,
1060-
10611059
```sql
10621060
select "x" from "t";
10631061
```
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use rowan::{TextRange, TextSize};
2+
use squawk_syntax::{
3+
SyntaxKind, SyntaxToken,
4+
ast::{self, AstNode},
5+
};
6+
7+
pub fn goto_definition(file: ast::SourceFile, offset: TextSize) -> Option<TextRange> {
8+
let token = token_from_offset(&file, offset)?;
9+
let parent = token.parent()?;
10+
11+
// goto def on case exprs
12+
if (token.kind() == SyntaxKind::WHEN_KW && parent.kind() == SyntaxKind::WHEN_CLAUSE)
13+
|| (token.kind() == SyntaxKind::ELSE_KW && parent.kind() == SyntaxKind::ELSE_CLAUSE)
14+
|| (token.kind() == SyntaxKind::END_KW && parent.kind() == SyntaxKind::CASE_EXPR)
15+
{
16+
for parent in token.parent_ancestors() {
17+
if let Some(case_expr) = ast::CaseExpr::cast(parent) {
18+
if let Some(case_token) = case_expr.case_token() {
19+
return Some(case_token.text_range());
20+
}
21+
}
22+
}
23+
}
24+
25+
return None;
26+
}
27+
28+
fn token_from_offset(file: &ast::SourceFile, offset: TextSize) -> Option<SyntaxToken> {
29+
let mut token = file.syntax().token_at_offset(offset).right_biased()?;
30+
// want to be lenient in case someone clicks the trailing `;` of a line
31+
// instead of an identifier
32+
if token.kind() == SyntaxKind::SEMICOLON {
33+
token = token.prev_token()?;
34+
}
35+
return Some(token);
36+
}
37+
38+
#[cfg(test)]
39+
mod test {
40+
use crate::goto_definition::goto_definition;
41+
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
42+
use insta::assert_snapshot;
43+
use log::info;
44+
use rowan::TextSize;
45+
use squawk_syntax::ast;
46+
47+
// TODO: we should probably use something else since `$0` is valid syntax, maybe `%0`?
48+
const MARKER: &str = "$0";
49+
50+
fn fixture(sql: &str) -> (Option<TextSize>, String) {
51+
if let Some(pos) = sql.find(MARKER) {
52+
return (
53+
Some(TextSize::new((pos - 1) as u32)),
54+
sql.replace(MARKER, ""),
55+
);
56+
}
57+
(None, sql.to_string())
58+
}
59+
60+
#[track_caller]
61+
fn goto(sql: &str) -> String {
62+
goto_(sql).expect("should always find a definition")
63+
}
64+
65+
#[track_caller]
66+
fn goto_(sql: &str) -> Option<String> {
67+
info!("starting");
68+
let (offset, sql) = fixture(sql);
69+
let parse = ast::SourceFile::parse(&sql);
70+
assert_eq!(parse.errors(), vec![]);
71+
let file: ast::SourceFile = parse.tree();
72+
let Some(offset) = offset else {
73+
info!("offset not found, did you put a marker `$0` in the sql?");
74+
return None;
75+
};
76+
if let Some(result) = goto_definition(file, offset) {
77+
let offset: usize = offset.into();
78+
let group = Level::INFO.primary_title("definition").element(
79+
Snippet::source(&sql)
80+
.fold(true)
81+
.annotation(
82+
AnnotationKind::Context
83+
.span(result.into())
84+
.label("2. destination"),
85+
)
86+
.annotation(
87+
AnnotationKind::Context
88+
.span(offset..offset + 1)
89+
.label("1. source"),
90+
),
91+
);
92+
let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
93+
return Some(
94+
renderer
95+
.render(&[group])
96+
.to_string()
97+
// hacky cleanup to make the text shorter
98+
.replace("info: definition", ""),
99+
);
100+
}
101+
None
102+
}
103+
104+
fn goto_not_found(sql: &str) {
105+
assert!(goto_(sql).is_none(), "Should not find a definition");
106+
}
107+
108+
#[test]
109+
fn goto_case_when() {
110+
assert_snapshot!(goto("
111+
select case when$0 x > 1 then 1 else 2 end;
112+
"), @r"
113+
╭▸
114+
2 │ select case when x > 1 then 1 else 2 end;
115+
│ ┬─── ─ 1. source
116+
│ │
117+
╰╴ 2. destination
118+
");
119+
}
120+
121+
#[test]
122+
fn goto_case_else() {
123+
assert_snapshot!(goto("
124+
select case when x > 1 then 1 else$0 2 end;
125+
"), @r"
126+
╭▸
127+
2 │ select case when x > 1 then 1 else 2 end;
128+
╰╴ ──── 2. destination ─ 1. source
129+
");
130+
}
131+
132+
#[test]
133+
fn goto_case_end() {
134+
assert_snapshot!(goto("
135+
select case when x > 1 then 1 else 2 end$0;
136+
"), @r"
137+
╭▸
138+
2 │ select case when x > 1 then 1 else 2 end;
139+
╰╴ ──── 2. destination ─ 1. source
140+
");
141+
}
142+
143+
#[test]
144+
fn goto_case_end_trailing_semi() {
145+
assert_snapshot!(goto("
146+
select case when x > 1 then 1 else 2 end;$0
147+
"), @r"
148+
╭▸
149+
2 │ select case when x > 1 then 1 else 2 end;
150+
╰╴ ──── 2. destination ─ 1. source
151+
");
152+
}
153+
154+
#[test]
155+
fn goto_case_then_not_found() {
156+
goto_not_found(
157+
"
158+
select case when x > 1 then$0 1 else 2 end;
159+
",
160+
)
161+
}
162+
}

crates/squawk_ide/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod expand_selection;
2+
pub mod goto_definition;

crates/squawk_parser/src/generated/syntax_kind.rs

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

crates/squawk_parser/src/grammar.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4993,10 +4993,14 @@ fn partition_option(p: &mut Parser<'_>) {
49934993
}
49944994

49954995
fn opt_inherits_tables(p: &mut Parser<'_>) {
4996+
let m = p.start();
49964997
if p.eat(INHERITS_KW) {
49974998
p.expect(L_PAREN);
49984999
path_name_ref_list(p);
49995000
p.expect(R_PAREN);
5001+
m.complete(p, INHERITS);
5002+
} else {
5003+
m.abandon(p);
50005004
}
50015005
}
50025006

@@ -12295,16 +12299,20 @@ fn update(p: &mut Parser<'_>, m: Option<Marker>) -> CompletedMarker {
1229512299
// [ FROM from_item [, ...] ]
1229612300
opt_from_clause(p);
1229712301
// [ WHERE condition | WHERE CURRENT OF cursor_name ]
12302+
opt_where_or_current_of(p);
12303+
// [ RETURNING { * | output_expression [ [ AS ] output_name ] } [, ...] ]
12304+
opt_returning_clause(p);
12305+
m.complete(p, UPDATE)
12306+
}
12307+
12308+
fn opt_where_or_current_of(p: &mut Parser<'_>) {
1229812309
if p.at(WHERE_KW) {
1229912310
if p.nth_at(1, CURRENT_KW) {
1230012311
opt_where_current_of(p);
1230112312
} else {
1230212313
opt_where_clause(p);
1230312314
}
1230412315
}
12305-
// [ RETURNING { * | output_expression [ [ AS ] output_name ] } [, ...] ]
12306-
opt_returning_clause(p);
12307-
m.complete(p, UPDATE)
1230812316
}
1230912317

1231012318
fn with(p: &mut Parser<'_>, m: Option<Marker>) -> Option<CompletedMarker> {
@@ -12355,24 +12363,22 @@ fn delete(p: &mut Parser<'_>, m: Option<Marker>) -> CompletedMarker {
1235512363
}
1235612364
}
1235712365
// [ WHERE condition | WHERE CURRENT OF cursor_name ]
12358-
if p.at(WHERE_KW) {
12359-
if p.nth_at(1, CURRENT_KW) {
12360-
opt_where_current_of(p);
12361-
} else {
12362-
opt_where_clause(p);
12363-
}
12364-
}
12366+
opt_where_or_current_of(p);
1236512367
opt_returning_clause(p);
1236612368
m.complete(p, DELETE)
1236712369
}
1236812370

1236912371
// WHERE CURRENT OF cursor_name
1237012372
fn opt_where_current_of(p: &mut Parser<'_>) {
12373+
let m = p.start();
1237112374
if p.eat(WHERE_KW) {
1237212375
if p.eat(CURRENT_KW) {
1237312376
p.expect(OF_KW);
1237412377
name_ref(p);
1237512378
}
12379+
m.complete(p, WHERE_CURRENT_OF);
12380+
} else {
12381+
m.abandon(p);
1237612382
}
1237712383
}
1237812384

crates/squawk_parser/tests/snapshots/tests__create_foreign_table_ok.snap

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,25 +189,26 @@ SOURCE_FILE
189189
WHITESPACE "\n"
190190
R_PAREN ")"
191191
WHITESPACE "\n "
192-
INHERITS_KW "inherits"
193-
WHITESPACE " "
194-
L_PAREN "("
195-
PATH
192+
INHERITS
193+
INHERITS_KW "inherits"
194+
WHITESPACE " "
195+
L_PAREN "("
196196
PATH
197+
PATH
198+
PATH_SEGMENT
199+
NAME_REF
200+
IDENT "foo"
201+
DOT "."
197202
PATH_SEGMENT
198203
NAME_REF
199-
IDENT "foo"
200-
DOT "."
201-
PATH_SEGMENT
202-
NAME_REF
203-
IDENT "bar"
204-
COMMA ","
205-
WHITESPACE " "
206-
PATH
207-
PATH_SEGMENT
208-
NAME_REF
209-
IDENT "bar"
210-
R_PAREN ")"
204+
IDENT "bar"
205+
COMMA ","
206+
WHITESPACE " "
207+
PATH
208+
PATH_SEGMENT
209+
NAME_REF
210+
IDENT "bar"
211+
R_PAREN ")"
211212
WHITESPACE "\n "
212213
SERVER_KW "server"
213214
WHITESPACE " "

crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -394,31 +394,32 @@ SOURCE_FILE
394394
INT_KW "int"
395395
R_PAREN ")"
396396
WHITESPACE "\n"
397-
INHERITS_KW "inherits"
398-
WHITESPACE " "
399-
L_PAREN "("
400-
PATH
397+
INHERITS
398+
INHERITS_KW "inherits"
399+
WHITESPACE " "
400+
L_PAREN "("
401401
PATH
402+
PATH
403+
PATH_SEGMENT
404+
NAME_REF
405+
IDENT "foo"
406+
DOT "."
402407
PATH_SEGMENT
403408
NAME_REF
404-
IDENT "foo"
405-
DOT "."
406-
PATH_SEGMENT
407-
NAME_REF
408-
IDENT "bar"
409-
COMMA ","
410-
WHITESPACE " "
411-
PATH
412-
PATH_SEGMENT
413-
NAME_REF
414-
IDENT "bar"
415-
COMMA ","
416-
WHITESPACE " "
417-
PATH
418-
PATH_SEGMENT
419-
NAME_REF
420-
IDENT "buzz"
421-
R_PAREN ")"
409+
IDENT "bar"
410+
COMMA ","
411+
WHITESPACE " "
412+
PATH
413+
PATH_SEGMENT
414+
NAME_REF
415+
IDENT "bar"
416+
COMMA ","
417+
WHITESPACE " "
418+
PATH
419+
PATH_SEGMENT
420+
NAME_REF
421+
IDENT "buzz"
422+
R_PAREN ")"
422423
SEMICOLON ";"
423424
WHITESPACE "\n\n"
424425
CREATE_TABLE

crates/squawk_parser/tests/snapshots/tests__delete_ok.snap

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,15 @@ SOURCE_FILE
477477
NAME_REF
478478
IDENT "invoices"
479479
WHITESPACE " \n"
480-
WHERE_KW "where"
481-
WHITESPACE " "
482-
CURRENT_KW "current"
483-
WHITESPACE " "
484-
OF_KW "of"
485-
WHITESPACE " "
486-
NAME_REF
487-
IDENT "invoice_cursor"
480+
WHERE_CURRENT_OF
481+
WHERE_KW "where"
482+
WHITESPACE " "
483+
CURRENT_KW "current"
484+
WHITESPACE " "
485+
OF_KW "of"
486+
WHITESPACE " "
487+
NAME_REF
488+
IDENT "invoice_cursor"
488489
SEMICOLON ";"
489490
WHITESPACE "\n\n"
490491
COMMENT "-- returning"
@@ -931,14 +932,15 @@ SOURCE_FILE
931932
NAME_REF
932933
IDENT "tasks"
933934
WHITESPACE " "
934-
WHERE_KW "WHERE"
935-
WHITESPACE " "
936-
CURRENT_KW "CURRENT"
937-
WHITESPACE " "
938-
OF_KW "OF"
939-
WHITESPACE " "
940-
NAME_REF
941-
IDENT "c_tasks"
935+
WHERE_CURRENT_OF
936+
WHERE_KW "WHERE"
937+
WHITESPACE " "
938+
CURRENT_KW "CURRENT"
939+
WHITESPACE " "
940+
OF_KW "OF"
941+
WHITESPACE " "
942+
NAME_REF
943+
IDENT "c_tasks"
942944
SEMICOLON ";"
943945
WHITESPACE "\n\n"
944946
DELETE

0 commit comments

Comments
 (0)