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
276 changes: 219 additions & 57 deletions pyrefly/lib/lsp/non_wasm/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ use lsp_types::request::CallHierarchyIncomingCalls;
use lsp_types::request::CallHierarchyOutgoingCalls;
use lsp_types::request::CallHierarchyPrepare;
use lsp_types::request::CodeActionRequest;
use lsp_types::request::CodeActionResolveRequest;
use lsp_types::request::Completion;
use lsp_types::request::DocumentDiagnosticRequest;
use lsp_types::request::DocumentHighlightRequest;
Expand Down Expand Up @@ -216,6 +217,7 @@ use pyrefly_util::watch_pattern::WatchPattern;
use ruff_text_size::Ranged;
use ruff_text_size::TextRange;
use ruff_text_size::TextSize;
use serde::Deserialize;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
Expand Down Expand Up @@ -896,6 +898,7 @@ pub fn capabilities(
CodeActionKind::REFACTOR_INLINE,
CodeActionKind::SOURCE_FIX_ALL,
]),
resolve_provider: Some(true),
..Default::default()
})),
completion_provider: Some(CompletionOptions {
Expand Down Expand Up @@ -1116,6 +1119,23 @@ fn record_code_action_telemetry(
event.finish_and_record(telemetry, None);
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum CodeActionResolveData {
IntroduceParameter {
uri: Url,
range: Range,
title: String,
},
}

fn code_action_resolve_data(action: &CodeAction) -> Option<CodeActionResolveData> {
action
.data
.clone()
.and_then(|data| serde_json::from_value(data).ok())
}

impl Server {
const FILEWATCHER_ID: &str = "FILEWATCHER";

Expand Down Expand Up @@ -1507,6 +1527,30 @@ impl Server {
.unwrap_or_default()),
));
}
} else if let Some(params) = as_request::<CodeActionResolveRequest>(&x) {
if let Some(params) = self
.extract_request_params_or_send_err_response::<CodeActionResolveRequest>(
params, &x.id,
)
{
if let Some(CodeActionResolveData::IntroduceParameter { uri, .. }) =
code_action_resolve_data(&params)
{
self.set_file_stats(uri.clone(), telemetry_event);
}
let activity_key = telemetry_event.activity_key.as_ref();
Comment on lines +1530 to +1541
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

codeAction/resolve requests are currently subject to the generic “cancel on subsequent mutation” behavior because CodeActionResolveRequest::METHOD is not included in the ONLY_ONCE allowlist. Since resolve requests commonly refer to a code action computed for an earlier document version, canceling them on subsequent edits can cause the client to receive RequestCanceled and never get a resolved edit. Consider adding CodeActionResolveRequest::METHOD to ONLY_ONCE (similar to ResolveCompletionItem::METHOD).

Copilot uses AI. Check for mistakes.
let file_stats = telemetry_event.file_stats.as_ref();
self.send_response(new_response(
x.id,
Ok(self.code_action_resolve(
&transaction,
params,
telemetry,
activity_key,
file_stats,
)),
));
}
} else if let Some(params) = as_request::<Completion>(&x) {
if let Some(params) = self
.extract_request_params_or_send_err_response::<Completion>(params, &x.id)
Expand Down Expand Up @@ -3427,6 +3471,33 @@ impl Server {
}))
}

fn workspace_edit_for_refactor_edits(
&self,
edits: Vec<(ModuleInfo, TextRange, String)>,
) -> Option<WorkspaceEdit> {
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
for (module, edit_range, new_text) in edits {
let Some(lsp_location) = self.to_lsp_location(&TextRangeWithModule {
module,
range: edit_range,
}) else {
continue;
};
changes.entry(lsp_location.uri).or_default().push(TextEdit {
range: lsp_location.range,
new_text,
});
}
if changes.is_empty() {
None
} else {
Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
})
}
}

