squawk_ide/
goto_definition.rs1use crate::binder;
2use crate::offsets::token_from_offset;
3use crate::resolve;
4use rowan::{TextRange, TextSize};
5use squawk_syntax::{
6 SyntaxKind,
7 ast::{self, AstNode},
8};
9
10pub fn goto_definition(file: ast::SourceFile, offset: TextSize) -> Option<TextRange> {
11 let token = token_from_offset(&file, offset)?;
12 let parent = token.parent()?;
13
14 if (token.kind() == SyntaxKind::WHEN_KW && parent.kind() == SyntaxKind::WHEN_CLAUSE)
16 || (token.kind() == SyntaxKind::ELSE_KW && parent.kind() == SyntaxKind::ELSE_CLAUSE)
17 || (token.kind() == SyntaxKind::END_KW && parent.kind() == SyntaxKind::CASE_EXPR)
18 {
19 for parent in token.parent_ancestors() {
20 if let Some(case_expr) = ast::CaseExpr::cast(parent)
21 && let Some(case_token) = case_expr.case_token()
22 {
23 return Some(case_token.text_range());
24 }
25 }
26 }
27
28 if ast::Commit::can_cast(parent.kind()) {
30 if let Some(begin_range) = find_preceding_begin(&file, token.text_range().start()) {
31 return Some(begin_range);
32 }
33 }
34
35 if ast::Rollback::can_cast(parent.kind()) {
37 if let Some(begin_range) = find_preceding_begin(&file, token.text_range().start()) {
38 return Some(begin_range);
39 }
40 }
41
42 if ast::Begin::can_cast(parent.kind()) {
44 if let Some(end_range) = find_following_commit_or_rollback(&file, token.text_range().end())
45 {
46 return Some(end_range);
47 }
48 }
49
50 if let Some(name_ref) = ast::NameRef::cast(parent.clone()) {
51 let binder_output = binder::bind(&file);
52 if let Some(ptr) = resolve::resolve_name_ref(&binder_output, &name_ref) {
53 let node = ptr.to_node(file.syntax());
54 return Some(node.text_range());
55 }
56 }
57
58 return None;
59}
60
61fn find_preceding_begin(file: &ast::SourceFile, before: TextSize) -> Option<TextRange> {
62 let mut last_begin: Option<TextRange> = None;
63 for stmt in file.stmts() {
64 if let ast::Stmt::Begin(begin) = stmt {
65 let range = begin.syntax().text_range();
66 if range.end() <= before {
67 last_begin = Some(range);
68 }
69 }
70 }
71 last_begin
72}
73
74fn find_following_commit_or_rollback(file: &ast::SourceFile, after: TextSize) -> Option<TextRange> {
75 for stmt in file.stmts() {
76 let range = match &stmt {
77 ast::Stmt::Commit(commit) => commit.syntax().text_range(),
78 ast::Stmt::Rollback(rollback) => rollback.syntax().text_range(),
79 _ => continue,
80 };
81 if range.start() >= after {
82 return Some(range);
83 }
84 }
85 None
86}
87
88#[cfg(test)]
89mod test {
90 use crate::goto_definition::goto_definition;
91 use crate::test_utils::fixture;
92 use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
93 use insta::assert_snapshot;
94 use log::info;
95 use squawk_syntax::ast;
96
97 #[track_caller]
98 fn goto(sql: &str) -> String {
99 goto_(sql).expect("should always find a definition")
100 }
101
102 #[track_caller]
103 fn goto_(sql: &str) -> Option<String> {
104 info!("starting");
105 let (mut offset, sql) = fixture(sql);
106 offset = offset.checked_sub(1.into()).unwrap_or_default();
109 let parse = ast::SourceFile::parse(&sql);
110 assert_eq!(parse.errors(), vec![]);
111 let file: ast::SourceFile = parse.tree();
112 if let Some(result) = goto_definition(file, offset) {
113 let offset: usize = offset.into();
114 let group = Level::INFO.primary_title("definition").element(
115 Snippet::source(&sql)
116 .fold(true)
117 .annotation(
118 AnnotationKind::Context
119 .span(result.into())
120 .label("2. destination"),
121 )
122 .annotation(
123 AnnotationKind::Context
124 .span(offset..offset + 1)
125 .label("1. source"),
126 ),
127 );
128 let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
129 return Some(
130 renderer
131 .render(&[group])
132 .to_string()
133 .replace("info: definition", ""),
135 );
136 }
137 None
138 }
139
140 fn goto_not_found(sql: &str) {
141 assert!(goto_(sql).is_none(), "Should not find a definition");
142 }
143
144 #[test]
145 fn goto_case_when() {
146 assert_snapshot!(goto("
147select case when$0 x > 1 then 1 else 2 end;
148"), @r"
149 ╭▸
150 2 │ select case when x > 1 then 1 else 2 end;
151 │ ┬─── ─ 1. source
152 │ │
153 ╰╴ 2. destination
154 ");
155 }
156
157 #[test]
158 fn goto_case_else() {
159 assert_snapshot!(goto("
160select case when x > 1 then 1 else$0 2 end;
161"), @r"
162 ╭▸
163 2 │ select case when x > 1 then 1 else 2 end;
164 ╰╴ ──── 2. destination ─ 1. source
165 ");
166 }
167
168 #[test]
169 fn goto_case_end() {
170 assert_snapshot!(goto("
171select case when x > 1 then 1 else 2 end$0;
172"), @r"
173 ╭▸
174 2 │ select case when x > 1 then 1 else 2 end;
175 ╰╴ ──── 2. destination ─ 1. source
176 ");
177 }
178
179 #[test]
180 fn goto_case_end_trailing_semi() {
181 assert_snapshot!(goto("
182select case when x > 1 then 1 else 2 end;$0
183"), @r"
184 ╭▸
185 2 │ select case when x > 1 then 1 else 2 end;
186 ╰╴ ──── 2. destination ─ 1. source
187 ");
188 }
189
190 #[test]
191 fn goto_case_then_not_found() {
192 goto_not_found(
193 "
194select case when x > 1 then$0 1 else 2 end;
195",
196 )
197 }
198
199 #[test]
200 fn rollback_to_begin() {
201 assert_snapshot!(goto(
202 "
203begin;
204select 1;
205rollback$0;
206",
207 ), @r"
208 ╭▸
209 2 │ begin;
210 │ ───── 2. destination
211 3 │ select 1;
212 4 │ rollback;
213 ╰╴ ─ 1. source
214 ");
215 }
216
217 #[test]
218 fn goto_drop_table() {
219 assert_snapshot!(goto("
220create table t();
221drop table t$0;
222"), @r"
223 ╭▸
224 2 │ create table t();
225 │ ─ 2. destination
226 3 │ drop table t;
227 ╰╴ ─ 1. source
228 ");
229 }
230
231 #[test]
232 fn goto_drop_table_with_schema() {
233 assert_snapshot!(goto("
234create table public.t();
235drop table t$0;
236"), @r"
237 ╭▸
238 2 │ create table public.t();
239 │ ─ 2. destination
240 3 │ drop table t;
241 ╰╴ ─ 1. source
242 ");
243
244 assert_snapshot!(goto("
245create table foo.t();
246drop table foo.t$0;
247"), @r"
248 ╭▸
249 2 │ create table foo.t();
250 │ ─ 2. destination
251 3 │ drop table foo.t;
252 ╰╴ ─ 1. source
253 ");
254
255 goto_not_found(
256 "
257-- defaults to public schema
258create table t();
259drop table foo.t$0;
260",
261 );
262 }
263
264 #[test]
265 fn goto_drop_temp_table() {
266 assert_snapshot!(goto("
267create temp table t();
268drop table t$0;
269"), @r"
270 ╭▸
271 2 │ create temp table t();
272 │ ─ 2. destination
273 3 │ drop table t;
274 ╰╴ ─ 1. source
275 ");
276 }
277
278 #[test]
279 fn goto_drop_temporary_table() {
280 assert_snapshot!(goto("
281create temporary table t();
282drop table t$0;
283"), @r"
284 ╭▸
285 2 │ create temporary table t();
286 │ ─ 2. destination
287 3 │ drop table t;
288 ╰╴ ─ 1. source
289 ");
290 }
291
292 #[test]
293 fn goto_drop_temp_table_with_pg_temp_schema() {
294 assert_snapshot!(goto("
295create temp table t();
296drop table pg_temp.t$0;
297"), @r"
298 ╭▸
299 2 │ create temp table t();
300 │ ─ 2. destination
301 3 │ drop table pg_temp.t;
302 ╰╴ ─ 1. source
303 ");
304 }
305
306 #[test]
307 fn goto_drop_temp_table_shadows_public() {
308 assert_snapshot!(goto("
310create table t();
311create temp table t();
312drop table t$0;
313"), @r"
314 ╭▸
315 3 │ create temp table t();
316 │ ─ 2. destination
317 4 │ drop table t;
318 ╰╴ ─ 1. source
319 ");
320 }
321
322 #[test]
323 fn goto_drop_public_table_when_temp_exists() {
324 assert_snapshot!(goto("
326create table t();
327create temp table t();
328drop table public.t$0;
329"), @r"
330 ╭▸
331 2 │ create table t();
332 │ ─ 2. destination
333 3 │ create temp table t();
334 4 │ drop table public.t;
335 ╰╴ ─ 1. source
336 ");
337 }
338
339 #[test]
340 fn goto_drop_table_defined_after() {
341 assert_snapshot!(goto("
342drop table t$0;
343create table t();
344"), @r"
345 ╭▸
346 2 │ drop table t;
347 │ ─ 1. source
348 3 │ create table t();
349 ╰╴ ─ 2. destination
350 ");
351 }
352
353 #[test]
354 fn begin_to_rollback() {
355 assert_snapshot!(goto(
356 "
357begin$0;
358select 1;
359rollback;
360commit;
361",
362 ), @r"
363 ╭▸
364 2 │ begin;
365 │ ─ 1. source
366 3 │ select 1;
367 4 │ rollback;
368 ╰╴──────── 2. destination
369 ");
370 }
371
372 #[test]
373 fn commit_to_begin() {
374 assert_snapshot!(goto(
375 "
376begin;
377select 1;
378commit$0;
379",
380 ), @r"
381 ╭▸
382 2 │ begin;
383 │ ───── 2. destination
384 3 │ select 1;
385 4 │ commit;
386 ╰╴ ─ 1. source
387 ");
388 }
389
390 #[test]
391 fn begin_to_commit() {
392 assert_snapshot!(goto(
393 "
394begin$0;
395select 1;
396commit;
397",
398 ), @r"
399 ╭▸
400 2 │ begin;
401 │ ─ 1. source
402 3 │ select 1;
403 4 │ commit;
404 ╰╴────── 2. destination
405 ");
406 }
407
408 #[test]
409 fn commit_to_start_transaction() {
410 assert_snapshot!(goto(
411 "
412start transaction;
413select 1;
414commit$0;
415",
416 ), @r"
417 ╭▸
418 2 │ start transaction;
419 │ ───────────────── 2. destination
420 3 │ select 1;
421 4 │ commit;
422 ╰╴ ─ 1. source
423 ");
424 }
425
426 #[test]
427 fn start_transaction_to_commit() {
428 assert_snapshot!(goto(
429 "
430start$0 transaction;
431select 1;
432commit;
433",
434 ), @r"
435 ╭▸
436 2 │ start transaction;
437 │ ─ 1. source
438 3 │ select 1;
439 4 │ commit;
440 ╰╴────── 2. destination
441 ");
442 }
443
444 #[test]
445 fn goto_with_search_path() {
446 assert_snapshot!(goto(r#"
447set search_path to "foo", public;
448create table foo.t();
449drop table t$0;
450"#), @r"
451 ╭▸
452 3 │ create table foo.t();
453 │ ─ 2. destination
454 4 │ drop table t;
455 ╰╴ ─ 1. source
456 ");
457 }
458
459 #[test]
460 fn goto_with_search_path_like_variable() {
461 goto_not_found(
463 "
464set bar.search_path to foo, public;
465create table foo.t();
466drop table t$0;
467",
468 )
469 }
470
471 #[test]
472 fn goto_with_search_path_second_schema() {
473 assert_snapshot!(goto("
474set search_path to foo, bar, public;
475create table bar.t();
476drop table t$0;
477"), @r"
478 ╭▸
479 3 │ create table bar.t();
480 │ ─ 2. destination
481 4 │ drop table t;
482 ╰╴ ─ 1. source
483 ");
484 }
485
486 #[test]
487 fn goto_with_search_path_skips_first() {
488 assert_snapshot!(goto("
489set search_path to foo, bar, public;
490create table foo.t();
491create table bar.t();
492drop table t$0;
493"), @r"
494 ╭▸
495 3 │ create table foo.t();
496 │ ─ 2. destination
497 4 │ create table bar.t();
498 5 │ drop table t;
499 ╰╴ ─ 1. source
500 ");
501 }
502
503 #[test]
504 fn goto_without_search_path_uses_default() {
505 assert_snapshot!(goto("
506create table foo.t();
507create table public.t();
508drop table t$0;
509"), @r"
510 ╭▸
511 3 │ create table public.t();
512 │ ─ 2. destination
513 4 │ drop table t;
514 ╰╴ ─ 1. source
515 ");
516 }
517
518 #[test]
519 fn goto_with_set_schema() {
520 assert_snapshot!(goto("
521set schema 'myschema';
522create table myschema.t();
523drop table t$0;
524"), @r"
525 ╭▸
526 3 │ create table myschema.t();
527 │ ─ 2. destination
528 4 │ drop table t;
529 ╰╴ ─ 1. source
530 ");
531 }
532
533 #[test]
534 fn goto_with_set_schema_ignores_other_schemas() {
535 assert_snapshot!(goto("
536set schema 'myschema';
537create table public.t();
538create table myschema.t();
539drop table t$0;
540"), @r"
541 ╭▸
542 4 │ create table myschema.t();
543 │ ─ 2. destination
544 5 │ drop table t;
545 ╰╴ ─ 1. source
546 ");
547 }
548
549 #[test]
550 fn goto_with_search_path_changed_twice() {
551 assert_snapshot!(goto("
552set search_path to foo;
553create table foo.t();
554set search_path to bar;
555create table bar.t();
556drop table t$0;
557"), @r"
558 ╭▸
559 5 │ create table bar.t();
560 │ ─ 2. destination
561 6 │ drop table t;
562 ╰╴ ─ 1. source
563 ");
564
565 assert_snapshot!(goto("
566set search_path to foo;
567create table foo.t();
568drop table t$0;
569set search_path to bar;
570create table bar.t();
571drop table t;
572"), @r"
573 ╭▸
574 3 │ create table foo.t();
575 │ ─ 2. destination
576 4 │ drop table t;
577 ╰╴ ─ 1. source
578 ");
579 }
580
581 #[test]
582 fn goto_with_empty_search_path() {
583 goto_not_found(
584 "
585set search_path to '';
586create table public.t();
587drop table t$0;
588",
589 )
590 }
591
592 #[test]
593 fn goto_with_search_path_uppercase() {
594 assert_snapshot!(goto("
595SET SEARCH_PATH TO foo;
596create table foo.t();
597drop table t$0;
598"), @r"
599 ╭▸
600 3 │ create table foo.t();
601 │ ─ 2. destination
602 4 │ drop table t;
603 ╰╴ ─ 1. source
604 ");
605 }
606
607 #[test]
608 fn goto_table_stmt() {
609 assert_snapshot!(goto("
610create table t();
611table t$0;
612"), @r"
613 ╭▸
614 2 │ create table t();
615 │ ─ 2. destination
616 3 │ table t;
617 ╰╴ ─ 1. source
618 ");
619 }
620
621 #[test]
622 fn goto_table_stmt_with_schema() {
623 assert_snapshot!(goto("
624create table public.t();
625table public.t$0;
626"), @r"
627 ╭▸
628 2 │ create table public.t();
629 │ ─ 2. destination
630 3 │ table public.t;
631 ╰╴ ─ 1. source
632 ");
633 }
634
635 #[test]
636 fn goto_table_stmt_with_search_path() {
637 assert_snapshot!(goto("
638set search_path to foo;
639create table foo.t();
640table t$0;
641"), @r"
642 ╭▸
643 3 │ create table foo.t();
644 │ ─ 2. destination
645 4 │ table t;
646 ╰╴ ─ 1. source
647 ");
648 }
649}