@@ -8,19 +8,18 @@ use std::borrow::Cow;
88use crate :: find_node:: covering_node;
99use crate :: stub_mapping:: StubMapper ;
1010use ruff_db:: parsed:: ParsedModuleRef ;
11- use ruff_python_ast:: ExprCall ;
1211use ruff_python_ast:: { self as ast, AnyNodeRef } ;
13- use ruff_python_parser:: TokenKind ;
12+ use ruff_python_parser:: { TokenKind , Tokens } ;
1413use ruff_text_size:: { Ranged , TextRange , TextSize } ;
15- use ty_python_semantic:: HasDefinition ;
16- use ty_python_semantic:: ImportAliasResolution ;
14+
1715use ty_python_semantic:: ResolvedDefinition ;
1816use ty_python_semantic:: types:: Type ;
1917use ty_python_semantic:: types:: ide_support:: {
2018 call_signature_details, definitions_for_keyword_argument,
2119} ;
2220use 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>(
794884fn 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