I usually have three or four VS Code windows open at once. The API, the frontend, some side project, and whatever repo I opened "just to check something" two hours ago. They all look the same, so I'd regularly type into the wrong one.

The fix everybody knows is to give each workspace its own color. Blue title bar for the API, orange for the frontend, and so on. There are extensions for that and they work fine.

My problem was with what they did to .vscode/settings.json.

The diff that annoyed me

In a couple of my projects .vscode/settings.json is committed. I keep it organized by hand: related keys together, a comment here and there explaining why something is set. Every time I changed the color, the diff looked something like this:

 "workbench.colorCustomizations": { - // My own tweaks, grouped on purpose  "editor.lineHighlightBackground": "#ffffff0d", "editorCursor.foreground": "#ffcc00", - // Workspace tint - "statusBar.background": "#2f6fd6", + "terminal.background": "#1b1b1f", + "statusBar.background": "#2e8b57",  "statusBar.foreground": "#e7e7e7", - "statusBarItem.hoverBackground": "#447dda", - "terminal.background": "#1b1b1f" + "statusBarItem.hoverBackground": "#439768"  } 

Two values changed. The diff touches eight lines, both comments are gone, and terminal.background, which I never touched, jumped to the middle of the block.

It's not a bug in those extensions, really. It's how the VS Code API works. When you call

workspace.getConfiguration().update('workbench.colorCustomizations', newObject) 

you're handing VS Code a brand new object, and it writes that object in place of the old one. Whatever order the keys have in newObject is the order you get in the file, and comments inside it are simply dropped because a JS object has nowhere to keep them.

So I wrote my own extension, Tidy Tint. It does the same job (color the activity bar, title bar and status bar per workspace) but it never calls update() for the colors. It edits the file as text.

Editing JSON as text without going crazy

Doing string surgery on JSON by hand sounds like a terrible idea, and it is. Luckily VS Code itself solves this problem with jsonc-parser, the same library it uses to read your settings. It understands comments and trailing commas, and it has a modify() function that, instead of returning a new document, returns a list of edits: "at offset X, replace N characters with this string".

That's exactly what I wanted. The rules I ended up with are:

  • if a key already exists, only its value changes, in place;
  • if it's new, it goes at the end of its object;
  • removing colors deletes only the keys Tidy Tint wrote, with their comma and their line;
  • nothing else in the file is touched.

The first version was basically applyEdits(text, modify(text, path, value, { formattingOptions })) in a loop, and it almost worked. The catch is that when you pass formatting options, jsonc-parser sometimes reformats the lines around the edit too. Not much, but enough to show up in the diff, which was the whole thing I was trying to avoid.

The workaround was to ask for the edits without formatting and then format only the text I just inserted:

function applyMinimal(text: string, edits: Edit[], options: FormattingOptions): string { for (const e of [...edits].sort((a, b) => b.offset - a.offset)) { text = text.slice(0, e.offset) + e.content + text.slice(e.offset + e.length); if (e.content.length > 0) { text = applyEdits(text, format(text, { offset: e.offset, length: e.content.length }, options)); } } return text; } 

The edits are applied from the end of the file backwards so earlier offsets stay valid. The formatting options aren't hardcoded either: the extension looks at the file first and copies whatever it finds (tabs or spaces, indent width, \n or \r\n). If your file uses tabs and CRLF, so do the new lines.

Deleting a key is the annoying part

Adding and changing values was the easy half. Removing them cleanly took more fiddling than I expected, because of commas.

If the key is in the middle of the object, you delete from the start of its line to the start of the next key. If it's the last one, you have to eat the comma of the previous key instead, otherwise you're left with a trailing comma (valid in JSONC, but ugly and not what was there before). And if it's the only key, the object just becomes {}.

if (siblings.length === 1) { // Only property: the object becomes "{}". start = obj.offset + 1; end = obj.offset + obj.length - 1; } else if (i < siblings.length - 1) { // Another one follows: delete from this property to the start of the next. start = lineStartIfOnlyIndent(text, prop.offset); end = lineStartIfOnlyIndent(text, siblings[i + 1].offset); } else { // It's the last one: delete from the end of the previous one (including its comma). const prev = siblings[i - 1]; start = prev.offset + prev.length; end = prop.offset + prop.length; } 

When you run "Remove Colors" and workbench.colorCustomizations ends up empty, the extension removes that key too, so you don't keep a lonely {} hanging around. If you had your own keys in there (like my cursor color or the terminal background) they stay exactly where they were.

Writing the file without breaking undo

The last detail: once I have the new text, I don't write it to disk with fs. I apply it with a WorkspaceEdit and then save the document. Two reasons. If you have settings.json open with unsaved changes, writing behind VS Code's back is asking for trouble. And with a WorkspaceEdit the change goes into the editor's history, so Ctrl+Z (Cmd+Z on Mac) undoes a color change like any other edit.

The colors

This part is less interesting but I'll mention it. You pick one base color and the extension derives the rest: foreground is light or dark depending on the base's luminance (the WCAG formula), inactive title bar is the same color with some transparency, the badge on the activity bar uses the hue rotated 150° so it stands out, and the hover color on the status bar is slightly lighter or darker. There are 13 keys in total, and those 13 are the only ones Tidy Tint will ever write or delete.

The random color isn't fully random either. It picks a random hue but keeps saturation and lightness in a range that doesn't give you neon green or a gray that looks like nothing changed.

Tests

Since the whole point of the extension is "don't mess up my file", the JSON editing code is tested with Node's built-in test runner, no extra framework. The tests are boring in a good way: take a settings file with comments and keys in a weird order, change the color twice, and check that the order of keys is identical and the comments are still there. Remove colors and check that only Tidy Tint's keys disappeared.

Using it

Install Tidy Tint from the Marketplace and open the command palette:

  • Tidy Tint: Change Color lets you pick from a list of favorites, type a hex value, or go random
  • Tidy Tint: Random Color if you don't care
  • Tidy Tint: Remove Colors to undo everything

You can turn off each bar separately (tidyTint.affectActivityBar, tidyTint.affectTitleBar, tidyTint.affectStatusBar) and change the favorites list in tidyTint.favoriteColors.

In a multi-root workspace the colors go in the .code-workspace file, so you need to save the workspace first.

One honest limitation: Tidy Tint only controls what it writes. If another extension or the Settings UI later rewrites workbench.colorCustomizations as a whole object, the order will change again. I can't do anything about that.

That's it

It's a small extension, around 350 lines of TypeScript, and it solves a problem that maybe only bothers people who read their own diffs too carefully. If you're one of them, give it a try.

The code is MIT and lives on GitHub: cbsumastre/tidy-tint. Issues and PRs are welcome, and if you find a settings file that it mangles, I'd really like to see it.