Skip to content

Commit 9d1ffd6

Browse files
[ty] Implement go-to for binary and unary operators (#21001)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 2dbca63 commit 9d1ffd6

File tree

12 files changed

+774
-121
lines changed

12 files changed

+774
-121
lines changed

crates/ruff_python_parser/src/token.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ impl TokenKind {
486486
///
487487
/// [`as_unary_operator`]: TokenKind::as_unary_operator
488488
#[inline]
489-
pub(crate) const fn as_unary_arithmetic_operator(self) -> Option<UnaryOp> {
489+
pub const fn as_unary_arithmetic_operator(self) -> Option<UnaryOp> {
490490
Some(match self {
491491
TokenKind::Plus => UnaryOp::UAdd,
492492
TokenKind::Minus => UnaryOp::USub,
@@ -501,7 +501,7 @@ impl TokenKind {
501501
///
502502
/// [`as_unary_arithmetic_operator`]: TokenKind::as_unary_arithmetic_operator
503503
#[inline]
504-
pub(crate) const fn as_unary_operator(self) -> Option<UnaryOp> {
504+
pub const fn as_unary_operator(self) -> Option<UnaryOp> {
505505
Some(match self {
506506
TokenKind::Plus => UnaryOp::UAdd,
507507
TokenKind::Minus => UnaryOp::USub,
@@ -514,7 +514,7 @@ impl TokenKind {
514514
/// Returns the [`BoolOp`] that corresponds to this token kind, if it is a boolean operator,
515515
/// otherwise return [None].
516516
#[inline]
517-
pub(crate) const fn as_bool_operator(self) -> Option<BoolOp> {
517+
pub const fn as_bool_operator(self) -> Option<BoolOp> {
518518
Some(match self {
519519
TokenKind::And => BoolOp::And,
520520
TokenKind::Or => BoolOp::Or,
@@ -528,7 +528,7 @@ impl TokenKind {
528528
/// Use [`as_augmented_assign_operator`] to match against an augmented assignment token.
529529
///
530530
/// [`as_augmented_assign_operator`]: TokenKind::as_augmented_assign_operator
531-
pub(crate) const fn as_binary_operator(self) -> Option<Operator> {
531+
pub const fn as_binary_operator(self) -> Option<Operator> {
532532
Some(match self {
533533
TokenKind::Plus => Operator::Add,
534534
TokenKind::Minus => Operator::Sub,
@@ -550,7 +550,7 @@ impl TokenKind {
550550
/// Returns the [`Operator`] that corresponds to this token kind, if it is
551551
/// an augmented assignment operator, or [`None`] otherwise.
552552
#[inline]
553-
pub(crate) const fn as_augmented_assign_operator(self) -> Option<Operator> {
553+
pub const fn as_augmented_assign_operator(self) -> Option<Operator> {
554554
Some(match self {
555555
TokenKind::PlusEqual => Operator::Add,
556556
TokenKind::MinusEqual => Operator::Sub,

crates/ty_ide/src/goto.rs

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,18 @@ use std::borrow::Cow;
88
use crate::find_node::covering_node;
99
use crate::stub_mapping::StubMapper;
1010
use ruff_db::parsed::ParsedModuleRef;
11-
use ruff_python_ast::ExprCall;
1211
use ruff_python_ast::{self as ast, AnyNodeRef};
13-
use ruff_python_parser::TokenKind;
12+
use ruff_python_parser::{TokenKind, Tokens};
1413
use ruff_text_size::{Ranged, TextRange, TextSize};
15-
use ty_python_semantic::HasDefinition;
16-
use ty_python_semantic::ImportAliasResolution;
14+
1715
use ty_python_semantic::ResolvedDefinition;
1816
use ty_python_semantic::types::Type;
1917
use ty_python_semantic::types::ide_support::{
2018
call_signature_details, definitions_for_keyword_argument,
2119
};
2220
use ty_python_semantic::{
23-
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
21+
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
22+
definitions_for_name,
2423
};
2524

2625
#[derive(Clone, Debug)]
@@ -30,6 +29,28 @@ pub(crate) enum GotoTarget<'a> {
3029
ClassDef(&'a ast::StmtClassDef),
3130
Parameter(&'a ast::Parameter),
3231

32+
/// Go to on the operator of a binary operation.
33+
///
34+
/// ```py
35+
/// a + b
36+
/// ^
37+
/// ```
38+
BinOp {
39+
expression: &'a ast::ExprBinOp,
40+
operator_range: TextRange,
41+
},
42+
43+
/// Go to where the operator of a unary operation is defined.
44+
///
45+
/// ```py
46+
/// -a
47+
/// ^
48+
/// ```
49+
UnaryOp {
50+
expression: &'a ast::ExprUnaryOp,
51+
operator_range: TextRange,
52+
},
53+
3354
/// Multi-part module names
3455
/// Handles both `import foo.bar` and `from foo.bar import baz` cases
3556
/// ```py
@@ -166,7 +187,7 @@ pub(crate) enum GotoTarget<'a> {
166187
/// The callable that can actually be selected by a cursor
167188
callable: ast::ExprRef<'a>,
168189
/// The call of the callable
169-
call: &'a ExprCall,
190+
call: &'a ast::ExprCall,
170191
},
171192
}
172193

@@ -295,6 +316,16 @@ impl GotoTarget<'_> {
295316
| GotoTarget::TypeParamTypeVarTupleName(_)
296317
| GotoTarget::NonLocal { .. }
297318
| GotoTarget::Globals { .. } => return None,
319+
GotoTarget::BinOp { expression, .. } => {
320+
let (_, ty) =
321+
ty_python_semantic::definitions_for_bin_op(model.db(), model, expression)?;
322+
ty
323+
}
324+
GotoTarget::UnaryOp { expression, .. } => {
325+
let (_, ty) =
326+
ty_python_semantic::definitions_for_unary_op(model.db(), model, expression)?;
327+
ty
328+
}
298329
};
299330

300331
Some(ty)
@@ -451,6 +482,23 @@ impl GotoTarget<'_> {
451482
}
452483
}
453484

485+
GotoTarget::BinOp { expression, .. } => {
486+
let model = SemanticModel::new(db, file);
487+
488+
let (definitions, _) =
489+
ty_python_semantic::definitions_for_bin_op(db, &model, expression)?;
490+
491+
Some(DefinitionsOrTargets::Definitions(definitions))
492+
}
493+
494+
GotoTarget::UnaryOp { expression, .. } => {
495+
let model = SemanticModel::new(db, file);
496+
let (definitions, _) =
497+
ty_python_semantic::definitions_for_unary_op(db, &model, expression)?;
498+
499+
Some(DefinitionsOrTargets::Definitions(definitions))
500+
}
501+
454502
_ => None,
455503
}
456504
}
@@ -524,13 +572,15 @@ impl GotoTarget<'_> {
524572
}
525573
GotoTarget::NonLocal { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
526574
GotoTarget::Globals { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
575+
GotoTarget::BinOp { .. } | GotoTarget::UnaryOp { .. } => None,
527576
}
528577
}
529578

530579
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
531580
pub(crate) fn from_covering_node<'a>(
532581
covering_node: &crate::find_node::CoveringNode<'a>,
533582
offset: TextSize,
583+
tokens: &Tokens,
534584
) -> Option<GotoTarget<'a>> {
535585
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
536586

@@ -690,6 +740,44 @@ impl GotoTarget<'_> {
690740
}
691741
},
692742

743+
AnyNodeRef::ExprBinOp(binary) => {
744+
if offset >= binary.left.end() && offset < binary.right.start() {
745+
let between_operands =
746+
tokens.in_range(TextRange::new(binary.left.end(), binary.right.start()));
747+
if let Some(operator_token) = between_operands
748+
.iter()
749+
.find(|token| token.kind().as_binary_operator().is_some())
750+
&& operator_token.range().contains_inclusive(offset)
751+
{
752+
return Some(GotoTarget::BinOp {
753+
expression: binary,
754+
operator_range: operator_token.range(),
755+
});
756+
}
757+
}
758+
759+
Some(GotoTarget::Expression(binary.into()))
760+
}
761+
762+
AnyNodeRef::ExprUnaryOp(unary) => {
763+
if offset >= unary.start() && offset < unary.operand.start() {
764+
let before_operand =
765+
tokens.in_range(TextRange::new(unary.start(), unary.operand.start()));
766+
767+
if let Some(operator_token) = before_operand
768+
.iter()
769+
.find(|token| token.kind().as_unary_operator().is_some())
770+
&& operator_token.range().contains_inclusive(offset)
771+
{
772+
return Some(GotoTarget::UnaryOp {
773+
expression: unary,
774+
operator_range: operator_token.range(),
775+
});
776+
}
777+
}
778+
Some(GotoTarget::Expression(unary.into()))
779+
}
780+
693781
node => {
694782
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
695783
let parent = covering_node.parent();
@@ -737,6 +825,8 @@ impl Ranged for GotoTarget<'_> {
737825
GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range,
738826
GotoTarget::NonLocal { identifier, .. } => identifier.range,
739827
GotoTarget::Globals { identifier, .. } => identifier.range,
828+
GotoTarget::BinOp { operator_range, .. }
829+
| GotoTarget::UnaryOp { operator_range, .. } => *operator_range,
740830
}
741831
}
742832
}
@@ -794,7 +884,7 @@ fn definitions_for_expression<'db>(
794884
fn definitions_for_callable<'db>(
795885
db: &'db dyn crate::Db,
796886
file: ruff_db::files::File,
797-
call: &ExprCall,
887+
call: &ast::ExprCall,
798888
) -> Vec<ResolvedDefinition<'db>> {
799889
let model = SemanticModel::new(db, file);
800890
// Attempt to refine to a specific call
@@ -835,14 +925,24 @@ pub(crate) fn find_goto_target(
835925
| TokenKind::Complex
836926
| TokenKind::Float
837927
| TokenKind::Int => 1,
928+
929+
TokenKind::Comment => -1,
930+
931+
// if we have a<CURSOR>+b`, prefer the `+` token (by respecting the token ordering)
932+
// This matches VS Code's behavior where it sends the start of the clicked token as offset.
933+
kind if kind.as_binary_operator().is_some() || kind.as_unary_operator().is_some() => 1,
838934
_ => 0,
839935
})?;
840936

937+
if token.kind().is_comment() {
938+
return None;
939+
}
940+
841941
let covering_node = covering_node(parsed.syntax().into(), token.range())
842942
.find_first(|node| node.is_identifier() || node.is_expression())
843943
.ok()?;
844944

845-
GotoTarget::from_covering_node(&covering_node, offset)
945+
GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens())
846946
}
847947

848948
/// Helper function to resolve a module name and create a navigation target.

0 commit comments

Comments
 (0)