diff --git a/rust-analyzer/editors/code/package-lock.json b/rust-analyzer/editors/code/package-lock.json index 4c5c1364..67081f3f 100644 --- a/rust-analyzer/editors/code/package-lock.json +++ b/rust-analyzer/editors/code/package-lock.json @@ -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", diff --git a/rust-analyzer/editors/code/package.json b/rust-analyzer/editors/code/package.json index 69298e91..f28ce177 100644 --- a/rust-analyzer/editors/code/package.json +++ b/rust-analyzer/editors/code/package.json @@ -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, diff --git a/rust-analyzer/editors/code/src/config.ts b/rust-analyzer/editors/code/src/config.ts index 4b388b80..a88be6e3 100644 --- a/rust-analyzer/editors/code/src/config.ts +++ b/rust-analyzer/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/rust-analyzer/editors/code/src/highlighting.ts b/rust-analyzer/editors/code/src/highlighting.ts index e1b0d13e..4e224a54 100644 --- a/rust-analyzer/editors/code/src/highlighting.ts +++ b/rust-analyzer/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/rust-analyzer/editors/code/src/scopes.ts b/rust-analyzer/editors/code/src/scopes.ts new file mode 100644 index 00000000..cb250b85 --- /dev/null +++ b/rust-analyzer/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(); + +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): 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()); + + 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/rust-analyzer/editors/code/src/scopes_mapper.ts b/rust-analyzer/editors/code/src/scopes_mapper.ts new file mode 100644 index 00000000..e738fa23 --- /dev/null +++ b/rust-analyzer/editors/code/src/scopes_mapper.ts @@ -0,0 +1,78 @@ +import * as vscode from 'vscode'; +import { TextMateRuleSettings } from './scopes'; + +let mappings = new Map(); + +const defaultMapping = new Map([ + [ + '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); +}