Skip to content

Commit ad7e11d

Browse files
fix
1 parent b26fd8d commit ad7e11d

9 files changed

Lines changed: 564 additions & 25 deletions

File tree

pyrefly/lib/lsp/non_wasm/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod external_provider;
1414
pub mod folding_ranges;
1515
pub mod lsp;
1616
pub mod module_helpers;
17+
pub mod move_symbol_new_file;
1718
mod mru;
1819
pub mod protocol;
1920
pub mod queue;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use std::collections::HashMap;
9+
10+
use dupe::Dupe;
11+
use lsp_types::ClientCapabilities;
12+
use lsp_types::CodeAction;
13+
use lsp_types::CodeActionKind;
14+
use lsp_types::CodeActionOrCommand;
15+
use lsp_types::CreateFile;
16+
use lsp_types::DocumentChangeOperation;
17+
use lsp_types::DocumentChanges;
18+
use lsp_types::OneOf;
19+
use lsp_types::OptionalVersionedTextDocumentIdentifier;
20+
use lsp_types::Position;
21+
use lsp_types::Range;
22+
use lsp_types::ResourceOp;
23+
use lsp_types::ResourceOperationKind;
24+
use lsp_types::TextDocumentEdit;
25+
use lsp_types::TextEdit;
26+
use lsp_types::Url;
27+
use lsp_types::WorkspaceEdit;
28+
use pyrefly_build::handle::Handle;
29+
use pyrefly_python::PYTHON_EXTENSIONS;
30+
use pyrefly_python::module_name::ModuleName;
31+
use pyrefly_python::module_path::ModulePath;
32+
use pyrefly_util::absolutize::Absolutize as _;
33+
use ruff_text_size::TextRange;
34+
35+
use crate::lsp::non_wasm::module_helpers::PathRemapper;
36+
use crate::lsp::non_wasm::module_helpers::module_info_to_uri;
37+
use crate::state::lsp::ImportFormat;
38+
use crate::state::state::Transaction;
39+
40+
fn supports_workspace_edit_document_changes(capabilities: &ClientCapabilities) -> bool {
41+
capabilities
42+
.workspace
43+
.as_ref()
44+
.and_then(|workspace| workspace.workspace_edit.as_ref())
45+
.and_then(|workspace_edit| workspace_edit.document_changes)
46+
.unwrap_or(false)
47+
}
48+
49+
fn supports_workspace_edit_resource_ops(
50+
capabilities: &ClientCapabilities,
51+
required: &[ResourceOperationKind],
52+
) -> bool {
53+
let supported = capabilities
54+
.workspace
55+
.as_ref()
56+
.and_then(|workspace| workspace.workspace_edit.as_ref())
57+
.and_then(|workspace_edit| workspace_edit.resource_operations.as_ref());
58+
required
59+
.iter()
60+
.all(|kind| supported.is_some_and(|ops| ops.contains(kind)))
61+
}
62+
63+
fn path_to_uri(path: &std::path::Path, remapper: Option<&PathRemapper>) -> Option<Url> {
64+
let final_path = remapper
65+
.map(|remap| remap(path).into_owned())
66+
.unwrap_or_else(|| path.to_path_buf());
67+
let abs_path = final_path.absolutize();
68+
Url::from_file_path(abs_path).ok()
69+
}
70+
71+
pub(crate) fn move_symbol_to_new_file_code_action(
72+
capabilities: &ClientCapabilities,
73+
transaction: &Transaction<'_>,
74+
handle: &Handle,
75+
uri: &Url,
76+
selection: TextRange,
77+
import_format: ImportFormat,
78+
path_remapper: Option<&PathRemapper>,
79+
) -> Option<CodeActionOrCommand> {
80+
if !supports_workspace_edit_document_changes(capabilities) {
81+
return None;
82+
}
83+
if !supports_workspace_edit_resource_ops(capabilities, &[ResourceOperationKind::Create]) {
84+
return None;
85+
}
86+
87+
let path = uri.to_file_path().ok()?;
88+
let extension = path.extension().and_then(|ext| ext.to_str())?;
89+
if !PYTHON_EXTENSIONS.contains(&extension) {
90+
return None;
91+
}
92+
93+
let context = transaction.module_member_move_context(handle, selection)?;
94+
let new_path = path
95+
.parent()?
96+
.join(format!("{}.{}", context.member_name, extension));
97+
if new_path == path || new_path.exists() {
98+
return None;
99+
}
100+
101+
let config = transaction
102+
.config_finder()
103+
.python_file(handle.module_kind(), context.module_info.path());
104+
let new_module_name = ModuleName::from_path(
105+
&new_path,
106+
config.search_path().chain(
107+
config
108+
.fallback_search_path
109+
.for_directory(new_path.parent())
110+
.iter(),
111+
),
112+
&config.extra_file_extensions,
113+
)?;
114+
let target_handle = Handle::new(
115+
new_module_name,
116+
ModulePath::filesystem(new_path.clone()),
117+
handle.sys_info().dupe(),
118+
);
119+
120+
let mut edits = transaction.module_member_source_move_edits(
121+
handle,
122+
&context,
123+
&target_handle,
124+
import_format,
125+
)?;
126+
edits.extend(transaction.module_member_consumer_import_updates(
127+
handle,
128+
&context.module_info,
129+
&context.member_name,
130+
&target_handle,
131+
import_format,
132+
));
133+
134+
let new_uri = path_to_uri(&new_path, path_remapper)?;
135+
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
136+
for (module, range, new_text) in edits {
137+
let Some(edit_uri) = module_info_to_uri(&module, path_remapper) else {
138+
continue;
139+
};
140+
changes.entry(edit_uri).or_default().push(TextEdit {
141+
range: module.to_lsp_range(range),
142+
new_text,
143+
});
144+
}
145+
146+
let mut operations = vec![
147+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
148+
uri: new_uri.clone(),
149+
options: None,
150+
annotation_id: None,
151+
})),
152+
DocumentChangeOperation::Edit(TextDocumentEdit {
153+
text_document: OptionalVersionedTextDocumentIdentifier {
154+
uri: new_uri,
155+
version: None,
156+
},
157+
edits: vec![OneOf::Left(TextEdit {
158+
range: Range {
159+
start: Position::new(0, 0),
160+
end: Position::new(0, 0),
161+
},
162+
new_text: context.member_text,
163+
})],
164+
}),
165+
];
166+
167+
let mut sorted_changes: Vec<(Url, Vec<TextEdit>)> = changes.into_iter().collect();
168+
sorted_changes.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
169+
for (uri, mut text_edits) in sorted_changes {
170+
text_edits.sort_by(|a, b| {
171+
(
172+
a.range.start.line,
173+
a.range.start.character,
174+
a.range.end.line,
175+
a.range.end.character,
176+
)
177+
.cmp(&(
178+
b.range.start.line,
179+
b.range.start.character,
180+
b.range.end.line,
181+
b.range.end.character,
182+
))
183+
});
184+
operations.push(DocumentChangeOperation::Edit(TextDocumentEdit {
185+
text_document: OptionalVersionedTextDocumentIdentifier { uri, version: None },
186+
edits: text_edits.into_iter().map(OneOf::Left).collect(),
187+
}));
188+
}
189+
190+
Some(CodeActionOrCommand::CodeAction(CodeAction {
191+
title: format!("Move `{}` to new file", context.member_name),
192+
kind: Some(CodeActionKind::new("refactor.move")),
193+
edit: Some(WorkspaceEdit {
194+
document_changes: Some(DocumentChanges::Operations(operations)),
195+
..Default::default()
196+
}),
197+
..Default::default()
198+
}))
199+
}

