Repository: astral-sh/ruff
File: crates/ty_ide/src/completion.rs (Rust)

Code before:
        } else {
            false
        }
    };
    if match tokens {
        [.., penultimate, _] if typed.is_some() => is_definition_keyword(penultimate),
        [.., last] if typed.is_none() => is_definition_keyword(last),
        _ => false,
    } {
        return true;
    }
    // Analyze the AST if token matching is insufficient
    // to determine if we're inside a name definition.
    is_in_variable_binding(parsed, offset, typed)
}

/// Returns true when the cursor sits on a binding statement.
/// E.g. naming a parameter, type parameter, or `for` <name>).
fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Option<&str>) -> bool {
    let range = if let Some(typed) = typed {
        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);
        TextRange::new(start, offset)
    } else {
        TextRange::empty(offset)
    };

    let covering = covering_node(parsed.syntax().into(), range);
    covering.ancestors().any(|node| match node {
        ast::AnyNodeRef::Parameter(param) => param.name.range.contains_range(range),
        ast::AnyNodeRef::TypeParamTypeVar(type_param) => {
            type_param.name.range.contains_range(range)
        }
        ast::AnyNodeRef::StmtFor(stmt_for) => stmt_for.target.range().contains_range(range),
        // The AST does not produce `ast::AnyNodeRef::Parameter` nodes for keywords
        // or otherwise invalid syntax. Rather they are captured in a
        // `ast::AnyNodeRef::Parameters` node as "empty space". To ensure
        // we still suppress suggestions even when the syntax is technically
        // invalid we extract the token under the cursor and check if it makes
        // up that "empty space" inside the Parameters Node. If it does, we know
        // that we are still binding variables, just that the current state is

Review comment (bug):
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.

Code after:
    db: &dyn Db,
    file: File,
    parsed: &ParsedModuleRef,
    offset: TextSize,
    tokens: &[Token],
    typed: Option<&str>,
) -> bool {
    is_in_comment(tokens)
        || is_in_string(tokens)
        || is_in_definition_place(db, file, parsed, offset, tokens, typed)
}

/// Whether the last token is within a comment or not.
fn is_in_comment(tokens: &[Token]) -> bool {
    tokens.last().is_some_and(|t| t.kind().is_comment())
}

/// Whether the last token is positioned within a string token (regular, f-string, t-string, etc).
///
/// Note that this will return `false` when the last token is positioned within an
/// interpolation block in an f-string or a t-string.
fn is_in_string(tokens: &[Token]) -> bool {
    tokens.last().is_some_and(|t| {
        matches!(
            t.kind(),
            TokenKind::String | TokenKind::FStringMiddle | TokenKind::TStringMiddle
        )
    })
}

/// Returns true when the tokens indicate that the definition of a new
/// name is being introduced at the end.
fn is_in_definition_place(
    db: &dyn Db,
    file: File,
    parsed: &ParsedModuleRef,
    offset: TextSize,
    tokens: &[Token],
    typed: Option<&str>,
) -> bool {
    fn is_definition_token(token: &Token) -> bool {
        matches!(
            token.kind(),
            TokenKind::Def | TokenKind::Class | TokenKind::Type | TokenKind::As | TokenKind::For
        )
    }

    let is_definition_keyword = |token: &Token| {
        if is_definition_token(token) {
            true
        } else if token.kind() == TokenKind::Name {