packages/baseweb-vscode-extension/ext-src/coloring.ts (180 lines of code) (raw):
import * as vscode from 'vscode';
import { LightTheme, DarkTheme } from 'baseui/themes';
function getContrastYIQ(color: string) {
let hexcolor = color;
if (hexcolor.length < 7) {
hexcolor = hexcolor.replace(/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/g, '#$1$1$2$2$3$3');
}
const r = parseInt(hexcolor.substr(1, 2), 16);
const g = parseInt(hexcolor.substr(3, 2), 16);
const b = parseInt(hexcolor.substr(5, 2), 16);
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? '#000000' : '#FFFFFF';
}
function isAcceptedColorValue(color: string | undefined) {
if (!color) return false;
return color.startsWith('#') || color.startsWith('rgb');
}
export default (context: vscode.ExtensionContext) => {
let timeout: NodeJS.Timer | undefined = undefined;
let activeEditor = vscode.window.activeTextEditor;
const decorationTypeMap: any = {};
// Handle theme props other than colors
// Create a decorator type that we use to decorate theme props.
// We do not apply any styles to the theme props other than colors
const emptyDecorationType = vscode.window.createTextEditorDecorationType({});
let themePropsDecorationList: Array<any> = [];
// Create a decorator type that we use to decorate theme colors
function getColorDecorationType(coloringStyle: string, colorVal: string) {
const decorationType = vscode.window.createTextEditorDecorationType({
borderWidth:
coloringStyle === 'border' ? '2px' : coloringStyle === 'underline' ? '0 0 2px' : '0',
borderStyle: 'solid',
borderColor: colorVal,
backgroundColor: coloringStyle === 'background' ? colorVal : 'transparent',
...(coloringStyle === 'background' && colorVal.startsWith('#')
? { color: getContrastYIQ(colorVal) }
: {}),
});
return decorationType;
}
// Memoize decorator types per color value and current coloringStyle setting
function getMemoizedDecorationType(coloringStyle: string, colorVal: string) {
if (!decorationTypeMap[colorVal]) {
decorationTypeMap[colorVal] = {};
decorationTypeMap[colorVal].style = coloringStyle;
decorationTypeMap[colorVal].type = getColorDecorationType(coloringStyle, colorVal);
decorationTypeMap[colorVal].decorations = [];
} else if (decorationTypeMap[colorVal].style !== coloringStyle) {
decorationTypeMap[colorVal].style = coloringStyle;
decorationTypeMap[colorVal].type = getColorDecorationType(coloringStyle, colorVal);
}
return decorationTypeMap[colorVal];
}
// Set decorations
function decorate(activeEditor: vscode.TextEditor) {
const keys: Array<any> = [];
for (const prop in decorationTypeMap) {
if (decorationTypeMap.hasOwnProperty(prop)) {
keys.push(prop);
}
}
keys.forEach((key) => {
activeEditor.setDecorations(decorationTypeMap[key].type, decorationTypeMap[key].decorations);
});
}
// Unset decorations
function undecorate(activeEditor: vscode.TextEditor) {
const keys: Array<any> = [];
for (const prop in decorationTypeMap) {
if (decorationTypeMap.hasOwnProperty(prop)) {
keys.push(prop);
}
}
keys.forEach((key) => {
decorationTypeMap[key].decorations = [];
activeEditor.setDecorations(decorationTypeMap[key].type, decorationTypeMap[key].decorations);
});
}
function updateDecorations() {
if (!activeEditor) {
return;
}
// Unset all memoized decorations
// and clear the lists of decorations for every decoration type
undecorate(activeEditor);
// Handle theme props other than colors
// Clear the lists of decorations
// and unset all decorations
themePropsDecorationList = [];
activeEditor.setDecorations(emptyDecorationType, themePropsDecorationList);
const workspaceConfig = vscode.workspace.getConfiguration('baseweb');
// Get the coloring enabled/disabled setting
const isColoringOn = workspaceConfig.get('theme.coloring.enabled');
// If the coloring feature is set to false exit
if (!isColoringOn) {
return;
}
// Get the Theme choice setting
const themeMode = workspaceConfig.get('theme.coloring.source');
// Use the theme according to the current setting
const theme = themeMode === 'Light' ? LightTheme : DarkTheme;
// Get the coloring style setting
const coloringStyle: string = workspaceConfig.get('theme.coloring.style') || '';
// RegExp for finding `colors.[THEME_PROP]`
const regEx = /(^|\W)(colors\.)(\w+)/gm;
// Get the current active document's text
const text = activeEditor.document.getText();
let match;
// Find matches to decorate accordingly
while ((match = regEx.exec(text))) {
// @ts-ignore
const themeColorVal: string | undefined = theme.colors[match[3]];
// Do not decorate if the found color key is not present in the theme object
if (!themeColorVal) {
continue;
}
// It should never get to here if `!themeColorVal`
// but adding a default `transparent` to satisfy types
const colorVal: string = isAcceptedColorValue(themeColorVal) ? themeColorVal : 'transparent';
// Start position excluding the `colors.` part
const startPos = activeEditor.document.positionAt(
match.index + match[1].length + match[2].length
);
// End position of the match
const endPos = activeEditor.document.positionAt(match.index + match[0].length);
// Create decoration for the current match position
const decoration = {
range: new vscode.Range(startPos, endPos),
hoverMessage: `${colorVal} | ${themeMode}Theme`,
};
// Create and memoize a decoration type for the found color value
// or pull a memoized decoration type if not the first match
// for the color value and current coloring style setting
const memoizedDecorationType = getMemoizedDecorationType(coloringStyle, colorVal);
// Add the decoration for the current match to the list
memoizedDecorationType.decorations.push(decoration);
}
// Apply all memoized decorations
decorate(activeEditor);
// Handle theme props other than colors
// List of theme props we show hints for
const themeProps = ['animation', 'borders', 'breakpoints', 'lighting', 'sizing', 'typography'];
for (let i = 0; i < themeProps.length; i++) {
const regEx = new RegExp(`(\^|\\W)(${themeProps[i]}\\.)(\\w+)`, 'gm');
// Find matches to decorate accordingly
while ((match = regEx.exec(text))) {
// @ts-ignore
const themePropVal: any = theme[themeProps[i]][match[3]];
// Do not decorate if the found prop key is not present in the theme object
if (!themePropVal) {
continue;
}
// Start position excluding the `sizing.` part
const startPos = activeEditor.document.positionAt(
match.index + match[1].length + match[2].length
);
// End position of the match
const endPos = activeEditor.document.positionAt(match.index + match[0].length);
// Create decoration for the current match position
const decoration = {
range: new vscode.Range(startPos, endPos),
hoverMessage: `${JSON.stringify(themePropVal)} | ${themeMode}Theme`,
};
// Add the decoration for the current match to the list
themePropsDecorationList.push(decoration);
}
}
// Apply all decorations
activeEditor.setDecorations(emptyDecorationType, themePropsDecorationList);
}
function triggerUpdateDecorations() {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
timeout = setTimeout(updateDecorations, 200);
}
if (activeEditor) {
triggerUpdateDecorations();
}
// Subscribe for the settings change events
vscode.workspace.onDidChangeConfiguration((event) => {
if (
event.affectsConfiguration('baseweb.theme.coloring.enabled') ||
event.affectsConfiguration('baseweb.theme.coloring.source') ||
event.affectsConfiguration('baseweb.theme.coloring.style')
) {
triggerUpdateDecorations();
}
}),
// Subscribe for active editor change events
vscode.window.onDidChangeActiveTextEditor(
(editor) => {
activeEditor = editor;
if (editor) {
triggerUpdateDecorations();
}
},
null,
context.subscriptions
);
// Subscribe for document change events
vscode.workspace.onDidChangeTextDocument(
(event) => {
if (activeEditor && event.document === activeEditor.document) {
triggerUpdateDecorations();
}
},
null,
context.subscriptions
);
};