pyrefly/lib/lsp/non_wasm/server.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ use crate::lsp::non_wasm::module_helpers::ThriftRemapper;
291291
use crate::lsp::non_wasm::module_helpers::handle_from_module_path;
292292
use crate::lsp::non_wasm::module_helpers::make_open_handle;
293293
use crate::lsp::non_wasm::module_helpers::module_info_to_uri;
294+
use crate::lsp::non_wasm::move_symbol_new_file::move_symbol_to_new_file_code_action;
294295
use crate::lsp::non_wasm::mru::CompletionMru;
295296
use crate::lsp::non_wasm::protocol::Message;
296297
use crate::lsp::non_wasm::protocol::Notification;
@@ -4729,6 +4730,19 @@ impl Server {
47294730
actions.push(action);
47304731
}
47314732
record_code_action_telemetry("convert_module_package", start);
4733+
let start = Instant::now();
4734+
if let Some(action) = move_symbol_to_new_file_code_action(
4735+
&self.initialize_params.capabilities,
4736+
transaction,
4737+
&handle,
4738+
uri,
4739+
range,
4740+
import_format,
4741+
self.path_remapper.as_ref(),
4742+
) {
4743+
actions.push(action);
4744+
}
4745+
record_code_action_telemetry("move_symbol_new_file", start);
47324746
}
47334747
let start = Instant::now();
47344748
if let Some(action) = safe_delete_file_code_action(

pyrefly/lib/state/lsp.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ use crate::types::types::Type;
9393
mod dict_completions;
9494
mod quick_fixes;
9595

96+
pub(crate) use self::quick_fixes::move_module::MoveModuleMemberContext;
9697
pub(crate) use self::quick_fixes::types::LocalRefactorCodeAction;
9798

9899
#[derive(Debug)]
@@ -2772,6 +2773,48 @@ impl<'a> Transaction<'a> {
27722773
)
27732774
}
27742775

2776+
pub(crate) fn module_member_move_context(
2777+
&self,
2778+
handle: &Handle,
2779+
selection: TextRange,
2780+
) -> Option<MoveModuleMemberContext> {
2781+
quick_fixes::move_module::module_member_move_context(self, handle, selection)
2782+
}
2783+
2784+
pub(crate) fn module_member_source_move_edits(
2785+
&self,
2786+
handle: &Handle,
2787+
context: &MoveModuleMemberContext,
2788+
target_handle: &Handle,
2789+
import_format: ImportFormat,
2790+
) -> Option<Vec<(ModuleInfo, TextRange, String)>> {
2791+
quick_fixes::move_module::build_module_member_source_move_edits(
2792+
self,
2793+
handle,
2794+
context,
2795+
target_handle,
2796+
import_format,
2797+
)
2798+
}
2799+
2800+
pub(crate) fn module_member_consumer_import_updates(
2801+
&self,
2802+
source_handle: &Handle,
2803+
source_module_info: &ModuleInfo,
2804+
member_name: &str,
2805+
target_handle: &Handle,
2806+
import_format: ImportFormat,
2807+
) -> Vec<(ModuleInfo, TextRange, String)> {
2808+
quick_fixes::move_module::build_module_member_consumer_import_updates(
2809+
self,
2810+
source_handle,
2811+
source_module_info,
2812+
member_name,
2813+
target_handle,
2814+
import_format,
2815+
)
2816+
}
2817+
27752818
pub fn make_local_function_top_level_code_actions(
27762819
&self,
27772820
handle: &Handle,

0 commit comments

Comments
 (0)