fn code_action(
&self,
transaction: &Transaction<'_>,
Expand All @@ -3442,15 +3513,25 @@ impl Server {
)?;
let import_format = lsp_config.and_then(|c| c.import_format).unwrap_or_default();
let module_info = transaction.get_module_info(&handle)?;
let range = self.from_lsp_range(uri, &module_info, params.range);
let lsp_range = params.range;
let range = self.from_lsp_range(uri, &module_info, lsp_range.clone());
let only_kinds = params.context.only.as_ref();
let allow_quickfix = only_kinds
.is_none_or(|kinds| kinds.iter().any(|kind| kind == &CodeActionKind::QUICKFIX));
let allow_fix_all = only_kinds.is_none_or(|kinds| {
kinds
.iter()
.any(|kind| kind == &CodeActionKind::SOURCE_FIX_ALL)
});
let kind_matches = |requested: &CodeActionKind, actual: &CodeActionKind| {
let requested = requested.as_str();
let actual = actual.as_str();
actual == requested
|| (actual.starts_with(requested)
&& actual
.as_bytes()
.get(requested.len())
.is_some_and(|byte| *byte == b'.'))
};
let allows_kind = |kind: &CodeActionKind| {
only_kinds
.is_none_or(|kinds| kinds.iter().any(|requested| kind_matches(requested, kind)))
};
let allow_quickfix = allows_kind(&CodeActionKind::QUICKFIX);
let allow_fix_all = allows_kind(&CodeActionKind::SOURCE_FIX_ALL);
let mut actions = Vec::new();
let server_state = self.telemetry_state();
let start = Instant::now();
Expand Down Expand Up @@ -3525,122 +3606,203 @@ impl Server {
file_stats,
);
}
// Optimization: do not calculate refactors for automated codeactions since they're expensive
// If we had lazy code actions, we could keep them.
if let Some(trigger_kind) = params.context.trigger_kind
&& trigger_kind == CodeActionTriggerKind::AUTOMATIC
{
// Optimization: do not calculate refactors for automated codeactions since they're expensive.
// If the client doesn't provide a trigger kind, treat unfiltered requests as automatic.
let is_automatic = only_kinds.is_none()
&& matches!(
params.context.trigger_kind,
Some(CodeActionTriggerKind::AUTOMATIC) | None
);
if is_automatic {
return (!actions.is_empty()).then_some(actions);
}
let mut push_refactor_actions = |refactors: Vec<LocalRefactorCodeAction>| {
for action in refactors {
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
for (module, edit_range, new_text) in action.edits {
let Some(lsp_location) = self.to_lsp_location(&TextRangeWithModule {
module,
range: edit_range,
}) else {
let mut push_refactor_actions =
|actions: &mut Vec<CodeActionOrCommand>, refactors: Vec<LocalRefactorCodeAction>| {
for action in refactors {
let Some(edit) = self.workspace_edit_for_refactor_edits(action.edits) else {
continue;
};
changes.entry(lsp_location.uri).or_default().push(TextEdit {
range: lsp_location.range,
new_text,
});
}
if changes.is_empty() {
continue;
}
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: action.title,
kind: Some(action.kind),
edit: Some(WorkspaceEdit {
changes: Some(changes),
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: action.title,
kind: Some(action.kind),
edit: Some(edit),
..Default::default()
}),
..Default::default()
}));
}
};
}));
}
};
macro_rules! timed_refactor_action {
($name:expr, $call:expr) => {{
let start = Instant::now();
if let Some(refactors) = $call {
push_refactor_actions(refactors);
record_code_action_telemetry(
$name,
start,
&server_state,
telemetry,
activity_key,
file_stats,
);
($name:expr, $kind:expr, $call:expr) => {{
if allows_kind($kind) {
let start = Instant::now();
if let Some(refactors) = $call {
push_refactor_actions(&mut actions, refactors);
record_code_action_telemetry(
$name,
start,
&server_state,
telemetry,
activity_key,
file_stats,
);
}
}
}};
}
let refactor_move_kind = CodeActionKind::new("refactor.move");
timed_refactor_action!(
"extract_field",
&CodeActionKind::REFACTOR_EXTRACT,
transaction.extract_field_code_actions(&handle, range)
);
timed_refactor_action!(
"extract_variable",
&CodeActionKind::REFACTOR_EXTRACT,
transaction.extract_variable_code_actions(&handle, range)
);
timed_refactor_action!(
"invert_boolean",
&CodeActionKind::REFACTOR_REWRITE,
transaction.invert_boolean_code_actions(&handle, range)
);
timed_refactor_action!(
"extract_function",
&CodeActionKind::REFACTOR_EXTRACT,
transaction.extract_function_code_actions(&handle, range)
);
timed_refactor_action!(
"extract_superclass",
&CodeActionKind::REFACTOR_EXTRACT,
transaction.extract_superclass_code_actions(&handle, range)
);
timed_refactor_action!(
"inline_variable",
&CodeActionKind::REFACTOR_INLINE,
transaction.inline_variable_code_actions(&handle, range)
);
timed_refactor_action!(
"inline_method",
&CodeActionKind::REFACTOR_INLINE,
transaction.inline_method_code_actions(&handle, range)
);
timed_refactor_action!(
"inline_parameter",
&CodeActionKind::REFACTOR_INLINE,
transaction.inline_parameter_code_actions(&handle, range)
);
timed_refactor_action!(
"pull_members_up",
&refactor_move_kind,
transaction.pull_members_up_code_actions(&handle, range)
);
timed_refactor_action!(
"push_members_down",
&refactor_move_kind,
transaction.push_members_down_code_actions(&handle, range)
);
timed_refactor_action!(
"move_module_member",
&refactor_move_kind,
transaction.move_module_member_code_actions(&handle, range, import_format)
);
timed_refactor_action!(
"make_local_function_top_level",
&refactor_move_kind,
transaction.make_local_function_top_level_code_actions(&handle, range, import_format)
);
timed_refactor_action!(
"introduce_parameter",
transaction.introduce_parameter_code_actions(&handle, range)
);
if allows_kind(&CodeActionKind::REFACTOR_EXTRACT) {
let start = Instant::now();
if let Some(titles) = transaction.introduce_parameter_action_titles(&handle, range) {
for title in titles {
let data = CodeActionResolveData::IntroduceParameter {
uri: uri.clone(),
range: lsp_range.clone(),
title: title.clone(),
};
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title,
kind: Some(CodeActionKind::REFACTOR_EXTRACT),
data: Some(
serde_json::to_value(data)
.expect("introduce_parameter code action data is serializable"),
),
..Default::default()
}));
Comment on lines +3712 to +3729
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

New behavior returns unresolved introduce_parameter code actions with data and relies on codeAction/resolve to populate edit. There doesn’t appear to be an LSP interaction test that exercises the full round-trip (list action -> resolve -> verify edit applied). Adding an integration test here would guard against regressions in serialization, request routing, and title matching.

Copilot uses AI. Check for mistakes.
}
record_code_action_telemetry(
"introduce_parameter",
start,
&server_state,
telemetry,
activity_key,
file_stats,
);
}
}
timed_refactor_action!(
"convert_star_import",
&CodeActionKind::REFACTOR_REWRITE,
transaction.convert_star_import_code_actions(&handle, range)
);
if let Some(action) =
convert_module_package_code_actions(&self.initialize_params.capabilities, uri)
if allows_kind(&refactor_move_kind)
&& let Some(action) =
convert_module_package_code_actions(&self.initialize_params.capabilities, uri)
{
actions.push(action);
}
(!actions.is_empty()).then_some(actions)
}

