1+ use std:: cmp:: Ordering ;
2+ use std:: fmt:: Formatter ;
13use std:: path:: PathBuf ;
4+ use std:: str:: FromStr ;
25
36use clap:: { command, Parser } ;
7+ use colored:: Colorize ;
48use regex:: Regex ;
59use rustc_hash:: FxHashMap ;
610
@@ -12,6 +16,8 @@ use ruff_linter::settings::types::{
1216 SerializationFormat , UnsafeFixes ,
1317} ;
1418use ruff_linter:: { warn_user, RuleParser , RuleSelector , RuleSelectorParser } ;
19+ use ruff_source_file:: { LineIndex , OneIndexed } ;
20+ use ruff_text_size:: TextRange ;
1521use ruff_workspace:: configuration:: { Configuration , RuleSelection } ;
1622use ruff_workspace:: options:: PycodestyleOptions ;
1723use ruff_workspace:: resolver:: ConfigurationTransformer ;
@@ -440,6 +446,21 @@ pub struct FormatCommand {
440446 preview : bool ,
441447 #[ clap( long, overrides_with( "preview" ) , hide = true ) ]
442448 no_preview : bool ,
449+
450+ /// When specified, Ruff will try to only format the code in the given range.
451+ /// It might be necessary to extend the start backwards or the end forwards, to fully enclose a logical line.
452+ /// The `<RANGE>` uses the format `<start_line>:<start_column>-<end_line>:<end_column>`.
453+ ///
454+ /// - The line and column numbers are 1 based.
455+ /// - The column specifies the nth-unicode codepoint on that line.
456+ /// - The end offset is exclusive.
457+ /// - The column numbers are optional. You can write `--range=1-2` instead of `--range=1:1-2:1`.
458+ /// - The end position is optional. You can write `--range=2` to format the entire document starting from the second line.
459+ /// - The start position is optional. You can write `--range=-3` to format the first three lines of the document.
460+ ///
461+ /// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported.
462+ #[ clap( long, help_heading = "Editor options" , verbatim_doc_comment) ]
463+ pub range : Option < FormatRange > ,
443464}
444465
445466#[ derive( Debug , Clone , Copy , clap:: ValueEnum ) ]
@@ -570,6 +591,7 @@ impl FormatCommand {
570591 isolated : self . isolated ,
571592 no_cache : self . no_cache ,
572593 stdin_filename : self . stdin_filename ,
594+ range : self . range ,
573595 } ,
574596 CliOverrides {
575597 line_length : self . line_length ,
@@ -670,6 +692,196 @@ pub struct FormatArguments {
670692 pub files : Vec < PathBuf > ,
671693 pub isolated : bool ,
672694 pub stdin_filename : Option < PathBuf > ,
695+ pub range : Option < FormatRange > ,
696+ }
697+
698+ /// A text range specified by line and column numbers.
699+ #[ derive( Copy , Clone , Debug ) ]
700+ pub struct FormatRange {
701+ start : LineColumn ,
702+ end : LineColumn ,
703+ }
704+
705+ impl FormatRange {
706+ /// Converts the line:column range to a byte offset range specific for `source`.
707+ ///
708+ /// Returns an empty range if the start range is past the end of `source`.
709+ pub ( super ) fn to_text_range ( self , source : & str , line_index : & LineIndex ) -> TextRange {
710+ let start_byte_offset = line_index. offset ( self . start . line , self . start . column , source) ;
711+ let end_byte_offset = line_index. offset ( self . end . line , self . end . column , source) ;
712+
713+ TextRange :: new ( start_byte_offset, end_byte_offset)
714+ }
715+ }
716+
717+ impl FromStr for FormatRange {
718+ type Err = FormatRangeParseError ;
719+
720+ fn from_str ( value : & str ) -> Result < Self , Self :: Err > {
721+ let ( start, end) = value. split_once ( '-' ) . unwrap_or ( ( value, "" ) ) ;
722+
723+ let start = if start. is_empty ( ) {
724+ LineColumn :: default ( )
725+ } else {
726+ start. parse ( ) . map_err ( FormatRangeParseError :: InvalidStart ) ?
727+ } ;
728+
729+ let end = if end. is_empty ( ) {
730+ LineColumn {
731+ line : OneIndexed :: MAX ,
732+ column : OneIndexed :: MAX ,
733+ }
734+ } else {
735+ end. parse ( ) . map_err ( FormatRangeParseError :: InvalidEnd ) ?
736+ } ;
737+
738+ if start > end {
739+ return Err ( FormatRangeParseError :: StartGreaterThanEnd ( start, end) ) ;
740+ }
741+
742+ Ok ( FormatRange { start, end } )
743+ }
744+ }
745+
746+ #[ derive( Clone , Debug ) ]
747+ pub enum FormatRangeParseError {
748+ InvalidStart ( LineColumnParseError ) ,
749+ InvalidEnd ( LineColumnParseError ) ,
750+
751+ StartGreaterThanEnd ( LineColumn , LineColumn ) ,
752+ }
753+
754+ impl std:: fmt:: Display for FormatRangeParseError {
755+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
756+ let tip = " tip:" . bold ( ) . green ( ) ;
757+ match self {
758+ FormatRangeParseError :: StartGreaterThanEnd ( start, end) => {
759+ write ! (
760+ f,
761+ "the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'" ,
762+ start_invalid=start. to_string( ) . bold( ) . yellow( ) ,
763+ end_invalid=end. to_string( ) . bold( ) . yellow( ) ,
764+ start=start. to_string( ) . green( ) . bold( ) ,
765+ end=end. to_string( ) . green( ) . bold( )
766+ )
767+ }
768+ FormatRangeParseError :: InvalidStart ( inner) => inner. write ( f, true ) ,
769+ FormatRangeParseError :: InvalidEnd ( inner) => inner. write ( f, false ) ,
770+ }
771+ }
772+ }
773+
774+ impl std:: error:: Error for FormatRangeParseError { }
775+
776+ #[ derive( Copy , Clone , Debug ) ]
777+ pub struct LineColumn {
778+ pub line : OneIndexed ,
779+ pub column : OneIndexed ,
780+ }
781+
782+ impl std:: fmt:: Display for LineColumn {
783+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
784+ write ! ( f, "{line}:{column}" , line = self . line, column = self . column)
785+ }
786+ }
787+
788+ impl Default for LineColumn {
789+ fn default ( ) -> Self {
790+ LineColumn {
791+ line : OneIndexed :: MIN ,
792+ column : OneIndexed :: MIN ,
793+ }
794+ }
795+ }
796+
797+ impl PartialOrd for LineColumn {
798+ #[ inline]
799+ fn partial_cmp ( & self , other : & Self ) -> Option < Ordering > {
800+ Some ( self . cmp ( other) )
801+ }
802+ }
803+
804+ impl Ord for LineColumn {
805+ fn cmp ( & self , other : & Self ) -> Ordering {
806+ self . line
807+ . cmp ( & other. line )
808+ . then ( self . column . cmp ( & other. column ) )
809+ }
810+ }
811+
812+ impl PartialEq for LineColumn {
813+ fn eq ( & self , other : & Self ) -> bool {
814+ self . cmp ( other) == Ordering :: Equal
815+ }
816+ }
817+
818+ impl Eq for LineColumn { }
819+
820+ impl FromStr for LineColumn {
821+ type Err = LineColumnParseError ;
822+
823+ fn from_str ( value : & str ) -> Result < Self , Self :: Err > {
824+ let ( line, column) = value. split_once ( ':' ) . unwrap_or ( ( value, "1" ) ) ;
825+
826+ let line: usize = line. parse ( ) . map_err ( LineColumnParseError :: LineParseError ) ?;
827+ let column: usize = column
828+ . parse ( )
829+ . map_err ( LineColumnParseError :: ColumnParseError ) ?;
830+
831+ match ( OneIndexed :: new ( line) , OneIndexed :: new ( column) ) {
832+ ( Some ( line) , Some ( column) ) => Ok ( LineColumn { line, column } ) ,
833+ ( Some ( line) , None ) => Err ( LineColumnParseError :: ZeroColumnIndex { line } ) ,
834+ ( None , Some ( column) ) => Err ( LineColumnParseError :: ZeroLineIndex { column } ) ,
835+ ( None , None ) => Err ( LineColumnParseError :: ZeroLineAndColumnIndex ) ,
836+ }
837+ }
838+ }
839+
840+ #[ derive( Clone , Debug ) ]
841+ pub enum LineColumnParseError {
842+ ZeroLineIndex { column : OneIndexed } ,
843+ ZeroColumnIndex { line : OneIndexed } ,
844+ ZeroLineAndColumnIndex ,
845+ LineParseError ( std:: num:: ParseIntError ) ,
846+ ColumnParseError ( std:: num:: ParseIntError ) ,
847+ }
848+
849+ impl LineColumnParseError {
850+ fn write ( & self , f : & mut std:: fmt:: Formatter , start_range : bool ) -> std:: fmt:: Result {
851+ let tip = "tip:" . bold ( ) . green ( ) ;
852+
853+ let range = if start_range { "start" } else { "end" } ;
854+
855+ match self {
856+ LineColumnParseError :: ColumnParseError ( inner) => {
857+ write ! ( f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'." )
858+ }
859+ LineColumnParseError :: LineParseError ( inner) => {
860+ write ! ( f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'." )
861+ }
862+ LineColumnParseError :: ZeroColumnIndex { line } => {
863+ write ! (
864+ f,
865+ "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." ,
866+ suggestion=format!( "{line}:1" ) . green( ) . bold( )
867+ )
868+ }
869+ LineColumnParseError :: ZeroLineIndex { column } => {
870+ write ! (
871+ f,
872+ "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." ,
873+ suggestion=format!( "1:{column}" ) . green( ) . bold( )
874+ )
875+ }
876+ LineColumnParseError :: ZeroLineAndColumnIndex => {
877+ write ! (
878+ f,
879+ "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." ,
880+ suggestion="1:1" . to_string( ) . green( ) . bold( )
881+ )
882+ }
883+ }
884+ }
673885}
674886
675887/// CLI settings that function as configuration overrides.
0 commit comments