Skip to content

Commit 0ef074d

Browse files
committed
Fail early when entity at cursor is not renamable
1 parent b37e65e commit 0ef074d

3 files changed

Lines changed: 51 additions & 24 deletions

File tree

crates/ark/src/lsp/rename.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub(crate) fn prepare_rename(
2929

3030
let offset = from_proto::offset_from_position(position, file.line_index(db), encoding)?;
3131

32-
let Some((range, placeholder)) = oak_ide::prepare_rename(db, file, offset) else {
32+
let Some((range, placeholder)) = oak_ide::prepare_rename(db, file, offset)? else {
3333
return Ok(None);
3434
};
3535

crates/oak_ide/src/rename.rs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use oak_db::Db;
66
use oak_db::Definition;
77
use oak_db::File;
88
use oak_db::Identifier;
9+
use oak_db::Name;
910
use oak_db::RootKind;
1011

1112
use crate::find_references;
@@ -22,22 +23,28 @@ pub struct RenameTargets {
2223
}
2324

2425
/// Identify the renamable identifier at `offset`, returning its range and
25-
/// current (unquoted) name. Returns `None` when the cursor is on a
26-
/// non-identifier, a `pkg::sym` namespace access, or a `$`/`@` member name.
27-
pub fn prepare_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<(TextRange, String)> {
28-
match Identifier::classify(db, file, offset)? {
29-
Identifier::Variable { name, range } => Some((range, name.text(db).to_string())),
30-
Identifier::Member { .. } => None,
31-
}
26+
/// current (unquoted) name.
27+
///
28+
/// Returns `Ok(None)` when the cursor isn't on something we can rename (a
29+
/// non-identifier, a `pkg::sym` namespace access, or a `$`/`@` member name),
30+
/// so the client simply offers no rename. Returns `Err` when the cursor is on
31+
/// a renamable identifier that we still refuse, today only a symbol defined in
32+
/// an installed package, so the client can surface why at prepare time.
33+
pub fn prepare_rename(
34+
db: &dyn Db,
35+
file: File,
36+
offset: TextSize,
37+
) -> anyhow::Result<Option<(TextRange, String)>> {
38+
Ok(renamable_at(db, file, offset)?.map(|(range, name)| (range, name.text(db).to_string())))
3239
}
3340

3441
/// Compute all rename edits across the database.
3542
///
3643
/// Returns `Err` when:
3744
/// - `new_name` is empty, is an R reserved word, or contains a literal
3845
/// backtick (which can't appear in a backtick-quoted identifier).
39-
/// - The cursor isn't on a renamable identifier (no `prepare_rename()` target).
40-
/// - Any target definition lives in an installed package.
46+
/// - The cursor isn't on a renamable identifier, or it resolves to an
47+
/// installed package (both via `renamable_at`).
4148
/// - Nothing in the database binds the cursor's symbol. Rename would
4249
/// produce no edits, so we refuse rather than silently succeed.
4350
pub fn rename(
@@ -48,19 +55,10 @@ pub fn rename(
4855
) -> anyhow::Result<RenameTargets> {
4956
let new_text = to_identifier_text(new_name)?;
5057

51-
let ident = Identifier::classify(db, file, offset);
52-
let Some(Identifier::Variable { range, .. }) = &ident else {
58+
let Some(_) = renamable_at(db, file, offset)? else {
5359
return Err(anyhow!("Can't rename identifier at cursor."));
5460
};
5561

56-
for def in file.resolve_at(db, range.start()) {
57-
if is_library_def(db, def) {
58-
return Err(anyhow!(
59-
"Can't rename: symbol is defined in an installed package."
60-
));
61-
}
62-
}
63-
6462
let ranges = find_references(db, file, offset, true);
6563
if ranges.is_empty() {
6664
return Err(anyhow!(
@@ -71,6 +69,26 @@ pub fn rename(
7169
Ok(RenameTargets { ranges, new_text })
7270
}
7371

72+
fn renamable_at<'db>(
73+
db: &'db dyn Db,
74+
file: File,
75+
offset: TextSize,
76+
) -> anyhow::Result<Option<(TextRange, Name<'db>)>> {
77+
let Some(Identifier::Variable { name, range }) = Identifier::classify(db, file, offset) else {
78+
return Ok(None);
79+
};
80+
81+
for def in file.resolve_at(db, range.start()) {
82+
if is_library_def(db, def) {
83+
return Err(anyhow!(
84+
"Can't rename: symbol is defined in an installed package."
85+
));
86+
}
87+
}
88+
89+
Ok(Some((range, name)))
90+
}
91+
7492
fn is_library_def(db: &dyn Db, def: Definition) -> bool {
7593
def.file(db)
7694
.root(db)

crates/oak_ide/tests/integration/rename.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fn test_prepare_rename_on_def() {
4848
let mut db = OakDatabase::new();
4949
let file = upsert(&mut db, "test.R", "foo <- 1\nfoo\n");
5050

51-
let result = prepare_rename(&db, file, offset(0)).unwrap();
51+
let result = prepare_rename(&db, file, offset(0)).unwrap().unwrap();
5252
assert_eq!(result, (range(0, 3), "foo".to_string()));
5353
}
5454

@@ -57,7 +57,7 @@ fn test_prepare_rename_on_use() {
5757
let mut db = OakDatabase::new();
5858
let file = upsert(&mut db, "test.R", "foo <- 1\nfoo\n");
5959

60-
let result = prepare_rename(&db, file, offset(9)).unwrap();
60+
let result = prepare_rename(&db, file, offset(9)).unwrap().unwrap();
6161
assert_eq!(result, (range(9, 12), "foo".to_string()));
6262
}
6363

@@ -66,15 +66,24 @@ fn test_prepare_rename_namespace_access_returns_none() {
6666
let mut db = OakDatabase::new();
6767
let file = upsert(&mut db, "test.R", "dplyr::mutate\n");
6868

69-
assert!(prepare_rename(&db, file, offset(7)).is_none());
69+
assert!(prepare_rename(&db, file, offset(7)).unwrap().is_none());
7070
}
7171

7272
#[test]
7373
fn test_prepare_rename_non_identifier_returns_none() {
7474
let mut db = OakDatabase::new();
7575
let file = upsert(&mut db, "test.R", "x <- 1\n");
7676

77-
assert!(prepare_rename(&db, file, offset(3)).is_none());
77+
assert!(prepare_rename(&db, file, offset(3)).unwrap().is_none());
78+
}
79+
80+
#[test]
81+
fn test_prepare_rename_library_package_symbol_errors() {
82+
let mut db = OakDatabase::new();
83+
let lib_file = build_library_package_file(&mut db, "foo <- function() {}\n");
84+
85+
let err = prepare_rename(&db, lib_file, offset(0)).unwrap_err();
86+
assert!(err.to_string().contains("installed package"));
7887
}
7988

8089
// --- rename: basic ---

0 commit comments

Comments
 (0)