diff --git a/README.md b/README.md
index eaf105c..78ba9ae 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,10 @@ alt="Action Buttons">
+### Color your folders
+
+> Give a folder a color from the **Edit Folder** screen to make it stand out in the tree. By default the color cascades to the snippets and subfolders nested inside it. Set `snippets.disableFolderColorCascade` to limit the color to the folder and the items directly inside it instead.
+
### Add a description to your Snippet
> Descriptions show when hovering on top of a Snippet and in IntelliSense.
diff --git a/package.json b/package.json
index 334d710..881e274 100644
--- a/package.json
+++ b/package.json
@@ -839,6 +839,11 @@
"type": "boolean",
"default": false,
"markdownDescription": "Open folders collapsed. Expanding a folder then reveals only its immediate children, one level at a time, instead of expanding the whole subtree. This change requires a window restart."
+ },
+ "snippets.disableFolderColorCascade": {
+ "type": "boolean",
+ "default": false,
+ "markdownDescription": "By default, a folder's color cascades to all snippets and subfolders nested inside it. Enable this to limit the color to the folder and the items directly inside it (deeper descendants keep their default color)."
}
}
}
diff --git a/src/extension.ts b/src/extension.ts
index 78a483d..4d00f4e 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -13,6 +13,7 @@ import { StringUtility } from './utility/stringUtility';
import { Labels } from './config/labels';
import { FileDataAccess } from './data/fileDataAccess';
import { LoggingUtility } from './utility/loggingUtility';
+import { DecorationProvider } from './provider/decorationProvider';
/**
* Activate extension by initializing views for snippets and feature commands.
@@ -31,6 +32,7 @@ export function activate(context: vscode.ExtensionContext) {
const useWorkspaceFolderKey = "useWorkspaceFolder";
const openButtonKey = "openButton";
const collapseFoldersKey = "collapseFolders";
+ const disableFolderColorCascadeKey = "disableFolderColorCascade";
const workspaceFileName = ".vscode/snippets.json";
let workspaceSnippetsAvailable = false;
let wsSnippetService: SnippetService;
@@ -63,6 +65,9 @@ export function activate(context: vscode.ExtensionContext) {
// initialize global snippets
const dataAccess = new MementoDataAccess(context.globalState);
const snippetService = new SnippetService(dataAccess);
+ // tints snippet/folder labels (icons are tinted via ThemeIcon in the provider)
+ const decorationProvider = new DecorationProvider();
+ context.subscriptions.push(decorationProvider);
const snippetsProvider = new SnippetsProvider(snippetService, allLanguages);
let cipDisposable: { dispose(): any } = {
dispose: function () {
@@ -186,6 +191,9 @@ export function activate(context: vscode.ExtensionContext) {
if (event.affectsConfiguration(`${snippetsConfigKey}.${collapseFoldersKey}`)) {
refreshUI();
}
+ if (event.affectsConfiguration(`${snippetsConfigKey}.${disableFolderColorCascadeKey}`)) {
+ refreshUI();
+ }
});
let snippetsExplorer = vscode.window.createTreeView('snippetsExplorer', {
@@ -208,6 +216,8 @@ export function activate(context: vscode.ExtensionContext) {
LoggingUtility.getInstance().debug('Refreshing UI');
// dispose CIP snippets
cipDisposable?.dispose();
+ // invalidate cached label decorations so colors (incl. cascade) re-resolve
+ decorationProvider.refresh();
snippetsProvider.refresh();
// re-check if .vscode/snippets.json is always available (use case when deleting file after enabling workspace in settings)
requestWSConfigSetup(false);
diff --git a/src/interface/snippet.ts b/src/interface/snippet.ts
index 026aba8..ef11dd4 100644
--- a/src/interface/snippet.ts
+++ b/src/interface/snippet.ts
@@ -13,6 +13,7 @@ export class Snippet {
prefix?: string;
language?: string;
icon?: string;
+ color?: string;
completionDescription?: string;
constructor(
diff --git a/src/provider/decorationProvider.ts b/src/provider/decorationProvider.ts
new file mode 100644
index 0000000..21a9b17
--- /dev/null
+++ b/src/provider/decorationProvider.ts
@@ -0,0 +1,47 @@
+import * as vscode from 'vscode';
+
+/**
+ * Tints snippet/folder labels in the tree view.
+ *
+ * VS Code's only hook for coloring a tree item's label is `FileDecorationProvider`.
+ * The chosen color rides in the item's `resourceUri` query (`...?color=`),
+ * so this provider stays stateless: it simply reads the color back from the uri.
+ *
+ * The icon itself is tinted separately via the item's `ThemeIcon` color (see
+ * SnippetsProvider) so that the original icon glyph is always preserved - a resourceUri
+ * alone would override icon resolution and blank out folder icons.
+ *
+ * VS Code caches decorations per resourceUri, so when colors change (e.g. toggling the
+ * cascade setting) `refresh()` must be called to invalidate the cache and re-query.
+ */
+export class DecorationProvider implements vscode.FileDecorationProvider {
+ private _disposables: vscode.Disposable[] = [];
+
+ private _onDidChangeFileDecorations = new vscode.EventEmitter();
+ readonly onDidChangeFileDecorations: vscode.Event = this._onDidChangeFileDecorations.event;
+
+ constructor() {
+ this._disposables.push(vscode.window.registerFileDecorationProvider(this));
+ }
+
+ // invalidate all decorations so VS Code re-queries them (colors may have changed)
+ refresh(): void {
+ this._onDidChangeFileDecorations.fire(undefined);
+ }
+
+ provideFileDecoration(uri: vscode.Uri, _token: vscode.CancellationToken): vscode.ProviderResult {
+ const color = new URLSearchParams(uri.query).get('color');
+ if (!color) {
+ return;
+ }
+ return {
+ color: new vscode.ThemeColor(color),
+ propagate: false
+ };
+ }
+
+ dispose() {
+ this._onDidChangeFileDecorations.dispose();
+ this._disposables.forEach(dispose => dispose.dispose());
+ }
+}
diff --git a/src/provider/snippetsProvider.ts b/src/provider/snippetsProvider.ts
index 4a0b207..6c5fdb8 100644
--- a/src/provider/snippetsProvider.ts
+++ b/src/provider/snippetsProvider.ts
@@ -178,6 +178,32 @@ export class SnippetsProvider implements vscode.TreeDataProvider, vscod
this.sync();
}
+ // resolve the color an item should display: its own color, otherwise the nearest
+ // colored ancestor folder. When the cascade is disabled the search stops at the
+ // direct parent, so a folder's color still reaches the items immediately inside it
+ // but not deeper descendants.
+ private getEffectiveColor(snippet: Snippet): string | undefined {
+ if (snippet.color) {
+ return snippet.color;
+ }
+ const cascadeDisabled = vscode.workspace.getConfiguration('snippets').get('disableFolderColorCascade');
+ let current = this._snippetService.getParent(snippet.parentId);
+ while (current) {
+ if (current.color) {
+ return current.color;
+ }
+ // direct parent had no color; with the cascade off we don't look further up
+ if (cascadeDisabled) {
+ return undefined;
+ }
+ if (current.parentId === undefined || current.parentId < 0) {
+ break;
+ }
+ current = this._snippetService.getParent(current.parentId);
+ }
+ return undefined;
+ }
+
private snippetToTreeItem(snippet: Snippet): vscode.TreeItem {
// when 'collapseFolders' is enabled, folders open collapsed so that
// expanding one only reveals its immediate children (one level at a time)
@@ -190,23 +216,45 @@ export class SnippetsProvider implements vscode.TreeDataProvider, vscod
? folderCollapsibleState
: vscode.TreeItemCollapsibleState.None
);
+ // effective color is the folder's own color, or the nearest colored ancestor's
+ // (so a folder's color cascades down to everything inside it). When set, the
+ // color tints both the icon and the label:
+ // - icon: a directly-rendered codicon (file-directory == folder glyph, symbol-file
+ // == file glyph) tinted via ThemeIcon. The 'file'/'folder' ThemeIcon ids can't
+ // be used here because VS Code derives them from the file-icon theme using the
+ // resourceUri, which blanks the icon for our synthetic uris.
+ // - label: the color rides in the resourceUri query read by DecorationProvider.
+ // Uncolored items keep the themed ThemeIcon.Folder/File exactly as before.
+ const color = this.getEffectiveColor(snippet);
+ const themeColor = color ? new vscode.ThemeColor(color) : undefined;
+ const colorUri = color
+ ? vscode.Uri.from({ scheme: 'snippets', path: `/${snippet.id}`, query: `color=${color}` })
+ : undefined;
// dynamic context value depending on item type (snippet or snippet folder)
// context value is used in view/item/context in 'when' condition
if (snippet.folder && snippet.folder === true) {
treeItem.contextValue = 'snippetFolder';
- if (snippet.icon) {
- treeItem.iconPath = new vscode.ThemeIcon(snippet.icon);
+ if (color) {
+ treeItem.iconPath = new vscode.ThemeIcon(snippet.icon || 'file-directory', themeColor);
+ treeItem.resourceUri = colorUri;
} else {
- treeItem.iconPath = vscode.ThemeIcon.Folder;
+ treeItem.iconPath = snippet.icon ? new vscode.ThemeIcon(snippet.icon) : vscode.ThemeIcon.Folder;
}
} else {
treeItem.tooltip = snippet.description ? `(${snippet.description})\n${snippet.value}` : `${snippet.value}`;
treeItem.contextValue = 'snippet';
- treeItem.iconPath = vscode.ThemeIcon.File;
- treeItem.description = snippet.prefix;
if (snippet.language) {
- treeItem.resourceUri = vscode.Uri.parse(`_${snippet.language}`);
+ // keep the themed language icon (resolved from the resourceUri path); a
+ // themed icon can't be tinted, so only the label is colored via the query
+ treeItem.iconPath = vscode.ThemeIcon.File;
+ treeItem.resourceUri = vscode.Uri.parse(`_${snippet.language}${color ? `?color=${color}` : ''}`);
+ } else if (color) {
+ treeItem.iconPath = new vscode.ThemeIcon('symbol-file', themeColor);
+ treeItem.resourceUri = colorUri;
+ } else {
+ treeItem.iconPath = vscode.ThemeIcon.File;
}
+ treeItem.description = snippet.prefix;
// conditional in configuration
treeItem.command = {
command: CommandsConsts.commonOpenSnippet,
diff --git a/src/views/editSnippetFolder.ts b/src/views/editSnippetFolder.ts
index 9dd8310..7ac5e7a 100644
--- a/src/views/editSnippetFolder.ts
+++ b/src/views/editSnippetFolder.ts
@@ -21,11 +21,13 @@ export class EditSnippetFolder extends EditView {
case 'edit-folder':
const label = message.data.label;
const icon = message.data.icon;
+ const color = message.data.color;
// call provider only if there is data change
if (label) {
this._snippet.label = label;
}
this._snippet.icon = icon;
+ this._snippet.color = color || undefined;
this._snippetsProvider.editSnippetFolder(this._snippet);
this._panel.dispose();
return;
diff --git a/views/css/vscode-custom.css b/views/css/vscode-custom.css
index 6750055..d929e3a 100644
--- a/views/css/vscode-custom.css
+++ b/views/css/vscode-custom.css
@@ -76,6 +76,7 @@ button.secondary:hover {
}
input:not([type="checkbox"]),
+select,
textarea {
display: block;
width: 100%;
@@ -98,6 +99,10 @@ textarea::placeholder {
margin: 0;
}
+.upper-p {
+ padding-top: 10px;
+}
+
div {
margin: 15px auto;
}
diff --git a/views/editSnippet.html b/views/editSnippet.html
index acd492b..33f40c4 100644
--- a/views/editSnippet.html
+++ b/views/editSnippet.html
@@ -15,7 +15,7 @@