Skip to content

テーマ 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;
}

ベストプラクティス

  1. テーマ構造: 標準的なテーマファイル構造と命名規則に従う
  2. カラーアクセシビリティ: アクセシビリティのために十分なコントラスト比を確保する
  3. パフォーマンス: より良いパフォーマンスのためにテーマの読み込みと適用を最適化する
  4. 一貫性: すべてのUI要素で一貫したカラースキームを維持する
  5. ユーザーエクスペリエンス: スムーズなテーマ切り替えとプレビュー機能を提供する
  6. ドキュメント: カスタムテーマのプロパティと使用方法を文書化する
  7. テスト: 異なるファイルタイプとUI状態でテーマをテストする
  8. 互換性: 異なるプラットフォームと設定でテーマが動作することを確認する

関連API

究極の AI 駆動 IDE 学習ガイド