テーマ API
テーマ APIは、開発環境内でカスタムテーマの作成、管理、適用を行うための包括的な機能を提供します。
概要
テーマ APIでは以下のことができます:
- カスタムカラーテーマとシンタックスハイライトの作成
- テーマの切り替えと設定の管理
- テーマカスタマイズオプションの提供
- ライト/ダークモードの切り替え処理
- アイコンテーマとファイル関連付けの作成
- テーマの継承とバリアントの実装
- テーマプレビューとテスト機能の提供
- テーマのパッケージ化と配布の処理
基本的な使用方法
テーマ管理
typescript
import { TraeAPI } from '@trae/api';
// テーママネージャーの実装
class ThemeManager {
private themes: Map<string, ThemeInfo> = new Map();
private currentTheme: string | null = null;
private themeChangeListeners: ((theme: ThemeInfo) => void)[] = [];
private customThemes: Map<string, CustomTheme> = new Map();
constructor() {
this.loadBuiltInThemes();
this.loadCustomThemes();
this.setupThemeListeners();
this.applyStoredTheme();
}
private loadBuiltInThemes(): void {
// 組み込みテーマを読み込み
const builtInThemes: ThemeInfo[] = [
{
id: 'dark-default',
label: 'ダーク(デフォルト)',
description: 'デフォルトのダークテーマ',
type: 'dark',
isBuiltIn: true,
colors: {
'editor.background': '#1e1e1e',
'editor.foreground': '#d4d4d4',
'activityBar.background': '#333333',
'sideBar.background': '#252526',
'statusBar.background': '#007acc',
'titleBar.activeBackground': '#3c3c3c'
},
tokenColors: [
{
scope: 'comment',
settings: {
foreground: '#6A9955',
fontStyle: 'italic'
}
},
{
scope: 'keyword',
settings: {
foreground: '#569CD6'
}
},
{
scope: 'string',
settings: {
foreground: '#CE9178'
}
}
]
},
{
id: 'light-default',
label: 'ライト(デフォルト)',
description: 'デフォルトのライトテーマ',
type: 'light',
isBuiltIn: true,
colors: {
'editor.background': '#ffffff',
'editor.foreground': '#000000',
'activityBar.background': '#f3f3f3',
'sideBar.background': '#f8f8f8',
'statusBar.background': '#007acc',
'titleBar.activeBackground': '#dddddd'
},
tokenColors: [
{
scope: 'comment',
settings: {
foreground: '#008000',
fontStyle: 'italic'
}
},
{
scope: 'keyword',
settings: {
foreground: '#0000ff'
}
},
{
scope: 'string',
settings: {
foreground: '#a31515'
}
}
]
},
{
id: 'high-contrast',
label: 'ハイコントラスト',
description: 'アクセシビリティ用のハイコントラストテーマ',
type: 'hc',
isBuiltIn: true,
colors: {
'editor.background': '#000000',
'editor.foreground': '#ffffff',
'activityBar.background': '#000000',
'sideBar.background': '#000000',
'statusBar.background': '#000000',
'titleBar.activeBackground': '#000000'
},
tokenColors: [
{
scope: 'comment',
settings: {
foreground: '#7ca668'
}
},
{
scope: 'keyword',
settings: {
foreground: '#569cd6'
}
},
{
scope: 'string',
settings: {
foreground: '#ce9178'
}
}
]
}
];
builtInThemes.forEach(theme => {
this.themes.set(theme.id, theme);
});
console.log(`${builtInThemes.length}個の組み込みテーマを読み込みました`);
}
private async loadCustomThemes(): Promise<void> {
try {
// ワークスペースとグローバルストレージからカスタムテーマを読み込み
const workspaceThemes = await this.loadWorkspaceThemes();
const globalThemes = await this.loadGlobalThemes();
[...workspaceThemes, ...globalThemes].forEach(theme => {
this.themes.set(theme.id, theme);
});
console.log(`${workspaceThemes.length + globalThemes.length}個のカスタムテーマを読み込みました`);
} catch (error) {
console.error('カスタムテーマの読み込みに失敗しました:', error);
}
}
private async loadWorkspaceThemes(): Promise<ThemeInfo[]> {
const themes: ThemeInfo[] = [];
if (!TraeAPI.workspace.workspaceFolders) {
return themes;
}
for (const folder of TraeAPI.workspace.workspaceFolders) {
const themeFiles = await TraeAPI.workspace.findFiles(
new TraeAPI.RelativePattern(folder, '**/*.json'),
'**/node_modules/**'
);
for (const file of themeFiles) {
if (file.fsPath.includes('themes') || file.fsPath.endsWith('-theme.json')) {
try {
const content = await TraeAPI.workspace.fs.readFile(file);
const themeData = JSON.parse(content.toString());
if (this.isValidTheme(themeData)) {
const theme = this.parseThemeFile(file, themeData);
themes.push(theme);
}
} catch (error) {
console.warn(`テーマファイル ${file.fsPath} の読み込みに失敗しました:`, error);
}
}
}
}
return themes;
}
private async loadGlobalThemes(): Promise<ThemeInfo[]> {
// グローバルストレージまたはユーザー設定からテーマを読み込み
const themes: ThemeInfo[] = [];
try {
const globalThemeData = TraeAPI.workspace.getConfiguration('themes').get('custom', []);
for (const themeData of globalThemeData) {
if (this.isValidTheme(themeData)) {
const theme: ThemeInfo = {
id: themeData.id || `custom-${Date.now()}`,
label: themeData.label || 'カスタムテーマ',
description: themeData.description || '',
type: themeData.type || 'dark',
isBuiltIn: false,
colors: themeData.colors || {},
tokenColors: themeData.tokenColors || []
};
themes.push(theme);
}
}
} catch (error) {
console.warn('グローバルテーマの読み込みに失敗しました:', error);
}
return themes;
}
private setupThemeListeners(): void {
// 設定変更をリッスン
TraeAPI.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('workbench.colorTheme') ||
event.affectsConfiguration('themes')) {
this.handleConfigurationChange();
}
});
// テーマファイルのファイルシステム変更をリッスン
const themeWatcher = TraeAPI.workspace.createFileSystemWatcher('**/*theme*.json');
themeWatcher.onDidCreate(uri => this.handleThemeFileChange(uri));
themeWatcher.onDidChange(uri => this.handleThemeFileChange(uri));
themeWatcher.onDidDelete(uri => this.handleThemeFileDelete(uri));
}
private async applyStoredTheme(): Promise<void> {
const storedTheme = TraeAPI.workspace.getConfiguration('workbench').get('colorTheme');
if (storedTheme && this.themes.has(storedTheme as string)) {
await this.applyTheme(storedTheme as string);
} else {
// システム設定に基づいてデフォルトテーマを適用
const prefersDark = await this.getSystemThemePreference();
const defaultTheme = prefersDark ? 'dark-default' : 'light-default';
await this.applyTheme(defaultTheme);
}
}
private async getSystemThemePreference(): Promise<boolean> {
// システムテーマ設定を確認
try {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
} catch (error) {
console.warn('システムテーマ設定の検出に失敗しました:', error);
}
return true; // デフォルトはダーク
}
// テーマを適用
async applyTheme(themeId: string): Promise<boolean> {
const theme = this.themes.get(themeId);
if (!theme) {
console.error(`テーマが見つかりません: ${themeId}`);
return false;
}
try {
console.log(`テーマを適用中: ${theme.label}`);
// カラーテーマを適用
await this.applyColorTheme(theme);
// シンタックスハイライトを適用
await this.applySyntaxHighlighting(theme);
// 現在のテーマを更新
this.currentTheme = themeId;
// 設定に保存
await TraeAPI.workspace.getConfiguration('workbench').update(
'colorTheme',
themeId,
TraeAPI.ConfigurationTarget.Global
);
// リスナーに通知
this.notifyThemeChange(theme);
console.log(`テーマが正常に適用されました: ${theme.label}`);
return true;
} catch (error) {
console.error(`テーマ ${themeId} の適用に失敗しました:`, error);
TraeAPI.window.showErrorMessage(`テーマの適用に失敗しました: ${error.message}`);
return false;
}
}
private async applyColorTheme(theme: ThemeInfo): Promise<void> {
// エディターとUIにカラーテーマを適用
const colors = theme.colors;
// CSSカスタムプロパティまたはテーマシステムに色を適用
if (typeof document !== 'undefined') {
const root = document.documentElement;
Object.entries(colors).forEach(([key, value]) => {
const cssVar = `--theme-${key.replace(/\./g, '-')}`;
root.style.setProperty(cssVar, value);
});
// テーマタイプクラスを設定
root.className = root.className.replace(/theme-(light|dark|hc)/g, '');
root.classList.add(`theme-${theme.type}`);
}
}
private async applySyntaxHighlighting(theme: ThemeInfo): Promise<void> {
// シンタックスハイライトルールを適用
const tokenColors = theme.tokenColors;
// シンタックスハイライト用のCSSを生成
const syntaxCSS = this.generateSyntaxCSS(tokenColors);
// シンタックスCSSを適用
if (typeof document !== 'undefined') {
let syntaxStyleElement = document.getElementById('theme-syntax-highlighting');
if (!syntaxStyleElement) {
syntaxStyleElement = document.createElement('style');
syntaxStyleElement.id = 'theme-syntax-highlighting';
document.head.appendChild(syntaxStyleElement);
}
syntaxStyleElement.textContent = syntaxCSS;
}
}
private generateSyntaxCSS(tokenColors: TokenColor[]): string {
let css = '';
tokenColors.forEach((tokenColor, index) => {
const scopes = Array.isArray(tokenColor.scope) ? tokenColor.scope : [tokenColor.scope];
scopes.forEach(scope => {
const selector = `.token.${scope.replace(/\./g, '-')}`;
const styles: string[] = [];
if (tokenColor.settings.foreground) {
styles.push(`color: ${tokenColor.settings.foreground}`);
}
if (tokenColor.settings.background) {
styles.push(`background-color: ${tokenColor.settings.background}`);
}
if (tokenColor.settings.fontStyle) {
if (tokenColor.settings.fontStyle.includes('italic')) {
styles.push('font-style: italic');
}
if (tokenColor.settings.fontStyle.includes('bold')) {
styles.push('font-weight: bold');
}
if (tokenColor.settings.fontStyle.includes('underline')) {
styles.push('text-decoration: underline');
}
}
if (styles.length > 0) {
css += `${selector} { ${styles.join('; ')} }\n`;
}
});
});
return css;
}
// テーマの作成とカスタマイズ
async createCustomTheme(options: ThemeCreationOptions): Promise<string> {
const themeId = `custom-${options.name.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`;
const customTheme: CustomTheme = {
id: themeId,
name: options.name,
description: options.description || '',
baseTheme: options.baseTheme || 'dark-default',
customizations: options.customizations || {},
author: options.author || 'ユーザー',
version: '1.0.0',
created: new Date(),
modified: new Date()
};
// ベーステーマとカスタマイズからテーマを生成
const theme = await this.generateThemeFromCustomization(customTheme);
// カスタムテーマを保存
this.customThemes.set(themeId, customTheme);
this.themes.set(themeId, theme);
// ストレージに保存
await this.saveCustomTheme(customTheme);
console.log(`カスタムテーマが作成されました: ${options.name}`);
TraeAPI.window.showInformationMessage(`カスタムテーマ「${options.name}」が正常に作成されました!`);
return themeId;
}
private async generateThemeFromCustomization(customTheme: CustomTheme): Promise<ThemeInfo> {
const baseTheme = this.themes.get(customTheme.baseTheme);
if (!baseTheme) {
throw new Error(`ベーステーマが見つかりません: ${customTheme.baseTheme}`);
}
// ベーステーマをクローン
const theme: ThemeInfo = {
id: customTheme.id,
label: customTheme.name,
description: customTheme.description,
type: baseTheme.type,
isBuiltIn: false,
colors: { ...baseTheme.colors },
tokenColors: [...baseTheme.tokenColors]
};
// カスタマイズを適用
const customizations = customTheme.customizations;
// カラーカスタマイズを適用
if (customizations.colors) {
Object.assign(theme.colors, customizations.colors);
}
// トークンカラーカスタマイズを適用
if (customizations.tokenColors) {
// トークンカラーをマージまたは置換
customizations.tokenColors.forEach(customToken => {
const existingIndex = theme.tokenColors.findIndex(token =>
token.scope === customToken.scope
);
if (existingIndex >= 0) {
// 既存のトークンカラーを更新
theme.tokenColors[existingIndex] = { ...theme.tokenColors[existingIndex], ...customToken };
} else {
// 新しいトークンカラーを追加
theme.tokenColors.push(customToken);
}
});
}
// セマンティックハイライトカスタマイズを適用
if (customizations.semanticHighlighting) {
theme.semanticHighlighting = customizations.semanticHighlighting;
}
return theme;
}
async updateCustomTheme(themeId: string, updates: Partial<ThemeCreationOptions>): Promise<boolean> {
const customTheme = this.customThemes.get(themeId);
if (!customTheme) {
console.error(`カスタムテーマが見つかりません: ${themeId}`);
return false;
}
try {
// カスタムテーマを更新
if (updates.name) customTheme.name = updates.name;
if (updates.description) customTheme.description = updates.description;
if (updates.customizations) {
customTheme.customizations = { ...customTheme.customizations, ...updates.customizations };
}
customTheme.modified = new Date();
// テーマを再生成
const theme = await this.generateThemeFromCustomization(customTheme);
this.themes.set(themeId, theme);
// ストレージに保存
await this.saveCustomTheme(customTheme);
// 現在のテーマの場合は再適用
if (this.currentTheme === themeId) {
await this.applyTheme(themeId);
}
console.log(`カスタムテーマが更新されました: ${customTheme.name}`);
return true;
} catch (error) {
console.error(`カスタムテーマ ${themeId} の更新に失敗しました:`, error);
return false;
}
}
async deleteCustomTheme(themeId: string): Promise<boolean> {
const customTheme = this.customThemes.get(themeId);
if (!customTheme) {
console.error(`カスタムテーマが見つかりません: ${themeId}`);
return false;
}
try {
// 現在のテーマを削除する場合はデフォルトテーマに切り替え
if (this.currentTheme === themeId) {
await this.applyTheme('dark-default');
}
// ストレージから削除
await this.removeCustomTheme(themeId);
// メモリから削除
this.customThemes.delete(themeId);
this.themes.delete(themeId);
console.log(`カスタムテーマが削除されました: ${customTheme.name}`);
TraeAPI.window.showInformationMessage(`カスタムテーマ「${customTheme.name}」が正常に削除されました!`);
return true;
} catch (error) {
console.error(`カスタムテーマ ${themeId} の削除に失敗しました:`, error);
return false;
}
}
// テーマのインポート/エクスポート
async exportTheme(themeId: string): Promise<string | undefined> {
const theme = this.themes.get(themeId);
if (!theme) {
console.error(`テーマが見つかりません: ${themeId}`);
return undefined;
}
try {
const exportData = {
name: theme.label,
type: theme.type,
colors: theme.colors,
tokenColors: theme.tokenColors,
semanticHighlighting: theme.semanticHighlighting,
metadata: {
id: theme.id,
description: theme.description,
exportedAt: new Date().toISOString(),
version: '1.0.0'
}
};
const exportJson = JSON.stringify(exportData, null, 2);
// ファイルに保存
const fileName = `${theme.label.replace(/\s+/g, '-').toLowerCase()}-theme.json`;
const uri = await TraeAPI.window.showSaveDialog({
defaultUri: TraeAPI.Uri.file(fileName),
filters: {
'テーマファイル': ['json']
}
});
if (uri) {
await TraeAPI.workspace.fs.writeFile(uri, Buffer.from(exportJson));
TraeAPI.window.showInformationMessage(`テーマが ${uri.fsPath} にエクスポートされました`);
return uri.fsPath;
}
return undefined;
} catch (error) {
console.error(`テーマ ${themeId} のエクスポートに失敗しました:`, error);
TraeAPI.window.showErrorMessage(`テーマのエクスポートに失敗しました: ${error.message}`);
return undefined;
}
}
async importTheme(filePath?: string): Promise<string | undefined> {
try {
let uri: TraeAPI.Uri;
if (filePath) {
uri = TraeAPI.Uri.file(filePath);
} else {
const uris = await TraeAPI.window.showOpenDialog({
canSelectFiles: true,
canSelectMany: false,
filters: {
'テーマファイル': ['json']
}
});
if (!uris || uris.length === 0) {
return undefined;
}
uri = uris[0];
}
// テーマファイルを読み込み
const content = await TraeAPI.workspace.fs.readFile(uri);
const themeData = JSON.parse(content.toString());
if (!this.isValidTheme(themeData)) {
throw new Error('無効なテーマファイル形式です');
}
// テーマを作成
const themeId = `imported-${themeData.name.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`;
const theme: ThemeInfo = {
id: themeId,
label: themeData.name,
description: themeData.description || themeData.metadata?.description || '',
type: themeData.type || 'dark',
isBuiltIn: false,
colors: themeData.colors || {},
tokenColors: themeData.tokenColors || [],
semanticHighlighting: themeData.semanticHighlighting
};
// テーマに追加
this.themes.set(themeId, theme);
// カスタムテーマとして保存
const customTheme: CustomTheme = {
id: themeId,
name: theme.label,
description: theme.description,
baseTheme: 'dark-default',
customizations: {
colors: theme.colors,
tokenColors: theme.tokenColors,
semanticHighlighting: theme.semanticHighlighting
},
author: themeData.metadata?.author || '不明',
version: themeData.metadata?.version || '1.0.0',
created: new Date(),
modified: new Date()
};
this.customThemes.set(themeId, customTheme);
await this.saveCustomTheme(customTheme);
console.log(`テーマがインポートされました: ${theme.label}`);
TraeAPI.window.showInformationMessage(`テーマ「${theme.label}」が正常にインポートされました!`);
return themeId;
} catch (error) {
console.error('テーマのインポートに失敗しました:', error);
TraeAPI.window.showErrorMessage(`テーマのインポートに失敗しました: ${error.message}`);
return undefined;
}
}
// テーマユーティリティ
getThemes(): ThemeInfo[] {
return Array.from(this.themes.values());
}
getTheme(themeId: string): ThemeInfo | undefined {
return this.themes.get(themeId);
}
getCurrentTheme(): ThemeInfo | undefined {
return this.currentTheme ? this.themes.get(this.currentTheme) : undefined;
}
getThemesByType(type: 'light' | 'dark' | 'hc'): ThemeInfo[] {
return Array.from(this.themes.values()).filter(theme => theme.type === type);
}
getCustomThemes(): CustomTheme[] {
return Array.from(this.customThemes.values());
}
searchThemes(query: string): ThemeInfo[] {
const lowercaseQuery = query.toLowerCase();
return Array.from(this.themes.values()).filter(theme =>
theme.label.toLowerCase().includes(lowercaseQuery) ||
theme.description.toLowerCase().includes(lowercaseQuery)
);
}
// テーマプレビュー
async previewTheme(themeId: string): Promise<boolean> {
const theme = this.themes.get(themeId);
if (!theme) {
console.error(`テーマが見つかりません: ${themeId}`);
return false;
}
try {
// 復元用に現在のテーマを保存
const previousTheme = this.currentTheme;
// プレビューテーマを適用
await this.applyTheme(themeId);
// プレビューダイアログを表示
const action = await TraeAPI.window.showInformationMessage(
`テーマをプレビュー中: ${theme.label}`,
{ modal: true },
'テーマを適用',
'前のテーマに戻す'
);
if (action === 'テーマを適用') {
// テーマを保持
return true;
} else {
// 前のテーマに復元
if (previousTheme) {
await this.applyTheme(previousTheme);
}
return false;
}
} catch (error) {
console.error(`テーマ ${themeId} のプレビューに失敗しました:`, error);
return false;
}
}
// イベント処理
onThemeChange(listener: (theme: ThemeInfo) => void): TraeAPI.Disposable {
this.themeChangeListeners.push(listener);
return {
dispose: () => {
const index = this.themeChangeListeners.indexOf(listener);
if (index >= 0) {
this.themeChangeListeners.splice(index, 1);
}
}
};
}
private notifyThemeChange(theme: ThemeInfo): void {
this.themeChangeListeners.forEach(listener => {
try {
listener(theme);
} catch (error) {
console.error('テーマ変更リスナーでエラーが発生しました:', error);
}
});
}
private async handleConfigurationChange(): Promise<void> {
const currentConfigTheme = TraeAPI.workspace.getConfiguration('workbench').get('colorTheme');
if (currentConfigTheme && currentConfigTheme !== this.currentTheme) {
await this.applyTheme(currentConfigTheme as string);
}
}
private async handleThemeFileChange(uri: TraeAPI.Uri): Promise<void> {
console.log(`テーマファイルが変更されました: ${uri.fsPath}`);
// テーマを再読み込み
await this.loadCustomThemes();
}
private async handleThemeFileDelete(uri: TraeAPI.Uri): Promise<void> {
console.log(`テーマファイルが削除されました: ${uri.fsPath}`);
// テーマを検索して削除
const themeToRemove = Array.from(this.themes.values()).find(theme =>
!theme.isBuiltIn && uri.fsPath.includes(theme.id)
);
if (themeToRemove) {
this.themes.delete(themeToRemove.id);
this.customThemes.delete(themeToRemove.id);
// 現在のテーマが削除された場合はデフォルトに切り替え
if (this.currentTheme === themeToRemove.id) {
await this.applyTheme('dark-default');
}
}
}
// ストレージメソッド
private async saveCustomTheme(customTheme: CustomTheme): Promise<void> {
try {
const customThemes = TraeAPI.workspace.getConfiguration('themes').get('custom', []);
const existingIndex = customThemes.findIndex((t: any) => t.id === customTheme.id);
if (existingIndex >= 0) {
customThemes[existingIndex] = customTheme;
} else {
customThemes.push(customTheme);
}
await TraeAPI.workspace.getConfiguration('themes').update(
'custom',
customThemes,
TraeAPI.ConfigurationTarget.Global
);
} catch (error) {
console.error('カスタムテーマの保存に失敗しました:', error);
}
}
private async removeCustomTheme(themeId: string): Promise<void> {
try {
const customThemes = TraeAPI.workspace.getConfiguration('themes').get('custom', []);
const filteredThemes = customThemes.filter((t: any) => t.id !== themeId);
await TraeAPI.workspace.getConfiguration('themes').update(
'custom',
filteredThemes,
TraeAPI.ConfigurationTarget.Global
);
} catch (error) {
console.error('カスタムテーマの削除に失敗しました:', error);
}
}
// バリデーション
private isValidTheme(themeData: any): boolean {
return (
themeData &&
typeof themeData === 'object' &&
(themeData.name || themeData.label) &&
(themeData.colors || themeData.tokenColors)
);
}
private parseThemeFile(file: TraeAPI.Uri, themeData: any): ThemeInfo {
const fileName = file.fsPath.split('/').pop() || '';
const themeId = `file-${fileName.replace(/\.json$/, '')}-${Date.now()}`;
return {
id: themeId,
label: themeData.name || themeData.label || fileName,
description: themeData.description || '',
type: themeData.type || 'dark',
isBuiltIn: false,
colors: themeData.colors || {},
tokenColors: themeData.tokenColors || [],
semanticHighlighting: themeData.semanticHighlighting
};
}
dispose(): void {
this.themes.clear();
this.customThemes.clear();
this.themeChangeListeners.length = 0;
this.currentTheme = null;
}
}
// インターフェース
interface ThemeInfo {
id: string;
label: string;
description: string;
type: 'light' | 'dark' | 'hc';
isBuiltIn: boolean;
colors: Record<string, string>;
tokenColors: TokenColor[];
semanticHighlighting?: Record<string, any>;
}
interface TokenColor {
scope: string | string[];
settings: {
foreground?: string;
background?: string;
fontStyle?: string;
};
}
interface CustomTheme {
id: string;
name: string;
description: string;
baseTheme: string;
customizations: {
colors?: Record<string, string>;
tokenColors?: TokenColor[];
semanticHighlighting?: Record<string, any>;
};
author: string;
version: string;
created: Date;
modified: Date;
}
interface ThemeCreationOptions {
name: string;
description?: string;
baseTheme?: string;
customizations?: {
colors?: Record<string, string>;
tokenColors?: TokenColor[];
semanticHighlighting?: Record<string, any>;
};
author?: string;
}
// テーママネージャーを初期化
const themeManager = new ThemeManager();アイコンテーマ
typescript
// アイコンテーマ管理
class IconThemeManager {
private iconThemes: Map<string, IconThemeInfo> = new Map();
private currentIconTheme: string | null = null;
private iconThemeChangeListeners: ((theme: IconThemeInfo) => void)[] = [];
constructor() {
this.loadBuiltInIconThemes();
this.loadCustomIconThemes();
this.applyStoredIconTheme();
}
private loadBuiltInIconThemes(): void {
const builtInIconThemes: IconThemeInfo[] = [
{
id: 'default-icons',
label: 'デフォルトアイコン',
description: 'デフォルトのファイルとフォルダーアイコン',
isBuiltIn: true,
iconDefinitions: {
'file': { iconPath: './icons/file.svg' },
'folder': { iconPath: './icons/folder.svg' },
'folder-open': { iconPath: './icons/folder-open.svg' }
},
fileExtensions: {
'js': 'javascript-icon',
'ts': 'typescript-icon',
'json': 'json-icon',
'md': 'markdown-icon'
},
fileNames: {
'package.json': 'npm-icon',
'README.md': 'readme-icon'
},
folderNames: {
'node_modules': 'node-modules-icon',
'src': 'source-icon',
'dist': 'dist-icon'
}
}
];
builtInIconThemes.forEach(theme => {
this.iconThemes.set(theme.id, theme);
});
}
private async loadCustomIconThemes(): Promise<void> {
// ワークスペースと設定からカスタムアイコンテーマを読み込み
try {
const customIconThemes = TraeAPI.workspace.getConfiguration('iconThemes').get('custom', []);
customIconThemes.forEach((themeData: any) => {
if (this.isValidIconTheme(themeData)) {
const theme: IconThemeInfo = {
id: themeData.id,
label: themeData.label,
description: themeData.description || '',
isBuiltIn: false,
iconDefinitions: themeData.iconDefinitions || {},
fileExtensions: themeData.fileExtensions || {},
fileNames: themeData.fileNames || {},
folderNames: themeData.folderNames || {}
};
this.iconThemes.set(theme.id, theme);
}
});
} catch (error) {
console.error('カスタムアイコンテーマの読み込みに失敗しました:', error);
}
}
private async applyStoredIconTheme(): Promise<void> {
const storedIconTheme = TraeAPI.workspace.getConfiguration('workbench').get('iconTheme');
if (storedIconTheme && this.iconThemes.has(storedIconTheme as string)) {
await this.applyIconTheme(storedIconTheme as string);
} else {
await this.applyIconTheme('default-icons');
}
}
async applyIconTheme(themeId: string): Promise<boolean> {
const theme = this.iconThemes.get(themeId);
if (!theme) {
console.error(`アイコンテーマが見つかりません: ${themeId}`);
return false;
}
try {
console.log(`アイコンテーマを適用中: ${theme.label}`);
// アイコンテーマCSSを適用
await this.applyIconThemeCSS(theme);
// 現在のアイコンテーマを更新
this.currentIconTheme = themeId;
// 設定に保存
await TraeAPI.workspace.getConfiguration('workbench').update(
'iconTheme',
themeId,
TraeAPI.ConfigurationTarget.Global
);
// リスナーに通知
this.notifyIconThemeChange(theme);
console.log(`アイコンテーマが正常に適用されました: ${theme.label}`);
return true;
} catch (error) {
console.error(`アイコンテーマ ${themeId} の適用に失敗しました:`, error);
return false;
}
}
private async applyIconThemeCSS(theme: IconThemeInfo): Promise<void> {
// アイコンテーマ用のCSSを生成
const iconCSS = this.generateIconCSS(theme);
// アイコンCSSを適用
if (typeof document !== 'undefined') {
let iconStyleElement = document.getElementById('icon-theme-styles');
if (!iconStyleElement) {
iconStyleElement = document.createElement('style');
iconStyleElement.id = 'icon-theme-styles';
document.head.appendChild(iconStyleElement);
}
iconStyleElement.textContent = iconCSS;
}
}
private generateIconCSS(theme: IconThemeInfo): string {
let css = '';
// ファイル拡張子アイコン
Object.entries(theme.fileExtensions).forEach(([extension, iconId]) => {
const iconDef = theme.iconDefinitions[iconId];
if (iconDef) {
css += `.file-icon[data-extension="${extension}"] { background-image: url(${iconDef.iconPath}); }\n`;
}
});
// ファイル名アイコン
Object.entries(theme.fileNames).forEach(([fileName, iconId]) => {
const iconDef = theme.iconDefinitions[iconId];
if (iconDef) {
css += `.file-icon[data-filename="${fileName}"] { background-image: url(${iconDef.iconPath}); }\n`;
}
});
// フォルダー名アイコン
Object.entries(theme.folderNames).forEach(([folderName, iconId]) => {
const iconDef = theme.iconDefinitions[iconId];
if (iconDef) {
css += `.folder-icon[data-foldername="${folderName}"] { background-image: url(${iconDef.iconPath}); }\n`;
}
});
return css;
}
getIconThemes(): IconThemeInfo[] {
return Array.from(this.iconThemes.values());
}
getCurrentIconTheme(): IconThemeInfo | undefined {
return this.currentIconTheme ? this.iconThemes.get(this.currentIconTheme) : undefined;
}
onIconThemeChange(listener: (theme: IconThemeInfo) => void): TraeAPI.Disposable {
this.iconThemeChangeListeners.push(listener);
return {
dispose: () => {
const index = this.iconThemeChangeListeners.indexOf(listener);
if (index >= 0) {
this.iconThemeChangeListeners.splice(index, 1);
}
}
};
}
private notifyIconThemeChange(theme: IconThemeInfo): void {
this.iconThemeChangeListeners.forEach(listener => {
try {
listener(theme);
} catch (error) {
console.error('アイコンテーマ変更リスナーでエラーが発生しました:', error);
}
});
}
private isValidIconTheme(themeData: any): boolean {
return (
themeData &&
typeof themeData === 'object' &&
themeData.id &&
themeData.label &&
themeData.iconDefinitions
);
}
dispose(): void {
this.iconThemes.clear();
this.iconThemeChangeListeners.length = 0;
this.currentIconTheme = null;
}
}
interface IconThemeInfo {
id: string;
label: string;
description: string;
isBuiltIn: boolean;
iconDefinitions: Record<string, { iconPath: string }>;
fileExtensions: Record<string, string>;
fileNames: Record<string, string>;
folderNames: Record<string, string>;
}
// アイコンテーママネージャーを初期化
const iconThemeManager = new IconThemeManager();APIリファレンス
コアインターフェース
typescript
interface ThemeAPI {
// テーマ管理
getThemes(): ThemeInfo[];
getCurrentTheme(): ThemeInfo | undefined;
applyTheme(themeId: string): Promise<boolean>;
// カスタムテーマ
createCustomTheme(options: ThemeCreationOptions): Promise<string>;
updateCustomTheme(themeId: string, updates: Partial<ThemeCreationOptions>): Promise<boolean>;
deleteCustomTheme(themeId: string): Promise<boolean>;
// インポート/エクスポート
exportTheme(themeId: string): Promise<string | undefined>;
importTheme(filePath?: string): Promise<string | undefined>;
// イベント
onThemeChange(listener: (theme: ThemeInfo) => void): Disposable;
// アイコンテーマ
getIconThemes(): IconThemeInfo[];
getCurrentIconTheme(): IconThemeInfo | undefined;
applyIconTheme(themeId: string): Promise<boolean>;
onIconThemeChange(listener: (theme: IconThemeInfo) => void): Disposable;
}ベストプラクティス
- テーマ構造: 標準的なテーマファイル構造と命名規則に従う
- カラーアクセシビリティ: アクセシビリティのために十分なコントラスト比を確保する
- パフォーマンス: より良いパフォーマンスのためにテーマの読み込みと適用を最適化する
- 一貫性: すべてのUI要素で一貫したカラースキームを維持する
- ユーザーエクスペリエンス: スムーズなテーマ切り替えとプレビュー機能を提供する
- ドキュメント: カスタムテーマのプロパティと使用方法を文書化する
- テスト: 異なるファイルタイプとUI状態でテーマをテストする
- 互換性: 異なるプラットフォームと設定でテーマが動作することを確認する
関連API
- UI API - UIテーマとスタイリング用
- エディター API - エディター固有のテーマ用
- ワークスペース API - ワークスペース固有のテーマ設定用
- 拡張機能 API - テーマ拡張機能開発用