@@ -122,6 +122,14 @@ struct Context<'a> {
122122 line_ending : & ' static str ,
123123}
124124
125+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
126+ enum Directive {
127+ Skip ,
128+ SkipFile ,
129+ On ,
130+ Off ,
131+ }
132+
125133fn traverse (
126134 out : & mut String ,
127135 cursor : & mut TreeCursor ,
@@ -164,54 +172,13 @@ fn traverse(
164172 out. push ( '}' ) ;
165173 Ok ( ( ) )
166174 } ;
167-
168- let get_raw = |node : Node | context. rope . byte_slice ( node. byte_range ( ) ) . to_string ( ) ;
169- let fmt_raw = |node : Node , out : & mut String | {
175+ let fmt_raw = |out : & mut String , node : Node | {
170176 // note: for CRLF documents, byte_range of comment node includes \r
171- out. push_str ( get_raw ( node) . trim_end_matches ( '\r' ) ) ;
177+ // see here: https://github.com/r-lib/tree-sitter-r/pull/184
178+ out. push_str ( get_raw ( node, context. rope ) . trim_end_matches ( '\r' ) ) ;
172179 Ok ( ( ) )
173180 } ;
174181
175- fn field < ' a > ( node : Node < ' a > , field_id : u16 ) -> Result < Node < ' a > , FormatError > {
176- node. child_by_field_id ( field_id)
177- . ok_or ( FormatError :: MissingField {
178- kind : node. kind ( ) ,
179- field : node
180- . language ( )
181- . field_name_for_id ( field_id)
182- . unwrap_or ( "unknown" ) ,
183- } )
184- }
185-
186- fn field_optional < ' a > ( node : Node < ' a > , field_id : u16 ) -> Option < Node < ' a > > {
187- node. child_by_field_id ( field_id)
188- }
189-
190- let is_comment =
191- |maybe_node : Option < Node > | maybe_node. is_some_and ( |next| next. kind_id ( ) == kind:: COMMENT ) ;
192-
193- // HACK: tree-sitter-r has wrong ending_position for extract with newlines before ths rhs:
194- // it only includes the newline but not the rhs. this hack uses at least the correct end_position
195- // see: https://github.com/users/felix-andreas/projects/5?pane=issue&itemId=100962575
196- let end_position = |node : Node | {
197- if node. kind_id ( ) != kind:: EXTRACT_OPERATOR {
198- return node. end_position ( ) ;
199- }
200-
201- field_optional ( node, field:: RHS )
202- . map ( |rhs| rhs. end_position ( ) )
203- . or_else ( || {
204- field_optional ( node, field:: OPERATOR ) . map ( |operator| operator. end_position ( ) )
205- } )
206- // note: this case is unexpected
207- . unwrap_or_else ( || node. end_position ( ) )
208- } ;
209-
210- let same_line = |a : Node , b : Node | end_position ( a) . row == b. start_position ( ) . row ;
211-
212- let is_fmt_skip_comment =
213- |node : Node | node. kind_id ( ) == kind:: COMMENT && get_raw ( node) . contains ( "fmt: skip" ) ;
214-
215182 let node = cursor. node ( ) ;
216183 let kind_id = node. kind_id ( ) ;
217184
@@ -231,8 +198,13 @@ fn traverse(
231198 } ) ;
232199 }
233200
234- // check if prev or next node is fmt- skip directive
201+ // handle skip directives
235202 {
203+ let is_fmt_skip_comment = |node : Node | {
204+ node. kind_id ( ) == kind:: COMMENT
205+ && parse_directive ( & get_raw ( node, context. rope ) )
206+ . is_some_and ( |directive| directive == Directive :: Skip )
207+ } ;
236208 let prev_is_fmt_skip = node. prev_sibling ( ) . is_some_and ( |prev| {
237209 is_fmt_skip_comment ( prev)
238210 && prev
@@ -244,22 +216,22 @@ fn traverse(
244216 . is_some_and ( |next| is_fmt_skip_comment ( next) && same_line ( node, next) ) ;
245217
246218 if prev_is_fmt_skip || next_is_fmt_skip {
247- return fmt_raw ( node , out ) ;
219+ return fmt_raw ( out , node ) ;
248220 }
249221 }
250222
251223 if !node. is_named ( ) {
252- return fmt_raw ( node , out ) ;
224+ return fmt_raw ( out , node ) ;
253225 }
254226
255227 match kind_id {
256228 // SPECIAL
257- kind:: IDENTIFIER => fmt_raw ( node , out ) ?,
229+ kind:: IDENTIFIER => fmt_raw ( out , node ) ?,
258230 kind:: COMMENT => {
259- let raw = get_raw ( node) ;
231+ let raw = get_raw ( node, context . rope ) ;
260232 let raw = raw. trim_end ( ) ;
261- let mut chars = raw. chars ( ) ;
262233
234+ let mut chars = raw. chars ( ) ;
263235 let _ = chars. next ( ) ; // Skip the '#'
264236 // reformat comments like #foo to # foo but keep #' foo
265237 match chars. next ( ) {
@@ -294,12 +266,12 @@ fn traverse(
294266 kind:: NULL => out. push_str ( "NULL" ) ,
295267 kind:: INF => out. push_str ( "Inf" ) ,
296268 kind:: NAN => out. push_str ( "NaN" ) ,
297- kind:: INTEGER => fmt_raw ( node , out ) ?,
298- kind:: COMPLEX => fmt_raw ( node , out ) ?,
299- kind:: FLOAT => fmt_raw ( node , out ) ?,
269+ kind:: INTEGER => fmt_raw ( out , node ) ?,
270+ kind:: COMPLEX => fmt_raw ( out , node ) ?,
271+ kind:: FLOAT => fmt_raw ( out , node ) ?,
300272 kind:: STRING => {
301273 if let Some ( content) = field_optional ( node, field:: CONTENT ) {
302- let raw = get_raw ( content) ;
274+ let raw = get_raw ( content, context . rope ) ;
303275 let mut all_quotes_escaped = true ;
304276 let mut prev_was_escape = false ;
305277 for char in raw. chars ( ) {
@@ -316,12 +288,12 @@ fn traverse(
316288 out. push_str ( r#""""# ) ;
317289 }
318290 }
319- kind:: NA => fmt_raw ( node , out ) ?,
291+ kind:: NA => fmt_raw ( out , node ) ?,
320292 // both handled by STRING
321293 kind:: ESCAPE_SEQUENCE | kind:: STRING_CONTENT => unreachable ! ( ) ,
322294 // KEYWORDS
323295 kind:: DOTS => out. push_str ( "..." ) ,
324- kind:: DOT_DOT_I => fmt_raw ( node , out ) ?,
296+ kind:: DOT_DOT_I => fmt_raw ( out , node ) ?,
325297 kind:: RETURN => out. push_str ( "return" ) ,
326298 kind:: NEXT => out. push_str ( "next" ) ,
327299 kind:: BREAK => out. push_str ( "break" ) ,
@@ -918,19 +890,62 @@ fn traverse(
918890 } ) ?;
919891 }
920892 kind:: PROGRAM => {
893+ let mut enabled = true ;
894+ let mut maybe_directive = None ;
895+ if node. child ( 0 ) . is_some_and ( |child| {
896+ child. kind_id ( ) == kind:: COMMENT
897+ && parse_directive ( & get_raw ( child, context. rope ) )
898+ . is_some_and ( |directive| directive == Directive :: SkipFile )
899+ } ) {
900+ return fmt_raw ( out, node) ;
901+ }
902+
921903 tree:: for_each_child ( cursor, |_, child, _, cursor| {
922- let maybe_prev = child. prev_sibling ( ) ;
904+ // Delay toggling the `enabled` flag until after handling newlines.
905+ // This ensures that any preceding newlines are attributed to the previous child
906+ match maybe_directive {
907+ Some ( Directive :: On ) => enabled = true ,
908+ Some ( Directive :: Off ) => enabled = false ,
909+ _ => { }
910+ }
923911
924- match child. kind_id ( ) {
925- kind:: COMMENT if maybe_prev. is_some_and ( |prev| same_line ( prev, child) ) => {
926- space ( out) ;
912+ maybe_directive = match child. kind_id ( ) {
913+ kind:: COMMENT => parse_directive ( & get_raw ( child, context. rope ) ) ,
914+ _ => None ,
915+ } ;
916+
917+ let maybe_prev = child. prev_sibling ( ) ;
918+ if enabled {
919+ match child. kind_id ( ) {
920+ kind:: COMMENT => {
921+ if maybe_prev. is_some_and ( |prev| same_line ( prev, child) ) {
922+ space ( out) ;
923+ } else {
924+ newlines ( out, child, maybe_prev) ;
925+ }
926+ }
927+ _ => {
928+ newlines ( out, child, maybe_prev) ;
929+ }
930+ }
931+ fmt ( out, cursor)
932+ } else {
933+ if let Some ( prev) = maybe_prev {
934+ out. push_str (
935+ & context
936+ . line_ending
937+ . repeat ( child. start_position ( ) . row - prev. end_position ( ) . row ) ,
938+ )
927939 }
928- _ => {
929- newlines ( out, child, maybe_prev) ;
940+ // we also want to format current directive comment
941+ if maybe_directive. is_some ( ) {
942+ fmt ( out, cursor)
943+ } else {
944+ fmt_raw ( out, child)
930945 }
931946 }
932- fmt ( out, cursor)
933947 } ) ?;
948+
934949 newline ( out) ;
935950 }
936951 kind:: REPEAT_STATEMENT => {
@@ -1095,10 +1110,93 @@ fn traverse(
10951110
10961111 return Err ( FormatError :: UnknownKind {
10971112 kind : node. kind ( ) ,
1098- raw : get_raw ( node) ,
1113+ raw : get_raw ( node, context . rope ) ,
10991114 } ) ;
11001115 }
11011116 } ;
11021117
11031118 Ok ( ( ) )
11041119}
1120+
1121+ fn get_raw ( node : Node , rope : & Rope ) -> String {
1122+ rope. byte_slice ( node. byte_range ( ) ) . to_string ( )
1123+ }
1124+
1125+ fn field < ' a > ( node : Node < ' a > , field_id : u16 ) -> Result < Node < ' a > , FormatError > {
1126+ node. child_by_field_id ( field_id)
1127+ . ok_or ( FormatError :: MissingField {
1128+ kind : node. kind ( ) ,
1129+ field : node
1130+ . language ( )
1131+ . field_name_for_id ( field_id)
1132+ . unwrap_or ( "unknown" ) ,
1133+ } )
1134+ }
1135+
1136+ fn field_optional < ' a > ( node : Node < ' a > , field_id : u16 ) -> Option < Node < ' a > > {
1137+ node. child_by_field_id ( field_id)
1138+ }
1139+
1140+ fn is_comment ( maybe_node : Option < Node > ) -> bool {
1141+ maybe_node. is_some_and ( |node| node. kind_id ( ) == kind:: COMMENT )
1142+ }
1143+
1144+ fn same_line ( a : Node , b : Node ) -> bool {
1145+ end_position ( a) . row == b. start_position ( ) . row
1146+ }
1147+
1148+ // HACK: tree-sitter-r has wrong ending_position for extract with newlines before ths rhs:
1149+ // it only includes the newline but not the rhs. this hack uses at least the correct end_position
1150+ // see: https://github.com/users/felix-andreas/projects/5?pane=issue&itemId=100962575
1151+ fn end_position ( node : Node ) -> tree_sitter:: Point {
1152+ if node. kind_id ( ) != kind:: EXTRACT_OPERATOR {
1153+ return node. end_position ( ) ;
1154+ }
1155+
1156+ field_optional ( node, field:: RHS )
1157+ . map ( |rhs| rhs. end_position ( ) )
1158+ . or_else ( || field_optional ( node, field:: OPERATOR ) . map ( |operator| operator. end_position ( ) ) )
1159+ // note: this case is unexpected
1160+ . unwrap_or_else ( || node. end_position ( ) )
1161+ }
1162+
1163+ fn parse_directive ( text : & str ) -> Option < Directive > {
1164+ text. trim_start_matches ( |c : char | c. is_whitespace ( ) || c == '#' )
1165+ . strip_prefix ( "fmt:" )
1166+ . and_then ( |rhs| match rhs. trim ( ) {
1167+ "skip" => Some ( Directive :: Skip ) ,
1168+ "skip-file" => Some ( Directive :: SkipFile ) ,
1169+ "on" => Some ( Directive :: On ) ,
1170+ "off" => Some ( Directive :: Off ) ,
1171+ _ => None ,
1172+ } )
1173+ }
1174+
1175+ #[ cfg( test) ]
1176+ mod tests {
1177+ use super :: * ;
1178+
1179+ #[ test]
1180+ fn parse_directive_skip ( ) {
1181+ assert_eq ! ( parse_directive( "# fmt: skip" ) , Some ( Directive :: Skip ) ) ;
1182+ assert_eq ! (
1183+ parse_directive( "# fmt: skip-file" ) ,
1184+ Some ( Directive :: SkipFile )
1185+ ) ;
1186+ assert_eq ! ( parse_directive( "# fmt: on" ) , Some ( Directive :: On ) ) ;
1187+ assert_eq ! ( parse_directive( "# fmt: off" ) , Some ( Directive :: Off ) ) ;
1188+
1189+ // check whitespace variations
1190+ assert_eq ! ( parse_directive( "#fmt:skip" ) , Some ( Directive :: Skip ) ) ;
1191+ assert_eq ! ( parse_directive( "# fmt:skip " ) , Some ( Directive :: Skip ) ) ;
1192+ assert_eq ! ( parse_directive( " # fmt: skip " ) , Some ( Directive :: Skip ) ) ;
1193+ }
1194+
1195+ #[ test]
1196+ fn parse_directive_none ( ) {
1197+ assert_eq ! ( parse_directive( "# fmt:unknown" ) , None ) ;
1198+ assert_eq ! ( parse_directive( "# something else" ) , None ) ;
1199+ assert_eq ! ( parse_directive( "" ) , None ) ;
1200+ assert_eq ! ( parse_directive( "" ) , None ) ;
1201+ }
1202+ }
0 commit comments