fn code_action_resolve(
&self,
transaction: &Transaction<'_>,
mut action: CodeAction,
telemetry: &dyn Telemetry,
activity_key: Option<&ActivityKey>,
file_stats: Option<&TelemetryFileStats>,
) -> CodeAction {
let Some(data) = code_action_resolve_data(&action) else {
return action;
};
match data {
CodeActionResolveData::IntroduceParameter { uri, range, title } => {
let (handle, _) = match self.make_handle_with_lsp_analysis_config_if_enabled(
&uri,
Some(CodeActionResolveRequest::METHOD),
) {
Some(result) => result,
None => return action,
};
let Some(module_info) = transaction.get_module_info(&handle) else {
return action;
};
let selection = self.from_lsp_range(&uri, &module_info, range);
let start = Instant::now();
if let Some(refactors) =
transaction.introduce_parameter_code_actions(&handle, selection)
{
if let Some(refactor) = refactors
.into_iter()
.find(|candidate| candidate.title == title)
{
if let Some(edit) = self.workspace_edit_for_refactor_edits(refactor.edits) {
action.edit = Some(edit);
}
}
}
let server_state = self.telemetry_state();
record_code_action_telemetry(
"introduce_parameter_resolve",
start,
&server_state,
telemetry,
activity_key,
file_stats,
);
}
}
action
}

fn document_highlight(
&self,
transaction: &Transaction<'_>,
Expand Down
Loading
Loading