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
8 changes: 4 additions & 4 deletions crates/pyrefly_bundled/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ fn extract_pyi_files_from_archive(filter: PathFilter) -> anyhow::Result<SmallMap
let mut relative_path_components = relative_path_context.components();

let first_component = relative_path_components.next();
if let Some(expected) = filter.expected_first_component() {
if first_component.is_none_or(|component| component.as_os_str() != expected) {
continue;
}
if let Some(expected) = filter.expected_first_component()
&& first_component.is_none_or(|component| component.as_os_str() != expected)
{
continue;
}
// For ThirdPartyStubs, we need to put first_component back into the path

Expand Down
28 changes: 28 additions & 0 deletions crates/pyrefly_python/src/docstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ fn dedented_lines_for_parsing(docstring: &str) -> Vec<String> {
.collect()
}

/// Dedent a block of text while preserving blank lines, similar to how we handle docstrings.
pub fn dedent_block_preserving_layout(text: &str) -> Option<String> {
if text.trim().is_empty() {
return None;
}

let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return None;
}

let min_indent = minimal_indentation(lines.iter().copied());
let mut dedented = String::new();
for line in lines {
if line.trim().is_empty() {
dedented.push('\n');
continue;
}
let start = min_indent.min(line.len());
dedented.push_str(&line[start..]);
dedented.push('\n');
}
if !text.ends_with('\n') {
dedented.push('\n');
}
Some(dedented)
}

fn leading_space_count(line: &str) -> usize {
line.as_bytes().iter().take_while(|c| **c == b' ').count()
}
Expand Down
74 changes: 57 additions & 17 deletions pyrefly/lib/lsp/non_wasm/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,10 @@ pub fn capabilities(
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
code_action_kinds: Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR_EXTRACT,
]),
..Default::default()
})),
completion_provider: Some(CompletionOptions {
Expand Down Expand Up @@ -2294,26 +2297,63 @@ 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 code_actions = transaction
.local_quickfix_code_actions(&handle, range, import_format)?
.into_map(|(title, info, range, insert_text)| {
CodeActionOrCommand::CodeAction(CodeAction {
title,
kind: Some(CodeActionKind::QUICKFIX),
let mut actions = Vec::new();
if let Some(quickfixes) =
transaction.local_quickfix_code_actions(&handle, range, import_format)
{
actions.extend(
quickfixes
.into_iter()
.map(|(title, info, range, insert_text)| {
CodeActionOrCommand::CodeAction(CodeAction {
title,
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(WorkspaceEdit {
changes: Some(HashMap::from([(
uri.clone(),
vec![TextEdit {
range: info.to_lsp_range(range),
new_text: insert_text,
}],
)])),
..Default::default()
}),
..Default::default()
})
}),
);
}
if let Some(refactors) = transaction.extract_function_code_actions(&handle, range) {
for action in refactors {
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
for (module, edit_range, new_text) in action.edits {
let Some(edit_uri) = module_info_to_uri(&module) else {
continue;
};
changes.entry(edit_uri).or_default().push(TextEdit {
range: module.to_lsp_range(edit_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(HashMap::from([(
uri.clone(),
vec![TextEdit {
range: info.to_lsp_range(range),
new_text: insert_text,
}],
)])),
changes: Some(changes),
..Default::default()
}),
..Default::default()
})
});
Some(code_actions)
}));
}
}
if actions.is_empty() {
None
} else {
Some(actions)
}
}

fn document_highlight(
Expand Down
12 changes: 12 additions & 0 deletions pyrefly/lib/state/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ use crate::types::callable::Param;
use crate::types::module::ModuleType;
use crate::types::types::Type;

mod quick_fixes;

use self::quick_fixes::extract_function::LocalRefactorCodeAction;

fn default_true() -> bool {
true
}
Expand Down Expand Up @@ -1713,6 +1717,14 @@ impl<'a> Transaction<'a> {
Some(code_actions)
}

pub fn extract_function_code_actions(
&self,
handle: &Handle,
selection: TextRange,
) -> Option<Vec<LocalRefactorCodeAction>> {
quick_fixes::extract_function::extract_function_code_actions(self, handle, selection)
}

/// Determines whether a module is a third-party package.
///
/// Checks if the module's path is located within any of the configured
Expand Down
Loading