about summary refs log tree commit diff
path: root/editors/code/src
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2019-12-29 16:49:40 +0000
committerGitHub <noreply@github.com>2019-12-29 16:49:40 +0000
commit232785251bc80bc32c2ab52b624ecffbf5e35185 (patch)
tree6f8005b895d4005a9c6997d65f6994260bbbca12 /editors/code/src
parent523b4cbc602447b14202dd2520f84241bb07c4e2 (diff)
parent25537d294cb7a3e01d2329a7d07b469d734fc829 (diff)
downloadrust-232785251bc80bc32c2ab52b624ecffbf5e35185.tar.gz
rust-232785251bc80bc32c2ab52b624ecffbf5e35185.zip
Merge #2061
2061: Theme loading and "editor.tokenColorCustomizations" support. r=matklad a=seivan

Fixes: [Issue#1294](https://github.com/rust-analyzer/rust-analyzer/issues/1294#issuecomment-497450325)

TODO: 
- [x] Load themes
- [x] Load existing `ralsp`-prefixed overrides from `"workbench.colorCustomizations"`.
- [x] Load overrides from `"editor.tokenColorCustomizations.textMateRules"`.
- [x] Use RA tags to load `vscode.DecorationRenderOptions` (colors) from theme & overrides.
- [x] Map RA tags to common TextMate scopes before loading colors.
- [x] Add default scope mappings in extension.
- [x] Cache mappings between settings updates. 
- [x] Add scope mapping configuration manifest in `package.json`
- [x] Load configurable scope mappings from settings.
- [x] Load JSON Scheme for text mate scope rules in settings.
- [x] Update [Readme](https://github.com/seivan/rust-analyzer/blob/feature/themes/docs/user/README.md#settings).

Borrowed the theme loading (`scopes.ts`) from `Tree Sitter` with some modifications to reading `"editor.tokenColorCustomizations"` for merging with loaded themes and had to remove the async portions to be able to load it from settings updates. 

~Just a PoC and an idea I toyed around with a lot of room for improvement.~
For starters, certain keywords aren't part of the standard TextMate grammar, so it still reads colors from the `ralsp` prefixed values in `"workbench.colorCustomizations"`. 

But I think there's more value making the extension work with existing themes by maping some of the decoration tags to existing key or keys. 

<img width="453" alt="Screenshot 2019-11-09 at 17 43 18" src="https://user-images.githubusercontent.com/55424/68531968-71b4e380-0318-11ea-924e-cdbb8d5eae06.png">
<img width="780" alt="Screenshot 2019-11-09 at 17 41 45" src="https://user-images.githubusercontent.com/55424/68531950-4b8f4380-0318-11ea-8f85-24a84efaf23b.png">
<img width="468" alt="Screenshot 2019-11-09 at 17 40 29" src="https://user-images.githubusercontent.com/55424/68531952-51852480-0318-11ea-800a-6ae9215f5368.png">


These will merge with the default ones coming with the extension, so you don't have to implement all of them and works well with overrides defined in settings. 

```jsonc
    "editor.tokenColorCustomizations": {
        "textMateRules": [
            {
                "scope": "keyword",
                "settings": {
                    "fontStyle": "bold",
                }
            },
        ]
    },
```


Edit: The idea is to work with 90% of the themes out there by working within existing scopes available that are generally styled. It's not to say I want to erase the custom Rust scopes - those should still remain and eventually worked into a custom grammar bundle for Rust specific themes that target those, I just want to make it work with generic themes offered on the market place for now. 

A custom grammar bundle and themes for Rust specific scopes is out of... scope for this PR. 
We'll make another round to tackle those issues. 


Current fallbacks implemented

```typescript
    [
        'comment',
        [
            'comment',
            'comment.block',
            'comment.line',
            'comment.block.documentation'
        ]
    ],
    ['string', ['string']],
    ['keyword', ['keyword']],
    ['keyword.control', ['keyword.control', 'keyword', 'keyword.other']],
    [
        'keyword.unsafe',
        ['storage.modifier', 'keyword.other', 'keyword.control', 'keyword']
    ],
    ['function', ['entity.name.function']],
    ['parameter', ['variable.parameter']],
    ['constant', ['constant', 'variable']],
    ['type', ['entity.name.type']],
    ['builtin', ['variable.language', 'support.type', 'support.type']],
    ['text', ['string', 'string.quoted', 'string.regexp']],
    ['attribute', ['keyword']],
    ['literal', ['string', 'string.quoted', 'string.regexp']],
    ['macro', ['support.other']],
    ['variable', ['variable']],
    ['variable.mut', ['variable', 'storage.modifier']],
    [
        'field',
        [
            'variable.object.property',
            'meta.field.declaration',
            'meta.definition.property',
            'variable.other'
        ]
    ],
    ['module', ['entity.name.section', 'entity.other']]
```


Co-authored-by: Seivan Heidari <seivan.heidari@icloud.com>
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/config.ts14
-rw-r--r--editors/code/src/highlighting.ts62
-rw-r--r--editors/code/src/scopes.ts146
-rw-r--r--editors/code/src/scopes_mapper.ts78
4 files changed, 289 insertions, 11 deletions
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 4b388b80c54..a88be6e3509 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,5 +1,6 @@
 import * as vscode from 'vscode';
-
+import * as scopes from './scopes';
+import * as scopesMapper from './scopes_mapper';
 import { Server } from './server';
 
 const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
@@ -54,10 +55,17 @@ export class Config {
 
     public userConfigChanged() {
         const config = vscode.workspace.getConfiguration('rust-analyzer');
+
+        Server.highlighter.removeHighlights();
+
         let requireReloadMessage = null;
 
         if (config.has('highlightingOn')) {
             this.highlightingOn = config.get('highlightingOn') as boolean;
+            if (this.highlightingOn) {
+                scopes.load();
+                scopesMapper.load();
+            }
         }
 
         if (config.has('rainbowHighlightingOn')) {
@@ -66,10 +74,6 @@ export class Config {
             ) as boolean;
         }
 
-        if (!this.highlightingOn && Server) {
-            Server.highlighter.removeHighlights();
-        }
-
         if (config.has('enableEnhancedTyping')) {
             this.enableEnhancedTyping = config.get(
                 'enableEnhancedTyping',
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts
index e1b0d13e701..4e224a54c11 100644
--- a/editors/code/src/highlighting.ts
+++ b/editors/code/src/highlighting.ts
@@ -1,6 +1,8 @@
 import seedrandom = require('seedrandom');
 import * as vscode from 'vscode';
 import * as lc from 'vscode-languageclient';
+import * as scopes from './scopes';
+import * as scopesMapper from './scopes_mapper';
 
 import { Server } from './server';
 
@@ -23,6 +25,41 @@ function fancify(seed: string, shade: 'light' | 'dark') {
     return `hsl(${h},${s}%,${l}%)`;
 }
 
+function createDecorationFromTextmate(
+    themeStyle: scopes.TextMateRuleSettings,
+): vscode.TextEditorDecorationType {
+    const decorationOptions: vscode.DecorationRenderOptions = {};
+    decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen;
+
+    if (themeStyle.foreground) {
+        decorationOptions.color = themeStyle.foreground;
+    }
+
+    if (themeStyle.background) {
+        decorationOptions.backgroundColor = themeStyle.background;
+    }
+
+    if (themeStyle.fontStyle) {
+        const parts: string[] = themeStyle.fontStyle.split(' ');
+        parts.forEach(part => {
+            switch (part) {
+                case 'italic':
+                    decorationOptions.fontStyle = 'italic';
+                    break;
+                case 'bold':
+                    decorationOptions.fontWeight = 'bold';
+                    break;
+                case 'underline':
+                    decorationOptions.textDecoration = 'underline';
+                    break;
+                default:
+                    break;
+            }
+        });
+    }
+    return vscode.window.createTextEditorDecorationType(decorationOptions);
+}
+
 export class Highlighter {
     private static initDecorations(): Map<
         string,
@@ -32,12 +69,25 @@ export class Highlighter {
             tag: string,
             textDecoration?: string,
         ): [string, vscode.TextEditorDecorationType] => {
-            const color = new vscode.ThemeColor('ralsp.' + tag);
-            const decor = vscode.window.createTextEditorDecorationType({
-                color,
-                textDecoration,
-            });
-            return [tag, decor];
+            const rule = scopesMapper.toRule(tag, scopes.find);
+
+            if (rule) {
+                const decor = createDecorationFromTextmate(rule);
+                return [tag, decor];
+            } else {
+                const fallBackTag = 'ralsp.' + tag;
+                // console.log(' ');
+                // console.log('Missing theme for: <"' + tag + '"> for following mapped scopes:');
+                // console.log(scopesMapper.find(tag));
+                // console.log('Falling back to values defined in: ' + fallBackTag);
+                // console.log(' ');
+                const color = new vscode.ThemeColor(fallBackTag);
+                const decor = vscode.window.createTextEditorDecorationType({
+                    color,
+                    textDecoration,
+                });
+                return [tag, decor];
+            }
         };
 
         const decorations: Iterable<[
diff --git a/editors/code/src/scopes.ts b/editors/code/src/scopes.ts
new file mode 100644
index 00000000000..cb250b853ad
--- /dev/null
+++ b/editors/code/src/scopes.ts
@@ -0,0 +1,146 @@
+import * as fs from 'fs';
+import * as jsonc from 'jsonc-parser';
+import * as path from 'path';
+import * as vscode from 'vscode';
+
+export interface TextMateRule {
+    scope: string | string[];
+    settings: TextMateRuleSettings;
+}
+
+export interface TextMateRuleSettings {
+    foreground: string | undefined;
+    background: string | undefined;
+    fontStyle: string | undefined;
+}
+
+// Current theme colors
+const rules = new Map<string, TextMateRuleSettings>();
+
+export function find(scope: string): TextMateRuleSettings | undefined {
+    return rules.get(scope);
+}
+
+// Load all textmate scopes in the currently active theme
+export function load() {
+    // Remove any previous theme
+    rules.clear();
+    // Find out current color theme
+    const themeName = vscode.workspace
+        .getConfiguration('workbench')
+        .get('colorTheme');
+
+    if (typeof themeName !== 'string') {
+        // console.warn('workbench.colorTheme is', themeName)
+        return;
+    }
+    // Try to load colors from that theme
+    try {
+        loadThemeNamed(themeName);
+    } catch (e) {
+        // console.warn('failed to load theme', themeName, e)
+    }
+}
+
+function filterThemeExtensions(extension: vscode.Extension<any>): boolean {
+    return (
+        extension.extensionKind === vscode.ExtensionKind.UI &&
+        extension.packageJSON.contributes &&
+        extension.packageJSON.contributes.themes
+    );
+}
+
+// Find current theme on disk
+function loadThemeNamed(themeName: string) {
+    const themePaths = vscode.extensions.all
+        .filter(filterThemeExtensions)
+        .reduce((list, extension) => {
+            return extension.packageJSON.contributes.themes
+                .filter(
+                    (element: any) =>
+                        (element.id || element.label) === themeName,
+                )
+                .map((element: any) =>
+                    path.join(extension.extensionPath, element.path),
+                )
+                .concat(list);
+        }, Array<string>());
+
+    themePaths.forEach(loadThemeFile);
+
+    const tokenColorCustomizations: [any] = [
+        vscode.workspace
+            .getConfiguration('editor')
+            .get('tokenColorCustomizations'),
+    ];
+
+    tokenColorCustomizations
+        .filter(custom => custom && custom.textMateRules)
+        .map(custom => custom.textMateRules)
+        .forEach(loadColors);
+}
+
+function loadThemeFile(themePath: string) {
+    const themeContent = [themePath]
+        .filter(isFile)
+        .map(readFileText)
+        .map(parseJSON)
+        .filter(theme => theme);
+
+    themeContent
+        .filter(theme => theme.tokenColors)
+        .map(theme => theme.tokenColors)
+        .forEach(loadColors);
+
+    themeContent
+        .filter(theme => theme.include)
+        .map(theme => path.join(path.dirname(themePath), theme.include))
+        .forEach(loadThemeFile);
+}
+
+function mergeRuleSettings(
+    defaultSetting: TextMateRuleSettings | undefined,
+    override: TextMateRuleSettings,
+): TextMateRuleSettings {
+    if (defaultSetting === undefined) {
+        return override;
+    }
+    const mergedRule = defaultSetting;
+
+    mergedRule.background = override.background || defaultSetting.background;
+    mergedRule.foreground = override.foreground || defaultSetting.foreground;
+    mergedRule.fontStyle = override.fontStyle || defaultSetting.foreground;
+
+    return mergedRule;
+}
+
+function updateRules(
+    scope: string,
+    updatedSettings: TextMateRuleSettings,
+): void {
+    [rules.get(scope)]
+        .map(settings => mergeRuleSettings(settings, updatedSettings))
+        .forEach(settings => rules.set(scope, settings));
+}
+
+function loadColors(textMateRules: TextMateRule[]): void {
+    textMateRules.forEach(rule => {
+        if (typeof rule.scope === 'string') {
+            updateRules(rule.scope, rule.settings);
+        } else if (rule.scope instanceof Array) {
+            rule.scope.forEach(scope => updateRules(scope, rule.settings));
+        }
+    });
+}
+
+function isFile(filePath: string): boolean {
+    return [filePath].map(fs.statSync).every(stat => stat.isFile());
+}
+
+function readFileText(filePath: string): string {
+    return fs.readFileSync(filePath, 'utf8');
+}
+
+function parseJSON(content: string): any {
+    return jsonc.parse(content);
+}
diff --git a/editors/code/src/scopes_mapper.ts b/editors/code/src/scopes_mapper.ts
new file mode 100644
index 00000000000..e738fa2396d
--- /dev/null
+++ b/editors/code/src/scopes_mapper.ts
@@ -0,0 +1,78 @@
+import * as vscode from 'vscode';
+import { TextMateRuleSettings } from './scopes';
+
+let mappings = new Map<string, string[]>();
+
+const defaultMapping = new Map<string, string[]>([
+    [
+        'comment',
+        [
+            'comment',
+            'comment.block',
+            'comment.line',
+            'comment.block.documentation',
+        ],
+    ],
+    ['string', ['string']],
+    ['keyword', ['keyword']],
+    ['keyword.control', ['keyword.control', 'keyword', 'keyword.other']],
+    [
+        'keyword.unsafe',
+        ['storage.modifier', 'keyword.other', 'keyword.control', 'keyword'],
+    ],
+    ['function', ['entity.name.function']],
+    ['parameter', ['variable.parameter']],
+    ['constant', ['constant', 'variable']],
+    ['type', ['entity.name.type']],
+    ['builtin', ['variable.language', 'support.type', 'support.type']],
+    ['text', ['string', 'string.quoted', 'string.regexp']],
+    ['attribute', ['keyword']],
+    ['literal', ['string', 'string.quoted', 'string.regexp']],
+    ['macro', ['entity.name.function', 'keyword.other', 'entity.name.macro']],
+    ['variable', ['variable']],
+    ['variable.mut', ['variable', 'storage.modifier']],
+    [
+        'field',
+        [
+            'variable.object.property',
+            'meta.field.declaration',
+            'meta.definition.property',
+            'variable.other',
+        ],
+    ],
+    ['module', ['entity.name.section', 'entity.other']],
+]);
+
+export function find(scope: string): string[] {
+    return mappings.get(scope) || [];
+}
+
+export function toRule(
+    scope: string,
+    intoRule: (scope: string) => TextMateRuleSettings | undefined,
+): TextMateRuleSettings | undefined {
+    return find(scope)
+        .map(intoRule)
+        .filter(rule => rule !== undefined)[0];
+}
+
+function isString(value: any): value is string {
+    return typeof value === 'string';
+}
+
+function isArrayOfString(value: any): value is string[] {
+    return Array.isArray(value) && value.every(item => isString(item));
+}
+
+export function load() {
+    const rawConfig: { [key: string]: any } =
+        vscode.workspace
+            .getConfiguration('rust-analyzer')
+            .get('scopeMappings') || {};
+
+    mappings = Object.entries(rawConfig)
+        .filter(([_, value]) => isString(value) || isArrayOfString(value))
+        .reduce((list, [key, value]: [string, string | string[]]) => {
+            return list.set(key, isString(value) ? [value] : value);
+        }, defaultMapping);
+}