Repository: prettier/prettier (JavaScript)
Date: 2023-09-27 16:24:28+00:00
Task type: Feature

Issue:
`--write` to show text identifier for unchanged files ('grey color only' an accessibility concern 🧑‍🦯)
The output of `--write` can't distinguish between changed and unchanged text if color is not visible.

### Suggestion: Distinguish using text.

Consider that the use of color to distinguish changed vs unchanged files is not the best way to convey this difference. It has these problems:
* Visually impaired users may use a two-tone terminal that doesn't output any color information. It's impossible to distinguish in such case.
* Copying the log and pasting it somewhere else that only support plain text and no formatting, will cause the user to lose the color information. It's impossible to distinguish in such case.
* Scripts that grep the output cannot distinguish the files unless they programmatically inspect the color information. The `(cached)` keyword is unreliable as it only appears when `isCacheExists` is true.
* Cloud services that only log the text output and not the color information will not be able to display to the service user which files changed and which remain unchanged.

I think while the grey color is useful to visually distinguish, to solve the above cases we should also add the keyword `(unchanged)` after the file name. See example below.

### Example demonstrating suggestion

Take two exact copies of a project with three files, only the second of which will change in formatting.

With the current version of Prettier, running `npx/yarn prettier --write .` on first copy:

```
path/to/file1.ext 1ms
path/to/file2.ext 5ms
path/to/file3.ext 0ms (cached)
```
_(can't show color but you can imagine it: file1 and file3 will be grey)_

With a fixed version of Prettier, running `npx/yarn prettier --write .` on second copy:

```
path/to/file1.ext 1ms (unchanged)
path/to/file2.ext 5ms
path/to/file3.ext 0ms (unchanged) (cached)
```
_(can't show color but you can imagine it: file1 and file3 will be grey)_

Note: Although `(cached)` only appears for cached unchanged files, I would suggest not combining the keywords to `(unchanged, cached)` because it's harder to grep for both `(unchanged)` and `(unchanged,` in the output. Also any scripts that grep for `/ \(cached\)$/` would start to fail. That's why I suggest `(unchanged) (cached)`.

### Relevant information

Relevant code:

https://github.com/prettier/prettier/blob/f7959cccbfaf38f685b660f39eed061f5eec5ec2/src/cli/format.js#L432-L441

Relevant issues:

This was kind of hinted before in #3525 but apparently misunderstood. I believe the original author there possibly couldn't see the grey vs white distinction because the terminal didn't support colors, and everything was in the same color. This may have led to the **false** impression that all files are being overwritten when in reality, all filenames were being output to log, but only changed files were overwritten. If the output included the keyword `(unchanged)` this could possibly be avoided.

Suggestion to reduce verbosity was moved to #15480.

### I can open a pull request

I believe both suggestions can be achieved with a simple change:
* On line 435, change ``}ms`;`` to ``}ms (unchanged)`;``

Just let me know if the suggestions are acceptable.
`--write` to show text identifier for unchanged files ('grey color only' an accessibility concern 🧑‍🦯)
The output of `--write` can't distinguish between changed and unchanged text if color is not visible.

### Suggestion: Distinguish using text.

Consider that the use of color to distinguish changed vs unchanged files is not the best way to convey this difference. It has these problems:
* Visually impaired users may use a two-tone terminal that doesn't output any color information. It's impossible to distinguish in such case.
* Copying the log and pasting it somewhere else that only support plain text and no formatting, will cause the user to lose the color information. It's impossible to distinguish in such case.
* Scripts that grep the output cannot distinguish the files unless they programmatically inspect the color information. The `(cached)` keyword is unreliable as it only appears when `isCacheExists` is true.
* Cloud services that only log the text output and not the color information will not be able to display to the service user which files changed and which remain unchanged.

I think while the grey color is useful to visually distinguish, to solve the above cases we should also add the keyword `(unchanged)` after the file name. See example below.

### Example demonstrating suggestion

Take two exact copies of a project with three files, only the second of which will change in formatting.

With the current version of Prettier, running `npx/yarn prettier --write .` on first copy:

```
path/to/file1.ext 1ms
path/to/file2.ext 5ms
path/to/file3.ext 0ms (cached)
```
_(can't show color but you can imagine it: file1 and file3 will be grey)_

With a fixed version of Prettier, running `npx/yarn prettier --write .` on second copy:

```
path/to/file1.ext 1ms (unchanged)
path/to/file2.ext 5ms
path/to/file3.ext 0ms (unchanged) (cached)
```
_(can't show color but you can imagine it: file1 and file3 will be grey)_

Note: Although `(cached)` only appears for cached unchanged files, I would suggest not combining the keywords to `(unchanged, cached)` because it's harder to grep for both `(unchanged)` and `(unchanged,` in the output. Also any scripts that grep for `/ \(cached\)$/` would start to fail. That's why I suggest `(unchanged) (cached)`.

### Relevant information

Relevant code:

https://github.com/prettier/prettier/blob/f7959cccbfaf38f685b660f39eed061f5eec5ec2/src/cli/format.js#L432-L441

Relevant issues:

This was kind of hinted before in #3525 but apparently misunderstood. I believe the original author there possibly couldn't see the grey vs white distinction because the terminal didn't support colors, and everything was in the same color. This may have led to the **false** impression that all files are being overwritten when in reality, all filenames were being output to log, but only changed files were overwritten. If the output included the keyword `(unchanged)` this could possibly be avoided.

Suggestion to reduce verbosity was moved to #15480.

### I can open a pull request

I believe both suggestions can be achieved with a simple change:
* On line 435, change ``}ms`;`` to ``}ms (unchanged)`;``

Just let me know if the suggestions are acceptable.


Issue discussion:
I agree with giving `(unchanged)` to output for accessibility.
I agree with giving `(unchanged)` to output for accessibility.

Solution patch:
diff --git a/changelog_unreleased/cli/15467.md b/changelog_unreleased/cli/15467.md
new file mode 100644
index 000000000000..b69c49842e3f
--- /dev/null
+++ b/changelog_unreleased/cli/15467.md
@@ -0,0 +1,19 @@
+#### Show `(unchanged)` keyword for accessibility in CLI `--write` (#15467 by @ADTC)
+
+Previously, the only distinction between a changed file and an unchanged file was the grey color of the file name. In the example below, we can't distinguish between `a.js` and `b.js` as the color is missing. This issue is fixed by adding the `(unchanged)` keyword which makes the distinction accessible without color.
+
+<!-- prettier-ignore -->
+```sh
+# Input
+prettier --write .
+
+# Prettier stable
+a.js 0ms
+b.js 0ms
+c.js 0ms (cached)
+
+# Prettier main
+a.js 0ms
+b.js 0ms (unchanged)
+c.js 0ms (unchanged) (cached)
+```
diff --git a/src/cli/format.js b/src/cli/format.js
index 0192b83266dc..9b4d6a2d2725 100644
--- a/src/cli/format.js
+++ b/src/cli/format.js
@@ -437,7 +437,7 @@ async function formatFiles(context) {
       } else if (!context.argv.check && !context.argv.listDifferent) {
         const message = `${chalk.grey(fileNameToDisplay)} ${
           Date.now() - start
-        }ms`;
+        }ms (unchanged)`;
         if (isCacheExists) {
           context.logger.log(`${message} (cached)`);
         } else {


Test patch:
diff --git a/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap b/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap
index f571b2fe4f19..54cd7779dd82 100644
--- a/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap
+++ b/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap
@@ -83,7 +83,7 @@ invalid-2.unknown
 [[called readline.clearLine(process.stdout)]]
 valid-1.js
 [[called readline.clearLine(process.stdout)]]
-valid-1.js 0ms"
+valid-1.js 0ms (unchanged)"
 `;
 
 exports[`Line breaking after filepath with errors (write) 1`] = `[]`;
diff --git a/tests/integration/__tests__/__snapshots__/write.js.snap b/tests/integration/__tests__/__snapshots__/write.js.snap
index 7a5d442919ba..15a6f0caa989 100644
--- a/tests/integration/__tests__/__snapshots__/write.js.snap
+++ b/tests/integration/__tests__/__snapshots__/write.js.snap
@@ -2,7 +2,7 @@
 
 exports[`do not write file with --write + formatted file (stderr) 1`] = `""`;
 
-exports[`do not write file with --write + formatted file (stdout) 1`] = `"formatted.js 0ms"`;
+exports[`do not write file with --write + formatted file (stdout) 1`] = `"formatted.js 0ms (unchanged)"`;
 
 exports[`do not write file with --write + invalid file (stderr) 1`] = `
 "[error] invalid.js: SyntaxError: Unexpected token (1:17)
diff --git a/tests/integration/__tests__/cache.js b/tests/integration/__tests__/cache.js
index f02df5439eb1..34363b096d89 100644
--- a/tests/integration/__tests__/cache.js
+++ b/tests/integration/__tests__/cache.js
@@ -133,7 +133,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -146,8 +146,8 @@ describe("--cache option", () => {
       ]);
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
-          expect.stringMatching(/^a\.js .+ms \(cached\)$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -167,7 +167,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -182,7 +182,7 @@ describe("--cache option", () => {
         // the cache of `b.js` is only available.
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -202,7 +202,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -218,7 +218,7 @@ describe("--cache option", () => {
         // the cache of `b.js` is only available.
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -234,7 +234,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -250,7 +250,7 @@ describe("--cache option", () => {
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
     });
@@ -273,7 +273,7 @@ describe("--cache option", () => {
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -309,7 +309,7 @@ describe("--cache option", () => {
       expect(thirdStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -336,7 +336,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -394,7 +394,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -404,8 +404,8 @@ describe("--cache option", () => {
       );
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
-          expect.stringMatching(/^a\.js .+ms \(cached\)$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -425,7 +425,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -440,7 +440,7 @@ describe("--cache option", () => {
         // the cache of `b.js` is only available.
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -460,7 +460,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -474,8 +474,8 @@ describe("--cache option", () => {
       );
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
-          expect.stringMatching(/^a\.js .+ms \(cached\)$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -491,7 +491,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -507,7 +507,7 @@ describe("--cache option", () => {
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
     });
@@ -530,7 +530,7 @@ describe("--cache option", () => {
       expect(secondStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -566,7 +566,7 @@ describe("--cache option", () => {
       expect(thirdStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
         ]),
       );
     });
@@ -593,7 +593,7 @@ describe("--cache option", () => {
       expect(firstStdout.split("\n")).toEqual(
         expect.arrayContaining([
           expect.stringMatching(/^a\.js .+ms$/),
-          expect.stringMatching(/^b\.js .+ms$/),
+          expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
         ]),
       );
 
@@ -677,7 +677,7 @@ describe("--cache option", () => {
         expect(firstStdout.split("\n")).toEqual(
           expect.arrayContaining([
             expect.stringMatching(/^a\.js .+ms$/),
-            expect.stringMatching(/^b\.js .+ms$/),
+            expect.stringMatching(/^b\.js .+ms \(unchanged\)$/),
           ]),
         );
 
@@ -687,8 +687,8 @@ describe("--cache option", () => {
         );
         expect(secondStdout.split("\n")).toEqual(
           expect.arrayContaining([
-            expect.stringMatching(/^a\.js .+ms \(cached\)$/),
-            expect.stringMatching(/^b\.js .+ms \(cached\)$/),
+            expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/),
+            expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/),
           ]),
         );
       });