-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Support formatting pycon markdown code blocks
#23112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
f600559
79fe8e4
83bdcfd
fee3bd8
e7565ea
21e84c0
dc4e450
6bf953d
257b0ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,7 @@ use ruff_python_ast::PySourceType; | |||||||||||||||||||||||||||
| use ruff_python_formatter::format_module_source; | ||||||||||||||||||||||||||||
| use ruff_python_trivia::textwrap::{dedent, indent}; | ||||||||||||||||||||||||||||
| use ruff_source_file::{Line, UniversalNewlines}; | ||||||||||||||||||||||||||||
| use ruff_text_size::{TextRange, TextSize}; | ||||||||||||||||||||||||||||
| use ruff_text_size::{TextLen, TextRange, TextSize}; | ||||||||||||||||||||||||||||
| use ruff_workspace::FormatterSettings; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[derive(Debug, PartialEq, Eq)] | ||||||||||||||||||||||||||||
|
|
@@ -80,46 +80,49 @@ pub fn format_code_blocks( | |||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if closing_fence != opening_fence { | ||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Found the matching end of the code block | ||||||||||||||||||||||||||||
| if closing_fence == opening_fence { | ||||||||||||||||||||||||||||
| let language = language.to_ascii_lowercase(); | ||||||||||||||||||||||||||||
| if state == MarkdownState::On | ||||||||||||||||||||||||||||
| && matches!( | ||||||||||||||||||||||||||||
| language.as_str(), | ||||||||||||||||||||||||||||
| "python" | "py" | "python3" | "py3" | "pyi" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| // Maybe python, try formatting it | ||||||||||||||||||||||||||||
| let end = code_line.start(); | ||||||||||||||||||||||||||||
| let unformatted_code = dedent(&source[TextRange::new(start, end)]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let py_source_type = match settings.extension.get_extension(&language) { | ||||||||||||||||||||||||||||
| None => PySourceType::from_extension(&language), | ||||||||||||||||||||||||||||
| Some(language) => PySourceType::from(language), | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| if state != MarkdownState::On { | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Maybe python, try formatting it | ||||||||||||||||||||||||||||
| let language = language.to_ascii_lowercase(); | ||||||||||||||||||||||||||||
| let py_source_type = match settings.extension.get_extension(&language) { | ||||||||||||||||||||||||||||
| None => PySourceType::from_extension(&language), | ||||||||||||||||||||||||||||
| Some(language) => PySourceType::from(language), | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let end = code_line.start(); | ||||||||||||||||||||||||||||
| let unformatted_code = dedent(&source[TextRange::new(start, end)]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let formatted_code = match language.as_str() { | ||||||||||||||||||||||||||||
| "python" | "py" | "python3" | "py3" | "pyi" => { | ||||||||||||||||||||||||||||
| let options = | ||||||||||||||||||||||||||||
| settings.to_format_options(py_source_type, &unformatted_code, path); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Using `Printed::into_code` requires adding `ruff_formatter` as a direct | ||||||||||||||||||||||||||||
| // dependency, and I suspect that Rust can optimize the closure away regardless. | ||||||||||||||||||||||||||||
| #[expect(clippy::redundant_closure_for_method_calls)] | ||||||||||||||||||||||||||||
| let formatted_code = format_module_source(&unformatted_code, options) | ||||||||||||||||||||||||||||
| .map(|formatted| formatted.into_code()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Formatting produced changes | ||||||||||||||||||||||||||||
| if let Ok(formatted_code) = formatted_code | ||||||||||||||||||||||||||||
| && (formatted_code.len() != unformatted_code.len() | ||||||||||||||||||||||||||||
| || formatted_code != *unformatted_code) | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| formatted.push_str(&source[TextRange::new(last_match, start)]); | ||||||||||||||||||||||||||||
| let formatted_code = indent(&formatted_code, code_indent); | ||||||||||||||||||||||||||||
| formatted.push_str(&formatted_code); | ||||||||||||||||||||||||||||
| last_match = end; | ||||||||||||||||||||||||||||
| changed = true; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| format_module_source(&unformatted_code, options) | ||||||||||||||||||||||||||||
| .map(ruff_formatter::Printed::into_code) | ||||||||||||||||||||||||||||
| .ok() | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| "pycon" => format_pycon_block(&unformatted_code, path, settings), | ||||||||||||||||||||||||||||
| _ => None, | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Formatting produced changes | ||||||||||||||||||||||||||||
| if let Some(formatted_code) = formatted_code | ||||||||||||||||||||||||||||
| && (formatted_code.len() != unformatted_code.len() | ||||||||||||||||||||||||||||
| || formatted_code != *unformatted_code) | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| formatted.push_str(&source[TextRange::new(last_match, start)]); | ||||||||||||||||||||||||||||
| let formatted_code = indent(&formatted_code, code_indent); | ||||||||||||||||||||||||||||
| formatted.push_str(&formatted_code); | ||||||||||||||||||||||||||||
| last_match = end; | ||||||||||||||||||||||||||||
| changed = true; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
@@ -132,6 +135,63 @@ pub fn format_code_blocks( | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| fn format_pycon_block( | ||||||||||||||||||||||||||||
| source: &str, | ||||||||||||||||||||||||||||
| path: Option<&Path>, | ||||||||||||||||||||||||||||
| settings: &FormatterSettings, | ||||||||||||||||||||||||||||
| ) -> Option<String> { | ||||||||||||||||||||||||||||
| static FIRST_LINE: &str = ">>> "; | ||||||||||||||||||||||||||||
| static CONTINUATION: &str = "... "; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let offset = FIRST_LINE.text_len(); | ||||||||||||||||||||||||||||
| let mut changed = false; | ||||||||||||||||||||||||||||
| let mut result = String::with_capacity(source.len()); | ||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might actually want to skip this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should I do the same thing in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably not a big deal either way, but yeah I guess you could do the same in I think it's fine to leave this as-is. |
||||||||||||||||||||||||||||
| let mut unformatted = String::with_capacity(source.len()); | ||||||||||||||||||||||||||||
| let mut last_match = TextSize::new(0); | ||||||||||||||||||||||||||||
|
amyreese marked this conversation as resolved.
|
||||||||||||||||||||||||||||
| let mut lines = source.universal_newlines().peekable(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| while let Some(line) = lines.next() { | ||||||||||||||||||||||||||||
| unformatted.clear(); | ||||||||||||||||||||||||||||
| if line.starts_with(FIRST_LINE) { | ||||||||||||||||||||||||||||
| let start = line.start(); | ||||||||||||||||||||||||||||
| let mut end = line.full_end(); | ||||||||||||||||||||||||||||
| unformatted.push_str(&source[TextRange::new(line.start() + offset, line.full_end())]); | ||||||||||||||||||||||||||||
| while let Some(next_line) = lines.peek() { | ||||||||||||||||||||||||||||
| if next_line.starts_with(CONTINUATION) { | ||||||||||||||||||||||||||||
| end = next_line.full_end(); | ||||||||||||||||||||||||||||
| unformatted.push_str(&source[TextRange::new(next_line.start() + offset, end)]); | ||||||||||||||||||||||||||||
| lines.next(); | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think you can simplify this slightly with the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if I like this as much once I had to add logic to deal with empty continuation lines (ie, no space after the "...") 🤔 What do you think?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That commit looks fine to me! What don't you like about it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mostly that it didn't feel "simpler" at this point, but ¯_(ツ)_/¯ |
||||||||||||||||||||||||||||
| let options = settings.to_format_options(PySourceType::Python, &unformatted, path); | ||||||||||||||||||||||||||||
| let Ok(formatted) = | ||||||||||||||||||||||||||||
| format_module_source(&unformatted, options).map(ruff_formatter::Printed::into_code) | ||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if formatted.len() != unformatted.len() || formatted != unformatted { | ||||||||||||||||||||||||||||
| result.push_str(&source[TextRange::new(last_match, start)]); | ||||||||||||||||||||||||||||
| for (idx, line) in formatted.universal_newlines().enumerate() { | ||||||||||||||||||||||||||||
| result.push_str(if idx == 0 { FIRST_LINE } else { CONTINUATION }); | ||||||||||||||||||||||||||||
| result.push_str(&formatted[TextRange::new(line.start(), line.full_end())]); | ||||||||||||||||||||||||||||
|
amyreese marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| last_match = end; | ||||||||||||||||||||||||||||
| changed = true; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if changed { | ||||||||||||||||||||||||||||
| result.push_str(&source[last_match.to_usize()..]); | ||||||||||||||||||||||||||||
| Some(result) | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||||||||||||
| mod tests { | ||||||||||||||||||||||||||||
| use insta::assert_snapshot; | ||||||||||||||||||||||||||||
|
|
@@ -431,4 +491,37 @@ def bar(): ... | |||||||||||||||||||||||||||
| ~~~ | ||||||||||||||||||||||||||||
| "#); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||
| fn format_code_blocks_python_console() { | ||||||||||||||||||||||||||||
| let code = r#" | ||||||||||||||||||||||||||||
| ```pycon | ||||||||||||||||||||||||||||
| >>> print( 'hello there' ) | ||||||||||||||||||||||||||||
| hello there | ||||||||||||||||||||||||||||
| >>> def foo(): pass | ||||||||||||||||||||||||||||
| >>> def bar(): | ||||||||||||||||||||||||||||
| ... print( 'thing1', "thing2", ) | ||||||||||||||||||||||||||||
| ... bar() | ||||||||||||||||||||||||||||
| thing1 thing2 | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
| "#; | ||||||||||||||||||||||||||||
| assert_snapshot!(format_code_blocks(code, None, &FormatterSettings::default()), @r#" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```pycon | ||||||||||||||||||||||||||||
| >>> print("hello there") | ||||||||||||||||||||||||||||
| hello there | ||||||||||||||||||||||||||||
| >>> def foo(): | ||||||||||||||||||||||||||||
| ... pass | ||||||||||||||||||||||||||||
| >>> def bar(): | ||||||||||||||||||||||||||||
| ... print( | ||||||||||||||||||||||||||||
| ... "thing1", | ||||||||||||||||||||||||||||
| ... "thing2", | ||||||||||||||||||||||||||||
| ... ) | ||||||||||||||||||||||||||||
| ... | ||||||||||||||||||||||||||||
| ... | ||||||||||||||||||||||||||||
| ... bar() | ||||||||||||||||||||||||||||
|
amyreese marked this conversation as resolved.
|
||||||||||||||||||||||||||||
| thing1 thing2 | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
| "#); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.