aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/color_theme.ts
blob: 2f2a39877e91878555ea9a627d510dd6dc7ff27a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import * as fs from 'fs';
import * as jsonc from 'jsonc-parser';
import * as path from 'path';
import * as vscode from 'vscode';

export interface TextMateRuleSettings {
    foreground?: string;
    background?: string;
    fontStyle?: string;
}

export class ColorTheme {
    private rules: Map<string, TextMateRuleSettings> = new Map();

    static load(): ColorTheme {
        // Find out current color theme
        const themeName = vscode.workspace
            .getConfiguration('workbench')
            .get('colorTheme');

        if (typeof themeName !== 'string') {
            // console.warn('workbench.colorTheme is', themeName)
            return new ColorTheme();
        }
        return loadThemeNamed(themeName);
    }

    static fromRules(rules: TextMateRule[]): ColorTheme {
        const res = new ColorTheme();
        for (const rule of rules) {
            const scopes = typeof rule.scope === 'undefined'
                ? []
                : typeof rule.scope === 'string'
                    ? [rule.scope]
                    : rule.scope;

            for (const scope of scopes) {
                res.rules.set(scope, rule.settings);
            }
        }
        return res;
    }

    lookup(scopes: string[]): TextMateRuleSettings {
        let res: TextMateRuleSettings = {};
        for (const scope of scopes) {
            this.rules.forEach((value, key) => {
                if (scope.startsWith(key)) {
                    res = mergeRuleSettings(res, value);
                }
            });
        }
        return res;
    }

    mergeFrom(other: ColorTheme) {
        other.rules.forEach((value, key) => {
            const merged = mergeRuleSettings(this.rules.get(key), value);
            this.rules.set(key, merged);
        });
    }
}

function loadThemeNamed(themeName: string): ColorTheme {
    function isTheme(extension: vscode.Extension<unknown>): boolean {
        return (
            extension.extensionKind === vscode.ExtensionKind.UI &&
            extension.packageJSON.contributes &&
            extension.packageJSON.contributes.themes
        );
    }

    const themePaths: string[] = vscode.extensions.all
        .filter(isTheme)
        .flatMap(
            ext => ext.packageJSON.contributes.themes
                .filter((it: any) => (it.id || it.label) === themeName)
                .map((it: any) => path.join(ext.extensionPath, it.path))
        );

    const res = new ColorTheme();
    for (const themePath of themePaths) {
        res.mergeFrom(loadThemeFile(themePath));
    }

    const global_customizations: any = vscode.workspace.getConfiguration('editor').get('tokenColorCustomizations');
    res.mergeFrom(ColorTheme.fromRules(global_customizations?.textMateRules ?? []));

    const theme_customizations: any = vscode.workspace.getConfiguration('editor.tokenColorCustomizations').get(`[${themeName}]`);
    res.mergeFrom(ColorTheme.fromRules(theme_customizations?.textMateRules ?? []));


    return res;
}

function loadThemeFile(themePath: string): ColorTheme {
    let text;
    try {
        text = fs.readFileSync(themePath, 'utf8');
    } catch {
        return new ColorTheme();
    }
    const obj = jsonc.parse(text);
    const tokenColors: TextMateRule[] = obj?.tokenColors ?? [];
    const res = ColorTheme.fromRules(tokenColors);

    for (const include of obj?.include ?? []) {
        const includePath = path.join(path.dirname(themePath), include);
        res.mergeFrom(loadThemeFile(includePath));
    }

    return res;
}

interface TextMateRule {
    scope: string | string[];
    settings: TextMateRuleSettings;
}

function mergeRuleSettings(
    defaultSetting: TextMateRuleSettings | undefined,
    override: TextMateRuleSettings,
): TextMateRuleSettings {
    return {
        foreground: override.foreground ?? defaultSetting?.foreground,
        background: override.background ?? defaultSetting?.background,
        fontStyle: override.fontStyle ?? defaultSetting?.fontStyle,
    };
}