aboutsummaryrefslogtreecommitdiff
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 <[email protected]>2019-12-29 16:49:40 +0000
commit232785251bc80bc32c2ab52b624ecffbf5e35185 (patch)
tree6f8005b895d4005a9c6997d65f6994260bbbca12 /editors/code/src
parent523b4cbc602447b14202dd2520f84241bb07c4e2 (diff)
parent25537d294cb7a3e01d2329a7d07b469d734fc829 (diff)
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 <[email protected]>
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 4b388b80c..a88be6e35 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,5 +1,6 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2 2import * as scopes from './scopes';
3import * as scopesMapper from './scopes_mapper';
3import { Server } from './server'; 4import { Server } from './server';
4 5
5const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; 6const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
@@ -54,10 +55,17 @@ export class Config {
54 55
55 public userConfigChanged() { 56 public userConfigChanged() {
56 const config = vscode.workspace.getConfiguration('rust-analyzer'); 57 const config = vscode.workspace.getConfiguration('rust-analyzer');
58
59 Server.highlighter.removeHighlights();
60
57 let requireReloadMessage = null; 61 let requireReloadMessage = null;
58 62
59 if (config.has('highlightingOn')) { 63 if (config.has('highlightingOn')) {
60 this.highlightingOn = config.get('highlightingOn') as boolean; 64 this.highlightingOn = config.get('highlightingOn') as boolean;
65 if (this.highlightingOn) {
66 scopes.load();
67 scopesMapper.load();
68 }
61 } 69 }
62 70
63 if (config.has('rainbowHighlightingOn')) { 71 if (config.has('rainbowHighlightingOn')) {
@@ -66,10 +74,6 @@ export class Config {
66 ) as boolean; 74 ) as boolean;
67 } 75 }
68 76
69 if (!this.highlightingOn && Server) {
70 Server.highlighter.removeHighlights();
71 }
72
73 if (config.has('enableEnhancedTyping')) { 77 if (config.has('enableEnhancedTyping')) {
74 this.enableEnhancedTyping = config.get( 78 this.enableEnhancedTyping = config.get(
75 'enableEnhancedTyping', 79 'enableEnhancedTyping',
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts
index e1b0d13e7..4e224a54c 100644
--- a/editors/code/src/highlighting.ts
+++ b/editors/code/src/highlighting.ts
@@ -1,6 +1,8 @@
1import seedrandom = require('seedrandom'); 1import seedrandom = require('seedrandom');
2import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3import * as lc from 'vscode-languageclient'; 3import * as lc from 'vscode-languageclient';
4import * as scopes from './scopes';
5import * as scopesMapper from './scopes_mapper';
4 6
5import { Server } from './server'; 7import { Server } from './server';
6 8
@@ -23,6 +25,41 @@ function fancify(seed: string, shade: 'light' | 'dark') {
23 return `hsl(${h},${s}%,${l}%)`; 25 return `hsl(${h},${s}%,${l}%)`;
24} 26}
25 27
28function createDecorationFromTextmate(
29 themeStyle: scopes.TextMateRuleSettings,
30): vscode.TextEditorDecorationType {
31 const decorationOptions: vscode.DecorationRenderOptions = {};
32 decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen;
33
34 if (themeStyle.foreground) {
35 decorationOptions.color = themeStyle.foreground;
36 }
37
38 if (themeStyle.background) {
39 decorationOptions.backgroundColor = themeStyle.background;
40 }
41
42 if (themeStyle.fontStyle) {
43 const parts: string[] = themeStyle.fontStyle.split(' ');
44 parts.forEach(part => {
45 switch (part) {
46 case 'italic':
47 decorationOptions.fontStyle = 'italic';
48 break;
49 case 'bold':
50 decorationOptions.fontWeight = 'bold';
51 break;
52 case 'underline':
53 decorationOptions.textDecoration = 'underline';
54 break;
55 default:
56 break;
57 }
58 });
59 }
60 return vscode.window.createTextEditorDecorationType(decorationOptions);
61}
62
26export class Highlighter { 63export class Highlighter {
27 private static initDecorations(): Map< 64 private static initDecorations(): Map<
28 string, 65 string,
@@ -32,12 +69,25 @@ export class Highlighter {
32 tag: string, 69 tag: string,
33 textDecoration?: string, 70 textDecoration?: string,
34 ): [string, vscode.TextEditorDecorationType] => { 71 ): [string, vscode.TextEditorDecorationType] => {
35 const color = new vscode.ThemeColor('ralsp.' + tag); 72 const rule = scopesMapper.toRule(tag, scopes.find);
36 const decor = vscode.window.createTextEditorDecorationType({ 73
37 color, 74 if (rule) {
38 textDecoration, 75 const decor = createDecorationFromTextmate(rule);
39 }); 76 return [tag, decor];
40 return [tag, decor]; 77 } else {
78 const fallBackTag = 'ralsp.' + tag;
79 // console.log(' ');
80 // console.log('Missing theme for: <"' + tag + '"> for following mapped scopes:');
81 // console.log(scopesMapper.find(tag));
82 // console.log('Falling back to values defined in: ' + fallBackTag);
83 // console.log(' ');
84 const color = new vscode.ThemeColor(fallBackTag);
85 const decor = vscode.window.createTextEditorDecorationType({
86 color,
87 textDecoration,
88 });
89 return [tag, decor];
90 }
41 }; 91 };
42 92
43 const decorations: Iterable<[ 93 const decorations: Iterable<[
diff --git a/editors/code/src/scopes.ts b/editors/code/src/scopes.ts
new file mode 100644
index 000000000..cb250b853
--- /dev/null
+++ b/editors/code/src/scopes.ts
@@ -0,0 +1,146 @@
1import * as fs from 'fs';
2import * as jsonc from 'jsonc-parser';
3import * as path from 'path';
4import * as vscode from 'vscode';
5
6export interface TextMateRule {
7 scope: string | string[];
8 settings: TextMateRuleSettings;
9}
10
11export interface TextMateRuleSettings {
12 foreground: string | undefined;
13 background: string | undefined;
14 fontStyle: string | undefined;
15}
16
17// Current theme colors
18const rules = new Map<string, TextMateRuleSettings>();
19
20export function find(scope: string): TextMateRuleSettings | undefined {
21 return rules.get(scope);
22}
23
24// Load all textmate scopes in the currently active theme
25export function load() {
26 // Remove any previous theme
27 rules.clear();
28 // Find out current color theme
29 const themeName = vscode.workspace
30 .getConfiguration('workbench')
31 .get('colorTheme');
32
33 if (typeof themeName !== 'string') {
34 // console.warn('workbench.colorTheme is', themeName)
35 return;
36 }
37 // Try to load colors from that theme
38 try {
39 loadThemeNamed(themeName);
40 } catch (e) {
41 // console.warn('failed to load theme', themeName, e)
42 }
43}
44
45function filterThemeExtensions(extension: vscode.Extension<any>): boolean {
46 return (
47 extension.extensionKind === vscode.ExtensionKind.UI &&
48 extension.packageJSON.contributes &&
49 extension.packageJSON.contributes.themes
50 );
51}
52
53// Find current theme on disk
54function loadThemeNamed(themeName: string) {
55 const themePaths = vscode.extensions.all
56 .filter(filterThemeExtensions)
57 .reduce((list, extension) => {
58 return extension.packageJSON.contributes.themes
59 .filter(
60 (element: any) =>
61 (element.id || element.label) === themeName,
62 )
63 .map((element: any) =>
64 path.join(extension.extensionPath, element.path),
65 )
66 .concat(list);
67 }, Array<string>());
68
69 themePaths.forEach(loadThemeFile);
70
71 const tokenColorCustomizations: [any] = [
72 vscode.workspace
73 .getConfiguration('editor')
74 .get('tokenColorCustomizations'),
75 ];
76
77 tokenColorCustomizations
78 .filter(custom => custom && custom.textMateRules)
79 .map(custom => custom.textMateRules)
80 .forEach(loadColors);
81}
82
83function loadThemeFile(themePath: string) {
84 const themeContent = [themePath]
85 .filter(isFile)
86 .map(readFileText)
87 .map(parseJSON)
88 .filter(theme => theme);
89
90 themeContent
91 .filter(theme => theme.tokenColors)
92 .map(theme => theme.tokenColors)
93 .forEach(loadColors);
94
95 themeContent
96 .filter(theme => theme.include)
97 .map(theme => path.join(path.dirname(themePath), theme.include))
98 .forEach(loadThemeFile);
99}
100
101function mergeRuleSettings(
102 defaultSetting: TextMateRuleSettings | undefined,
103 override: TextMateRuleSettings,
104): TextMateRuleSettings {
105 if (defaultSetting === undefined) {
106 return override;
107 }
108 const mergedRule = defaultSetting;
109
110 mergedRule.background = override.background || defaultSetting.background;
111 mergedRule.foreground = override.foreground || defaultSetting.foreground;
112 mergedRule.fontStyle = override.fontStyle || defaultSetting.foreground;
113
114 return mergedRule;
115}
116
117function updateRules(
118 scope: string,
119 updatedSettings: TextMateRuleSettings,
120): void {
121 [rules.get(scope)]
122 .map(settings => mergeRuleSettings(settings, updatedSettings))
123 .forEach(settings => rules.set(scope, settings));
124}
125
126function loadColors(textMateRules: TextMateRule[]): void {
127 textMateRules.forEach(rule => {
128 if (typeof rule.scope === 'string') {
129 updateRules(rule.scope, rule.settings);
130 } else if (rule.scope instanceof Array) {
131 rule.scope.forEach(scope => updateRules(scope, rule.settings));
132 }
133 });
134}
135
136function isFile(filePath: string): boolean {
137 return [filePath].map(fs.statSync).every(stat => stat.isFile());
138}
139
140function readFileText(filePath: string): string {
141 return fs.readFileSync(filePath, 'utf8');
142}
143
144function parseJSON(content: string): any {
145 return jsonc.parse(content);
146}
diff --git a/editors/code/src/scopes_mapper.ts b/editors/code/src/scopes_mapper.ts
new file mode 100644
index 000000000..e738fa239
--- /dev/null
+++ b/editors/code/src/scopes_mapper.ts
@@ -0,0 +1,78 @@
1import * as vscode from 'vscode';
2import { TextMateRuleSettings } from './scopes';
3
4let mappings = new Map<string, string[]>();
5
6const defaultMapping = new Map<string, string[]>([
7 [
8 'comment',
9 [
10 'comment',
11 'comment.block',
12 'comment.line',
13 'comment.block.documentation',
14 ],
15 ],
16 ['string', ['string']],
17 ['keyword', ['keyword']],
18 ['keyword.control', ['keyword.control', 'keyword', 'keyword.other']],
19 [
20 'keyword.unsafe',
21 ['storage.modifier', 'keyword.other', 'keyword.control', 'keyword'],
22 ],
23 ['function', ['entity.name.function']],
24 ['parameter', ['variable.parameter']],
25 ['constant', ['constant', 'variable']],
26 ['type', ['entity.name.type']],
27 ['builtin', ['variable.language', 'support.type', 'support.type']],
28 ['text', ['string', 'string.quoted', 'string.regexp']],
29 ['attribute', ['keyword']],
30 ['literal', ['string', 'string.quoted', 'string.regexp']],
31 ['macro', ['entity.name.function', 'keyword.other', 'entity.name.macro']],
32 ['variable', ['variable']],
33 ['variable.mut', ['variable', 'storage.modifier']],
34 [
35 'field',
36 [
37 'variable.object.property',
38 'meta.field.declaration',
39 'meta.definition.property',
40 'variable.other',
41 ],
42 ],
43 ['module', ['entity.name.section', 'entity.other']],
44]);
45
46export function find(scope: string): string[] {
47 return mappings.get(scope) || [];
48}
49
50export function toRule(
51 scope: string,
52 intoRule: (scope: string) => TextMateRuleSettings | undefined,
53): TextMateRuleSettings | undefined {
54 return find(scope)
55 .map(intoRule)
56 .filter(rule => rule !== undefined)[0];
57}
58
59function isString(value: any): value is string {
60 return typeof value === 'string';
61}
62
63function isArrayOfString(value: any): value is string[] {
64 return Array.isArray(value) && value.every(item => isString(item));
65}
66
67export function load() {
68 const rawConfig: { [key: string]: any } =
69 vscode.workspace
70 .getConfiguration('rust-analyzer')
71 .get('scopeMappings') || {};
72
73 mappings = Object.entries(rawConfig)
74 .filter(([_, value]) => isString(value) || isArrayOfString(value))
75 .reduce((list, [key, value]: [string, string | string[]]) => {
76 return list.set(key, isString(value) ? [value] : value);
77 }, defaultMapping);
78}