mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:21 +00:00
Add --range
option to ruff format
(#9733)
Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
This commit is contained in:
parent
e708c08b64
commit
b3dc565473
7 changed files with 652 additions and 20 deletions
|
@ -1,6 +1,10 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::fmt::Formatter;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use clap::{command, Parser};
|
use clap::{command, Parser};
|
||||||
|
use colored::Colorize;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
@ -12,6 +16,8 @@ use ruff_linter::settings::types::{
|
||||||
SerializationFormat, UnsafeFixes,
|
SerializationFormat, UnsafeFixes,
|
||||||
};
|
};
|
||||||
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
|
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
|
||||||
|
use ruff_source_file::{LineIndex, OneIndexed};
|
||||||
|
use ruff_text_size::TextRange;
|
||||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||||
use ruff_workspace::options::PycodestyleOptions;
|
use ruff_workspace::options::PycodestyleOptions;
|
||||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||||
|
@ -440,6 +446,21 @@ 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,
|
||||||
|
|
||||||
|
/// When specified, Ruff will try to only format the code in the given range.
|
||||||
|
/// It might be necessary to extend the start backwards or the end forwards, to fully enclose a logical line.
|
||||||
|
/// The `<RANGE>` uses the format `<start_line>:<start_column>-<end_line>:<end_column>`.
|
||||||
|
///
|
||||||
|
/// - The line and column numbers are 1 based.
|
||||||
|
/// - The column specifies the nth-unicode codepoint on that line.
|
||||||
|
/// - The end offset is exclusive.
|
||||||
|
/// - The column numbers are optional. You can write `--range=1-2` instead of `--range=1:1-2:1`.
|
||||||
|
/// - The end position is optional. You can write `--range=2` to format the entire document starting from the second line.
|
||||||
|
/// - The start position is optional. You can write `--range=-3` to format the first three lines of the document.
|
||||||
|
///
|
||||||
|
/// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported.
|
||||||
|
#[clap(long, help_heading = "Editor options", verbatim_doc_comment)]
|
||||||
|
pub range: Option<FormatRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||||
|
@ -570,6 +591,7 @@ impl FormatCommand {
|
||||||
isolated: self.isolated,
|
isolated: self.isolated,
|
||||||
no_cache: self.no_cache,
|
no_cache: self.no_cache,
|
||||||
stdin_filename: self.stdin_filename,
|
stdin_filename: self.stdin_filename,
|
||||||
|
range: self.range,
|
||||||
},
|
},
|
||||||
CliOverrides {
|
CliOverrides {
|
||||||
line_length: self.line_length,
|
line_length: self.line_length,
|
||||||
|
@ -670,6 +692,196 @@ 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 range: Option<FormatRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text range specified by line and column numbers.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct FormatRange {
|
||||||
|
start: LineColumn,
|
||||||
|
end: LineColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatRange {
|
||||||
|
/// Converts the line:column range to a byte offset range specific for `source`.
|
||||||
|
///
|
||||||
|
/// Returns an empty range if the start range is past the end of `source`.
|
||||||
|
pub(super) fn to_text_range(self, source: &str, line_index: &LineIndex) -> TextRange {
|
||||||
|
let start_byte_offset = line_index.offset(self.start.line, self.start.column, source);
|
||||||
|
let end_byte_offset = line_index.offset(self.end.line, self.end.column, source);
|
||||||
|
|
||||||
|
TextRange::new(start_byte_offset, end_byte_offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FormatRange {
|
||||||
|
type Err = FormatRangeParseError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (start, end) = value.split_once('-').unwrap_or((value, ""));
|
||||||
|
|
||||||
|
let start = if start.is_empty() {
|
||||||
|
LineColumn::default()
|
||||||
|
} else {
|
||||||
|
start.parse().map_err(FormatRangeParseError::InvalidStart)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let end = if end.is_empty() {
|
||||||
|
LineColumn {
|
||||||
|
line: OneIndexed::MAX,
|
||||||
|
column: OneIndexed::MAX,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
end.parse().map_err(FormatRangeParseError::InvalidEnd)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if start > end {
|
||||||
|
return Err(FormatRangeParseError::StartGreaterThanEnd(start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(FormatRange { start, end })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum FormatRangeParseError {
|
||||||
|
InvalidStart(LineColumnParseError),
|
||||||
|
InvalidEnd(LineColumnParseError),
|
||||||
|
|
||||||
|
StartGreaterThanEnd(LineColumn, LineColumn),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for FormatRangeParseError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let tip = " tip:".bold().green();
|
||||||
|
match self {
|
||||||
|
FormatRangeParseError::StartGreaterThanEnd(start, end) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'",
|
||||||
|
start_invalid=start.to_string().bold().yellow(),
|
||||||
|
end_invalid=end.to_string().bold().yellow(),
|
||||||
|
start=start.to_string().green().bold(),
|
||||||
|
end=end.to_string().green().bold()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FormatRangeParseError::InvalidStart(inner) => inner.write(f, true),
|
||||||
|
FormatRangeParseError::InvalidEnd(inner) => inner.write(f, false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FormatRangeParseError {}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct LineColumn {
|
||||||
|
pub line: OneIndexed,
|
||||||
|
pub column: OneIndexed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for LineColumn {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{line}:{column}", line = self.line, column = self.column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LineColumn {
|
||||||
|
fn default() -> Self {
|
||||||
|
LineColumn {
|
||||||
|
line: OneIndexed::MIN,
|
||||||
|
column: OneIndexed::MIN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for LineColumn {
|
||||||
|
#[inline]
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for LineColumn {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.line
|
||||||
|
.cmp(&other.line)
|
||||||
|
.then(self.column.cmp(&other.column))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for LineColumn {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cmp(other) == Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for LineColumn {}
|
||||||
|
|
||||||
|
impl FromStr for LineColumn {
|
||||||
|
type Err = LineColumnParseError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (line, column) = value.split_once(':').unwrap_or((value, "1"));
|
||||||
|
|
||||||
|
let line: usize = line.parse().map_err(LineColumnParseError::LineParseError)?;
|
||||||
|
let column: usize = column
|
||||||
|
.parse()
|
||||||
|
.map_err(LineColumnParseError::ColumnParseError)?;
|
||||||
|
|
||||||
|
match (OneIndexed::new(line), OneIndexed::new(column)) {
|
||||||
|
(Some(line), Some(column)) => Ok(LineColumn { line, column }),
|
||||||
|
(Some(line), None) => Err(LineColumnParseError::ZeroColumnIndex { line }),
|
||||||
|
(None, Some(column)) => Err(LineColumnParseError::ZeroLineIndex { column }),
|
||||||
|
(None, None) => Err(LineColumnParseError::ZeroLineAndColumnIndex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum LineColumnParseError {
|
||||||
|
ZeroLineIndex { column: OneIndexed },
|
||||||
|
ZeroColumnIndex { line: OneIndexed },
|
||||||
|
ZeroLineAndColumnIndex,
|
||||||
|
LineParseError(std::num::ParseIntError),
|
||||||
|
ColumnParseError(std::num::ParseIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineColumnParseError {
|
||||||
|
fn write(&self, f: &mut std::fmt::Formatter, start_range: bool) -> std::fmt::Result {
|
||||||
|
let tip = "tip:".bold().green();
|
||||||
|
|
||||||
|
let range = if start_range { "start" } else { "end" };
|
||||||
|
|
||||||
|
match self {
|
||||||
|
LineColumnParseError::ColumnParseError(inner) => {
|
||||||
|
write!(f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'.")
|
||||||
|
}
|
||||||
|
LineColumnParseError::LineParseError(inner) => {
|
||||||
|
write!(f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'.")
|
||||||
|
}
|
||||||
|
LineColumnParseError::ZeroColumnIndex { line } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"the {range} column is 0, but it should be 1 or greater.\n {tip} The column numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||||
|
suggestion=format!("{line}:1").green().bold()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LineColumnParseError::ZeroLineIndex { column } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"the {range} line is 0, but it should be 1 or greater.\n {tip} The line numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||||
|
suggestion=format!("1:{column}").green().bold()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LineColumnParseError::ZeroLineAndColumnIndex => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"the {range} line and column are both 0, but they should be 1 or greater.\n {tip} The line and column numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||||
|
suggestion="1:1".to_string().green().bold()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLI settings that function as configuration overrides.
|
/// CLI settings that function as configuration overrides.
|
||||||
|
|
|
@ -1050,6 +1050,7 @@ mod tests {
|
||||||
&self.settings.formatter,
|
&self.settings.formatter,
|
||||||
PySourceType::Python,
|
PySourceType::Python,
|
||||||
FormatMode::Write,
|
FormatMode::Write,
|
||||||
|
None,
|
||||||
Some(cache),
|
Some(cache),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,13 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
|
||||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||||
use ruff_linter::warn_user_once;
|
use ruff_linter::warn_user_once;
|
||||||
use ruff_python_ast::{PySourceType, SourceType};
|
use ruff_python_ast::{PySourceType, SourceType};
|
||||||
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
|
use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle};
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
|
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
||||||
use crate::args::{CliOverrides, FormatArguments};
|
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||||
use crate::panic::{catch_unwind, PanicError};
|
use crate::panic::{catch_unwind, PanicError};
|
||||||
use crate::resolve::resolve;
|
use crate::resolve::resolve;
|
||||||
|
@ -77,6 +78,13 @@ pub(crate) fn format(
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cli.range.is_some() && paths.len() > 1 {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
|
||||||
|
paths.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
warn_incompatible_formatter_settings(&resolver);
|
warn_incompatible_formatter_settings(&resolver);
|
||||||
|
|
||||||
// Discover the package root for each Python file.
|
// Discover the package root for each Python file.
|
||||||
|
@ -139,7 +147,14 @@ pub(crate) fn format(
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
match catch_unwind(|| {
|
match catch_unwind(|| {
|
||||||
format_path(path, &settings.formatter, source_type, mode, cache)
|
format_path(
|
||||||
|
path,
|
||||||
|
&settings.formatter,
|
||||||
|
source_type,
|
||||||
|
mode,
|
||||||
|
cli.range,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
}) {
|
}) {
|
||||||
Ok(inner) => inner.map(|result| FormatPathResult {
|
Ok(inner) => inner.map(|result| FormatPathResult {
|
||||||
path: resolved_file.path().to_path_buf(),
|
path: resolved_file.path().to_path_buf(),
|
||||||
|
@ -226,6 +241,7 @@ pub(crate) fn format_path(
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
mode: FormatMode,
|
mode: FormatMode,
|
||||||
|
range: Option<FormatRange>,
|
||||||
cache: Option<&Cache>,
|
cache: Option<&Cache>,
|
||||||
) -> Result<FormatResult, FormatCommandError> {
|
) -> Result<FormatResult, FormatCommandError> {
|
||||||
if let Some(cache) = cache {
|
if let Some(cache) = cache {
|
||||||
|
@ -250,8 +266,12 @@ pub(crate) fn format_path(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Don't write back to the cache if formatting a range.
|
||||||
|
let cache = cache.filter(|_| range.is_none());
|
||||||
|
|
||||||
// Format the source.
|
// Format the source.
|
||||||
let format_result = match format_source(&unformatted, source_type, Some(path), settings)? {
|
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)?
|
||||||
|
{
|
||||||
FormattedSource::Formatted(formatted) => match mode {
|
FormattedSource::Formatted(formatted) => match mode {
|
||||||
FormatMode::Write => {
|
FormatMode::Write => {
|
||||||
let mut writer = File::create(path).map_err(|err| {
|
let mut writer = File::create(path).map_err(|err| {
|
||||||
|
@ -319,12 +339,31 @@ pub(crate) fn format_source(
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
|
range: Option<FormatRange>,
|
||||||
) -> Result<FormattedSource, FormatCommandError> {
|
) -> Result<FormattedSource, FormatCommandError> {
|
||||||
match &source_kind {
|
match &source_kind {
|
||||||
SourceKind::Python(unformatted) => {
|
SourceKind::Python(unformatted) => {
|
||||||
let options = settings.to_format_options(source_type, unformatted);
|
let options = settings.to_format_options(source_type, unformatted);
|
||||||
|
|
||||||
let formatted = format_module_source(unformatted, options).map_err(|err| {
|
let formatted = if let Some(range) = range {
|
||||||
|
let line_index = LineIndex::from_source_text(unformatted);
|
||||||
|
let byte_range = range.to_text_range(unformatted, &line_index);
|
||||||
|
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
||||||
|
let mut formatted = unformatted.to_string();
|
||||||
|
formatted.replace_range(
|
||||||
|
std::ops::Range::<usize>::from(formatted_range.source_range()),
|
||||||
|
formatted_range.as_code(),
|
||||||
|
);
|
||||||
|
|
||||||
|
formatted
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
|
||||||
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
format_module_source(unformatted, options).map(|formatted| formatted.into_code())
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted = formatted.map_err(|err| {
|
||||||
if let FormatModuleError::ParseError(err) = err {
|
if let FormatModuleError::ParseError(err) = err {
|
||||||
DisplayParseError::from_source_kind(
|
DisplayParseError::from_source_kind(
|
||||||
err,
|
err,
|
||||||
|
@ -337,7 +376,6 @@ pub(crate) fn format_source(
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let formatted = formatted.into_code();
|
|
||||||
if formatted.len() == unformatted.len() && formatted == *unformatted {
|
if formatted.len() == unformatted.len() && formatted == *unformatted {
|
||||||
Ok(FormattedSource::Unchanged)
|
Ok(FormattedSource::Unchanged)
|
||||||
} else {
|
} else {
|
||||||
|
@ -349,6 +387,12 @@ pub(crate) fn format_source(
|
||||||
return Ok(FormattedSource::Unchanged);
|
return Ok(FormattedSource::Unchanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if range.is_some() {
|
||||||
|
return Err(FormatCommandError::RangeFormatNotebook(
|
||||||
|
path.map(Path::to_path_buf),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let options = settings.to_format_options(source_type, notebook.source_code());
|
let options = settings.to_format_options(source_type, notebook.source_code());
|
||||||
|
|
||||||
let mut output: Option<String> = None;
|
let mut output: Option<String> = None;
|
||||||
|
@ -589,6 +633,7 @@ pub(crate) enum FormatCommandError {
|
||||||
Format(Option<PathBuf>, FormatModuleError),
|
Format(Option<PathBuf>, FormatModuleError),
|
||||||
Write(Option<PathBuf>, SourceError),
|
Write(Option<PathBuf>, SourceError),
|
||||||
Diff(Option<PathBuf>, io::Error),
|
Diff(Option<PathBuf>, io::Error),
|
||||||
|
RangeFormatNotebook(Option<PathBuf>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FormatCommandError {
|
impl FormatCommandError {
|
||||||
|
@ -606,7 +651,8 @@ impl FormatCommandError {
|
||||||
| Self::Read(path, _)
|
| Self::Read(path, _)
|
||||||
| Self::Format(path, _)
|
| Self::Format(path, _)
|
||||||
| Self::Write(path, _)
|
| Self::Write(path, _)
|
||||||
| Self::Diff(path, _) => path.as_deref(),
|
| Self::Diff(path, _)
|
||||||
|
| Self::RangeFormatNotebook(path) => path.as_deref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -628,9 +674,10 @@ impl Display for FormatCommandError {
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{} {}",
|
"{header} {error}",
|
||||||
"Encountered error:".bold(),
|
header = "Encountered error:".bold(),
|
||||||
err.io_error()
|
error = err
|
||||||
|
.io_error()
|
||||||
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
|
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -648,7 +695,7 @@ impl Display for FormatCommandError {
|
||||||
":".bold()
|
":".bold()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{}{} {err}", "Failed to read".bold(), ":".bold())
|
write!(f, "{header} {err}", header = "Failed to read:".bold())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Write(path, err) => {
|
Self::Write(path, err) => {
|
||||||
|
@ -661,7 +708,7 @@ impl Display for FormatCommandError {
|
||||||
":".bold()
|
":".bold()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
|
write!(f, "{header} {err}", header = "Failed to write:".bold())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Format(path, err) => {
|
Self::Format(path, err) => {
|
||||||
|
@ -674,7 +721,7 @@ impl Display for FormatCommandError {
|
||||||
":".bold()
|
":".bold()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
|
write!(f, "{header} {err}", header = "Failed to format:".bold())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Diff(path, err) => {
|
Self::Diff(path, err) => {
|
||||||
|
@ -689,9 +736,25 @@ impl Display for FormatCommandError {
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{}{} {err}",
|
"{header} {err}",
|
||||||
"Failed to generate diff".bold(),
|
header = "Failed to generate diff:".bold(),
|
||||||
":".bold()
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::RangeFormatNotebook(path) => {
|
||||||
|
if let Some(path) = path {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{header}{path}{colon} Range formatting isn't supported for notebooks.",
|
||||||
|
header = "Failed to format ".bold(),
|
||||||
|
path = fs::relativize_path(path).bold(),
|
||||||
|
colon = ":".bold()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{header} Range formatting isn't supported for notebooks",
|
||||||
|
header = "Failed to format:".bold()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use ruff_python_ast::{PySourceType, SourceType};
|
||||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
|
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
||||||
use crate::args::{CliOverrides, FormatArguments};
|
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||||
use crate::commands::format::{
|
use crate::commands::format::{
|
||||||
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
|
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
|
||||||
FormatResult, FormattedSource,
|
FormatResult, FormattedSource,
|
||||||
|
@ -69,7 +69,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the file.
|
// Format the file.
|
||||||
match format_source_code(path, settings, source_type, mode) {
|
match format_source_code(path, cli.range, settings, source_type, mode) {
|
||||||
Ok(result) => match mode {
|
Ok(result) => match mode {
|
||||||
FormatMode::Write => Ok(ExitStatus::Success),
|
FormatMode::Write => Ok(ExitStatus::Success),
|
||||||
FormatMode::Check | FormatMode::Diff => {
|
FormatMode::Check | FormatMode::Diff => {
|
||||||
|
@ -90,6 +90,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||||
/// Format source code read from `stdin`.
|
/// Format source code read from `stdin`.
|
||||||
fn format_source_code(
|
fn format_source_code(
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
|
range: Option<FormatRange>,
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
mode: FormatMode,
|
mode: FormatMode,
|
||||||
|
@ -107,7 +108,7 @@ fn format_source_code(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the source.
|
// Format the source.
|
||||||
let formatted = format_source(&source_kind, source_type, path, settings)?;
|
let formatted = format_source(&source_kind, source_type, path, settings, range)?;
|
||||||
|
|
||||||
match &formatted {
|
match &formatted {
|
||||||
FormattedSource::Formatted(formatted) => match mode {
|
FormattedSource::Formatted(formatted) => match mode {
|
||||||
|
|
|
@ -1544,3 +1544,322 @@ include = ["*.ipy"]
|
||||||
"###);
|
"###);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_formatting() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:8-2:14"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
def foo(
|
||||||
|
arg1,
|
||||||
|
arg2,
|
||||||
|
):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_formatting_unicode() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:21-3"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1="👋🏽" ): print("Format this" )
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
def foo(arg1="👋🏽" ):
|
||||||
|
print("Format this")
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_formatting_multiple_files() -> std::io::Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let file1 = tempdir.path().join("file1.py");
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
&file1,
|
||||||
|
r#"
|
||||||
|
def file1(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let file2 = tempdir.path().join("file2.py");
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
&file2,
|
||||||
|
r#"
|
||||||
|
def file2(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--range=1:8-1:15"])
|
||||||
|
.arg(file1)
|
||||||
|
.arg(file2), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
ruff failed
|
||||||
|
Cause: The `--range` option is only supported when formatting a single file but the specified paths resolve to 2 files.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_formatting_out_of_bounds() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=100:40-200:1"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_start_larger_than_end() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=90-50"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: invalid value '90-50' for '--range <RANGE>': the start position '90:1' is greater than the end position '50:1'.
|
||||||
|
tip: Try switching start and end: '50:1-90:1'
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_line_numbers_only() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2-3"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
def foo(
|
||||||
|
arg1,
|
||||||
|
arg2,
|
||||||
|
):
|
||||||
|
print("Shouldn't format this" )
|
||||||
|
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_start_only() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=3"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Should format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Should format this")
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_end_only() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=-3"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Should format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
def foo(
|
||||||
|
arg1,
|
||||||
|
arg2,
|
||||||
|
):
|
||||||
|
print("Should format this" )
|
||||||
|
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_missing_line() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=1-:20"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Should format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: invalid value '1-:20' for '--range <RANGE>': the end line is not a valid number (cannot parse integer from empty string)
|
||||||
|
tip: The format is 'line:column'.
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_line_number() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:2"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Should format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: invalid value '0:2' for '--range <RANGE>': the start line is 0, but it should be 1 or greater.
|
||||||
|
tip: The line numbers start at 1.
|
||||||
|
tip: Try 1:2 instead.
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_and_line_zero() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:0"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
def foo(arg1, arg2,):
|
||||||
|
print("Should format this" )
|
||||||
|
|
||||||
|
"#), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: invalid value '0:0' for '--range <RANGE>': the start line and column are both 0, but they should be 1 or greater.
|
||||||
|
tip: The line and column numbers start at 1.
|
||||||
|
tip: Try 1:1 instead.
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_formatting_notebook() {
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["format", "--isolated", "--no-cache", "--stdin-filename", "main.ipynb", "--range=1-2"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"x=1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
|
"#), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
|
@ -215,6 +215,34 @@ impl LineIndex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [byte offset](TextSize) at `line` and `column`.
|
||||||
|
pub fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize {
|
||||||
|
// If start-of-line position after last line
|
||||||
|
if line.to_zero_indexed() > self.line_starts().len() {
|
||||||
|
return contents.text_len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_range = self.line_range(line, contents);
|
||||||
|
|
||||||
|
match self.kind() {
|
||||||
|
IndexKind::Ascii => {
|
||||||
|
line_range.start()
|
||||||
|
+ TextSize::try_from(column.get())
|
||||||
|
.unwrap_or(line_range.len())
|
||||||
|
.clamp(TextSize::new(0), line_range.len())
|
||||||
|
}
|
||||||
|
IndexKind::Utf8 => {
|
||||||
|
let rest = &contents[line_range];
|
||||||
|
let column_offset: TextSize = rest
|
||||||
|
.chars()
|
||||||
|
.take(column.get())
|
||||||
|
.map(ruff_text_size::TextLen::text_len)
|
||||||
|
.sum();
|
||||||
|
line_range.start() + column_offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the [byte offsets](TextSize) for every line
|
/// Returns the [byte offsets](TextSize) for every line
|
||||||
pub fn line_starts(&self) -> &[TextSize] {
|
pub fn line_starts(&self) -> &[TextSize] {
|
||||||
&self.inner.line_starts
|
&self.inner.line_starts
|
||||||
|
|
|
@ -654,7 +654,7 @@ Options:
|
||||||
Enable preview mode; enables unstable formatting. Use `--no-preview`
|
Enable preview mode; enables unstable formatting. Use `--no-preview`
|
||||||
to disable
|
to disable
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help
|
Print help (see more with '--help')
|
||||||
|
|
||||||
Miscellaneous:
|
Miscellaneous:
|
||||||
-n, --no-cache
|
-n, --no-cache
|
||||||
|
@ -679,6 +679,14 @@ File selection:
|
||||||
Format configuration:
|
Format configuration:
|
||||||
--line-length <LINE_LENGTH> Set the line-length
|
--line-length <LINE_LENGTH> Set the line-length
|
||||||
|
|
||||||
|
Editor options:
|
||||||
|
--range <RANGE> When specified, Ruff will try to only format the code in
|
||||||
|
the given range.
|
||||||
|
It might be necessary to extend the start backwards or
|
||||||
|
the end forwards, to fully enclose a logical line.
|
||||||
|
The `<RANGE>` uses the format
|
||||||
|
`<start_line>:<start_column>-<end_line>:<end_column>`.
|
||||||
|
|
||||||
Log levels:
|
Log levels:
|
||||||
-v, --verbose Enable verbose logging
|
-v, --verbose Enable verbose logging
|
||||||
-q, --quiet Print diagnostics, but nothing else
|
-q, --quiet Print diagnostics, but nothing else
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue