Skip to content

Commit ce84d56

Browse files
committed
feat: implement Go to Assignments for local variables
1 parent b2a58b8 commit ce84d56

File tree

10 files changed

+381
-0
lines changed

10 files changed

+381
-0
lines changed

crates/ide/src/goto_assignments.rs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
use hir::Semantics;
2+
use ide_db::{
3+
RootDatabase,
4+
defs::{Definition, IdentClass},
5+
helpers::pick_best_token,
6+
search::ReferenceCategory,
7+
};
8+
use syntax::{AstNode, SyntaxKind::IDENT};
9+
10+
use crate::{FilePosition, NavigationTarget, RangeInfo, TryToNav};
11+
12+
/// Find all assignment sites for the variable at the given position.
13+
///
14+
/// Returns all locations where the variable is assigned a value, including:
15+
/// - Initial definition sites (let bindings, function parameters, etc.)
16+
/// - Explicit assignment expressions (x = value)
17+
/// - Compound assignment expressions (x += value, x *= value, etc.)
18+
pub(crate) fn goto_assignments(
19+
db: &RootDatabase,
20+
position: FilePosition,
21+
) -> Option<RangeInfo<Vec<NavigationTarget>>> {
22+
let sema = &Semantics::new(db);
23+
24+
let def = find_definition_at_position(sema, position)?;
25+
26+
let Definition::Local(_) = def else {
27+
return None;
28+
};
29+
30+
find_assignments_for_def(sema, def, position)
31+
}
32+
33+
fn find_definition_at_position(
34+
sema: &Semantics<'_, RootDatabase>,
35+
position: FilePosition,
36+
) -> Option<Definition> {
37+
let file = sema.parse_guess_edition(position.file_id);
38+
let token =
39+
pick_best_token(file.syntax().token_at_offset(position.offset), |kind| match kind {
40+
IDENT => 1,
41+
_ => 0,
42+
})?;
43+
44+
let token = sema.descend_into_macros_no_opaque(token, false).pop()?;
45+
let parent = token.value.parent()?;
46+
47+
IdentClass::classify_node(sema, &parent)?.definitions().pop().map(|(def, _)| def)
48+
}
49+
50+
fn find_assignments_for_def(
51+
sema: &Semantics<'_, RootDatabase>,
52+
def: Definition,
53+
position: FilePosition,
54+
) -> Option<RangeInfo<Vec<NavigationTarget>>> {
55+
let mut targets = Vec::new();
56+
57+
if let Some(nav_result) = def.try_to_nav(sema.db) {
58+
targets.push(nav_result.call_site);
59+
}
60+
61+
let usages = def.usages(sema).include_self_refs().all();
62+
63+
targets.extend(usages.iter().flat_map(|(file_id, refs)| {
64+
refs.iter().filter_map(move |file_ref| {
65+
file_ref.category.contains(ReferenceCategory::WRITE).then(|| {
66+
NavigationTarget::from_syntax(
67+
file_id.file_id(sema.db),
68+
"assignment".into(),
69+
Some(file_ref.range),
70+
file_ref.range,
71+
ide_db::SymbolKind::Local,
72+
)
73+
})
74+
})
75+
}));
76+
77+
if targets.is_empty() {
78+
return None;
79+
}
80+
81+
let range = sema
82+
.parse_guess_edition(position.file_id)
83+
.syntax()
84+
.token_at_offset(position.offset)
85+
.next()
86+
.map(|token| token.text_range())?;
87+
88+
Some(RangeInfo::new(range, targets))
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use ide_db::FileRange;
94+
use itertools::Itertools;
95+
96+
use crate::fixture;
97+
98+
fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
99+
let (analysis, position, expected) = fixture::annotations(ra_fixture);
100+
let navs = analysis.goto_assignments(position).unwrap().expect("no assignments found").info;
101+
if navs.is_empty() {
102+
panic!("unresolved reference")
103+
}
104+
105+
let cmp = |&FileRange { file_id, range }: &_| (file_id, range.start());
106+
let navs = navs
107+
.into_iter()
108+
.map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() })
109+
.sorted_by_key(cmp)
110+
.collect::<Vec<_>>();
111+
let expected = expected
112+
.into_iter()
113+
.map(|(FileRange { file_id, range }, _)| FileRange { file_id, range })
114+
.sorted_by_key(cmp)
115+
.collect::<Vec<_>>();
116+
assert_eq!(expected, navs);
117+
}
118+
119+
#[test]
120+
fn goto_assignments_reassignments() {
121+
check(
122+
r#"
123+
//- /main.rs
124+
fn main() {
125+
let mut a = 0;
126+
let mut x = 1;
127+
// ^
128+
x$0 = 2;
129+
// ^
130+
println!("{}", x);
131+
x = 3;
132+
// ^
133+
}
134+
"#,
135+
)
136+
}
137+
138+
#[test]
139+
fn goto_assignments_compound_operators() {
140+
check(
141+
r#"
142+
//- /main.rs
143+
fn main() {
144+
let mut x = 10;
145+
// ^
146+
x += 5;
147+
// ^
148+
x$0 *= 2;
149+
// ^
150+
println!("{}", x);
151+
}
152+
"#,
153+
)
154+
}
155+
156+
#[test]
157+
fn goto_assignments_struct_field_mutation() {
158+
check(
159+
r#"
160+
//- /main.rs
161+
struct Point { x: i32, y: i32 }
162+
163+
fn main() {
164+
let mut p = Point { x: 0, y: 0 };
165+
// ^
166+
p$0 = Point { x: 10, y: 20 };
167+
// ^
168+
p.x = 5; // This is not an assignment to `p` itself
169+
println!("{:?}", p);
170+
}
171+
"#,
172+
)
173+
}
174+
175+
#[test]
176+
fn goto_assignments_immutable_variable() {
177+
// Immutable variables only have the initial definition, no assignments
178+
check(
179+
r#"
180+
//- /main.rs
181+
fn main() {
182+
let x$0 = 5;
183+
// ^
184+
println!("{}", x);
185+
}
186+
"#,
187+
);
188+
}
189+
190+
#[test]
191+
fn goto_assignments_closure_capture() {
192+
check(
193+
r#"
194+
//- /main.rs
195+
fn main() {
196+
let mut x = 0;
197+
// ^
198+
let closure = |mut y: i32| {
199+
x$0 = 42;
200+
// ^
201+
y = 1; // This is a different variable
202+
};
203+
closure(0);
204+
}
205+
"#,
206+
);
207+
}
208+
209+
#[test]
210+
fn goto_assignments_loop_variable() {
211+
check(
212+
r#"
213+
//- /main.rs
214+
fn main() {
215+
for mut i in 0..3 {
216+
// ^
217+
if i > 1 {
218+
i$0 += 1;
219+
// ^
220+
}
221+
}
222+
}
223+
"#,
224+
);
225+
}
226+
227+
#[test]
228+
fn goto_assignments_shadowing() {
229+
// Each `x` is a separate variable, so only assignments to the same binding
230+
check(
231+
r#"
232+
//- /main.rs
233+
fn main() {
234+
let mut x = 1;
235+
let mut x = 2; // Different variable (shadowing)
236+
// ^
237+
x$0 = 3;
238+
// ^
239+
println!("{}", x);
240+
}
241+
"#,
242+
);
243+
}
244+
}

