Skip to content

Conversation

@RasmusNygren
Copy link
Contributor

Summary

This handles a few edge cases related to astral-sh/ty#1563 that was not fully handled in #21549

Only using the individual Parameter nodes in the AST to determine parameter bindings is not enough when you're in a state where the syntax is invalid. For example when the name of the parameter clashes with a keyword. In those scenarios we now go up a level and inspect the Parameters node to determine if we're still binding variables.

Test Plan

New tests and ty-playground sanity-check.

@AlexWaygood AlexWaygood added server Related to the LSP server ty Multi-file analysis & type inference labels Nov 22, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 22, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 22, 2025

mypy_primer results

Changes were detected when running on open source projects
scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:98:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 45 diagnostics
+ Found 44 diagnostics

No memory usage changes detected ✅

Comment on lines 1344 to 1355
let tokens = tokens_start_before(parsed.tokens(), offset);
// `typed` contains the entire token even if the cursor sits in the
// middle of it (pa<CURSOR>ram). To correctly suggest completions
// based on the cursor's current position, we must only consider
// the characters in the token that are left of the cursor.
// E.g. we should only count "pa" if the state is "pa<CUSROR>ram".
let typed_len = typed
.text_len()
.min(tokens.last().map_or(typed.text_len(), |token| {
offset.saturating_sub(token.start())
}));
let start = offset.saturating_sub(typed_len);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically not relevant to this PR but I realised from #21570 that this calculation was logically incorrect. typed.textlen() is the length of the entire token, even if the cursor is currently in the middle of the token which is not quite what we want.

I have not managed to write a test based on suggestions that highlights this bug, but by calling is_in_variable_binding directly with different inputs it became obvious that it was previously incorrect.

@RasmusNygren RasmusNygren force-pushed the improve-variable-binding-suggestions branch from ee861d2 to 0308c41 Compare November 22, 2025 12:42
// middle of it (pa<CURSOR>ram). To correctly suggest completions
// based on the cursor's current position, we must only consider
// the characters in the token that are left of the cursor.
// E.g. we should only count "pa" if the state is "pa<CUSROR>ram".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually wasn't my intent, but maybe I was wrong. In particular, rust-analyzer and pylance work how you describe: it only considers the characters before the cursor when the cursor splits a token. ty doesn't. It uses the entire token as the query.

I initially went this route because when you select a completion, clients seem to usually replace the entire token with the selected suggestion. So in my mind, it made sense for the query to also correspond to the entire token. With that said, while rust-analyzer replaces the entire token, pylance does not.

I do kind of feel like only taking the text before the cursor is perhaps more intuitive and even more useful. (I'm undecided on the token replacement strategy though.)

So to that end, perhaps the code that extracts the typed text should cut itself off based on the offset? Then I think that would fix this without needing to do this extra arithmetic here.

Copy link
Contributor Author

@RasmusNygren RasmusNygren Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I agree with you that only considering text to the left of the cursor feels more intuitive, independently from whether the client replaces the entire token or not. Given that, it would be quite nice to only extract the relevant bit from the start, so we can be consistent without any special handling.

Would you like me to open a separate PR (maybe even an issue) for that (as I jumbled two issues into one PR here) or are you fine if I execute on that directly here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just doing that directly here is fine. :-) Thank you!

.iter()
.map(|param| param.range())
.all(|r| !r.contains_range(range))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me. Nice find!

@RasmusNygren RasmusNygren force-pushed the improve-variable-binding-suggestions branch from 0308c41 to ea88222 Compare November 24, 2025 21:36
Previously we extracted the entire token as the query
independently of the cursor position. By not doing that
you avoid having to do special range handling
to figure out the start position of the current token.

It's likely also more intuitive from a user perspective
to only consider characters left of the cursor when
suggesting autocompletions.
Autocomplete suggestions were not suppressed correctly during
some variable bindings if the parameter name was currently
matching a keyword. E.g. `def f(foo<CURSOR>` was handled
correctly but not `def f(in<CURSOR>`.
@BurntSushi BurntSushi force-pushed the improve-variable-binding-suggestions branch from ea88222 to 2b54227 Compare November 25, 2025 13:25
Copy link
Member

@BurntSushi BurntSushi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful. I also added a test that foo<CURSOR>bar only takes foo into account when offering suggestions.

Before:

2025-11-25T08.22.41-05.00.mp4

After:

2025-11-25T08.23.41-05.00.mp4

@BurntSushi BurntSushi merged commit 4628180 into astral-sh:main Nov 25, 2025
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the LSP server ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants