ffmt 0.3.4__tar.gz → 0.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ffmt-0.3.4 → ffmt-0.3.5}/Cargo.lock +11 -11
- {ffmt-0.3.4 → ffmt-0.3.5}/Cargo.toml +1 -1
- {ffmt-0.3.4 → ffmt-0.3.5}/PKG-INFO +2 -2
- {ffmt-0.3.4 → ffmt-0.3.5}/README.md +1 -1
- {ffmt-0.3.4 → ffmt-0.3.5}/pyproject.toml +1 -1
- {ffmt-0.3.4 → ffmt-0.3.5}/src/formatter.rs +115 -16
- {ffmt-0.3.4 → ffmt-0.3.5}/src/lsp.rs +4 -6
- {ffmt-0.3.4 → ffmt-0.3.5}/src/reader.rs +65 -16
- ffmt-0.3.5/tests/fixtures/trailing_comment_wrap.expected.fpp +24 -0
- ffmt-0.3.5/tests/fixtures/trailing_comment_wrap.input.fpp +26 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/integration.rs +17 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.github/workflows/ci.yml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.github/workflows/release.yml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.github/workflows/update-readme-version.yml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.gitignore +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.markdownlint.json +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.pre-commit-hooks.yaml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/.readthedocs.yml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/LICENSE +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/action.yml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/docs/ci.md +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/docs/configuration.md +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/docs/editors.md +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/docs/getting-started.md +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/docs/index.md +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/docs/requirements.txt +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/mkdocs.yml +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/align.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/case_norm.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/classifier.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/cli.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/config.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/keyword_norm.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/lib.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/main.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/scope.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/unicode.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/src/whitespace.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/case_norm.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/classifier.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/config_integration.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/blank_lines.expected.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/blank_lines.input.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/call_block.expected.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/call_block.input.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/directives.expected.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/directives.input.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/doxygen_spacing.expected.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/doxygen_spacing.input.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/fypp.expected.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/fypp.input.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/simple.expected.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/fixtures/simple.input.fpp +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/reader.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/scope.rs +0 -0
- {ffmt-0.3.4 → ffmt-0.3.5}/tests/whitespace.rs +0 -0
|
@@ -73,9 +73,9 @@ dependencies = [
|
|
|
73
73
|
|
|
74
74
|
[[package]]
|
|
75
75
|
name = "clap"
|
|
76
|
-
version = "4.6.
|
|
76
|
+
version = "4.6.1"
|
|
77
77
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
78
|
-
checksum = "
|
|
78
|
+
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
|
79
79
|
dependencies = [
|
|
80
80
|
"clap_builder",
|
|
81
81
|
"clap_derive",
|
|
@@ -95,9 +95,9 @@ dependencies = [
|
|
|
95
95
|
|
|
96
96
|
[[package]]
|
|
97
97
|
name = "clap_derive"
|
|
98
|
-
version = "4.6.
|
|
98
|
+
version = "4.6.1"
|
|
99
99
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
100
|
-
checksum = "
|
|
100
|
+
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
|
101
101
|
dependencies = [
|
|
102
102
|
"heck",
|
|
103
103
|
"proc-macro2",
|
|
@@ -156,7 +156,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
|
156
156
|
|
|
157
157
|
[[package]]
|
|
158
158
|
name = "ffmt"
|
|
159
|
-
version = "0.3.
|
|
159
|
+
version = "0.3.5"
|
|
160
160
|
dependencies = [
|
|
161
161
|
"clap",
|
|
162
162
|
"ignore",
|
|
@@ -182,9 +182,9 @@ dependencies = [
|
|
|
182
182
|
|
|
183
183
|
[[package]]
|
|
184
184
|
name = "hashbrown"
|
|
185
|
-
version = "0.
|
|
185
|
+
version = "0.17.1"
|
|
186
186
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
187
|
-
checksum = "
|
|
187
|
+
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|
188
188
|
|
|
189
189
|
[[package]]
|
|
190
190
|
name = "heck"
|
|
@@ -210,9 +210,9 @@ dependencies = [
|
|
|
210
210
|
|
|
211
211
|
[[package]]
|
|
212
212
|
name = "indexmap"
|
|
213
|
-
version = "2.
|
|
213
|
+
version = "2.14.0"
|
|
214
214
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
215
|
-
checksum = "
|
|
215
|
+
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
|
216
216
|
dependencies = [
|
|
217
217
|
"equivalent",
|
|
218
218
|
"hashbrown",
|
|
@@ -268,9 +268,9 @@ dependencies = [
|
|
|
268
268
|
|
|
269
269
|
[[package]]
|
|
270
270
|
name = "rayon"
|
|
271
|
-
version = "1.
|
|
271
|
+
version = "1.12.0"
|
|
272
272
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
273
|
-
checksum = "
|
|
273
|
+
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
|
274
274
|
dependencies = [
|
|
275
275
|
"either",
|
|
276
276
|
"rayon-core",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ffmt
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -88,7 +88,7 @@ multdiv = false
|
|
|
88
88
|
# pre-commit
|
|
89
89
|
repos:
|
|
90
90
|
- repo: https://github.com/sbryngelson/ffmt
|
|
91
|
-
rev: v0.3.
|
|
91
|
+
rev: v0.3.5
|
|
92
92
|
hooks:
|
|
93
93
|
- id: ffmt
|
|
94
94
|
```
|
|
@@ -555,6 +555,7 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u
|
|
|
555
555
|
if line.len() > ll
|
|
556
556
|
&& find_inline_doxygen_in_line(line).is_none()
|
|
557
557
|
&& !line.trim_end().ends_with('&')
|
|
558
|
+
&& !line.trim_start().starts_with("& ")
|
|
558
559
|
{
|
|
559
560
|
final_lines.extend(rewrap_line(line, ll, iw));
|
|
560
561
|
} else {
|
|
@@ -1455,31 +1456,27 @@ fn join_short_comments(lines: &[String], max_length: usize) -> Vec<String> {
|
|
|
1455
1456
|
}
|
|
1456
1457
|
}
|
|
1457
1458
|
|
|
1458
|
-
// If only one line
|
|
1459
|
+
// If only one line, emit as-is
|
|
1459
1460
|
if j == i + 1 {
|
|
1460
1461
|
result.push(line.clone());
|
|
1461
1462
|
i = j;
|
|
1462
1463
|
continue;
|
|
1463
1464
|
}
|
|
1464
1465
|
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
current.push_str(word);
|
|
1474
|
-
} else {
|
|
1475
|
-
result.push(format!("{}{}", prefix, current));
|
|
1476
|
-
current = word.to_string();
|
|
1466
|
+
// Only join when the combined text fits on a single line.
|
|
1467
|
+
// When each line is already at a reasonable length, they are
|
|
1468
|
+
// independent comments and merging them would destroy the
|
|
1469
|
+
// author's intent (and can cause a moved trailing comment to
|
|
1470
|
+
// be merged into an unrelated preceding standalone comment).
|
|
1471
|
+
if full_text.len() > avail {
|
|
1472
|
+
for line in lines.iter().take(j).skip(i) {
|
|
1473
|
+
result.push(line.clone());
|
|
1477
1474
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
result.push(format!("{}{}", prefix, current));
|
|
1475
|
+
i = j;
|
|
1476
|
+
continue;
|
|
1481
1477
|
}
|
|
1482
1478
|
|
|
1479
|
+
result.push(format!("{}{}", prefix, full_text));
|
|
1483
1480
|
i = j;
|
|
1484
1481
|
} else {
|
|
1485
1482
|
result.push(line.clone());
|
|
@@ -1903,6 +1900,27 @@ fn rewrap_line(line: &str, max_length: usize, indent_width: usize) -> Vec<String
|
|
|
1903
1900
|
let indent = leading_spaces(line);
|
|
1904
1901
|
let content = line.trim_start();
|
|
1905
1902
|
|
|
1903
|
+
// Split off trailing comment so we never produce a dangling "& ! comment" line.
|
|
1904
|
+
// We wrap only the code portion; the comment is reattached to the last line.
|
|
1905
|
+
let (code_content, trailing_comment) = split_trailing_comment(content);
|
|
1906
|
+
let code_content = code_content.trim_end();
|
|
1907
|
+
|
|
1908
|
+
// If the code portion (with indent) fits but the comment makes it too long,
|
|
1909
|
+
// move the comment to its own line(s) above the code.
|
|
1910
|
+
if !trailing_comment.is_empty() && indent + code_content.len() <= max_length {
|
|
1911
|
+
let comment_line = format!("{}{}", " ".repeat(indent), trailing_comment.trim());
|
|
1912
|
+
let mut result = wrap_comment(&comment_line, max_length, 0, indent_width);
|
|
1913
|
+
result.push(format!("{}{}", " ".repeat(indent), code_content));
|
|
1914
|
+
return result;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Use code-only content for wrapping; comment will be appended at the end
|
|
1918
|
+
let content = if trailing_comment.is_empty() {
|
|
1919
|
+
content
|
|
1920
|
+
} else {
|
|
1921
|
+
code_content
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1906
1924
|
// Find token boundaries in the content
|
|
1907
1925
|
let breaks = find_token_breaks(content);
|
|
1908
1926
|
|
|
@@ -2036,10 +2054,46 @@ fn rewrap_line(line: &str, max_length: usize, indent_width: usize) -> Vec<String
|
|
|
2036
2054
|
if result.is_empty() {
|
|
2037
2055
|
vec![line.to_string()]
|
|
2038
2056
|
} else {
|
|
2057
|
+
// Reattach trailing comment to the last line
|
|
2058
|
+
if !trailing_comment.is_empty() {
|
|
2059
|
+
if let Some(last) = result.last_mut() {
|
|
2060
|
+
last.push(' ');
|
|
2061
|
+
last.push_str(trailing_comment);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2039
2064
|
result
|
|
2040
2065
|
}
|
|
2041
2066
|
}
|
|
2042
2067
|
|
|
2068
|
+
/// Split content at the first `!` that starts a trailing comment (outside strings).
|
|
2069
|
+
/// Returns (code_part, comment_part). Comment part includes the `!`.
|
|
2070
|
+
fn split_trailing_comment(content: &str) -> (&str, &str) {
|
|
2071
|
+
let bytes = content.as_bytes();
|
|
2072
|
+
let mut in_string = false;
|
|
2073
|
+
let mut quote_char = b' ';
|
|
2074
|
+
|
|
2075
|
+
for (i, &b) in bytes.iter().enumerate() {
|
|
2076
|
+
if in_string {
|
|
2077
|
+
if b == quote_char {
|
|
2078
|
+
if i + 1 < bytes.len() && bytes[i + 1] == quote_char {
|
|
2079
|
+
continue; // doubled quote escape
|
|
2080
|
+
}
|
|
2081
|
+
in_string = false;
|
|
2082
|
+
}
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if b == b'\'' || b == b'"' {
|
|
2086
|
+
in_string = true;
|
|
2087
|
+
quote_char = b;
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if b == b'!' {
|
|
2091
|
+
return (&content[..i], &content[i..]);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
(content, "")
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2043
2097
|
#[derive(Clone, Copy)]
|
|
2044
2098
|
enum BreakKind {
|
|
2045
2099
|
Comma,
|
|
@@ -2058,10 +2112,37 @@ fn find_token_breaks(content: &str) -> Vec<(usize, BreakKind)> {
|
|
|
2058
2112
|
let mut in_string = false;
|
|
2059
2113
|
let mut quote_char = b' ';
|
|
2060
2114
|
let mut paren_depth = 0i32;
|
|
2115
|
+
let mut in_fypp = false; // inside ${...}$ or @{...}@ block
|
|
2061
2116
|
|
|
2062
2117
|
while i < len {
|
|
2063
2118
|
let b = bytes[i];
|
|
2064
2119
|
|
|
2120
|
+
// Skip entire Fypp inline expressions — never break inside ${...}$ or @{...}@
|
|
2121
|
+
if !in_string && !in_fypp && (b == b'$' || b == b'@') && i + 1 < len && bytes[i + 1] == b'{'
|
|
2122
|
+
{
|
|
2123
|
+
in_fypp = true;
|
|
2124
|
+
i += 2;
|
|
2125
|
+
let mut depth = 1i32;
|
|
2126
|
+
while i < len && depth > 0 {
|
|
2127
|
+
if bytes[i] == b'{' {
|
|
2128
|
+
depth += 1;
|
|
2129
|
+
} else if bytes[i] == b'}' {
|
|
2130
|
+
depth -= 1;
|
|
2131
|
+
if depth == 0 {
|
|
2132
|
+
i += 1;
|
|
2133
|
+
// consume trailing $ or @
|
|
2134
|
+
if i < len && (bytes[i] == b'$' || bytes[i] == b'@') {
|
|
2135
|
+
i += 1;
|
|
2136
|
+
}
|
|
2137
|
+
in_fypp = false;
|
|
2138
|
+
break;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
i += 1;
|
|
2142
|
+
}
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2065
2146
|
// Track strings — never break inside
|
|
2066
2147
|
if in_string {
|
|
2067
2148
|
if b == quote_char {
|
|
@@ -2104,6 +2185,19 @@ fn find_token_breaks(content: &str) -> Vec<(usize, BreakKind)> {
|
|
|
2104
2185
|
breaks.push((end, BreakKind::Comma));
|
|
2105
2186
|
}
|
|
2106
2187
|
|
|
2188
|
+
// Dot-keyword logical operators: .and. .or. .eqv. .neqv.
|
|
2189
|
+
// Break BEFORE the operator when preceded by a space, so a long
|
|
2190
|
+
// `if (a .or. b .or. c)` wraps as `a .or. b &\n& .or. c`.
|
|
2191
|
+
// .not. is unary — skip it. .true./.false. are literals — skip them.
|
|
2192
|
+
if !in_string && b == b'.' && i > 0 && bytes[i - 1] == b' ' {
|
|
2193
|
+
let rest = &bytes[i..];
|
|
2194
|
+
let lo =
|
|
2195
|
+
|op: &[u8]| rest.len() >= op.len() && rest[..op.len()].eq_ignore_ascii_case(op);
|
|
2196
|
+
if lo(b".and.") || lo(b".or.") || lo(b".eqv.") || lo(b".neqv.") {
|
|
2197
|
+
breaks.push((i, BreakKind::Operator));
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2107
2201
|
// Binary operators at any paren depth
|
|
2108
2202
|
{
|
|
2109
2203
|
// Multi-char operators: check for //, /=, ==, <=, >=, =>, **
|
|
@@ -2151,6 +2245,11 @@ fn find_token_breaks(content: &str) -> Vec<(usize, BreakKind)> {
|
|
|
2151
2245
|
i += 1;
|
|
2152
2246
|
}
|
|
2153
2247
|
|
|
2248
|
+
// Never break immediately before `%` (member accessor). A break there
|
|
2249
|
+
// would produce `& %member` on the next line, which is a blank within a
|
|
2250
|
+
// lexical token — invalid in Fortran free-form (§6.3.2.2).
|
|
2251
|
+
breaks.retain(|&(bp, _)| bp >= len || bytes[bp] != b'%');
|
|
2252
|
+
|
|
2154
2253
|
breaks
|
|
2155
2254
|
}
|
|
2156
2255
|
|
|
@@ -216,18 +216,16 @@ fn handle_message(
|
|
|
216
216
|
|
|
217
217
|
_ => {
|
|
218
218
|
// Unknown method — return method not found for requests (with id)
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
msg.get("id").map(|id| {
|
|
220
|
+
vec![json!({
|
|
221
221
|
"jsonrpc": "2.0",
|
|
222
222
|
"id": id,
|
|
223
223
|
"error": {
|
|
224
224
|
"code": -32601,
|
|
225
225
|
"message": format!("method not found: {}", method)
|
|
226
226
|
}
|
|
227
|
-
})]
|
|
228
|
-
}
|
|
229
|
-
None // notification, ignore
|
|
230
|
-
}
|
|
227
|
+
})]
|
|
228
|
+
})
|
|
231
229
|
}
|
|
232
230
|
}
|
|
233
231
|
}
|
|
@@ -86,14 +86,11 @@ fn strip_leading_amp(line: &str) -> &str {
|
|
|
86
86
|
fn strip_amp_comment_continuation(line: &str) -> String {
|
|
87
87
|
let trimmed = line.trim_start();
|
|
88
88
|
// Match: leading `&` followed by optional spaces then `!` (but not `!$` for OpenACC/OMP)
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
let after_amp = &trimmed[1..].trim_start();
|
|
89
|
+
if let Some(stripped) = trimmed.strip_prefix('&') {
|
|
90
|
+
let after_amp = stripped.trim_start();
|
|
92
91
|
if after_amp.starts_with('!') && !after_amp.starts_with("!$") {
|
|
93
|
-
// Convert to plain comment: preserve indentation, replace `& !` with `!`
|
|
94
92
|
let indent = line.len() - line.trim_start().len();
|
|
95
|
-
|
|
96
|
-
return format!("{}{}", " ".repeat(indent), comment);
|
|
93
|
+
return format!("{}{}", " ".repeat(indent), after_amp);
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
line.to_string()
|
|
@@ -236,22 +233,65 @@ pub fn read_logical_lines(source: &str) -> Vec<LogicalLine> {
|
|
|
236
233
|
}
|
|
237
234
|
|
|
238
235
|
// If this line is a comment (possibly after `& !` was cleaned to `!`),
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
236
|
+
// check whether more continuation code follows. Cray ftn (ftn-71)
|
|
237
|
+
// rejects `& ! comment` lines, so these are converted to plain
|
|
238
|
+
// comments. If they appear mid-continuation (more `& code` follows),
|
|
239
|
+
// we preserve them in the raw lines but skip them in the joined
|
|
240
|
+
// content so the logical statement stays intact. If the comment is
|
|
241
|
+
// the last line of the continuation, strip the trailing `&` from the
|
|
242
|
+
// preceding code line and stop.
|
|
243
243
|
let cont_trimmed = cont_line.trim_start();
|
|
244
244
|
if cont_trimmed.starts_with('!') && !cont_trimmed.starts_with("!$") {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
// Peek ahead past any additional blank/comment lines to see
|
|
246
|
+
// whether a continuation line (starting with `&`) follows.
|
|
247
|
+
let mut peek = i + 1;
|
|
248
|
+
while peek < raw_count {
|
|
249
|
+
let p = cleaned[peek].as_str().trim_start();
|
|
250
|
+
if p.is_empty() {
|
|
251
|
+
peek += 1;
|
|
252
|
+
continue;
|
|
248
253
|
}
|
|
254
|
+
if p.starts_with('!') && !p.starts_with("!$") {
|
|
255
|
+
peek += 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
let more_continuation =
|
|
261
|
+
peek < raw_count && cleaned[peek].as_str().trim_start().starts_with('&');
|
|
262
|
+
|
|
263
|
+
if more_continuation {
|
|
264
|
+
// Mid-continuation comment: keep in raw lines so the
|
|
265
|
+
// original structure is preserved in the
|
|
266
|
+
// continuation_interrupted path, but don't add any content
|
|
267
|
+
// to joined_parts (comments carry no code).
|
|
268
|
+
raw_lines_acc.push(cont_line.to_string());
|
|
269
|
+
i += 1;
|
|
270
|
+
continue;
|
|
271
|
+
} else {
|
|
272
|
+
// Terminal comment: strip trailing `&` from the last code
|
|
273
|
+
// line and stop — the comment becomes its own logical line.
|
|
274
|
+
if let Some(last_raw) = raw_lines_acc.last_mut() {
|
|
275
|
+
if let Some(pos) = find_continuation_amp(last_raw) {
|
|
276
|
+
*last_raw = last_raw[..pos].trim_end().to_string();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
249
280
|
}
|
|
250
|
-
break;
|
|
251
281
|
}
|
|
252
282
|
|
|
253
283
|
raw_lines_acc.push(cont_line.to_string());
|
|
254
284
|
|
|
285
|
+
// Track whether this continuation line had a leading `&`.
|
|
286
|
+
// Per the Fortran free-form standard, when `&` is the last
|
|
287
|
+
// non-blank character on line N and line N+1 begins with `&`,
|
|
288
|
+
// the statement continues immediately after the leading `&`
|
|
289
|
+
// with NO implicit space inserted (the two tokens are glued).
|
|
290
|
+
// When line N+1 does NOT begin with `&`, one implicit space
|
|
291
|
+
// is inserted. This matters for member access: `x &\n& %y`
|
|
292
|
+
// must join as `x%y`, not `x %y`.
|
|
293
|
+
let had_leading_amp = cont_line.trim_start().starts_with('&');
|
|
294
|
+
|
|
255
295
|
// Strip leading `&` if present
|
|
256
296
|
let stripped = strip_leading_amp(cont_line);
|
|
257
297
|
|
|
@@ -265,8 +305,17 @@ pub fn read_logical_lines(source: &str) -> Vec<LogicalLine> {
|
|
|
265
305
|
stripped.to_string()
|
|
266
306
|
};
|
|
267
307
|
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
let content_trimmed = content.trim_start();
|
|
309
|
+
// Suppress the space separator when the continuation starts with
|
|
310
|
+
// `%` (member accessor). Adding a space there would produce
|
|
311
|
+
// `a %b` which is a blank within a lexical token — invalid in
|
|
312
|
+
// Fortran free-form (§6.3.2.2).
|
|
313
|
+
let sep = if had_leading_amp && content_trimmed.starts_with('%') {
|
|
314
|
+
""
|
|
315
|
+
} else {
|
|
316
|
+
" "
|
|
317
|
+
};
|
|
318
|
+
joined_parts.push(format!("{}{}", sep, content_trimmed));
|
|
270
319
|
i += 1;
|
|
271
320
|
|
|
272
321
|
if !this_fort {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
subroutine test_trailing_comment()
|
|
2
|
+
|
|
3
|
+
! Already-broken dangling continuation + comment (illegal output from old formatter)
|
|
4
|
+
select case (dir)
|
|
5
|
+
case (101)
|
|
6
|
+
! this was an illegal dangling continuation comment
|
|
7
|
+
x = 2
|
|
8
|
+
end select
|
|
9
|
+
|
|
10
|
+
! Long trailing comment on a call - should move above and wrap
|
|
11
|
+
! this is a very long trailing comment that exceeds the line length limit and should be moved above the code line rather than
|
|
12
|
+
! dangling
|
|
13
|
+
call some_long_sub(arg1, arg2)
|
|
14
|
+
|
|
15
|
+
! Short trailing comment that fits - should stay inline
|
|
16
|
+
call some_sub(a, b, c) ! quick note
|
|
17
|
+
|
|
18
|
+
! Mid-continuation comment: code must stay intact (comment is dropped)
|
|
19
|
+
call some_sub(arg1, arg2, arg3, arg4)
|
|
20
|
+
|
|
21
|
+
! Mid-continuation comment with multiple following continuations
|
|
22
|
+
x = 1 + 2 + 3 + y + z
|
|
23
|
+
|
|
24
|
+
end subroutine test_trailing_comment
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
subroutine test_trailing_comment()
|
|
2
|
+
|
|
3
|
+
! Already-broken dangling continuation + comment (illegal output from old formatter)
|
|
4
|
+
select case (dir)
|
|
5
|
+
case (101) &
|
|
6
|
+
& ! this was an illegal dangling continuation comment
|
|
7
|
+
x = 2
|
|
8
|
+
end select
|
|
9
|
+
|
|
10
|
+
! Long trailing comment on a call — should move above and wrap
|
|
11
|
+
call some_long_sub(arg1, arg2) ! this is a very long trailing comment that exceeds the line length limit and should be moved above the code line rather than dangling
|
|
12
|
+
|
|
13
|
+
! Short trailing comment that fits — should stay inline
|
|
14
|
+
call some_sub(a, b, c) ! quick note
|
|
15
|
+
|
|
16
|
+
! Mid-continuation comment: code must stay intact (comment is dropped)
|
|
17
|
+
call some_sub(arg1, arg2, &
|
|
18
|
+
& ! comment about args
|
|
19
|
+
& arg3, arg4)
|
|
20
|
+
|
|
21
|
+
! Mid-continuation comment with multiple following continuations
|
|
22
|
+
x = 1+2+3 &
|
|
23
|
+
& ! explain the split
|
|
24
|
+
& + y + z
|
|
25
|
+
|
|
26
|
+
end subroutine test_trailing_comment
|
|
@@ -81,6 +81,23 @@ fn test_idempotent_fypp() {
|
|
|
81
81
|
assert_eq!(first, second, "Formatter is not idempotent on fypp fixture");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
#[test]
|
|
85
|
+
fn test_trailing_comment_wrap() {
|
|
86
|
+
run_fixture("trailing_comment_wrap");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn test_idempotent_trailing_comment_wrap() {
|
|
91
|
+
let fixture_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
|
|
92
|
+
let input = fs::read_to_string(fixture_dir.join("trailing_comment_wrap.input.fpp")).unwrap();
|
|
93
|
+
let first = ffmt::format_string(&input);
|
|
94
|
+
let second = ffmt::format_string(&first);
|
|
95
|
+
assert_eq!(
|
|
96
|
+
first, second,
|
|
97
|
+
"Formatter is not idempotent on trailing_comment_wrap fixture"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
#[test]
|
|
85
102
|
fn test_error_recovery() {
|
|
86
103
|
let input =
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|