Skip to content

Commit 3809b5c

Browse files
authored
ide: add basic find refs support (#760)
1 parent c69c76d commit 3809b5c

File tree

4 files changed

+410
-4
lines changed

4 files changed

+410
-4
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
use crate::binder::{self, Binder};
2+
use crate::offsets::token_from_offset;
3+
use crate::resolve;
4+
use rowan::{TextRange, TextSize};
5+
use squawk_syntax::{
6+
SyntaxNodePtr,
7+
ast::{self, AstNode},
8+
match_ast,
9+
};
10+
11+
pub fn find_references(file: &ast::SourceFile, offset: TextSize) -> Vec<TextRange> {
12+
let binder = binder::bind(file);
13+
let Some(target) = find_target(file, offset, &binder) else {
14+
return vec![];
15+
};
16+
17+
let mut refs = vec![];
18+
19+
for node in file.syntax().descendants() {
20+
match_ast! {
21+
match node {
22+
ast::NameRef(name_ref) => {
23+
if let Some(found) = resolve::resolve_name_ref(&binder, &name_ref)
24+
&& found == target
25+
{
26+
refs.push(name_ref.syntax().text_range());
27+
}
28+
},
29+
ast::Name(name) => {
30+
let found = SyntaxNodePtr::new(name.syntax());
31+
if found == target {
32+
refs.push(name.syntax().text_range());
33+
}
34+
},
35+
_ => (),
36+
}
37+
}
38+
}
39+
40+
refs.sort_by_key(|range| range.start());
41+
refs
42+
}
43+
44+
fn find_target(file: &ast::SourceFile, offset: TextSize, binder: &Binder) -> Option<SyntaxNodePtr> {
45+
let token = token_from_offset(file, offset)?;
46+
let parent = token.parent()?;
47+
48+
if let Some(name) = ast::Name::cast(parent.clone()) {
49+
return Some(SyntaxNodePtr::new(name.syntax()));
50+
}
51+
52+
if let Some(name_ref) = ast::NameRef::cast(parent.clone())
53+
&& let Some(ptr) = resolve::resolve_name_ref(binder, &name_ref)
54+
{
55+
return Some(ptr);
56+
}
57+
58+
None
59+
}
60+
61+
#[cfg(test)]
62+
mod test {
63+
use crate::find_references::find_references;
64+
use crate::test_utils::fixture;
65+
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
66+
use insta::assert_snapshot;
67+
use squawk_syntax::ast;
68+
69+
#[track_caller]
70+
fn find_refs(sql: &str) -> String {
71+
let (mut offset, sql) = fixture(sql);
72+
offset = offset.checked_sub(1.into()).unwrap_or_default();
73+
let parse = ast::SourceFile::parse(&sql);
74+
assert_eq!(parse.errors(), vec![]);
75+
let file: ast::SourceFile = parse.tree();
76+
77+
let references = find_references(&file, offset);
78+
79+
let offset_usize: usize = offset.into();
80+
81+
let labels: Vec<String> = (1..=references.len())
82+
.map(|i| format!("{}. reference", i))
83+
.collect();
84+
85+
let mut snippet = Snippet::source(&sql).fold(true).annotation(
86+
AnnotationKind::Context
87+
.span(offset_usize..offset_usize + 1)
88+
.label("0. query"),
89+
);
90+
91+
for (i, range) in references.iter().enumerate() {
92+
snippet = snippet.annotation(
93+
AnnotationKind::Context
94+
.span((*range).into())
95+
.label(&labels[i]),
96+
);
97+
}
98+
99+
let group = Level::INFO.primary_title("references").element(snippet);
100+
let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
101+
renderer
102+
.render(&[group])
103+
.to_string()
104+
.replace("info: references", "")
105+
}
106+
107+
#[test]
108+
fn simple_table_reference() {
109+
assert_snapshot!(find_refs("
110+
create table t();
111+
drop table t$0;
112+
"), @r"
113+
╭▸
114+
2 │ create table t();
115+
│ ─ 1. reference
116+
3 │ drop table t;
117+
│ ┬
118+
│ │
119+
│ 0. query
120+
╰╴ 2. reference
121+
");
122+
}
123+
124+
#[test]
125+
fn multiple_references() {
126+
assert_snapshot!(find_refs("
127+
create table users();
128+
drop table users$0;
129+
table users;
130+
"), @r"
131+
╭▸
132+
2 │ create table users();
133+
│ ───── 1. reference
134+
3 │ drop table users;
135+
│ ┬───┬
136+
│ │ │
137+
│ │ 0. query
138+
│ 2. reference
139+
4 │ table users;
140+
╰╴ ───── 3. reference
141+
");
142+
}
143+
144+
#[test]
145+
fn find_from_definition() {
146+
assert_snapshot!(find_refs("
147+
create table t$0();
148+
drop table t;
149+
"), @r"
150+
╭▸
151+
2 │ create table t();
152+
│ ┬
153+
│ │
154+
│ 0. query
155+
│ 1. reference
156+
3 │ drop table t;
157+
╰╴ ─ 2. reference
158+
");
159+
}
160+
161+
#[test]
162+
fn with_schema_qualified() {
163+
assert_snapshot!(find_refs("
164+
create table public.users();
165+
drop table public.users$0;
166+
table users;
167+
"), @r"
168+
╭▸
169+
2 │ create table public.users();
170+
│ ───── 1. reference
171+
3 │ drop table public.users;
172+
│ ┬───┬
173+
│ │ │
174+
│ │ 0. query
175+
│ 2. reference
176+
4 │ table users;
177+
╰╴ ───── 3. reference
178+
");
179+
}
180+
181+
#[test]
182+
fn temp_table_shadows_public() {
183+
assert_snapshot!(find_refs("
184+
create table t();
185+
create temp table t$0();
186+
drop table t;
187+
"), @r"
188+
╭▸
189+
3 │ create temp table t();
190+
│ ┬
191+
│ │
192+
│ 0. query
193+
│ 1. reference
194+
4 │ drop table t;
195+
╰╴ ─ 2. reference
196+
");
197+
}
198+
199+
#[test]
200+
fn different_schema_no_match() {
201+
assert_snapshot!(find_refs("
202+
create table foo.t();
203+
create table bar.t$0();
204+
"), @r"
205+
╭▸
206+
3 │ create table bar.t();
207+
│ ┬
208+
│ │
209+
│ 0. query
210+
╰╴ 1. reference
211+
");
212+
}
213+
214+
#[test]
215+
fn with_search_path() {
216+
assert_snapshot!(find_refs("
217+
set search_path to myschema;
218+
create table myschema.users$0();
219+
drop table users;
220+
"), @r"
221+
╭▸
222+
3 │ create table myschema.users();
223+
│ ┬───┬
224+
│ │ │
225+
│ │ 0. query
226+
│ 1. reference
227+
4 │ drop table users;
228+
╰╴ ───── 2. reference
229+
");
230+
}
231+
232+
#[test]
233+
fn temp_table_with_pg_temp_schema() {
234+
assert_snapshot!(find_refs("
235+
create temp table t();
236+
drop table pg_temp.t$0;
237+
"), @r"
238+
╭▸
239+
2 │ create temp table t();
240+
│ ─ 1. reference
241+
3 │ drop table pg_temp.t;
242+
│ ┬
243+
│ │
244+
│ 0. query
245+
╰╴ 2. reference
246+
");
247+
}
248+
249+
#[test]
250+
fn case_insensitive() {
251+
assert_snapshot!(find_refs("
252+
create table Users();
253+
drop table USERS$0;
254+
table users;
255+
"), @r"
256+
╭▸
257+
2 │ create table Users();
258+
│ ───── 1. reference
259+
3 │ drop table USERS;
260+
│ ┬───┬
261+
│ │ │
262+
│ │ 0. query
263+
│ 2. reference
264+
4 │ table users;
265+
╰╴ ───── 3. reference
266+
");
267+
}
268+
#[test]
269+
fn case_insensitive_part_2() {
270+
// we should see refs for `drop table` and `table`
271+
assert_snapshot!(find_refs(r#"
272+
create table actors();
273+
create table "Actors"();
274+
drop table ACTORS$0;
275+
table actors;
276+
"#), @r#"
277+
╭▸
278+
2 │ create table actors();
279+
│ ────── 1. reference
280+
3 │ create table "Actors"();
281+
4 │ drop table ACTORS;
282+
│ ┬────┬
283+
│ │ │
284+
│ │ 0. query
285+
│ 2. reference
286+
5 │ table actors;
287+
╰╴ ────── 3. reference
288+
"#);
289+
}
290+
291+
#[test]
292+
fn case_insensitive_with_schema() {
293+
assert_snapshot!(find_refs("
294+
create table Public.Users();
295+
drop table PUBLIC.USERS$0;
296+
table public.users;
297+
"), @r"
298+
╭▸
299+
2 │ create table Public.Users();
300+
│ ───── 1. reference
301+
3 │ drop table PUBLIC.USERS;
302+
│ ┬───┬
303+
│ │ │
304+
│ │ 0. query
305+
│ 2. reference
306+
4 │ table public.users;
307+
╰╴ ───── 3. reference
308+
");
309+
}
310+
311+
#[test]
312+
fn no_partial_match() {
313+
assert_snapshot!(find_refs("
314+
create table t$0();
315+
create table temp_t();
316+
"), @r"
317+
╭▸
318+
2 │ create table t();
319+
│ ┬
320+
│ │
321+
│ 0. query
322+
╰╴ 1. reference
323+
");
324+
}
325+
326+
#[test]
327+
fn identifier_boundaries() {
328+
assert_snapshot!(find_refs("
329+
create table foo$0();
330+
drop table foo;
331+
drop table foo1;
332+
drop table barfoo;
333+
drop table foo_bar;
334+
"), @r"
335+
╭▸
336+
2 │ create table foo();
337+
│ ┬─┬
338+
│ │ │
339+
│ │ 0. query
340+
│ 1. reference
341+
3 │ drop table foo;
342+
╰╴ ─── 2. reference
343+
");
344+
}
345+
}

crates/squawk_ide/src/goto_definition.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ pub fn goto_definition(file: ast::SourceFile, offset: TextSize) -> Option<TextRa
4747
}
4848
}
4949

50+
if let Some(name) = ast::Name::cast(parent.clone()) {
51+
return Some(name.syntax().text_range());
52+
}
53+
5054
if let Some(name_ref) = ast::NameRef::cast(parent.clone()) {
5155
let binder_output = binder::bind(&file);
5256
if let Some(ptr) = resolve::resolve_name_ref(&binder_output, &name_ref) {
@@ -303,6 +307,20 @@ drop table pg_temp.t$0;
303307
");
304308
}
305309

310+
#[test]
311+
fn goto_table_definition_returns_self() {
312+
assert_snapshot!(goto("
313+
create table t$0(x bigint, y bigint);
314+
"), @r"
315+
╭▸
316+
2 │ create table t(x bigint, y bigint);
317+
│ ┬
318+
│ │
319+
│ 2. destination
320+
╰╴ 1. source
321+
");
322+
}
323+
306324
#[test]
307325
fn goto_drop_temp_table_shadows_public() {
308326
// temp tables shadow public tables when no schema is specified

crates/squawk_ide/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod binder;
22
pub mod code_actions;
33
pub mod column_name;
44
pub mod expand_selection;
5+
pub mod find_references;
56
mod generated;
67
pub mod goto_definition;
78
mod offsets;

0 commit comments

Comments
 (0)