Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ alt="Action Buttons">
<img src="https://raw.githubusercontent.com/tahabasri/snippets/main/images/features/051-folder-icons.gif"
alt="Set Folder Icon">

### 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.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)."
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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', {
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/interface/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class Snippet {
prefix?: string;
language?: string;
icon?: string;
color?: string;
completionDescription?: string;

constructor(
Expand Down
47 changes: 47 additions & 0 deletions src/provider/decorationProvider.ts
Original file line number Diff line number Diff line change
@@ -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=<themeColorId>`),
* 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<vscode.Uri | vscode.Uri[] | undefined>();
readonly onDidChangeFileDecorations: vscode.Event<vscode.Uri | vscode.Uri[] | undefined> = 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<vscode.FileDecoration> {
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());
}
}
60 changes: 54 additions & 6 deletions src/provider/snippetsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,32 @@ export class SnippetsProvider implements vscode.TreeDataProvider<Snippet>, 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)
Expand All @@ -190,23 +216,45 @@ export class SnippetsProvider implements vscode.TreeDataProvider<Snippet>, 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,
Expand Down
2 changes: 2 additions & 0 deletions src/views/editSnippetFolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions views/css/vscode-custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ button.secondary:hover {
}

input:not([type="checkbox"]),
select,
textarea {
display: block;
width: 100%;
Expand All @@ -98,6 +99,10 @@ textarea::placeholder {
margin: 0;
}

.upper-p {
padding-top: 10px;
}

div {
margin: 15px auto;
}
Expand Down
2 changes: 1 addition & 1 deletion views/editSnippet.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<form name="edit-snippet-form" data-vscode-context='{"preventDefaultContextMenuItems": true }'>
<div class="edit-layout">
<div class="left-panel">
<div class="no-m"><label for="snippet-label">Snippet Label</label></div>
<div class="no-m upper-p"><label for="snippet-label">Snippet Label</label></div>
<div><input type="text" id="snippet-label" value="{{snippet.label}}" required></div>
<div class="no-m"><label for="snippet-description">Snippet Description</label></div>
<p class="hint">Optional description of the snippet displayed by IntelliSense.</p>
Expand Down
42 changes: 32 additions & 10 deletions views/editSnippetFolder.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,41 @@

<body>
<form name="edit-folder-form">
<div class="no-m"><label for="folder-name">Folder Name</label></div>
<div><input type="text" id="folder-name" value="{{snippet.label}}" required></div>
<div>
<label for="snippet-value">Folder Icon</label>
<div class="popup" name="_snippets_syntax">
Copy/paste the icon name from
<a href="https://microsoft.github.io/vscode-codicons/dist/codicon.html" target="_blank">here</a>
<div class="edit-layout">
<div class="left-panel">
<div class="no-m upper-p"><label for="folder-name">Folder Name</label></div>
<div><input type="text" id="folder-name" value="{{snippet.label}}" required></div>
</div>
<div class="right-panel">
<div><input class="saveButton" type="submit" value="Save"></div>
<div class="no-m"><label for="folder-icon">Folder Icon</label></div>
<p class="hint">Optional. Copy/paste an icon name from
<a href="https://microsoft.github.io/vscode-codicons/dist/codicon.html" target="_blank">the codicon
list</a>.</p>
<div><input type="text" id="folder-icon" value="{{snippet.icon}}"></div>
<div class="no-m"><label for="folder-color">Folder Color</label></div>
<p class="hint">Tints the folder label. Unless the cascade is disabled in settings, it also colors the
snippets and subfolders inside.</p>
<div class="select-wrapper">
<select id="folder-color" data-current="{{snippet.color}}">
<option value="">Default</option>
<option value="charts.red">Red</option>
<option value="charts.orange">Orange</option>
<option value="charts.yellow">Yellow</option>
<option value="charts.green">Green</option>
<option value="terminal.ansiCyan">Cyan</option>
<option value="charts.blue">Blue</option>
<option value="charts.purple">Purple</option>
<option value="terminal.ansiMagenta">Magenta</option>
<option value="charts.foreground">Gray</option>
<option value="terminal.ansiWhite">White</option>
</select>
</div>
</div>
</div>
<div><input type="text" id="folder-icon" value="{{snippet.icon}}"></div>
<div><input class="saveButton" type="submit" value="Save"></div>
</form>
</body>

<script src="{{jsUri}}"></script>
</html>

</html>
10 changes: 9 additions & 1 deletion views/js/editSnippetFolder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
(function() {
const vscode = acquireVsCodeApi();

// preselect the folder's saved color
const colorSelect = document.getElementById('folder-color');
if (colorSelect) {
colorSelect.value = colorSelect.dataset.current || '';
}

document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault();
const form = document.querySelector('form[name="edit-folder-form"]');
Expand All @@ -11,11 +17,13 @@

const snippetLabel = form.elements['folder-name'].value;
const snippetIcon = form.elements['folder-icon'].value;
const snippetColor = form.elements['folder-color'].value;

vscode.postMessage({
data: {
label: snippetLabel,
icon: snippetIcon
icon: snippetIcon,
color: snippetColor
},
command: 'edit-folder'
});
Expand Down
Loading