From 7f41c8e6426d0ca90c60c12cf016918e9f191511 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 29 Jan 2026 15:16:06 +0900 Subject: [PATCH 1/4] impl bracket --- Cargo.lock | 1 + lsp/package.json | 6 ++ pyrefly/lib/lsp/non_wasm/server.rs | 23 ++++++- pyrefly/lib/lsp/non_wasm/workspace.rs | 1 + pyrefly/lib/state/lsp.rs | 56 +++++++++++++++++ .../test/lsp/lsp_interaction/completion.rs | 62 +++++++++++++++++++ 6 files changed, 146 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da5575d121..1b5ca7a275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "backtrace" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", diff --git a/lsp/package.json b/lsp/package.json index b0c229d8d3..afd04d64e1 100644 --- a/lsp/package.json +++ b/lsp/package.json @@ -207,6 +207,12 @@ "description": "Controls whether hover tooltips include 'Go to definition' and 'Go to type definition' navigation links.", "scope": "resource" }, + "python.analysis.completeFunctionParens": { + "type": "boolean", + "default": false, + "description": "Automatically insert parentheses when completing a function or method.", + "scope": "resource" + }, "python.pyrefly.syncNotebooks": { "type": "boolean", "default": true, diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index c7f23d1cf4..b3d991ffce 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -2003,6 +2003,17 @@ impl Server { .unwrap_or(false) } + fn supports_snippet_completions(&self) -> bool { + self.initialize_params + .capabilities + .text_document + .as_ref() + .and_then(|t| t.completion.as_ref()) + .and_then(|c| c.completion_item.as_ref()) + .and_then(|ci| ci.snippet_support) + .unwrap_or(false) + } + /// Helper to append all additional diagnostics (unreachable, unused parameters/imports/variables) fn append_ide_specific_diagnostics( transaction: &Transaction<'_>, @@ -3067,7 +3078,7 @@ impl Server { params: CompletionParams, ) -> anyhow::Result { let uri = ¶ms.text_document_position.text_document.uri; - let (handle, import_format) = match self + let (handle, lsp_config) = match self .make_handle_with_lsp_analysis_config_if_enabled(uri, Some(Completion::METHOD)) { None => { @@ -3076,16 +3087,22 @@ impl Server { items: Vec::new(), })); } - Some((x, config)) => (x, config.and_then(|c| c.import_format).unwrap_or_default()), + Some((x, config)) => (x, config), }; + let import_format = lsp_config.and_then(|c| c.import_format).unwrap_or_default(); + let complete_function_parens = lsp_config + .and_then(|c| c.complete_function_parens) + .unwrap_or(false); let (items, is_incomplete) = transaction .get_module_info(&handle) .map(|info| { - transaction.completion_with_incomplete( + transaction.completion_with_incomplete_with_function_parens( &handle, self.from_lsp_position(uri, &info, params.text_document_position.position), import_format, self.supports_completion_item_details(), + complete_function_parens, + self.supports_snippet_completions(), ) }) .unwrap_or_default(); diff --git a/pyrefly/lib/lsp/non_wasm/workspace.rs b/pyrefly/lib/lsp/non_wasm/workspace.rs index 366f7d98b5..b6dd2e32f2 100644 --- a/pyrefly/lib/lsp/non_wasm/workspace.rs +++ b/pyrefly/lib/lsp/non_wasm/workspace.rs @@ -258,6 +258,7 @@ pub struct LspAnalysisConfig { #[allow(dead_code)] pub diagnostic_mode: Option, pub import_format: Option, + pub complete_function_parens: Option, pub inlay_hints: Option, // TODO: this is not a pylance setting. it should be in pyrefly settings #[serde(default)] diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 75dec322e0..a89f2cd166 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -18,6 +18,7 @@ use lsp_types::CompletionItem; use lsp_types::CompletionItemKind; use lsp_types::CompletionItemLabelDetails; use lsp_types::CompletionItemTag; +use lsp_types::InsertTextFormat; use lsp_types::TextEdit; use pyrefly_build::handle::Handle; use pyrefly_python::ast::Ast; @@ -2743,6 +2744,25 @@ impl<'a> Transaction<'a> { position: TextSize, import_format: ImportFormat, supports_completion_item_details: bool, + ) -> (Vec, bool) { + self.completion_with_incomplete_with_function_parens( + handle, + position, + import_format, + supports_completion_item_details, + false, + false, + ) + } + + pub fn completion_with_incomplete_with_function_parens( + &self, + handle: &Handle, + position: TextSize, + import_format: ImportFormat, + supports_completion_item_details: bool, + complete_function_parens: bool, + supports_snippets: bool, ) -> (Vec, bool) { // Check if position is in a disabled range (comments) if let Some(module) = self.get_module_info(handle) { @@ -2757,6 +2777,8 @@ impl<'a> Transaction<'a> { position, import_format, supports_completion_item_details, + complete_function_parens, + supports_snippets, ); results.sort_by(|item1, item2| { item1 @@ -2792,9 +2814,12 @@ impl<'a> Transaction<'a> { position: TextSize, import_format: ImportFormat, supports_completion_item_details: bool, + complete_function_parens: bool, + supports_snippets: bool, ) -> (Vec, bool) { let mut result = Vec::new(); let mut is_incomplete = false; + let mut allow_function_call_parens = false; match self.identifier_at(handle, position) { Some(IdentifierWithContext { identifier, @@ -2853,6 +2878,7 @@ impl<'a> Transaction<'a> { identifier: _, context: IdentifierContext::Attribute { base_range, .. }, }) => { + allow_function_call_parens = true; if let Some(answers) = self.get_answers(handle) && let Some(base_type) = answers.get_type_trace(base_range) { @@ -2899,6 +2925,12 @@ impl<'a> Transaction<'a> { identifier, context, }) => { + if matches!( + context, + IdentifierContext::Expr(ExprContext::Load | ExprContext::Invalid) + ) { + allow_function_call_parens = true; + } if matches!(context, IdentifierContext::MethodDef { .. }) { Self::add_magic_method_completions(&identifier, &mut result); } @@ -2963,6 +2995,9 @@ impl<'a> Transaction<'a> { } } } + if complete_function_parens && allow_function_call_parens { + Self::add_function_call_parens(&mut result, supports_snippets); + } for item in &mut result { let sort_text = if item .tags @@ -2988,6 +3023,27 @@ impl<'a> Transaction<'a> { (result, is_incomplete) } + fn add_function_call_parens(completions: &mut [CompletionItem], supports_snippets: bool) { + for item in completions { + if item.insert_text.is_some() || item.text_edit.is_some() { + continue; + } + if !matches!( + item.kind, + Some(CompletionItemKind::FUNCTION | CompletionItemKind::METHOD) + ) { + continue; + } + + if supports_snippets { + item.insert_text = Some(format!("{}($0)", item.label)); + item.insert_text_format = Some(InsertTextFormat::SNIPPET); + } else { + item.insert_text = Some(format!("{}()", item.label)); + } + } + } + fn export_from_location( &self, handle: &Handle, diff --git a/pyrefly/lib/test/lsp/lsp_interaction/completion.rs b/pyrefly/lib/test/lsp/lsp_interaction/completion.rs index d713bcdb04..378b33f5c4 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/completion.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/completion.rs @@ -8,6 +8,7 @@ use itertools::Itertools; use lsp_types::CompletionItemKind; use lsp_types::CompletionResponse; +use lsp_types::InsertTextFormat; use lsp_types::Url; use lsp_types::notification::DidChangeTextDocument; use lsp_types::request::Completion; @@ -56,6 +57,67 @@ fn test_completion_basic() { interaction.shutdown().unwrap(); } +#[test] +fn test_completion_function_parens_snippet() { + let root = get_test_files_root(); + let mut interaction = LspInteraction::new(); + interaction.set_root(root.path().join("basic")); + interaction + .initialize(InitializeSettings { + configuration: Some(Some(json!([{ + "analysis": { + "completeFunctionParens": true + } + }]))), + capabilities: Some(json!({ + "textDocument": { + "completion": { + "completionItem": { + "snippetSupport": true + } + } + } + })), + ..Default::default() + }) + .unwrap(); + + interaction.client.did_open("foo.py"); + + let root_path = root.path().join("basic"); + let foo_path = root_path.join("foo.py"); + interaction + .client + .send_notification::(json!({ + "textDocument": { + "uri": Url::from_file_path(&foo_path).unwrap().to_string(), + "languageId": "python", + "version": 2 + }, + "contentChanges": [{ + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0} + }, + "text": "def spam(x: int) -> None:\n pass\n\nsp\n" + }], + })); + + interaction + .client + .completion("foo.py", 3, 2) + .expect_completion_response_with(|list| { + list.items.iter().any(|item| { + item.label == "spam" + && item.insert_text.as_deref() == Some("spam($0)") + && item.insert_text_format == Some(InsertTextFormat::SNIPPET) + }) + }) + .unwrap(); + + interaction.shutdown().unwrap(); +} + #[test] fn test_completion_sorted_in_sorttext_order() { let root = get_test_files_root(); From ca1044cdcc3862bdfe1426d5df8c5add506cbcb2 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 30 Jan 2026 02:22:02 +0900 Subject: [PATCH 2/4] update --- pyrefly/lib/lsp/non_wasm/server.rs | 14 ++--------- pyrefly/lib/lsp/wasm/completion.rs | 37 ++++++++++++++++++++++++++++++ pyrefly/lib/state/lsp.rs | 22 ------------------ 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index b3d991ffce..766572e911 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -250,6 +250,7 @@ use crate::lsp::non_wasm::will_rename_files::will_rename_files; use crate::lsp::non_wasm::workspace::LspAnalysisConfig; use crate::lsp::non_wasm::workspace::Workspace; use crate::lsp::non_wasm::workspace::Workspaces; +use crate::lsp::wasm::completion::supports_snippet_completions; use crate::lsp::wasm::hover::get_hover; use crate::lsp::wasm::notebook::DidChangeNotebookDocument; use crate::lsp::wasm::notebook::DidChangeNotebookDocumentParams; @@ -2003,17 +2004,6 @@ impl Server { .unwrap_or(false) } - fn supports_snippet_completions(&self) -> bool { - self.initialize_params - .capabilities - .text_document - .as_ref() - .and_then(|t| t.completion.as_ref()) - .and_then(|c| c.completion_item.as_ref()) - .and_then(|ci| ci.snippet_support) - .unwrap_or(false) - } - /// Helper to append all additional diagnostics (unreachable, unused parameters/imports/variables) fn append_ide_specific_diagnostics( transaction: &Transaction<'_>, @@ -3102,7 +3092,7 @@ impl Server { import_format, self.supports_completion_item_details(), complete_function_parens, - self.supports_snippet_completions(), + supports_snippet_completions(&self.initialize_params.capabilities), ) }) .unwrap_or_default(); diff --git a/pyrefly/lib/lsp/wasm/completion.rs b/pyrefly/lib/lsp/wasm/completion.rs index 6ab2f6d03d..5ef745cdb8 100644 --- a/pyrefly/lib/lsp/wasm/completion.rs +++ b/pyrefly/lib/lsp/wasm/completion.rs @@ -10,6 +10,7 @@ use fuzzy_matcher::skim::SkimMatcherV2; use lsp_types::CompletionItem; use lsp_types::CompletionItemKind; use lsp_types::CompletionItemTag; +use lsp_types::InsertTextFormat; use pyrefly_build::handle::Handle; use pyrefly_python::docstring::Docstring; use pyrefly_python::dunder; @@ -30,6 +31,17 @@ use crate::state::state::Transaction; use crate::types::callable::Param; use crate::types::types::Type; +/// Returns true if the client supports snippet completions in completion items. +pub(crate) fn supports_snippet_completions(capabilities: &lsp_types::ClientCapabilities) -> bool { + capabilities + .text_document + .as_ref() + .and_then(|t| t.completion.as_ref()) + .and_then(|c| c.completion_item.as_ref()) + .and_then(|ci| ci.snippet_support) + .unwrap_or(false) +} + impl Transaction<'_> { /// Adds completion items for literal types (e.g., `Literal["foo", "bar"]`). pub(crate) fn add_literal_completions_from_type( @@ -100,6 +112,31 @@ impl Transaction<'_> { }); } + /// Adds function/method completion inserts with parentheses, using snippets when supported. + pub(crate) fn add_function_call_parens( + completions: &mut [CompletionItem], + supports_snippets: bool, + ) { + for item in completions { + if item.insert_text.is_some() || item.text_edit.is_some() { + continue; + } + if !matches!( + item.kind, + Some(CompletionItemKind::FUNCTION | CompletionItemKind::METHOD) + ) { + continue; + } + + if supports_snippets { + item.insert_text = Some(format!("{}($0)", item.label)); + item.insert_text_format = Some(InsertTextFormat::SNIPPET); + } else { + item.insert_text = Some(format!("{}()", item.label)); + } + } + } + /// Retrieves documentation for an export to display in completion items. pub(crate) fn get_documentation_from_export( &self, diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index a89f2cd166..6c86685613 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -18,7 +18,6 @@ use lsp_types::CompletionItem; use lsp_types::CompletionItemKind; use lsp_types::CompletionItemLabelDetails; use lsp_types::CompletionItemTag; -use lsp_types::InsertTextFormat; use lsp_types::TextEdit; use pyrefly_build::handle::Handle; use pyrefly_python::ast::Ast; @@ -3023,27 +3022,6 @@ impl<'a> Transaction<'a> { (result, is_incomplete) } - fn add_function_call_parens(completions: &mut [CompletionItem], supports_snippets: bool) { - for item in completions { - if item.insert_text.is_some() || item.text_edit.is_some() { - continue; - } - if !matches!( - item.kind, - Some(CompletionItemKind::FUNCTION | CompletionItemKind::METHOD) - ) { - continue; - } - - if supports_snippets { - item.insert_text = Some(format!("{}($0)", item.label)); - item.insert_text_format = Some(InsertTextFormat::SNIPPET); - } else { - item.insert_text = Some(format!("{}()", item.label)); - } - } - } - fn export_from_location( &self, handle: &Handle, From ed1189c306a504930be33869f82fded728aee80e Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 31 Jan 2026 16:18:56 +0900 Subject: [PATCH 3/4] fix --- pyrefly/lib/lsp/wasm/completion.rs | 14 ++ pyrefly/lib/state/lsp.rs | 215 ----------------------------- 2 files changed, 14 insertions(+), 215 deletions(-) diff --git a/pyrefly/lib/lsp/wasm/completion.rs b/pyrefly/lib/lsp/wasm/completion.rs index c2e09aab99..8a5711e7e2 100644 --- a/pyrefly/lib/lsp/wasm/completion.rs +++ b/pyrefly/lib/lsp/wasm/completion.rs @@ -24,6 +24,7 @@ use pyrefly_types::display::LspDisplayMode; use pyrefly_types::literal::Lit; use pyrefly_types::types::Union; use ruff_python_ast::AnyNodeRef; +use ruff_python_ast::ExprContext; use ruff_python_ast::Identifier; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; @@ -482,9 +483,12 @@ impl Transaction<'_> { position: TextSize, import_format: ImportFormat, supports_completion_item_details: bool, + complete_function_parens: bool, + supports_snippets: bool, ) -> (Vec, bool) { let mut result = Vec::new(); let mut is_incomplete = false; + let mut allow_function_call_parens = false; // Because of parser error recovery, `from x impo...` looks like `from x import impo...` // If the user might be typing the `import` keyword, add that as an autocomplete option. match self.identifier_at(handle, position) { @@ -543,6 +547,7 @@ impl Transaction<'_> { identifier: _, context: IdentifierContext::Attribute { base_range, .. }, }) => { + allow_function_call_parens = true; if let Some(answers) = self.get_answers(handle) && let Some(base_type) = answers.get_type_trace(base_range) { @@ -589,6 +594,12 @@ impl Transaction<'_> { identifier, context, }) => { + if matches!( + context, + IdentifierContext::Expr(ExprContext::Load | ExprContext::Invalid) + ) { + allow_function_call_parens = true; + } if matches!(context, IdentifierContext::MethodDef { .. }) { Self::add_magic_method_completions(&identifier, &mut result); } @@ -653,6 +664,9 @@ impl Transaction<'_> { } } } + if complete_function_parens && allow_function_call_parens { + Self::add_function_call_parens(&mut result, supports_snippets); + } for item in &mut result { let sort_text = if item .tags diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 6cf90675a5..fb31c8b77a 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -2673,221 +2673,6 @@ impl<'a> Transaction<'a> { ranges } - fn completion_sorted_opt_with_incomplete( - &self, - handle: &Handle, - position: TextSize, - import_format: ImportFormat, - supports_completion_item_details: bool, - complete_function_parens: bool, - supports_snippets: bool, - ) -> (Vec, bool) { - let mut result = Vec::new(); - let mut is_incomplete = false; - let mut allow_function_call_parens = false; - match self.identifier_at(handle, position) { - Some(IdentifierWithContext { - identifier, - context: IdentifierContext::ImportedName { module_name, .. }, - }) => { - if let Some(handle) = self.import_handle(handle, module_name, None).finding() { - // Because of parser error recovery, `from x impo...` looks like `from x import impo...` - // If the user might be typing the `import` keyword, add that as an autocomplete option. - if "import".starts_with(identifier.as_str()) { - result.push(CompletionItem { - label: "import".to_owned(), - kind: Some(CompletionItemKind::KEYWORD), - ..Default::default() - }) - } - let exports = self.get_exports(&handle); - for (name, export) in exports.iter() { - let is_deprecated = match export { - ExportLocation::ThisModule(export) => export.deprecation.is_some(), - ExportLocation::OtherModule(_, _) => false, - }; - result.push(CompletionItem { - label: name.to_string(), - // todo(kylei): completion kind for exports - kind: Some(CompletionItemKind::VARIABLE), - tags: if is_deprecated { - Some(vec![CompletionItemTag::DEPRECATED]) - } else { - None - }, - ..Default::default() - }) - } - } - } - // TODO: Handle relative import (via ModuleName::new_maybe_relative) - Some(IdentifierWithContext { - identifier, - context: IdentifierContext::ImportedModule { .. }, - }) => self - .import_prefixes(handle, ModuleName::from_name(identifier.id())) - .iter() - .for_each(|module_name| { - result.push(CompletionItem { - label: module_name - .components() - .last() - .unwrap_or(&Name::empty()) - .to_string(), - detail: Some(module_name.to_string()), - kind: Some(CompletionItemKind::MODULE), - ..Default::default() - }) - }), - Some(IdentifierWithContext { - identifier: _, - context: IdentifierContext::Attribute { base_range, .. }, - }) => { - allow_function_call_parens = true; - if let Some(answers) = self.get_answers(handle) - && let Some(base_type) = answers.get_type_trace(base_range) - { - self.ad_hoc_solve(handle, |solver| { - solver - .completions(base_type, None, true) - .iter() - .for_each(|x| { - let kind = match x.ty { - Some(Type::BoundMethod(_)) => Some(CompletionItemKind::METHOD), - Some(Type::Function(_) | Type::Overload(_)) => { - Some(CompletionItemKind::FUNCTION) - } - Some(Type::Module(_)) => Some(CompletionItemKind::MODULE), - Some(Type::ClassDef(_)) => Some(CompletionItemKind::CLASS), - _ => Some(CompletionItemKind::FIELD), - }; - let ty = &x.ty; - let detail = - ty.clone().map(|t| t.as_lsp_string(LspDisplayMode::Hover)); - let documentation = self.get_docstring_for_attribute(handle, x); - result.push(CompletionItem { - label: x.name.as_str().to_owned(), - detail, - kind, - documentation, - sort_text: if x.is_reexport { - Some("1".to_owned()) - } else { - None - }, - tags: if x.is_deprecated { - Some(vec![CompletionItemTag::DEPRECATED]) - } else { - None - }, - ..Default::default() - }); - }); - }); - } - } - Some(IdentifierWithContext { - identifier, - context, - }) => { - if matches!( - context, - IdentifierContext::Expr(ExprContext::Load | ExprContext::Invalid) - ) { - allow_function_call_parens = true; - } - if matches!(context, IdentifierContext::MethodDef { .. }) { - Self::add_magic_method_completions(&identifier, &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( - handle, - Some(&identifier), - position, - &mut result, - ); - if !has_local_completions { - self.add_autoimport_completions( - handle, - &identifier, - &mut result, - import_format, - supports_completion_item_details, - ); - } - // Mark results as incomplete in the following cases so clients keep asking - // for completions as the user types more: - // 1. If identifier is below MIN_CHARACTERS_TYPED_AUTOIMPORT threshold, - // autoimport completions are skipped and will be checked once threshold - // is reached. - // 2. If local completions exist and blocked autoimport completions, - // the local completions might not match as the user continues typing, - // and autoimport completions should then be shown. - if identifier.as_str().len() < MIN_CHARACTERS_TYPED_AUTOIMPORT - || has_local_completions - { - is_incomplete = true; - } - self.add_builtins_autoimport_completions(handle, Some(&identifier), &mut result); - } - None => { - // todo(kylei): optimization, avoid duplicate ast walks - if let Some(mod_module) = self.get_ast(handle) { - let nodes = Ast::locate_node(&mod_module, position); - if nodes.is_empty() { - Self::add_keyword_completions(handle, &mut result); - self.add_local_variable_completions(handle, None, position, &mut result); - self.add_builtins_autoimport_completions(handle, None, &mut result); - } - let in_string_literal = nodes - .iter() - .any(|node| matches!(node, AnyNodeRef::ExprStringLiteral(_))); - self.add_literal_completions(handle, position, &mut result, in_string_literal); - self.add_dict_key_completions( - handle, - mod_module.as_ref(), - position, - &mut result, - ); - // in foo(x=<>, y=2<>), the first containing node is AnyNodeRef::Arguments(_) - // in foo(<>), the first containing node is AnyNodeRef::ExprCall - if let Some(first) = nodes.first() - && matches!(first, AnyNodeRef::ExprCall(_) | AnyNodeRef::Arguments(_)) - { - self.add_kwargs_completions(handle, position, &mut result); - } - } - } - } - if complete_function_parens && allow_function_call_parens { - Self::add_function_call_parens(&mut result, supports_snippets); - } - for item in &mut result { - let sort_text = if item - .tags - .as_ref() - .is_some_and(|tags| tags.contains(&CompletionItemTag::DEPRECATED)) - { - "9" - } else if item.additional_text_edits.is_some() { - "4" - } else if item.label.starts_with("__") { - "3" - } else if item.label.as_str().starts_with("_") { - "2" - } else if let Some(sort_text) = &item.sort_text { - // 1 is reserved for re-exports - sort_text.as_str() - } else { - "0" - } - .to_owned(); - item.sort_text = Some(sort_text); - } - (result, is_incomplete) - } - fn export_from_location( &self, handle: &Handle, From 8411d25f6ca6a0046b630342a354143575ad5085 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 3 Feb 2026 02:39:29 +0900 Subject: [PATCH 4/4] update fmt --- pyrefly/lib/lsp/non_wasm/server.rs | 14 +++++++--- pyrefly/lib/lsp/wasm/completion.rs | 19 +++++++++++--- pyrefly/lib/state/lsp.rs | 41 ++++++++---------------------- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index 1ea5e7c108..b73e52bf0a 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -250,6 +250,7 @@ use crate::lsp::non_wasm::will_rename_files::will_rename_files; use crate::lsp::non_wasm::workspace::LspAnalysisConfig; use crate::lsp::non_wasm::workspace::Workspace; use crate::lsp::non_wasm::workspace::Workspaces; +use crate::lsp::wasm::completion::CompletionOptions as CompletionRequestOptions; use crate::lsp::wasm::completion::supports_snippet_completions; use crate::lsp::wasm::hover::get_hover; use crate::lsp::wasm::notebook::DidChangeNotebookDocument; @@ -3083,16 +3084,21 @@ impl Server { let complete_function_parens = lsp_config .and_then(|c| c.complete_function_parens) .unwrap_or(false); + let completion_options = CompletionRequestOptions { + supports_completion_item_details: self.supports_completion_item_details(), + complete_function_parens, + supports_snippet_completions: supports_snippet_completions( + &self.initialize_params.capabilities, + ), + }; let (items, is_incomplete) = transaction .get_module_info(&handle) .map(|info| { - transaction.completion_with_incomplete_with_function_parens( + transaction.completion_with_incomplete( &handle, self.from_lsp_position(uri, &info, params.text_document_position.position), import_format, - self.supports_completion_item_details(), - complete_function_parens, - supports_snippet_completions(&self.initialize_params.capabilities), + completion_options, ) }) .unwrap_or_default(); diff --git a/pyrefly/lib/lsp/wasm/completion.rs b/pyrefly/lib/lsp/wasm/completion.rs index 8a5711e7e2..ca7704672f 100644 --- a/pyrefly/lib/lsp/wasm/completion.rs +++ b/pyrefly/lib/lsp/wasm/completion.rs @@ -46,6 +46,14 @@ use crate::state::state::Transaction; use crate::types::callable::Param; use crate::types::types::Type; +/// Options that influence completion item formatting and behavior. +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct CompletionOptions { + pub supports_completion_item_details: bool, + pub complete_function_parens: bool, + pub supports_snippet_completions: bool, +} + /// Returns true if the client supports snippet completions in completion items. pub(crate) fn supports_snippet_completions(capabilities: &lsp_types::ClientCapabilities) -> bool { capabilities @@ -482,10 +490,13 @@ impl Transaction<'_> { handle: &Handle, position: TextSize, import_format: ImportFormat, - supports_completion_item_details: bool, - complete_function_parens: bool, - supports_snippets: bool, + options: CompletionOptions, ) -> (Vec, bool) { + let CompletionOptions { + supports_completion_item_details, + complete_function_parens, + supports_snippet_completions, + } = options; let mut result = Vec::new(); let mut is_incomplete = false; let mut allow_function_call_parens = false; @@ -665,7 +676,7 @@ impl Transaction<'_> { } } if complete_function_parens && allow_function_call_parens { - Self::add_function_call_parens(&mut result, supports_snippets); + Self::add_function_call_parens(&mut result, supports_snippet_completions); } for item in &mut result { let sort_text = if item diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index fb31c8b77a..65aaaa6eba 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -65,6 +65,7 @@ use crate::config::error_kind::ErrorKind; use crate::export::exports::Export; use crate::export::exports::ExportLocation; use crate::lsp::module_helpers::collect_symbol_def_paths; +use crate::lsp::wasm::completion::CompletionOptions; use crate::state::ide::IntermediateDefinition; use crate::state::ide::import_regular_import_edit; use crate::state::ide::insert_import_edit; @@ -2584,8 +2585,8 @@ impl<'a> Transaction<'a> { Some(references) } - // Kept for backwards compatibility - used by external callers (lsp/server.rs, playground.rs) - // who don't need the is_incomplete flag + // Kept for backwards compatibility - used by external callers who don't need the + // is_incomplete flag. pub fn completion( &self, handle: &Handle, @@ -2597,7 +2598,10 @@ impl<'a> Transaction<'a> { handle, position, import_format, - supports_completion_item_details, + CompletionOptions { + supports_completion_item_details, + ..Default::default() + }, ) .0 } @@ -2608,26 +2612,7 @@ impl<'a> Transaction<'a> { handle: &Handle, position: TextSize, import_format: ImportFormat, - supports_completion_item_details: bool, - ) -> (Vec, bool) { - self.completion_with_incomplete_with_function_parens( - handle, - position, - import_format, - supports_completion_item_details, - false, - false, - ) - } - - pub fn completion_with_incomplete_with_function_parens( - &self, - handle: &Handle, - position: TextSize, - import_format: ImportFormat, - supports_completion_item_details: bool, - complete_function_parens: bool, - supports_snippets: bool, + options: CompletionOptions, ) -> (Vec, bool) { // Check if position is in a disabled range (comments) if let Some(module) = self.get_module_info(handle) { @@ -2637,14 +2622,8 @@ impl<'a> Transaction<'a> { } } - let (mut results, is_incomplete) = self.completion_sorted_opt_with_incomplete( - handle, - position, - import_format, - supports_completion_item_details, - complete_function_parens, - supports_snippets, - ); + let (mut results, is_incomplete) = + self.completion_sorted_opt_with_incomplete(handle, position, import_format, options); results.sort_by(|item1, item2| { item1 .sort_text