ffmt 0.4.1__tar.gz → 0.4.2__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.4.1 → ffmt-0.4.2}/Cargo.lock +11 -11
- {ffmt-0.4.1 → ffmt-0.4.2}/Cargo.toml +1 -1
- {ffmt-0.4.1 → ffmt-0.4.2}/PKG-INFO +2 -2
- {ffmt-0.4.1 → ffmt-0.4.2}/README.md +1 -1
- {ffmt-0.4.1 → ffmt-0.4.2}/pyproject.toml +1 -1
- {ffmt-0.4.1 → ffmt-0.4.2}/src/classifier.rs +28 -1
- {ffmt-0.4.1 → ffmt-0.4.2}/src/cli.rs +42 -24
- {ffmt-0.4.1 → ffmt-0.4.2}/src/formatter.rs +117 -50
- {ffmt-0.4.1 → ffmt-0.4.2}/src/lsp.rs +119 -12
- {ffmt-0.4.1 → ffmt-0.4.2}/src/reader.rs +58 -2
- {ffmt-0.4.1 → ffmt-0.4.2}/src/whitespace.rs +75 -38
- ffmt-0.4.2/tests/bugfixes.rs +366 -0
- ffmt-0.4.2/tests/cli_bugs.rs +128 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/trailing_comment_wrap.expected.fpp +2 -2
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/trailing_comment_wrap.input.fpp +2 -2
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/reader.rs +56 -1
- {ffmt-0.4.1 → ffmt-0.4.2}/.github/workflows/ci.yml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/.github/workflows/release.yml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/.github/workflows/update-readme-version.yml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/.gitignore +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/.markdownlint.json +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/.pre-commit-hooks.yaml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/.readthedocs.yml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/LICENSE +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/action.yml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/docs/ci.md +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/docs/configuration.md +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/docs/editors.md +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/docs/getting-started.md +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/docs/index.md +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/docs/requirements.txt +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/mkdocs.yml +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/align.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/case_norm.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/config.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/keyword_norm.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/lib.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/main.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/scope.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/src/unicode.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/case_norm.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/classifier.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/config_integration.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/blank_lines.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/blank_lines.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/call_block.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/call_block.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/directives.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/directives.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/doxygen_spacing.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/doxygen_spacing.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/fypp.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/fypp.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/member_access_wrap.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/member_access_wrap.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/simple.expected.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/fixtures/simple.input.fpp +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/integration.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/scope.rs +0 -0
- {ffmt-0.4.1 → ffmt-0.4.2}/tests/whitespace.rs +0 -0
|
@@ -156,7 +156,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
|
156
156
|
|
|
157
157
|
[[package]]
|
|
158
158
|
name = "ffmt"
|
|
159
|
-
version = "0.4.
|
|
159
|
+
version = "0.4.2"
|
|
160
160
|
dependencies = [
|
|
161
161
|
"clap",
|
|
162
162
|
"ignore",
|
|
@@ -194,9 +194,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "ignore"
|
|
197
|
-
version = "0.4.
|
|
197
|
+
version = "0.4.26"
|
|
198
198
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
199
|
-
checksum = "
|
|
199
|
+
checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d"
|
|
200
200
|
dependencies = [
|
|
201
201
|
"crossbeam-deque",
|
|
202
202
|
"globset",
|
|
@@ -232,15 +232,15 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
|
232
232
|
|
|
233
233
|
[[package]]
|
|
234
234
|
name = "log"
|
|
235
|
-
version = "0.4.
|
|
235
|
+
version = "0.4.32"
|
|
236
236
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
237
|
-
checksum = "
|
|
237
|
+
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
|
238
238
|
|
|
239
239
|
[[package]]
|
|
240
240
|
name = "memchr"
|
|
241
|
-
version = "2.8.
|
|
241
|
+
version = "2.8.2"
|
|
242
242
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
243
|
-
checksum = "
|
|
243
|
+
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
244
244
|
|
|
245
245
|
[[package]]
|
|
246
246
|
name = "once_cell_polyfill"
|
|
@@ -288,9 +288,9 @@ dependencies = [
|
|
|
288
288
|
|
|
289
289
|
[[package]]
|
|
290
290
|
name = "regex"
|
|
291
|
-
version = "1.12.
|
|
291
|
+
version = "1.12.4"
|
|
292
292
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
293
|
-
checksum = "
|
|
293
|
+
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
|
294
294
|
dependencies = [
|
|
295
295
|
"aho-corasick",
|
|
296
296
|
"memchr",
|
|
@@ -311,9 +311,9 @@ dependencies = [
|
|
|
311
311
|
|
|
312
312
|
[[package]]
|
|
313
313
|
name = "regex-syntax"
|
|
314
|
-
version = "0.8.
|
|
314
|
+
version = "0.8.11"
|
|
315
315
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
316
|
-
checksum = "
|
|
316
|
+
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
317
317
|
|
|
318
318
|
[[package]]
|
|
319
319
|
name = "same-file"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ffmt
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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.4.
|
|
91
|
+
rev: v0.4.2
|
|
92
92
|
hooks:
|
|
93
93
|
- id: ffmt
|
|
94
94
|
```
|
|
@@ -100,11 +100,31 @@ fn strip_trailing_comment(s: &str) -> String {
|
|
|
100
100
|
|
|
101
101
|
/// Find the matching closing paren for the paren at position `start`.
|
|
102
102
|
/// Returns the index of the closing paren, or None if not found.
|
|
103
|
+
/// Parens inside string literals are ignored (e.g., `if (ch == '(') then`).
|
|
103
104
|
fn find_matching_paren(s: &str, start: usize) -> Option<usize> {
|
|
104
105
|
let bytes = s.as_bytes();
|
|
105
106
|
let mut depth = 0i32;
|
|
106
|
-
|
|
107
|
+
let mut in_string = false;
|
|
108
|
+
let mut quote = b' ';
|
|
109
|
+
let mut i = start;
|
|
110
|
+
while i < bytes.len() {
|
|
111
|
+
let byte = bytes[i];
|
|
112
|
+
if in_string {
|
|
113
|
+
if byte == quote {
|
|
114
|
+
if i + 1 < bytes.len() && bytes[i + 1] == quote {
|
|
115
|
+
i += 2; // escaped quote — stay in string
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
in_string = false;
|
|
119
|
+
}
|
|
120
|
+
i += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
107
123
|
match byte {
|
|
124
|
+
b'\'' | b'"' => {
|
|
125
|
+
in_string = true;
|
|
126
|
+
quote = byte;
|
|
127
|
+
}
|
|
108
128
|
b'(' => depth += 1,
|
|
109
129
|
b')' => {
|
|
110
130
|
depth -= 1;
|
|
@@ -114,6 +134,7 @@ fn find_matching_paren(s: &str, start: usize) -> Option<usize> {
|
|
|
114
134
|
}
|
|
115
135
|
_ => {}
|
|
116
136
|
}
|
|
137
|
+
i += 1;
|
|
117
138
|
}
|
|
118
139
|
None
|
|
119
140
|
}
|
|
@@ -178,6 +199,12 @@ fn classify_fortran(trimmed: &str) -> LineKind {
|
|
|
178
199
|
}
|
|
179
200
|
};
|
|
180
201
|
|
|
202
|
+
// Strip any trailing comment so checks like `lower == "contains"` and the
|
|
203
|
+
// "what follows the closing paren" tests are not confused by comment text
|
|
204
|
+
// (e.g., `contains ! public api`, `where (mask > 0) ! note`).
|
|
205
|
+
let stripped = strip_trailing_comment(line);
|
|
206
|
+
let line = stripped.as_str();
|
|
207
|
+
|
|
181
208
|
let lower = line.to_ascii_lowercase();
|
|
182
209
|
|
|
183
210
|
// --- Block closers (check first since "end" prefix is distinctive) ---
|
|
@@ -6,7 +6,6 @@ use std::fs;
|
|
|
6
6
|
use std::io::{self, Read, Write};
|
|
7
7
|
use std::path::{Path, PathBuf};
|
|
8
8
|
use std::process;
|
|
9
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
10
9
|
|
|
11
10
|
/// Default cache directory (relative to cwd).
|
|
12
11
|
const CACHE_DIR: &str = ".ffmt_cache";
|
|
@@ -201,7 +200,6 @@ pub fn run() {
|
|
|
201
200
|
};
|
|
202
201
|
|
|
203
202
|
let range = parse_range(&args.range);
|
|
204
|
-
let any_changed = AtomicBool::new(false);
|
|
205
203
|
|
|
206
204
|
let opts = ProcessOptions {
|
|
207
205
|
check: args.check,
|
|
@@ -212,25 +210,37 @@ pub fn run() {
|
|
|
212
210
|
verbose,
|
|
213
211
|
};
|
|
214
212
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
213
|
+
let outcomes: Vec<(&PathBuf, Outcome)> = files_to_process
|
|
214
|
+
.par_iter()
|
|
215
|
+
.map(|path| (*path, process_file(path, &opts, &config)))
|
|
216
|
+
.collect();
|
|
217
|
+
let any_changed = outcomes.iter().any(|(_, o)| *o == Outcome::Changed);
|
|
218
|
+
let any_failed = outcomes.iter().any(|(_, o)| *o == Outcome::Failed);
|
|
219
|
+
let failed: std::collections::HashSet<&PathBuf> = outcomes
|
|
220
|
+
.iter()
|
|
221
|
+
.filter(|(_, o)| *o == Outcome::Failed)
|
|
222
|
+
.map(|(p, _)| *p)
|
|
223
|
+
.collect();
|
|
224
|
+
|
|
225
|
+
// Update cache for successfully processed files (including ones we skipped
|
|
226
|
+
// via the cache — they're still valid). Files that failed must NOT be
|
|
227
|
+
// cached, or the failure would be silently skipped on the next run.
|
|
223
228
|
if let Some(ref mut cache) = cache {
|
|
224
229
|
// Only update cache when formatting (not in check/diff mode)
|
|
225
230
|
if !args.check && !args.diff {
|
|
226
231
|
for f in &files {
|
|
227
|
-
|
|
232
|
+
if !failed.contains(f) {
|
|
233
|
+
cache.update(f);
|
|
234
|
+
}
|
|
228
235
|
}
|
|
229
236
|
cache.save(cache_dir);
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
|
|
233
|
-
if
|
|
240
|
+
if any_failed {
|
|
241
|
+
process::exit(2);
|
|
242
|
+
}
|
|
243
|
+
if args.check && any_changed {
|
|
234
244
|
process::exit(1);
|
|
235
245
|
}
|
|
236
246
|
}
|
|
@@ -276,7 +286,10 @@ fn run_stdin(args: &Args, color: bool, quiet: bool, config: &crate::config::Conf
|
|
|
276
286
|
if args.diff {
|
|
277
287
|
print_diff(Path::new(filepath), &source, &formatted, color);
|
|
278
288
|
}
|
|
279
|
-
|
|
289
|
+
// --diff alone is informational; only --check signals failure
|
|
290
|
+
if args.check {
|
|
291
|
+
process::exit(1);
|
|
292
|
+
}
|
|
280
293
|
}
|
|
281
294
|
} else {
|
|
282
295
|
io::stdout()
|
|
@@ -332,7 +345,7 @@ fn discover_files_with_config(
|
|
|
332
345
|
|
|
333
346
|
for entry in builder.build().flatten() {
|
|
334
347
|
let entry_path = entry.path();
|
|
335
|
-
if entry_path.is_file() &&
|
|
348
|
+
if entry_path.is_file() && is_fortran_file_ext(entry_path, extensions) {
|
|
336
349
|
files.push(entry_path.to_path_buf());
|
|
337
350
|
}
|
|
338
351
|
}
|
|
@@ -343,10 +356,6 @@ fn discover_files_with_config(
|
|
|
343
356
|
files
|
|
344
357
|
}
|
|
345
358
|
|
|
346
|
-
fn is_fortran_file(path: &Path) -> bool {
|
|
347
|
-
is_fortran_file_ext(path, &crate::config::FilesConfig::default().extensions)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
359
|
fn is_fortran_file_ext(path: &Path, extensions: &[String]) -> bool {
|
|
351
360
|
path.extension()
|
|
352
361
|
.and_then(|e| e.to_str())
|
|
@@ -362,16 +371,24 @@ struct ProcessOptions {
|
|
|
362
371
|
verbose: bool,
|
|
363
372
|
}
|
|
364
373
|
|
|
365
|
-
|
|
374
|
+
/// Result of processing one file.
|
|
375
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
376
|
+
enum Outcome {
|
|
377
|
+
Unchanged,
|
|
378
|
+
Changed,
|
|
379
|
+
Failed,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Config) -> Outcome {
|
|
366
383
|
let source = match fs::read_to_string(path) {
|
|
367
384
|
Ok(s) => s,
|
|
368
385
|
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
|
|
369
386
|
// Binary or invalid UTF-8: not a text Fortran file, skip silently
|
|
370
|
-
return
|
|
387
|
+
return Outcome::Unchanged;
|
|
371
388
|
}
|
|
372
389
|
Err(e) => {
|
|
373
390
|
eprintln!("ffmt: cannot read {}: {e}", path.display());
|
|
374
|
-
return
|
|
391
|
+
return Outcome::Failed;
|
|
375
392
|
}
|
|
376
393
|
};
|
|
377
394
|
|
|
@@ -382,7 +399,7 @@ fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Conf
|
|
|
382
399
|
let formatted = crate::formatter::format_with_config(&source, config, opts.range);
|
|
383
400
|
|
|
384
401
|
if formatted == source {
|
|
385
|
-
return
|
|
402
|
+
return Outcome::Unchanged;
|
|
386
403
|
}
|
|
387
404
|
|
|
388
405
|
if opts.check || opts.diff {
|
|
@@ -392,14 +409,15 @@ fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Conf
|
|
|
392
409
|
if opts.diff {
|
|
393
410
|
print_diff(path, &source, &formatted, opts.color);
|
|
394
411
|
}
|
|
395
|
-
return
|
|
412
|
+
return Outcome::Changed;
|
|
396
413
|
}
|
|
397
414
|
|
|
398
415
|
if let Err(e) = fs::write(path, &formatted) {
|
|
399
416
|
eprintln!("ffmt: cannot write {}: {e}", path.display());
|
|
417
|
+
return Outcome::Failed;
|
|
400
418
|
}
|
|
401
419
|
|
|
402
|
-
|
|
420
|
+
Outcome::Changed
|
|
403
421
|
}
|
|
404
422
|
|
|
405
423
|
fn print_diff(path: &Path, original: &str, formatted: &str, color: bool) {
|
|
@@ -73,6 +73,9 @@ pub fn format_with_range(source: &str, range: Option<(usize, usize)>) -> String
|
|
|
73
73
|
|
|
74
74
|
/// Format with full config and optional range.
|
|
75
75
|
pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, usize)>) -> String {
|
|
76
|
+
// Remember the original line-ending style for EndOfLine::Preserve
|
|
77
|
+
// (the source is normalized to \n for parsing below).
|
|
78
|
+
let source_had_crlf = source.contains("\r\n");
|
|
76
79
|
// Normalize line endings before parsing (ensures reader sees consistent \n)
|
|
77
80
|
let normalized_source;
|
|
78
81
|
let src = if source.contains('\r') {
|
|
@@ -361,6 +364,12 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u
|
|
|
361
364
|
// normalizer can't parse correctly.
|
|
362
365
|
// For multi-line, use the joined line (reader normalizes spacing).
|
|
363
366
|
let source = if ll.raw_lines.len() > 1 {
|
|
367
|
+
emit_hoisted_comments(
|
|
368
|
+
&ll.hoisted_comments,
|
|
369
|
+
depth,
|
|
370
|
+
config,
|
|
371
|
+
&mut output_lines,
|
|
372
|
+
);
|
|
364
373
|
&ll.joined
|
|
365
374
|
} else {
|
|
366
375
|
trimmed
|
|
@@ -458,6 +467,17 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u
|
|
|
458
467
|
break;
|
|
459
468
|
}
|
|
460
469
|
|
|
470
|
+
// Comments found inside the continuation are hoisted
|
|
471
|
+
// above the statement: re-emitting from the joined
|
|
472
|
+
// text would lose them, and leaving them between
|
|
473
|
+
// continuation lines is rejected by some compilers.
|
|
474
|
+
emit_hoisted_comments(
|
|
475
|
+
&ll.hoisted_comments,
|
|
476
|
+
depth,
|
|
477
|
+
config,
|
|
478
|
+
&mut output_lines,
|
|
479
|
+
);
|
|
480
|
+
|
|
461
481
|
// Normal multi-line: unravel joined line, normalize, rewrap
|
|
462
482
|
let processed = process_line(&ll.joined, config);
|
|
463
483
|
let formatted = apply_indent(processed.trim(), depth, config.indent_width);
|
|
@@ -634,7 +654,14 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u
|
|
|
634
654
|
result = result.replace("\r\n", "\n").replace('\r', "\n");
|
|
635
655
|
result = result.replace('\n', "\r\n");
|
|
636
656
|
}
|
|
637
|
-
EndOfLine::Preserve => {
|
|
657
|
+
EndOfLine::Preserve => {
|
|
658
|
+
// The source was normalized to LF for parsing; restore the
|
|
659
|
+
// original ending style.
|
|
660
|
+
if source_had_crlf {
|
|
661
|
+
result = result.replace("\r\n", "\n").replace('\r', "\n");
|
|
662
|
+
result = result.replace('\n', "\r\n");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
638
665
|
}
|
|
639
666
|
|
|
640
667
|
result
|
|
@@ -642,6 +669,29 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u
|
|
|
642
669
|
|
|
643
670
|
/// Check if a line is a `! ffmt off` or `! ffmt on` marker.
|
|
644
671
|
/// Returns Some(true) for off, Some(false) for on, None otherwise.
|
|
672
|
+
/// Emit comments hoisted out of a `&` continuation as full-line comments
|
|
673
|
+
/// above the statement, at the statement's indent.
|
|
674
|
+
fn emit_hoisted_comments(
|
|
675
|
+
comments: &[String],
|
|
676
|
+
depth: usize,
|
|
677
|
+
config: &Config,
|
|
678
|
+
output_lines: &mut Vec<String>,
|
|
679
|
+
) {
|
|
680
|
+
for comment in comments {
|
|
681
|
+
let c = if config.unicode_to_ascii {
|
|
682
|
+
crate::unicode::replace_unicode(comment)
|
|
683
|
+
} else {
|
|
684
|
+
comment.clone()
|
|
685
|
+
};
|
|
686
|
+
let c = if config.space_after_comment.is_enabled() {
|
|
687
|
+
normalize_comment_space(&c)
|
|
688
|
+
} else {
|
|
689
|
+
c
|
|
690
|
+
};
|
|
691
|
+
output_lines.push(apply_indent(&c, depth, config.indent_width));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
645
695
|
fn is_ffmt_marker(line: &str) -> Option<bool> {
|
|
646
696
|
let t = line.trim().to_ascii_lowercase();
|
|
647
697
|
if t == "! ffmt off" || t == "! ffmt: off" {
|
|
@@ -1505,19 +1555,23 @@ fn is_declaration_line(line: &str) -> bool {
|
|
|
1505
1555
|
let bytes = trimmed.as_bytes();
|
|
1506
1556
|
let mut in_string = false;
|
|
1507
1557
|
let mut quote_char = b' ';
|
|
1508
|
-
|
|
1558
|
+
let mut i = 0;
|
|
1559
|
+
while i < bytes.len() {
|
|
1509
1560
|
if in_string {
|
|
1510
1561
|
if bytes[i] == quote_char {
|
|
1511
1562
|
if i + 1 < bytes.len() && bytes[i + 1] == quote_char {
|
|
1563
|
+
i += 2; // escaped quote — stay in string
|
|
1512
1564
|
continue;
|
|
1513
1565
|
}
|
|
1514
1566
|
in_string = false;
|
|
1515
1567
|
}
|
|
1568
|
+
i += 1;
|
|
1516
1569
|
continue;
|
|
1517
1570
|
}
|
|
1518
1571
|
if bytes[i] == b'\'' || bytes[i] == b'"' {
|
|
1519
1572
|
in_string = true;
|
|
1520
1573
|
quote_char = bytes[i];
|
|
1574
|
+
i += 1;
|
|
1521
1575
|
continue;
|
|
1522
1576
|
}
|
|
1523
1577
|
if bytes[i] == b'!' {
|
|
@@ -1526,6 +1580,7 @@ fn is_declaration_line(line: &str) -> bool {
|
|
|
1526
1580
|
if bytes[i] == b':' && i + 1 < bytes.len() && bytes[i + 1] == b':' {
|
|
1527
1581
|
return true;
|
|
1528
1582
|
}
|
|
1583
|
+
i += 1;
|
|
1529
1584
|
}
|
|
1530
1585
|
false
|
|
1531
1586
|
}
|
|
@@ -1552,12 +1607,14 @@ fn ensure_two_spaces_before_inline_comment(lines: &[String]) -> Vec<String> {
|
|
|
1552
1607
|
let bytes = line.as_bytes();
|
|
1553
1608
|
let mut in_string = false;
|
|
1554
1609
|
let mut delim = b' ';
|
|
1555
|
-
|
|
1610
|
+
let mut i = 0;
|
|
1611
|
+
while i < bytes.len() {
|
|
1556
1612
|
if in_string {
|
|
1557
1613
|
if bytes[i] == delim {
|
|
1558
1614
|
// Check for doubled quote escape
|
|
1559
1615
|
if i + 1 < bytes.len() && bytes[i + 1] == delim {
|
|
1560
|
-
|
|
1616
|
+
i += 2; // escaped quote — stay in string
|
|
1617
|
+
continue;
|
|
1561
1618
|
}
|
|
1562
1619
|
in_string = false;
|
|
1563
1620
|
}
|
|
@@ -1592,6 +1649,7 @@ fn ensure_two_spaces_before_inline_comment(lines: &[String]) -> Vec<String> {
|
|
|
1592
1649
|
let comment_part = &line[i..];
|
|
1593
1650
|
return format!("{} {}", code_part, comment_part);
|
|
1594
1651
|
}
|
|
1652
|
+
i += 1;
|
|
1595
1653
|
}
|
|
1596
1654
|
line.clone()
|
|
1597
1655
|
})
|
|
@@ -1620,10 +1678,12 @@ fn strip_trailing_semicolons(lines: &[String]) -> Vec<String> {
|
|
|
1620
1678
|
let mut in_string = false;
|
|
1621
1679
|
let mut delim = b' ';
|
|
1622
1680
|
let mut comment_start = bytes.len();
|
|
1623
|
-
|
|
1681
|
+
let mut i = 0;
|
|
1682
|
+
while i < bytes.len() {
|
|
1624
1683
|
if in_string {
|
|
1625
1684
|
if bytes[i] == delim {
|
|
1626
1685
|
if i + 1 < bytes.len() && bytes[i + 1] == delim {
|
|
1686
|
+
i += 2; // escaped quote — stay in string
|
|
1627
1687
|
continue;
|
|
1628
1688
|
}
|
|
1629
1689
|
in_string = false;
|
|
@@ -1635,6 +1695,7 @@ fn strip_trailing_semicolons(lines: &[String]) -> Vec<String> {
|
|
|
1635
1695
|
comment_start = i;
|
|
1636
1696
|
break;
|
|
1637
1697
|
}
|
|
1698
|
+
i += 1;
|
|
1638
1699
|
}
|
|
1639
1700
|
let code = &line[..comment_start];
|
|
1640
1701
|
let comment = &line[comment_start..];
|
|
@@ -2271,36 +2332,37 @@ fn normalize_fypp_lists(line: &str) -> String {
|
|
|
2271
2332
|
let bytes = line.as_bytes();
|
|
2272
2333
|
let len = bytes.len();
|
|
2273
2334
|
let mut i = 0;
|
|
2335
|
+
// Start of the pending region to copy verbatim (always on a char boundary,
|
|
2336
|
+
// since we only advance past ASCII delimiters).
|
|
2337
|
+
let mut plain_start = 0;
|
|
2274
2338
|
|
|
2275
2339
|
while i < len {
|
|
2276
2340
|
// Look for '[
|
|
2277
2341
|
if i + 1 < len && bytes[i] == b'\'' && bytes[i + 1] == b'[' {
|
|
2278
2342
|
// Find the matching ]'
|
|
2279
|
-
let
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
// Found ]' -- normalize commas in content
|
|
2285
|
-
let normalized_content = normalize_comma_spacing(&content);
|
|
2286
|
-
result.push_str("'[");
|
|
2287
|
-
result.push_str(&normalized_content);
|
|
2288
|
-
result.push_str("]'");
|
|
2289
|
-
i += 2; // skip ]'
|
|
2343
|
+
let mut j = i + 2;
|
|
2344
|
+
let mut close = None;
|
|
2345
|
+
while j + 1 < len {
|
|
2346
|
+
if bytes[j] == b']' && bytes[j + 1] == b'\'' {
|
|
2347
|
+
close = Some(j);
|
|
2290
2348
|
break;
|
|
2291
2349
|
}
|
|
2292
|
-
|
|
2293
|
-
i += 1;
|
|
2350
|
+
j += 1;
|
|
2294
2351
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
result.push_str(&line[
|
|
2352
|
+
if let Some(close) = close {
|
|
2353
|
+
// Found ]' -- normalize commas in content
|
|
2354
|
+
result.push_str(&line[plain_start..i]);
|
|
2355
|
+
result.push_str("'[");
|
|
2356
|
+
result.push_str(&normalize_comma_spacing(&line[i + 2..close]));
|
|
2357
|
+
result.push_str("]'");
|
|
2358
|
+
i = close + 2;
|
|
2359
|
+
plain_start = i;
|
|
2360
|
+
continue;
|
|
2298
2361
|
}
|
|
2299
|
-
} else {
|
|
2300
|
-
result.push(bytes[i] as char);
|
|
2301
|
-
i += 1;
|
|
2302
2362
|
}
|
|
2363
|
+
i += 1;
|
|
2303
2364
|
}
|
|
2365
|
+
result.push_str(&line[plain_start..]);
|
|
2304
2366
|
|
|
2305
2367
|
result
|
|
2306
2368
|
}
|
|
@@ -2339,19 +2401,19 @@ fn modernize_relational_operators(lines: &[String]) -> Vec<String> {
|
|
|
2339
2401
|
let mut i = 0;
|
|
2340
2402
|
let mut in_string = false;
|
|
2341
2403
|
let mut quote_char = b' ';
|
|
2404
|
+
// Start of the pending region to copy verbatim (always on a char
|
|
2405
|
+
// boundary, since we only cut at ASCII operator positions).
|
|
2406
|
+
let mut plain_start = 0;
|
|
2342
2407
|
|
|
2343
2408
|
while i < len {
|
|
2344
2409
|
if in_string {
|
|
2345
2410
|
if bytes[i] == quote_char {
|
|
2346
2411
|
if i + 1 < len && bytes[i + 1] == quote_char {
|
|
2347
|
-
|
|
2348
|
-
result.push(bytes[i + 1] as char);
|
|
2349
|
-
i += 2;
|
|
2412
|
+
i += 2; // escaped quote — stay in string
|
|
2350
2413
|
continue;
|
|
2351
2414
|
}
|
|
2352
2415
|
in_string = false;
|
|
2353
2416
|
}
|
|
2354
|
-
result.push(bytes[i] as char);
|
|
2355
2417
|
i += 1;
|
|
2356
2418
|
continue;
|
|
2357
2419
|
}
|
|
@@ -2359,43 +2421,39 @@ fn modernize_relational_operators(lines: &[String]) -> Vec<String> {
|
|
|
2359
2421
|
if bytes[i] == b'\'' || bytes[i] == b'"' {
|
|
2360
2422
|
in_string = true;
|
|
2361
2423
|
quote_char = bytes[i];
|
|
2362
|
-
result.push(bytes[i] as char);
|
|
2363
2424
|
i += 1;
|
|
2364
2425
|
continue;
|
|
2365
2426
|
}
|
|
2366
2427
|
|
|
2367
|
-
// Comment start —
|
|
2428
|
+
// Comment start — rest is copied verbatim below
|
|
2368
2429
|
if bytes[i] == b'!' {
|
|
2369
|
-
|
|
2370
|
-
return result;
|
|
2430
|
+
break;
|
|
2371
2431
|
}
|
|
2372
2432
|
|
|
2373
2433
|
// Check for legacy operator
|
|
2374
2434
|
if bytes[i] == b'.' {
|
|
2375
|
-
let mut matched =
|
|
2435
|
+
let mut matched = None;
|
|
2376
2436
|
for &(legacy, modern) in operators {
|
|
2377
2437
|
let legacy_len = legacy.len();
|
|
2378
|
-
if i + legacy_len <= len
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
if candidate == legacy {
|
|
2384
|
-
result.push_str(modern);
|
|
2385
|
-
i += legacy_len;
|
|
2386
|
-
matched = true;
|
|
2387
|
-
break;
|
|
2388
|
-
}
|
|
2438
|
+
if i + legacy_len <= len
|
|
2439
|
+
&& bytes[i..i + legacy_len].eq_ignore_ascii_case(legacy.as_bytes())
|
|
2440
|
+
{
|
|
2441
|
+
matched = Some((legacy_len, modern));
|
|
2442
|
+
break;
|
|
2389
2443
|
}
|
|
2390
2444
|
}
|
|
2391
|
-
if matched {
|
|
2445
|
+
if let Some((legacy_len, modern)) = matched {
|
|
2446
|
+
result.push_str(&line[plain_start..i]);
|
|
2447
|
+
result.push_str(modern);
|
|
2448
|
+
i += legacy_len;
|
|
2449
|
+
plain_start = i;
|
|
2392
2450
|
continue;
|
|
2393
2451
|
}
|
|
2394
2452
|
}
|
|
2395
2453
|
|
|
2396
|
-
result.push(bytes[i] as char);
|
|
2397
2454
|
i += 1;
|
|
2398
2455
|
}
|
|
2456
|
+
result.push_str(&line[plain_start..]);
|
|
2399
2457
|
|
|
2400
2458
|
result
|
|
2401
2459
|
})
|
|
@@ -2535,19 +2593,23 @@ fn split_multi_statements(lines: &[String]) -> Vec<String> {
|
|
|
2535
2593
|
let mut quote_char = b' ';
|
|
2536
2594
|
let mut semicolons: Vec<usize> = Vec::new();
|
|
2537
2595
|
|
|
2538
|
-
|
|
2596
|
+
let mut i = 0;
|
|
2597
|
+
while i < len {
|
|
2539
2598
|
if in_string {
|
|
2540
2599
|
if bytes[i] == quote_char {
|
|
2541
2600
|
if i + 1 < len && bytes[i + 1] == quote_char {
|
|
2601
|
+
i += 2; // escaped quote — stay in string
|
|
2542
2602
|
continue;
|
|
2543
2603
|
}
|
|
2544
2604
|
in_string = false;
|
|
2545
2605
|
}
|
|
2606
|
+
i += 1;
|
|
2546
2607
|
continue;
|
|
2547
2608
|
}
|
|
2548
2609
|
if bytes[i] == b'\'' || bytes[i] == b'"' {
|
|
2549
2610
|
in_string = true;
|
|
2550
2611
|
quote_char = bytes[i];
|
|
2612
|
+
i += 1;
|
|
2551
2613
|
continue;
|
|
2552
2614
|
}
|
|
2553
2615
|
if bytes[i] == b'!' {
|
|
@@ -2556,6 +2618,7 @@ fn split_multi_statements(lines: &[String]) -> Vec<String> {
|
|
|
2556
2618
|
if bytes[i] == b';' {
|
|
2557
2619
|
semicolons.push(i);
|
|
2558
2620
|
}
|
|
2621
|
+
i += 1;
|
|
2559
2622
|
}
|
|
2560
2623
|
|
|
2561
2624
|
if semicolons.is_empty() {
|
|
@@ -2868,21 +2931,25 @@ fn normalize_comma_spacing(s: &str) -> String {
|
|
|
2868
2931
|
let bytes = s.as_bytes();
|
|
2869
2932
|
let len = bytes.len();
|
|
2870
2933
|
let mut i = 0;
|
|
2934
|
+
// Start of the pending region to copy verbatim (always on a char boundary,
|
|
2935
|
+
// since we only cut at ASCII commas/spaces).
|
|
2936
|
+
let mut plain_start = 0;
|
|
2871
2937
|
|
|
2872
2938
|
while i < len {
|
|
2873
2939
|
if bytes[i] == b',' {
|
|
2874
|
-
result.
|
|
2875
|
-
result.
|
|
2940
|
+
result.push_str(&s[plain_start..i]);
|
|
2941
|
+
result.push_str(", ");
|
|
2876
2942
|
// Skip any whitespace after the comma
|
|
2877
2943
|
i += 1;
|
|
2878
2944
|
while i < len && bytes[i] == b' ' {
|
|
2879
2945
|
i += 1;
|
|
2880
2946
|
}
|
|
2947
|
+
plain_start = i;
|
|
2881
2948
|
} else {
|
|
2882
|
-
result.push(bytes[i] as char);
|
|
2883
2949
|
i += 1;
|
|
2884
2950
|
}
|
|
2885
2951
|
}
|
|
2952
|
+
result.push_str(&s[plain_start..]);
|
|
2886
2953
|
|
|
2887
2954
|
result
|
|
2888
2955
|
}
|