Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions pyrefly/lib/lsp/wasm/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use crate::state::lsp::IdentifierContext;
use crate::state::lsp::IdentifierWithContext;
use crate::state::lsp::ImportFormat;
use crate::state::lsp::MIN_CHARACTERS_TYPED_AUTOIMPORT;
use crate::state::lsp::PatternMatchParameterKind;
use crate::state::state::Transaction;
use crate::types::callable::Param;
use crate::types::types::Type;
Expand Down Expand Up @@ -836,6 +837,52 @@ impl Transaction<'_> {
}
}

/// Suggest `attr=` completions inside a class pattern like `case Point(x=...)`.
pub(crate) fn add_match_class_keyword_completions(
&self,
handle: &Handle,
covering_nodes: &[AnyNodeRef],
completions: &mut Vec<RankedCompletion>,
) {
let Some(pattern_class) = covering_nodes.iter().find_map(|node| match node {
AnyNodeRef::PatternMatchClass(pattern_class) => Some(pattern_class),
_ => None,
}) else {
return;
};
let Some(class_ty) = self.get_type_trace(handle, pattern_class.cls.range()) else {
return;
};
let Some(items) = self.ad_hoc_solve(handle, "completion_match_class_keywords", |solver| {
let instance_ty = match class_ty {
Type::ClassDef(cls) => solver.instantiate(&cls),
Type::ClassType(cls) => Type::ClassType(cls),
Type::Type(box Type::ClassType(cls)) => Type::ClassType(cls),
_ => return Vec::new(),
};
solver
.completions(instance_ty, None, true)
.into_iter()
.map(|attr| {
RankedCompletion::new(CompletionItem {
label: format!("{}=", attr.name.as_str()),
detail: attr.ty.map(|ty| ty.to_string()),
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The completion detail here uses Type::to_string(), while other completion paths in this file format types with as_lsp_string(LspDisplayMode::Hover) for consistency and readability. Consider using the same formatting helper so match-class keyword completions display types consistently with attribute completions.

Suggested change
detail: attr.ty.map(|ty| ty.to_string()),
detail: attr
.ty
.map(|ty| ty.as_lsp_string(LspDisplayMode::Hover)),

Copilot uses AI. Check for mistakes.
kind: Some(CompletionItemKind::VARIABLE),
tags: if attr.is_deprecated {
Some(vec![CompletionItemTag::DEPRECATED])
} else {
None
},
..Default::default()
})
})
.collect::<Vec<_>>()
}) else {
return;
};
completions.extend(items);
}

/// Core completion implementation returning items and incomplete flag.
pub(crate) fn completion_sorted_opt_with_incomplete<F>(
&self,
Expand Down Expand Up @@ -1045,6 +1092,14 @@ impl Transaction<'_> {
if matches!(context, IdentifierContext::MethodDef { .. }) {
Self::add_magic_method_completions(&identifier, &mut result);
}
if matches!(
context,
IdentifierContext::PatternMatch(PatternMatchParameterKind::KeywordArgName)
) && let Some(mod_module) = self.get_ast(handle)
{
let nodes = Ast::locate_node(&mod_module, position);
self.add_match_class_keyword_completions(handle, &nodes, &mut result);
}
self.add_kwargs_completions(handle, position, &mut result);
Self::add_keyword_completions(handle, &mut result);
let has_local_completions = self.add_local_variable_completions(
Expand Down Expand Up @@ -1104,6 +1159,7 @@ impl Transaction<'_> {
&mut result,
in_string_literal,
);
self.add_match_class_keyword_completions(handle, &nodes, &mut result);
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

add_match_class_keyword_completions is invoked in the generic identifier_at == None completion path, which means these attr= suggestions will appear anywhere the cursor is inside a PatternMatchClass node (including the pattern value position after =). This can surface invalid/noisy suggestions while the user is typing the subpattern (e.g. case A(a=|)), where attr= is not syntactically valid without inserting a comma. Consider gating this call to positions where a new keyword can start (e.g. when the previous non-whitespace character is ( or ,, or by checking AST/token context so the cursor is in the keyword-name slot, not inside the value subpattern).

Suggested change
self.add_match_class_keyword_completions(handle, &nodes, &mut result);
// Only offer match class keyword completions when the innermost
// node at the cursor is a PatternMatchClass. This avoids
// suggesting `attr=` in value positions such as `case A(a=|)`,
// where a new keyword cannot legally start.
if let Some(first) = nodes.first()
&& matches!(first, AnyNodeRef::PatternMatchClass(_))
{
self.add_match_class_keyword_completions(handle, &nodes, &mut result);
}

Copilot uses AI. Check for mistakes.
let dict_key_claimed = self.add_dict_key_completions(
handle,
mod_module.as_ref(),
Expand Down
31 changes: 31 additions & 0 deletions pyrefly/lib/test/lsp/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,37 @@ Completion Results:
);
}

#[test]
fn completion_match_class_keyword_dataclass() {
let code = r#"
from dataclasses import dataclass

@dataclass
class A:
a: int
b: int

x = object()
match x:
case A( ): ...
# ^
"#;
let (handles, state) = mk_multi_file_state(&[("main", code)], Require::Exports, false);
let handle = handles.get("main").unwrap();
let position = extract_cursors_for_test(code)[0];
let txn = state.transaction();
let completions = txn.completion(handle, position, ImportFormat::Absolute, true, None);
let completion_labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();

for expected in ["a=", "b="] {
assert!(
completion_labels.contains(&expected),
"missing {expected} in completions: {:?}",
completion_labels
);
}
}

#[test]
fn completion_literal_union_alias() {
let code = r#"
Expand Down
Loading