mirror of https://github.com/astral-sh/ruff
Make basic range formatting work in vs code
This commit is contained in:
parent
d1b12acb3c
commit
a1239b8f2d
|
|
@ -11,6 +11,7 @@ use ruff_linter::settings::types::{
|
||||||
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
|
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
|
||||||
};
|
};
|
||||||
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
||||||
|
use ruff_python_formatter::LspRowColumn;
|
||||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||||
|
|
||||||
|
|
@ -395,6 +396,14 @@ pub struct FormatCommand {
|
||||||
preview: bool,
|
preview: bool,
|
||||||
#[clap(long, overrides_with("preview"), hide = true)]
|
#[clap(long, overrides_with("preview"), hide = true)]
|
||||||
no_preview: bool,
|
no_preview: bool,
|
||||||
|
/// Range formatting start: Zero-indexed row and zero-indexed char-based column separated by
|
||||||
|
/// colon, e.g. `1:2`
|
||||||
|
#[clap(long)]
|
||||||
|
pub start: Option<LspRowColumn>,
|
||||||
|
/// Range formatting end: Zero-indexed row and zero-indexed char-based column separated by
|
||||||
|
/// colon, e.g. `3:4`
|
||||||
|
#[clap(long)]
|
||||||
|
pub end: Option<LspRowColumn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||||
|
|
@ -516,6 +525,8 @@ impl FormatCommand {
|
||||||
files: self.files,
|
files: self.files,
|
||||||
isolated: self.isolated,
|
isolated: self.isolated,
|
||||||
stdin_filename: self.stdin_filename,
|
stdin_filename: self.stdin_filename,
|
||||||
|
start: self.start,
|
||||||
|
end: self.end,
|
||||||
},
|
},
|
||||||
CliOverrides {
|
CliOverrides {
|
||||||
line_length: self.line_length,
|
line_length: self.line_length,
|
||||||
|
|
@ -572,6 +583,8 @@ pub struct FormatArguments {
|
||||||
pub files: Vec<PathBuf>,
|
pub files: Vec<PathBuf>,
|
||||||
pub isolated: bool,
|
pub isolated: bool,
|
||||||
pub stdin_filename: Option<PathBuf>,
|
pub stdin_filename: Option<PathBuf>,
|
||||||
|
pub start: Option<LspRowColumn>,
|
||||||
|
pub end: Option<LspRowColumn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLI settings that function as configuration overrides.
|
/// CLI settings that function as configuration overrides.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ use anyhow::Result;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
use ruff_python_ast::PySourceType;
|
use ruff_python_ast::PySourceType;
|
||||||
use ruff_python_formatter::{format_module_source, PyFormatOptions};
|
use ruff_python_formatter::{
|
||||||
|
format_module_source, format_module_source_range, LspRowColumn, PyFormatOptions,
|
||||||
|
};
|
||||||
use ruff_workspace::resolver::python_file_at_path;
|
use ruff_workspace::resolver::python_file_at_path;
|
||||||
|
|
||||||
use crate::args::{CliOverrides, FormatArguments};
|
use crate::args::{CliOverrides, FormatArguments};
|
||||||
|
|
@ -42,7 +44,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||||
.formatter
|
.formatter
|
||||||
.to_format_options(path.map(PySourceType::from).unwrap_or_default());
|
.to_format_options(path.map(PySourceType::from).unwrap_or_default());
|
||||||
|
|
||||||
match format_source(path, options, mode) {
|
match format_source(path, options, mode, cli.start, cli.end) {
|
||||||
Ok(result) => match mode {
|
Ok(result) => match mode {
|
||||||
FormatMode::Write => Ok(ExitStatus::Success),
|
FormatMode::Write => Ok(ExitStatus::Success),
|
||||||
FormatMode::Check => {
|
FormatMode::Check => {
|
||||||
|
|
@ -65,12 +67,21 @@ fn format_source(
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
options: PyFormatOptions,
|
options: PyFormatOptions,
|
||||||
mode: FormatMode,
|
mode: FormatMode,
|
||||||
|
start: Option<LspRowColumn>,
|
||||||
|
end: Option<LspRowColumn>,
|
||||||
) -> Result<FormatCommandResult, FormatCommandError> {
|
) -> Result<FormatCommandResult, FormatCommandError> {
|
||||||
let unformatted = read_from_stdin()
|
let unformatted = read_from_stdin()
|
||||||
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err))?;
|
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err))?;
|
||||||
|
let formatted = if start.is_some() || end.is_some() {
|
||||||
|
let formatted = format_module_source_range(&unformatted, options, start, end)
|
||||||
|
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
|
||||||
|
formatted
|
||||||
|
} else {
|
||||||
let formatted = format_module_source(&unformatted, options)
|
let formatted = format_module_source(&unformatted, options)
|
||||||
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
|
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
|
||||||
let formatted = formatted.as_code();
|
let formatted = formatted.as_code();
|
||||||
|
formatted.to_string()
|
||||||
|
};
|
||||||
if formatted.len() == unformatted.len() && formatted == unformatted {
|
if formatted.len() == unformatted.len() && formatted == unformatted {
|
||||||
Ok(FormatCommandResult::Unchanged)
|
Ok(FormatCommandResult::Unchanged)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::prelude::TagKind;
|
use crate::prelude::TagKind;
|
||||||
use crate::GroupId;
|
use crate::GroupId;
|
||||||
use ruff_text_size::TextRange;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
|
@ -12,7 +11,7 @@ pub enum FormatError {
|
||||||
SyntaxError { message: &'static str },
|
SyntaxError { message: &'static str },
|
||||||
/// In case range formatting failed because the provided range was larger
|
/// In case range formatting failed because the provided range was larger
|
||||||
/// than the formatted syntax tree
|
/// than the formatted syntax tree
|
||||||
RangeError { input: TextRange, tree: TextRange },
|
RangeError { row: usize, col: usize },
|
||||||
|
|
||||||
/// In case printing the document failed because it has an invalid structure.
|
/// In case printing the document failed because it has an invalid structure.
|
||||||
InvalidDocument(InvalidDocumentError),
|
InvalidDocument(InvalidDocumentError),
|
||||||
|
|
@ -32,9 +31,9 @@ impl std::fmt::Display for FormatError {
|
||||||
FormatError::SyntaxError {message} => {
|
FormatError::SyntaxError {message} => {
|
||||||
std::write!(fmt, "syntax error: {message}")
|
std::write!(fmt, "syntax error: {message}")
|
||||||
},
|
},
|
||||||
FormatError::RangeError { input, tree } => std::write!(
|
FormatError::RangeError { row, col } => std::write!(
|
||||||
fmt,
|
fmt,
|
||||||
"formatting range {input:?} is larger than syntax tree {tree:?}"
|
"formatting range {row}:{col} is not a valid index"
|
||||||
),
|
),
|
||||||
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please report if necessary."),
|
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please report if necessary."),
|
||||||
FormatError::PoorLayout => {
|
FormatError::PoorLayout => {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,11 @@ use clap::{command, Parser, ValueEnum};
|
||||||
use ruff_formatter::SourceCode;
|
use ruff_formatter::SourceCode;
|
||||||
use ruff_python_index::tokens_and_ranges;
|
use ruff_python_index::tokens_and_ranges;
|
||||||
use ruff_python_parser::{parse_ok_tokens, Mode};
|
use ruff_python_parser::{parse_ok_tokens, Mode};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_source_file::Locator;
|
||||||
|
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
use crate::comments::collect_comments;
|
use crate::comments::collect_comments;
|
||||||
use crate::{format_module_ast, PyFormatOptions};
|
use crate::{format_module_ast, format_module_range, PyFormatOptions};
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
#[derive(ValueEnum, Clone, Debug)]
|
||||||
pub enum Emit {
|
pub enum Emit {
|
||||||
|
|
@ -36,19 +37,38 @@ pub struct Cli {
|
||||||
pub print_ir: bool,
|
pub print_ir: bool,
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub print_comments: bool,
|
pub print_comments: bool,
|
||||||
|
/// byte offset for range formatting
|
||||||
|
#[clap(long)]
|
||||||
|
pub start: Option<u32>,
|
||||||
|
/// byte offset for range formatting
|
||||||
|
#[clap(long)]
|
||||||
|
pub end: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
|
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
|
||||||
let (tokens, comment_ranges) = tokens_and_ranges(source)
|
let (tokens, comment_ranges) = tokens_and_ranges(source)
|
||||||
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
|
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
|
||||||
|
|
||||||
// Parse the AST.
|
|
||||||
let module =
|
let module =
|
||||||
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
|
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
|
||||||
|
|
||||||
let options = PyFormatOptions::from_extension(source_type);
|
let options = PyFormatOptions::from_extension(source_type);
|
||||||
|
|
||||||
let source_code = SourceCode::new(source);
|
let source_code = SourceCode::new(source);
|
||||||
|
let locator = Locator::new(source);
|
||||||
|
|
||||||
|
if cli.start.is_some() || cli.end.is_some() {
|
||||||
|
let range = TextRange::new(
|
||||||
|
cli.start.map(TextSize::new).unwrap_or_default(),
|
||||||
|
cli.end.map(TextSize::new).unwrap_or(source.text_len()),
|
||||||
|
);
|
||||||
|
return Ok(format_module_range(
|
||||||
|
&module,
|
||||||
|
&comment_ranges,
|
||||||
|
source,
|
||||||
|
options,
|
||||||
|
&locator,
|
||||||
|
range,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
let formatted = format_module_ast(&module, &comment_ranges, source, options)
|
let formatted = format_module_ast(&module, &comment_ranges, source, options)
|
||||||
.context("Failed to format node")?;
|
.context("Failed to format node")?;
|
||||||
if cli.print_ir {
|
if cli.print_ir {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::Level;
|
use tracing::{warn, Level};
|
||||||
|
|
||||||
use ruff_formatter::prelude::*;
|
use ruff_formatter::prelude::*;
|
||||||
use ruff_formatter::{format, FormatError, Formatted, PrintError, Printed, SourceCode};
|
use ruff_formatter::{format, FormatError, Formatted, PrintError, Printed, SourceCode};
|
||||||
|
|
@ -10,12 +11,14 @@ use ruff_python_parser::lexer::LexicalError;
|
||||||
use ruff_python_parser::{parse_ok_tokens, Mode, ParseError};
|
use ruff_python_parser::{parse_ok_tokens, Mode, ParseError};
|
||||||
use ruff_python_trivia::CommentRanges;
|
use ruff_python_trivia::CommentRanges;
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
|
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
use crate::comments::{
|
use crate::comments::{
|
||||||
dangling_comments, leading_comments, trailing_comments, Comments, SourceComment,
|
dangling_comments, leading_comments, trailing_comments, Comments, SourceComment,
|
||||||
};
|
};
|
||||||
pub use crate::context::PyFormatContext;
|
pub use crate::context::PyFormatContext;
|
||||||
pub use crate::options::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle};
|
pub use crate::options::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle};
|
||||||
|
use crate::statement::suite::SuiteKind;
|
||||||
use crate::verbatim::suppressed_node;
|
use crate::verbatim::suppressed_node;
|
||||||
pub use settings::FormatterSettings;
|
pub use settings::FormatterSettings;
|
||||||
|
|
||||||
|
|
@ -134,6 +137,73 @@ pub fn format_module_source(
|
||||||
Ok(formatted.print()?)
|
Ok(formatted.print()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Range formatting coordinate: Zero-indexed row and zero-indexed char-based column separated by
|
||||||
|
/// colon, e.g. `1:2`.
|
||||||
|
///
|
||||||
|
/// See [`Locator::convert_row_and_column`] for details on the semantics.
|
||||||
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
|
pub struct LspRowColumn {
|
||||||
|
row: usize,
|
||||||
|
col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for LspRowColumn {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let Some((row, col)) = s.split_once(':') else {
|
||||||
|
return Err("Coordinate is missing a colon, the format is `<row>:<column>`");
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(LspRowColumn {
|
||||||
|
row: row.parse().map_err(|_| "row must be a number")?,
|
||||||
|
col: col.parse().map_err(|_| "col must be a number")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[tracing::instrument(name = "format", level = Level::TRACE, skip_all)]
|
||||||
|
pub fn format_module_source_range(
|
||||||
|
source: &str,
|
||||||
|
options: PyFormatOptions,
|
||||||
|
start: Option<LspRowColumn>,
|
||||||
|
end: Option<LspRowColumn>,
|
||||||
|
) -> Result<String, FormatModuleError> {
|
||||||
|
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
|
||||||
|
let module = parse_ok_tokens(tokens, Mode::Module, "<filename>")?;
|
||||||
|
let locator = Locator::new(source);
|
||||||
|
|
||||||
|
let start = if let Some(start) = start {
|
||||||
|
locator
|
||||||
|
.convert_row_and_column(start.row, start.col)
|
||||||
|
.ok_or(FormatError::RangeError {
|
||||||
|
row: start.row,
|
||||||
|
col: start.col,
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
TextSize::default()
|
||||||
|
};
|
||||||
|
let end = if let Some(end) = end {
|
||||||
|
locator
|
||||||
|
.convert_row_and_column(end.row, end.col)
|
||||||
|
.ok_or(FormatError::RangeError {
|
||||||
|
row: end.row,
|
||||||
|
col: end.col,
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
source.text_len()
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted = format_module_range(
|
||||||
|
&module,
|
||||||
|
&comment_ranges,
|
||||||
|
source,
|
||||||
|
options,
|
||||||
|
&locator,
|
||||||
|
TextRange::new(start, end),
|
||||||
|
)?;
|
||||||
|
Ok(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_module_ast<'a>(
|
pub fn format_module_ast<'a>(
|
||||||
module: &'a Mod,
|
module: &'a Mod,
|
||||||
comment_ranges: &'a CommentRanges,
|
comment_ranges: &'a CommentRanges,
|
||||||
|
|
@ -155,6 +225,57 @@ pub fn format_module_ast<'a>(
|
||||||
Ok(formatted)
|
Ok(formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format_module_range<'a>(
|
||||||
|
module: &'a Mod,
|
||||||
|
comment_ranges: &'a CommentRanges,
|
||||||
|
source: &'a str,
|
||||||
|
options: PyFormatOptions,
|
||||||
|
locator: &Locator<'a>,
|
||||||
|
range: TextRange,
|
||||||
|
) -> FormatResult<String> {
|
||||||
|
let comments = Comments::from_ast(&module, SourceCode::new(source), &comment_ranges);
|
||||||
|
|
||||||
|
let Mod::Module(module_inner) = &module else {
|
||||||
|
panic!("That's not a module");
|
||||||
|
};
|
||||||
|
|
||||||
|
// ```
|
||||||
|
// a = 1; b = 2; c = 3; d = 4; e = 5
|
||||||
|
// ^ b end ^ d start
|
||||||
|
// ^^^^^^^^^^^^^^^ range
|
||||||
|
// ^ range start ^ range end
|
||||||
|
// ```
|
||||||
|
// TODO: If it goes beyond the end of the last stmt or before start, do we need to format the
|
||||||
|
// parent?
|
||||||
|
// TODO: Change suite formatting so we can use a slice instead
|
||||||
|
let in_range: Vec<_> = module_inner
|
||||||
|
.body
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
// TODO: check whether these bounds need equality
|
||||||
|
.skip_while(|child| range.start() > child.end())
|
||||||
|
.take_while(|child| child.start() < range.end())
|
||||||
|
.collect();
|
||||||
|
let (Some(first), Some(last)) = (in_range.first(), in_range.last()) else {
|
||||||
|
// TODO: Use tracing again https://github.com/tokio-rs/tracing/issues/2721
|
||||||
|
// TODO: Forward this to something proper
|
||||||
|
eprintln!("The formatting range contains no statements");
|
||||||
|
return Ok(source.to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = source[TextRange::up_to(first.start())].to_string();
|
||||||
|
|
||||||
|
let formatted: Formatted<PyFormatContext> = format!(
|
||||||
|
PyFormatContext::new(options.clone(), locator.contents(), comments),
|
||||||
|
[in_range.format().with_options(SuiteKind::TopLevel)]
|
||||||
|
)?;
|
||||||
|
//println!("{}", formatted.document().display(SourceCode::new(source)));
|
||||||
|
// TODO: Make the printer use the buffer instead
|
||||||
|
buffer += formatted.print()?.as_code();
|
||||||
|
buffer += &source[TextRange::new(last.end(), source.text_len())];
|
||||||
|
return Ok(buffer.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Public function for generating a printable string of the debug comments.
|
/// Public function for generating a printable string of the debug comments.
|
||||||
pub fn pretty_comments(module: &Mod, comment_ranges: &CommentRanges, source: &str) -> String {
|
pub fn pretty_comments(module: &Mod, comment_ranges: &CommentRanges, source: &str) -> String {
|
||||||
let source_code = SourceCode::new(source);
|
let source_code = SourceCode::new(source);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue