From 0d159b2f38d5c5bbe8b2540ed0b0fbaa908cec30 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 17 Oct 2025 22:32:48 +0200 Subject: [PATCH] [ty] Provide completions at the end of the file --- crates/ty_ide/src/completion.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 8cfdb18099..71fdce6624 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -409,7 +409,7 @@ impl<'t> CompletionTargetTokens<'t> { TokenAt::Single(tok) => tok.end(), TokenAt::Between(_, tok) => tok.start(), }; - let before = tokens_start_before(parsed.tokens(), offset); + let before = strip_eof_newline(tokens_start_before(parsed.tokens(), offset)); Some( // Our strategy when it comes to `object.attribute` here is // to look for the `.` and then take the token immediately @@ -627,6 +627,19 @@ fn tokens_start_before(tokens: &Tokens, offset: TextSize) -> &[Token] { &tokens[..idx] } +/// Strip trailing Newline token that the parser automatically adds at the +/// end of the file. +fn strip_eof_newline(tokens: &[Token]) -> &[Token] { + if tokens + .last() + .is_some_and(|t| t.kind() == TokenKind::Newline) + { + &tokens[..tokens.len() - 1] + } else { + tokens + } +} + /// Returns a suffix of `tokens` corresponding to the `kinds` given. /// /// If a suffix of `tokens` with the given `kinds` could not be found, @@ -800,7 +813,7 @@ fn find_typed_text( offset: TextSize, ) -> Option { let source = source_text(db, file); - let tokens = tokens_start_before(parsed.tokens(), offset); + let tokens = strip_eof_newline(tokens_start_before(parsed.tokens(), offset)); let last = tokens.last()?; if !matches!(last.kind(), TokenKind::Name) { return None; @@ -3945,6 +3958,21 @@ def f[T](x: T): test.assert_completions_include("__repr__"); } + #[test] + fn attribute_at_eof_without_trailing_newline() { + // Regression test for https://github.com/astral-sh/ty/issues/1392 + // This test ensures completions work when the cursor is at the end + // of the file with no trailing newline. + + let test = cursor_test("def f(msg: str):\n msg."); + test.assert_completions_include("upper"); + test.assert_completions_include("capitalize"); + + let test = cursor_test("def f(msg: str):\n msg.u"); + test.assert_completions_include("upper"); + test.assert_completions_do_not_include("capitalize"); + } + // NOTE: The methods below are getting somewhat ridiculous. // We should refactor this by converting to using a builder // to set different modes. ---AG