]> git.lizzy.rs Git - rust.git/blob - editors/code/src/highlighting.ts
Rename file
[rust.git] / editors / code / src / highlighting.ts
1 import * as vscode from 'vscode';
2 import * as lc from 'vscode-languageclient';
3 import * as seedrandom_ from 'seedrandom';
4 const seedrandom = seedrandom_; // https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207
5
6 import { loadThemeColors, TextMateRuleSettings } from './load_theme_colors';
7 import * as scopesMapper from './scopes_mapper';
8
9 import { Ctx } from './ctx';
10
11 export function activateHighlighting(ctx: Ctx) {
12     const highlighter = new Highlighter(ctx);
13
14     ctx.client.onReady().then(() => {
15         ctx.client.onNotification(
16             'rust-analyzer/publishDecorations',
17             (params: PublishDecorationsParams) => {
18                 if (!ctx.config.highlightingOn) return;
19
20                 const targetEditor = vscode.window.visibleTextEditors.find(
21                     editor => {
22                         const unescapedUri = unescape(
23                             editor.document.uri.toString(),
24                         );
25                         // Unescaped URI looks like:
26                         // file:///c:/Workspace/ra-test/src/main.rs
27                         return unescapedUri === params.uri;
28                     },
29                 );
30                 if (!targetEditor) return;
31
32                 highlighter.setHighlights(targetEditor, params.decorations);
33             },
34         );
35     });
36
37     vscode.workspace.onDidChangeConfiguration(
38         _ => highlighter.removeHighlights(),
39         ctx.subscriptions,
40     );
41
42     vscode.window.onDidChangeActiveTextEditor(
43         async (editor: vscode.TextEditor | undefined) => {
44             if (!editor || editor.document.languageId !== 'rust') return;
45             if (!ctx.config.highlightingOn) return;
46
47             const params: lc.TextDocumentIdentifier = {
48                 uri: editor.document.uri.toString(),
49             };
50             const decorations = await ctx.sendRequestWithRetry<Decoration[]>(
51                 'rust-analyzer/decorationsRequest',
52                 params,
53             );
54             highlighter.setHighlights(editor, decorations);
55         },
56         ctx.subscriptions,
57     );
58 }
59
60 interface PublishDecorationsParams {
61     uri: string;
62     decorations: Decoration[];
63 }
64
65 interface Decoration {
66     range: lc.Range;
67     tag: string;
68     bindingHash?: string;
69 }
70
71 // Based on this HSL-based color generator: https://gist.github.com/bendc/76c48ce53299e6078a76
72 function fancify(seed: string, shade: 'light' | 'dark') {
73     const random = seedrandom(seed);
74     const randomInt = (min: number, max: number) => {
75         return Math.floor(random() * (max - min + 1)) + min;
76     };
77
78     const h = randomInt(0, 360);
79     const s = randomInt(42, 98);
80     const l = shade === 'light' ? randomInt(15, 40) : randomInt(40, 90);
81     return `hsl(${h},${s}%,${l}%)`;
82 }
83
84 class Highlighter {
85     private ctx: Ctx;
86     private decorations: Map<
87         string,
88         vscode.TextEditorDecorationType
89     > | null = null;
90
91     constructor(ctx: Ctx) {
92         this.ctx = ctx;
93     }
94
95     public removeHighlights() {
96         if (this.decorations == null) {
97             return;
98         }
99
100         // Decorations are removed when the object is disposed
101         for (const decoration of this.decorations.values()) {
102             decoration.dispose();
103         }
104
105         this.decorations = null;
106     }
107
108     public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) {
109         // Initialize decorations if necessary
110         //
111         // Note: decoration objects need to be kept around so we can dispose them
112         // if the user disables syntax highlighting
113         if (this.decorations == null) {
114             this.decorations = initDecorations();
115         }
116
117         const byTag: Map<string, vscode.Range[]> = new Map();
118         const colorfulIdents: Map<
119             string,
120             [vscode.Range[], boolean]
121         > = new Map();
122         const rainbowTime = this.ctx.config.rainbowHighlightingOn;
123
124         for (const tag of this.decorations.keys()) {
125             byTag.set(tag, []);
126         }
127
128         for (const d of highlights) {
129             if (!byTag.get(d.tag)) {
130                 continue;
131             }
132
133             if (rainbowTime && d.bindingHash) {
134                 if (!colorfulIdents.has(d.bindingHash)) {
135                     const mut = d.tag.endsWith('.mut');
136                     colorfulIdents.set(d.bindingHash, [[], mut]);
137                 }
138                 colorfulIdents
139                     .get(d.bindingHash)![0]
140                     .push(
141                         this.ctx.client.protocol2CodeConverter.asRange(d.range),
142                     );
143             } else {
144                 byTag
145                     .get(d.tag)!
146                     .push(
147                         this.ctx.client.protocol2CodeConverter.asRange(d.range),
148                     );
149             }
150         }
151
152         for (const tag of byTag.keys()) {
153             const dec = this.decorations.get(
154                 tag,
155             ) as vscode.TextEditorDecorationType;
156             const ranges = byTag.get(tag)!;
157             editor.setDecorations(dec, ranges);
158         }
159
160         for (const [hash, [ranges, mut]] of colorfulIdents.entries()) {
161             const textDecoration = mut ? 'underline' : undefined;
162             const dec = vscode.window.createTextEditorDecorationType({
163                 light: { color: fancify(hash, 'light'), textDecoration },
164                 dark: { color: fancify(hash, 'dark'), textDecoration },
165             });
166             editor.setDecorations(dec, ranges);
167         }
168     }
169 }
170
171 function initDecorations(): Map<
172     string,
173     vscode.TextEditorDecorationType
174 > {
175     const themeColors = loadThemeColors();
176
177     const decoration = (
178         tag: string,
179         textDecoration?: string,
180     ): [string, vscode.TextEditorDecorationType] => {
181         const rule = scopesMapper.toRule(tag, it => themeColors.get(it));
182
183         if (rule) {
184             const decor = createDecorationFromTextmate(rule);
185             return [tag, decor];
186         } else {
187             const fallBackTag = 'ralsp.' + tag;
188             // console.log(' ');
189             // console.log('Missing theme for: <"' + tag + '"> for following mapped scopes:');
190             // console.log(scopesMapper.find(tag));
191             // console.log('Falling back to values defined in: ' + fallBackTag);
192             // console.log(' ');
193             const color = new vscode.ThemeColor(fallBackTag);
194             const decor = vscode.window.createTextEditorDecorationType({
195                 color,
196                 textDecoration,
197             });
198             return [tag, decor];
199         }
200     };
201
202     const decorations: Iterable<[
203         string,
204         vscode.TextEditorDecorationType,
205     ]> = [
206             decoration('comment'),
207             decoration('string'),
208             decoration('keyword'),
209             decoration('keyword.control'),
210             decoration('keyword.unsafe'),
211             decoration('function'),
212             decoration('parameter'),
213             decoration('constant'),
214             decoration('type.builtin'),
215             decoration('type.generic'),
216             decoration('type.lifetime'),
217             decoration('type.param'),
218             decoration('type.self'),
219             decoration('type'),
220             decoration('text'),
221             decoration('attribute'),
222             decoration('literal'),
223             decoration('literal.numeric'),
224             decoration('literal.char'),
225             decoration('literal.byte'),
226             decoration('macro'),
227             decoration('variable'),
228             decoration('variable.mut', 'underline'),
229             decoration('field'),
230             decoration('module'),
231         ];
232
233     return new Map<string, vscode.TextEditorDecorationType>(decorations);
234 }
235
236 function createDecorationFromTextmate(
237     themeStyle: TextMateRuleSettings,
238 ): vscode.TextEditorDecorationType {
239     const decorationOptions: vscode.DecorationRenderOptions = {};
240     decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen;
241
242     if (themeStyle.foreground) {
243         decorationOptions.color = themeStyle.foreground;
244     }
245
246     if (themeStyle.background) {
247         decorationOptions.backgroundColor = themeStyle.background;
248     }
249
250     if (themeStyle.fontStyle) {
251         const parts: string[] = themeStyle.fontStyle.split(' ');
252         parts.forEach(part => {
253             switch (part) {
254                 case 'italic':
255                     decorationOptions.fontStyle = 'italic';
256                     break;
257                 case 'bold':
258                     decorationOptions.fontWeight = 'bold';
259                     break;
260                 case 'underline':
261                     decorationOptions.textDecoration = 'underline';
262                     break;
263                 default:
264                     break;
265             }
266         });
267     }
268     return vscode.window.createTextEditorDecorationType(decorationOptions);
269 }