Skip to content

Commit bb08005

Browse files
Merge branch 'main' into copilot/fix-58
2 parents d541573 + 1355f5d commit bb08005

21 files changed

+534
-77
lines changed

.vscode/settings.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
2-
"editor.insertSpaces": false,
3-
"typescript.tsc.autoDetect": "off",
4-
"typescript.preferences.quoteStyle": "single",
5-
"editor.codeActionsOnSave": {
6-
"source.fixAll.eslint": "explicit"
7-
},
2+
"editor.codeActionsOnSave": {
3+
"source.fixAll.eslint": "explicit"
4+
},
5+
"files.associations": {
6+
"*.R.test": "r",
7+
},
8+
"typescript.tsc.autoDetect": "off",
89
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Download the pre-built binary for your platform from the [releases page](https:/
5656

5757
### Build from Source
5858

59-
Alternatively, build from source (requires Rust nightly):
59+
Alternatively, build from source:
6060

6161
```sh
6262
cargo build --release

crates/roughly/src/format.rs

Lines changed: 162 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
125133
fn 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

Comments
 (0)