@@ -2369,12 +2369,51 @@ def _determine_completion_context(self, line):
23692369 Determine whether the cursor is in an attribute or global completion context.
23702370 """
23712371 # Cursor in string/comment → GLOBAL.
2372- if self ._is_in_string_or_comment (line ):
2372+ is_string , is_in_expression = self ._is_in_string_or_comment (line )
2373+ if is_string and not is_in_expression :
23732374 return self ._CompletionContextType .GLOBAL
23742375
2375- # Match 'anything-dot-word' at end for attribute context.
2376+ # If we're in a template string expression, handle specially
2377+ if is_string and is_in_expression :
2378+ # Extract the expression part - look for the last { that isn't closed
2379+ expr_start = line .rfind ("{" )
2380+ if expr_start >= 0 :
2381+ # We're looking at the expression inside a template string
2382+ expr = line [expr_start + 1 :]
2383+ # Recursively determine the context of the expression
2384+ return self ._determine_completion_context (expr )
2385+
2386+ # Match for number literals should come first
2387+ # Handle plain number literals - should be global context
2388+ if re .search (r"^[-+]?\d+\.(\d+)?$" , line ):
2389+ return self ._CompletionContextType .GLOBAL
2390+
2391+ # Match numeric literals in parentheses followed by dot
2392+ # Handles cases like (3).to_
2393+ numeric_paren_attr_match = re .search (
2394+ r"\([-+]?\d+(\.\d*)?\)\.([a-zA-Z_][a-zA-Z0-9_]*)?$" , line
2395+ )
2396+ if numeric_paren_attr_match :
2397+ return self ._CompletionContextType .ATTRIBUTE
2398+
2399+ # Match float literals followed by dot and optional attribute
2400+ # Handles cases like 3.1.as_, -3.1.r_
2401+ float_attr_match = re .search (r"[-+]?\d+\.\d+\.([a-zA-Z_][a-zA-Z0-9_]*)?$" , line )
2402+ if float_attr_match :
2403+ return self ._CompletionContextType .ATTRIBUTE
2404+
2405+ # Handle indexed access followed by dot - like d[0].k
2406+ indexed_attr_match = re .search (
2407+ r"[a-zA-Z_][a-zA-Z0-9_]*\[[^\]]+\]\.([a-zA-Z_][a-zA-Z0-9_]*)?$" , line
2408+ )
2409+ if indexed_attr_match :
2410+ return self ._CompletionContextType .ATTRIBUTE
2411+
2412+ # Match 'word-dot-word' at end for attribute context.
23762413 # Ex: 'obj.', 'np.random.ran'.
2377- chain_match = re .search (r"([a-zA-Z_].*)\.([a-zA-Z_][a-zA-Z0-9_]*)?$" , line )
2414+ chain_match = re .search (
2415+ r"([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)?$" , line
2416+ )
23782417 if chain_match :
23792418 return self ._CompletionContextType .ATTRIBUTE
23802419
@@ -2383,12 +2422,19 @@ def _determine_completion_context(self, line):
23832422 return self ._CompletionContextType .GLOBAL
23842423
23852424 def _is_in_string_or_comment (self , text ):
2425+ """
2426+ Determine if the cursor is inside a string or comment.
2427+ Returns (is_string, is_in_expression) tuple:
2428+ - is_string: True if in any kind of string
2429+ - is_in_expression: True if inside an f-string/t-string expression
2430+ """
23862431 in_single_quote = False
23872432 in_double_quote = False
23882433 in_triple_single = False
23892434 in_triple_double = False
23902435 in_template_string = False # Covers both f-strings and t-strings
23912436 in_expression = False # For expressions in f/t-strings
2437+ expression_depth = 0 # Track nested braces in expressions
23922438 i = 0
23932439
23942440 while i < len (text ):
@@ -2397,9 +2443,15 @@ def _is_in_string_or_comment(self, text):
23972443 i + 1 < len (text )
23982444 and text [i ] in ("f" , "t" )
23992445 and (text [i + 1 ] == '"' or text [i + 1 ] == "'" )
2446+ and not (
2447+ in_single_quote
2448+ or in_double_quote
2449+ or in_triple_single
2450+ or in_triple_double
2451+ )
24002452 ):
24012453 in_template_string = True
2402- i += 1
2454+ i += 1 # Skip the 'f' or 't'
24032455
24042456 # Handle triple quotes
24052457 if i + 2 < len (text ):
@@ -2409,6 +2461,8 @@ def _is_in_string_or_comment(self, text):
24092461 and not in_triple_single
24102462 ):
24112463 in_triple_double = not in_triple_double
2464+ if not in_triple_double :
2465+ in_template_string = False
24122466 i += 3
24132467 continue
24142468 if (
@@ -2417,6 +2471,8 @@ def _is_in_string_or_comment(self, text):
24172471 and not in_triple_double
24182472 ):
24192473 in_triple_single = not in_triple_single
2474+ if not in_triple_single :
2475+ in_template_string = False
24202476 i += 3
24212477 continue
24222478
@@ -2426,54 +2482,82 @@ def _is_in_string_or_comment(self, text):
24262482 continue
24272483
24282484 # Handle expressions in f-strings or t-strings
2429- if in_template_string and text [i ] == "{" :
2485+ if (
2486+ in_template_string
2487+ and text [i ] == "{"
2488+ and not (in_expression and expression_depth > 0 and text [i - 1 ] != "{" )
2489+ ):
24302490 in_expression = True
2431- elif in_template_string and text [i ] == "}" :
2432- in_expression = False
2491+ expression_depth += 1
2492+ i += 1
2493+ continue
2494+ elif in_template_string and text [i ] == "}" and in_expression :
2495+ expression_depth -= 1
2496+ if expression_depth == 0 :
2497+ in_expression = False
2498+ i += 1
2499+ continue
2500+
2501+ # Handle nested braces within expressions
2502+ if in_expression and text [i ] == "{" :
2503+ expression_depth += 1
2504+ elif in_expression and text [i ] == "}" and expression_depth > 0 :
2505+ expression_depth -= 1
24332506
2434- # Handle quotes
2507+ # Handle quotes - also reset template string when closing quotes are encountered
24352508 if (
24362509 text [i ] == '"'
24372510 and not in_single_quote
24382511 and not in_triple_single
24392512 and not in_triple_double
24402513 ):
24412514 in_double_quote = not in_double_quote
2515+ if (
2516+ not in_double_quote
2517+ and not in_triple_double
2518+ and not in_triple_single
2519+ ):
2520+ in_template_string = False
24422521 elif (
24432522 text [i ] == "'"
24442523 and not in_double_quote
24452524 and not in_triple_double
24462525 and not in_triple_single
24472526 ):
24482527 in_single_quote = not in_single_quote
2528+ if (
2529+ not in_single_quote
2530+ and not in_triple_single
2531+ and not in_triple_double
2532+ ):
2533+ in_template_string = False
24492534
24502535 # Check for comment
2451- if text [i ] == "#" and (
2452- not (
2453- in_single_quote
2454- or in_double_quote
2455- or in_triple_single
2456- or in_triple_double
2457- )
2458- or in_expression
2536+ if text [i ] == "#" and not (
2537+ in_single_quote
2538+ or in_double_quote
2539+ or in_triple_single
2540+ or in_triple_double
24592541 ):
2460- return True
2542+ return True , False
24612543
24622544 i += 1
24632545
2464- # If we're in an expression (f-string or t-string), return False
2465- if in_expression :
2466- return False
2546+ is_string = (
2547+ in_single_quote or in_double_quote or in_triple_single or in_triple_double
2548+ )
24672549
2468- # Otherwise, return True if we're in any type of string
2550+ # Return tuple (is_string, is_in_expression)
2551+ # For nested f-strings, we're in a string but not necessarily in an expression
24692552 return (
2470- in_single_quote or in_double_quote or in_triple_single or in_triple_double
2553+ is_string or (in_template_string and not in_expression ),
2554+ in_expression and expression_depth > 0 ,
24712555 )
24722556
24732557 @context_matcher ()
24742558 def python_matcher (self , context : CompletionContext ) -> SimpleMatcherResult :
24752559 """Match attributes or global python names"""
2476- text = context .line_with_cursor
2560+ text = context .text_until_cursor
24772561 completion_type = self ._determine_completion_context (text )
24782562 if completion_type == self ._CompletionContextType .ATTRIBUTE :
24792563 try :
0 commit comments