mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-13 05:38:46 +00:00
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>
This commit is contained in:
commit
232785251b
7 changed files with 369 additions and 14 deletions
|
@ -87,7 +87,16 @@ host.
|
|||
|
||||
### Settings
|
||||
|
||||
* `rust-analyzer.highlightingOn`: enables experimental syntax highlighting
|
||||
* `rust-analyzer.highlightingOn`: enables experimental syntax highlighting.
|
||||
* `rust-analyzer.scopeMappings` -- a scheme backed JSON object to tweak Rust Analyzer scopes to TextMate scopes.
|
||||
```jsonc
|
||||
{
|
||||
//Will autocomplete keys to available RA scopes.
|
||||
"keyword.unsafe": ["keyword", "keyword.control"],
|
||||
//Values are string | TextMateScope | [string | TextMateScope]
|
||||
"variable.mut": "variable"
|
||||
}
|
||||
```
|
||||
* `rust-analyzer.enableEnhancedTyping`: by default, rust-analyzer intercepts
|
||||
`Enter` key to make it easier to continue comments. Note that it may conflict with VIM emulation plugin.
|
||||
* `rust-analyzer.raLspServerPath`: path to `ra_lsp_server` executable
|
||||
|
@ -106,7 +115,7 @@ host.
|
|||
* `rust-analyzer.trace.cargo-watch`: enables cargo-watch logging
|
||||
* `RUST_SRC_PATH`: environment variable that overwrites the sysroot
|
||||
* `rust-analyzer.featureFlags` -- a JSON object to tweak fine-grained behavior:
|
||||
```js
|
||||
```jsonc
|
||||
{
|
||||
// Show diagnostics produced by rust-analyzer itself.
|
||||
"lsp.diagnostics": true,
|
||||
|
|
5
editors/code/package-lock.json
generated
5
editors/code/package-lock.json
generated
|
@ -750,6 +750,11 @@
|
|||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"jsonc-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-4fLQxW1j/5fWj6p78vAlAafoCKtuBm6ghv+Ij5W2DrDx0qE+ZdEl2c6Ko1mgJNF5ftX1iEWQQ4Ap7+3GlhjkOA=="
|
||||
},
|
||||
"lines-and-columns": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||
|
|
|
@ -34,7 +34,8 @@
|
|||
"dependencies": {
|
||||
"lookpath": "^1.0.4",
|
||||
"seedrandom": "^3.0.5",
|
||||
"vscode-languageclient": "^6.0.0-next.9"
|
||||
"vscode-languageclient": "^6.0.0-next.9",
|
||||
"jsonc-parser": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^7.1.1",
|
||||
|
@ -166,6 +167,68 @@
|
|||
"default": false,
|
||||
"description": "Highlight Rust code (overrides built-in syntax highlighting)"
|
||||
},
|
||||
"rust-analyzer.scopeMappings": {
|
||||
"type": "object",
|
||||
"definitions": {},
|
||||
"properties": {
|
||||
"comment": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"string": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"keyword": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"keyword.control": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"keyword.unsafe": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"function": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"parameter": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"constant": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"builtin": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"text": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"attribute": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"literal": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"macro": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"variable": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"variable.mut": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"field": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
},
|
||||
"module": {
|
||||
"$ref": "vscode://schemas/textmate-colors#/items/properties/scope"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Mapping Rust Analyzer scopes to TextMateRule scopes list."
|
||||
},
|
||||
"rust-analyzer.rainbowHighlightingOn": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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<[
|
||||
|
|
146
editors/code/src/scopes.ts
Normal file
146
editors/code/src/scopes.ts
Normal file
|
@ -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);
|
||||
}
|
78
editors/code/src/scopes_mapper.ts
Normal file
78
editors/code/src/scopes_mapper.ts
Normal file
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue