diff options
| author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-12-29 16:49:40 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-12-29 16:49:40 +0000 |
| commit | 232785251bc80bc32c2ab52b624ecffbf5e35185 (patch) | |
| tree | 6f8005b895d4005a9c6997d65f6994260bbbca12 /editors/code/src | |
| parent | 523b4cbc602447b14202dd2520f84241bb07c4e2 (diff) | |
| parent | 25537d294cb7a3e01d2329a7d07b469d734fc829 (diff) | |
| download | rust-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.ts | 14 | ||||
| -rw-r--r-- | editors/code/src/highlighting.ts | 62 | ||||
| -rw-r--r-- | editors/code/src/scopes.ts | 146 | ||||
| -rw-r--r-- | editors/code/src/scopes_mapper.ts | 78 |
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); +} |
