diff --git a/pyrefly/lib/lsp/wasm/completion.rs b/pyrefly/lib/lsp/wasm/completion.rs index f001fb7022..61b8aa2f70 100644 --- a/pyrefly/lib/lsp/wasm/completion.rs +++ b/pyrefly/lib/lsp/wasm/completion.rs @@ -376,9 +376,36 @@ impl Transaction<'_> { })); } } - Param::Varargs(None, _) - | Param::Kwargs(_, _) - | Param::PosOnly(None, _, _) => {} + Param::Varargs(None, _) | Param::PosOnly(None, _, _) => {} + Param::Kwargs(_, Type::Unpack(unpacked)) => { + if let Type::TypedDict(typed_dict) = unpacked.as_ref() { + for (name, field) in self + .ad_hoc_solve( + handle, + "completion_typed_dict_kwargs", + |solver| { + solver.type_order().typed_dict_fields(typed_dict) + }, + ) + .into_iter() + .flatten() + { + let label = format!("{}=", name.as_str()); + let detail = field.ty.to_string(); + if seen.insert((label.clone(), detail.clone())) { + completions.push(RankedCompletion::new( + CompletionItem { + label, + detail: Some(detail), + kind: Some(CompletionItemKind::VARIABLE), + ..Default::default() + }, + )); + } + } + } + } + Param::Kwargs(_, _) => {} } } } diff --git a/pyrefly/lib/test/lsp/completion.rs b/pyrefly/lib/test/lsp/completion.rs index 0fc6ffa52a..08a53a1399 100644 --- a/pyrefly/lib/test/lsp/completion.rs +++ b/pyrefly/lib/test/lsp/completion.rs @@ -1117,6 +1117,26 @@ Completion Results: ); } +#[test] +fn kwargs_completion_unpack_typed_dict() { + let code = r#" +from typing import TypedDict, Unpack + +class Movie(TypedDict): + name: str + year: int + +def foo(**kwargs: Unpack[Movie]) -> None: ... +foo( +# ^ +"#; + let report = + get_batched_lsp_operations_report_allow_error(&[("main", code)], get_default_test_report()); + let report = strip_ansi(&report); + assert!(report.contains("- (Variable) name=: str"), "{report}"); + assert!(report.contains("- (Variable) year=: int"), "{report}"); +} + #[test] fn no_value_completions_after_keyword_argument() { // `foo(a=1, x|`: because a keyword argument precedes the cursor, the next