[ty] Implemented support for "rename" language server feature (#19551)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This PR adds support for the "rename" language server feature. It builds
upon existing functionality used for "go to references".

The "rename" feature involves two language server requests. The first is
a "prepare rename" request that determines whether renaming should be
possible for the identifier at the current offset. The second is a
"rename" request that returns a list of file ranges where the rename
should be applied.

Care must be taken when attempting to rename symbols that span files,
especially if the symbols are defined in files that are not part of the
project. We don't want to modify code in the user's Python environment
or in the vendored stub files.

I found a few bugs in the "go to references" feature when implementing
"rename", and those bug fixes are included in this PR.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
This commit is contained in:
UnboundVariable 2025-08-07 03:28:18 -07:00 committed by GitHub
parent b96aa4605b
commit b005cdb7ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1065 additions and 73 deletions

View file

@ -19,6 +19,7 @@ use ruff_python_ast::{
visitor::source_order::{SourceOrderVisitor, TraversalSignal},
};
use ruff_text_size::{Ranged, TextRange};
use ty_python_semantic::ImportAliasResolution;
/// Mode for references search behavior
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -27,8 +28,10 @@ pub enum ReferencesMode {
References,
/// Find all references but skip the declaration
ReferencesSkipDeclaration,
/// Find references for rename operations (behavior differs for imported symbols)
/// Find references for rename operations, limited to current file only
Rename,
/// Find references for multi-file rename operations (searches across all files)
RenameMultiFile,
/// Find references for document highlights (limits search to current file)
DocumentHighlights,
}
@ -42,7 +45,14 @@ pub(crate) fn references(
mode: ReferencesMode,
) -> Option<Vec<ReferenceTarget>> {
// Get the definitions for the symbol at the cursor position
let target_definitions_nav = goto_target.get_definition_targets(file, db, None)?;
// When finding references, do not resolve any local aliases.
let target_definitions_nav = goto_target.get_definition_targets(
file,
db,
None,
ImportAliasResolution::PreserveAliases,
)?;
let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect();
// Extract the target text from the goto target for fast comparison
@ -60,7 +70,12 @@ pub(crate) fn references(
);
// Check if we should search across files based on the mode
let search_across_files = !matches!(mode, ReferencesMode::DocumentHighlights);
let search_across_files = matches!(
mode,
ReferencesMode::References
| ReferencesMode::ReferencesSkipDeclaration
| ReferencesMode::RenameMultiFile
);
// Check if the symbol is potentially visible outside of this module
if search_across_files && is_symbol_externally_visible(goto_target) {
@ -211,6 +226,17 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
self.check_identifier_reference(rest_name);
}
}
AnyNodeRef::Alias(alias) if self.should_include_declaration() => {
// Handle import alias declarations
if let Some(asname) = &alias.asname {
self.check_identifier_reference(asname);
}
// Only check the original name if it matches our target text
// This is for cases where we're renaming the imported symbol name itself
if alias.name.id == self.target_text {
self.check_identifier_reference(&alias.name);
}
}
_ => {}
}
@ -231,6 +257,7 @@ impl LocalReferencesFinder<'_> {
ReferencesMode::References
| ReferencesMode::DocumentHighlights
| ReferencesMode::Rename
| ReferencesMode::RenameMultiFile
)
}
@ -259,21 +286,21 @@ impl LocalReferencesFinder<'_> {
let offset = covering_node.node().start();
if let Some(goto_target) = GotoTarget::from_covering_node(covering_node, offset) {
// Use the range of the covering node (the identifier) rather than the goto target
// This ensures we highlight just the identifier, not the entire expression
let range = covering_node.node().range();
// Get the definitions for this goto target
if let Some(current_definitions_nav) =
goto_target.get_definition_targets(self.file, self.db, None)
{
if let Some(current_definitions_nav) = goto_target.get_definition_targets(
self.file,
self.db,
None,
ImportAliasResolution::PreserveAliases,
) {
let current_definitions: Vec<NavigationTarget> =
current_definitions_nav.into_iter().collect();
// Check if any of the current definitions match our target definitions
if self.navigation_targets_match(&current_definitions) {
// Determine if this is a read or write reference
let kind = self.determine_reference_kind(covering_node);
let target = ReferenceTarget::new(self.file, range, kind);
let target =
ReferenceTarget::new(self.file, covering_node.node().range(), kind);
self.references.push(target);
}
}