crates/ide/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod extend_selection;
2727
mod fetch_crates;
2828
mod file_structure;
2929
mod folding_ranges;
30+
mod goto_assignments;
3031
mod goto_declaration;
3132
mod goto_definition;
3233
mod goto_implementation;
@@ -518,6 +519,14 @@ impl Analysis {
518519
self.with_db(|db| goto_type_definition::goto_type_definition(db, position))
519520
}
520521

522+
/// Returns the type definitions for the symbol at `position`.
523+
pub fn goto_assignments(
524+
&self,
525+
position: FilePosition,
526+
) -> Cancellable<Option<RangeInfo<Vec<NavigationTarget>>>> {
527+
self.with_db(|db| goto_assignments::goto_assignments(db, position))
528+
}
529+
521530
pub fn find_all_refs(
522531
&self,
523532
position: FilePosition,

crates/rust-analyzer/src/handlers/request.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use syntax::{TextRange, TextSize};
3232
use triomphe::Arc;
3333
use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
3434

35+
use crate::lsp::ext::GotoAssignmentsResponse;
3536
use crate::{
3637
config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
3738
diagnostics::convert_diagnostic,
@@ -870,6 +871,21 @@ pub(crate) fn handle_goto_type_definition(
870871
Ok(Some(res))
871872
}
872873

874+
pub(crate) fn handle_goto_assignments(
875+
snap: GlobalStateSnapshot,
876+
params: lsp_types::TextDocumentPositionParams,
877+
) -> anyhow::Result<Option<GotoAssignmentsResponse>> {
878+
let _p = tracing::info_span!("handle_goto_assignments").entered();
879+
let position = try_default!(from_proto::file_position(&snap, params)?);
880+
let nav_info = match snap.analysis.goto_assignments(position)? {
881+
None => return Ok(None),
882+
Some(it) => it,
883+
};
884+
let src = FileRange { file_id: position.file_id, range: nav_info.range };
885+
let res = to_proto::goto_assignments_response(&snap, Some(src), nav_info.info)?;
886+
Ok(Some(res))
887+
}
888+
873889
pub(crate) fn handle_parent_module(
874890
snap: GlobalStateSnapshot,
875891
params: lsp_types::TextDocumentPositionParams,

crates/rust-analyzer/src/lsp/ext.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,40 @@ impl Request for ChildModules {
407407
const METHOD: &'static str = "experimental/childModules";
408408
}
409409

410+
pub enum GotoAssignments {}
411+
412+
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
413+
#[serde(untagged)]
414+
pub enum GotoAssignmentsResponse {
415+
Scalar(lsp_types::Location),
416+
Array(Vec<lsp_types::Location>),
417+
Link(Vec<lsp_types::LocationLink>),
418+
}
419+
420+
impl From<lsp_types::Location> for GotoAssignmentsResponse {
421+
fn from(location: lsp_types::Location) -> Self {
422+
GotoAssignmentsResponse::Scalar(location)
423+
}
424+
}
425+
426+
impl From<Vec<lsp_types::Location>> for GotoAssignmentsResponse {
427+
fn from(locations: Vec<lsp_types::Location>) -> Self {
428+
GotoAssignmentsResponse::Array(locations)
429+
}
430+
}
431+
432+
impl From<Vec<lsp_types::LocationLink>> for GotoAssignmentsResponse {
433+
fn from(locations: Vec<lsp_types::LocationLink>) -> Self {
434+
GotoAssignmentsResponse::Link(locations)
435+
}
436+
}
437+
438+
impl Request for GotoAssignments {
439+
type Params = lsp_types::TextDocumentPositionParams;
440+
type Result = Option<GotoAssignmentsResponse>;
441+
const METHOD: &'static str = "experimental/gotoAssignments";
442+
}
443+
410444
pub enum JoinLines {}
411445

412446
impl Request for JoinLines {

crates/rust-analyzer/src/lsp/to_proto.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use semver::VersionReq;
2323
use serde_json::to_value;
2424
use vfs::AbsPath;
2525

26+
use crate::lsp::ext::GotoAssignmentsResponse;
2627
use crate::{
2728
config::{CallInfoConfig, Config},
2829
global_state::GlobalStateSnapshot,
@@ -1080,6 +1081,29 @@ pub(crate) fn goto_definition_response(
10801081
}
10811082
}
10821083

1084+
pub(crate) fn goto_assignments_response(
1085+
snap: &GlobalStateSnapshot,
1086+
src: Option<FileRange>,
1087+
targets: Vec<NavigationTarget>,
1088+
) -> Cancellable<GotoAssignmentsResponse> {
1089+
if snap.config.location_link() {
1090+
let links = targets
1091+
.into_iter()
1092+
.unique_by(|nav| (nav.file_id, nav.full_range, nav.focus_range))
1093+
.map(|nav| location_link(snap, src, nav))
1094+
.collect::<Cancellable<Vec<_>>>()?;
1095+
Ok(links.into())
1096+
} else {
1097+
let locations = targets
1098+
.into_iter()
1099+
.map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() })
1100+
.unique()
1101+
.map(|range| location(snap, range))
1102+
.collect::<Cancellable<Vec<_>>>()?;
1103+
Ok(locations.into())
1104+
}
1105+
}
1106+
10831107
fn outside_workspace_annotation_id() -> String {
10841108
String::from("OutsideWorkspace")
10851109
}

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,7 @@ impl GlobalState {
12021202
.on::<NO_RETRY, lsp_ext::ExpandMacro>(handlers::handle_expand_macro)
12031203
.on::<NO_RETRY, lsp_ext::ParentModule>(handlers::handle_parent_module)
12041204
.on::<NO_RETRY, lsp_ext::ChildModules>(handlers::handle_child_modules)
1205+
.on::<NO_RETRY, lsp_ext::GotoAssignments>(handlers::handle_goto_assignments)
12051206
.on::<NO_RETRY, lsp_ext::Runnables>(handlers::handle_runnables)
12061207
.on::<NO_RETRY, lsp_ext::RelatedTests>(handlers::handle_related_tests)
12071208
.on::<NO_RETRY, lsp_ext::CodeActionRequest>(handlers::handle_code_action)

0 commit comments

Comments
 (0)