impl implement codeaction/resolve #2540#2542
impl implement codeaction/resolve #2540#2542asukaminato0721 wants to merge 5 commits intofacebook:mainfrom
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Implements codeAction/resolve support in the non-wasm LSP server to make expensive refactors (notably introduce_parameter) lazy/resolved-on-demand, and reduces automatic code-action request cost by tightening refactor computation based on context.only and triggerKind.
Changes:
- Advertise
codeActionProvider.resolveProviderand handlecodeAction/resolveto resolveintroduce_parameteredits on demand. - Adjust code action kind filtering to properly respect hierarchical kinds in
context.only, and skip refactors for unfiltered automatic/unspecified-trigger requests. - Update LSP interaction tests to expect the new capability and set
triggerKindwhere refactors are required.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
pyrefly/lib/lsp/non_wasm/server.rs |
Adds resolve capability + request handling, introduces resolve data payload, refines kind filtering and automatic-request refactor skipping, and defers introduce_parameter edits to resolve. |
pyrefly/lib/state/lsp/quick_fixes/introduce_parameter.rs |
Adds a lightweight titles-only path for introduce_parameter and adjusts occurrence handling to avoid consuming ranges. |
pyrefly/lib/state/lsp.rs |
Exposes Transaction::introduce_parameter_action_titles for server use. |
pyrefly/lib/test/lsp/lsp_interaction/basic.rs |
Updates initialize-capabilities snapshot to include resolveProvider under codeActionProvider. |
pyrefly/lib/test/lsp/lsp_interaction/convert_module_package.rs |
Updates code action request contexts to include triggerKind: 1 so refactor actions are computed in tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub(crate) fn introduce_parameter_action_titles( | ||
| transaction: &Transaction<'_>, | ||
| handle: &Handle, | ||
| selection: TextRange, | ||
| ) -> Option<Vec<String>> { | ||
| let module_info = transaction.get_module_info(handle)?; | ||
| let ast = transaction.get_ast(handle)?; | ||
| let selection_text = validate_non_empty_selection(selection, module_info.code_at(selection))?; | ||
| let (_, expression_text, _, expression_range) = split_selection(selection_text, selection)?; | ||
| if !is_exact_expression(ast.as_ref(), expression_range) { | ||
| return None; | ||
| } | ||
| let function_ctx = find_function_context(ast.as_ref(), expression_range)?; |
There was a problem hiding this comment.
introduce_parameter_action_titles largely duplicates the validation / analysis logic from introduce_parameter_code_actions. This duplication makes it easy for the title computation and resolve computation to drift over time (e.g., producing titles that no longer match resolvable actions). Consider factoring out a shared helper that returns the computed context (function ctx, template, param_name, occurrence count, etc.) used by both paths.
There was a problem hiding this comment.
i'd like to see if we can share more logic
| let template = | ||
| ExpressionTemplate::new(expression_text, expression_range, &name_refs, ¶m_names); | ||
| let base_name = suggest_parameter_name(ast.as_ref(), expression_range, ¶m_names); | ||
| let param_name = unique_name(&base_name, |name| param_names.contains(name)); | ||
| let occurrence_ranges = collect_matching_expression_ranges( | ||
| module_info.contents(), | ||
| &function_ctx.function_def.body, | ||
| &template.text, | ||
| ); | ||
| let mut titles = vec![format!("Introduce parameter `{param_name}`")]; | ||
| if occurrence_ranges.len() > 1 { | ||
| titles.push(format!( | ||
| "Introduce parameter `{param_name}` (replace all occurrences)" | ||
| )); | ||
| } |
There was a problem hiding this comment.
In introduce_parameter_action_titles, collect_matching_expression_ranges computes and allocates the full list of occurrence ranges, but the titles path only needs to know whether there is more than one occurrence. This can still be expensive for large function bodies / many matches. Consider adding a cheap “count up to 2” helper (or early-exit traversal) so listing only determines 0/1/many without materializing every range.
There was a problem hiding this comment.
i wonder if benchmarking will help us understand the implications of this split
43d7434 to
966a065
Compare
kinto0
left a comment
There was a problem hiding this comment.
awesome! and quick turnaround, thank you
a few suggestions. and I'd love to see benchmarks on a really large codebase of the before / after. if you want to automate the benchmarks, they can be done in a similar way to pytorch_benchmark. otherwise, a regular timer or cli works just fine
| if is_automatic { | ||
| return (!actions.is_empty()).then_some(actions); | ||
| } | ||
| if allow_refactor { |
There was a problem hiding this comment.
introduce parameter was the slowest, but I am not convinced we can remove this gating just yet. let's keep the gating for now until we're confident it's fast
966a065 to
aeb0bdf
Compare
This comment has been minimized.
This comment has been minimized.
kinto0
left a comment
There was a problem hiding this comment.
still very interested in a benchmark to understand if this change is worthwhile. let some other comments as well
| ); | ||
| if allows_kind(&CodeActionKind::REFACTOR_EXTRACT) { | ||
| let start = Instant::now(); | ||
| if let Some(titles) = transaction.introduce_parameter_action_titles(&handle, range) |
There was a problem hiding this comment.
since I imaging we will move a lot more of these to resolve, is there a way this logic can be abstracted?
| ..Default::default() | ||
| })); | ||
| } | ||
| record_code_action_telemetry( |
There was a problem hiding this comment.
why the new call instead of the macro here? can we abstract this away?
| pub(crate) fn introduce_parameter_action_titles( | ||
| transaction: &Transaction<'_>, | ||
| handle: &Handle, | ||
| selection: TextRange, | ||
| ) -> Option<Vec<String>> { | ||
| let module_info = transaction.get_module_info(handle)?; | ||
| let ast = transaction.get_ast(handle)?; | ||
| let selection_text = validate_non_empty_selection(selection, module_info.code_at(selection))?; | ||
| let (_, expression_text, _, expression_range) = split_selection(selection_text, selection)?; | ||
| if !is_exact_expression(ast.as_ref(), expression_range) { | ||
| return None; | ||
| } | ||
| let function_ctx = find_function_context(ast.as_ref(), expression_range)?; |
There was a problem hiding this comment.
i'd like to see if we can share more logic
| let template = | ||
| ExpressionTemplate::new(expression_text, expression_range, &name_refs, ¶m_names); | ||
| let base_name = suggest_parameter_name(ast.as_ref(), expression_range, ¶m_names); | ||
| let param_name = unique_name(&base_name, |name| param_names.contains(name)); | ||
| let occurrence_ranges = collect_matching_expression_ranges( | ||
| module_info.contents(), | ||
| &function_ctx.function_def.body, | ||
| &template.text, | ||
| ); | ||
| let mut titles = vec![format!("Introduce parameter `{param_name}`")]; | ||
| if occurrence_ranges.len() > 1 { | ||
| titles.push(format!( | ||
| "Introduce parameter `{param_name}` (replace all occurrences)" | ||
| )); | ||
| } |
There was a problem hiding this comment.
i wonder if benchmarking will help us understand the implications of this split
0ddf8a9 to
372ac12
Compare
This comment has been minimized.
This comment has been minimized.
fcb72ff to
0f1e176
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
kinto0
left a comment
There was a problem hiding this comment.
thanks for writing the benchmark! could you add details to the PR on where you tested it and what the results were?
| //! CODE_ACTION_BENCH_RANGE=START_LINE:START_COL-END_LINE:END_COL \ | ||
| //! CODE_ACTION_BENCH_ITERS=10 \ | ||
| //! CODE_ACTION_BENCH_TITLE="Introduce parameter `param`" \ | ||
| //! cargo test --release test_code_action_latency -- --ignored --nocapture |
There was a problem hiding this comment.
what were the results of this benchmark before/after?
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
|
No diffs to classify. |
|
I would hope that list would be more performant than resolve. I'm not sure this difference makes the complexity worth it. those ~18 second |
|
I noticed that it's under debug build, so... will test again in release build |
|
release build branch ver main |
I'm still not sure this performance difference is worth the complexity. on large codebases list still takes too long. what do you think? should we remove the github issue? |
Summary
Fixes #2540
Added codeAction/resolve support and advertised resolveProvider; introduce-parameter actions now return unresolved code actions with data and resolve to edits on demand.
Made refactor computation respect context.only and skip refactors for unfiltered requests without a triggerKind, reducing automatic-request cost.
Added introduce_parameter_action_titles to avoid expensive callsite edits during listing
Test Plan
updated LSP tests to expect resolveProvider and set triggerKind for refactor requests.